Skip to content

Commit

Permalink
Add a uv-python shim executable
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb committed Sep 25, 2024
1 parent 538b0f1 commit 6c1ef6d
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 10 deletions.
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
1 change: 1 addition & 0 deletions .github/workflows/build-docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
8 changes: 8 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
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 @@ -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) {
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 @@ -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(),
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
153 changes: 153 additions & 0 deletions crates/uv/src/bin/uv-python.rs
Original file line number Diff line number Diff line change
@@ -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<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,
}

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

Check warning on line 46 in crates/uv/src/bin/uv-python.rs

View workflow job for this annotation

GitHub Actions / typos

"implemenation" should be "implementation".
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 `+<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
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,
_ => 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(),
));
}
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)
}
}
}
62 changes: 57 additions & 5 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
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_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 +29,7 @@ pub(crate) async fn install(
project_dir: &Path,
targets: Vec<String>,
reinstall: bool,
shim: Option<bool>,
python_downloads: PythonDownloads,
native_tls: bool,
connectivity: Connectivity,
Expand Down Expand Up @@ -170,6 +177,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 +250,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(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)
}
1 change: 1 addition & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
&project_dir,
args.targets,
args.reinstall,
args.shim,
globals.python_downloads,
globals.native_tls,
globals.connectivity,
Expand Down
14 changes: 12 additions & 2 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,15 +577,25 @@ impl PythonListSettings {
pub(crate) struct PythonInstallSettings {
pub(crate) targets: Vec<String>,
pub(crate) reinstall: bool,
pub(crate) shim: Option<bool>,
}

impl PythonInstallSettings {
/// Resolve the [`PythonInstallSettings`] from the CLI and filesystem configuration.
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn resolve(args: PythonInstallArgs, _filesystem: Option<FilesystemOptions>) -> Self {
let PythonInstallArgs { targets, reinstall } = args;
let PythonInstallArgs {
targets,
reinstall,
shim,
no_shim,
} = args;

Self { targets, reinstall }
Self {
targets,
reinstall,
shim: flag(shim, no_shim),
}
}
}

Expand Down
Loading

0 comments on commit 6c1ef6d

Please sign in to comment.