From f7b83e9e83513f7572b307d3c12e60b12f73a42d Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 17 Apr 2024 11:20:43 -0500 Subject: [PATCH] Add `uv run` command (#3074) Adds `uv run` which executes a command in your current virtual environment. This is a simple first milestone, lots of remaining work and behavior. The command is hidden. --- crates/uv/src/cli.rs | 13 ++++++++ crates/uv/src/commands/mod.rs | 2 ++ crates/uv/src/commands/run.rs | 63 +++++++++++++++++++++++++++++++++++ crates/uv/src/main.rs | 1 + 4 files changed, 79 insertions(+) create mode 100644 crates/uv/src/commands/run.rs diff --git a/crates/uv/src/cli.rs b/crates/uv/src/cli.rs index b1c6fefc2..7793ec409 100644 --- a/crates/uv/src/cli.rs +++ b/crates/uv/src/cli.rs @@ -111,6 +111,8 @@ pub(crate) enum Commands { /// Clear the cache, removing all entries or those linked to specific packages. #[command(hide = true)] Clean(CleanArgs), + #[clap(hide = true)] + Run(RunArgs), /// Display uv's version Version { #[arg(long, value_enum, default_value = "text")] @@ -1307,6 +1309,17 @@ pub(crate) struct VenvArgs { pub(crate) compat_args: compat::VenvCompatArgs, } +#[derive(Args)] +#[allow(clippy::struct_excessive_bools)] +pub(crate) struct RunArgs { + /// Command + pub(crate) command: String, + + /// Arguments + #[clap(allow_hyphen_values = true)] + pub(crate) args: Vec, +} + #[derive(Args)] #[allow(clippy::struct_excessive_bools)] struct AddArgs { diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index ff7eaaa2f..d3d5f20cf 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -16,6 +16,7 @@ pub(crate) use pip_list::pip_list; pub(crate) use pip_show::pip_show; pub(crate) use pip_sync::pip_sync; pub(crate) use pip_uninstall::pip_uninstall; +pub(crate) use run::run; #[cfg(feature = "self-update")] pub(crate) use self_update::self_update; use uv_cache::Cache; @@ -40,6 +41,7 @@ mod pip_show; mod pip_sync; mod pip_uninstall; mod reporters; +mod run; #[cfg(feature = "self-update")] mod self_update; mod venv; diff --git a/crates/uv/src/commands/run.rs b/crates/uv/src/commands/run.rs new file mode 100644 index 000000000..27d77bc78 --- /dev/null +++ b/crates/uv/src/commands/run.rs @@ -0,0 +1,63 @@ +use std::ffi::OsString; +use std::{env, iter}; + +use anyhow::Result; +use owo_colors::OwoColorize; +use tracing::debug; +use uv_fs::Simplified; +use uv_interpreter::PythonEnvironment; + +use crate::commands::ExitStatus; +use tokio::process::Command; +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(" ")); + + // 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()), + }; + + // Construct the command + 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); + }; + + // 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 + let mut handle = process.spawn()?; + let status = handle.wait().await?; + + // Exit based on the result of the command + // TODO(zanieb): Do we want to exit with the code of the child process? Probably. + if status.success() { + Ok(ExitStatus::Success) + } else { + Ok(ExitStatus::Failure) + } +} diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 2259356b3..41fb7c468 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -539,6 +539,7 @@ async fn run() -> Result { ) .await } + Commands::Run(args) => commands::run(args.command, args.args, &cache).await, #[cfg(feature = "self-update")] Commands::Self_(SelfNamespace { command: SelfCommand::Update,