Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a uv-python shim executable #7677

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ jobs:
# Generate Dockerfile content
cat <<EOF > Dockerfile
FROM ${BASE_IMAGE}
COPY --from=${{ env.UV_BASE_IMG }}:latest /uv /uvx /usr/local/bin/
COPY --from=${{ env.UV_BASE_IMG }}:latest /uv /uvx /uv-python /usr/local/bin/
ENTRYPOINT []
CMD ["/usr/local/bin/uv"]
EOF
Expand Down
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ 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 /uvx /
COPY --from=build /uv /uvx /uv-python /
WORKDIR /io
ENTRYPOINT ["/uv"]
8 changes: 8 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3546,6 +3546,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)]
Expand Down
1 change: 1 addition & 0 deletions crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,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) {
Expand Down
1 change: 1 addition & 0 deletions crates/uv-python/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ impl InterpreterInfo {
.arg("-B") // Don't write bytecode.
.arg("-c")
.arg(script)
.env("UV_INTERNAL__PYTHON_QUERY", "1")
.output()
.map_err(|err| Error::SpawnFailed {
path: interpreter.to_path_buf(),
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
168 changes: 168 additions & 0 deletions crates/uv/src/bin/uv-python.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
use std::convert::Infallible;
use std::io::Write;
use std::{
ffi::OsString,
process::{Command, ExitCode, ExitStatus},
};

/// Spawns a command exec style.
fn exec_spawn(cmd: &mut Command) -> std::io::Result<Infallible> {
#[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<String>,
system: bool,
managed: bool,
verbose: 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 implementation with the shim
args.push("cpython");
}
if self.system {
args.push("--system");
}
if self.verbose {
args.push("--verbose");
}
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 `+<option>` into [`Options`].
///
/// Supports the following options:
///
/// - `+system`: Use the system Python, ignore virtual environments.
/// - `+managed`: Use only managed Python installations.
/// - `+<request>`: Request a Python version
/// - `+v`: Enable verbose mode.
fn parse_options(mut args: Vec<OsString>) -> (Vec<OsString>, Options) {
let mut position = 0;
let mut options = Options::default();
while position < args.len() {
let arg = &args[position].to_string_lossy();

// If the argument doesn't start with `+`, we're done.
let Some(option) = arg.strip_prefix('+') else {
break;
};

match option {
"system" => options.system = true,
"managed" => options.managed = true,
"v" => options.verbose = true,
_ => options.request = Some(option.to_string()),
}

position += 1;
}

(args.split_off(position), options)
}

/// Find the `uv` binary to use.
fn find_uv() -> Result<std::path::PathBuf, Error> {
// We prefer one next to the current binary.
let current_exe = std::env::current_exe().map_err(Error::Io)?;
if let Some(bin) = current_exe.parent() {
let uv = bin.join("uv");
if uv.exists() {
return Ok(uv);
}
}
// Otherwise, we'll search for it on the `PATH`.
which::which("uv").map_err(Error::Which)
}

fn run() -> Result<ExitStatus, Error> {
if std::env::var_os("UV_INTERNAL__PYTHON_QUERY").is_some() {
return Err(Error::RecursiveQuery);
}

let args = std::env::args_os().skip(1).collect::<Vec<_>>();
let (args, options) = parse_options(args);
let uv = find_uv()?;
let mut cmd = Command::new(uv);
let uv_args = ["python", "find"].iter().copied().chain(options.as_args());
cmd.args(uv_args);
let output = cmd.output().map_err(Error::Io)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(output.stderr.as_slice());
return Err(Error::NoInterpreter(
stderr
.strip_prefix("error: ")
.unwrap_or(&*stderr)
.to_string(),
));
}

// If verbose is enabled, print the output of the `uv python find` command
if options.verbose {
std::io::stderr()
.write_all(&output.stderr)
.map_err(Error::Io)?;
}

let python = std::path::PathBuf::from(String::from_utf8_lossy(output.stdout.as_slice()).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)
}
}
}
64 changes: 59 additions & 5 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
use anyhow::Result;
use std::collections::BTreeSet;
use std::fmt::Write;
use std::path::{Path, PathBuf};

use anyhow::{bail, Result};
use fs_err as fs;
use futures::stream::FuturesUnordered;
use futures::StreamExt;
use itertools::Itertools;
use owo_colors::OwoColorize;
use std::collections::BTreeSet;
use std::fmt::Write;
use std::path::Path;

use tracing::debug;
use uv_client::Connectivity;
use uv_configuration::PreviewMode;
use uv_fs::Simplified;
use uv_python::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest};
use uv_python::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
use uv_python::{PythonDownloads, PythonRequest, PythonVersionFile};
use uv_python::{
ImplementationName, LenientImplementationName, PythonDownloads, PythonRequest,
PythonVersionFile,
};

use crate::commands::python::{ChangeEvent, ChangeEventKind};
use crate::commands::reporters::PythonDownloadReporter;
Expand All @@ -23,6 +30,8 @@ pub(crate) async fn install(
project_dir: &Path,
targets: Vec<String>,
reinstall: bool,
shim: Option<bool>,
preview: PreviewMode,
python_downloads: PythonDownloads,
native_tls: bool,
connectivity: Connectivity,
Expand Down Expand Up @@ -170,6 +179,13 @@ pub(crate) async fn install(
}
}

let any_cpython = installed.iter().any(|key| {
matches!(
key.implementation(),
LenientImplementationName::Known(ImplementationName::CPython)
)
});

if !installed.is_empty() {
if let [installed] = installed.as_slice() {
// Ex) "Installed Python 3.9.7 in 1.68s"
Expand Down Expand Up @@ -236,5 +252,43 @@ pub(crate) async fn install(
return Ok(ExitStatus::Failure);
}

// Install a shim if explicitly requested or if we installed a CPython version
if shim.unwrap_or(preview.is_enabled() && any_cpython) {
let shim_src = find_shim()?;
let executable_dir = uv_tool::find_executable_directory()?;
let shim_dst = executable_dir.join("python");
if shim_dst.try_exists()? {
writeln!(
printer.stderr(),
"Python executable already exists at `{}`",
shim_dst.user_display().cyan()
)?;
} else {
debug!(
"Linking {} -> {}",
shim_src.user_display(),
shim_dst.user_display()
);
uv_fs::replace_symlink(&shim_src, &shim_dst)?;
writeln!(
printer.stderr(),
"Installed Python shim to `{}`",
shim_dst.user_display().cyan()
)?;
}
}

Ok(ExitStatus::Success)
}

fn find_shim() -> Result<PathBuf> {
let current_exe = std::env::current_exe()?;
let Some(bin) = current_exe.parent() else {
bail!("Could not find the directory for the `uv-python` binary");
};
let uv_python = bin.join("uv-python");
if !uv_python.try_exists()? {
bail!("Could not find the `uv-python` binary");
}
Ok(uv_python)
}
8 changes: 8 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1006,10 +1006,18 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
let args = settings::PythonInstallSettings::resolve(args, filesystem);
show_settings!(args);

if matches!(args.shim, Some(true)) && globals.preview.is_disabled() {
warn_user_once!(
"The uv Python shim is experimental and may change without warning"
);
}

commands::python_install(
&project_dir,
args.targets,
args.reinstall,
args.shim,
globals.preview,
globals.python_downloads,
globals.native_tls,
globals.connectivity,
Expand Down
Loading
Loading