11#![ allow( clippy:: disallowed_types) ]
22use std:: ffi:: { CString , c_void} ;
3- use std:: fs:: File ;
4- use std:: io:: { Read , Seek , SeekFrom } ;
5- use std:: mem:: size_of;
63use std:: path:: { Path , PathBuf } ;
74use std:: vec:: Vec ;
85
@@ -14,6 +11,7 @@ use windows::Win32::{
1411 System :: Console :: {
1512 GetStdHandle , STD_INPUT_HANDLE , STD_OUTPUT_HANDLE , SetConsoleCtrlHandler , SetStdHandle ,
1613 } ,
14+ System :: LibraryLoader :: { FindResourceA , LoadResource , LockResource , SizeofResource } ,
1715 System :: Environment :: GetCommandLineA ,
1816 System :: JobObjects :: {
1917 AssignProcessToJobObject , CreateJobObjectA , JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE ,
@@ -32,10 +30,14 @@ use windows::Win32::{
3230} ;
3331use windows:: core:: { BOOL , PSTR , s} ;
3432
33+ // TODO: see if we need object::pe after all. Maybe we can keep the nice const for readability.
34+ // use object::pe::RT_RCDATA;
35+
3536use crate :: { error, format, warn} ;
3637
37- const PATH_LEN_SIZE : usize = size_of :: < u32 > ( ) ;
38- const MAX_PATH_LEN : u32 = 32 * 1024 ;
38+ /// Resource IDs for the trampoline metadata
39+ const RESOURCE_TRAMPOLINE_TYPE : & str = "TRAMPOLINE_TYPE\0 " ;
40+ const RESOURCE_PYTHON_PATH : & str = "PYTHON_PATH\0 " ;
3941
4042/// The kind of trampoline.
4143enum TrampolineKind {
@@ -46,21 +48,33 @@ enum TrampolineKind {
4648}
4749
4850impl TrampolineKind {
49- const fn magic_number ( & self ) -> & ' static [ u8 ; 4 ] {
50- match self {
51- Self :: Script => b"UVSC" ,
52- Self :: Python => b"UVPY" ,
51+ fn from_resource ( data : & [ u8 ] ) -> Option < Self > {
52+ match data. get ( 0 ) {
53+ Some ( 1 ) => Some ( Self :: Script ) ,
54+ Some ( 2 ) => Some ( Self :: Python ) ,
55+ _ => { None } ,
5356 }
5457 }
58+ }
5559
56- fn from_buffer ( buffer : & [ u8 ] ) -> Option < Self > {
57- if buffer. ends_with ( Self :: Script . magic_number ( ) ) {
58- Some ( Self :: Script )
59- } else if buffer. ends_with ( Self :: Python . magic_number ( ) ) {
60- Some ( Self :: Python )
61- } else {
62- None
63- }
60+ /// Safely loads a resource from the current module
61+ fn load_resource ( resource_id : & str ) -> Option < Vec < u8 > > {
62+ unsafe {
63+ // Find the resource
64+ let resource = FindResourceA (
65+ None ,
66+ windows:: core:: PCSTR ( resource_id. as_ptr ( ) as _ ) ,
67+ windows:: core:: PCSTR ( 10 as * const _ ) , // RT_RCDATA = 10
68+ ) . ok ( ) ?;
69+
70+ // Get resource size and data
71+ let size = SizeofResource ( None , resource) ;
72+ let data = LoadResource ( None , resource) . ok ( ) ;
73+
74+ let ptr = LockResource ( data?) as * const u8 ;
75+
76+ // Copy the resource data into a Vec
77+ Some ( std:: slice:: from_raw_parts ( ptr, size as usize ) . to_vec ( ) )
6478 }
6579}
6680
@@ -70,14 +84,34 @@ fn make_child_cmdline() -> CString {
7084 let executable_name = std:: env:: current_exe ( ) . unwrap_or_else ( |_| {
7185 error_and_exit ( "Failed to get executable name" ) ;
7286 } ) ;
73- let ( kind, python_exe) = read_trampoline_metadata ( executable_name. as_ref ( ) ) ;
87+
88+ // Load trampoline type
89+ let trampoline_type = load_resource ( RESOURCE_TRAMPOLINE_TYPE )
90+ . and_then ( |data| TrampolineKind :: from_resource ( & data) )
91+ . unwrap_or_else ( || error_and_exit ( "Failed to load trampoline type from resources" ) ) ;
92+
93+ // Load Python path
94+ let python_path = load_resource ( RESOURCE_PYTHON_PATH )
95+ . and_then ( |data| String :: from_utf8 ( data) . ok ( ) )
96+ . map ( PathBuf :: from)
97+ . unwrap_or_else ( || error_and_exit ( "Failed to load Python path from resources" ) ) ;
98+
99+ // Convert relative paths to absolute
100+ let python_exe = if python_path. is_absolute ( ) {
101+ python_path
102+ } else {
103+ executable_name
104+ . parent ( )
105+ . unwrap_or_else ( || error_and_exit ( "Executable path has no parent directory" ) )
106+ . join ( python_path)
107+ } ;
108+
74109 let mut child_cmdline = Vec :: < u8 > :: new ( ) ;
75-
76110 push_quoted_path ( python_exe. as_ref ( ) , & mut child_cmdline) ;
77111 child_cmdline. push ( b' ' ) ;
78112
79113 // Only execute the trampoline again if it's a script, otherwise, just invoke Python.
80- match kind {
114+ match trampoline_type {
81115 TrampolineKind :: Python => {
82116 // SAFETY: `std::env::set_var` is safe to call on Windows, and
83117 // this code only ever runs on Windows.
@@ -145,6 +179,7 @@ fn push_quoted_path(path: &Path, command: &mut Vec<u8>) {
145179 command. extend ( br#"""# ) ;
146180}
147181
182+
148183/// Checks if the given executable is part of a virtual environment
149184///
150185/// Checks if a `pyvenv.cfg` file exists in grandparent directory of the given executable.
@@ -159,143 +194,6 @@ fn is_virtualenv(executable: &Path) -> bool {
159194 . unwrap_or ( false )
160195}
161196
162- /// Reads the executable binary from the back to find:
163- ///
164- /// * The path to the Python executable
165- /// * The kind of trampoline we are executing
166- ///
167- /// The executable is expected to have the following format:
168- ///
169- /// * The file must end with the magic number 'UVPY' or 'UVSC' (identifying the trampoline kind)
170- /// * The last 4 bytes (little endian) are the length of the path to the Python executable.
171- /// * The path encoded as UTF-8 comes right before the length
172- ///
173- /// # Panics
174- ///
175- /// If there's any IO error, or the file does not conform to the specified format.
176- fn read_trampoline_metadata ( executable_name : & Path ) -> ( TrampolineKind , PathBuf ) {
177- let mut file_handle = File :: open ( executable_name) . unwrap_or_else ( |_| {
178- print_last_error_and_exit ( & format ! (
179- "Failed to open executable '{}'" ,
180- & * executable_name. to_string_lossy( ) ,
181- ) ) ;
182- } ) ;
183-
184- let metadata = executable_name. metadata ( ) . unwrap_or_else ( |_| {
185- print_last_error_and_exit ( & format ! (
186- "Failed to get the size of the executable '{}'" ,
187- & * executable_name. to_string_lossy( ) ,
188- ) ) ;
189- } ) ;
190- let file_size = metadata. len ( ) ;
191-
192- // Start with a size of 1024 bytes which should be enough for most paths but avoids reading the
193- // entire file.
194- let mut buffer: Vec < u8 > = Vec :: new ( ) ;
195- let mut bytes_to_read = 1024 . min ( u32:: try_from ( file_size) . unwrap_or ( u32:: MAX ) ) ;
196-
197- let mut kind;
198- let path: String = loop {
199- // SAFETY: Casting to usize is safe because we only support 64bit systems where usize is guaranteed to be larger than u32.
200- buffer. resize ( bytes_to_read as usize , 0 ) ;
201-
202- file_handle
203- . seek ( SeekFrom :: Start ( file_size - u64:: from ( bytes_to_read) ) )
204- . unwrap_or_else ( |_| {
205- print_last_error_and_exit ( "Failed to set the file pointer to the end of the file" ) ;
206- } ) ;
207-
208- // Pulls in core::fmt::{write, Write, getcount}
209- let read_bytes = file_handle. read ( & mut buffer) . unwrap_or_else ( |_| {
210- print_last_error_and_exit ( "Failed to read the executable file" ) ;
211- } ) ;
212-
213- // Truncate the buffer to the actual number of bytes read.
214- buffer. truncate ( read_bytes) ;
215-
216- let Some ( inner_kind) = TrampolineKind :: from_buffer ( & buffer) else {
217- error_and_exit (
218- "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?" ,
219- ) ;
220- } ;
221- kind = inner_kind;
222-
223- // Remove the magic number
224- buffer. truncate ( buffer. len ( ) - kind. magic_number ( ) . len ( ) ) ;
225-
226- let path_len = match buffer. get ( buffer. len ( ) - PATH_LEN_SIZE ..) {
227- Some ( path_len) => {
228- let path_len = u32:: from_le_bytes ( path_len. try_into ( ) . unwrap_or_else ( |_| {
229- error_and_exit ( "Slice length is not equal to 4 bytes" ) ;
230- } ) ) ;
231-
232- if path_len > MAX_PATH_LEN {
233- error_and_exit ( & format ! (
234- "Only paths with a length up to 32KBs are supported but the python path has a length of {}" ,
235- path_len
236- ) ) ;
237- }
238-
239- // SAFETY: path len is guaranteed to be less than 32KBs
240- path_len as usize
241- }
242- None => {
243- error_and_exit (
244- "Python executable length missing. Did you write the length of the path to the Python executable before the Magic number?" ,
245- ) ;
246- }
247- } ;
248-
249- // Remove the path length
250- buffer. truncate ( buffer. len ( ) - PATH_LEN_SIZE ) ;
251-
252- if let Some ( path_offset) = buffer. len ( ) . checked_sub ( path_len) {
253- buffer. drain ( ..path_offset) ;
254-
255- break String :: from_utf8 ( buffer) . unwrap_or_else ( |_| {
256- error_and_exit ( "Python executable path is not a valid UTF-8 encoded path" ) ;
257- } ) ;
258- } else {
259- // SAFETY: Casting to u32 is safe because `path_len` is guaranteed to be less than 32KBs,
260- // MAGIC_NUMBER is 4 bytes and PATH_LEN_SIZE is 4 bytes.
261- bytes_to_read = ( path_len + kind. magic_number ( ) . len ( ) + PATH_LEN_SIZE ) as u32 ;
262-
263- if u64:: from ( bytes_to_read) > file_size {
264- error_and_exit (
265- "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" ,
266- ) ;
267- }
268- }
269- } ;
270-
271- let path = PathBuf :: from ( path) ;
272- let path = if path. is_absolute ( ) {
273- path
274- } else {
275- let parent_dir = match executable_name. parent ( ) {
276- Some ( parent) => parent,
277- None => {
278- error_and_exit ( "Executable path has no parent directory" ) ;
279- }
280- } ;
281- parent_dir. join ( path)
282- } ;
283-
284- let path = if !path. is_absolute ( ) || matches ! ( kind, TrampolineKind :: Script ) {
285- // NOTICE: dunce adds 5kb~
286- // TODO(john): In order to avoid resolving junctions and symlinks for relative paths and
287- // scripts, we can consider reverting https://github.com/astral-sh/uv/pull/5750/files#diff-969979506be03e89476feade2edebb4689a9c261f325988d3c7efc5e51de26d1L273-L277.
288- dunce:: canonicalize ( path. as_path ( ) ) . unwrap_or_else ( |_| {
289- error_and_exit ( "Failed to canonicalize script path" ) ;
290- } )
291- } else {
292- // For Python trampolines with absolute paths, we skip `dunce::canonicalize` to
293- // avoid resolving junctions.
294- path
295- } ;
296-
297- ( kind, path)
298- }
299197
300198fn push_arguments ( output : & mut Vec < u8 > ) {
301199 // SAFETY: We rely on `GetCommandLineA` to return a valid pointer to a null terminated string.
0 commit comments