From ee07f64b1724f061f3add5476815aea98a12dc8e Mon Sep 17 00:00:00 2001 From: Pavel Dikov Date: Sat, 16 Aug 2025 16:33:05 +0100 Subject: [PATCH] refactor(uv-trampoline,uv-trampoline-builder): address feedback --- .../src/metadata/requires_dist.rs | 6 +- crates/uv-trampoline-builder/src/lib.rs | 306 ++++++++++-------- crates/uv-trampoline/src/bounce.rs | 10 +- .../uv-trampoline-x86_64-console.exe | Bin 40448 -> 39936 bytes 4 files changed, 174 insertions(+), 148 deletions(-) diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index e2f342234..a5645c126 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -619,11 +619,11 @@ mod test { "#}; assert_snapshot!(format_err(input).await, @r#" - error: TOML parse error at line 8, column 16 + error: TOML parse error at line 8, column 28 | 8 | tqdm = { url = invalid url to tqdm-4.66.0-py3-none-any.whl" } - | ^ - missing opening quote, expected `"` + | ^ + missing comma between key-value pairs, expected `,` "#); } diff --git a/crates/uv-trampoline-builder/src/lib.rs b/crates/uv-trampoline-builder/src/lib.rs index fa963bdd3..d89c17eb9 100644 --- a/crates/uv-trampoline-builder/src/lib.rs +++ b/crates/uv-trampoline-builder/src/lib.rs @@ -1,11 +1,8 @@ -use std::io::{self, Cursor, Write}; +use std::io; use std::path::{Path, PathBuf}; use std::str::Utf8Error; 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] = @@ -36,12 +33,15 @@ const LAUNCHER_AARCH64_CONSOLE: &[u8] = const RT_RCDATA: u16 = 10; // Resource IDs matching uv-trampoline -const RESOURCE_TRAMPOLINE_KIND: &str = "UV_TRAMPOLINE_KIND\0"; -const RESOURCE_PYTHON_PATH: &str = "UV_PYTHON_PATH\0"; +#[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. -const RESOURCE_SCRIPT_DATA: &str = "UV_SCRIPT_DATA\0"; +#[cfg(windows)] +const RESOURCE_SCRIPT_DATA: windows::core::PCWSTR = windows::core::w!("UV_SCRIPT_DATA"); #[derive(Debug)] pub struct Launcher { @@ -67,8 +67,11 @@ impl Launcher { use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_AS_DATAFILE; use windows::Win32::System::LibraryLoader::LoadLibraryExW; - let mut path_str = path.as_os_str().encode_wide().collect::>(); - path_str.push(0); + let path_str = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); // SAFETY: winapi call; null-terminated strings #[allow(unsafe_code)] @@ -119,29 +122,38 @@ impl Launcher { } } + #[allow(unused_variables)] pub fn write_to_file(self, file_path: &Path, is_gui: bool) -> Result<(), Error> { - let python_path = self.python_path.simplified_display().to_string(); - - // 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(not(windows))] + { + Err(Error::NotWindows) } + #[cfg(windows)] + { + use uv_fs::Simplified; + let python_path = self.python_path.simplified_display().to_string(); - Ok(()) + // 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)?; + } + + Ok(()) + } } #[must_use] @@ -166,6 +178,7 @@ pub enum LauncherKind { } impl LauncherKind { + #[cfg(windows)] fn to_resource_value(self) -> u8 { match self { Self::Script => 1, @@ -203,6 +216,7 @@ pub enum Error { } #[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"))] @@ -233,58 +247,54 @@ 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 #[allow(unused_variables)] -fn write_resources(path: &Path, resources: &[(&str, &[u8])]) -> Result<(), Error> { - #[cfg(not(windows))] - { - Err(Error::NotWindows) - } - #[cfg(windows)] - { - // SAFETY: winapi calls; null-terminated strings - #[allow(unsafe_code)] - unsafe { - use std::os::windows::ffi::OsStrExt; - use windows::Win32::System::LibraryLoader::{ - BeginUpdateResourceW, EndUpdateResourceW, UpdateResourceW, - }; +#[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 mut path_str = path.as_os_str().encode_wide().collect::>(); - path_str.push(0); - 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)))?; + let path_str = path + .as_os_str() + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + 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 { - let mut name_null_term = name.encode_utf16().collect::>(); - name_null_term.push(0); - UpdateResourceW( - handle, - windows::core::PCWSTR(RT_RCDATA as *const _), - windows::core::PCWSTR(name_null_term.as_ptr()), - 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)))?; + 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)))?; } - Ok(()) + EndUpdateResourceW(handle, false) + .map_err(|err| Error::Io(io::Error::from_raw_os_error(err.code().0)))?; } + + Ok(()) } #[cfg(windows)] /// Safely reads a resource from a PE file -fn read_resource(handle: windows::Win32::Foundation::HMODULE, name: &str) -> Option> { +fn read_resource( + handle: windows::Win32::Foundation::HMODULE, + name: windows::core::PCWSTR, +) -> Option> { // SAFETY: winapi calls; null-terminated strings; all pointers are checked. #[allow(unsafe_code)] unsafe { @@ -294,7 +304,7 @@ fn read_resource(handle: windows::Win32::Foundation::HMODULE, name: &str) -> Opt // Find the resource let resource = FindResourceW( Some(handle), - windows::core::PCWSTR(name.encode_utf16().collect::>().as_ptr()), + name, windows::core::PCWSTR(RT_RCDATA as *const _), ); if resource.is_invalid() { @@ -303,6 +313,9 @@ fn read_resource(handle: windows::Win32::Foundation::HMODULE, name: &str) -> Opt // 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() { @@ -324,57 +337,67 @@ pub fn windows_script_launcher( is_gui: bool, python_executable: impl AsRef, ) -> Result, 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); - } - - let launcher_bin: &[u8] = get_launcher_bin(is_gui)?; - - let mut payload: Vec = Vec::new(); + // This method should only be called on Windows, but we avoid function-scope + // `#[cfg(windows)]` to retain compilation on all platforms. + #[cfg(not(windows))] { - // 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 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); + Err(Error::NotWindows) } + #[cfg(windows)] + { + use std::io::{Cursor, Write}; - let python = python_executable.as_ref(); - let python_path = python.simplified_display().to_string(); + use zip::ZipWriter; + use zip::write::SimpleFileOptions; - // Start with base launcher binary - // Create temporary file for the launcher - let temp_dir = tempfile::tempdir()?; - let temp_file = temp_dir - .path() - .join(format!("uv-trampoline-{}.exe", std::process::id())); - fs_err::write(&temp_file, launcher_bin)?; + use uv_fs::Simplified; - // 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 launcher_bin: &[u8] = get_launcher_bin(is_gui)?; - // Read back the complete file - let launcher = fs_err::read(&temp_file)?; - fs_err::remove_file(temp_file)?; + let mut payload: Vec = Vec::new(); + { + // 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 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); + } - Ok(launcher) + let python = python_executable.as_ref(); + 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) + } } /// A minimal .exe launcher binary for Python. @@ -385,39 +408,44 @@ pub fn windows_python_launcher( python_executable: impl AsRef, is_gui: bool, ) -> Result, 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); + // 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; - 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_path = python.simplified_display().to_string(); + let python = python_executable.as_ref(); + let python_path = python.simplified_display().to_string(); - // Create temporary file for the launcher - let temp_dir = tempfile::tempdir()?; - let temp_file = temp_dir - .path() - .join(format!("uv-trampoline-{}.exe", std::process::id())); - fs_err::write(&temp_file, launcher_bin)?; + // 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)?; + // 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)?; + // Read back the complete file + let launcher = fs_err::read(&temp_file)?; + fs_err::remove_file(temp_file)?; - Ok(launcher) + Ok(launcher) + } } #[cfg(all(test, windows))] @@ -569,7 +597,7 @@ if __name__ == "__main__": /// Signs the given binary using `PowerShell`'s `Set-AuthenticodeSignature` with a temporary certificate. fn sign_authenticode(bin_path: impl AsRef) { - let temp_dir = tempfile::tempdir().expect("Failed to create temporary directory"); + let temp_dir = tempfile::TempDir::new().expect("Failed to create temporary directory"); let temp_pfx = create_temp_certificate(&temp_dir).expect("Failed to create self-signed certificate"); diff --git a/crates/uv-trampoline/src/bounce.rs b/crates/uv-trampoline/src/bounce.rs index 3e6866c6b..0046e174b 100644 --- a/crates/uv-trampoline/src/bounce.rs +++ b/crates/uv-trampoline/src/bounce.rs @@ -36,8 +36,8 @@ use crate::{error, format, warn}; const RT_RCDATA: u16 = 10; /// Resource IDs for the trampoline metadata -const RESOURCE_TRAMPOLINE_KIND: &str = "UV_TRAMPOLINE_KIND\0"; -const RESOURCE_PYTHON_PATH: &str = "UV_PYTHON_PATH\0"; +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 { @@ -58,15 +58,13 @@ impl TrampolineKind { } /// Safely loads a resource from the current module -fn load_resource(resource_id: &str) -> Option> { +fn load_resource(resource_id: windows::core::PCWSTR) -> Option> { // SAFETY: winapi calls; null-terminated strings; all pointers are checked. unsafe { - let mut resource_id_null_term = resource_id.encode_utf16().collect::>(); - resource_id_null_term.push(0); // Find the resource let resource = FindResourceW( None, - windows::core::PCWSTR(resource_id_null_term.as_ptr()), + resource_id, windows::core::PCWSTR(RT_RCDATA as *const _), ); if resource.is_invalid() { diff --git a/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe b/crates/uv-trampoline/trampolines/uv-trampoline-x86_64-console.exe index a8b1e1d8d14e6c99f48ef5090653b6a81e0b37fd..6463da4a21029e7a3c25146a2279f103a920d985 100755 GIT binary patch delta 8765 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 T(Xyp=3wzt|ZO_(?&kFwoM=|$d delta 9149 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