Skip to content

Commit 1d23153

Browse files
committed
Use trampolines for Python executables on Windows
1 parent 34a4330 commit 1d23153

File tree

5 files changed

+73
-16
lines changed

5 files changed

+73
-16
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-python/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ uv-platform-tags = { workspace = true }
3131
uv-pypi-types = { workspace = true }
3232
uv-state = { workspace = true }
3333
uv-static = { workspace = true }
34+
uv-trampoline-builder = { workspace = true }
3435
uv-warnings = { workspace = true }
3536

3637
anyhow = { workspace = true }

crates/uv-python/src/managed.rs

+35-14
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
use core::fmt;
2-
use fs_err as fs;
3-
use itertools::Itertools;
42
use std::cmp::Reverse;
53
use std::ffi::OsStr;
64
use std::io::{self, Write};
75
use std::path::{Path, PathBuf};
86
use std::str::FromStr;
7+
8+
use fs_err::{self as fs, File};
9+
use itertools::Itertools;
910
use thiserror::Error;
1011
use tracing::{debug, warn};
1112

13+
use uv_fs::{LockedFile, Simplified};
1214
use uv_state::{StateBucket, StateStore};
15+
use uv_static::EnvVars;
16+
use uv_trampoline_builder::windows_python_launcher;
1317

1418
use crate::downloads::Error as DownloadError;
1519
use crate::implementation::{
@@ -21,9 +25,6 @@ use crate::platform::Error as PlatformError;
2125
use crate::platform::{Arch, Libc, Os};
2226
use crate::python_version::PythonVersion;
2327
use crate::{PythonRequest, PythonVariant};
24-
use uv_fs::{LockedFile, Simplified};
25-
use uv_static::EnvVars;
26-
2728
#[derive(Error, Debug)]
2829
pub enum Error {
2930
#[error(transparent)]
@@ -74,6 +75,8 @@ pub enum Error {
7475
},
7576
#[error("Failed to find a directory to install executables into")]
7677
NoExecutableDirectory,
78+
#[error(transparent)]
79+
LauncherError(#[from] uv_trampoline_builder::Error),
7780
#[error("Failed to read managed Python directory name: {0}")]
7881
NameError(String),
7982
#[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
@@ -485,16 +488,34 @@ impl ManagedPythonInstallation {
485488
err,
486489
})?;
487490

488-
match uv_fs::symlink_copy_fallback_file(&python, target) {
489-
Ok(()) => Ok(()),
490-
Err(err) if err.kind() == io::ErrorKind::NotFound => {
491-
Err(Error::MissingExecutable(python.clone()))
491+
if cfg!(unix) {
492+
match uv_fs::symlink_copy_fallback_file(&python, target) {
493+
Ok(()) => Ok(()),
494+
Err(err) if err.kind() == io::ErrorKind::NotFound => {
495+
Err(Error::MissingExecutable(python.clone()))
496+
}
497+
Err(err) => Err(Error::LinkExecutable {
498+
from: python,
499+
to: target.to_path_buf(),
500+
err,
501+
}),
492502
}
493-
Err(err) => Err(Error::LinkExecutable {
494-
from: python,
495-
to: target.to_path_buf(),
496-
err,
497-
}),
503+
} else if cfg!(windows) {
504+
// TODO(zanieb): Install GUI launchers as well
505+
let launcher = windows_python_launcher(&python, false)?;
506+
match File::create(target)?.write_all(launcher.as_ref()) {
507+
Ok(()) => Ok(()),
508+
Err(err) if err.kind() == io::ErrorKind::NotFound => {
509+
Err(Error::MissingExecutable(python.clone()))
510+
}
511+
Err(err) => Err(Error::LinkExecutable {
512+
from: python,
513+
to: target.to_path_buf(),
514+
err,
515+
}),
516+
}
517+
} else {
518+
unimplemented!("Only Windows and Unix systems are supported.")
498519
}
499520
}
500521
}

crates/uv-trampoline-builder/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ workspace = true
2323

2424
[dependencies]
2525
uv-fs = { workspace = true }
26+
27+
fs-err = {workspace = true }
2628
thiserror = { workspace = true }
2729
zip = { workspace = true }
2830

crates/uv-trampoline-builder/src/lib.rs

+34-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
use std::io::{Cursor, Write};
1+
use std::io::{self, Cursor, Read, Seek, Write};
22
use std::path::Path;
33

4+
use fs_err::File;
45
use thiserror::Error;
56
use uv_fs::Simplified;
67
use zip::write::FileOptions;
@@ -33,7 +34,7 @@ const LAUNCHER_AARCH64_CONSOLE: &[u8] =
3334
/// The kind of trampoline launcher to create.
3435
///
3536
/// See [`uv-trampoline::bounce::TrampolineKind`].
36-
enum LauncherKind {
37+
pub enum LauncherKind {
3738
/// The trampoline should execute itself, it's a zipped Python script.
3839
Script,
3940
/// The trampoline should just execute Python, it's a proxy Python executable.
@@ -47,6 +48,37 @@ impl LauncherKind {
4748
Self::Python => b"UVPY",
4849
}
4950
}
51+
52+
/// Read a [`LauncherKind`] from 4 byte buffer.
53+
///
54+
/// If the buffer does not contain a matching magic number, `None` is returned.
55+
fn try_from_bytes(bytes: [u8; 4]) -> Option<Self> {
56+
if &bytes == Self::Script.magic_number() {
57+
return Some(Self::Script);
58+
}
59+
if &bytes == Self::Python.magic_number() {
60+
return Some(Self::Python);
61+
}
62+
None
63+
}
64+
65+
/// Read a [`LauncherKind`] from a file path.
66+
///
67+
/// If the file cannot be read, an [`io::Error`] is returned. If the path is not a launcher,
68+
/// `None` is returned.
69+
pub fn try_from_path(path: &Path) -> Result<Option<Self>, io::Error> {
70+
let mut file = File::open(path)?;
71+
let mut buffer = [0; 4];
72+
73+
// If the file is less than four bytes, it's not a launcher.
74+
let Ok(_) = file.seek(io::SeekFrom::End(-4)) else {
75+
return Ok(None);
76+
};
77+
78+
file.read_exact(&mut buffer)?;
79+
80+
Ok(Self::try_from_bytes(buffer))
81+
}
5082
}
5183

5284
/// Note: The caller is responsible for adding the path of the wheel we're installing.

0 commit comments

Comments
 (0)