This commit is contained in:
Zanie Blue 2025-08-28 09:41:17 -05:00
parent ee07f64b17
commit f2b69d292c
8 changed files with 219 additions and 231 deletions

View File

@ -29,6 +29,7 @@ uv-normalize = { workspace = true }
uv-pep440 = { workspace = true } uv-pep440 = { workspace = true }
uv-pypi-types = { workspace = true } uv-pypi-types = { workspace = true }
uv-shell = { workspace = true } uv-shell = { workspace = true }
uv-trampoline-builder = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
clap = { workspace = true, optional = true, features = ["derive"] } clap = { workspace = true, optional = true, features = ["derive"] }
@ -51,8 +52,6 @@ tracing = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
uv-trampoline-builder = { workspace = true }
same-file = { workspace = true } same-file = { workspace = true }
self-replace = { workspace = true } self-replace = { workspace = true }

View File

@ -80,7 +80,6 @@ pub enum Error {
MismatchedVersion(Version, Version), MismatchedVersion(Version, Version),
#[error("Invalid egg-link")] #[error("Invalid egg-link")]
InvalidEggLink(PathBuf), InvalidEggLink(PathBuf),
#[cfg(windows)]
#[error(transparent)] #[error(transparent)]
LauncherError(#[from] uv_trampoline_builder::Error), LauncherError(#[from] uv_trampoline_builder::Error),
#[error("Scripts must not use the reserved name {0}")] #[error("Scripts must not use the reserved name {0}")]

View File

@ -17,6 +17,7 @@ use uv_fs::{Simplified, persist_with_retry_sync, relative_to};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pypi_types::DirectUrl; use uv_pypi_types::DirectUrl;
use uv_shell::escape_posix_for_single_quotes; use uv_shell::escape_posix_for_single_quotes;
use uv_trampoline_builder::windows_script_launcher;
use uv_warnings::warn_user_once; use uv_warnings::warn_user_once;
use crate::record::RecordEntry; use crate::record::RecordEntry;
@ -225,18 +226,14 @@ pub(crate) fn write_script_entrypoints(
); );
// If necessary, wrap the launcher script in a Windows launcher binary. // If necessary, wrap the launcher script in a Windows launcher binary.
#[cfg(windows)] if cfg!(windows) {
{
use uv_trampoline_builder::windows_script_launcher;
write_file_recorded( write_file_recorded(
site_packages, site_packages,
&entrypoint_relative, &entrypoint_relative,
&windows_script_launcher(&launcher_python_script, is_gui, &launcher_executable)?, &windows_script_launcher(&launcher_python_script, is_gui, &launcher_executable)?,
record, record,
)?; )?;
} } else {
#[cfg(not(windows))]
{
write_file_recorded( write_file_recorded(
site_packages, site_packages,
&entrypoint_relative, &entrypoint_relative,

View File

@ -34,6 +34,7 @@ uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true } uv-redacted = { workspace = true }
uv-state = { workspace = true } uv-state = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-trampoline-builder = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
@ -68,8 +69,6 @@ which = { workspace = true }
once_cell = { workspace = true } once_cell = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
uv-trampoline-builder = { workspace = true }
windows-registry = { workspace = true } windows-registry = { workspace = true }
windows-result = { workspace = true } windows-result = { workspace = true }
windows-sys = { workspace = true } windows-sys = { workspace = true }

View File

@ -20,6 +20,7 @@ use uv_platform::{Error as PlatformError, Os};
use uv_platform::{LibcDetectionError, Platform}; use uv_platform::{LibcDetectionError, Platform};
use uv_state::{StateBucket, StateStore}; use uv_state::{StateBucket, StateStore};
use uv_static::EnvVars; use uv_static::EnvVars;
use uv_trampoline_builder::{Launcher, LauncherKind};
use crate::downloads::{Error as DownloadError, ManagedPythonDownload}; use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
use crate::implementation::{ use crate::implementation::{
@ -92,7 +93,6 @@ pub enum Error {
}, },
#[error("Failed to find a directory to install executables into")] #[error("Failed to find a directory to install executables into")]
NoExecutableDirectory, NoExecutableDirectory,
#[cfg(windows)]
#[error(transparent)] #[error(transparent)]
LauncherError(#[from] uv_trampoline_builder::Error), LauncherError(#[from] uv_trampoline_builder::Error),
#[error("Failed to read managed Python directory name: {0}")] #[error("Failed to read managed Python directory name: {0}")]
@ -619,13 +619,9 @@ impl ManagedPythonInstallation {
/// Returns `true` if the path is a link to this installation's binary, e.g., as created by /// Returns `true` if the path is a link to this installation's binary, e.g., as created by
/// [`create_bin_link`]. /// [`create_bin_link`].
pub fn is_bin_link(&self, path: &Path) -> bool { pub fn is_bin_link(&self, path: &Path) -> bool {
#[cfg(unix)] if cfg!(unix) {
{
same_file::is_same_file(path, self.executable(false)).unwrap_or_default() same_file::is_same_file(path, self.executable(false)).unwrap_or_default()
} } else if cfg!(windows) {
#[cfg(windows)]
{
use uv_trampoline_builder::{Launcher, LauncherKind};
let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else { let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
return false; return false;
}; };
@ -637,9 +633,7 @@ impl ManagedPythonInstallation {
// directly. // directly.
dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path) dunce::canonicalize(&launcher.python_path).unwrap_or(launcher.python_path)
== self.executable(false) == self.executable(false)
} } else {
#[cfg(not(any(unix, windows)))]
{
unreachable!("Only Windows and Unix are supported") unreachable!("Only Windows and Unix are supported")
} }
} }
@ -882,8 +876,7 @@ pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), E
err, err,
})?; })?;
#[cfg(unix)] if cfg!(unix) {
{
// Note this will never copy on Unix — we use it here to allow compilation on Windows // Note this will never copy on Unix — we use it here to allow compilation on Windows
match symlink_or_copy_file(executable, link) { match symlink_or_copy_file(executable, link) {
Ok(()) => Ok(()), Ok(()) => Ok(()),
@ -896,9 +889,7 @@ pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), E
err, err,
}), }),
} }
} } else if cfg!(windows) {
#[cfg(windows)]
{
use uv_trampoline_builder::windows_python_launcher; use uv_trampoline_builder::windows_python_launcher;
// TODO(zanieb): Install GUI launchers as well // TODO(zanieb): Install GUI launchers as well
@ -916,10 +907,8 @@ pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), E
err, err,
}) })
} }
} } else {
#[cfg(not(any(unix, windows)))] unimplemented!("Only Windows and Unix are supported.")
{
unimplemented!("Only Windows and Unix systems are supported.")
} }
} }

