Skip to content

Commit 9110f3c

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

File tree

8 files changed

+215
-86
lines changed

8 files changed

+215
-86
lines changed

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

Lines changed: 25 additions & 22 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");
@@ -235,7 +237,7 @@ fn unpack_wheel_files<R: Read + Seek>(
235237
record_path.with_extension("jws"),
236238
record_path.with_extension("p7s"),
237239
]
238-
.contains(&relative)
240+
.contains(&relative)
239241
{
240242
continue;
241243
}
@@ -281,18 +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-
};
286+
let path = location.python().normalized().to_string_lossy().to_string();
296287
format!("#!{path}")
297288
}
298289

@@ -305,6 +296,7 @@ fn get_shebang(location: &InstallLocation<impl AsRef<Path>>) -> String {
305296
pub(crate) fn windows_script_launcher(
306297
launcher_python_script: &str,
307298
is_gui: bool,
299+
installation: &InstallLocation<impl AsRef<Path>>,
308300
) -> Result<Vec<u8>, Error> {
309301
// This method should only be called on Windows, but we avoid `#[cfg(windows)]` to retain
310302
// compilation on all platforms.
@@ -352,9 +344,20 @@ pub(crate) fn windows_script_launcher(
352344
archive.finish().expect(error_msg);
353345
}
354346

347+
let python = installation.python();
348+
let python_path = python.normalized().to_string_lossy();
349+
355350
let mut launcher: Vec<u8> = Vec::with_capacity(launcher_bin.len() + payload.len());
356351
launcher.extend_from_slice(launcher_bin);
357352
launcher.extend_from_slice(&payload);
353+
launcher.extend_from_slice(python_path.as_bytes());
354+
launcher.extend_from_slice(
355+
&u32::try_from(python_path.as_bytes().len())
356+
.expect("File Path to be smaller than 4GB")
357+
.to_le_bytes(),
358+
);
359+
launcher.extend_from_slice(&LAUNCHER_MAGIC_NUMBER);
360+
358361
Ok(launcher)
359362
}
360363

@@ -393,7 +396,7 @@ pub(crate) fn write_script_entrypoints(
393396
write_file_recorded(
394397
site_packages,
395398
&entrypoint_relative,
396-
&windows_script_launcher(&launcher_python_script, is_gui)?,
399+
&windows_script_launcher(&launcher_python_script, is_gui, location)?,
397400
record,
398401
)?;
399402
} else {
@@ -949,7 +952,7 @@ pub fn parse_key_value_file(
949952
///
950953
/// Wheel 1.0: <https://www.python.org/dev/peps/pep-0427/>
951954
#[allow(clippy::too_many_arguments)]
952-
#[instrument(skip_all, fields(name = %filename.name))]
955+
#[instrument(skip_all, fields(name = % filename.name))]
953956
pub fn install_wheel(
954957
location: &InstallLocation<LockedDir>,
955958
reader: impl Read + Seek,
@@ -1224,8 +1227,8 @@ mod test {
12241227
"selenium-4.1.0.dist-info/METADATA",
12251228
"selenium-4.1.0.dist-info/RECORD",
12261229
]
1227-
.map(ToString::to_string)
1228-
.to_vec();
1230+
.map(ToString::to_string)
1231+
.to_vec();
12291232
let actual = entries
12301233
.into_iter()
12311234
.map(|entry| entry.path)
@@ -1240,23 +1243,23 @@ mod test {
12401243
Path::new("/home/ferris/carcinization/lib/python/site-packages/foo/__init__.py"),
12411244
Path::new("/home/ferris/carcinization/lib/python/site-packages"),
12421245
)
1243-
.unwrap(),
1246+
.unwrap(),
12441247
Path::new("foo/__init__.py")
12451248
);
12461249
assert_eq!(
12471250
relative_to(
12481251
Path::new("/home/ferris/carcinization/lib/marker.txt"),
12491252
Path::new("/home/ferris/carcinization/lib/python/site-packages"),
12501253
)
1251-
.unwrap(),
1254+
.unwrap(),
12521255
Path::new("../../marker.txt")
12531256
);
12541257
assert_eq!(
12551258
relative_to(
12561259
Path::new("/home/ferris/carcinization/bin/foo_launcher"),
12571260
Path::new("/home/ferris/carcinization/lib/python/site-packages"),
12581261
)
1259-
.unwrap(),
1262+
.unwrap(),
12601263
Path::new("../../../bin/foo_launcher")
12611264
);
12621265
}
@@ -1277,7 +1280,7 @@ mod test {
12771280
"foo.bar:main",
12781281
Some(&["bar".to_string(), "baz".to_string()]),
12791282
)
1280-
.unwrap(),
1283+
.unwrap(),
12811284
Some(Script {
12821285
script_name: "launcher".to_string(),
12831286
module: "foo.bar".to_string(),
@@ -1294,7 +1297,7 @@ mod test {
12941297
"foomod:main_bar [bar,baz]",
12951298
Some(&["bar".to_string(), "baz".to_string()]),
12961299
)
1297-
.unwrap(),
1300+
.unwrap(),
12981301
Some(Script {
12991302
script_name: "launcher".to_string(),
13001303
module: "foomod".to_string(),

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)