diff --git a/crates/distribution-types/src/file.rs b/crates/distribution-types/src/file.rs index 0b47cf3ff644..14ea09a736d7 100644 --- a/crates/distribution-types/src/file.rs +++ b/crates/distribution-types/src/file.rs @@ -1,3 +1,4 @@ +use std::borrow::Cow; use std::fmt::{self, Display, Formatter}; use std::path::PathBuf; use std::str::FromStr; @@ -197,6 +198,12 @@ impl From<&Url> for UrlString { } } +impl From> for UrlString { + fn from(value: Cow<'_, Url>) -> Self { + UrlString(value.to_string()) + } +} + impl From for UrlString { fn from(value: VerbatimUrl) -> Self { UrlString(value.raw().to_string()) diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index 22b76c2d25f0..c0a23bf1f209 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -348,6 +348,15 @@ impl IndexLocations { no_index: self.no_index || no_index, } } + + /// Returns `true` if no index configuration is set, i.e., the [`IndexLocations`] matches the + /// default configuration. + pub fn is_none(&self) -> bool { + self.index.is_none() + && self.extra_index.is_empty() + && self.flat_index.is_empty() + && !self.no_index + } } impl<'a> IndexLocations { diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index cee6efc5ebbe..f94ba9f7901b 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -1210,10 +1210,19 @@ impl Package { &self.id.version } + /// Return the fork markers for this package, if any. pub fn fork_markers(&self) -> Option<&BTreeSet> { self.fork_markers.as_ref() } + /// Return the index URL for this package, if it is a registry source. + pub fn index(&self) -> Option<&UrlString> { + match &self.id.source { + Source::Registry(url) => Some(url), + _ => None, + } + } + /// Returns a [`VersionId`] for this package that can be used for resolution. fn version_id(&self, workspace_root: &Path) -> Result { match &self.id.source { diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 7691ef717077..76cdfcfc2631 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -8,7 +8,9 @@ use owo_colors::OwoColorize; use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; -use distribution_types::{Diagnostic, UnresolvedRequirementSpecification}; +use distribution_types::{ + Diagnostic, FlatIndexLocation, IndexUrl, UnresolvedRequirementSpecification, UrlString, +}; use pep440_rs::Version; use uv_auth::store_credentials_from_url; use uv_cache::Cache; @@ -427,7 +429,53 @@ async fn do_lock( existing_lock.and_then(|lock| lock.fork_markers().clone()) }); - let resolution = match existing_lock.filter(|_| upgrade.is_none()) { + // If any upgrades are specified, don't use the existing lockfile. + let existing_lock = existing_lock.filter(|_| upgrade.is_none()); + + // If the user provided at least one index URL (from the command line, or from a configuration + // file), don't use the existing lockfile if it references any registries that are no longer + // included in the current configuration. + let existing_lock = existing_lock.filter(|lock| { + // If _no_ indexes were provided, we assume that the user wants to reuse the existing + // distributions, even though a failure to reuse the lockfile will result in re-resolving + // against PyPI by default. + if settings.index_locations.is_none() { + return true; + } + + // Collect the set of available indexes (both `--index-url` and `--find-links` entries). + let indexes = settings + .index_locations + .indexes() + .map(IndexUrl::redacted) + .chain( + settings + .index_locations + .flat_index() + .map(FlatIndexLocation::redacted), + ) + .map(UrlString::from) + .collect::>(); + + // Find any packages in the lockfile that reference a registry that is no longer included in + // the current configuration. + for package in lock.packages() { + let Some(index) = package.index() else { + continue; + }; + if !indexes.contains(index) { + let _ = writeln!( + printer.stderr(), + "Ignoring existing lockfile due to removal of referenced registry: {index}" + ); + return false; + } + } + + true + }); + + let resolution = match existing_lock { None => None, // Try to resolve using metadata in the lockfile. diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 84c41d8cc52e..ee4dfe315dcf 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -7652,3 +7652,121 @@ fn unconditional_overlapping_marker_disjoint_version_constraints() -> Result<()> Ok(()) } + +/// Change indexes between locking operations. +#[test] +fn lock_change_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().arg("--index-url").arg("https://public:heron@pypi-proxy.fly.dev/basic-auth/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi-proxy.fly.dev/basic-auth/simple" } + sdist = { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://pypi-proxy.fly.dev/basic-auth/files/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + "### + ); + }); + + // Re-run against PyPI. + uv_snapshot!(context.filters(), context.lock().arg("--index-url").arg("https://pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Ignoring existing lockfile due to removal of referenced registry: https://pypi-proxy.fly.dev/basic-auth/simple + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25 00:00:00 UTC" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "iniconfig" }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + warning: `uv lock` is experimental and may change without warning + Resolved 2 packages in [TIME] + "###); + + Ok(()) +} diff --git a/crates/uv/tests/lock_scenarios.rs b/crates/uv/tests/lock_scenarios.rs index e378dfed5974..17f0a1da7eaf 100644 --- a/crates/uv/tests/lock_scenarios.rs +++ b/crates/uv/tests/lock_scenarios.rs @@ -117,7 +117,7 @@ fn fork_allows_non_conflicting_non_overlapping_dependencies() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -225,7 +225,7 @@ fn fork_allows_non_conflicting_repeated_dependencies() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -341,7 +341,7 @@ fn fork_basic() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -676,7 +676,7 @@ fn fork_filter_sibling_dependencies() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -789,7 +789,7 @@ fn fork_upgrade() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -939,7 +939,7 @@ fn fork_incomplete_markers() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -1069,7 +1069,7 @@ fn fork_marker_accrue() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -1310,7 +1310,7 @@ fn fork_marker_inherit_combined_allowed() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -1474,7 +1474,7 @@ fn fork_marker_inherit_combined_disallowed() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -1639,7 +1639,7 @@ fn fork_marker_inherit_combined() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -1777,7 +1777,7 @@ fn fork_marker_inherit_isolated() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -1933,7 +1933,7 @@ fn fork_marker_inherit_transitive() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -2061,7 +2061,7 @@ fn fork_marker_inherit() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -2217,7 +2217,7 @@ fn fork_marker_limited_inherit() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -2355,7 +2355,7 @@ fn fork_marker_selection() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -2517,7 +2517,7 @@ fn fork_marker_track() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -2646,7 +2646,7 @@ fn fork_non_fork_marker_transitive() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -2925,7 +2925,7 @@ fn fork_overlapping_markers_basic() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -3164,7 +3164,7 @@ fn preferences_dependent_forking_bistable() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -3586,7 +3586,7 @@ fn preferences_dependent_forking_tristable() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -3782,7 +3782,7 @@ fn preferences_dependent_forking() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -3960,7 +3960,7 @@ fn fork_remaining_universe_partitioning() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -4043,7 +4043,7 @@ fn fork_requires_python_full_prerelease() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -4126,7 +4126,7 @@ fn fork_requires_python_full() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -4225,7 +4225,7 @@ fn fork_requires_python_patch_overlap() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; @@ -4305,7 +4305,7 @@ fn fork_requires_python() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?; diff --git a/scripts/scenarios/templates/lock.mustache b/scripts/scenarios/templates/lock.mustache index 150a26b79596..4277a50112f5 100644 --- a/scripts/scenarios/templates/lock.mustache +++ b/scripts/scenarios/templates/lock.mustache @@ -76,7 +76,7 @@ fn {{module_name}}() -> Result<()> { .lock() .env_remove("UV_EXCLUDE_NEWER") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.31/simple-html/") + .arg(packse_index_url()) .assert() .success(); let lock2 = fs_err::read_to_string(context.temp_dir.join("uv.lock"))?;