diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index eab2348049ee..471b0dfa7ca9 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 +3.11 --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 +3.11 --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 +3.11 --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 +3.11 --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 14936abd927a..ff819cf1d8e6 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 2a4622296115..5e037edd6d19 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/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-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index 9d143d521480..22f5e6fd9a0e 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -3512,6 +3512,14 @@ pub struct PythonInstallArgs { /// installed. #[arg(long, short, alias = "force")] pub reinstall: bool, + + /// Install a `python` shim. + #[arg(long, overrides_with("no_shim"))] + pub shim: bool, + + /// Do not install a `python` shim. + #[arg(long, overrides_with("shim"))] + pub no_shim: bool, } #[derive(Args)] diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index 05c21e964eee..64216aac79c6 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-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index b14708169103..568ca23c942e 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -602,6 +602,7 @@ impl InterpreterInfo { .arg("-I") .arg("-c") .arg(script) + .env("UV_INTERNAL__PYTHON_QUERY", "1") .output() .map_err(|err| Error::SpawnFailed { path: interpreter.to_path_buf(), diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index f8c3903222e5..408b967b89d3 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -6,7 +6,7 @@ pub use crate::discovery::{ PythonNotFound, PythonPreference, PythonRequest, PythonSource, VersionRequest, }; pub use crate::environment::{InvalidEnvironment, InvalidEnvironmentKind, PythonEnvironment}; -pub use crate::implementation::ImplementationName; +pub use crate::implementation::{ImplementationName, LenientImplementationName}; pub use crate::installation::{PythonInstallation, PythonInstallationKey}; pub use crate::interpreter::{Error as InterpreterError, Interpreter}; pub use crate::pointer_size::PointerSize; diff --git a/crates/uv/src/bin/uv-python.rs b/crates/uv/src/bin/uv-python.rs new file mode 100644 index 000000000000..fcb6c358a9fa --- /dev/null +++ b/crates/uv/src/bin/uv-python.rs @@ -0,0 +1,153 @@ +use std::convert::Infallible; +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), + RecursiveQuery, +} + +#[derive(Debug, Default)] +struct Options { + request: Option, + system: bool, + managed: bool, +} + +impl Options { + fn as_args(&self) -> Vec<&str> { + let mut args = Vec::new(); + if let Some(request) = &self.request { + args.push(request.as_str()); + } else { + // By default, we should never select an alternative implemenation with the shim + args.push("cpython"); + } + if self.system { + args.push("--system"); + } + if self.managed { + args.push("--python-preference"); + args.push("only-managed"); + } + args + } +} + +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}"), + Self::RecursiveQuery => write!(f, "Ignoring recursive query from uv"), + } + } +} + +/// Parse `+