Add uvx alias for uv tool run (#4632)

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](433af5aed9/crates/monotrail/src/monotrail.rs (L764-L799)).
This commit is contained in:
Zanie Blue 2024-07-01 21:42:02 -04:00 committed by GitHub
parent 8e935e2c17
commit ec2723a9f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

39
crates/uv/src/bin/uvx.rs Normal file
View file

@ -0,0 +1,39 @@
use std::{
ffi::OsString,
process::{Command, ExitCode, ExitStatus},
};
use anyhow::bail;
fn run() -> Result<ExitStatus, anyhow::Error> {
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::<Vec<_>>();
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)
}
}
}