From ec2723a9f528d6e0a82bbe831046e03e2894190e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 1 Jul 2024 21:42:02 -0400 Subject: [PATCH] Add `uvx` alias for `uv tool run` (#4632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes https://github.com/astral-sh/uv/issues/4476 Originally, this used the changes in #4642 to invoke `main()` from a `uvx` binary. This had the benefit of `uvx` being entirely standalone at the cost of doubling our artifact size. We think that's the incorrect trade-off. Instead, we assume `uvx` is always next to `uv` and create a tiny binary (<1MB) that invokes `uv` in a child process. This seems preferable to a `cargo-dist` alias because we have more control over it. This binary should "just work" for all of our cargo-dist distributions and installers, but we'll need to add a new entry point for our PyPI distribution. I'll probably tackle support there separately? ``` ❯ ls -lah target/release/uv target/release/uvx -rwxr-xr-x 1 zb staff 31M Jun 28 23:23 target/release/uv -rwxr-xr-x 1 zb staff 452K Jun 28 23:22 target/release/uvx ``` This includes some small overhead: ``` ❯ hyperfine --shell=none --warmup=100 './target/release/uv tool run --help' './target/release/uvx --help' --min-runs 2000 Benchmark 1: ./target/release/uv tool run --help Time (mean ± σ): 2.2 ms ± 0.1 ms [User: 1.3 ms, System: 0.5 ms] Range (min … max): 2.0 ms … 4.0 ms 2000 runs Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options. Benchmark 2: ./target/release/uvx --help Time (mean ± σ): 2.9 ms ± 0.1 ms [User: 1.7 ms, System: 0.9 ms] Range (min … max): 2.8 ms … 4.2 ms 2000 runs Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet system without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options. Summary ./target/release/uv tool run --help ran 1.35 ± 0.09 times faster than ./target/release/uvx --help ``` I presume there may be some other downsides to a child process? The wrapper is a little awkward. We could consider `execv` but this is complicated across platforms. An example implementation of that over in [monotrail](https://github.com/konstin/poc-monotrail/blob/433af5aed90921e2c3f4b426b3ffffd83f04d3ff/crates/monotrail/src/monotrail.rs#L764-L799). --- crates/uv/src/bin/uvx.rs | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 crates/uv/src/bin/uvx.rs diff --git a/crates/uv/src/bin/uvx.rs b/crates/uv/src/bin/uvx.rs new file mode 100644 index 000000000..656f9a323 --- /dev/null +++ b/crates/uv/src/bin/uvx.rs @@ -0,0 +1,39 @@ +use std::{ + ffi::OsString, + process::{Command, ExitCode, ExitStatus}, +}; + +use anyhow::bail; + +fn run() -> Result { + let current_exe = std::env::current_exe()?; + let Some(bin) = current_exe.parent() else { + bail!("Could not determine the location of the `uvx` binary") + }; + let uv = bin.join("uv"); + let args = ["tool", "run"] + .iter() + .map(OsString::from) + // Skip the `uvx` name + .chain(std::env::args_os().skip(1)) + .collect::>(); + + Ok(Command::new(uv).args(&args).status()?) +} + +#[allow(clippy::print_stderr)] +fn main() -> ExitCode { + let result = run(); + match result { + // Fail with 2 if the status cannot be cast to an exit code + Ok(status) => u8::try_from(status.code().unwrap_or(2)).unwrap_or(2).into(), + Err(err) => { + let mut causes = err.chain(); + eprintln!("error: {}", causes.next().unwrap()); + for err in causes { + eprintln!(" Caused by: {err}"); + } + ExitCode::from(2) + } + } +}