Skip to content
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
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(
Copy link
Member Author

@zanieb zanieb Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bitten (once again) by the fact that replace_symlink is only valid for directories on Windows, so I've added a new method for files. I plan to do some sort of refactor following this to make it hard to call the wrong thing here.

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