Skip to content

Commit

Permalink
Add uvx alias for uv tool run (#4632)
Browse files Browse the repository at this point in the history
Closes #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).
  • Loading branch information
zanieb authored Jul 2, 2024
1 parent 8e935e2 commit ec2723a
Showing 1 changed file with 39 additions and 0 deletions.
39 changes: 39 additions & 0 deletions crates/uv/src/bin/uvx.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
}

0 comments on commit ec2723a

Please sign in to comment.