diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18c9113cadaa3..034688e43ba6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/crates/uv-fs/src/lib.rs b/crates/uv-fs/src/lib.rs index 3ed23f66e6ba1..83991543483c6 100644 --- a/crates/uv-fs/src/lib.rs +++ b/crates/uv-fs/src/lib.rs @@ -104,6 +104,26 @@ pub fn remove_symlink(path: impl AsRef) -> 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`. +/// +/// If working with a directory, use [`replace_symlink`] instead which will use a junction on +/// Windows. +pub fn symlink_copy_fallback_file( + src: impl AsRef, + dst: impl AsRef, +) -> 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) -> std::io::Result<()> { match junction::delete(dunce::simplified(path.as_ref())) { diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 8aec1fc7f9941..b0a326bad96cd 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -142,6 +142,7 @@ impl PythonInstallation { let installed = ManagedPythonInstallation::new(path)?; installed.ensure_externally_managed()?; + installed.ensure_canonical_executables()?; Ok(Self { source: PythonSource::Managed, diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index 5c077079d5ec5..ec7886b0885d3 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -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}; @@ -323,6 +323,36 @@ 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 + // executable on Windows. + // See https://github.com/astral-sh/uv/issues/8298 + if !python.try_exists()? { + match self.key.variant { + PythonVariant::Default => {} + PythonVariant::Freethreaded => { + let existing = self.python_dir().join(format!( + "python{}.{}t{}", + self.key.major, + self.key.minor, + std::env::consts::EXE_SUFFIX + )); + debug!( + "Creating link {} -> {}", + python.user_display(), + existing.user_display() + ); + uv_fs::symlink_copy_fallback_file(existing, python)?; + } + } + } + + Ok(()) + } + /// Ensure the environment is marked as externally managed with the /// standard `EXTERNALLY-MANAGED` file. pub fn ensure_externally_managed(&self) -> Result<(), Error> { diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index b6df4af36a2ef..31cafbea6a7fd 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -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));