From 9a4251104ea15d0f6b5c69cb14c7a2f413f97658 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 22 Aug 2025 12:42:14 +0100 Subject: [PATCH] Use separate rules --- crates/uv-dispatch/src/lib.rs | 3 +- crates/uv-installer/src/lib.rs | 2 +- crates/uv-installer/src/plan.rs | 3 ++ crates/uv-installer/src/satisfies.rs | 22 +++++++++ crates/uv-installer/src/site_packages.rs | 18 +++++++ crates/uv/src/commands/pip/install.rs | 4 +- crates/uv/src/commands/pip/operations.rs | 4 +- crates/uv/src/commands/pip/sync.rs | 3 +- crates/uv/src/commands/project/mod.rs | 5 +- crates/uv/src/commands/project/run.rs | 3 +- crates/uv/src/commands/project/sync.rs | 63 ++---------------------- crates/uv/src/commands/tool/run.rs | 3 +- 12 files changed, 66 insertions(+), 67 deletions(-) diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index f5b5f9f0f..dc1e86c2c 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -28,7 +28,7 @@ use uv_distribution_types::{ PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef, }; use uv_git::GitResolver; -use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages}; +use uv_installer::{Installer, Plan, Planner, Preparer, SitePackages, SyncModel}; use uv_pypi_types::Conflicts; use uv_python::{Interpreter, PythonEnvironment}; use uv_resolver::{ @@ -319,6 +319,7 @@ impl BuildContext for BuildDispatch<'_> { self.build_options, self.hasher, self.index_locations, + SyncModel::Stateless, self.config_settings, self.config_settings_package, self.extra_build_requires(), diff --git a/crates/uv-installer/src/lib.rs b/crates/uv-installer/src/lib.rs index 414f8f245..7f0b25cc8 100644 --- a/crates/uv-installer/src/lib.rs +++ b/crates/uv-installer/src/lib.rs @@ -2,7 +2,7 @@ pub use compile::{CompileError, compile_tree}; pub use installer::{Installer, Reporter as InstallReporter}; pub use plan::{Plan, Planner}; pub use preparer::{Error as PrepareError, Preparer, Reporter as PrepareReporter}; -pub use site_packages::{SatisfiesResult, SitePackages, SitePackagesDiagnostic}; +pub use site_packages::{SatisfiesResult, SitePackages, SitePackagesDiagnostic, SyncModel}; pub use uninstall::{UninstallError, uninstall}; mod compile; diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 4659173f3..49a2eda48 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -25,6 +25,7 @@ use uv_types::HashStrategy; use crate::SitePackages; use crate::satisfies::RequirementSatisfaction; +use crate::site_packages::SyncModel; /// A planner to generate an [`Plan`] based on a set of requirements. #[derive(Debug)] @@ -56,6 +57,7 @@ impl<'a> Planner<'a> { build_options: &BuildOptions, hasher: &HashStrategy, index_locations: &IndexLocations, + model: SyncModel, config_settings: &ConfigSettings, config_settings_package: &PackageConfigSettings, extra_build_requires: &ExtraBuildRequires, @@ -125,6 +127,7 @@ impl<'a> Planner<'a> { dist.name(), installed, &source, + model, config_settings, config_settings_package, extra_build_requires, diff --git a/crates/uv-installer/src/satisfies.rs b/crates/uv-installer/src/satisfies.rs index 184d1ad8d..b2c0d98c0 100644 --- a/crates/uv-installer/src/satisfies.rs +++ b/crates/uv-installer/src/satisfies.rs @@ -5,6 +5,7 @@ use same_file::is_same_file; use tracing::{debug, trace}; use url::Url; +use crate::site_packages::SyncModel; use uv_cache_info::CacheInfo; use uv_cache_key::{CanonicalUrl, RepositoryUrl}; use uv_distribution_types::{ @@ -32,6 +33,7 @@ impl RequirementSatisfaction { name: &PackageName, distribution: &InstalledDist, source: &RequirementSource, + model: SyncModel, config_settings: &ConfigSettings, config_settings_package: &PackageConfigSettings, extra_build_requires: &ExtraBuildRequires, @@ -55,6 +57,7 @@ impl RequirementSatisfaction { ); dist_build_info != &build_info }) { + // If the requirement came from a registry, debug!("Build info mismatch for {name}: {distribution:?}"); return Self::OutOfDate; } @@ -63,6 +66,25 @@ impl RequirementSatisfaction { match source { // If the requirement comes from a registry, check by name. RequirementSource::Registry { specifier, .. } => { + // If the installed distribution is _not_ from a registry, reject it if and only if + // we're in a stateless install. + // + // For example: the `uv pip` CLI is stateful, in that it "respects" + // already-installed packages in the virtual environment. So if you run `uv pip + // install ./path/to/idna`, and then `uv pip install anyio` (which depends on + // `idna`), we'll "accept" the already-installed `idna` even though it is implicitly + // being "required" as a registry package. + // + // The `uv sync` CLI is stateless, in that all requirements must be defined + // declaratively ahead-of-time. So if you `uv sync` to install `./path/to/idna` and + // later `uv sync` to install `anyio`, we'll know (during that second sync) if the + // already-installed `idna` should come from the registry or not. + if model == SyncModel::Stateless { + if !matches!(distribution, InstalledDist::Registry { .. }) { + debug!("Distribution type mismatch for {name}: {distribution:?}"); + return Self::Mismatch; + } + } if specifier.contains(distribution.version()) { return Self::Satisfied; } diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 02a035401..e68df41a8 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -296,6 +296,7 @@ impl SitePackages { constraints: &[NameRequirementSpecification], overrides: &[UnresolvedRequirementSpecification], markers: &ResolverMarkerEnvironment, + model: SyncModel, config_settings: &ConfigSettings, config_settings_package: &PackageConfigSettings, extra_build_requires: &ExtraBuildRequires, @@ -385,6 +386,7 @@ impl SitePackages { constraints.iter().map(|constraint| &constraint.requirement), overrides.iter().map(Cow::as_ref), markers, + model, config_settings, config_settings_package, extra_build_requires, @@ -399,6 +401,7 @@ impl SitePackages { constraints: impl Iterator, overrides: impl Iterator, markers: &ResolverMarkerEnvironment, + model: SyncModel, config_settings: &ConfigSettings, config_settings_package: &PackageConfigSettings, extra_build_requires: &ExtraBuildRequires, @@ -460,6 +463,7 @@ impl SitePackages { name, distribution, &requirement.source, + model, config_settings, config_settings_package, extra_build_requires, @@ -481,6 +485,7 @@ impl SitePackages { name, distribution, &constraint.source, + model, config_settings, config_settings_package, extra_build_requires, @@ -536,6 +541,19 @@ impl SitePackages { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SyncModel { + /// A stateful sync, as in the `uv pip` CLI, whereby packages that are already installed in + /// the environment may be reused if they implicitly match the requirements. For example, if + /// the user installs `./path/to/idna`, then runs `uv pip install anyio` (which depends on + /// `idna`), the existing `idna` installation will be reused if it satisfies the requirements, + /// even though it is implicitly being requested from the registry. + Stateful, + /// A stateless sync, as in the `uv sync` CLI, whereby the sources of all packages are defined + /// declaratively upfront. + Stateless, +} + /// We check if all requirements are already satisfied, recursing through the requirements tree. #[derive(Debug)] pub enum SatisfiesResult { diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 456e664e6..a57c9f3fc 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -22,7 +22,7 @@ use uv_distribution_types::{ }; use uv_fs::Simplified; use uv_install_wheel::LinkMode; -use uv_installer::{SatisfiesResult, SitePackages}; +use uv_installer::{SatisfiesResult, SitePackages, SyncModel}; use uv_normalize::{DefaultExtras, DefaultGroups}; use uv_pypi_types::Conflicts; use uv_python::{ @@ -290,6 +290,7 @@ pub(crate) async fn pip_install( &constraints, &overrides, &marker_env, + SyncModel::Stateful, config_settings, config_settings_package, &extra_build_requires, @@ -601,6 +602,7 @@ pub(crate) async fn pip_install( match operations::install( &resolution, site_packages, + SyncModel::Stateful, modifications, &reinstall, &build_options, diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 7db3d6a88..012e5a854 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -25,7 +25,7 @@ use uv_distribution_types::{ use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution}; use uv_fs::Simplified; use uv_install_wheel::LinkMode; -use uv_installer::{Plan, Planner, Preparer, SitePackages}; +use uv_installer::{Plan, Planner, Preparer, SitePackages, SyncModel}; use uv_normalize::PackageName; use uv_pep508::{MarkerEnvironment, RequirementOrigin}; use uv_platform_tags::Tags; @@ -433,6 +433,7 @@ impl Changelog { pub(crate) async fn install( resolution: &Resolution, site_packages: SitePackages, + model: SyncModel, modifications: Modifications, reinstall: &Reinstall, build_options: &BuildOptions, @@ -463,6 +464,7 @@ pub(crate) async fn install( build_options, hasher, build_dispatch.locations(), + model, build_dispatch.config_settings(), build_dispatch.config_settings_package(), build_dispatch.extra_build_requires(), diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 46d5a5e0e..4bb436ee3 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -20,7 +20,7 @@ use uv_distribution_types::{ }; use uv_fs::Simplified; use uv_install_wheel::LinkMode; -use uv_installer::SitePackages; +use uv_installer::{SitePackages, SyncModel}; use uv_normalize::{DefaultExtras, DefaultGroups}; use uv_pypi_types::Conflicts; use uv_python::{ @@ -531,6 +531,7 @@ pub(crate) async fn pip_sync( match operations::install( &resolution, site_packages, + SyncModel::Stateful, Modifications::Exact, &reinstall, &build_options, diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 4807b8fb4..43e1e8939 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -23,7 +23,7 @@ use uv_distribution_types::{ }; use uv_fs::{CWD, LockedFile, Simplified}; use uv_git::ResolvedRepositoryReference; -use uv_installer::{SatisfiesResult, SitePackages}; +use uv_installer::{SatisfiesResult, SitePackages, SyncModel}; use uv_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName}; use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers}; use uv_pep508::MarkerTreeContents; @@ -2158,6 +2158,7 @@ pub(crate) async fn sync_environment( pip::operations::install( resolution, site_packages, + SyncModel::Stateless, modifications, reinstall, build_options, @@ -2281,6 +2282,7 @@ pub(crate) async fn update_environment( &constraints, &overrides, &marker_env, + SyncModel::Stateless, config_setting, config_settings_package, &extra_build_requires, @@ -2428,6 +2430,7 @@ pub(crate) async fn update_environment( let changelog = pip::operations::install( &resolution, site_packages, + SyncModel::Stateless, modifications, reinstall, build_options, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 11d126f74..707e45502 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -25,7 +25,7 @@ use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::Requirement; use uv_fs::which::is_executable; use uv_fs::{PythonExt, Simplified, create_symlink}; -use uv_installer::{SatisfiesResult, SitePackages}; +use uv_installer::{SatisfiesResult, SitePackages, SyncModel}; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_python::{ EnvironmentPreference, Interpreter, PyVenvConfiguration, PythonDownloads, PythonEnvironment, @@ -1363,6 +1363,7 @@ fn can_skip_ephemeral( &spec.constraints, &spec.overrides, &interpreter.resolver_marker_environment(), + SyncModel::Stateless, config_setting, config_settings_package, &extra_build_requires, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index b52c5cc75..0ea28c813 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -19,11 +19,10 @@ use uv_configuration::{ use uv_dispatch::BuildDispatch; use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::{ - BuiltDist, DirectorySourceDist, Dist, Index, Name, Requirement, Resolution, ResolvedDist, - SourceDist, + DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; -use uv_installer::SitePackages; +use uv_installer::{SitePackages, SyncModel}; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_pep508::{MarkerTree, VersionOrUrl}; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; @@ -787,67 +786,13 @@ pub(super) async fn do_sync( let site_packages = SitePackages::from_environment(venv)?; - // When --no-sources is used, force reinstall only when the current install is editable/URL - // but the resolution selects a registry distribution for that package. This avoids - // unnecessary reinstalls for packages that are legitimately specified via direct URL. - let reinstall = if matches!(sources, uv_configuration::SourceStrategy::Disabled) { - tracing::debug!("Sources are disabled; evaluating packages to selectively reinstall"); - - // Collect package names which the resolution intends to install from a registry. - let resolved_registry_names = { - let mut names = std::collections::HashSet::new(); - for resolved in resolution.distributions() { - if let ResolvedDist::Installable { dist, .. } = resolved { - match dist.as_ref() { - Dist::Built(BuiltDist::Registry(_)) - | Dist::Source(SourceDist::Registry(_)) => { - names.insert(dist.name().clone()); - } - _ => {} - } - } - } - names - }; - - let mut updated_reinstall = reinstall.clone(); - for dist in site_packages.iter() { - let is_url_like = matches!( - dist, - uv_distribution_types::InstalledDist::Url(_) - | uv_distribution_types::InstalledDist::LegacyEditable(_) - ); - if is_url_like && resolved_registry_names.contains(dist.name()) { - tracing::debug!( - "Marking package for reinstall due to --no-sources registry preference: {}", - dist.name() - ); - updated_reinstall = match updated_reinstall { - uv_configuration::Reinstall::None => { - uv_configuration::Reinstall::Packages(vec![dist.name().clone()], Vec::new()) - } - uv_configuration::Reinstall::All => updated_reinstall, - uv_configuration::Reinstall::Packages(mut packages, paths) => { - if !packages.contains(dist.name()) { - packages.push(dist.name().clone()); - } - uv_configuration::Reinstall::Packages(packages, paths) - } - }; - } - } - - updated_reinstall - } else { - reinstall.clone() - }; - // Sync the environment. operations::install( &resolution, site_packages, + SyncModel::Stateless, modifications, - &reinstall, + reinstall, build_options, link_mode, compile_bytecode, diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 09c1ced26..1bb9727c9 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -25,7 +25,7 @@ use uv_distribution_types::{ UnresolvedRequirement, UnresolvedRequirementSpecification, }; use uv_fs::Simplified; -use uv_installer::{SatisfiesResult, SitePackages}; +use uv_installer::{SatisfiesResult, SitePackages, SyncModel}; use uv_normalize::PackageName; use uv_pep440::{VersionSpecifier, VersionSpecifiers}; use uv_pep508::MarkerTree; @@ -972,6 +972,7 @@ async fn get_or_create_environment( constraints.iter(), overrides.iter(), &interpreter.resolver_marker_environment(), + SyncModel::Stateless, config_setting, config_settings_package, &extra_build_requires,