Skip to content

Commit 03acd45

Browse files
committed
Add and remove managed Pythons from the Windows registry
1 parent 843ebb5 commit 03acd45

15 files changed

+396
-135
lines changed

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
@@ -23,13 +23,13 @@ use crate::interpreter::Error as InterpreterError;
2323
use crate::managed::ManagedPythonInstallations;
2424
#[cfg(windows)]
2525
use crate::microsoft_store::find_microsoft_store_pythons;
26-
#[cfg(windows)]
27-
use crate::py_launcher::{registry_pythons, WindowsPython};
2826
use crate::virtualenv::Error as VirtualEnvError;
2927
use crate::virtualenv::{
3028
conda_environment_from_env, virtualenv_from_env, virtualenv_from_working_dir,
3129
virtualenv_python_executable, CondaEnvironmentKind,
3230
};
31+
#[cfg(windows)]
32+
use crate::windows_registry::{registry_pythons, WindowsPython};
3333
use crate::{Interpreter, PythonVersion};
3434

3535
/// A request to find a Python installation.
@@ -323,7 +323,7 @@ fn python_executables_from_installed<'a>(
323323
}
324324
})
325325
.inspect(|installation| debug!("Found managed installation `{installation}`"))
326-
.map(|installation| (PythonSource::Managed, installation.executable())))
326+
.map(|installation| (PythonSource::Managed, installation.executable(false))))
327327
})
328328
})
329329
.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 {

crates/uv-python/src/py_launcher.rs

-98
This file was deleted.

0 commit comments

Comments
 (0)