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 support for installing versioned Python executables on Windows #8663

Merged
merged 1 commit into from
Oct 31, 2024
Merged
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
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
Loading