mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-18 19:21:46 +00:00
Use .rcdata to store trampoline type + path to python binary (#15068)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | windows python install manager (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 10 (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
zizmor / Run zizmor (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | aarch64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux libc (push) Blocked by required conditions
CI / build binary | linux aarch64 (push) Blocked by required conditions
CI / build binary | linux musl (push) Blocked by required conditions
CI / smoke test | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows x86_64 (push) Blocked by required conditions
CI / build binary | windows aarch64 (push) Blocked by required conditions
CI / build binary | msrv (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / ecosystem test | pydantic/pydantic-core (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / smoke test | linux aarch64 (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / smoke test | macos (push) Blocked by required conditions
CI / smoke test | windows x86_64 (push) Blocked by required conditions
CI / smoke test | windows aarch64 (push) Blocked by required conditions
CI / integration test | activate nushell venv (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | deadsnakes python3.9 on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / integration test | aarch64 windows implicit (push) Blocked by required conditions
CI / integration test | aarch64 windows explicit (push) Blocked by required conditions
CI / integration test | windows python install manager (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | pyodide on ubuntu (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | free-threaded python on github actions (push) Blocked by required conditions
CI / integration test | pyenv on wsl x86-64 (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | registries (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / integration test | uv_build (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on rocky linux 10 (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | graalpy on ubuntu (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | windows registry (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | x86-64 python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on macos x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86-64 (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows x86-64 (push) Blocked by required conditions
CI / check system | x86-64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | aarch64 python3.13 on windows aarch64 (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.8 on macos aarch64 (push) Blocked by required conditions
CI / check system | conda3.11 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on linux x86-64 (push) Blocked by required conditions
CI / check system | conda3.11 on windows x86-64 (push) Blocked by required conditions
CI / check system | conda3.8 on windows x86-64 (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows x86-64 (push) Blocked by required conditions
CI / benchmarks | walltime aarch64 linux (push) Blocked by required conditions
CI / benchmarks | instrumented (push) Blocked by required conditions
zizmor / Run zizmor (push) Waiting to run
`.rsrc` is the idiomatic way of storing metadata and non-code resources in PE binaries. This should make the resulting binaries more robust as they are no longer dependent on the exact location of a certain magic number. Addresses: #15022 ## Test Plan Existing integration test for `uv-trampoline-builder` + addition to ensure robustness to code signing. --------- Co-authored-by: samypr100 <3933065+samypr100@users.noreply.github.com> Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
parent
1b7faafd7a
commit
caf49f845f
16 changed files with 565 additions and 370 deletions
|
|
@ -10,7 +10,6 @@ use std::str::FromStr;
|
|||
|
||||
use fs_err as fs;
|
||||
use itertools::Itertools;
|
||||
use same_file::is_same_file;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, warn};
|
||||
use uv_preview::{Preview, PreviewFeatures};
|
||||
|
|
@ -22,7 +21,7 @@ use uv_platform::{Error as PlatformError, Os};
|
|||
use uv_platform::{LibcDetectionError, Platform};
|
||||
use uv_state::{StateBucket, StateStore};
|
||||
use uv_static::EnvVars;
|
||||
use uv_trampoline_builder::{Launcher, windows_python_launcher};
|
||||
use uv_trampoline_builder::{Launcher, LauncherKind};
|
||||
|
||||
use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
|
||||
use crate::implementation::{
|
||||
|
|
@ -649,12 +648,12 @@ impl ManagedPythonInstallation {
|
|||
/// [`create_bin_link`].
|
||||
pub fn is_bin_link(&self, path: &Path) -> bool {
|
||||
if cfg!(unix) {
|
||||
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) {
|
||||
let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
|
||||
return false;
|
||||
};
|
||||
if !matches!(launcher.kind, uv_trampoline_builder::LauncherKind::Python) {
|
||||
if !matches!(launcher.kind, LauncherKind::Python) {
|
||||
return false;
|
||||
}
|
||||
// We canonicalize the target path of the launcher in case it includes a minor version
|
||||
|
|
@ -922,6 +921,8 @@ pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), E
|
|||
}),
|
||||
}
|
||||
} else if cfg!(windows) {
|
||||
use uv_trampoline_builder::windows_python_launcher;
|
||||
|
||||
// TODO(zanieb): Install GUI launchers as well
|
||||
let launcher = windows_python_launcher(executable, false)?;
|
||||
|
||||
|
|
@ -938,7 +939,7 @@ pub fn create_link_to_executable(link: &Path, executable: &Path) -> Result<(), E
|
|||
})
|
||||
}
|
||||
} else {
|
||||
unimplemented!("Only Windows and Unix systems are supported.")
|
||||
unimplemented!("Only Windows and Unix are supported.")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,14 +23,18 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
uv-fs = { workspace = true }
|
||||
|
||||
fs-err = {workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = { workspace = true }
|
||||
assert_fs = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
rcgen = { workspace = true }
|
||||
which = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,12 +1,9 @@
|
|||
use std::io::{self, Cursor, Read, Seek, Write};
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use fs_err::File;
|
||||
use thiserror::Error;
|
||||
use uv_fs::Simplified;
|
||||
use zip::ZipWriter;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
#[cfg(all(windows, target_arch = "x86"))]
|
||||
const LAUNCHER_I686_GUI: &[u8] =
|
||||
|
|
@ -32,136 +29,150 @@ const LAUNCHER_AARCH64_GUI: &[u8] =
|
|||
const LAUNCHER_AARCH64_CONSOLE: &[u8] =
|
||||
include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-aarch64-console.exe");
|
||||
|
||||
// See `uv-trampoline::bounce`. These numbers must match.
|
||||
const PATH_LENGTH_SIZE: usize = size_of::<u32>();
|
||||
const MAX_PATH_LENGTH: u32 = 32 * 1024;
|
||||
const MAGIC_NUMBER_SIZE: usize = 4;
|
||||
// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
|
||||
#[cfg(windows)]
|
||||
const RT_RCDATA: u16 = 10;
|
||||
|
||||
// Resource IDs matching uv-trampoline
|
||||
#[cfg(windows)]
|
||||
const RESOURCE_TRAMPOLINE_KIND: windows::core::PCWSTR = windows::core::w!("UV_TRAMPOLINE_KIND");
|
||||
#[cfg(windows)]
|
||||
const RESOURCE_PYTHON_PATH: windows::core::PCWSTR = windows::core::w!("UV_PYTHON_PATH");
|
||||
// Note: This does not need to be looked up as a resource, as we rely on `zipimport`
|
||||
// to do the loading work. Still, keeping the content under a resource means that it
|
||||
// sits nicely under the PE format.
|
||||
#[cfg(windows)]
|
||||
const RESOURCE_SCRIPT_DATA: windows::core::PCWSTR = windows::core::w!("UV_SCRIPT_DATA");
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Launcher {
|
||||
pub kind: LauncherKind,
|
||||
pub python_path: PathBuf,
|
||||
payload: Vec<u8>,
|
||||
pub script_data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// Expects the following metadata to be at the end of the file:
|
||||
///
|
||||
/// ```text
|
||||
/// - file path (no greater than 32KB)
|
||||
/// - file path length (u32)
|
||||
/// - magic number(4 bytes)
|
||||
/// ```
|
||||
///
|
||||
/// This should only be used on Windows, but should just return `Ok(None)` on other platforms.
|
||||
///
|
||||
/// This is an implementation of [`uv-trampoline::bounce::read_trampoline_metadata`] that
|
||||
/// returns errors instead of panicking. Unlike the utility there, we don't assume that the
|
||||
/// file we are reading is a trampoline.
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
#[cfg(windows)]
|
||||
pub fn try_from_path(path: &Path) -> Result<Option<Self>, Error> {
|
||||
let mut file = File::open(path)?;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE;
|
||||
use windows::Win32::System::LibraryLoader::LoadLibraryExW;
|
||||
|
||||
// Read the magic number
|
||||
let Some(kind) = LauncherKind::try_from_file(&mut file)? else {
|
||||
let path_str = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// SAFETY: winapi call; null-terminated strings
|
||||
#[allow(unsafe_code)]
|
||||
let Some(module) = (unsafe {
|
||||
LoadLibraryExW(
|
||||
windows::core::PCWSTR(path_str.as_ptr()),
|
||||
None,
|
||||
LOAD_LIBRARY_AS_DATAFILE,
|
||||
)
|
||||
.ok()
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Seek to the start of the path length.
|
||||
let path_length_offset = (MAGIC_NUMBER_SIZE + PATH_LENGTH_SIZE) as i64;
|
||||
file.seek(io::SeekFrom::End(-path_length_offset))
|
||||
.map_err(|err| {
|
||||
Error::InvalidLauncherSeek("path length".to_string(), path_length_offset, err)
|
||||
})?;
|
||||
let result = (|| {
|
||||
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);
|
||||
};
|
||||
|
||||
// Read the path length
|
||||
let mut buffer = [0; PATH_LENGTH_SIZE];
|
||||
file.read_exact(&mut buffer)
|
||||
.map_err(|err| Error::InvalidLauncherRead("path length".to_string(), err))?;
|
||||
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 path_length = {
|
||||
let raw_length = u32::from_le_bytes(buffer);
|
||||
let script_data = read_resource(module, RESOURCE_SCRIPT_DATA);
|
||||
|
||||
if raw_length > MAX_PATH_LENGTH {
|
||||
return Err(Error::InvalidPathLength(raw_length));
|
||||
}
|
||||
Ok(Some(Self {
|
||||
kind,
|
||||
python_path,
|
||||
script_data,
|
||||
}))
|
||||
})();
|
||||
|
||||
// SAFETY: Above we guarantee the length is less than 32KB
|
||||
raw_length as usize
|
||||
// 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)))?;
|
||||
};
|
||||
|
||||
// Seek to the start of the path
|
||||
let path_offset = (MAGIC_NUMBER_SIZE + PATH_LENGTH_SIZE + path_length) as i64;
|
||||
file.seek(io::SeekFrom::End(-path_offset)).map_err(|err| {
|
||||
Error::InvalidLauncherSeek("executable path".to_string(), path_offset, err)
|
||||
})?;
|
||||
|
||||
// Read the path
|
||||
let mut buffer = vec![0u8; path_length];
|
||||
file.read_exact(&mut buffer)
|
||||
.map_err(|err| Error::InvalidLauncherRead("executable path".to_string(), err))?;
|
||||
|
||||
let path = PathBuf::from(
|
||||
String::from_utf8(buffer).map_err(|err| Error::InvalidPath(err.utf8_error()))?,
|
||||
);
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let file_size = {
|
||||
let raw_length = file
|
||||
.seek(io::SeekFrom::End(0))
|
||||
.map_err(|e| Error::InvalidLauncherSeek("size probe".into(), 0, e))?;
|
||||
|
||||
if raw_length > usize::MAX as u64 {
|
||||
return Err(Error::InvalidDataLength(raw_length));
|
||||
}
|
||||
|
||||
// SAFETY: Above we guarantee the length is less than uszie
|
||||
raw_length as usize
|
||||
};
|
||||
|
||||
// Read the payload
|
||||
file.seek(io::SeekFrom::Start(0))
|
||||
.map_err(|e| Error::InvalidLauncherSeek("rewind".into(), 0, e))?;
|
||||
let payload_len =
|
||||
file_size.saturating_sub(MAGIC_NUMBER_SIZE + PATH_LENGTH_SIZE + path_length);
|
||||
let mut buffer = vec![0u8; payload_len];
|
||||
file.read_exact(&mut buffer)
|
||||
.map_err(|err| Error::InvalidLauncherRead("payload".into(), err))?;
|
||||
|
||||
Ok(Some(Self {
|
||||
kind,
|
||||
payload: buffer,
|
||||
python_path: path,
|
||||
}))
|
||||
result
|
||||
}
|
||||
|
||||
pub fn write_to_file(self, file: &mut File) -> Result<(), Error> {
|
||||
/// 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: &mut File, _is_gui: bool) -> Result<(), Error> {
|
||||
Err(Error::NotWindows)
|
||||
}
|
||||
|
||||
/// Write this trampoline launcher to a file.
|
||||
#[cfg(windows)]
|
||||
pub fn write_to_file(self, file: &mut File, is_gui: bool) -> Result<(), Error> {
|
||||
use std::io::Write;
|
||||
use uv_fs::Simplified;
|
||||
|
||||
let python_path = self.python_path.simplified_display().to_string();
|
||||
|
||||
if python_path.len() > MAX_PATH_LENGTH as usize {
|
||||
return Err(Error::InvalidPathLength(
|
||||
u32::try_from(python_path.len()).expect("path length already checked"),
|
||||
));
|
||||
// Create temporary file for the base launcher
|
||||
let temp_dir = tempfile::TempDir::new()?;
|
||||
let temp_file = temp_dir
|
||||
.path()
|
||||
.join(format!("uv-trampoline-{}.exe", std::process::id()));
|
||||
|
||||
// Write the launcher binary
|
||||
fs_err::write(&temp_file, 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(&temp_file, &all_resources)?;
|
||||
} else {
|
||||
write_resources(&temp_file, resources)?;
|
||||
}
|
||||
|
||||
let mut launcher: Vec<u8> = Vec::with_capacity(
|
||||
self.payload.len() + python_path.len() + PATH_LENGTH_SIZE + MAGIC_NUMBER_SIZE,
|
||||
);
|
||||
launcher.extend_from_slice(&self.payload);
|
||||
launcher.extend_from_slice(python_path.as_bytes());
|
||||
launcher.extend_from_slice(
|
||||
&u32::try_from(python_path.len())
|
||||
.expect("file path should be smaller than 4GB")
|
||||
.to_le_bytes(),
|
||||
);
|
||||
launcher.extend_from_slice(self.kind.magic_number());
|
||||
// Read back the complete file
|
||||
let launcher = fs_err::read(&temp_file)?;
|
||||
fs_err::remove_file(&temp_file)?;
|
||||
|
||||
// Then write it to the handle
|
||||
file.write_all(&launcher)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -169,8 +180,8 @@ impl Launcher {
|
|||
pub fn with_python_path(self, path: PathBuf) -> Self {
|
||||
Self {
|
||||
kind: self.kind,
|
||||
payload: self.payload,
|
||||
python_path: path,
|
||||
script_data: self.script_data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -187,45 +198,21 @@ pub enum LauncherKind {
|
|||
}
|
||||
|
||||
impl LauncherKind {
|
||||
/// Return the magic number for this [`LauncherKind`].
|
||||
const fn magic_number(self) -> &'static [u8; 4] {
|
||||
#[cfg(windows)]
|
||||
fn to_resource_value(self) -> u8 {
|
||||
match self {
|
||||
Self::Script => b"UVSC",
|
||||
Self::Python => b"UVPY",
|
||||
Self::Script => 1,
|
||||
Self::Python => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a [`LauncherKind`] from 4 byte buffer.
|
||||
///
|
||||
/// If the buffer does not contain a matching magic number, `None` is returned.
|
||||
fn try_from_bytes(bytes: [u8; MAGIC_NUMBER_SIZE]) -> Option<Self> {
|
||||
if &bytes == Self::Script.magic_number() {
|
||||
return Some(Self::Script);
|
||||
#[cfg(windows)]
|
||||
fn from_resource_value(value: u8) -> Option<Self> {
|
||||
match value {
|
||||
1 => Some(Self::Script),
|
||||
2 => Some(Self::Python),
|
||||
_ => None,
|
||||
}
|
||||
if &bytes == Self::Python.magic_number() {
|
||||
return Some(Self::Python);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Read a [`LauncherKind`] from a file handle, based on the magic number.
|
||||
///
|
||||
/// This will mutate the file handle, seeking to the end of the file.
|
||||
///
|
||||
/// If the file cannot be read, an [`io::Error`] is returned. If the path is not a launcher,
|
||||
/// `None` is returned.
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
pub fn try_from_file(file: &mut File) -> Result<Option<Self>, Error> {
|
||||
// If the file is less than four bytes, it's not a launcher.
|
||||
let Ok(_) = file.seek(io::SeekFrom::End(-(MAGIC_NUMBER_SIZE as i64))) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut buffer = [0; MAGIC_NUMBER_SIZE];
|
||||
file.read_exact(&mut buffer)
|
||||
.map_err(|err| Error::InvalidLauncherRead("magic number".to_string(), err))?;
|
||||
|
||||
Ok(Self::try_from_bytes(buffer))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -234,25 +221,22 @@ impl LauncherKind {
|
|||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Only paths with a length up to 32KB are supported but found a length of {0} bytes")]
|
||||
InvalidPathLength(u32),
|
||||
#[error("Only data with a length up to usize is supported but found a length of {0} bytes")]
|
||||
InvalidDataLength(u64),
|
||||
#[error("Failed to parse executable path")]
|
||||
InvalidPath(#[source] Utf8Error),
|
||||
#[error("Failed to seek to {0} at offset {1}")]
|
||||
InvalidLauncherSeek(String, i64, #[source] io::Error),
|
||||
#[error("Failed to read launcher {0}")]
|
||||
InvalidLauncherRead(String, #[source] io::Error),
|
||||
#[error(
|
||||
"Unable to create Windows launcher for: {0} (only x86_64, x86, and arm64 are supported)"
|
||||
)]
|
||||
UnsupportedWindowsArch(&'static str),
|
||||
#[error("Unable to create Windows launcher on non-Windows platform")]
|
||||
NotWindows,
|
||||
#[error("Cannot process launcher metadata from resource")]
|
||||
UnprocessableMetadata,
|
||||
#[error("Resources over 2^32 bytes are not supported")]
|
||||
ResourceTooLarge,
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps, unused_variables)]
|
||||
#[cfg(windows)]
|
||||
fn get_launcher_bin(gui: bool) -> Result<&'static [u8], Error> {
|
||||
Ok(match std::env::consts::ARCH {
|
||||
#[cfg(all(windows, target_arch = "x86"))]
|
||||
|
|
@ -283,26 +267,116 @@ fn get_launcher_bin(gui: bool) -> Result<&'static [u8], Error> {
|
|||
arch => {
|
||||
return Err(Error::UnsupportedWindowsArch(arch));
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
_ => &[],
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper to write Windows PE resources
|
||||
#[cfg(windows)]
|
||||
fn write_resources(path: &Path, resources: &[(windows::core::PCWSTR, &[u8])]) -> Result<(), Error> {
|
||||
// SAFETY: winapi calls; null-terminated strings
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows::Win32::System::LibraryLoader::{
|
||||
BeginUpdateResourceW, EndUpdateResourceW, UpdateResourceW,
|
||||
};
|
||||
|
||||
let path_str = path
|
||||
.as_os_str()
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect::<Vec<_>>();
|
||||
let handle = BeginUpdateResourceW(windows::core::PCWSTR(path_str.as_ptr()), false)
|
||||
.map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?;
|
||||
|
||||
for (name, data) in resources {
|
||||
UpdateResourceW(
|
||||
handle,
|
||||
windows::core::PCWSTR(RT_RCDATA as *const _),
|
||||
*name,
|
||||
0,
|
||||
Some(data.as_ptr().cast()),
|
||||
u32::try_from(data.len()).map_err(|_| Error::ResourceTooLarge)?,
|
||||
)
|
||||
.map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?;
|
||||
}
|
||||
|
||||
EndUpdateResourceW(handle, false)
|
||||
.map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Safely reads a resource from a PE file
|
||||
#[cfg(windows)]
|
||||
fn read_resource(
|
||||
handle: windows::Win32::Foundation::HMODULE,
|
||||
name: windows::core::PCWSTR,
|
||||
) -> Option<Vec<u8>> {
|
||||
// SAFETY: winapi calls; null-terminated strings; all pointers are checked.
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
use windows::Win32::System::LibraryLoader::{
|
||||
FindResourceW, LoadResource, LockResource, SizeofResource,
|
||||
};
|
||||
// Find the resource
|
||||
let resource = FindResourceW(
|
||||
Some(handle),
|
||||
name,
|
||||
windows::core::PCWSTR(RT_RCDATA as *const _),
|
||||
);
|
||||
if resource.is_invalid() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get resource size and data
|
||||
let size = SizeofResource(Some(handle), resource);
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
let data = LoadResource(Some(handle), resource).ok()?;
|
||||
let ptr = LockResource(data) as *const u8;
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Copy the resource data into a Vec
|
||||
Some(std::slice::from_raw_parts(ptr, size as usize).to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// stored zip file.
|
||||
///
|
||||
/// <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(
|
||||
launcher_python_script: &str,
|
||||
is_gui: bool,
|
||||
python_executable: impl AsRef<Path>,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
// This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain
|
||||
// compilation on all platforms.
|
||||
if cfg!(not(windows)) {
|
||||
return Err(Error::NotWindows);
|
||||
}
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
use zip::ZipWriter;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
use uv_fs::Simplified;
|
||||
|
||||
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
|
||||
|
||||
|
|
@ -325,48 +399,84 @@ pub fn windows_script_launcher(
|
|||
let python = python_executable.as_ref();
|
||||
let python_path = python.simplified_display().to_string();
|
||||
|
||||
let mut launcher: Vec<u8> = Vec::with_capacity(launcher_bin.len() + payload.len());
|
||||
launcher.extend_from_slice(launcher_bin);
|
||||
launcher.extend_from_slice(&payload);
|
||||
launcher.extend_from_slice(python_path.as_bytes());
|
||||
launcher.extend_from_slice(
|
||||
&u32::try_from(python_path.len())
|
||||
.expect("file path should be smaller than 4GB")
|
||||
.to_le_bytes(),
|
||||
);
|
||||
launcher.extend_from_slice(LauncherKind::Script.magic_number());
|
||||
// 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
|
||||
// TODO(zanieb): It's weird that we write/read from a temporary file here because in the main
|
||||
// usage at `write_script_entrypoints` we do the same thing again. We should refactor these
|
||||
// to avoid repeated work.
|
||||
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.
|
||||
///
|
||||
/// Sort of equivalent to a `python` symlink on Unix.
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(windows)]
|
||||
pub fn windows_python_launcher(
|
||||
python_executable: impl AsRef<Path>,
|
||||
is_gui: bool,
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
// This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain
|
||||
// compilation on all platforms.
|
||||
if cfg!(not(windows)) {
|
||||
return Err(Error::NotWindows);
|
||||
}
|
||||
use uv_fs::Simplified;
|
||||
|
||||
let launcher_bin: &[u8] = get_launcher_bin(is_gui)?;
|
||||
|
||||
let python = python_executable.as_ref();
|
||||
let python_path = python.simplified_display().to_string();
|
||||
|
||||
let mut launcher: Vec<u8> = Vec::with_capacity(launcher_bin.len());
|
||||
launcher.extend_from_slice(launcher_bin);
|
||||
launcher.extend_from_slice(python_path.as_bytes());
|
||||
launcher.extend_from_slice(
|
||||
&u32::try_from(python_path.len())
|
||||
.expect("file path should be smaller than 4GB")
|
||||
.to_le_bytes(),
|
||||
);
|
||||
launcher.extend_from_slice(LauncherKind::Python.magic_number());
|
||||
// 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::Python.to_resource_value()][..],
|
||||
),
|
||||
(RESOURCE_PYTHON_PATH, python_path.as_bytes()),
|
||||
];
|
||||
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)
|
||||
}
|
||||
|
|
@ -376,6 +486,7 @@ pub fn windows_python_launcher(
|
|||
mod test {
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -486,6 +597,69 @@ if __name__ == "__main__":
|
|||
format!("#!{executable}")
|
||||
}
|
||||
|
||||
/// Creates a self-signed certificate and returns its path.
|
||||
fn create_temp_certificate(temp_dir: &tempfile::TempDir) -> Result<(PathBuf, PathBuf)> {
|
||||
use rcgen::{
|
||||
CertificateParams, DnType, ExtendedKeyUsagePurpose, KeyPair, KeyUsagePurpose, SanType,
|
||||
};
|
||||
|
||||
let mut params = CertificateParams::default();
|
||||
params.key_usages.push(KeyUsagePurpose::DigitalSignature);
|
||||
params
|
||||
.extended_key_usages
|
||||
.push(ExtendedKeyUsagePurpose::CodeSigning);
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::OrganizationName, "Astral Software Inc.");
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "uv-test-signer");
|
||||
params
|
||||
.subject_alt_names
|
||||
.push(SanType::DnsName("uv-test-signer".try_into()?));
|
||||
|
||||
let private_key = KeyPair::generate()?;
|
||||
let public_cert = params.self_signed(&private_key)?;
|
||||
|
||||
let public_cert_path = temp_dir.path().join("uv-trampoline-test.crt");
|
||||
let private_key_path = temp_dir.path().join("uv-trampoline-test.key");
|
||||
fs_err::write(public_cert_path.as_path(), public_cert.pem())?;
|
||||
fs_err::write(private_key_path.as_path(), private_key.serialize_pem())?;
|
||||
|
||||
Ok((public_cert_path, private_key_path))
|
||||
}
|
||||
|
||||
/// Signs the given binary using `PowerShell`'s `Set-AuthenticodeSignature` with a temporary certificate.
|
||||
fn sign_authenticode(bin_path: impl AsRef<Path>) {
|
||||
let temp_dir = tempfile::TempDir::new().expect("Failed to create temporary directory");
|
||||
let (public_cert, private_key) =
|
||||
create_temp_certificate(&temp_dir).expect("Failed to create self-signed certificate");
|
||||
|
||||
// Instead of powershell, we rely on pwsh which supports CreateFromPemFile.
|
||||
Command::new("pwsh")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-Command",
|
||||
&format!(
|
||||
r"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Import-Module Microsoft.PowerShell.Security
|
||||
$cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPemFile('{}', '{}')
|
||||
Set-AuthenticodeSignature -FilePath '{}' -Certificate $cert;
|
||||
",
|
||||
public_cert.display().to_string().replace('\'', "''"),
|
||||
private_key.display().to_string().replace('\'', "''"),
|
||||
bin_path.as_ref().display().to_string().replace('\'', "''"),
|
||||
),
|
||||
])
|
||||
.env_remove("PSModulePath")
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
println!("Signed binary: {}", bin_path.as_ref().display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn console_script_launcher() -> Result<()> {
|
||||
// Create Temp Dirs
|
||||
|
|
@ -540,6 +714,17 @@ if __name__ == "__main__":
|
|||
assert!(launcher.kind == LauncherKind::Script);
|
||||
assert!(launcher.python_path == python_executable_path);
|
||||
|
||||
// Now code-sign the launcher and verify that it still works.
|
||||
sign_authenticode(console_bin_path.path());
|
||||
|
||||
let stdout_predicate = "Hello from uv-trampoline-console.exe\r\n";
|
||||
let stderr_predicate = "Hello from uv-trampoline-console.exe\r\n";
|
||||
Command::new(console_bin_path.path())
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(stdout_predicate)
|
||||
.stderr(stderr_predicate);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -556,7 +741,9 @@ if __name__ == "__main__":
|
|||
let console_launcher = windows_python_launcher(&python_executable_path, false)?;
|
||||
|
||||
// Create Launcher
|
||||
File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?;
|
||||
{
|
||||
File::create(console_bin_path.path())?.write_all(console_launcher.as_ref())?;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Wrote Python Launcher in {}",
|
||||
|
|
@ -578,6 +765,15 @@ if __name__ == "__main__":
|
|||
assert!(launcher.kind == LauncherKind::Python);
|
||||
assert!(launcher.python_path == python_executable_path);
|
||||
|
||||
// Now code-sign the launcher and verify that it still works.
|
||||
sign_authenticode(console_bin_path.path());
|
||||
Command::new(console_bin_path.path())
|
||||
.arg("-c")
|
||||
.arg("print('Hello from Python Launcher')")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout("Hello from Python Launcher\r\n");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -600,7 +796,9 @@ if __name__ == "__main__":
|
|||
windows_script_launcher(&launcher_gui_script, true, &pythonw_executable_path)?;
|
||||
|
||||
// Create Launcher
|
||||
File::create(gui_bin_path.path())?.write_all(gui_launcher.as_ref())?;
|
||||
{
|
||||
File::create(gui_bin_path.path())?.write_all(gui_launcher.as_ref())?;
|
||||
}
|
||||
|
||||
println!("Wrote GUI Launcher in {}", gui_bin_path.path().display());
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ windows = { version = "0.61.0", features = [
|
|||
"Win32_System_Console",
|
||||
"Win32_System_Environment",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Threading",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ rustup target add --toolchain nightly-2025-06-23 aarch64-pc-windows-msvc
|
|||
Then, build the trampolines for all supported architectures:
|
||||
|
||||
```shell
|
||||
cargo +nightly-2025-06-23 xwin build --release --target i686-pc-windows-msvc
|
||||
cargo +nightly-2025-06-23 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc
|
||||
cargo +nightly-2025-06-23 xwin build --release --target x86_64-pc-windows-msvc
|
||||
cargo +nightly-2025-06-23 xwin build --release --target aarch64-pc-windows-msvc
|
||||
```
|
||||
|
|
@ -92,24 +92,16 @@ arbitrary Python scripts, and when invoked it bounces to invoking `python <the s
|
|||
Basically, this looks up `python.exe` (for console programs) and invokes
|
||||
`python.exe path\to\the\<the .exe>`.
|
||||
|
||||
The intended use is:
|
||||
It uses PE resources to store/load the information required to do this:
|
||||
|
||||
- take your Python script, name it `__main__.py`, and pack it into a `.zip` file. Then concatenate
|
||||
that `.zip` file onto the end of one of our prebuilt `.exe`s.
|
||||
- After the zip file content, write the path to the Python executable that the script uses to run
|
||||
the Python script as UTF-8 encoded string, followed by the path's length as a 32-bit little-endian
|
||||
integer.
|
||||
- At the very end, write the magic number `UVUV` in bytes.
|
||||
| Resource name | Contains |
|
||||
| :------------------------: | :-------------------------------------------------------: |
|
||||
| `RESOURCE_TRAMPOLINE_KIND` | `1` (script) or `2` (Python launcher) |
|
||||
| `RESOURCE_PYTHON_PATH` | Path to `python.exe` |
|
||||
| `RESOURCE_SCRIPT_DATA` | Zip file, containing a Python script called `__main__.py` |
|
||||
|
||||
| `launcher.exe` |
|
||||
| :-------------------------: |
|
||||
| `<zipped python script>` |
|
||||
| `<path to python.exe>` |
|
||||
| `<len(path to python.exe)>` |
|
||||
| `<b'U', b'V', b'U', b'V'>` |
|
||||
|
||||
Then when you run `python` on the `.exe`, it will see the `.zip` trailer at the end of the `.exe`,
|
||||
and automagically look inside to find and execute `__main__.py`. Easy-peasy.
|
||||
This works because when you run `python` on the `.exe`, the `zipimport` mechanism will see the
|
||||
embedded `.zip` file, and automagically look inside to find and execute `__main__.py`. Easy-peasy.
|
||||
|
||||
### Why does this exist?
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
#![allow(clippy::disallowed_types)]
|
||||
use std::ffi::{CString, c_void};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::mem::size_of;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::vec::Vec;
|
||||
|
||||
|
|
@ -20,6 +17,7 @@ use windows::Win32::{
|
|||
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
||||
JobObjectExtendedLimitInformation, QueryInformationJobObject, SetInformationJobObject,
|
||||
},
|
||||
System::LibraryLoader::{FindResourceW, LoadResource, LockResource, SizeofResource},
|
||||
System::Threading::{
|
||||
CreateProcessA, GetExitCodeProcess, GetStartupInfoA, INFINITE, PROCESS_CREATION_FLAGS,
|
||||
PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOA, WaitForInputIdle,
|
||||
|
|
@ -34,8 +32,12 @@ use windows::core::{BOOL, PSTR, s};
|
|||
|
||||
use crate::{error, format, warn};
|
||||
|
||||
const PATH_LEN_SIZE: usize = size_of::<u32>();
|
||||
const MAX_PATH_LEN: u32 = 32 * 1024;
|
||||
// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
|
||||
const RT_RCDATA: u16 = 10;
|
||||
|
||||
/// Resource IDs for the trampoline metadata
|
||||
const RESOURCE_TRAMPOLINE_KIND: windows::core::PCWSTR = windows::core::w!("UV_TRAMPOLINE_KIND");
|
||||
const RESOURCE_PYTHON_PATH: windows::core::PCWSTR = windows::core::w!("UV_PYTHON_PATH");
|
||||
|
||||
/// The kind of trampoline.
|
||||
enum TrampolineKind {
|
||||
|
|
@ -46,21 +48,42 @@ enum TrampolineKind {
|
|||
}
|
||||
|
||||
impl TrampolineKind {
|
||||
const fn magic_number(&self) -> &'static [u8; 4] {
|
||||
match self {
|
||||
Self::Script => b"UVSC",
|
||||
Self::Python => b"UVPY",
|
||||
fn from_resource(data: &[u8]) -> Option<Self> {
|
||||
match data.first() {
|
||||
Some(1) => Some(Self::Script),
|
||||
Some(2) => Some(Self::Python),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_buffer(buffer: &[u8]) -> Option<Self> {
|
||||
if buffer.ends_with(Self::Script.magic_number()) {
|
||||
Some(Self::Script)
|
||||
} else if buffer.ends_with(Self::Python.magic_number()) {
|
||||
Some(Self::Python)
|
||||
} else {
|
||||
None
|
||||
/// Safely loads a resource from the current module
|
||||
fn load_resource(resource_id: windows::core::PCWSTR) -> Option<Vec<u8>> {
|
||||
// SAFETY: winapi calls; null-terminated strings; all pointers are checked.
|
||||
unsafe {
|
||||
// Find the resource
|
||||
let resource = FindResourceW(
|
||||
None,
|
||||
resource_id,
|
||||
windows::core::PCWSTR(RT_RCDATA as *const _),
|
||||
);
|
||||
if resource.is_invalid() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Get resource size and data
|
||||
let size = SizeofResource(None, resource);
|
||||
if size == 0 {
|
||||
return None;
|
||||
}
|
||||
let data = LoadResource(None, resource).ok();
|
||||
let ptr = LockResource(data?) as *const u8;
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Copy the resource data into a Vec
|
||||
Some(std::slice::from_raw_parts(ptr, size as usize).to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,14 +93,50 @@ fn make_child_cmdline() -> CString {
|
|||
let executable_name = std::env::current_exe().unwrap_or_else(|_| {
|
||||
error_and_exit("Failed to get executable name");
|
||||
});
|
||||
let (kind, python_exe) = read_trampoline_metadata(executable_name.as_ref());
|
||||
let mut child_cmdline = Vec::<u8>::new();
|
||||
|
||||
// Load trampoline kind
|
||||
let trampoline_kind = load_resource(RESOURCE_TRAMPOLINE_KIND)
|
||||
.and_then(|data| TrampolineKind::from_resource(&data))
|
||||
.unwrap_or_else(|| error_and_exit("Failed to load trampoline kind from resources"));
|
||||
|
||||
// Load Python path
|
||||
let python_path = load_resource(RESOURCE_PYTHON_PATH)
|
||||
.and_then(|data| String::from_utf8(data).ok())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| error_and_exit("Failed to load Python path from resources"));
|
||||
|
||||
let python_exe = if python_path.is_absolute() {
|
||||
python_path
|
||||
} else {
|
||||
let parent_dir = match executable_name.parent() {
|
||||
Some(parent) => parent,
|
||||
None => {
|
||||
error_and_exit("Executable path has no parent directory");
|
||||
}
|
||||
};
|
||||
parent_dir.join(python_path)
|
||||
};
|
||||
|
||||
let python_exe =
|
||||
if !python_exe.is_absolute() || matches!(trampoline_kind, TrampolineKind::Script) {
|
||||
// NOTICE: dunce adds 5kb~
|
||||
// TODO(john): In order to avoid resolving junctions and symlinks for relative paths and
|
||||
// scripts, we can consider reverting https://github.com/astral-sh/uv/pull/5750/files#diff-969979506be03e89476feade2edebb4689a9c261f325988d3c7efc5e51de26d1L273-L277.
|
||||
dunce::canonicalize(python_exe.as_path()).unwrap_or_else(|_| {
|
||||
error_and_exit("Failed to canonicalize script path");
|
||||
})
|
||||
} else {
|
||||
// For Python trampolines with absolute paths, we skip `dunce::canonicalize` to
|
||||
// avoid resolving junctions.
|
||||
python_exe
|
||||
};
|
||||
|
||||
let mut child_cmdline = Vec::<u8>::new();
|
||||
push_quoted_path(python_exe.as_ref(), &mut child_cmdline);
|
||||
child_cmdline.push(b' ');
|
||||
|
||||
// Only execute the trampoline again if it's a script, otherwise, just invoke Python.
|
||||
match kind {
|
||||
match trampoline_kind {
|
||||
TrampolineKind::Python => {
|
||||
// SAFETY: `std::env::set_var` is safe to call on Windows, and
|
||||
// this code only ever runs on Windows.
|
||||
|
|
@ -159,144 +218,6 @@ fn is_virtualenv(executable: &Path) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Reads the executable binary from the back to find:
|
||||
///
|
||||
/// * The path to the Python executable
|
||||
/// * The kind of trampoline we are executing
|
||||
///
|
||||
/// The executable is expected to have the following format:
|
||||
///
|
||||
/// * The file must end with the magic number 'UVPY' or 'UVSC' (identifying the trampoline kind)
|
||||
/// * The last 4 bytes (little endian) are the length of the path to the Python executable.
|
||||
/// * The path encoded as UTF-8 comes right before the length
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If there's any IO error, or the file does not conform to the specified format.
|
||||
fn read_trampoline_metadata(executable_name: &Path) -> (TrampolineKind, PathBuf) {
|
||||
let mut file_handle = File::open(executable_name).unwrap_or_else(|_| {
|
||||
print_last_error_and_exit(&format!(
|
||||
"Failed to open executable '{}'",
|
||||
&*executable_name.to_string_lossy(),
|
||||
));
|
||||
});
|
||||
|
||||
let metadata = executable_name.metadata().unwrap_or_else(|_| {
|
||||
print_last_error_and_exit(&format!(
|
||||
"Failed to get the size of the executable '{}'",
|
||||
&*executable_name.to_string_lossy(),
|
||||
));
|
||||
});
|
||||
let file_size = metadata.len();
|
||||
|
||||
// Start with a size of 1024 bytes which should be enough for most paths but avoids reading the
|
||||
// entire file.
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
let mut bytes_to_read = 1024.min(u32::try_from(file_size).unwrap_or(u32::MAX));
|
||||
|
||||
let mut kind;
|
||||
let path: String = loop {
|
||||
// SAFETY: Casting to usize is safe because we only support 64bit systems where usize is guaranteed to be larger than u32.
|
||||
buffer.resize(bytes_to_read as usize, 0);
|
||||
|
||||
file_handle
|
||||
.seek(SeekFrom::Start(file_size - u64::from(bytes_to_read)))
|
||||
.unwrap_or_else(|_| {
|
||||
print_last_error_and_exit("Failed to set the file pointer to the end of the file");
|
||||
});
|
||||
|
||||
// Pulls in core::fmt::{write, Write, getcount}
|
||||
let read_bytes = file_handle.read(&mut buffer).unwrap_or_else(|_| {
|
||||
print_last_error_and_exit("Failed to read the executable file");
|
||||
});
|
||||
|
||||
// Truncate the buffer to the actual number of bytes read.
|
||||
buffer.truncate(read_bytes);
|
||||
|
||||
let Some(inner_kind) = TrampolineKind::from_buffer(&buffer) else {
|
||||
error_and_exit(
|
||||
"Magic number 'UVSC' or 'UVPY' not found at the end of the file. Did you append the magic number, the length and the path to the python executable at the end of the file?",
|
||||
);
|
||||
};
|
||||
kind = inner_kind;
|
||||
|
||||
// Remove the magic number
|
||||
buffer.truncate(buffer.len() - kind.magic_number().len());
|
||||
|
||||
let path_len = match buffer.get(buffer.len() - PATH_LEN_SIZE..) {
|
||||
Some(path_len) => {
|
||||
let path_len = u32::from_le_bytes(path_len.try_into().unwrap_or_else(|_| {
|
||||
error_and_exit("Slice length is not equal to 4 bytes");
|
||||
}));
|
||||
|
||||
if path_len > MAX_PATH_LEN {
|
||||
error_and_exit(&format!(
|
||||
"Only paths with a length up to 32KBs are supported but the python path has a length of {}",
|
||||
path_len
|
||||
));
|
||||
}
|
||||
|
||||
// SAFETY: path len is guaranteed to be less than 32KBs
|
||||
path_len as usize
|
||||
}
|
||||
None => {
|
||||
error_and_exit(
|
||||
"Python executable length missing. Did you write the length of the path to the Python executable before the Magic number?",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Remove the path length
|
||||
buffer.truncate(buffer.len() - PATH_LEN_SIZE);
|
||||
|
||||
if let Some(path_offset) = buffer.len().checked_sub(path_len) {
|
||||
buffer.drain(..path_offset);
|
||||
|
||||
break String::from_utf8(buffer).unwrap_or_else(|_| {
|
||||
error_and_exit("Python executable path is not a valid UTF-8 encoded path");
|
||||
});
|
||||
} else {
|
||||
// SAFETY: Casting to u32 is safe because `path_len` is guaranteed to be less than 32KBs,
|
||||
// MAGIC_NUMBER is 4 bytes and PATH_LEN_SIZE is 4 bytes.
|
||||
bytes_to_read = (path_len + kind.magic_number().len() + PATH_LEN_SIZE) as u32;
|
||||
|
||||
if u64::from(bytes_to_read) > file_size {
|
||||
error_and_exit(
|
||||
"The length of the python executable path exceeds the file size. Verify that the path length is appended to the end of the launcher script as a u32 in little endian",
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let path = PathBuf::from(path);
|
||||
let path = if path.is_absolute() {
|
||||
path
|
||||
} else {
|
||||
let parent_dir = match executable_name.parent() {
|
||||
Some(parent) => parent,
|
||||
None => {
|
||||
error_and_exit("Executable path has no parent directory");
|
||||
}
|
||||
};
|
||||
parent_dir.join(path)
|
||||
};
|
||||
|
||||
let path = if !path.is_absolute() || matches!(kind, TrampolineKind::Script) {
|
||||
// NOTICE: dunce adds 5kb~
|
||||
// TODO(john): In order to avoid resolving junctions and symlinks for relative paths and
|
||||
// scripts, we can consider reverting https://github.com/astral-sh/uv/pull/5750/files#diff-969979506be03e89476feade2edebb4689a9c261f325988d3c7efc5e51de26d1L273-L277.
|
||||
dunce::canonicalize(path.as_path()).unwrap_or_else(|_| {
|
||||
error_and_exit("Failed to canonicalize script path");
|
||||
})
|
||||
} else {
|
||||
// For Python trampolines with absolute paths, we skip `dunce::canonicalize` to
|
||||
// avoid resolving junctions.
|
||||
path
|
||||
};
|
||||
|
||||
(kind, path)
|
||||
}
|
||||
|
||||
fn push_arguments(output: &mut Vec<u8>) {
|
||||
// SAFETY: We rely on `GetCommandLineA` to return a valid pointer to a null terminated string.
|
||||
let arguments_as_str = unsafe { GetCommandLineA() };
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1982,7 +1982,7 @@ fn copy_entrypoint(
|
|||
.create_new(true)
|
||||
.write(true)
|
||||
.open(target)?;
|
||||
launcher.write_to_file(&mut file)?;
|
||||
launcher.write_to_file(&mut file, is_gui)?;
|
||||
|
||||
trace!("Updated entrypoint at {}", target.user_display());
|
||||
|
||||
|
|
|
|||
|
|
@ -1060,20 +1060,22 @@ fn find_matching_bin_link<'a>(
|
|||
mut installations: impl Iterator<Item = &'a ManagedPythonInstallation>,
|
||||
path: &Path,
|
||||
) -> Option<&'a ManagedPythonInstallation> {
|
||||
let target = if cfg!(unix) {
|
||||
if cfg!(unix) {
|
||||
if !path.is_symlink() {
|
||||
return None;
|
||||
}
|
||||
fs_err::canonicalize(path).ok()?
|
||||
let target = fs_err::canonicalize(path).ok()?;
|
||||
|
||||
installations.find(|installation| installation.executable(false) == target)
|
||||
} else if cfg!(windows) {
|
||||
let launcher = Launcher::try_from_path(path).ok()??;
|
||||
if !matches!(launcher.kind, LauncherKind::Python) {
|
||||
return None;
|
||||
}
|
||||
dunce::canonicalize(launcher.python_path).ok()?
|
||||
} else {
|
||||
unreachable!("Only Windows and Unix are supported")
|
||||
};
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue