Skip to content

Commit e772c05

Browse files
committed
Lookup Python executable from binary
1 parent 1b1319b commit e772c05

File tree

8 files changed

+203
-80
lines changed

8 files changed

+203
-80
lines changed

crates/install-wheel-rs/src/wheel.rs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ use crate::{find_dist_info, Error};
3333
/// `#!/usr/bin/env python`
3434
pub const SHEBANG_PYTHON: &str = "#!/usr/bin/env python";
3535

36+
const LAUNCHER_MAGIC_NUMBER: [u8; 4] = [b'U', b'V', b'U', b'V'];
37+
3638
#[cfg(all(windows, target_arch = "x86_64"))]
3739
const LAUNCHER_X86_64_GUI: &[u8] =
3840
include_bytes!("../../uv-trampoline/trampolines/uv-trampoline-x86_64-gui.exe");
@@ -281,19 +283,7 @@ fn unpack_wheel_files<R: Read + Seek>(
281283
}
282284

283285
fn get_shebang(location: &InstallLocation<impl AsRef<Path>>) -> String {
284-
let path = location.python().to_string_lossy().to_string();
285-
let path = if cfg!(windows) {
286-
// https://stackoverflow.com/a/50323079
287-
const VERBATIM_PREFIX: &str = r"\\?\";
288-
if let Some(stripped) = path.strip_prefix(VERBATIM_PREFIX) {
289-
stripped.to_string()
290-
} else {
291-
path
292-
}
293-
} else {
294-
path
295-
};
296-
format!("#!{path}")
286+
format!("#!{}", location.python().normalized().display())
297287
}
298288

299289
/// A Windows script is a minimal .exe launcher binary with the python entrypoint script appended as
@@ -305,6 +295,7 @@ fn get_shebang(location: &InstallLocation<impl AsRef<Path>>) -> String {
305295
pub(crate) fn windows_script_launcher(
306296
launcher_python_script: &str,
307297
is_gui: bool,
298+
installation: &InstallLocation<impl AsRef<Path>>,
308299
) -> Result<Vec<u8>, Error> {
309300
// This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain
310301
// compilation on all platforms.
@@ -352,9 +343,20 @@ pub(crate) fn windows_script_launcher(
352343
archive.finish().expect(error_msg);
353344
}
354345

346+
let python = installation.python();
347+
let python_path = python.normalized().to_string_lossy();
348+
355349
let mut launcher: Vec<u8> = Vec::with_capacity(launcher_bin.len() + payload.len());
356350
launcher.extend_from_slice(launcher_bin);
357351
launcher.extend_from_slice(&payload);
352+
launcher.extend_from_slice(python_path.as_bytes());
353+
launcher.extend_from_slice(
354+
&u32::try_from(python_path.as_bytes().len())
355+
.expect("File Path to be smaller than 4GB")
356+
.to_le_bytes(),
357+
);
358+
launcher.extend_from_slice(&LAUNCHER_MAGIC_NUMBER);
359+
358360
Ok(launcher)
359361
}
360362

@@ -393,7 +395,7 @@ pub(crate) fn write_script_entrypoints(
393395
write_file_recorded(
394396
site_packages,
395397
&entrypoint_relative,
396-
&windows_script_launcher(&launcher_python_script, is_gui)?,
398+
&windows_script_launcher(&launcher_python_script, is_gui, location)?,
397399
record,
398400
)?;
399401
} else {
@@ -949,7 +951,7 @@ pub fn parse_key_value_file(
949951
///
950952
/// Wheel 1.0: <https://www.python.org/dev/peps/pep-0427/>
951953
#[allow(clippy::too_many_arguments)]
952-
#[instrument(skip_all, fields(name = %filename.name))]
954+
#[instrument(skip_all, fields(name = % filename.name))]
953955
pub fn install_wheel(
954956
location: &InstallLocation<LockedDir>,
955957
reader: impl Read + Seek,

crates/uv-trampoline/Cargo.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-trampoline/README.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Windows trampolines
22

3-
This is a fork of [posy trampolines](https://github.com/njsmith/posy/tree/dda22e6f90f5fefa339b869dd2bbe107f5b48448/src/trampolines/windows-trampolines/posy-trampoline).
3+
This is a fork
4+
of [posy trampolines](https://github.com/njsmith/posy/tree/dda22e6f90f5fefa339b869dd2bbe107f5b48448/src/trampolines/windows-trampolines/posy-trampoline).
45

56
# What is this?
67

@@ -13,27 +14,26 @@ That's what this does: it's a generic "trampoline" that lets us generate custom
1314
`.exe`s for arbitrary Python scripts, and when invoked it bounces to invoking
1415
`python <the script>` instead.
1516

16-
1717
# How do you use it?
1818

1919
Basically, this looks up `python.exe` (for console programs) or
2020
`pythonw.exe` (for GUI programs) in the adjacent directory, and invokes
2121
`python[w].exe path\to\the\<the .exe>`.
2222

23-
The intended use is: take your Python script, name it `__main__.py`, and pack it
24-
into a `.zip` file. Then concatenate that `.zip` file onto the end of one of our
25-
prebuilt `.exe`s.
23+
The intended use is:
24+
25+
* take your Python script, name it `__main__.py`, and pack it
26+
into a `.zip` file. Then concatenate that `.zip` file onto the end of one of our
27+
prebuilt `.exe`s.
28+
* After the zip file content, write the path to the Python executable that the script uses to run
29+
the Python script as UTF-8 encoded string, followed by the path's length as a 32-bit little-endian
30+
integer.
31+
* At the very end, write the magic number `UVUV` in bytes.
2632

2733
Then when you run `python` on the `.exe`, it will see the `.zip` trailer at the
2834
end of the `.exe`, and automagically look inside to find and execute
2935
`__main__.py`. Easy-peasy.
3036

31-
(TODO: we should probably make the Python-finding logic slightly more flexible
32-
at some point -- in particular to support more conventional venv-style
33-
installation where you find `python` by looking in the directory next to the
34-
trampoline `.exe` -- but this is good enough to get started.)
35-
36-
3737
# Why does this exist?
3838

3939
I probably could have used Vinay's C++ implementation from `distlib`, but what's
@@ -47,7 +47,6 @@ Python-finding logic we want. But mostly it was just an interesting challenge.
4747
This does owe a *lot* to the `distlib` implementation though. The overall logic
4848
is copied more-or-less directly.
4949

50-
5150
# Anything I should know for hacking on this?
5251

5352
In order to minimize binary size, this uses `#![no_std]`, `panic="abort"`, and
@@ -64,7 +63,7 @@ this:
6463
Though uh, this does mean that literally all of our code is `unsafe`. Sorry!
6564

6665
- `runtime.rs` has the core glue to get panicking, heap allocation, and linking
67-
working.
66+
working.
6867

6968
- `diagnostics.rs` uses `ufmt` and some cute Windows tricks to get a convenient
7069
version of `eprintln!` that works without `std`, and automatically prints to
@@ -85,7 +84,6 @@ Miscellaneous tips:
8584
`.unwrap_unchecked()` avoids this. Similar for `slice[idx]` vs
8685
`slice.get_unchecked(idx)`.
8786

88-
8987
# How do you build this stupid thing?
9088

9189
Building this can be frustrating, because the low-level compiler/runtime
@@ -107,7 +105,6 @@ Two approaches that are reasonably likely to work:
107105
- Leave `compiler-builtins` commented-out, and build like: `cargo build
108106
--release -Z build-std=core,panic_abort,alloc -Z
109107
build-std-features=compiler-builtins-mem --target x86_64-pc-windows-msvc`
110-
111108

112109
Hopefully in the future as `#![no_std]` develops, this will get smoother.
113110

0 commit comments

Comments
 (0)