From e0ecb54302c9dd71f73f943dc1358c3102c6ca47 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 24 Sep 2024 12:52:46 -0500 Subject: [PATCH] Add a `uv-python` shim executable --- .github/workflows/build-binaries.yml | 10 +++ .github/workflows/build-docker.yml | 1 + Dockerfile | 4 +- crates/uv-python/src/discovery.rs | 1 + crates/uv/src/bin/uv-python.rs | 92 ++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 crates/uv/src/bin/uv-python.rs diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index eab2348049ee8..532c75554bddf 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -56,6 +56,7 @@ jobs: ${{ env.MODULE_NAME }} --help python -m ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload sdist" uses: actions/upload-artifact@v4 with: @@ -128,6 +129,7 @@ jobs: ${{ env.MODULE_NAME }} --help python -m ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -185,6 +187,7 @@ jobs: ${{ env.MODULE_NAME }} --help python -m ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -251,6 +254,7 @@ jobs: ${{ env.MODULE_NAME }} --help python -m ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -325,6 +329,7 @@ jobs: pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -394,6 +399,7 @@ jobs: pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -476,6 +482,7 @@ jobs: pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -540,6 +547,7 @@ jobs: .venv/bin/pip install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall .venv/bin/${{ env.MODULE_NAME }} --help .venv/bin/uvx --help + .venv/bin/uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: @@ -615,6 +623,7 @@ jobs: .venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall .venv/bin/${{ env.MODULE_NAME }} --help .venv/bin/uvx --help + .venv/bin/uv-python --help - uses: uraimo/run-on-arch-action@v2 name: Test wheel (manylinux) if: matrix.platform.arch == 'aarch64' @@ -630,6 +639,7 @@ jobs: pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall ${{ env.MODULE_NAME }} --help uvx --help + uv-python --help - name: "Upload wheels" uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 14936abd927af..ff819cf1d8e66 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -198,6 +198,7 @@ jobs: FROM ${BASE_IMAGE} COPY --from=${{ env.UV_BASE_IMG }}:latest /uv /usr/local/bin/uv COPY --from=${{ env.UV_BASE_IMG }}:latest /uvx /usr/local/bin/uvx + COPY --from=${{ env.UV_BASE_IMG }}:latest /uv-python /usr/local/bin/uv-python ENTRYPOINT [] CMD ["/usr/local/bin/uv"] EOF diff --git a/Dockerfile b/Dockerfile index 2a46222961157..19f665f669a43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,14 +34,16 @@ RUN rustup target add $(cat rust_target.txt) COPY crates crates COPY ./Cargo.toml Cargo.toml COPY ./Cargo.lock Cargo.lock -RUN cargo zigbuild --bin uv --bin uvx --target $(cat rust_target.txt) --release +RUN cargo zigbuild --bin uv --bin uvx --bin uv-python --target $(cat rust_target.txt) --release RUN cp target/$(cat rust_target.txt)/release/uv /uv \ && cp target/$(cat rust_target.txt)/release/uvx /uvx + && cp target/$(cat rust_target.txt)/release/uv-python /uv-python # TODO(konsti): Optimize binary size, with a version that also works when cross compiling # RUN strip --strip-all /uv FROM scratch COPY --from=build /uv /uv COPY --from=build /uvx /uvx +COPY --from=build /uv-python /uv-python WORKDIR /io ENTRYPOINT ["/uv"] diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 05c21e964eeea..64216aac79c6a 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -1188,6 +1188,7 @@ impl PythonRequest { if let Ok(version) = VersionRequest::from_str(value) { return Self::Version(version); } + // e.g. `python3.12.1` if let Some(remainder) = value.strip_prefix("python") { if let Ok(version) = VersionRequest::from_str(remainder) { diff --git a/crates/uv/src/bin/uv-python.rs b/crates/uv/src/bin/uv-python.rs new file mode 100644 index 0000000000000..7fb81d5d4827d --- /dev/null +++ b/crates/uv/src/bin/uv-python.rs @@ -0,0 +1,92 @@ +use std::convert::Infallible; +use std::os::unix::ffi::OsStringExt; +use std::{ + ffi::OsString, + process::{Command, ExitCode, ExitStatus}, +}; + +/// Spawns a command exec style. +fn exec_spawn(cmd: &mut Command) -> std::io::Result { + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + let err = cmd.exec(); + Err(err) + } + #[cfg(windows)] + { + cmd.stdin(std::process::Stdio::inherit()); + let status = cmd.status()?; + + #[allow(clippy::exit)] + std::process::exit(status.code().unwrap()) + } +} + +#[derive(Debug)] +enum Error { + Io(std::io::Error), + Which(which::Error), + NoInterpreter(String), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Io(err) => write!(f, "{err}"), + Self::Which(err) => write!(f, "Failed to find uv binary: {err}"), + Self::NoInterpreter(inner) => write!(f, "{inner}"), + } + } +} + +/// Parse a `+` Python version request from the arguments. +fn parse_python_request(mut args: Vec) -> (Vec, Option) { + let Some(arg) = args.first() else { + return (args, None); + }; + let arg = arg.to_string_lossy(); + let Some(version) = arg.strip_prefix('+') else { + return (args, None); + }; + let version = version.to_string(); + (args.split_off(1), Some(version)) +} + +fn run() -> Result { + let args = std::env::args_os().skip(1).collect::>(); + let (args, request) = parse_python_request(args); + let uv = which::which("uv").map_err(Error::Which)?; + let mut cmd = Command::new(uv); + let uv_args = ["python", "find"] + .iter() + .copied() + .chain(request.iter().map(std::string::String::as_str)); + cmd.args(uv_args); + let output = cmd.output().map_err(Error::Io)?; + if !output.status.success() { + return Err(Error::NoInterpreter( + OsString::from_vec(output.stderr) + .to_string_lossy() + .to_string(), + )); + } + let python = + std::path::PathBuf::from(OsString::from_vec(output.stdout).to_string_lossy().trim()); + let mut cmd = Command::new(python); + cmd.args(&args); + match exec_spawn(&mut cmd).map_err(Error::Io)? {} +} + +#[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) => { + eprintln!("error: {err}"); + ExitCode::from(2) + } + } +}