mirror of https://github.com/astral-sh/uv
Review
This commit is contained in:
parent
ee07f64b17
commit
f2b69d292c
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}")]
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,109 +51,113 @@ 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)]
|
#[cfg(windows)]
|
||||||
pub fn try_from_path(path: &Path) -> Result<Option<Self>, Error> {
|
pub fn try_from_path(path: &Path) -> Result<Option<Self>, Error> {
|
||||||
#[cfg(not(windows))]
|
use std::os::windows::ffi::OsStrExt;
|
||||||
{
|
use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE;
|
||||||
Err(Error::NotWindows)
|
use windows::Win32::System::LibraryLoader::LoadLibraryExW;
|
||||||
}
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
use std::os::windows::ffi::OsStrExt;
|
|
||||||
use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE;
|
|
||||||
use windows::Win32::System::LibraryLoader::LoadLibraryExW;
|
|
||||||
|
|
||||||
let path_str = path
|
let path_str = path
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.encode_wide()
|
.encode_wide()
|
||||||
.chain(std::iter::once(0))
|
.chain(std::iter::once(0))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// SAFETY: winapi call; null-terminated strings
|
// SAFETY: winapi call; null-terminated strings
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
let Some(module) = (unsafe {
|
let Some(module) = (unsafe {
|
||||||
LoadLibraryExW(
|
LoadLibraryExW(
|
||||||
windows::core::PCWSTR(path_str.as_ptr()),
|
windows::core::PCWSTR(path_str.as_ptr()),
|
||||||
None,
|
None,
|
||||||
LOAD_LIBRARY_AS_DATAFILE,
|
LOAD_LIBRARY_AS_DATAFILE,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
}) else {
|
}) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = (|| {
|
||||||
|
let Some(kind_data) = read_resource(module, RESOURCE_TRAMPOLINE_KIND) else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
};
|
};
|
||||||
|
let Some(kind) = LauncherKind::from_resource_value(kind_data[0]) else {
|
||||||
let result = (|| {
|
return Err(Error::UnprocessableMetadata);
|
||||||
let Some(kind_data) = read_resource(module, RESOURCE_TRAMPOLINE_KIND) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let Some(kind) = LauncherKind::from_resource_value(kind_data[0]) else {
|
|
||||||
return Err(Error::UnprocessableMetadata);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(path_data) = read_resource(module, RESOURCE_PYTHON_PATH) else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let python_path = PathBuf::from(
|
|
||||||
String::from_utf8(path_data)
|
|
||||||
.map_err(|err| Error::InvalidPath(err.utf8_error()))?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let script_data = read_resource(module, RESOURCE_SCRIPT_DATA);
|
|
||||||
|
|
||||||
Ok(Some(Self {
|
|
||||||
kind,
|
|
||||||
python_path,
|
|
||||||
script_data,
|
|
||||||
}))
|
|
||||||
})();
|
|
||||||
|
|
||||||
// SAFETY: winapi call; handle is known to be valid.
|
|
||||||
#[allow(unsafe_code)]
|
|
||||||
unsafe {
|
|
||||||
windows::Win32::Foundation::FreeLibrary(module)
|
|
||||||
.map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
result
|
let Some(path_data) = read_resource(module, RESOURCE_PYTHON_PATH) else {
|
||||||
}
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let python_path = PathBuf::from(
|
||||||
|
String::from_utf8(path_data).map_err(|err| Error::InvalidPath(err.utf8_error()))?,
|
||||||
|
);
|
||||||
|
|
||||||
|
let script_data = read_resource(module, RESOURCE_SCRIPT_DATA);
|
||||||
|
|
||||||
|
Ok(Some(Self {
|
||||||
|
kind,
|
||||||
|
python_path,
|
||||||
|
script_data,
|
||||||
|
}))
|
||||||
|
})();
|
||||||
|
|
||||||
|
// SAFETY: winapi call; handle is known to be valid.
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
unsafe {
|
||||||
|
windows::Win32::Foundation::FreeLibrary(module)
|
||||||
|
.map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?;
|
||||||
|
};
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
/// Write this trampoline launcher to a file.
|
||||||
|
///
|
||||||
|
/// On Unix, this always returns [`Error::NotWindows`]. Trampolines are a Windows-specific
|
||||||
|
/// feature and cannot be written on other platforms.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
pub fn write_to_file(self, _file_path: &Path, _is_gui: bool) -> Result<(), Error> {
|
||||||
|
Err(Error::NotWindows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write this trampoline launcher to a file.
|
||||||
|
#[cfg(windows)]
|
||||||
pub fn write_to_file(self, file_path: &Path, is_gui: bool) -> Result<(), Error> {
|
pub fn write_to_file(self, file_path: &Path, is_gui: bool) -> Result<(), Error> {
|
||||||
#[cfg(not(windows))]
|
use uv_fs::Simplified;
|
||||||
{
|
let python_path = self.python_path.simplified_display().to_string();
|
||||||
Err(Error::NotWindows)
|
|
||||||
|
// Write the launcher binary
|
||||||
|
fs_err::write(file_path, get_launcher_bin(is_gui)?)?;
|
||||||
|
|
||||||
|
// Write resources
|
||||||
|
let resources = &[
|
||||||
|
(
|
||||||
|
RESOURCE_TRAMPOLINE_KIND,
|
||||||
|
&[self.kind.to_resource_value()][..],
|
||||||
|
),
|
||||||
|
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
||||||
|
];
|
||||||
|
if let Some(script_data) = self.script_data {
|
||||||
|
let mut all_resources = resources.to_vec();
|
||||||
|
all_resources.push((RESOURCE_SCRIPT_DATA, &script_data));
|
||||||
|
write_resources(file_path, &all_resources)?;
|
||||||
|
} else {
|
||||||
|
write_resources(file_path, resources)?;
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
use uv_fs::Simplified;
|
|
||||||
let python_path = self.python_path.simplified_display().to_string();
|
|
||||||
|
|
||||||
// Write the launcher binary
|
Ok(())
|
||||||
fs_err::write(file_path, get_launcher_bin(is_gui)?)?;
|
|
||||||
|
|
||||||
// Write resources
|
|
||||||
let resources = &[
|
|
||||||
(
|
|
||||||
RESOURCE_TRAMPOLINE_KIND,
|
|
||||||
&[self.kind.to_resource_value()][..],
|
|
||||||
),
|
|
||||||
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
|
||||||
];
|
|
||||||
if let Some(script_data) = self.script_data {
|
|
||||||
let mut all_resources = resources.to_vec();
|
|
||||||
all_resources.push((RESOURCE_SCRIPT_DATA, &script_data));
|
|
||||||
write_resources(file_path, &all_resources)?;
|
|
||||||
} else {
|
|
||||||
write_resources(file_path, resources)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
|
|
@ -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,125 +330,136 @@ 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
|
use std::io::{Cursor, Write};
|
||||||
// `#[cfg(windows)]` to retain compilation on all platforms.
|
|
||||||
#[cfg(not(windows))]
|
use zip::ZipWriter;
|
||||||
|
use zip::write::SimpleFileOptions;
|
||||||
|
|
||||||
|
use uv_fs::Simplified;
|
||||||
|
|
||||||
|
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
|
||||||
|
|
||||||
|
let mut payload: Vec<u8> = Vec::new();
|
||||||
{
|
{
|
||||||
Err(Error::NotWindows)
|
// We're using the zip writer, but with stored compression
|
||||||
|
// https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82
|
||||||
|
// https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271
|
||||||
|
let stored =
|
||||||
|
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
||||||
|
let mut archive = ZipWriter::new(Cursor::new(&mut payload));
|
||||||
|
let error_msg = "Writing to Vec<u8> should never fail";
|
||||||
|
archive.start_file("__main__.py", stored).expect(error_msg);
|
||||||
|
archive
|
||||||
|
.write_all(launcher_python_script.as_bytes())
|
||||||
|
.expect(error_msg);
|
||||||
|
archive.finish().expect(error_msg);
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
use std::io::{Cursor, Write};
|
|
||||||
|
|
||||||
use zip::ZipWriter;
|
let python = python_executable.as_ref();
|
||||||
use zip::write::SimpleFileOptions;
|
let python_path = python.simplified_display().to_string();
|
||||||
|
|
||||||
use uv_fs::Simplified;
|
// Start with base launcher binary
|
||||||
|
// Create temporary file for the launcher
|
||||||
|
let temp_dir = tempfile::TempDir::new()?;
|
||||||
|
let temp_file = temp_dir
|
||||||
|
.path()
|
||||||
|
.join(format!("uv-trampoline-{}.exe", std::process::id()));
|
||||||
|
fs_err::write(&temp_file, launcher_bin)?;
|
||||||
|
|
||||||
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
|
// Write resources
|
||||||
|
let resources = &[
|
||||||
|
(
|
||||||
|
RESOURCE_TRAMPOLINE_KIND,
|
||||||
|
&[LauncherKind::Script.to_resource_value()][..],
|
||||||
|
),
|
||||||
|
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
||||||
|
(RESOURCE_SCRIPT_DATA, &payload),
|
||||||
|
];
|
||||||
|
write_resources(&temp_file, resources)?;
|
||||||
|
|
||||||
let mut payload: Vec<u8> = Vec::new();
|
// Read back the complete file
|
||||||
{
|
let launcher = fs_err::read(&temp_file)?;
|
||||||
// We're using the zip writer, but with stored compression
|
fs_err::remove_file(temp_file)?;
|
||||||
// https://github.com/njsmith/posy/blob/04927e657ca97a5e35bb2252d168125de9a3a025/src/trampolines/mod.rs#L75-L82
|
|
||||||
// https://github.com/pypa/distlib/blob/8ed03aab48add854f377ce392efffb79bb4d6091/PC/launcher.c#L259-L271
|
|
||||||
let stored =
|
|
||||||
SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
|
|
||||||
let mut archive = ZipWriter::new(Cursor::new(&mut payload));
|
|
||||||
let error_msg = "Writing to Vec<u8> should never fail";
|
|
||||||
archive.start_file("__main__.py", stored).expect(error_msg);
|
|
||||||
archive
|
|
||||||
.write_all(launcher_python_script.as_bytes())
|
|
||||||
.expect(error_msg);
|
|
||||||
archive.finish().expect(error_msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
let python = python_executable.as_ref();
|
Ok(launcher)
|
||||||
let python_path = python.simplified_display().to_string();
|
|
||||||
|
|
||||||
// Start with base launcher binary
|
|
||||||
// Create temporary file for the launcher
|
|
||||||
let temp_dir = tempfile::TempDir::new()?;
|
|
||||||
let temp_file = temp_dir
|
|
||||||
.path()
|
|
||||||
.join(format!("uv-trampoline-{}.exe", std::process::id()));
|
|
||||||
fs_err::write(&temp_file, launcher_bin)?;
|
|
||||||
|
|
||||||
// Write resources
|
|
||||||
let resources = &[
|
|
||||||
(
|
|
||||||
RESOURCE_TRAMPOLINE_KIND,
|
|
||||||
&[LauncherKind::Script.to_resource_value()][..],
|
|
||||||
),
|
|
||||||
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
|
||||||
(RESOURCE_SCRIPT_DATA, &payload),
|
|
||||||
];
|
|
||||||
write_resources(&temp_file, resources)?;
|
|
||||||
|
|
||||||
// Read back the complete file
|
|
||||||
let launcher = fs_err::read(&temp_file)?;
|
|
||||||
fs_err::remove_file(temp_file)?;
|
|
||||||
|
|
||||||
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
|
use uv_fs::Simplified;
|
||||||
// `#[cfg(windows)]` to retain compilation on all platforms.
|
|
||||||
#[cfg(not(windows))]
|
|
||||||
{
|
|
||||||
Err(Error::NotWindows)
|
|
||||||
}
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
use uv_fs::Simplified;
|
|
||||||
|
|
||||||
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
|
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
|
||||||
|
|
||||||
let python = python_executable.as_ref();
|
let python = python_executable.as_ref();
|
||||||
let python_path = python.simplified_display().to_string();
|
let python_path = python.simplified_display().to_string();
|
||||||
|
|
||||||
// Create temporary file for the launcher
|
// Create temporary file for the launcher
|
||||||
let temp_dir = tempfile::TempDir::new()?;
|
let temp_dir = tempfile::TempDir::new()?;
|
||||||
let temp_file = temp_dir
|
let temp_file = temp_dir
|
||||||
.path()
|
.path()
|
||||||
.join(format!("uv-trampoline-{}.exe", std::process::id()));
|
.join(format!("uv-trampoline-{}.exe", std::process::id()));
|
||||||
fs_err::write(&temp_file, launcher_bin)?;
|
fs_err::write(&temp_file, launcher_bin)?;
|
||||||
|
|
||||||
// Write resources
|
// Write resources
|
||||||
let resources = &[
|
let resources = &[
|
||||||
(
|
(
|
||||||
RESOURCE_TRAMPOLINE_KIND,
|
RESOURCE_TRAMPOLINE_KIND,
|
||||||
&[LauncherKind::Python.to_resource_value()][..],
|
&[LauncherKind::Python.to_resource_value()][..],
|
||||||
),
|
),
|
||||||
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
||||||
];
|
];
|
||||||
write_resources(&temp_file, resources)?;
|
write_resources(&temp_file, resources)?;
|
||||||
|
|
||||||
// Read back the complete file
|
// Read back the complete file
|
||||||
let launcher = fs_err::read(&temp_file)?;
|
let launcher = fs_err::read(&temp_file)?;
|
||||||
fs_err::remove_file(temp_file)?;
|
fs_err::remove_file(temp_file)?;
|
||||||
|
|
||||||
Ok(launcher)
|
Ok(launcher)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(test, windows))]
|
#[cfg(all(test, windows))]
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()??;
|
||||||
{
|
if !matches!(launcher.kind, LauncherKind::Python) {
|
||||||
let target = {
|
return None;
|
||||||
use uv_trampoline_builder::{Launcher, LauncherKind};
|
}
|
||||||
|
let target = dunce::canonicalize(launcher.python_path).ok()?;
|
||||||
let launcher: Launcher = Launcher::try_from_path(path).ok()??;
|
|
||||||
if !matches!(launcher.kind, LauncherKind::Python) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue