Skip to content

Commit db4ab9d

Browse files
authored
Install and remove managed Python to and from the Windows Registry (PEP 514) (#10634)
## Summary In preview mode on windows, register und un-register the managed python build standalone installations in the Windows registry following PEP 514. We write the values defined in the PEP plus the download URL and hash. We add an entry when installing a version, remove an entry when uninstalling and removing all values when uninstalling with `--all`. We update entries only by overwriting existing values, there is no "syncing" involved. Since they are not official builds, pbs gets a prefix. `py -V:Astral/CPython3.13.1` works, `py -3.13` doesn't. ``` $ py --list-paths -V:3.12 * C:\Users\Konsti\AppData\Local\Programs\Python\Python312\python.exe -V:3.11.9 C:\Users\Konsti\.pyenv\pyenv-win\versions\3.11.9\python.exe -V:3.11 C:\Users\micro\AppData\Local\Programs\Python\Python311\python.exe -V:3.8 C:\Users\micro\AppData\Local\Programs\Python\Python38\python.exe -V:Astral/CPython3.13.1 C:\Users\Konsti\AppData\Roaming\uv\data\python\cpython-3.13.1-windows-x86_64-none\python.exe ``` Registry errors are reported but not fatal, except for operations on the company key since it's not bound to any specific python interpreter. On uninstallation, we prune registry entries that have no matching Python installation (i.e. broken entries). The code uses the official `windows_registry` crate of the `winreg` crate. Best reviewed commit-by-commit. ## Test Plan We're reusing an existing system check to test different (un)installation scenarios.
1 parent a497176 commit db4ab9d

17 files changed

+831
-301
lines changed

.github/workflows/ci.yml

+18
Original file line numberDiff line numberDiff line change
@@ -1733,6 +1733,24 @@ jobs:
17331733
- name: "Validate global Python install"
17341734
run: py -3.13 ./scripts/check_system_python.py --uv ./uv.exe
17351735

1736+
# Test our PEP 514 integration that installs Python into the Windows registry.
1737+
system-test-windows-registry:
1738+
timeout-minutes: 10
1739+
needs: build-binary-windows-x86_64
1740+
name: "check system | windows registry"
1741+
runs-on: windows-latest
1742+
steps:
1743+
- uses: actions/checkout@v4
1744+
1745+
- name: "Download binary"
1746+
uses: actions/download-artifact@v4
1747+
with:
1748+
name: uv-windows-x86_64-${{ github.sha }}
1749+
1750+
# NB: Run this last, we are modifying the registry
1751+
- name: "Test PEP 514 registration"
1752+
run: python ./scripts/check_registry.py --uv ./uv.exe
1753+
17361754
system-test-choco:
17371755
timeout-minutes: 10
17381756
needs: build-binary-windows-x86_64

crates/uv-python/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ which = { workspace = true }
6464
procfs = { workspace = true }
6565

6666
[target.'cfg(target_os = "windows")'.dependencies]
67-
windows-sys = { workspace = true }
6867
windows-registry = { workspace = true }
6968
windows-result = { workspace = true }
69+
windows-sys = { workspace = true }
7070

7171
[dev-dependencies]
7272
anyhow = { version = "1.0.89" }

crates/uv-python/src/discovery.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ use crate::interpreter::{StatusCodeError, UnexpectedResponseError};
2424
use crate::managed::ManagedPythonInstallations;
2525
#[cfg(windows)]
2626
use crate::microsoft_store::find_microsoft_store_pythons;
27-
#[cfg(windows)]
28-
use crate::py_launcher::{registry_pythons, WindowsPython};
2927
use crate::virtualenv::Error as VirtualEnvError;
3028
use crate::virtualenv::{
3129
conda_environment_from_env, virtualenv_from_env, virtualenv_from_working_dir,
3230
virtualenv_python_executable, CondaEnvironmentKind,
3331
};
32+
#[cfg(windows)]
33+
use crate::windows_registry::{registry_pythons, WindowsPython};
3434
use crate::{Interpreter, PythonVersion};
3535

3636
/// A request to find a Python installation.
@@ -324,7 +324,7 @@ fn python_executables_from_installed<'a>(
324324
}
325325
})
326326
.inspect(|installation| debug!("Found managed installation `{installation}`"))
327-
.map(|installation| (PythonSource::Managed, installation.executable())))
327+
.map(|installation| (PythonSource::Managed, installation.executable(false))))
328328
})
329329
})
330330
.flatten_ok();

crates/uv-python/src/downloads.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ impl ManagedPythonDownload {
453453
.filter(|download| download.key.libc != Libc::Some(target_lexicon::Environment::Musl))
454454
}
455455

456-
pub fn url(&self) -> &str {
456+
pub fn url(&self) -> &'static str {
457457
self.url
458458
}
459459

@@ -465,7 +465,7 @@ impl ManagedPythonDownload {
465465
self.key.os()
466466
}
467467

468-
pub fn sha256(&self) -> Option<&str> {
468+
pub fn sha256(&self) -> Option<&'static str> {
469469
self.sha256
470470
}
471471

crates/uv-python/src/installation.rs

+8-3
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ impl PythonInstallation {
161161
DownloadResult::Fetched(path) => path,
162162
};
163163

164-
let installed = ManagedPythonInstallation::new(path)?;
164+
let installed = ManagedPythonInstallation::new(path, download);
165165
installed.ensure_externally_managed()?;
166166
installed.ensure_sysconfig_patched()?;
167167
installed.ensure_canonical_executables()?;
@@ -171,7 +171,7 @@ impl PythonInstallation {
171171

172172
Ok(Self {
173173
source: PythonSource::Managed,
174-
interpreter: Interpreter::query(installed.executable(), cache)?,
174+
interpreter: Interpreter::query(installed.executable(false), cache)?,
175175
})
176176
}
177177

@@ -282,7 +282,7 @@ impl PythonInstallationKey {
282282
}
283283
}
284284

285-
pub fn new_from_version(
285+
fn new_from_version(
286286
implementation: LenientImplementationName,
287287
version: &PythonVersion,
288288
os: Os,
@@ -320,6 +320,11 @@ impl PythonInstallationKey {
320320
.expect("Python installation keys must have valid Python versions")
321321
}
322322

323+
/// The version in `x.y.z` format.
324+
pub fn sys_version(&self) -> String {
325+
format!("{}.{}.{}", self.major, self.minor, self.patch)
326+
}
327+
323328
pub fn arch(&self) -> &Arch {
324329
&self.arch
325330
}

crates/uv-python/src/lib.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,18 @@ mod microsoft_store;
3737
pub mod platform;
3838
mod pointer_size;
3939
mod prefix;
40-
#[cfg(windows)]
41-
mod py_launcher;
4240
mod python_version;
4341
mod sysconfig;
4442
mod target;
4543
mod version_files;
4644
mod virtualenv;
45+
#[cfg(windows)]
46+
pub mod windows_registry;
47+
48+
#[cfg(windows)]
49+
pub(crate) const COMPANY_KEY: &str = "Astral";
50+
#[cfg(windows)]
51+
pub(crate) const COMPANY_DISPLAY_NAME: &str = "Astral Software Inc.";
4752

4853
#[cfg(not(test))]
4954
pub(crate) fn current_dir() -> Result<std::path::PathBuf, std::io::Error> {

crates/uv-python/src/macos_dylib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ impl Error {
5656
};
5757
warn_user!(
5858
"Failed to patch the install name of the dynamic library for {}. This may cause issues when building Python native extensions.{}",
59-
installation.executable().simplified_display(),
59+
installation.executable(false).simplified_display(),
6060
error
6161
);
6262
}

crates/uv-python/src/managed.rs

+47-11
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use uv_state::{StateBucket, StateStore};
1616
use uv_static::EnvVars;
1717
use uv_trampoline_builder::{windows_python_launcher, Launcher};
1818

19-
use crate::downloads::Error as DownloadError;
19+
use crate::downloads::{Error as DownloadError, ManagedPythonDownload};
2020
use crate::implementation::{
2121
Error as ImplementationError, ImplementationName, LenientImplementationName,
2222
};
@@ -229,7 +229,7 @@ impl ManagedPythonInstallations {
229229
.unwrap_or(true)
230230
})
231231
.filter_map(|path| {
232-
ManagedPythonInstallation::new(path)
232+
ManagedPythonInstallation::from_path(path)
233233
.inspect_err(|err| {
234234
warn!("Ignoring malformed managed Python entry:\n {err}");
235235
})
@@ -294,10 +294,27 @@ pub struct ManagedPythonInstallation {
294294
path: PathBuf,
295295
/// An install key for the Python version.
296296
key: PythonInstallationKey,
297+
/// The URL with the Python archive.
298+
///
299+
/// Empty when self was constructed from a path.
300+
url: Option<&'static str>,
301+
/// The SHA256 of the Python archive at the URL.
302+
///
303+
/// Empty when self was constructed from a path.
304+
sha256: Option<&'static str>,
297305
}
298306

299307
impl ManagedPythonInstallation {
300-
pub fn new(path: PathBuf) -> Result<Self, Error> {
308+
pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self {
309+
Self {
310+
path,
311+
key: download.key().clone(),
312+
url: Some(download.url()),
313+
sha256: download.sha256(),
314+
}
315+
}
316+
317+
pub(crate) fn from_path(path: PathBuf) -> Result<Self, Error> {
301318
let key = PythonInstallationKey::from_str(
302319
path.file_name()
303320
.ok_or(Error::NameError("name is empty".to_string()))?
@@ -307,15 +324,23 @@ impl ManagedPythonInstallation {
307324

308325
let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
309326

310-
Ok(Self { path, key })
327+
Ok(Self {
328+
path,
329+
key,
330+
url: None,
331+
sha256: None,
332+
})
311333
}
312334

313335
/// The path to this managed installation's Python executable.
314336
///
315337
/// If the installation has multiple execututables i.e., `python`, `python3`, etc., this will
316338
/// return the _canonical_ executable name which the other names link to. On Unix, this is
317339
/// `python{major}.{minor}{variant}` and on Windows, this is `python{exe}`.
318-
pub fn executable(&self) -> PathBuf {
340+
///
341+
/// If windowed is true, `pythonw.exe` is selected over `python.exe` on windows, with no changes
342+
/// on non-windows.
343+
pub fn executable(&self, windowed: bool) -> PathBuf {
319344
let implementation = match self.implementation() {
320345
ImplementationName::CPython => "python",
321346
ImplementationName::PyPy => "pypy",
@@ -342,6 +367,9 @@ impl ManagedPythonInstallation {
342367
// On Windows, the executable is just `python.exe` even for alternative variants
343368
let variant = if cfg!(unix) {
344369
self.key.variant.suffix()
370+
} else if cfg!(windows) && windowed {
371+
// Use windowed Python that doesn't open a terminal.
372+
"w"
345373
} else {
346374
""
347375
};
@@ -412,11 +440,11 @@ impl ManagedPythonInstallation {
412440

413441
pub fn satisfies(&self, request: &PythonRequest) -> bool {
414442
match request {
415-
PythonRequest::File(path) => self.executable() == *path,
443+
PythonRequest::File(path) => self.executable(false) == *path,
416444
PythonRequest::Default | PythonRequest::Any => true,
417445
PythonRequest::Directory(path) => self.path() == *path,
418446
PythonRequest::ExecutableName(name) => self
419-
.executable()
447+
.executable(false)
420448
.file_name()
421449
.is_some_and(|filename| filename.to_string_lossy() == *name),
422450
PythonRequest::Implementation(implementation) => {
@@ -432,7 +460,7 @@ impl ManagedPythonInstallation {
432460

433461
/// Ensure the environment contains the canonical Python executable names.
434462
pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
435-
let python = self.executable();
463+
let python = self.executable(false);
436464

437465
let canonical_names = &["python"];
438466

@@ -539,7 +567,7 @@ impl ManagedPythonInstallation {
539567
///
540568
/// If the file already exists at the target path, an error will be returned.
541569
pub fn create_bin_link(&self, target: &Path) -> Result<(), Error> {
542-
let python = self.executable();
570+
let python = self.executable(false);
543571

544572
let bin = target.parent().ok_or(Error::NoExecutableDirectory)?;
545573
fs_err::create_dir_all(bin).map_err(|err| Error::ExecutableDirectory {
@@ -585,15 +613,15 @@ impl ManagedPythonInstallation {
585613
/// [`ManagedPythonInstallation::create_bin_link`].
586614
pub fn is_bin_link(&self, path: &Path) -> bool {
587615
if cfg!(unix) {
588-
is_same_file(path, self.executable()).unwrap_or_default()
616+
is_same_file(path, self.executable(false)).unwrap_or_default()
589617
} else if cfg!(windows) {
590618
let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else {
591619
return false;
592620
};
593621
if !matches!(launcher.kind, uv_trampoline_builder::LauncherKind::Python) {
594622
return false;
595623
}
596-
launcher.python_path == self.executable()
624+
launcher.python_path == self.executable(false)
597625
} else {
598626
unreachable!("Only Windows and Unix are supported")
599627
}
@@ -627,6 +655,14 @@ impl ManagedPythonInstallation {
627655
// Do not upgrade if the patch versions are the same
628656
self.key.patch != other.key.patch
629657
}
658+
659+
pub fn url(&self) -> Option<&'static str> {
660+
self.url
661+
}
662+
663+
pub fn sha256(&self) -> Option<&'static str> {
664+
self.sha256
665+
}
630666
}
631667

632668
/// Generate a platform portion of a key from the environment.

crates/uv-python/src/microsoft_store.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//!
44
//! Effectively a port of <https://github.com/python/cpython/blob/58ce131037ecb34d506a613f21993cde2056f628/PC/launcher2.c#L1744>
55
6-
use crate::py_launcher::WindowsPython;
6+
use crate::windows_registry::WindowsPython;
77
use crate::PythonVersion;
88
use itertools::Either;
99
use std::env;

crates/uv-python/src/platform.rs

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ impl Arch {
102102
variant: None,
103103
}
104104
}
105+
106+
pub fn family(&self) -> target_lexicon::Architecture {
107+
self.family
108+
}
105109
}
106110

107111
impl Display for Libc {

0 commit comments

Comments
 (0)