Skip to content

Commit

Permalink
Add initial implementation of uv tool run
Browse files Browse the repository at this point in the history
# Conflicts:
#	crates/uv/src/commands/mod.rs
#	crates/uv/src/commands/project/run.rs

# Conflicts:
#	crates/uv/src/commands/project/mod.rs
  • Loading branch information
zanieb committed May 21, 2024
1 parent dfd6ccf commit 69fa904
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 1 deletion.
35 changes: 35 additions & 0 deletions crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ impl From<ColorChoice> for anstream::ColorChoice {
pub(crate) enum Commands {
/// Resolve and install Python packages.
Pip(PipNamespace),
/// Run and manage executable Python packages.
Tool(ToolNamespace),
/// Create a virtual environment.
#[command(alias = "virtualenv", alias = "v")]
Venv(VenvArgs),
Expand Down Expand Up @@ -1920,3 +1922,36 @@ struct RemoveArgs {
/// The name of the package to remove (e.g., `Django`).
name: PackageName,
}

#[derive(Args)]
pub(crate) struct ToolNamespace {
#[command(subcommand)]
pub(crate) command: ToolCommand,
}

#[derive(Subcommand)]
pub(crate) enum ToolCommand {
/// Run a tool
Run(ToolRunArgs),
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub(crate) struct ToolRunArgs {
/// The command to run.
pub(crate) target: String,

/// The arguments to the command.
#[arg(allow_hyphen_values = true)]
pub(crate) args: Vec<OsString>,

/// The Python interpreter to use to build the run environment.
#[arg(
long,
short,
env = "UV_PYTHON",
verbatim_doc_comment,
group = "discovery"
)]
pub(crate) python: Option<String>,
}
3 changes: 3 additions & 0 deletions crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub(crate) use project::run::run;
pub(crate) use project::sync::sync;
#[cfg(feature = "self-update")]
pub(crate) use self_update::self_update;
pub(crate) use tool::run::run as run_tool;
use uv_cache::Cache;
use uv_fs::Simplified;
use uv_installer::compile_tree;
Expand All @@ -37,6 +38,8 @@ mod cache_prune;
mod pip;
mod project;
pub(crate) mod reporters;
mod tool;

#[cfg(feature = "self-update")]
mod self_update;
mod venv;
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ pub(crate) async fn install(
}

/// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
async fn update_environment(
pub(crate) async fn update_environment(
venv: PythonEnvironment,
requirements: &[RequirementsSource],
preview: PreviewMode,
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/tool/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod run;
127 changes: 127 additions & 0 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
use std::ffi::OsString;
use std::path::PathBuf;

use anyhow::Result;
use itertools::Itertools;
use tempfile::tempdir_in;
use tokio::process::Command;
use tracing::debug;

use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_interpreter::PythonEnvironment;
use uv_requirements::RequirementsSource;
use uv_warnings::warn_user;

use crate::commands::project::update_environment;
use crate::commands::ExitStatus;
use crate::printer::Printer;

/// Run a command.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run(
target: String,
args: Vec<OsString>,
python: Option<String>,
_isolated: bool,
preview: PreviewMode,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
if preview.is_disabled() {
warn_user!("`uv tool run` is experimental and may change without warning.");
}

// TODO(zanieb): Allow users to pass an explicit package name different than the target
// as well as additional requirements
let requirements = [RequirementsSource::from_package(target.clone())];

// TODO(zanieb): When implementing project-level tools, discover the project and check if it has the tool
// TOOD(zanieb): Determine if we sould layer on top of the project environment if it is present

// If necessary, create an environment for the ephemeral requirements.
debug!("Syncing ephemeral environment.");

// Discover an interpreter.
let interpreter = if let Some(python) = python.as_ref() {
PythonEnvironment::from_requested_python(python, cache)?.into_interpreter()
} else {
PythonEnvironment::from_default_python(cache)?.into_interpreter()
};

// Create a virtual environment
// 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)?;
let venv = uv_virtualenv::create_venv(
tmpdir.path(),
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
)?;

// Install the ephemeral requirements.
let ephemeral_env =
Some(update_environment(venv, &requirements, preview, cache, printer).await?);

// TODO(zanieb): Determine the command via the package entry points
let command = target;

// Construct the command
let mut process = Command::new(&command);
process.args(&args);

// Construct the `PATH` environment variable.
let new_path = std::env::join_paths(
ephemeral_env
.as_ref()
.map(PythonEnvironment::scripts)
.into_iter()
.map(PathBuf::from)
.chain(
std::env::var_os("PATH")
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env("PATH", new_path);

// Construct the `PYTHONPATH` environment variable.
let new_python_path = std::env::join_paths(
ephemeral_env
.as_ref()
.map(PythonEnvironment::site_packages)
.into_iter()
.flatten()
.map(PathBuf::from)
.chain(
std::env::var_os("PYTHONPATH")
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env("PYTHONPATH", new_python_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 space = if args.is_empty() { "" } else { " " };
debug!(
"Running `{command}{space}{}`",
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
);
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)
}
}
15 changes: 15 additions & 0 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use anstream::eprintln;
use anyhow::Result;
use clap::error::{ContextKind, ContextValue};
use clap::{CommandFactory, Parser};
use cli::{ToolCommand, ToolNamespace};
use owo_colors::OwoColorize;
use tracing::instrument;

Expand Down Expand Up @@ -599,6 +600,20 @@ async fn run() -> Result<ExitStatus> {
shell.generate(&mut Cli::command(), &mut stdout());
Ok(ExitStatus::Success)
}
Commands::Tool(ToolNamespace {
command: ToolCommand::Run(args),
}) => {
commands::run_tool(
args.target,
args.args,
args.python,
globals.isolated,
globals.preview,
&cache,
printer,
)
.await
}
}
}

Expand Down

0 comments on commit 69fa904

Please sign in to comment.