From 539d3bd044a5f49b53c67735a4cc74c62a9f2673 Mon Sep 17 00:00:00 2001 From: shikinamiasuka Date: Tue, 12 Aug 2025 16:32:29 +0800 Subject: [PATCH 1/3] Enhance `uv sync` behavior with `--no-sources` option - Added logic to force reinstall URL-based packages when `--no-sources` is specified, ensuring they are replaced with registry versions. - Updated the `do_sync` function to handle the reinstall logic based on the presence of URL-based packages in the environment. - Introduced a new test to verify the correct transition from editable to package installation when using the `--no-sources` flag, addressing Issue #15190. This change improves the reliability of package installations in environments where source installations are disabled. --- crates/uv/src/commands/project/sync.rs | 31 +++++++- crates/uv/tests/it/sync.rs | 102 +++++++++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 7cc8a2aade3c9..e49bab4ee6ef3 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -19,7 +19,7 @@ use uv_configuration::{ use uv_dispatch::BuildDispatch; use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, Requirement, Resolution, ResolvedDist, SourceDist, + DirectorySourceDist, Dist, Index, Name, Requirement, Resolution, ResolvedDist, SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; @@ -788,12 +788,39 @@ pub(super) async fn do_sync( let site_packages = SitePackages::from_environment(venv)?; + // When --no-sources is used, force reinstall of URL-based packages + // to ensure they are replaced with registry versions + let reinstall = if matches!(sources, uv_configuration::SourceStrategy::Disabled) { + tracing::debug!("Sources are disabled, checking for URL-based packages to reinstall"); + let mut updated_reinstall = reinstall.clone(); + + for dist in site_packages.iter() { + if matches!(dist, uv_distribution_types::InstalledDist::Url(_)) { + tracing::debug!("Marking URL-based package for reinstall: {}", 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) => { + packages.push(dist.name().clone()); + uv_configuration::Reinstall::Packages(packages, paths) + } + }; + } + } + + updated_reinstall + } else { + reinstall.clone() + }; + // Sync the environment. operations::install( &resolution, site_packages, modifications, - reinstall, + &reinstall, build_options, link_mode, compile_bytecode, diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index b2ff3a16a8f99..7e163e382e58a 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -12904,3 +12904,105 @@ fn sync_extra_build_variables() -> Result<()> { Ok(()) } + +/// Test for Issue #15190: `uv sync --no-sources` should consistently switch from editable to package installation +/// +/// This reproduces the bug where editable → package transitions are not detected when using --no-sources +#[test] +fn sync_no_sources_editable_to_package_switch() -> Result<()> { + let context = TestContext::new("3.12"); + + // Create a local package that will be used as editable dependency + let local_dep = context.temp_dir.child("local_dep"); + local_dep.create_dir_all()?; + + let local_dep_pyproject = local_dep.child("pyproject.toml"); + local_dep_pyproject.write_str( + r#" + [project] + name = "anyio" + version = "4.3.0" + description = "Local test package mimicking anyio" + requires-python = ">=3.8" + + [build-system] + requires = ["setuptools>=61", "wheel"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // Create main project with editable source for the local dependency + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "test_no_sources" + version = "0.0.1" + requires-python = ">=3.12" + dependencies = ["anyio"] + + [tool.uv.sources] + anyio = { path = "./local_dep", editable = true } + + [build-system] + requires = ["setuptools>=67"] + build-backend = "setuptools.build_meta" + + [tool.setuptools.packages.find] + exclude = ["local_dep*"] + "#, + )?; + + // Step 1: `uv sync --no-sources` - should install anyio from PyPI + uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Prepared 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + + test-no-sources==0.0.1 (from file://[TEMP_DIR]/) + "); + + // Step 2: `uv sync` - should switch to editable installation + uv_snapshot!(context.filters(), context.sync(), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 3 packages in [TIME] + Installed 1 package in [TIME] + - anyio==4.3.0 + + anyio==4.3.0 (from file://[TEMP_DIR]/local_dep) + - idna==3.6 + - sniffio==1.3.1 + "); + + // Step 3: `uv sync --no-sources` again - should switch back to PyPI package + // This is the failing case from Issue #15190 - currently shows no changes + uv_snapshot!(context.filters(), context.sync().arg("--no-sources"), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Uninstalled 2 packages in [TIME] + Installed 4 packages in [TIME] + - anyio==4.3.0 (from file://[TEMP_DIR]/local_dep) + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + ~ test-no-sources==0.0.1 (from file://[TEMP_DIR]/) + "); + + Ok(()) +} From b552e402ab80b3788e960e4804e72cbcf7013951 Mon Sep 17 00:00:00 2001 From: shikinamiasuka Date: Tue, 12 Aug 2025 17:18:46 +0800 Subject: [PATCH 2/3] Refine `--no-sources` reinstall logic in `do_sync` function - Updated the logic to selectively reinstall URL-based packages only when the resolution selects a registry distribution, preventing unnecessary reinstalls for packages specified via direct URL. - Enhanced debug logging for better clarity on package evaluation during the reinstall process. - Adjusted related test expectations to reflect the changes in package installation behavior. This change improves the efficiency of package management when source installations are disabled. --- crates/uv/src/commands/project/sync.rs | 45 +++++++++++++++++++++----- crates/uv/tests/it/sync.rs | 5 ++- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index e49bab4ee6ef3..f2580d5cb782f 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -19,7 +19,8 @@ use uv_configuration::{ use uv_dispatch::BuildDispatch; use uv_distribution::LoweredExtraBuildDependencies; use uv_distribution_types::{ - DirectorySourceDist, Dist, Index, Name, Requirement, Resolution, ResolvedDist, SourceDist, + BuiltDist, DirectorySourceDist, Dist, Index, Name, Requirement, Resolution, ResolvedDist, + SourceDist, }; use uv_fs::{PortablePathBuf, Simplified}; use uv_installer::SitePackages; @@ -788,22 +789,50 @@ pub(super) async fn do_sync( let site_packages = SitePackages::from_environment(venv)?; - // When --no-sources is used, force reinstall of URL-based packages - // to ensure they are replaced with registry versions + // 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, checking for URL-based packages to reinstall"); - let mut updated_reinstall = reinstall.clone(); + 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() { - if matches!(dist, uv_distribution_types::InstalledDist::Url(_)) { - tracing::debug!("Marking URL-based package for reinstall: {}", dist.name()); + 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) => { - packages.push(dist.name().clone()); + if !packages.contains(dist.name()) { + packages.push(dist.name().clone()); + } uv_configuration::Reinstall::Packages(packages, paths) } }; diff --git a/crates/uv/tests/it/sync.rs b/crates/uv/tests/it/sync.rs index 7e163e382e58a..9cd55b57b0899 100644 --- a/crates/uv/tests/it/sync.rs +++ b/crates/uv/tests/it/sync.rs @@ -12995,13 +12995,12 @@ fn sync_no_sources_editable_to_package_switch() -> Result<()> { ----- stderr ----- Resolved 4 packages in [TIME] - Uninstalled 2 packages in [TIME] - Installed 4 packages in [TIME] + Uninstalled 1 package in [TIME] + Installed 3 packages in [TIME] - anyio==4.3.0 (from file://[TEMP_DIR]/local_dep) + anyio==4.3.0 + idna==3.6 + sniffio==1.3.1 - ~ test-no-sources==0.0.1 (from file://[TEMP_DIR]/) "); Ok(()) From 9a4251104ea15d0f6b5c69cb14c7a2f413f97658 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 22 Aug 2025 12:42:14 +0100 Subject: [PATCH 3/3] 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 f5b5f9f0f4ee5..dc1e86c2c8d74 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 414f8f245e5a0..7f0b25cc897d6 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 4659173f303a3..49a2eda4888fe 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 184d1ad8d0863..b2c0d98c0b869 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 02a035401a12b..e68df41a86d93 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 456e664e617a8..a57c9f3fca564 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 7db3d6a88560b..012e5a854812c 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 46d5a5e0e3883..4bb436ee37f2f 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 4807b8fb489a4..43e1e8939fd40 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 11d126f74e375..707e45502bd35 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 b52c5cc75c79c..0ea28c81337d5 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 09c1ced260c1c..1bb9727c96b74 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,