From ed056d168efd5cad17f6566c88aa23a7d079320d Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 14 Jan 2025 14:58:37 +0100 Subject: [PATCH 01/22] Refactoring: Move to function --- crates/uv/src/commands/python/install.rs | 372 ++++++++++++----------- 1 file changed, 202 insertions(+), 170 deletions(-) diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index e2eb24c93b07..57635068ae80 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use std::io::ErrorKind; use std::path::{Path, PathBuf}; -use anyhow::Result; +use anyhow::{Error, Result}; use futures::stream::FuturesUnordered; use futures::StreamExt; use itertools::{Either, Itertools}; @@ -293,7 +293,7 @@ pub(crate) async fn install( downloaded.push(installation); } Err(err) => { - errors.push((key, anyhow::Error::new(err))); + errors.push((key.clone(), anyhow::Error::new(err))); } } } @@ -326,174 +326,19 @@ pub(crate) async fn install( .expect("We should have a bin directory with preview enabled") .as_path(); - let targets = if (default || is_default_install) - && first_request.matches_installation(installation) - { - vec![ - installation.key().executable_name_minor(), - installation.key().executable_name_major(), - installation.key().executable_name(), - ] - } else { - vec![installation.key().executable_name_minor()] - }; - - for target in targets { - let target = bin.join(target); - match installation.create_bin_link(&target) { - Ok(()) => { - debug!( - "Installed executable at `{}` for {}", - target.simplified_display(), - installation.key(), - ); - changelog.installed.insert(installation.key().clone()); - changelog - .installed_executables - .entry(installation.key().clone()) - .or_default() - .insert(target.clone()); - } - Err(uv_python::managed::Error::LinkExecutable { from: _, to, err }) - if err.kind() == ErrorKind::AlreadyExists => - { - debug!( - "Inspecting existing executable at `{}`", - target.simplified_display() - ); - - // Figure out what installation it references, if any - let existing = find_matching_bin_link( - installations - .iter() - .copied() - .chain(existing_installations.iter()), - &target, - ); - - match existing { - None => { - // Determine if the link is valid, i.e., if it points to an existing - // Python we don't manage. On Windows, we just assume it is valid because - // symlinks are not common for Python interpreters. - let valid_link = cfg!(windows) - || target - .read_link() - .and_then(|target| target.try_exists()) - .inspect_err(|err| { - debug!("Failed to inspect executable with error: {err}"); - }) - // If we can't verify the link, assume it is valid. - .unwrap_or(true); - - // There's an existing executable we don't manage, require `--force` - if valid_link { - if !force { - errors.push(( - installation.key(), - anyhow::anyhow!( - "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it", - to.simplified_display() - ), - )); - continue; - } - debug!( - "Replacing existing executable at `{}` due to `--force`", - target.simplified_display() - ); - } else { - debug!( - "Replacing broken symlink at `{}`", - target.simplified_display() - ); - } - } - Some(existing) if existing == *installation => { - // The existing link points to the same installation, so we're done unless - // they requested we reinstall - if !(reinstall || force) { - debug!( - "Executable at `{}` is already for `{}`", - target.simplified_display(), - installation.key(), - ); - continue; - } - debug!( - "Replacing existing executable for `{}` at `{}`", - installation.key(), - target.simplified_display(), - ); - } - Some(existing) => { - // The existing link points to a different installation, check if it - // is reasonable to replace - if force { - debug!( - "Replacing existing executable for `{}` at `{}` with executable for `{}` due to `--force` flag", - existing.key(), - target.simplified_display(), - installation.key(), - ); - } else { - if installation.is_upgrade_of(existing) { - debug!( - "Replacing existing executable for `{}` at `{}` with executable for `{}` since it is an upgrade", - existing.key(), - target.simplified_display(), - installation.key(), - ); - } else if default { - debug!( - "Replacing existing executable for `{}` at `{}` with executable for `{}` since `--default` was requested`", - existing.key(), - target.simplified_display(), - installation.key(), - ); - } else { - debug!( - "Executable already exists for `{}` at `{}`. Use `--force` to replace it", - existing.key(), - to.simplified_display() - ); - continue; - } - } - } - } - - // Replace the existing link - fs_err::remove_file(&to)?; - - if let Some(existing) = existing { - // Ensure we do not report installation of this executable for an existing - // key if we undo it - changelog - .installed_executables - .entry(existing.key().clone()) - .or_default() - .remove(&target); - } - - installation.create_bin_link(&target)?; - debug!( - "Updated executable at `{}` to {}", - target.simplified_display(), - installation.key(), - ); - changelog.installed.insert(installation.key().clone()); - changelog - .installed_executables - .entry(installation.key().clone()) - .or_default() - .insert(target.clone()); - } - Err(err) => { - errors.push((installation.key(), anyhow::Error::new(err))); - } - } - } + create_bin_links( + installation, + bin, + reinstall, + force, + default, + is_default_install, + first_request, + &existing_installations, + &installations, + &mut changelog, + &mut errors, + )?; } if changelog.installed.is_empty() && errors.is_empty() { @@ -601,6 +446,193 @@ pub(crate) async fn install( Ok(ExitStatus::Success) } +/// Link the binaries of a managed Python installation to the bin directory. +#[allow(clippy::fn_params_excessive_bools)] +fn create_bin_links( + installation: &ManagedPythonInstallation, + bin: &Path, + reinstall: bool, + force: bool, + default: bool, + is_default_install: bool, + first_request: &InstallRequest, + existing_installations: &[ManagedPythonInstallation], + installations: &[&ManagedPythonInstallation], + changelog: &mut Changelog, + errors: &mut Vec<(PythonInstallationKey, Error)>, +) -> Result<(), Error> { + let targets = + if (default || is_default_install) && first_request.matches_installation(installation) { + vec![ + installation.key().executable_name_minor(), + installation.key().executable_name_major(), + installation.key().executable_name(), + ] + } else { + vec![installation.key().executable_name_minor()] + }; + + for target in targets { + let target = bin.join(target); + match installation.create_bin_link(&target) { + Ok(()) => { + debug!( + "Installed executable at `{}` for {}", + target.simplified_display(), + installation.key(), + ); + changelog.installed.insert(installation.key().clone()); + changelog + .installed_executables + .entry(installation.key().clone()) + .or_default() + .insert(target.clone()); + } + Err(uv_python::managed::Error::LinkExecutable { from: _, to, err }) + if err.kind() == ErrorKind::AlreadyExists => + { + debug!( + "Inspecting existing executable at `{}`", + target.simplified_display() + ); + + // Figure out what installation it references, if any + let existing = + find_matching_bin_link( + installations + .iter() + .copied() + .chain(existing_installations.iter()), + &target, + ) + ; + + match existing { + None => { + // Determine if the link is valid, i.e., if it points to an existing + // Python we don't manage. On Windows, we just assume it is valid because + // symlinks are not common for Python interpreters. + let valid_link = cfg!(windows) + || target + .read_link() + .and_then(|target| target.try_exists()) + .inspect_err(|err| { + debug!("Failed to inspect executable with error: {err}"); + }) + // If we can't verify the link, assume it is valid. + .unwrap_or(true); + + // There's an existing executable we don't manage, require `--force` + if valid_link { + if !force { + errors.push(( + installation.key().clone(), + anyhow::anyhow!( + "Executable already exists at `{}` but is not managed by uv; use `--force` to replace it", + to.simplified_display() + ), + )); + continue; + } + debug!( + "Replacing existing executable at `{}` due to `--force`", + target.simplified_display() + ); + } else { + debug!( + "Replacing broken symlink at `{}`", + target.simplified_display() + ); + } + } + Some(existing) if existing == installation => { + // The existing link points to the same installation, so we're done unless + // they requested we reinstall + if !(reinstall || force) { + debug!( + "Executable at `{}` is already for `{}`", + target.simplified_display(), + installation.key(), + ); + continue; + } + debug!( + "Replacing existing executable for `{}` at `{}`", + installation.key(), + target.simplified_display(), + ); + } + Some(existing) => { + // The existing link points to a different installation, check if it + // is reasonable to replace + if force { + debug!( + "Replacing existing executable for `{}` at `{}` with executable for `{}` due to `--force` flag", + existing.key(), + target.simplified_display(), + installation.key(), + ); + } else { + if installation.is_upgrade_of(existing) { + debug!( + "Replacing existing executable for `{}` at `{}` with executable for `{}` since it is an upgrade", + existing.key(), + target.simplified_display(), + installation.key(), + ); + } else if default { + debug!( + "Replacing existing executable for `{}` at `{}` with executable for `{}` since `--default` was requested`", + existing.key(), + target.simplified_display(), + installation.key(), + ); + } else { + debug!( + "Executable already exists for `{}` at `{}`. Use `--force` to replace it", + existing.key(), + to.simplified_display() + ); + continue; + } + } + } + } + + // Replace the existing link + fs_err::remove_file(&to)?; + + if let Some(existing) = existing { + // Ensure we do not report installation of this executable for an existing + // key if we undo it + changelog + .installed_executables + .entry(existing.key().clone()) + .or_default() + .remove(&target); + } + + installation.create_bin_link(&target)?; + debug!( + "Updated executable at `{}` to {}", + target.simplified_display(), + installation.key(), + ); + changelog.installed.insert(installation.key().clone()); + changelog + .installed_executables + .entry(installation.key().clone()) + .or_default() + .insert(target.clone()); + } + Err(err) => { + errors.push((installation.key().clone(), anyhow::Error::new(err))); + } + } + } + Ok(()) +} + pub(crate) fn format_executables( event: &ChangeEvent, executables: &FxHashMap>, From e46269a8205a8d1b11daaa6ac10ddee857191f72 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 15 Jan 2025 12:28:53 +0100 Subject: [PATCH 02/22] Rename `py_launcher` to `windows_registry` --- crates/uv-python/src/discovery.rs | 4 ++-- crates/uv-python/src/lib.rs | 4 ++-- crates/uv-python/src/microsoft_store.rs | 2 +- crates/uv-python/src/{py_launcher.rs => windows_registry.rs} | 2 ++ 4 files changed, 7 insertions(+), 5 deletions(-) rename crates/uv-python/src/{py_launcher.rs => windows_registry.rs} (98%) diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index d35e3b0df551..a831c5eb776f 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -23,13 +23,13 @@ use crate::interpreter::Error as InterpreterError; use crate::managed::ManagedPythonInstallations; #[cfg(windows)] use crate::microsoft_store::find_microsoft_store_pythons; -#[cfg(windows)] -use crate::py_launcher::{registry_pythons, WindowsPython}; use crate::virtualenv::Error as VirtualEnvError; use crate::virtualenv::{ conda_environment_from_env, virtualenv_from_env, virtualenv_from_working_dir, virtualenv_python_executable, CondaEnvironmentKind, }; +#[cfg(windows)] +use crate::windows_registry::{registry_pythons, WindowsPython}; use crate::{Interpreter, PythonVersion}; /// A request to find a Python installation. diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 0eb3cb36f7f6..5b9b8d92cd9a 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -37,13 +37,13 @@ mod microsoft_store; pub mod platform; mod pointer_size; mod prefix; -#[cfg(windows)] -mod py_launcher; mod python_version; mod sysconfig; mod target; mod version_files; mod virtualenv; +#[cfg(windows)] +mod windows_registry; #[cfg(not(test))] pub(crate) fn current_dir() -> Result { diff --git a/crates/uv-python/src/microsoft_store.rs b/crates/uv-python/src/microsoft_store.rs index c0226733c03d..3709f54f17e2 100644 --- a/crates/uv-python/src/microsoft_store.rs +++ b/crates/uv-python/src/microsoft_store.rs @@ -3,7 +3,7 @@ //! //! Effectively a port of -use crate::py_launcher::WindowsPython; +use crate::windows_registry::WindowsPython; use crate::PythonVersion; use itertools::Either; use std::env; diff --git a/crates/uv-python/src/py_launcher.rs b/crates/uv-python/src/windows_registry.rs similarity index 98% rename from crates/uv-python/src/py_launcher.rs rename to crates/uv-python/src/windows_registry.rs index af7482d19def..d322cf480a10 100644 --- a/crates/uv-python/src/py_launcher.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -1,3 +1,5 @@ +//! PEP 514 interactions with the Windows registry. + use crate::PythonVersion; use std::cmp::Ordering; use std::path::PathBuf; From 6d1bbded94d0175abd69f3f87632e6202eaf77f8 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 15 Jan 2025 10:24:49 +0100 Subject: [PATCH 03/22] Windows: Register managed pythons through PEP 514 --- Cargo.lock | 46 ++++++++++++ Cargo.toml | 1 + crates/uv-python/Cargo.toml | 1 + crates/uv-python/src/discovery.rs | 2 +- crates/uv-python/src/downloads.rs | 4 +- crates/uv-python/src/installation.rs | 11 ++- crates/uv-python/src/lib.rs | 4 +- crates/uv-python/src/managed.rs | 58 ++++++++++++--- crates/uv-python/src/platform.rs | 4 ++ crates/uv-python/src/windows_registry.rs | 90 +++++++++++++++++++++++- crates/uv/src/commands/python/install.rs | 21 ++++-- crates/uv/tests/it/common/mod.rs | 2 +- 12 files changed, 216 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2442f15c5ede..512f8f47790c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5447,6 +5447,7 @@ dependencies = [ "uv-trampoline-builder", "uv-warnings", "which", + "windows 0.59.0", "windows-registry 0.4.0", "windows-result 0.3.0", "windows-sys 0.59.0", @@ -6041,6 +6042,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" +dependencies = [ + "windows-core 0.59.0", + "windows-targets 0.53.0", +] + [[package]] name = "windows-core" version = "0.57.0" @@ -6066,6 +6077,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" +dependencies = [ + "windows-implement 0.59.0", + "windows-interface 0.59.0", + "windows-result 0.3.0", + "windows-strings 0.3.0", + "windows-targets 0.53.0", +] + [[package]] name = "windows-implement" version = "0.57.0" @@ -6088,6 +6112,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-implement" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.57.0" @@ -6110,6 +6145,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 5b139bc0f11f..78272cb2cc92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,6 +180,7 @@ urlencoding = { version = "2.1.3" } version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "648aa343486e5529953153781fc86025c73c4a61" } walkdir = { version = "2.5.0" } which = { version = "7.0.0", features = ["regex"] } +windows = { version = "0.59.0" } windows-registry = { version = "0.4.0" } windows-result = { version = "0.3.0" } windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO"] } diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index fcea7d5ecb60..a08fec83de09 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -59,6 +59,7 @@ tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } url = { workspace = true } which = { workspace = true } +windows = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] procfs = { workspace = true } diff --git a/crates/uv-python/src/discovery.rs b/crates/uv-python/src/discovery.rs index a831c5eb776f..5e40562f54de 100644 --- a/crates/uv-python/src/discovery.rs +++ b/crates/uv-python/src/discovery.rs @@ -323,7 +323,7 @@ fn python_executables_from_installed<'a>( } }) .inspect(|installation| debug!("Found managed installation `{installation}`")) - .map(|installation| (PythonSource::Managed, installation.executable()))) + .map(|installation| (PythonSource::Managed, installation.executable(false)))) }) }) .flatten_ok(); diff --git a/crates/uv-python/src/downloads.rs b/crates/uv-python/src/downloads.rs index b607149f66cc..54b20d6c8790 100644 --- a/crates/uv-python/src/downloads.rs +++ b/crates/uv-python/src/downloads.rs @@ -453,7 +453,7 @@ impl ManagedPythonDownload { .filter(|download| download.key.libc != Libc::Some(target_lexicon::Environment::Musl)) } - pub fn url(&self) -> &str { + pub fn url(&self) -> &'static str { self.url } @@ -465,7 +465,7 @@ impl ManagedPythonDownload { self.key.os() } - pub fn sha256(&self) -> Option<&str> { + pub fn sha256(&self) -> Option<&'static str> { self.sha256 } diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 12a32b0c6c09..90ccd955fbee 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -161,7 +161,7 @@ impl PythonInstallation { DownloadResult::Fetched(path) => path, }; - let installed = ManagedPythonInstallation::new(path)?; + let installed = ManagedPythonInstallation::new(path, download); installed.ensure_externally_managed()?; installed.ensure_sysconfig_patched()?; installed.ensure_canonical_executables()?; @@ -171,7 +171,7 @@ impl PythonInstallation { Ok(Self { source: PythonSource::Managed, - interpreter: Interpreter::query(installed.executable(), cache)?, + interpreter: Interpreter::query(installed.executable(false), cache)?, }) } @@ -282,7 +282,7 @@ impl PythonInstallationKey { } } - pub fn new_from_version( + fn new_from_version( implementation: LenientImplementationName, version: &PythonVersion, os: Os, @@ -320,6 +320,11 @@ impl PythonInstallationKey { .expect("Python installation keys must have valid Python versions") } + /// The version in `x.y.z` format. + pub fn sys_version(&self) -> String { + format!("{}.{}.{}", self.major, self.minor, self.patch,) + } + pub fn arch(&self) -> &Arch { &self.arch } diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 5b9b8d92cd9a..2769eeb6883f 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -43,7 +43,9 @@ mod target; mod version_files; mod virtualenv; #[cfg(windows)] -mod windows_registry; +pub mod windows_registry; + +pub(crate) const COMPANY: &str = "Astral"; #[cfg(not(test))] pub(crate) fn current_dir() -> Result { diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index 90f2e65de8be..0c8d513dd40d 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -16,7 +16,7 @@ use uv_state::{StateBucket, StateStore}; use uv_static::EnvVars; use uv_trampoline_builder::{windows_python_launcher, Launcher}; -use crate::downloads::Error as DownloadError; +use crate::downloads::{Error as DownloadError, ManagedPythonDownload}; use crate::implementation::{ Error as ImplementationError, ImplementationName, LenientImplementationName, }; @@ -229,7 +229,7 @@ impl ManagedPythonInstallations { .unwrap_or(true) }) .filter_map(|path| { - ManagedPythonInstallation::new(path) + ManagedPythonInstallation::from_path(path) .inspect_err(|err| { warn!("Ignoring malformed managed Python entry:\n {err}"); }) @@ -294,10 +294,27 @@ pub struct ManagedPythonInstallation { path: PathBuf, /// An install key for the Python version. key: PythonInstallationKey, + /// The URL with the Python archive. + /// + /// Empty when self was built from a path. + url: Option<&'static str>, + /// The SHA256 of the Python archive at the URL. + /// + /// Empty when self was built from a path. + sha256: Option<&'static str>, } impl ManagedPythonInstallation { - pub fn new(path: PathBuf) -> Result { + pub fn new(path: PathBuf, download: &ManagedPythonDownload) -> Self { + Self { + path, + key: download.key().clone(), + url: Some(download.url()), + sha256: download.sha256(), + } + } + + pub(crate) fn from_path(path: PathBuf) -> Result { let key = PythonInstallationKey::from_str( path.file_name() .ok_or(Error::NameError("name is empty".to_string()))? @@ -307,7 +324,12 @@ impl ManagedPythonInstallation { let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?; - Ok(Self { path, key }) + Ok(Self { + path, + key, + url: None, + sha256: None, + }) } /// The path to this managed installation's Python executable. @@ -315,7 +337,10 @@ impl ManagedPythonInstallation { /// If the installation has multiple execututables i.e., `python`, `python3`, etc., this will /// return the _canonical_ executable name which the other names link to. On Unix, this is /// `python{major}.{minor}{variant}` and on Windows, this is `python{exe}`. - pub fn executable(&self) -> PathBuf { + /// + /// If windowed is true, `pythonw.exe` is selected over `python.exe` on windows, with no changes + /// on non-windows. + pub fn executable(&self, windowed: bool) -> PathBuf { let implementation = match self.implementation() { ImplementationName::CPython => "python", ImplementationName::PyPy => "pypy", @@ -342,6 +367,9 @@ impl ManagedPythonInstallation { // On Windows, the executable is just `python.exe` even for alternative variants let variant = if cfg!(unix) { self.key.variant.suffix() + } else if cfg!(windows) && windowed { + // Use windowed Python that doesn't open a terminal. + "w" } else { "" }; @@ -412,11 +440,11 @@ impl ManagedPythonInstallation { pub fn satisfies(&self, request: &PythonRequest) -> bool { match request { - PythonRequest::File(path) => self.executable() == *path, + PythonRequest::File(path) => self.executable(false) == *path, PythonRequest::Default | PythonRequest::Any => true, PythonRequest::Directory(path) => self.path() == *path, PythonRequest::ExecutableName(name) => self - .executable() + .executable(false) .file_name() .is_some_and(|filename| filename.to_string_lossy() == *name), PythonRequest::Implementation(implementation) => { @@ -432,7 +460,7 @@ impl ManagedPythonInstallation { /// Ensure the environment contains the canonical Python executable names. pub fn ensure_canonical_executables(&self) -> Result<(), Error> { - let python = self.executable(); + let python = self.executable(false); let canonical_names = &["python"]; @@ -537,7 +565,7 @@ impl ManagedPythonInstallation { /// /// If the file already exists at the target path, an error will be returned. pub fn create_bin_link(&self, target: &Path) -> Result<(), Error> { - let python = self.executable(); + let python = self.executable(false); let bin = target.parent().ok_or(Error::NoExecutableDirectory)?; fs_err::create_dir_all(bin).map_err(|err| Error::ExecutableDirectory { @@ -583,7 +611,7 @@ impl ManagedPythonInstallation { /// [`ManagedPythonInstallation::create_bin_link`]. pub fn is_bin_link(&self, path: &Path) -> bool { if cfg!(unix) { - is_same_file(path, self.executable()).unwrap_or_default() + is_same_file(path, self.executable(false)).unwrap_or_default() } else if cfg!(windows) { let Some(launcher) = Launcher::try_from_path(path).unwrap_or_default() else { return false; @@ -591,7 +619,7 @@ impl ManagedPythonInstallation { if !matches!(launcher.kind, uv_trampoline_builder::LauncherKind::Python) { return false; } - launcher.python_path == self.executable() + launcher.python_path == self.executable(false) } else { unreachable!("Only Windows and Unix are supported") } @@ -625,6 +653,14 @@ impl ManagedPythonInstallation { // Do not upgrade if the patch versions are the same self.key.patch != other.key.patch } + + pub fn url(&self) -> Option<&'static str> { + self.url + } + + pub fn sha256(&self) -> Option<&'static str> { + self.sha256 + } } /// Generate a platform portion of a key from the environment. diff --git a/crates/uv-python/src/platform.rs b/crates/uv-python/src/platform.rs index ec6abfeaa72d..7ebc892c816e 100644 --- a/crates/uv-python/src/platform.rs +++ b/crates/uv-python/src/platform.rs @@ -102,6 +102,10 @@ impl Arch { variant: None, } } + + pub fn family(&self) -> target_lexicon::Architecture { + self.family + } } impl Display for Libc { diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index d322cf480a10..12047b229a97 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -1,11 +1,15 @@ //! PEP 514 interactions with the Windows registry. -use crate::PythonVersion; +use crate::managed::ManagedPythonInstallation; +use crate::platform::Arch; +use crate::{PythonInstallationKey, PythonVersion, COMPANY}; use std::cmp::Ordering; use std::path::PathBuf; use std::str::FromStr; +use target_lexicon::PointerWidth; +use thiserror::Error; use tracing::debug; -use windows_registry::{Key, CURRENT_USER, LOCAL_MACHINE}; +use windows_registry::{Key, Value, CURRENT_USER, HSTRING, LOCAL_MACHINE}; /// A Python interpreter found in the Windows registry through PEP 514 or from a known Microsoft /// Store path. @@ -98,3 +102,85 @@ fn read_registry_entry(company: &str, tag: &str, tag_key: &Key) -> Option, +) -> Result<(), ManagedPep514Error> { + let pointer_width = match installation.key().arch().family().pointer_width() { + Ok(PointerWidth::U32) => 32, + Ok(PointerWidth::U64) => 64, + _ => { + return Err(ManagedPep514Error::InvalidPointerSize( + *installation.key().arch(), + )); + } + }; + + if let Err(err) = write_registry_entry(installation, pointer_width) { + errors.push((installation.key().clone(), err.into())); + } + + Ok(()) +} + +fn write_registry_entry( + installation: &ManagedPythonInstallation, + pointer_width: i32, +) -> windows_registry::Result<()> { + // We currently just overwrite all known keys, without removing prior entries first + + // Similar to using the bin directory in HOME on Unix, we only install for the current user + // on Windows. + let company = CURRENT_USER.create(format!("Software\\Python\\{COMPANY}"))?; + company.set_string("DisplayName", "Astral")?; + company.set_string("SupportUrl", "https://github.com/astral-sh/uv")?; + + // Ex) Cpython3.13.1 + let python_tag = format!( + "{}{}", + installation.key().implementation().pretty(), + installation.key().version() + ); + let tag = company.create(&python_tag)?; + let display_name = format!( + "{} {} ({}-bit)", + installation.key().implementation().pretty(), + installation.key().version(), + pointer_width + ); + tag.set_string("DisplayName", &display_name)?; + tag.set_string("SupportUrl", "https://github.com/astral-sh/uv")?; + tag.set_string("Version", &installation.key().version().to_string())?; + tag.set_string("SysVersion", &installation.key().sys_version())?; + tag.set_string("SysArchitecture", &format!("{pointer_width}bit"))?; + // Store python build standalone release + if let Some(url) = installation.url() { + tag.set_string("DownloadUrl", url)?; + } + if let Some(sha256) = installation.sha256() { + tag.set_string("DownloadSha256", sha256)?; + } + + let install_path = tag.create("InstallPath")?; + install_path.set_value( + "", + &Value::from(&HSTRING::from(installation.path().as_os_str())), + )?; + install_path.set_value( + "ExecutablePath", + &Value::from(&HSTRING::from(installation.executable(false).as_os_str())), + )?; + install_path.set_value( + "WindowedExecutablePath", + &Value::from(&HSTRING::from(installation.executable(true).as_os_str())), + )?; + Ok(()) +} diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 57635068ae80..d48ba9b2ceba 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -258,7 +258,7 @@ pub(crate) async fn install( for download in &downloads { tasks.push(async { ( - download.key(), + *download, download .fetch_with_retry( &client, @@ -276,16 +276,16 @@ pub(crate) async fn install( let mut errors = vec![]; let mut downloaded = Vec::with_capacity(downloads.len()); - while let Some((key, result)) = tasks.next().await { + while let Some((download, result)) = tasks.next().await { match result { - Ok(download) => { - let path = match download { + Ok(download_result) => { + let path = match download_result { // We should only encounter already-available during concurrent installs DownloadResult::AlreadyAvailable(path) => path, DownloadResult::Fetched(path) => path, }; - let installation = ManagedPythonInstallation::new(path)?; + let installation = ManagedPythonInstallation::new(path, download); changelog.installed.insert(installation.key().clone()); if changelog.existing.contains(installation.key()) { changelog.uninstalled.insert(installation.key().clone()); @@ -293,7 +293,7 @@ pub(crate) async fn install( downloaded.push(installation); } Err(err) => { - errors.push((key.clone(), anyhow::Error::new(err))); + errors.push((download.key().clone(), anyhow::Error::new(err))); } } } @@ -339,6 +339,13 @@ pub(crate) async fn install( &mut changelog, &mut errors, )?; + + #[cfg(windows)] + { + if preview.is_enabled() { + uv_python::windows_registry::create_registry_entry(installation, &mut errors)?; + } + } } if changelog.installed.is_empty() && errors.is_empty() { @@ -713,5 +720,5 @@ fn find_matching_bin_link<'a>( unreachable!("Only Windows and Unix are supported") }; - installations.find(|installation| installation.executable() == target) + installations.find(|installation| installation.executable(false) == target) } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 46bdb94ca3b6..827037bc2a35 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -1068,7 +1068,7 @@ pub fn get_python(version: &PythonVersion) -> PathBuf { .expect("Tests are run on a supported platform") .next() .as_ref() - .map(uv_python::managed::ManagedPythonInstallation::executable) + .map(|python| python.executable(false)) }) // We'll search for the request Python on the PATH if not found in the python versions // We hack this into a `PathBuf` to satisfy the compiler but it's just a string From 21a0e3c7a8a5bdfd8be31e0fc4de97c58599dad4 Mon Sep 17 00:00:00 2001 From: konstin Date: Wed, 15 Jan 2025 11:12:23 +0100 Subject: [PATCH 04/22] Windows: Un-Register managed pythons through PEP 514 --- crates/uv-python/src/windows_registry.rs | 49 ++++++++++++++++++++++ crates/uv/src/commands/python/uninstall.rs | 41 ++++++++++++++++-- crates/uv/src/lib.rs | 9 +++- 3 files changed, 94 insertions(+), 5 deletions(-) diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index 12047b229a97..66e23156d1dd 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -9,7 +9,10 @@ use std::str::FromStr; use target_lexicon::PointerWidth; use thiserror::Error; use tracing::debug; +use uv_warnings::warn_user; use windows_registry::{Key, Value, CURRENT_USER, HSTRING, LOCAL_MACHINE}; +use windows_result::HRESULT; +use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND; /// A Python interpreter found in the Windows registry through PEP 514 or from a known Microsoft /// Store path. @@ -184,3 +187,49 @@ fn write_registry_entry( )?; Ok(()) } + +/// Remove Python entries from the Windows Registry (PEP 514). +pub fn uninstall_windows_registry( + installations: &[ManagedPythonInstallation], + all: bool, + errors: &mut Vec<(PythonInstallationKey, anyhow::Error)>, +) { + // Windows returns this code when the registry key doesn't exist. + let error_not_found = HRESULT::from_win32(ERROR_FILE_NOT_FOUND); + let astral_key = format!("Software\\Python\\{COMPANY}"); + if all { + if let Err(err) = CURRENT_USER.remove_tree(&astral_key) { + if err.code() == error_not_found { + debug!("No registry entries to remove, no registry key {astral_key}"); + } else { + warn_user!("Failed to clear registry entries under {astral_key}: {err}"); + } + } + return; + } + + for installation in installations { + let python_tag = format!( + "{}{}", + installation.key().implementation().pretty(), + installation.key().version() + ); + + let python_entry = format!("{astral_key}\\{python_tag}"); + if let Err(err) = CURRENT_USER.remove_tree(&python_entry) { + if err.code() == error_not_found { + debug!( + "No registry entries to remove for {}, no registry key {}", + installation.key(), + python_entry + ); + } else { + errors.push(( + installation.key().clone(), + anyhow::Error::new(err) + .context("Failed to clear registry entries under {astral_key}"), + )); + } + }; + } +} diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index 12556382ec74..59a62a2a42c1 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -7,9 +7,10 @@ use futures::stream::FuturesUnordered; use futures::StreamExt; use itertools::Itertools; use owo_colors::OwoColorize; - use rustc_hash::{FxHashMap, FxHashSet}; use tracing::{debug, warn}; + +use uv_configuration::PreviewMode; use uv_fs::Simplified; use uv_python::downloads::PythonDownloadRequest; use uv_python::managed::{python_executable_dir, ManagedPythonInstallations}; @@ -25,15 +26,15 @@ pub(crate) async fn uninstall( install_dir: Option, targets: Vec, all: bool, - printer: Printer, + preview: PreviewMode, ) -> Result { let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?; let _lock = installations.lock().await?; // Perform the uninstallation. - do_uninstall(&installations, targets, all, printer).await?; + do_uninstall(&installations, targets, all, printer, preview).await?; // Clean up any empty directories. if uv_fs::directories(installations.root()).all(|path| uv_fs::is_temporary(&path)) { @@ -62,6 +63,7 @@ async fn do_uninstall( targets: Vec, all: bool, printer: Printer, + preview: PreviewMode, ) -> Result { let start = std::time::Instant::now(); @@ -107,6 +109,28 @@ async fn do_uninstall( matching_installations.insert(installation.clone()); } if !found { + // Clear any remnants in the registry + #[cfg(windows)] + if preview.is_enabled() { + let mut errors = Vec::new(); + uv_python::windows_registry::uninstall_windows_registry( + &installed_installations, + all, + &mut errors, + ); + if !errors.is_empty() { + for (key, err) in errors { + writeln!( + printer.stderr(), + "Failed to uninstall {}: {}", + key.green(), + err.to_string().trim() + )?; + } + return Ok(ExitStatus::Failure); + } + } + if matches!(requests.as_slice(), [PythonRequest::Default]) { writeln!(printer.stderr(), "No Python installations found")?; return Ok(ExitStatus::Failure); @@ -190,12 +214,21 @@ async fn do_uninstall( let mut errors = vec![]; while let Some((key, result)) = tasks.next().await { if let Err(err) = result { - errors.push((key.clone(), err)); + errors.push((key.clone(), anyhow::Error::new(err))); } else { uninstalled.push(key.clone()); } } + #[cfg(windows)] + if preview.is_enabled() { + uv_python::windows_registry::uninstall_windows_registry( + &installed_installations, + all, + &mut errors, + ); + } + // Report on any uninstalled installations. if !uninstalled.is_empty() { if let [uninstalled] = uninstalled.as_slice() { diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index 0734e3deca75..69d3bf718331 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -1175,7 +1175,14 @@ async fn run(mut cli: Cli) -> Result { let args = settings::PythonUninstallSettings::resolve(args, filesystem); show_settings!(args); - commands::python_uninstall(args.install_dir, args.targets, args.all, printer).await + commands::python_uninstall( + args.install_dir, + args.targets, + args.all, + printer, + globals.preview, + ) + .await } Commands::Python(PythonNamespace { command: PythonCommand::Find(args), From 016f80fe96a0c915431c191aa673b59633b4044b Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 14:03:57 +0100 Subject: [PATCH 05/22] Review and CI fixes --- Cargo.lock | 46 ---------------------- Cargo.toml | 1 - crates/uv-python/Cargo.toml | 3 +- crates/uv-python/src/installation.rs | 2 +- crates/uv-python/src/lib.rs | 1 + crates/uv-python/src/managed.rs | 4 +- crates/uv-python/src/windows_registry.rs | 23 +++++------ crates/uv/src/commands/python/uninstall.rs | 34 ++++++++-------- 8 files changed, 32 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 512f8f47790c..2442f15c5ede 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5447,7 +5447,6 @@ dependencies = [ "uv-trampoline-builder", "uv-warnings", "which", - "windows 0.59.0", "windows-registry 0.4.0", "windows-result 0.3.0", "windows-sys 0.59.0", @@ -6042,16 +6041,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f919aee0a93304be7f62e8e5027811bbba96bcb1de84d6618be56e43f8a32a1" -dependencies = [ - "windows-core 0.59.0", - "windows-targets 0.53.0", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -6077,19 +6066,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810ce18ed2112484b0d4e15d022e5f598113e220c53e373fb31e67e21670c1ce" -dependencies = [ - "windows-implement 0.59.0", - "windows-interface 0.59.0", - "windows-result 0.3.0", - "windows-strings 0.3.0", - "windows-targets 0.53.0", -] - [[package]] name = "windows-implement" version = "0.57.0" @@ -6112,17 +6088,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-implement" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83577b051e2f49a058c308f17f273b570a6a758386fc291b5f6a934dd84e48c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-interface" version = "0.57.0" @@ -6145,17 +6110,6 @@ dependencies = [ "syn", ] -[[package]] -name = "windows-interface" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26fd936d991781ea39e87c3a27285081e3c0da5ca0fcbc02d368cc6f52ff01" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-registry" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 78272cb2cc92..5b139bc0f11f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,7 +180,6 @@ urlencoding = { version = "2.1.3" } version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "648aa343486e5529953153781fc86025c73c4a61" } walkdir = { version = "2.5.0" } which = { version = "7.0.0", features = ["regex"] } -windows = { version = "0.59.0" } windows-registry = { version = "0.4.0" } windows-result = { version = "0.3.0" } windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO"] } diff --git a/crates/uv-python/Cargo.toml b/crates/uv-python/Cargo.toml index a08fec83de09..7b9b98d6b6fb 100644 --- a/crates/uv-python/Cargo.toml +++ b/crates/uv-python/Cargo.toml @@ -59,15 +59,14 @@ tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } url = { workspace = true } which = { workspace = true } -windows = { workspace = true } [target.'cfg(target_os = "linux")'.dependencies] procfs = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] -windows-sys = { workspace = true } windows-registry = { workspace = true } windows-result = { workspace = true } +windows-sys = { workspace = true } [dev-dependencies] anyhow = { version = "1.0.89" } diff --git a/crates/uv-python/src/installation.rs b/crates/uv-python/src/installation.rs index 90ccd955fbee..4e9f8eac3ea3 100644 --- a/crates/uv-python/src/installation.rs +++ b/crates/uv-python/src/installation.rs @@ -322,7 +322,7 @@ impl PythonInstallationKey { /// The version in `x.y.z` format. pub fn sys_version(&self) -> String { - format!("{}.{}.{}", self.major, self.minor, self.patch,) + format!("{}.{}.{}", self.major, self.minor, self.patch) } pub fn arch(&self) -> &Arch { diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 2769eeb6883f..9728ad26ef0e 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -45,6 +45,7 @@ mod virtualenv; #[cfg(windows)] pub mod windows_registry; +#[cfg(windows)] pub(crate) const COMPANY: &str = "Astral"; #[cfg(not(test))] diff --git a/crates/uv-python/src/managed.rs b/crates/uv-python/src/managed.rs index 0c8d513dd40d..d48c8c0db2eb 100644 --- a/crates/uv-python/src/managed.rs +++ b/crates/uv-python/src/managed.rs @@ -296,11 +296,11 @@ pub struct ManagedPythonInstallation { key: PythonInstallationKey, /// The URL with the Python archive. /// - /// Empty when self was built from a path. + /// Empty when self was constructed from a path. url: Option<&'static str>, /// The SHA256 of the Python archive at the URL. /// - /// Empty when self was built from a path. + /// Empty when self was constructed from a path. sha256: Option<&'static str>, } diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index 66e23156d1dd..9d5a40a56479 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -146,13 +146,8 @@ fn write_registry_entry( company.set_string("DisplayName", "Astral")?; company.set_string("SupportUrl", "https://github.com/astral-sh/uv")?; - // Ex) Cpython3.13.1 - let python_tag = format!( - "{}{}", - installation.key().implementation().pretty(), - installation.key().version() - ); - let tag = company.create(&python_tag)?; + // Ex) CPython3.13.1 + let tag = company.create(&python_tag_(installation.key()))?; let display_name = format!( "{} {} ({}-bit)", installation.key().implementation().pretty(), @@ -164,7 +159,7 @@ fn write_registry_entry( tag.set_string("Version", &installation.key().version().to_string())?; tag.set_string("SysVersion", &installation.key().sys_version())?; tag.set_string("SysArchitecture", &format!("{pointer_width}bit"))?; - // Store python build standalone release + // Store `python-build-standalone` release if let Some(url) = installation.url() { tag.set_string("DownloadUrl", url)?; } @@ -188,6 +183,10 @@ fn write_registry_entry( Ok(()) } +fn python_tag_(key: &PythonInstallationKey) -> String { + format!("{}{}", key.implementation().pretty(), key.version()) +} + /// Remove Python entries from the Windows Registry (PEP 514). pub fn uninstall_windows_registry( installations: &[ManagedPythonInstallation], @@ -209,11 +208,7 @@ pub fn uninstall_windows_registry( } for installation in installations { - let python_tag = format!( - "{}{}", - installation.key().implementation().pretty(), - installation.key().version() - ); + let python_tag = python_tag_(installation.key()); let python_entry = format!("{astral_key}\\{python_tag}"); if let Err(err) = CURRENT_USER.remove_tree(&python_entry) { @@ -227,7 +222,7 @@ pub fn uninstall_windows_registry( errors.push(( installation.key().clone(), anyhow::Error::new(err) - .context("Failed to clear registry entries under {astral_key}"), + .context("Failed to clear registry entries under HKCU:\\{python_entry}"), )); } }; diff --git a/crates/uv/src/commands/python/uninstall.rs b/crates/uv/src/commands/python/uninstall.rs index 59a62a2a42c1..5282a3842603 100644 --- a/crates/uv/src/commands/python/uninstall.rs +++ b/crates/uv/src/commands/python/uninstall.rs @@ -110,24 +110,26 @@ async fn do_uninstall( } if !found { // Clear any remnants in the registry - #[cfg(windows)] if preview.is_enabled() { - let mut errors = Vec::new(); - uv_python::windows_registry::uninstall_windows_registry( - &installed_installations, - all, - &mut errors, - ); - if !errors.is_empty() { - for (key, err) in errors { - writeln!( - printer.stderr(), - "Failed to uninstall {}: {}", - key.green(), - err.to_string().trim() - )?; + #[cfg(windows)] + { + let mut errors = Vec::new(); + uv_python::windows_registry::uninstall_windows_registry( + &installed_installations, + all, + &mut errors, + ); + if !errors.is_empty() { + for (key, err) in errors { + writeln!( + printer.stderr(), + "Failed to uninstall {}: {}", + key.green(), + err.to_string().trim() + )?; + } + return Ok(ExitStatus::Failure); } - return Ok(ExitStatus::Failure); } } From ba15ae841b6bdf2a53eaf5fa7b9590850fe4ae55 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 14:38:59 +0100 Subject: [PATCH 06/22] Add system test --- .github/workflows/ci.yml | 14 ++++++++++++++ crates/uv/src/commands/python/install.rs | 6 +++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65a5bdcbbbe7..d6b60d62e3bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1561,6 +1561,20 @@ jobs: - name: "Validate global Python install" run: python ./scripts/check_system_python.py --uv ./uv.exe + # NB: Run this last, we are modifying the registry + - name: "Test PEP 514 registration" + run: | + ./uv.exe python install --preview 3.11 + ./uv.exe python install --preview 3.12 + ./uv.exe python install --preview 3.13 + Get-ChildItem -Path "HKCU:\Software\Python" -Recurse + py --list-paths + ./uv.exe python uninstall --preview 3.11 + Get-ChildItem -Path "HKCU:\Software\Python" -Recurse + py --list-paths + ./uv.exe python uninstall --preview --all + Get-ChildItem -Path "HKCU:\Software\Python" -Recurse + system-test-windows-python-313: timeout-minutes: 10 needs: build-binary-windows diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index d48ba9b2ceba..2154c52f96df 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -340,9 +340,9 @@ pub(crate) async fn install( &mut errors, )?; - #[cfg(windows)] - { - if preview.is_enabled() { + if preview.is_enabled() { + #[cfg(windows)] + { uv_python::windows_registry::create_registry_entry(installation, &mut errors)?; } } From e4833a930fa697f8d9e3cf506ad7c5c35d4260e2 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 14:43:45 +0100 Subject: [PATCH 07/22] Rebase fix --- crates/uv-python/src/macos_dylib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-python/src/macos_dylib.rs b/crates/uv-python/src/macos_dylib.rs index 7294497b3c9c..a77284da0c1a 100644 --- a/crates/uv-python/src/macos_dylib.rs +++ b/crates/uv-python/src/macos_dylib.rs @@ -56,7 +56,7 @@ impl Error { }; warn_user!( "Failed to patch the install name of the dynamic library for {}. This may cause issues when building Python native extensions.{}", - installation.executable().simplified_display(), + installation.executable(false).simplified_display(), error ); } From 7d3a10b6fabe5970f5f145d8d41c6abd50577f0a Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 14:48:01 +0100 Subject: [PATCH 08/22] More rebases fixes --- crates/uv-python/src/windows_registry.rs | 2 +- crates/uv/src/commands/python/install.rs | 42 +++++++++++------------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index 9d5a40a56479..3bb1dd08238e 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -147,7 +147,7 @@ fn write_registry_entry( company.set_string("SupportUrl", "https://github.com/astral-sh/uv")?; // Ex) CPython3.13.1 - let tag = company.create(&python_tag_(installation.key()))?; + let tag = company.create(python_tag_(installation.key()))?; let display_name = format!( "{} {} ({}-bit)", installation.key().implementation().pretty(), diff --git a/crates/uv/src/commands/python/install.rs b/crates/uv/src/commands/python/install.rs index 2154c52f96df..aa795844f080 100644 --- a/crates/uv/src/commands/python/install.rs +++ b/crates/uv/src/commands/python/install.rs @@ -504,32 +504,30 @@ fn create_bin_links( ); // Figure out what installation it references, if any - let existing = - find_matching_bin_link( - installations - .iter() - .copied() - .chain(existing_installations.iter()), - &target, - ) - ; + let existing = find_matching_bin_link( + installations + .iter() + .copied() + .chain(existing_installations.iter()), + &target, + ); match existing { None => { // Determine if the link is valid, i.e., if it points to an existing - // Python we don't manage. On Windows, we just assume it is valid because - // symlinks are not common for Python interpreters. - let valid_link = cfg!(windows) - || target - .read_link() - .and_then(|target| target.try_exists()) - .inspect_err(|err| { - debug!("Failed to inspect executable with error: {err}"); - }) - // If we can't verify the link, assume it is valid. - .unwrap_or(true); - - // There's an existing executable we don't manage, require `--force` + // Python we don't manage. On Windows, we just assume it is valid because + // symlinks are not common for Python interpreters. + let valid_link = cfg!(windows) + || target + .read_link() + .and_then(|target| target.try_exists()) + .inspect_err(|err| { + debug!("Failed to inspect executable with error: {err}"); + }) + // If we can't verify the link, assume it is valid. + .unwrap_or(true); + + // There's an existing executable we don't manage, require `--force` if valid_link { if !force { errors.push(( From 234ade9a92c0d498a27ca854149d3bcec3448005 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 16:27:47 +0100 Subject: [PATCH 09/22] Test in CI --- .github/workflows/ci.yml | 12 +-- scripts/check_registry.py | 178 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+), 11 deletions(-) create mode 100644 scripts/check_registry.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d6b60d62e3bc..457b0466133f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1563,17 +1563,7 @@ jobs: # NB: Run this last, we are modifying the registry - name: "Test PEP 514 registration" - run: | - ./uv.exe python install --preview 3.11 - ./uv.exe python install --preview 3.12 - ./uv.exe python install --preview 3.13 - Get-ChildItem -Path "HKCU:\Software\Python" -Recurse - py --list-paths - ./uv.exe python uninstall --preview 3.11 - Get-ChildItem -Path "HKCU:\Software\Python" -Recurse - py --list-paths - ./uv.exe python uninstall --preview --all - Get-ChildItem -Path "HKCU:\Software\Python" -Recurse + run: python ./scripts/check_registry.py --uv ./uv.exe system-test-windows-python-313: timeout-minutes: 10 diff --git a/scripts/check_registry.py b/scripts/check_registry.py new file mode 100644 index 000000000000..b15b7286c024 --- /dev/null +++ b/scripts/check_registry.py @@ -0,0 +1,178 @@ +"""Check that adding uv's python-build-standalone distributions are successfully added +and removed from the Windows registry following PEP 514.""" + +import re +import subprocess +import sys +from argparse import ArgumentParser + +# This is the snapshot as of python build standalone 20250115, we redact URL and hash +# below. We don't redact the path inside the runner, if the runner configuration changes +# (or uv's installation paths), please update the snapshot. +expected_registry = r""" + Hive: HKEY_CURRENT_USER\Software\Python + +Name Property +---- -------- +Astral DisplayName : Astral + SupportUrl : https://github.com/astral-sh/uv + + Hive: HKEY_CURRENT_USER\Software\Python\Astral + +Name Property +---- -------- +CPython3.11.11 DisplayName : CPython 3.11.11 (64-bit) + SupportUrl : https://github.com/astral-sh/uv + Version : 3.11.11 + SysVersion : 3.11.11 + SysArchitecture : 64bit + DownloadUrl : https://github.com/astral-sh/python-build-standalone/releases/download + /20250115/cpython-3.11.11%2B20250115-x86_64-pc-windows-msvc-install_only_stripped.tar.gz + DownloadSha256 : 6810fee03a788bf83c2450d4aaaff8b25bf6d52853947c829983cd4382ca3027 + + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 + +Name Property +---- -------- +InstallPath (default) : + C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none + ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- + windows-x86_64-none\python.exe + WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- + windows-x86_64-none\pythonw.exe + + Hive: HKEY_CURRENT_USER\Software\Python\Astral + +Name Property +---- -------- +CPython3.12.8 DisplayName : CPython 3.12.8 (64-bit) + SupportUrl : https://github.com/astral-sh/uv + Version : 3.12.8 + SysVersion : 3.12.8 + SysArchitecture : 64bit + DownloadUrl : https://github.com/astral-sh/python-build-standalone/releases/download + /20250115/cpython-3.12.8%2B20250115-x86_64-pc-windows-msvc-install_only_stripped.tar.gz + DownloadSha256 : 8a4e9e748eeee7ae71048a108a55a9bac48f8bedf9dff413a7c87744f0408ef1 + + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.12.8 + +Name Property +---- -------- +InstallPath (default) : + C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none + ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-w + indows-x86_64-none\python.exe + WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-w + indows-x86_64-none\pythonw.exe + + Hive: HKEY_CURRENT_USER\Software\Python\Astral + +Name Property +---- -------- +CPython3.13.1 DisplayName : CPython 3.13.1 (64-bit) + SupportUrl : https://github.com/astral-sh/uv + Version : 3.13.1 + SysVersion : 3.13.1 + SysArchitecture : 64bit + DownloadUrl : https://github.com/astral-sh/python-build-standalone/releases/download + /20250115/cpython-3.13.1%2B20250115-x86_64-pc-windows-msvc-install_only_stripped.tar.gz + DownloadSha256 : 8ccd98ae4a4f36a72195ec4063c749f17e39a5f7923fa672757fc69e91892572 + + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.13.1 + +Name Property +---- -------- +InstallPath (default) : + C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none + ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-w + indows-x86_64-none\python.exe + WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-w + indows-x86_64-none\pythonw.exe +""" + + +def filter_snapshot(snapshot: str) -> str: + snapshot = snapshot.strip() + snapshot = re.sub( + "DownloadUrl ( *): .*\n.*tar.gz", r"DownloadUrl \1: ", snapshot + ) + snapshot = re.sub( + "DownloadSha256 ( *): .*", r"DownloadSha256 \1: ", snapshot + ) + return snapshot + + +def main(uv: str): + # Check 1: Install interpreters and check that all their keys are set in the + # registry and that the Python launcher for Windows finds it. + print("Installing Python 3.11.11, 3.12.8, and 3.13.1") + subprocess.check_call([uv, "python", "install", "--preview", "3.11.11"]) + subprocess.check_call([uv, "python", "install", "--preview", "3.12.8"]) + subprocess.check_call([uv, "python", "install", "--preview", "3.13.1"]) + actual_registry = subprocess.check_output( + ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], text=True + ) + if filter_snapshot(actual_registry) != filter_snapshot(expected_registry): + print("Registry mismatch:") + print("Expected:") + print("=" * 80) + print(filter_snapshot(expected_registry)) + print("Actual:") + print("=" * 80) + print(filter_snapshot(actual_registry)) + print("=" * 80) + sys.exit(1) + py_311_line = r" -V:Astral/CPython3.11.11 C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\python.exe" + py_312_line = r" -V:Astral/CPython3.12.8 C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\python.exe" + py_313_line = r" -V:Astral/CPython3.13.1 C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\python.exe" + listed_interpreters = subprocess.check_output(["py", "--list-paths"], text=True) + py_listed = set(listed_interpreters.splitlines()) + if ( + py_311_line not in py_listed + or py_312_line not in py_listed + or py_313_line not in py_listed + ): + print( + f"Python launcher interpreter mismatch: {py_listed} vs. {py_311_line}, {py_312_line}, {py_313_line}" + ) + sys.exit(1) + + # Check 2: Remove a single interpreter and check that its gone. + print("Removing Python 3.11.11") + subprocess.check_call([uv, "python", "uninstall", "--preview", "3.11.11"]) + listed_interpreters = subprocess.check_output(["py", "--list-paths"], text=True) + py_listed = set(listed_interpreters.splitlines()) + if ( + py_311_line in py_listed + or py_312_line not in py_listed + or py_313_line not in py_listed + ): + print( + f"Python launcher interpreter not removed: {py_listed} vs. {py_312_line}, {py_313_line}" + ) + sys.exit(1) + + # Check 3: Remove all interpreters and check that they are all gone. + subprocess.check_call([uv, "python", "uninstall", "--preview", "--all"]) + empty_registry = subprocess.check_output( + ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], text=True + ) + if empty_registry.strip(): + print("Registry not cleared:") + print("=" * 80) + print(empty_registry) + print("=" * 80) + sys.exit(1) + listed_interpreters = subprocess.check_output(["py", "--list-paths"], text=True) + py_listed = set(listed_interpreters.splitlines()) + if py_311_line in py_listed or py_312_line in py_listed or py_313_line in py_listed: + print(f"Python launcher interpreter not cleared: {py_listed}") + sys.exit(1) + + +if __name__ == "__main__": + parser = ArgumentParser() + parser.add_argument("--uv", default="./uv.exe") + args = parser.parse_args() + main(args.uv) + From a7c1954dc88d9a92fc084c284613cad7fc1b0dcd Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 16:34:28 +0100 Subject: [PATCH 10/22] Add comment --- crates/uv-python/src/windows_registry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index 3bb1dd08238e..db05d86b0c87 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -28,6 +28,7 @@ pub(crate) struct WindowsPython { /// Find all Pythons registered in the Windows registry following PEP 514. pub(crate) fn registry_pythons() -> Result, windows_result::Error> { let mut registry_pythons = Vec::new(); + // Prefer `HKEY_CURRENT_USER` over `HKEY_LOCAL_MACHINE` for root_key in [CURRENT_USER, LOCAL_MACHINE] { let Ok(key_python) = root_key.open(r"Software\Python") else { continue; From 1c1e0907c65674f315509ec5da1dd260da72e304 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 16:37:22 +0100 Subject: [PATCH 11/22] Run shell command in shell --- scripts/check_registry.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index b15b7286c024..f392bbd682a9 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -109,8 +109,11 @@ def main(uv: str): subprocess.check_call([uv, "python", "install", "--preview", "3.11.11"]) subprocess.check_call([uv, "python", "install", "--preview", "3.12.8"]) subprocess.check_call([uv, "python", "install", "--preview", "3.13.1"]) + # Use the powershell command to get an outside view on the registry values we wrote actual_registry = subprocess.check_output( - ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], text=True + ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], + shell=True, + text=True, ) if filter_snapshot(actual_registry) != filter_snapshot(expected_registry): print("Registry mismatch:") @@ -155,7 +158,9 @@ def main(uv: str): # Check 3: Remove all interpreters and check that they are all gone. subprocess.check_call([uv, "python", "uninstall", "--preview", "--all"]) empty_registry = subprocess.check_output( - ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], text=True + ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], + shell=True, + text=True, ) if empty_registry.strip(): print("Registry not cleared:") @@ -175,4 +180,3 @@ def main(uv: str): parser.add_argument("--uv", default="./uv.exe") args = parser.parse_args() main(args.uv) - From c8a9fde1b4b2f0ef6c1bfe666d35e88209856631 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 16:50:08 +0100 Subject: [PATCH 12/22] Powerhshell, next try --- scripts/check_registry.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index f392bbd682a9..62cecf6cd5de 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -111,8 +111,11 @@ def main(uv: str): subprocess.check_call([uv, "python", "install", "--preview", "3.13.1"]) # Use the powershell command to get an outside view on the registry values we wrote actual_registry = subprocess.check_output( - ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], - shell=True, + [ + "powershell", + "-Command", + "Get-ChildItem -Path HKCU:\Software\Python -Recurse", + ], text=True, ) if filter_snapshot(actual_registry) != filter_snapshot(expected_registry): @@ -158,8 +161,11 @@ def main(uv: str): # Check 3: Remove all interpreters and check that they are all gone. subprocess.check_call([uv, "python", "uninstall", "--preview", "--all"]) empty_registry = subprocess.check_output( - ["Get-ChildItem", "-Path", r"HKCU:\Software\Python", "-Recurse"], - shell=True, + [ + "powershell", + "-Command", + "Get-ChildItem -Path HKCU:\Software\Python -Recurse", + ], text=True, ) if empty_registry.strip(): From 0bb47775608725570f1224fbb3af498c9342ddd8 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 17:00:07 +0100 Subject: [PATCH 13/22] Powerhshell, next try --- scripts/check_registry.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 62cecf6cd5de..ab734bf160b5 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -93,14 +93,16 @@ def filter_snapshot(snapshot: str) -> str: snapshot = snapshot.strip() + # Long URLs are wrapped into multiple lines snapshot = re.sub( - "DownloadUrl ( *): .*\n.*tar.gz", r"DownloadUrl \1: ", snapshot + "DownloadUrl ( *): .*(\n.*)+?(\n +)DownloadSha256", r"DownloadUrl \1: \3DownloadSha256", snapshot ) snapshot = re.sub( "DownloadSha256 ( *): .*", r"DownloadSha256 \1: ", snapshot ) return snapshot +print(filter_snapshot(expected_registry)) def main(uv: str): # Check 1: Install interpreters and check that all their keys are set in the From 98225ccb2a9d3dd5a02e5b57786bda362dcc3a0d Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 17:26:52 +0100 Subject: [PATCH 14/22] Powerhshell, next try --- scripts/check_registry.py | 92 ++++++++++++++++++++++----------------- 1 file changed, 52 insertions(+), 40 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index ab734bf160b5..7b3f26399b11 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -9,40 +9,44 @@ # This is the snapshot as of python build standalone 20250115, we redact URL and hash # below. We don't redact the path inside the runner, if the runner configuration changes # (or uv's installation paths), please update the snapshot. -expected_registry = r""" - Hive: HKEY_CURRENT_USER\Software\Python - +expected_registry = [ + r""" Name Property ---- -------- Astral DisplayName : Astral SupportUrl : https://github.com/astral-sh/uv - +""", + r""" Hive: HKEY_CURRENT_USER\Software\Python\Astral + Name Property ----- -------- + ---- -------- CPython3.11.11 DisplayName : CPython 3.11.11 (64-bit) - SupportUrl : https://github.com/astral-sh/uv - Version : 3.11.11 - SysVersion : 3.11.11 - SysArchitecture : 64bit - DownloadUrl : https://github.com/astral-sh/python-build-standalone/releases/download - /20250115/cpython-3.11.11%2B20250115-x86_64-pc-windows-msvc-install_only_stripped.tar.gz - DownloadSha256 : 6810fee03a788bf83c2450d4aaaff8b25bf6d52853947c829983cd4382ca3027 +SupportUrl : https://github.com/astral-sh/uv +Version : 3.11.11 +SysVersion : 3.11.11 +SysArchitecture : 64bit +DownloadUrl : + DownloadSha256 : + + + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 - Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 Name Property ----- -------- + ---- -------- InstallPath (default) : - C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none - ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- - windows-x86_64-none\python.exe - WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- - windows-x86_64-none\pythonw.exe - +C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none +ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- + windows-x86_64-none\python.exe +WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- + windows-x86_64-none\pythonw.exe +""", + r""" Hive: HKEY_CURRENT_USER\Software\Python\Astral + Name Property ---- -------- CPython3.12.8 DisplayName : CPython 3.12.8 (64-bit) @@ -50,12 +54,13 @@ Version : 3.12.8 SysVersion : 3.12.8 SysArchitecture : 64bit - DownloadUrl : https://github.com/astral-sh/python-build-standalone/releases/download - /20250115/cpython-3.12.8%2B20250115-x86_64-pc-windows-msvc-install_only_stripped.tar.gz - DownloadSha256 : 8a4e9e748eeee7ae71048a108a55a9bac48f8bedf9dff413a7c87744f0408ef1 + DownloadUrl : + DownloadSha256 : + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.12.8 + Name Property ---- -------- InstallPath (default) : @@ -64,9 +69,11 @@ indows-x86_64-none\python.exe WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-w indows-x86_64-none\pythonw.exe - +""", + r""" Hive: HKEY_CURRENT_USER\Software\Python\Astral + Name Property ---- -------- CPython3.13.1 DisplayName : CPython 3.13.1 (64-bit) @@ -74,12 +81,13 @@ Version : 3.13.1 SysVersion : 3.13.1 SysArchitecture : 64bit - DownloadUrl : https://github.com/astral-sh/python-build-standalone/releases/download - /20250115/cpython-3.13.1%2B20250115-x86_64-pc-windows-msvc-install_only_stripped.tar.gz - DownloadSha256 : 8ccd98ae4a4f36a72195ec4063c749f17e39a5f7923fa672757fc69e91892572 + DownloadUrl : + DownloadSha256 : + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.13.1 + Name Property ---- -------- InstallPath (default) : @@ -88,21 +96,23 @@ indows-x86_64-none\python.exe WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-w indows-x86_64-none\pythonw.exe -""" +""", +] def filter_snapshot(snapshot: str) -> str: snapshot = snapshot.strip() # Long URLs are wrapped into multiple lines snapshot = re.sub( - "DownloadUrl ( *): .*(\n.*)+?(\n +)DownloadSha256", r"DownloadUrl \1: \3DownloadSha256", snapshot + "DownloadUrl ( *): .*(\n.*)+?(\n +)DownloadSha256", + r"DownloadUrl \1: \3DownloadSha256", + snapshot, ) snapshot = re.sub( "DownloadSha256 ( *): .*", r"DownloadSha256 \1: ", snapshot ) return snapshot -print(filter_snapshot(expected_registry)) def main(uv: str): # Check 1: Install interpreters and check that all their keys are set in the @@ -120,16 +130,18 @@ def main(uv: str): ], text=True, ) - if filter_snapshot(actual_registry) != filter_snapshot(expected_registry): - print("Registry mismatch:") - print("Expected:") - print("=" * 80) - print(filter_snapshot(expected_registry)) - print("Actual:") - print("=" * 80) - print(filter_snapshot(actual_registry)) - print("=" * 80) - sys.exit(1) + for expected in expected_registry: + if filter_snapshot(expected) not in filter_snapshot(actual_registry): + print("Registry mismatch:") + print("Expected Snippet:") + print("=" * 80) + print(filter_snapshot(expected)) + print("=" * 80) + print("Actual:") + print("=" * 80) + print(filter_snapshot(actual_registry)) + print("=" * 80) + sys.exit(1) py_311_line = r" -V:Astral/CPython3.11.11 C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\python.exe" py_312_line = r" -V:Astral/CPython3.12.8 C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\python.exe" py_313_line = r" -V:Astral/CPython3.13.1 C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\python.exe" From 982530c3bd8a57b3dad8f74b589ac77421b25970 Mon Sep 17 00:00:00 2001 From: konstin Date: Fri, 17 Jan 2025 19:10:36 +0100 Subject: [PATCH 15/22] . --- scripts/check_registry.py | 58 ++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 7b3f26399b11..d1b6a7b09e6d 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -23,25 +23,23 @@ Name Property ---- -------- CPython3.11.11 DisplayName : CPython 3.11.11 (64-bit) -SupportUrl : https://github.com/astral-sh/uv -Version : 3.11.11 -SysVersion : 3.11.11 -SysArchitecture : 64bit -DownloadUrl : - DownloadSha256 : + SupportUrl : https://github.com/astral-sh/uv + Version : 3.11.11 + SysVersion : 3.11.11 + SysArchitecture : 64bit + DownloadUrl : + DownloadSha256 : - Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 +Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 Name Property ---- -------- InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none -ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- - windows-x86_64-none\python.exe -WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11- - windows-x86_64-none\pythonw.exe +ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\python.exe +WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\pythonw.exe """, r""" Hive: HKEY_CURRENT_USER\Software\Python\Astral @@ -65,10 +63,8 @@ ---- -------- InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none - ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-w - indows-x86_64-none\python.exe - WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-w - indows-x86_64-none\pythonw.exe + ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\python.exe + WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\pythonw.exe """, r""" Hive: HKEY_CURRENT_USER\Software\Python\Astral @@ -92,16 +88,16 @@ ---- -------- InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none - ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-w - indows-x86_64-none\python.exe - WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-w - indows-x86_64-none\pythonw.exe + ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\python.exe + WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\pythonw.exe """, ] def filter_snapshot(snapshot: str) -> str: snapshot = snapshot.strip() + # Trim trailing whitespace + snapshot = "\n".join(line.rstrip() for line in snapshot.splitlines()) # Long URLs are wrapped into multiple lines snapshot = re.sub( "DownloadUrl ( *): .*(\n.*)+?(\n +)DownloadSha256", @@ -122,14 +118,13 @@ def main(uv: str): subprocess.check_call([uv, "python", "install", "--preview", "3.12.8"]) subprocess.check_call([uv, "python", "install", "--preview", "3.13.1"]) # Use the powershell command to get an outside view on the registry values we wrote - actual_registry = subprocess.check_output( - [ - "powershell", - "-Command", - "Get-ChildItem -Path HKCU:\Software\Python -Recurse", - ], - text=True, - ) + list_registry_command = [ + "powershell", + "-Command", + # By default, powershell wraps the output at terminal size + r"Get-ChildItem -Path HKCU:\Software\Python -Recurse | Format-Table | Out-String -width 1000", + ] + actual_registry = subprocess.check_output(list_registry_command, text=True) for expected in expected_registry: if filter_snapshot(expected) not in filter_snapshot(actual_registry): print("Registry mismatch:") @@ -174,14 +169,7 @@ def main(uv: str): # Check 3: Remove all interpreters and check that they are all gone. subprocess.check_call([uv, "python", "uninstall", "--preview", "--all"]) - empty_registry = subprocess.check_output( - [ - "powershell", - "-Command", - "Get-ChildItem -Path HKCU:\Software\Python -Recurse", - ], - text=True, - ) + empty_registry = subprocess.check_output(list_registry_command, text=True) if empty_registry.strip(): print("Registry not cleared:") print("=" * 80) From 6fcb8985eb46cbc7131ceea46f97d90f85b4e951 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 12:57:20 +0100 Subject: [PATCH 16/22] . --- scripts/check_registry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index d1b6a7b09e6d..cb0cca9875da 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -21,7 +21,7 @@ Name Property - ---- -------- +---- -------- CPython3.11.11 DisplayName : CPython 3.11.11 (64-bit) SupportUrl : https://github.com/astral-sh/uv Version : 3.11.11 @@ -35,7 +35,7 @@ Name Property - ---- -------- +---- -------- InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\python.exe From 415b2768f7e0283fb5641154e9385e94892b4cc9 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 13:04:14 +0100 Subject: [PATCH 17/22] Rename display name to Astral Software Inc. --- crates/uv-python/src/lib.rs | 4 +++- crates/uv-python/src/windows_registry.rs | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/uv-python/src/lib.rs b/crates/uv-python/src/lib.rs index 9728ad26ef0e..8e40ba1229cb 100644 --- a/crates/uv-python/src/lib.rs +++ b/crates/uv-python/src/lib.rs @@ -46,7 +46,9 @@ mod virtualenv; pub mod windows_registry; #[cfg(windows)] -pub(crate) const COMPANY: &str = "Astral"; +pub(crate) const COMPANY_KEY: &str = "Astral"; +#[cfg(windows)] +pub(crate) const COMPANY_DISPLAY_NAME: &str = "Astral Software Inc."; #[cfg(not(test))] pub(crate) fn current_dir() -> Result { diff --git a/crates/uv-python/src/windows_registry.rs b/crates/uv-python/src/windows_registry.rs index db05d86b0c87..30735a40c4f8 100644 --- a/crates/uv-python/src/windows_registry.rs +++ b/crates/uv-python/src/windows_registry.rs @@ -2,7 +2,7 @@ use crate::managed::ManagedPythonInstallation; use crate::platform::Arch; -use crate::{PythonInstallationKey, PythonVersion, COMPANY}; +use crate::{PythonInstallationKey, PythonVersion, COMPANY_DISPLAY_NAME, COMPANY_KEY}; use std::cmp::Ordering; use std::path::PathBuf; use std::str::FromStr; @@ -143,8 +143,8 @@ fn write_registry_entry( // Similar to using the bin directory in HOME on Unix, we only install for the current user // on Windows. - let company = CURRENT_USER.create(format!("Software\\Python\\{COMPANY}"))?; - company.set_string("DisplayName", "Astral")?; + let company = CURRENT_USER.create(format!("Software\\Python\\{COMPANY_KEY}"))?; + company.set_string("DisplayName", COMPANY_DISPLAY_NAME)?; company.set_string("SupportUrl", "https://github.com/astral-sh/uv")?; // Ex) CPython3.13.1 @@ -196,7 +196,7 @@ pub fn uninstall_windows_registry( ) { // Windows returns this code when the registry key doesn't exist. let error_not_found = HRESULT::from_win32(ERROR_FILE_NOT_FOUND); - let astral_key = format!("Software\\Python\\{COMPANY}"); + let astral_key = format!("Software\\Python\\{COMPANY_KEY}"); if all { if let Err(err) = CURRENT_USER.remove_tree(&astral_key) { if err.code() == error_not_found { From 6bc0a1fe87692c13ee6276ce52ccab9961256626 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 13:15:44 +0100 Subject: [PATCH 18/22] . --- scripts/check_registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index cb0cca9875da..7b4cf0c6f0d8 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -13,7 +13,7 @@ r""" Name Property ---- -------- -Astral DisplayName : Astral +Astral DisplayName : Astral Software Inc. SupportUrl : https://github.com/astral-sh/uv """, r""" From 5f8a3ec2ec31021bf553ede21006cc40b459522d Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 13:38:15 +0100 Subject: [PATCH 19/22] . --- scripts/check_registry.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 7b4cf0c6f0d8..bae80dae965b 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -95,7 +95,6 @@ def filter_snapshot(snapshot: str) -> str: - snapshot = snapshot.strip() # Trim trailing whitespace snapshot = "\n".join(line.rstrip() for line in snapshot.splitlines()) # Long URLs are wrapped into multiple lines From 478b85d0f4a5ddddd4eca7e619bd99cfd51acd26 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 14:00:13 +0100 Subject: [PATCH 20/22] . --- scripts/check_registry.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index bae80dae965b..110bdf67d283 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -17,6 +17,17 @@ SupportUrl : https://github.com/astral-sh/uv """, r""" + + + Hive: HKEY_CURRENT_USER\Software\Python + + +Name Property +---- -------- +Astral DisplayName : Astral Software Inc. + SupportUrl : https://github.com/astral-sh/uv + + Hive: HKEY_CURRENT_USER\Software\Python\Astral @@ -31,15 +42,14 @@ DownloadSha256 : -Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 + Hive: HKEY_CURRENT_USER\Software\Python\Astral\CPython3.11.11 Name Property ---- -------- -InstallPath (default) : -C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none -ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\python.exe -WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\pythonw.exe +InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none + ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\python.exe + WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.11.11-windows-x86_64-none\pythonw.exe """, r""" Hive: HKEY_CURRENT_USER\Software\Python\Astral @@ -61,13 +71,12 @@ Name Property ---- -------- -InstallPath (default) : - C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none +InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\python.exe WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\pythonw.exe """, r""" - Hive: HKEY_CURRENT_USER\Software\Python\Astral + Hive: HKEY_CURRENT_USER\Software\Python\Astral Name Property @@ -86,8 +95,7 @@ Name Property ---- -------- -InstallPath (default) : - C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none +InstallPath (default) : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none ExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\python.exe WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.13.1-windows-x86_64-none\pythonw.exe """, From b786a34b2796b5cf67e6dec4b518f0a8ea49cb4c Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 14:16:30 +0100 Subject: [PATCH 21/22] . --- scripts/check_registry.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 110bdf67d283..42373153a5ab 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -103,6 +103,7 @@ def filter_snapshot(snapshot: str) -> str: + snapshot = snapshot.strip("\n\r") # Trim trailing whitespace snapshot = "\n".join(line.rstrip() for line in snapshot.splitlines()) # Long URLs are wrapped into multiple lines From 6634a09371f005991bfea0291d4c570ca533e938 Mon Sep 17 00:00:00 2001 From: konstin Date: Mon, 20 Jan 2025 14:25:12 +0100 Subject: [PATCH 22/22] 3.13.1 it's a single space --- scripts/check_registry.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/check_registry.py b/scripts/check_registry.py index 42373153a5ab..cd1abc9d608b 100644 --- a/scripts/check_registry.py +++ b/scripts/check_registry.py @@ -17,8 +17,6 @@ SupportUrl : https://github.com/astral-sh/uv """, r""" - - Hive: HKEY_CURRENT_USER\Software\Python @@ -76,7 +74,7 @@ WindowedExecutablePath : C:\Users\runneradmin\AppData\Roaming\uv\python\cpython-3.12.8-windows-x86_64-none\pythonw.exe """, r""" - Hive: HKEY_CURRENT_USER\Software\Python\Astral + Hive: HKEY_CURRENT_USER\Software\Python\Astral Name Property