Use separate rules

This commit is contained in:
Charlie Marsh 2025-08-22 12:42:14 +01:00
parent 849071e86b
commit 9a4251104e
12 changed files with 66 additions and 67 deletions

View File

@ -28,7 +28,7 @@ use uv_distribution_types::{
PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef, PackageConfigSettings, Requirement, Resolution, SourceDist, VersionOrUrlRef,
}; };
use uv_git::GitResolver; 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_pypi_types::Conflicts;
use uv_python::{Interpreter, PythonEnvironment}; use uv_python::{Interpreter, PythonEnvironment};
use uv_resolver::{ use uv_resolver::{
@ -319,6 +319,7 @@ impl BuildContext for BuildDispatch<'_> {
self.build_options, self.build_options,
self.hasher, self.hasher,
self.index_locations, self.index_locations,
SyncModel::Stateless,
self.config_settings, self.config_settings,
self.config_settings_package, self.config_settings_package,
self.extra_build_requires(), self.extra_build_requires(),

View File

@ -2,7 +2,7 @@ pub use compile::{CompileError, compile_tree};
pub use installer::{Installer, Reporter as InstallReporter}; pub use installer::{Installer, Reporter as InstallReporter};
pub use plan::{Plan, Planner}; pub use plan::{Plan, Planner};
pub use preparer::{Error as PrepareError, Preparer, Reporter as PrepareReporter}; 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}; pub use uninstall::{UninstallError, uninstall};
mod compile; mod compile;

View File

@ -25,6 +25,7 @@ use uv_types::HashStrategy;
use crate::SitePackages; use crate::SitePackages;
use crate::satisfies::RequirementSatisfaction; use crate::satisfies::RequirementSatisfaction;
use crate::site_packages::SyncModel;
/// A planner to generate an [`Plan`] based on a set of requirements. /// A planner to generate an [`Plan`] based on a set of requirements.
#[derive(Debug)] #[derive(Debug)]
@ -56,6 +57,7 @@ impl<'a> Planner<'a> {
build_options: &BuildOptions, build_options: &BuildOptions,
hasher: &HashStrategy, hasher: &HashStrategy,
index_locations: &IndexLocations, index_locations: &IndexLocations,
model: SyncModel,
config_settings: &ConfigSettings, config_settings: &ConfigSettings,
config_settings_package: &PackageConfigSettings, config_settings_package: &PackageConfigSettings,
extra_build_requires: &ExtraBuildRequires, extra_build_requires: &ExtraBuildRequires,
@ -125,6 +127,7 @@ impl<'a> Planner<'a> {
dist.name(), dist.name(),
installed, installed,
&source, &source,
model,
config_settings, config_settings,
config_settings_package, config_settings_package,
extra_build_requires, extra_build_requires,

View File

@ -5,6 +5,7 @@ use same_file::is_same_file;
use tracing::{debug, trace}; use tracing::{debug, trace};
use url::Url; use url::Url;
use crate::site_packages::SyncModel;
use uv_cache_info::CacheInfo; use uv_cache_info::CacheInfo;
use uv_cache_key::{CanonicalUrl, RepositoryUrl}; use uv_cache_key::{CanonicalUrl, RepositoryUrl};
use uv_distribution_types::{ use uv_distribution_types::{
@ -32,6 +33,7 @@ impl RequirementSatisfaction {
name: &PackageName, name: &PackageName,
distribution: &InstalledDist, distribution: &InstalledDist,
source: &RequirementSource, source: &RequirementSource,
model: SyncModel,
config_settings: &ConfigSettings, config_settings: &ConfigSettings,
config_settings_package: &PackageConfigSettings, config_settings_package: &PackageConfigSettings,
extra_build_requires: &ExtraBuildRequires, extra_build_requires: &ExtraBuildRequires,
@ -55,6 +57,7 @@ impl RequirementSatisfaction {
); );
dist_build_info != &build_info dist_build_info != &build_info
}) { }) {
// If the requirement came from a registry,
debug!("Build info mismatch for {name}: {distribution:?}"); debug!("Build info mismatch for {name}: {distribution:?}");
return Self::OutOfDate; return Self::OutOfDate;
} }
@ -63,6 +66,25 @@ impl RequirementSatisfaction {
match source { match source {
// If the requirement comes from a registry, check by name. // If the requirement comes from a registry, check by name.
RequirementSource::Registry { specifier, .. } => { 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()) { if specifier.contains(distribution.version()) {
return Self::Satisfied; return Self::Satisfied;
} }

View File

@ -296,6 +296,7 @@ impl SitePackages {
constraints: &[NameRequirementSpecification], constraints: &[NameRequirementSpecification],
overrides: &[UnresolvedRequirementSpecification], overrides: &[UnresolvedRequirementSpecification],
markers: &ResolverMarkerEnvironment, markers: &ResolverMarkerEnvironment,
model: SyncModel,
config_settings: &ConfigSettings, config_settings: &ConfigSettings,
config_settings_package: &PackageConfigSettings, config_settings_package: &PackageConfigSettings,
extra_build_requires: &ExtraBuildRequires, extra_build_requires: &ExtraBuildRequires,
@ -385,6 +386,7 @@ impl SitePackages {
constraints.iter().map(|constraint| &constraint.requirement), constraints.iter().map(|constraint| &constraint.requirement),
overrides.iter().map(Cow::as_ref), overrides.iter().map(Cow::as_ref),
markers, markers,
model,
config_settings, config_settings,
config_settings_package, config_settings_package,
extra_build_requires, extra_build_requires,
@ -399,6 +401,7 @@ impl SitePackages {
constraints: impl Iterator<Item = &'a Requirement>, constraints: impl Iterator<Item = &'a Requirement>,
overrides: impl Iterator<Item = &'a Requirement>, overrides: impl Iterator<Item = &'a Requirement>,
markers: &ResolverMarkerEnvironment, markers: &ResolverMarkerEnvironment,
model: SyncModel,
config_settings: &ConfigSettings, config_settings: &ConfigSettings,
config_settings_package: &PackageConfigSettings, config_settings_package: &PackageConfigSettings,
extra_build_requires: &ExtraBuildRequires, extra_build_requires: &ExtraBuildRequires,
@ -460,6 +463,7 @@ impl SitePackages {
name, name,
distribution, distribution,
&requirement.source, &requirement.source,
model,
config_settings, config_settings,
config_settings_package, config_settings_package,
extra_build_requires, extra_build_requires,
@ -481,6 +485,7 @@ impl SitePackages {
name, name,
distribution, distribution,
&constraint.source, &constraint.source,
model,
config_settings, config_settings,
config_settings_package, config_settings_package,
extra_build_requires, 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. /// We check if all requirements are already satisfied, recursing through the requirements tree.
#[derive(Debug)] #[derive(Debug)]
pub enum SatisfiesResult { pub enum SatisfiesResult {

View File

@ -22,7 +22,7 @@ use uv_distribution_types::{
}; };
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_install_wheel::LinkMode; use uv_install_wheel::LinkMode;
use uv_installer::{SatisfiesResult, SitePackages}; use uv_installer::{SatisfiesResult, SitePackages, SyncModel};
use uv_normalize::{DefaultExtras, DefaultGroups}; use uv_normalize::{DefaultExtras, DefaultGroups};
use uv_pypi_types::Conflicts; use uv_pypi_types::Conflicts;
use uv_python::{ use uv_python::{
@ -290,6 +290,7 @@ pub(crate) async fn pip_install(
&constraints, &constraints,
&overrides, &overrides,
&marker_env, &marker_env,
SyncModel::Stateful,
config_settings, config_settings,
config_settings_package, config_settings_package,
&extra_build_requires, &extra_build_requires,
@ -601,6 +602,7 @@ pub(crate) async fn pip_install(
match operations::install( match operations::install(
&resolution, &resolution,
site_packages, site_packages,
SyncModel::Stateful,
modifications, modifications,
&reinstall, &reinstall,
&build_options, &build_options,

View File

@ -25,7 +25,7 @@ use uv_distribution_types::{
use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution}; use uv_distribution_types::{DistributionMetadata, InstalledMetadata, Name, Resolution};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_install_wheel::LinkMode; 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_normalize::PackageName;
use uv_pep508::{MarkerEnvironment, RequirementOrigin}; use uv_pep508::{MarkerEnvironment, RequirementOrigin};
use uv_platform_tags::Tags; use uv_platform_tags::Tags;
@ -433,6 +433,7 @@ impl Changelog {
pub(crate) async fn install( pub(crate) async fn install(
resolution: &Resolution, resolution: &Resolution,
site_packages: SitePackages, site_packages: SitePackages,
model: SyncModel,
modifications: Modifications, modifications: Modifications,
reinstall: &Reinstall, reinstall: &Reinstall,
build_options: &BuildOptions, build_options: &BuildOptions,
@ -463,6 +464,7 @@ pub(crate) async fn install(
build_options, build_options,
hasher, hasher,
build_dispatch.locations(), build_dispatch.locations(),
model,
build_dispatch.config_settings(), build_dispatch.config_settings(),
build_dispatch.config_settings_package(), build_dispatch.config_settings_package(),
build_dispatch.extra_build_requires(), build_dispatch.extra_build_requires(),

View File

@ -20,7 +20,7 @@ use uv_distribution_types::{
}; };
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_install_wheel::LinkMode; use uv_install_wheel::LinkMode;
use uv_installer::SitePackages; use uv_installer::{SitePackages, SyncModel};
use uv_normalize::{DefaultExtras, DefaultGroups}; use uv_normalize::{DefaultExtras, DefaultGroups};
use uv_pypi_types::Conflicts; use uv_pypi_types::Conflicts;
use uv_python::{ use uv_python::{
@ -531,6 +531,7 @@ pub(crate) async fn pip_sync(
match operations::install( match operations::install(
&resolution, &resolution,
site_packages, site_packages,
SyncModel::Stateful,
Modifications::Exact, Modifications::Exact,
&reinstall, &reinstall,
&build_options, &build_options,

View File

@ -23,7 +23,7 @@ use uv_distribution_types::{
}; };
use uv_fs::{CWD, LockedFile, Simplified}; use uv_fs::{CWD, LockedFile, Simplified};
use uv_git::ResolvedRepositoryReference; 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_normalize::{DEV_DEPENDENCIES, DefaultGroups, ExtraName, GroupName, PackageName};
use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers}; use uv_pep440::{TildeVersionSpecifier, Version, VersionSpecifiers};
use uv_pep508::MarkerTreeContents; use uv_pep508::MarkerTreeContents;
@ -2158,6 +2158,7 @@ pub(crate) async fn sync_environment(
pip::operations::install( pip::operations::install(
resolution, resolution,
site_packages, site_packages,
SyncModel::Stateless,
modifications, modifications,
reinstall, reinstall,
build_options, build_options,
@ -2281,6 +2282,7 @@ pub(crate) async fn update_environment(
&constraints, &constraints,
&overrides, &overrides,
&marker_env, &marker_env,
SyncModel::Stateless,
config_setting, config_setting,
config_settings_package, config_settings_package,
&extra_build_requires, &extra_build_requires,
@ -2428,6 +2430,7 @@ pub(crate) async fn update_environment(
let changelog = pip::operations::install( let changelog = pip::operations::install(
&resolution, &resolution,
site_packages, site_packages,
SyncModel::Stateless,
modifications, modifications,
reinstall, reinstall,
build_options, build_options,

View File

@ -25,7 +25,7 @@ use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::Requirement; use uv_distribution_types::Requirement;
use uv_fs::which::is_executable; use uv_fs::which::is_executable;
use uv_fs::{PythonExt, Simplified, create_symlink}; 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_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_python::{ use uv_python::{
EnvironmentPreference, Interpreter, PyVenvConfiguration, PythonDownloads, PythonEnvironment, EnvironmentPreference, Interpreter, PyVenvConfiguration, PythonDownloads, PythonEnvironment,
@ -1363,6 +1363,7 @@ fn can_skip_ephemeral(
&spec.constraints, &spec.constraints,
&spec.overrides, &spec.overrides,
&interpreter.resolver_marker_environment(), &interpreter.resolver_marker_environment(),
SyncModel::Stateless,
config_setting, config_setting,
config_settings_package, config_settings_package,
&extra_build_requires, &extra_build_requires,

View File

@ -19,11 +19,10 @@ use uv_configuration::{
use uv_dispatch::BuildDispatch; use uv_dispatch::BuildDispatch;
use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution::LoweredExtraBuildDependencies;
use uv_distribution_types::{ use uv_distribution_types::{
BuiltDist, DirectorySourceDist, Dist, Index, Name, Requirement, Resolution, ResolvedDist, DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist,
SourceDist,
}; };
use uv_fs::{PortablePathBuf, Simplified}; use uv_fs::{PortablePathBuf, Simplified};
use uv_installer::SitePackages; use uv_installer::{SitePackages, SyncModel};
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName}; use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_pep508::{MarkerTree, VersionOrUrl}; use uv_pep508::{MarkerTree, VersionOrUrl};
use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl}; use uv_pypi_types::{ParsedArchiveUrl, ParsedGitUrl, ParsedUrl};
@ -787,67 +786,13 @@ pub(super) async fn do_sync(
let site_packages = SitePackages::from_environment(venv)?; 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. // Sync the environment.
operations::install( operations::install(
&resolution, &resolution,
site_packages, site_packages,
SyncModel::Stateless,
modifications, modifications,
&reinstall, reinstall,
build_options, build_options,
link_mode, link_mode,
compile_bytecode, compile_bytecode,

View File

@ -25,7 +25,7 @@ use uv_distribution_types::{
UnresolvedRequirement, UnresolvedRequirementSpecification, UnresolvedRequirement, UnresolvedRequirementSpecification,
}; };
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_installer::{SatisfiesResult, SitePackages}; use uv_installer::{SatisfiesResult, SitePackages, SyncModel};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::{VersionSpecifier, VersionSpecifiers}; use uv_pep440::{VersionSpecifier, VersionSpecifiers};
use uv_pep508::MarkerTree; use uv_pep508::MarkerTree;
@ -972,6 +972,7 @@ async fn get_or_create_environment(
constraints.iter(), constraints.iter(),
overrides.iter(), overrides.iter(),
&interpreter.resolver_marker_environment(), &interpreter.resolver_marker_environment(),
SyncModel::Stateless,
config_setting, config_setting,
config_settings_package, config_settings_package,
&extra_build_requires, &extra_build_requires,