diff --git a/crates/uv-interpreter/src/python_environment.rs b/crates/uv-interpreter/src/python_environment.rs index d79004ffe..63164314a 100644 --- a/crates/uv-interpreter/src/python_environment.rs +++ b/crates/uv-interpreter/src/python_environment.rs @@ -126,6 +126,11 @@ impl PythonEnvironment { ) } } + + /// Return the [`Interpreter`] for this virtual environment. + pub fn into_interpreter(self) -> Interpreter { + self.interpreter + } } /// Locate the current virtual environment. diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index 7793ec409..d238826c9 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -1312,12 +1312,16 @@ pub(crate) struct VenvArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub(crate) struct RunArgs { - /// Command + /// The command to run. pub(crate) command: String, - /// Arguments - #[clap(allow_hyphen_values = true)] + /// The arguments to the command. + #[arg(allow_hyphen_values = true)] pub(crate) args: Vec, + + /// Always use a new virtual environment. + #[arg(long)] + pub(crate) isolated: bool, } #[derive(Args)] diff --git a/crates/uv/src/commands/run.rs b/crates/uv/src/commands/run.rs index 27d77bc78..f01ca5679 100644 --- a/crates/uv/src/commands/run.rs +++ b/crates/uv/src/commands/run.rs @@ -3,6 +3,7 @@ use std::{env, iter}; use anyhow::Result; use owo_colors::OwoColorize; +use tempfile::{tempdir_in, TempDir}; use tracing::debug; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; @@ -13,43 +14,42 @@ use uv_cache::Cache; /// Run a command. #[allow(clippy::unnecessary_wraps, clippy::too_many_arguments)] -pub(crate) async fn run(command: String, args: Vec, cache: &Cache) -> Result { - debug!("Running `{command} {}`", args.join(" ")); - +pub(crate) async fn run( + command: String, + args: Vec, + isolated: bool, + cache: &Cache, +) -> Result { // Detect the current Python interpreter. // TODO(zanieb): Create ephemeral environments // TODO(zanieb): Accept `--python` - let python_env = match PythonEnvironment::from_virtualenv(cache) { - Ok(env) => Some(env), - Err(uv_interpreter::Error::VenvNotFound) => None, - Err(err) => return Err(err.into()), - }; + let run_env = environment_for_run(isolated, cache)?; + let python_env = run_env.python; // Construct the command - let mut process = Command::new(command); - process.args(args); + let mut process = Command::new(&command); + process.args(&args); // Set up the PATH - if let Some(python_env) = python_env { - debug!( - "Using Python {} environment at {}", - python_env.interpreter().python_version(), - python_env.python_executable().user_display().cyan() - ); - let new_path = if let Some(path) = std::env::var_os("PATH") { - let python_env_path = - iter::once(python_env.scripts().to_path_buf()).chain(env::split_paths(&path)); - env::join_paths(python_env_path)? - } else { - OsString::from(python_env.scripts()) - }; - - process.env("PATH", new_path); + debug!( + "Using Python {} environment at {}", + python_env.interpreter().python_version(), + python_env.python_executable().user_display().cyan() + ); + let new_path = if let Some(path) = std::env::var_os("PATH") { + let python_env_path = + iter::once(python_env.scripts().to_path_buf()).chain(env::split_paths(&path)); + env::join_paths(python_env_path)? + } else { + OsString::from(python_env.scripts()) }; + process.env("PATH", new_path); + // Spawn and wait for completion // Standard input, output, and error streams are all inherited // TODO(zanieb): Throw a nicer error message if the command is not found + debug!("Running `{command} {}`", args.join(" ")); let mut handle = process.spawn()?; let status = handle.wait().await?; @@ -61,3 +61,61 @@ pub(crate) async fn run(command: String, args: Vec, cache: &Cache) -> Re Ok(ExitStatus::Failure) } } + +struct RunEnvironment { + /// The Python environment to execute the run in. + python: PythonEnvironment, + /// A temporary directory, if a new virtual environment was created. + /// + /// Included to ensure that the temporary directory exists for the length of the operation, but + /// is dropped at the end as appropriate. + _temp_dir_drop: Option, +} + +/// Returns an environment for a `run` invocation. +/// +/// Will use the current virtual environment (if any) unless `isolated` is true. +/// Will create virtual environments in a temporary directory (if necessary). +fn environment_for_run(isolated: bool, cache: &Cache) -> Result { + if !isolated { + // Return the active environment if it exists + match PythonEnvironment::from_virtualenv(cache) { + Ok(env) => { + return Ok(RunEnvironment { + python: env, + _temp_dir_drop: None, + }) + } + Err(uv_interpreter::Error::VenvNotFound) => {} + Err(err) => return Err(err.into()), + }; + } + + // Find an interpreter to use + // TODO(zanieb): Populate `python` from the user + let python = None; + let python_env = if let Some(python) = python { + PythonEnvironment::from_requested_python(python, cache)? + } else { + PythonEnvironment::from_default_python(cache)? + }; + + // Create a virtual environment directory + // TODO(zanieb): Move this path derivation elsewhere + let uv_state_path = std::env::current_dir()?.join(".uv"); + fs_err::create_dir_all(&uv_state_path)?; + let tmpdir = tempdir_in(uv_state_path)?; + + // Create the environment + // TODO(zanieb): Add dependencies to the env + Ok(RunEnvironment { + python: uv_virtualenv::create_venv( + tmpdir.path(), + python_env.into_interpreter(), + uv_virtualenv::Prompt::None, + false, + Vec::new(), + )?, + _temp_dir_drop: Some(tmpdir), + }) +} diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 41fb7c468..885d00bcf 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -539,7 +539,7 @@ async fn run() -> Result { ) .await } - Commands::Run(args) => commands::run(args.command, args.args, &cache).await, + Commands::Run(args) => commands::run(args.command, args.args, args.isolated, &cache).await, #[cfg(feature = "self-update")] Commands::Self_(SelfNamespace { command: SelfCommand::Update,