View File

@ -51,18 +51,21 @@ pub struct Launcher {
} }
impl Launcher { impl Launcher {
/// Attempt to read [`Launcher`] metadata from a trampoline executable file.
///
/// On Unix, this always returns [`None`]. Trampolines are a Windows-specific feature and cannot
/// be read on other platforms.
#[cfg(not(windows))]
pub fn try_from_path(_path: &Path) -> Result<Option<Self>, Error> {
Ok(None)
}
/// Read [`Launcher`] metadata from a trampoline executable file. /// Read [`Launcher`] metadata from a trampoline executable file.
/// ///
/// Returns `Ok(None)` if the file is not a trampoline executable. /// Returns `Ok(None)` if the file is not a trampoline executable.
/// Returns `Err` if the file looks like a trampoline executable but is formatted incorrectly. /// Returns `Err` if the file looks like a trampoline executable but is formatted incorrectly.
#[allow(unused_variables)]
pub fn try_from_path(path: &Path) -> Result<Option<Self>, Error> {
#[cfg(not(windows))]
{
Err(Error::NotWindows)
}
#[cfg(windows)] #[cfg(windows)]
{ pub fn try_from_path(path: &Path) -> Result<Option<Self>, Error> {
use std::os::windows::ffi::OsStrExt; use std::os::windows::ffi::OsStrExt;
use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE; use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE;
use windows::Win32::System::LibraryLoader::LoadLibraryExW; use windows::Win32::System::LibraryLoader::LoadLibraryExW;
@ -98,8 +101,7 @@ impl Launcher {
return Ok(None); return Ok(None);
}; };
let python_path = PathBuf::from( let python_path = PathBuf::from(
String::from_utf8(path_data) String::from_utf8(path_data).map_err(|err| Error::InvalidPath(err.utf8_error()))?,
.map_err(|err| Error::InvalidPath(err.utf8_error()))?,
); );
let script_data = read_resource(module, RESOURCE_SCRIPT_DATA); let script_data = read_resource(module, RESOURCE_SCRIPT_DATA);
@ -120,16 +122,19 @@ impl Launcher {
result result
} }
}
#[allow(unused_variables)] /// Write this trampoline launcher to a file.
pub fn write_to_file(self, file_path: &Path, is_gui: bool) -> Result<(), Error> { ///
/// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific
/// feature and cannot be written on other platforms.
#[cfg(not(windows))] #[cfg(not(windows))]
{ pub fn write_to_file(self, _file_path: &Path, _is_gui: bool) -> Result<(), Error> {
Err(Error::NotWindows) Err(Error::NotWindows)
} }
/// Write this trampoline launcher to a file.
#[cfg(windows)] #[cfg(windows)]
{ pub fn write_to_file(self, file_path: &Path, is_gui: bool) -> Result<(), Error> {
use uv_fs::Simplified; use uv_fs::Simplified;
let python_path = self.python_path.simplified_display().to_string(); let python_path = self.python_path.simplified_display().to_string();
@ -154,7 +159,6 @@ impl Launcher {
Ok(()) Ok(())
} }
}
#[must_use] #[must_use]
pub fn with_python_path(self, path: PathBuf) -> Self { pub fn with_python_path(self, path: PathBuf) -> Self {
@ -251,7 +255,6 @@ fn get_launcher_bin(gui: bool) -> Result<&'static [u8], Error> {
} }
/// Helper to write Windows PE resources /// Helper to write Windows PE resources
#[allow(unused_variables)]
#[cfg(windows)] #[cfg(windows)]
fn write_resources(path: &Path, resources: &[(windows::core::PCWSTR, &[u8])]) -> Result<(), Error> { fn write_resources(path: &Path, resources: &[(windows::core::PCWSTR, &[u8])]) -> Result<(), Error> {
// SAFETY: winapi calls; null-terminated strings // SAFETY: winapi calls; null-terminated strings
@ -289,8 +292,8 @@ fn write_resources(path: &Path, resources: &[(windows::core::PCWSTR, &[u8])]) ->
Ok(()) Ok(())
} }
#[cfg(windows)]
/// Safely reads a resource from a PE file /// Safely reads a resource from a PE file
#[cfg(windows)]
fn read_resource( fn read_resource(
handle: windows::Win32::Foundation::HMODULE, handle: windows::Win32::Foundation::HMODULE,
name: windows::core::PCWSTR, name: windows::core::PCWSTR,
@ -327,24 +330,31 @@ fn read_resource(
} }
} }
/// Construct a Windows script launcher.
///
/// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific feature
/// and cannot be created on other platforms.
#[cfg(not(windows))]
pub fn windows_script_launcher(
_launcher_python_script: &str,
_is_gui: bool,
_python_executable: impl AsRef<Path>,
) -> Result<Vec<u8>, Error> {
Err(Error::NotWindows)
}
/// Construct a Windows script launcher.
///
/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as /// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as
/// stored zip file. /// stored zip file.
/// ///
/// <https://github.com/pypa/pip/blob/fd0ea6bc5e8cb95e518c23d901c26ca14db17f89/src/pip/_vendor/distlib/scripts.py#L248-L262> /// <https://github.com/pypa/pip/blob/fd0ea6bc5e8cb95e518c23d901c26ca14db17f89/src/pip/_vendor/distlib/scripts.py#L248-L262>
#[allow(unused_variables)] #[cfg(windows)]
pub fn windows_script_launcher( pub fn windows_script_launcher(
launcher_python_script: &str, launcher_python_script: &str,
is_gui: bool, is_gui: bool,
python_executable: impl AsRef<Path>, python_executable: impl AsRef<Path>,
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>, Error> {
// This method should only be called on Windows, but we avoid function-scope
// `#[cfg(windows)]` to retain compilation on all platforms.
#[cfg(not(windows))]
{
Err(Error::NotWindows)
}
#[cfg(windows)]
{
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use zip::ZipWriter; use zip::ZipWriter;
@ -397,25 +407,30 @@ pub fn windows_script_launcher(
fs_err::remove_file(temp_file)?; fs_err::remove_file(temp_file)?;
Ok(launcher) Ok(launcher)
}
} }
/// Construct a Windows Python launcher.
///
/// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific feature
/// and cannot be created on other platforms.
#[cfg(not(windows))]
pub fn windows_python_launcher(
_python_executable: impl AsRef<Path>,
_is_gui: bool,
) -> Result<Vec<u8>, Error> {
Err(Error::NotWindows)
}
/// Construct a Windows Python launcher.
///
/// A minimal .exe launcher binary for Python. /// A minimal .exe launcher binary for Python.
/// ///
/// Sort of equivalent to a `python` symlink on Unix. /// Sort of equivalent to a `python` symlink on Unix.
#[allow(unused_variables)] #[cfg(windows)]
pub fn windows_python_launcher( pub fn windows_python_launcher(
python_executable: impl AsRef<Path>, python_executable: impl AsRef<Path>,
is_gui: bool, is_gui: bool,
) -> Result<Vec<u8>, Error> { ) -> Result<Vec<u8>, Error> {
// This method should only be called on Windows, but we avoid function-scope
// `#[cfg(windows)]` to retain compilation on all platforms.
#[cfg(not(windows))]
{
Err(Error::NotWindows)
}
#[cfg(windows)]
{
use uv_fs::Simplified; use uv_fs::Simplified;
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?; let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
@ -445,7 +460,6 @@ pub fn windows_python_launcher(
fs_err::remove_file(temp_file)?; fs_err::remove_file(temp_file)?;
Ok(launcher) Ok(launcher)
}
} }
#[cfg(all(test, windows))] #[cfg(all(test, windows))]

View File

@ -53,6 +53,7 @@ uv-shell = { workspace = true }
uv-static = { workspace = true } uv-static = { workspace = true }
uv-tool = { workspace = true } uv-tool = { workspace = true }
uv-torch = { workspace = true } uv-torch = { workspace = true }
uv-trampoline-builder = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-version = { workspace = true } uv-version = { workspace = true }
uv-virtualenv = { workspace = true } uv-virtualenv = { workspace = true }
@ -107,13 +108,8 @@ walkdir = { workspace = true }
which = { workspace = true } which = { workspace = true }
zip = { workspace = true } zip = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
uv-trampoline-builder = { workspace = true }
arrayvec = { workspace = true } arrayvec = { workspace = true }
self-replace = { workspace = true } self-replace = { workspace = true }
windows = { workspace = true }
windows-result = { workspace = true }
[dev-dependencies] [dev-dependencies]
assert_cmd = { workspace = true } assert_cmd = { workspace = true }
@ -135,6 +131,10 @@ whoami = { workspace = true }
wiremock = { workspace = true } wiremock = { workspace = true }
zip = { workspace = true } zip = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true }
windows-result = { workspace = true }
[target.'cfg(unix)'.dependencies] [target.'cfg(unix)'.dependencies]
nix = { workspace = true } nix = { workspace = true }

View File

@ -29,6 +29,7 @@ use uv_python::{
PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest, PythonVersionFile, VersionFileDiscoveryOptions, VersionFilePreference, VersionRequest,
}; };
use uv_shell::Shell; use uv_shell::Shell;
use uv_trampoline_builder::{Launcher, LauncherKind};
use uv_warnings::{warn_user, write_error_chain}; use uv_warnings::{warn_user, write_error_chain};
use crate::commands::python::{ChangeEvent, ChangeEventKind}; use crate::commands::python::{ChangeEvent, ChangeEventKind};
@ -1050,32 +1051,22 @@ fn find_matching_bin_link<'a>(
mut installations: impl Iterator<Item = &'a ManagedPythonInstallation>, mut installations: impl Iterator<Item = &'a ManagedPythonInstallation>,
path: &Path, path: &Path,
) -> Option<&'a ManagedPythonInstallation> { ) -> Option<&'a ManagedPythonInstallation> {
#[cfg(not(any(unix, windows)))] if cfg!(unix) {
{
unreachable!("Only Windows and Unix are supported");
}
#[cfg(unix)]
{
if !path.is_symlink() { if !path.is_symlink() {
return None; return None;
} }
let target = fs_err::canonicalize(path).ok()?; let target = fs_err::canonicalize(path).ok()?;
installations.find(|installation| installation.executable(false) == target) installations.find(|installation| installation.executable(false) == target)
} } else if cfg!(windows) {
#[cfg(windows)] let launcher = Launcher::try_from_path(path).ok()??;
{
let target = {
use uv_trampoline_builder::{Launcher, LauncherKind};
let launcher: Launcher = Launcher::try_from_path(path).ok()??;
if !matches!(launcher.kind, LauncherKind::Python) { if !matches!(launcher.kind, LauncherKind::Python) {
return None; return None;
} }
dunce::canonicalize(launcher.python_path).ok()? let target = dunce::canonicalize(launcher.python_path).ok()?;
};
installations.find(|installation| installation.executable(false) == target) installations.find(|installation| installation.executable(false) == target)
} else {
unreachable!("Only Unix and Windows are supported")
} }
} }