Skip to content

Commit

Permalink
Patch Python executable name for Windows free-threaded builds (#8310)
Browse files Browse the repository at this point in the history
A temporary fix for #8298 while we
wait for my slower upstream fix at
astral-sh/python-build-standalone#373

I think we'll want this machinery anyway to ensure that the various
executable names are available? Otherwise we need to special-case all
the `python` names in `uv run`?

We don't have unit test coverage of managed downloads, so I added an
[integration
test](https://github.com/astral-sh/uv/actions/runs/11394150653/job/31703956805?pr=8310)
similar to what we have for Linux.
  • Loading branch information
zanieb authored Oct 17, 2024
1 parent 3fd69b4 commit c8cbd62
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 1 deletion.
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,46 @@ jobs:
run: |
./uv pip install -v anyio
integration-test-free-threaded-windows:
timeout-minutes: 10
needs: build-binary-windows
name: "integration test | free-threaded on windows"
runs-on: windows-latest
env:
# Avoid debug build stack overflows.
UV_STACK_SIZE: 2000000

steps:
- name: "Download binary"
uses: actions/download-artifact@v4
with:
name: uv-windows-${{ github.sha }}

- name: "Install free-threaded Python via uv"
run: |
./uv python install -v 3.13t
- name: "Create a virtual environment"
run: |
./uv venv -p 3.13t --python-preference only-managed
- name: "Check version"
run: |
.venv/Scripts/python --version
- name: "Check is free-threaded"
run: |
.venv/Scripts/python -c "import sys; exit(1) if sys._is_gil_enabled() else exit(0)"
- name: "Check install"
run: |
./uv pip install -v anyio
- name: "Check uv run"
run: |
./uv run python -c ""
./uv run -p 3.13t python -c ""
integration-test-pypy-linux:
timeout-minutes: 10
needs: build-binary-linux
Expand Down
20 changes: 20 additions & 0 deletions crates/uv-fs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,26 @@ 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`.
///
/// 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<()> {
#[cfg(windows)]
{
fs_err::copy(src.as_ref(), dst.as_ref())?;
}
#[cfg(unix)]
{
std::os::unix::fs::symlink(src.as_ref(), dst.as_ref())?;
}

Ok(())
}

#[cfg(windows)]
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
match junction::delete(dunce::simplified(path.as_ref())) {
Expand Down
1 change: 1 addition & 0 deletions crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ impl PythonInstallation {

let installed = ManagedPythonInstallation::new(path)?;
installed.ensure_externally_managed()?;
installed.ensure_canonical_executables()?;

Ok(Self {
source: PythonSource::Managed,
Expand Down
53 changes: 52 additions & 1 deletion crates/uv-python/src/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use thiserror::Error;
use tracing::warn;
use tracing::{debug, warn};

use uv_state::{StateBucket, StateStore};

Expand Down Expand Up @@ -44,6 +44,15 @@ pub enum Error {
#[source]
err: io::Error,
},
#[error("Missing expected Python executable at {}", _0.user_display())]
MissingExecutable(PathBuf),
#[error("Failed to create canonical Python executable at {} from {}", to.user_display(), from.user_display())]
CanonicalizeExecutable {
from: PathBuf,
to: PathBuf,
#[source]
err: io::Error,
},
#[error("Failed to read Python installation directory: {0}", dir.user_display())]
ReadError {
dir: PathBuf,
Expand Down Expand Up @@ -323,6 +332,48 @@ impl ManagedPythonInstallation {
}
}

/// Ensure the environment contains the canonical Python executable names.
pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
let python = self.executable();

// Workaround for python-build-standalone v20241016 which is missing the standard
// `python.exe` executable in free-threaded distributions on Windows.
//
// See https://github.com/astral-sh/uv/issues/8298
if !python.try_exists()? {
match self.key.variant {
PythonVariant::Default => return Err(Error::MissingExecutable(python.clone())),
PythonVariant::Freethreaded => {
// This is the alternative executable name for the freethreaded variant
let python_in_dist = self.python_dir().join(format!(
"python{}.{}t{}",
self.key.major,
self.key.minor,
std::env::consts::EXE_SUFFIX
));
debug!(
"Creating link {} -> {}",
python.user_display(),
python_in_dist.user_display()
);
uv_fs::symlink_copy_fallback_file(&python_in_dist, &python).map_err(|err| {
if err.kind() == io::ErrorKind::NotFound {
Error::MissingExecutable(python_in_dist.clone())
} else {
Error::CanonicalizeExecutable {
from: python_in_dist,
to: python,
err,
}
}
})?;
}
}
}

Ok(())
}

/// Ensure the environment is marked as externally managed with the
/// standard `EXTERNALLY-MANAGED` file.
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
Expand Down
1 change: 1 addition & 0 deletions crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ pub(crate) async fn install(
// Ensure the installations have externally managed markers
let managed = ManagedPythonInstallation::new(path.clone())?;
managed.ensure_externally_managed()?;
managed.ensure_canonical_executables()?;
}
Err(err) => {
errors.push((key, err));
Expand Down

0 comments on commit c8cbd62

Please sign in to comment.