Skip to content

Commit

Permalink
Add support for installing versioned Python executables on Windows (a…
Browse files Browse the repository at this point in the history
…stral-sh#8663)

Incorporating astral-sh#8637 into astral-sh#8458 

- Adds `python-managed` feature selection to Windows CI for `python
install` tests
- Adds trampoline sniffing utilities to `uv-trampoline-builder`
- Uses a trampoline to install Python executables into the `PATH` on
Windows
  • Loading branch information
zanieb authored Oct 31, 2024
1 parent f5a7d70 commit 8d3408f
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 44 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ jobs:
# See https://github.com/astral-sh/uv/issues/6940
UV_LINK_MODE: copy
run: |
cargo nextest run --no-default-features --features python,pypi --workspace --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow
cargo nextest run --no-default-features --features python,pypi,python-managed --workspace --status-level skip --failure-output immediate-final --no-fail-fast -j 20 --final-status-level slow
- name: "Smoke test"
working-directory: ${{ env.UV_WORKSPACE }}
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions crates/uv-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,21 +104,22 @@ pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
fs_err::remove_file(path.as_ref())
}

/// Create a symlink at `dst` pointing to `src` or, on Windows, copy `src` to `dst`.
/// Create a symlink at `dst` pointing to `src` on Unix or copy `src` to `dst` on Windows
///
/// This does not replace an existing symlink or file at `dst`.
///
/// This does not fallback to copying on Unix.
///
/// This function should only be used for files. If targeting a directory, use [`replace_symlink`]
/// instead; it will use a junction on Windows, which is more performant.
pub fn symlink_copy_fallback_file(
src: impl AsRef<Path>,
dst: impl AsRef<Path>,
) -> std::io::Result<()> {
pub fn symlink_or_copy_file(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
#[cfg(windows)]
{
fs_err::copy(src.as_ref(), dst.as_ref())?;
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
fs_err::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
}

Ok(())
Expand Down
1 change: 1 addition & 0 deletions crates/uv-python/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true }
uv-state = { workspace = true }
uv-static = { workspace = true }
uv-trampoline-builder = { workspace = true }
uv-warnings = { workspace = true }

anyhow = { workspace = true }
Expand Down
81 changes: 62 additions & 19 deletions crates/uv-python/src/managed.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
use core::fmt;
use fs_err as fs;
use itertools::Itertools;
use std::cmp::Reverse;
use std::ffi::OsStr;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;

use fs_err as fs;
use itertools::Itertools;
use same_file::is_same_file;
use thiserror::Error;
use tracing::{debug, warn};

use uv_fs::{symlink_or_copy_file, LockedFile, Simplified};
use uv_state::{StateBucket, StateStore};
use uv_static::EnvVars;
use uv_trampoline_builder::{windows_python_launcher, Launcher};

use crate::downloads::Error as DownloadError;
use crate::implementation::{
Expand All @@ -21,9 +26,6 @@ use crate::platform::Error as PlatformError;
use crate::platform::{Arch, Libc, Os};
use crate::python_version::PythonVersion;
use crate::{PythonRequest, PythonVariant};
use uv_fs::{LockedFile, Simplified};
use uv_static::EnvVars;

#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Expand Down Expand Up @@ -74,6 +76,8 @@ pub enum Error {
},
#[error("Failed to find a directory to install executables into")]
NoExecutableDirectory,
#[error(transparent)]
LauncherError(#[from] uv_trampoline_builder::Error),
#[error("Failed to read managed Python directory name: {0}")]
NameError(String),
#[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
Expand Down Expand Up @@ -425,7 +429,7 @@ impl ManagedPythonInstallation {
continue;
}

match uv_fs::symlink_copy_fallback_file(&python, &executable) {
match uv_fs::symlink_or_copy_file(&python, &executable) {
Ok(()) => {
debug!(
"Created link {} -> {}",
Expand Down Expand Up @@ -475,28 +479,67 @@ impl ManagedPythonInstallation {
Ok(())
}

/// Create a link to the Python executable in the given `bin` directory.
pub fn create_bin_link(&self, bin: &Path) -> Result<PathBuf, Error> {
/// Create a link to the managed Python executable.
///
/// If the file already exists at the target path, an error will be returned.
pub fn create_bin_link(&self, target: &Path) -> Result<(), Error> {
let python = self.executable();

let bin = target.parent().ok_or(Error::NoExecutableDirectory)?;
fs_err::create_dir_all(bin).map_err(|err| Error::ExecutableDirectory {
to: bin.to_path_buf(),
err,
})?;

// TODO(zanieb): Add support for a "default" which
let python_in_bin = bin.join(self.key.versioned_executable_name());
if cfg!(unix) {
// Note this will never copy on Unix — we use it here to allow compilation on Windows
match symlink_or_copy_file(&python, target) {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::NotFound => {
Err(Error::MissingExecutable(python.clone()))
}
Err(err) => Err(Error::LinkExecutable {
from: python,
to: target.to_path_buf(),
err,
}),
}
} else if cfg!(windows) {
// TODO(zanieb): Install GUI launchers as well
let launcher = windows_python_launcher(&python, false)?;

// OK to use `std::fs` here, `fs_err` does not support `File::create_new` and we attach
// error context anyway
#[allow(clippy::disallowed_types)]
{
std::fs::File::create_new(target)
.and_then(|mut file| file.write_all(launcher.as_ref()))
.map_err(|err| Error::LinkExecutable {
from: python,
to: target.to_path_buf(),
err,
})
}
} else {
unimplemented!("Only Windows and Unix systems are supported.")
}
}

match uv_fs::symlink_copy_fallback_file(&python, &python_in_bin) {
Ok(()) => Ok(python_in_bin),
Err(err) if err.kind() == io::ErrorKind::NotFound => {
Err(Error::MissingExecutable(python.clone()))
/// Returns `true` if the path is a link to this installation's binary, e.g., as created by
/// [`ManagedPythonInstallation::create_bin_link`].
pub fn is_bin_link(&self, path: &Path) -> bool {
if cfg!(unix) {
is_same_file(path, self.executable()).unwrap_or_default()
} else if cfg!(windows) {
let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
return false;
};
if !matches!(launcher.kind, uv_trampoline_builder::LauncherKind::Python) {
return false;
}
Err(err) => Err(Error::LinkExecutable {
from: python,
to: python_in_bin,
err,
}),
launcher.python_path == self.executable()
} else {
unreachable!("Only Windows and Unix are supported")
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-trampoline-builder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ workspace = true

[dependencies]
uv-fs = { workspace = true }

fs-err = {workspace = true }
thiserror = { workspace = true }
zip = { workspace = true }

Expand Down
Loading

0 comments on commit 8d3408f

Please sign in to comment.