mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-30 23:37:24 +00:00
Patch Python executable name for Windows free-threaded builds (#8310)
A temporary fix for https://github.com/astral-sh/uv/issues/8298 while we
wait for my slower upstream fix at
https://github.com/indygreg/python-build-standalone/pull/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](3170395680
)
similar to what we have for Linux.
This commit is contained in:
parent
3fd69b448e
commit
c8cbd62a30
5 changed files with 114 additions and 1 deletions
40
.github/workflows/ci.yml
vendored
40
.github/workflows/ci.yml
vendored
|
@ -718,6 +718,46 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
./uv pip install -v anyio
|
./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:
|
integration-test-pypy-linux:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
needs: build-binary-linux
|
needs: build-binary-linux
|
||||||
|
|
|
@ -104,6 +104,26 @@ pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
fs_err::remove_file(path.as_ref())
|
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)]
|
#[cfg(windows)]
|
||||||
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
|
pub fn remove_symlink(path: impl AsRef<Path>) -> std::io::Result<()> {
|
||||||
match junction::delete(dunce::simplified(path.as_ref())) {
|
match junction::delete(dunce::simplified(path.as_ref())) {
|
||||||
|
|
|
@ -142,6 +142,7 @@ impl PythonInstallation {
|
||||||
|
|
||||||
let installed = ManagedPythonInstallation::new(path)?;
|
let installed = ManagedPythonInstallation::new(path)?;
|
||||||
installed.ensure_externally_managed()?;
|
installed.ensure_externally_managed()?;
|
||||||
|
installed.ensure_canonical_executables()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
source: PythonSource::Managed,
|
source: PythonSource::Managed,
|
||||||
|
|
|
@ -7,7 +7,7 @@ use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::warn;
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use uv_state::{StateBucket, StateStore};
|
use uv_state::{StateBucket, StateStore};
|
||||||
|
|
||||||
|
@ -44,6 +44,15 @@ pub enum Error {
|
||||||
#[source]
|
#[source]
|
||||||
err: io::Error,
|
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())]
|
#[error("Failed to read Python installation directory: {0}", dir.user_display())]
|
||||||
ReadError {
|
ReadError {
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
|
@ -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
|
/// Ensure the environment is marked as externally managed with the
|
||||||
/// standard `EXTERNALLY-MANAGED` file.
|
/// standard `EXTERNALLY-MANAGED` file.
|
||||||
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
|
pub fn ensure_externally_managed(&self) -> Result<(), Error> {
|
||||||
|
|
|
@ -167,6 +167,7 @@ pub(crate) async fn install(
|
||||||
// Ensure the installations have externally managed markers
|
// Ensure the installations have externally managed markers
|
||||||
let managed = ManagedPythonInstallation::new(path.clone())?;
|
let managed = ManagedPythonInstallation::new(path.clone())?;
|
||||||
managed.ensure_externally_managed()?;
|
managed.ensure_externally_managed()?;
|
||||||
|
managed.ensure_canonical_executables()?;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
errors.push((key, err));
|
errors.push((key, err));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue