mirror of https://github.com/astral-sh/uv
Add initial implementation of `uv tool run` (#3657)
This is mostly a shorter version of `uv run` that infers a requirement name from the command. The main goal here is to do the smallest amount of work necessary to get #3560 started. Closes #3613 e.g. ```shell $ uv tool run -- ruff check warning: `uv tool run` is experimental and may change without warning. Resolved 1 package in 34ms Installed 1 package in 2ms + ruff==0.4.4 error: Failed to parse example.py:1:5: Expected an expression example.py:1:5: E999 SyntaxError: Expected an expression Found 1 error. ```
This commit is contained in:
parent
d540d0f28b
commit
d313d9b1fa
|
|
@ -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),
|
||||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -464,7 +464,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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
pub(crate) mod run;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue