eee90a340c
## Summary Partially closes #1917 This PR picks up on some of the great work from #1864 and opted to keep `panic_immediate_abort` (for size reasons). I split the PR in different isolated commits in case we want to separate/cherry-pick them out. 1. The first commit ports mostly all std changes from that PR into this PR. Binary sizes stayed the same ~16kb. 2. The second commit migrates our existing usage of windows-sys to windows for a safer ffi calls with Results!. It also changes all large unsafe blocks to be isolated to the actual unsafe calls, and switches some areas to use std such as getenv port ( which seemed buggy! ) from launcher.c. In addition, this also adds more error checking in order to match some missing assertions from distlib's launcher.c. Note, due to the additional .text data, the binary sizes increased to ~20.5kb, but we can cut back on some of the added error msgs as needed. 3. The third commit switches to using xwin for building on all 3 supported trampoline targets for sanity, and adds a CI bloat check for core::fmt and panic as a precaution. Sadly, this will invalidate the xwin cache on the first run. ## Test Plan Most changes were tested on a couple of local GUI apps and console apps, also tested some of the error states manually by using SetLastError at different points in the code and/or passing in invalid handles. I'm not sure how far we can get with migrating some of the other calls without increasing binary size substantially. An initial attempt at using std::path didn't seem so bad size wise when I tried it (~1k). On other cases, such as std::process::exit added ~10k to the total binary size. --------- Co-authored-by: konstin <konstin@mailbox.org>
174 lines
6.9 KiB
Markdown
174 lines
6.9 KiB
Markdown
# Windows trampolines
|
|
|
|
This is a fork of [posy trampolines](https://github.com/njsmith/posy/tree/dda22e6f90f5fefa339b869dd2bbe107f5b48448/src/trampolines/windows-trampolines/posy-trampoline).
|
|
|
|
## Building
|
|
|
|
### Cross-compiling from Linux
|
|
|
|
Install [cargo xwin](https://github.com/rust-cross/cargo-xwin). Use your
|
|
package manager to install LLD and add the `rustup` targets:
|
|
|
|
```shell
|
|
sudo apt install llvm clang lld
|
|
rustup target add i686-pc-windows-msvc
|
|
rustup target add x86_64-pc-windows-msvc
|
|
rustup target add aarch64-pc-windows-msvc
|
|
```
|
|
|
|
Then, build the trampolines for both supported architectures:
|
|
|
|
```shell
|
|
cargo +nightly-2024-06-08 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc
|
|
cargo +nightly-2024-06-08 xwin build --release --target x86_64-pc-windows-msvc
|
|
cargo +nightly-2024-06-08 xwin build --release --target aarch64-pc-windows-msvc
|
|
```
|
|
|
|
### Cross-compiling from macOS
|
|
|
|
Install [cargo xwin](https://github.com/rust-cross/cargo-xwin). Use your
|
|
package manager to install LLVM and add the `rustup` targets:
|
|
|
|
```shell
|
|
brew install llvm
|
|
rustup target add i686-pc-windows-msvc
|
|
rustup target add x86_64-pc-windows-msvc
|
|
rustup target add aarch64-pc-windows-msvc
|
|
```
|
|
|
|
Then, build the trampolines for both supported architectures:
|
|
|
|
```shell
|
|
cargo +nightly-2024-06-08 xwin build --release --target i686-pc-windows-msvc
|
|
cargo +nightly-2024-06-08 xwin build --release --target x86_64-pc-windows-msvc
|
|
cargo +nightly-2024-06-08 xwin build --release --target aarch64-pc-windows-msvc
|
|
```
|
|
|
|
### Updating the prebuilt executables
|
|
|
|
After building the trampolines for both supported architectures:
|
|
|
|
```shell
|
|
cp target/aarch64-pc-windows-msvc/release/uv-trampoline-console.exe trampolines/uv-trampoline-aarch64-console.exe
|
|
cp target/aarch64-pc-windows-msvc/release/uv-trampoline-gui.exe trampolines/uv-trampoline-aarch64-gui.exe
|
|
cp target/x86_64-pc-windows-msvc/release/uv-trampoline-console.exe trampolines/uv-trampoline-x86_64-console.exe
|
|
cp target/x86_64-pc-windows-msvc/release/uv-trampoline-gui.exe trampolines/uv-trampoline-x86_64-gui.exe
|
|
cp target/i686-pc-windows-msvc/release/uv-trampoline-console.exe trampolines/uv-trampoline-i686-console.exe
|
|
cp target/i686-pc-windows-msvc/release/uv-trampoline-gui.exe trampolines/uv-trampoline-i686-gui.exe
|
|
```
|
|
|
|
### Testing the trampolines
|
|
|
|
To perform a basic smoke test of the trampolines, run the following commands on a Windows machine, from the root of the
|
|
repository:
|
|
|
|
```shell
|
|
cargo clean
|
|
cargo run venv
|
|
cargo run pip install black
|
|
.venv\Scripts\black --version
|
|
```
|
|
|
|
## Background
|
|
|
|
### What is this?
|
|
|
|
Sometimes you want to run a tool on Windows that's written in Python, like
|
|
`black` or `mypy` or `jupyter` or whatever. But, Windows does not know how to
|
|
run Python files! It knows how to run `.exe` files. So we need to somehow
|
|
convert our Python file a `.exe` file.
|
|
|
|
That's what this does: it's a generic "trampoline" that lets us generate custom
|
|
`.exe`s for arbitrary Python scripts, and when invoked it bounces to invoking
|
|
`python <the script>` instead.
|
|
|
|
### How do you use it?
|
|
|
|
Basically, this looks up `python.exe` (for console programs)
|
|
and invokes `python.exe path\to\the\<the .exe>`.
|
|
|
|
The intended use is:
|
|
|
|
* 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.
|
|
|
|
| `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.
|
|
|
|
### Why does this exist?
|
|
|
|
I probably could have used Vinay's C++ implementation from `distlib`, but what's
|
|
the fun in that? In particular, optimizing for binary size was entertaining
|
|
(these are ~7x smaller than the distlib, which doesn't matter much, but does a
|
|
little bit, considering that it gets added to every Python script). There are
|
|
also some minor advantages, like I think the Rust code is easier to understand
|
|
(multiple files!) and it's convenient to be able to straightforwardly code the
|
|
Python-finding logic we want. But mostly it was just an interesting challenge.
|
|
|
|
This does owe a *lot* to the `distlib` implementation though. The overall logic
|
|
is copied more-or-less directly.
|
|
|
|
### Anything I should know for hacking on this?
|
|
|
|
In order to minimize binary size, this uses, `panic="abort"`, and carefully
|
|
avoids using `core::fmt`. This removes a bunch of runtime overhead: by
|
|
default, Rust "hello world" on Windows is ~150 KB! So these binaries are ~10x
|
|
smaller.
|
|
|
|
Of course the tradeoff is that this is an awkward super-limited
|
|
environment. No C runtime and limited platform APIs... you don't
|
|
even panicking support by default. To work around this:
|
|
|
|
- We use `windows` to access Win32 APIs directly. Who needs a C runtime?
|
|
Though uh, this does mean that literally all of our code is `unsafe`. Sorry!
|
|
|
|
- `diagnostics.rs` uses `ufmt` and some cute Windows tricks to get a convenient
|
|
version of `eprintln!` that works without `core::fmt`, and automatically prints
|
|
to either the console if available or pops up a message box if not.
|
|
|
|
- All the meat is in `bounce.rs`.
|
|
|
|
Miscellaneous tips:
|
|
|
|
- `cargo-bloat` is a useful tool for checking what code is ending up in the
|
|
final binary and how much space it's taking. (It makes it very obvious whether
|
|
you've pulled in `core::fmt`!)
|
|
|
|
- Lots of Rust built-in panicking checks will pull in `core::fmt`, e.g., if you
|
|
ever use `.unwrap()` then suddenly our binaries double in size, because the
|
|
`if foo.is_none() { panic!(...) }` that's hidden inside `.unwrap()` will
|
|
invoke `core::fmt`, even if the unwrap will actually never fail.
|
|
`.unwrap_unchecked()` avoids this. Similar for `slice[idx]` vs
|
|
`slice.get_unchecked(idx)`.
|
|
|
|
### How do you build this stupid thing?
|
|
|
|
Building this can be frustrating, because the low-level compiler/runtime
|
|
machinery have a bunch of implicit assumptions about the environment they'll run
|
|
in, and the facilities it provides for things like `memcpy`, unwinding, etc.
|
|
So we need to replace the bits that we actually need, and which bits we need
|
|
can change depending on stuff like optimization options.
|
|
For example: we use `panic="abort"`, so we don't actually need unwinding support,
|
|
but at lower optimization levels the compiler might not realize that, and still
|
|
emit references to the unwinding helper`__CxxFrameHandler3`. And then the linker
|
|
blows up because that symbol doesn't exist.
|
|
|
|
```
|
|
cargo build --release --target i686-pc-windows-msvc
|
|
cargo build --release --target x86_64-pc-windows-msvc
|
|
cargo build --release --target aarch64-pc-windows-msvc
|
|
```
|