![]() > NOTE: The PRs that were merged into this feature branch have all been independently reviewed. But it's also useful to see all of the changes in their final form. I've added comments to significant changes throughout the PR to aid discussion. This PR introduces transparent Python version upgrades to uv, allowing for a smoother experience when upgrading to new patch versions. Previously, upgrading Python patch versions required manual updates to each virtual environment. Now, virtual environments can transparently upgrade to newer patch versions. Due to significant changes in how uv installs and executes managed Python executables, this functionality is initially available behind a `--preview` flag. Once an installation has been made upgradeable through `--preview`, subsequent operations (like `uv venv -p 3.10` or patch upgrades) will work without requiring the flag again. This is accomplished by checking for the existence of a minor version symlink directory (or junction on Windows). ### Features * New `uv python upgrade` command to upgrade installed Python versions to the latest available patch release: ``` # Upgrade specific minor version uv python upgrade 3.12 --preview # Upgrade all installed minor versions uv python upgrade --preview ``` * Transparent upgrades also occur when installing newer patch versions: ``` uv python install 3.10.8 --preview # Automatically upgrades existing 3.10 environments uv python install 3.10.18 ``` * Support for transparently upgradeable Python `bin` installations via `--preview` flag ``` uv python install 3.13 --preview # Automatically upgrades the `bin` installation if there is a newer patch version available uv python upgrade 3.13 --preview ``` * Virtual environments can still be tied to a patch version if desired (ignoring patch upgrades): ``` uv venv -p 3.10.8 ``` ### Implementation Transparent upgrades are implemented using: * Minor version symlink directories (Unix) or junctions (Windows) * On Windows, trampolines simulate paths with junctions * Symlink directory naming follows Python build standalone format: e.g., `cpython-3.10-macos-aarch64-none` * Upgrades are scoped to the minor version key (as represented in the naming format: implementation-minor version+variant-os-arch-libc) * If the context does not provide a patch version request and the interpreter is from a managed CPython installation, the `Interpreter` used by `uv python run` will use the full symlink directory executable path when available, enabling transparently upgradeable environments created with the `venv` module (`uv run python -m venv`) New types: * `PythonMinorVersionLink`: in a sense, the core type for this PR, this is a representation of a minor version symlink directory (or junction on Windows) that points to the highest installed managed CPython patch version for a minor version key. * `PythonInstallationMinorVersionKey`: provides a view into a `PythonInstallationKey` that excludes the patch and prerelease. This is used for grouping installations by minor version key (e.g., to find the highest available patch installation for that minor version key) and for minor version directory naming. ### Compatibility * Supports virtual environments created with: * `uv venv` * `uv run python -m venv` (using managed Python that was installed or upgraded with `--preview`) * Virtual environments created within these environments * Existing virtual environments from before these changes continue to work but aren't transparently upgradeable without being recreated * Supports both standard Python (`python3.10`) and freethreaded Python (`python3.10t`) * Support for transparently upgrades is currently only available for managed CPython installations Closes #7287 Closes #7325 Closes #7892 Closes #9031 Closes #12977 --------- Co-authored-by: Zanie Blue <contact@zanie.dev> |
||
---|---|---|
.. | ||
.cargo | ||
src | ||
trampolines | ||
build.rs | ||
Cargo.lock | ||
Cargo.toml | ||
README.md | ||
rust-toolchain.toml |
Windows trampolines
This is a fork of posy trampolines.
Building
Cross-compiling from Linux
Install cargo xwin. Use your package manager to install
LLD and add the rustup
targets:
sudo apt install llvm clang lld
cargo install cargo-xwin
rustup toolchain install nightly-2025-02-16
rustup component add rust-src --toolchain nightly-2025-02-16-x86_64-unknown-linux-gnu
rustup target add --toolchain nightly-2025-02-16 i686-pc-windows-msvc
rustup target add --toolchain nightly-2025-02-16 x86_64-pc-windows-msvc
rustup target add --toolchain nightly-2025-02-16 aarch64-pc-windows-msvc
Then, build the trampolines for all supported architectures:
cargo +nightly-2025-02-16 xwin build --xwin-arch x86 --release --target i686-pc-windows-msvc
cargo +nightly-2025-02-16 xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly-2025-02-16 xwin build --release --target aarch64-pc-windows-msvc
Cross-compiling from macOS
Install cargo xwin. Use your package manager to install
LLVM and add the rustup
targets:
brew install llvm
cargo install cargo-xwin
rustup toolchain install nightly-2025-02-16
rustup component add rust-src --toolchain nightly-2025-02-16-aarch64-apple-darwin
rustup target add --toolchain nightly-2025-02-16 i686-pc-windows-msvc
rustup target add --toolchain nightly-2025-02-16 x86_64-pc-windows-msvc
rustup target add --toolchain nightly-2025-02-16 aarch64-pc-windows-msvc
Then, build the trampolines for all supported architectures:
cargo +nightly-2025-02-16 xwin build --release --target i686-pc-windows-msvc
cargo +nightly-2025-02-16 xwin build --release --target x86_64-pc-windows-msvc
cargo +nightly-2025-02-16 xwin build --release --target aarch64-pc-windows-msvc
Updating the prebuilt executables
After building the trampolines for all supported architectures:
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:
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 isunsafe
. Sorry! -
diagnostics.rs
usesufmt
and some cute Windows tricks to get a convenient version ofeprintln!
that works withoutcore::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 incore::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 theif foo.is_none() { panic!(...) }
that's hidden inside.unwrap()
will invokecore::fmt
, even if the unwrap will actually never fail..unwrap_unchecked()
avoids this. Similar forslice[idx]
vsslice.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