diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index ce662d65c..8a472b373 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -7,7 +7,7 @@ use uv_configuration::Upgrade; use uv_fs::CWD; use uv_git::ResolvedRepositoryReference; use uv_requirements_txt::RequirementsTxt; -use uv_resolver::{Lock, Preference, PreferenceError}; +use uv_resolver::{Lock, LockError, Preference, PreferenceError}; #[derive(Debug, Default)] pub struct LockedRequirements { @@ -63,7 +63,11 @@ pub async fn read_requirements_txt( } /// Load the preferred requirements from an existing lockfile, applying the upgrade strategy. -pub fn read_lock_requirements(lock: &Lock, upgrade: &Upgrade) -> LockedRequirements { +pub fn read_lock_requirements( + lock: &Lock, + install_path: &Path, + upgrade: &Upgrade, +) -> Result { let mut preferences = Vec::new(); let mut git = Vec::new(); @@ -74,7 +78,7 @@ pub fn read_lock_requirements(lock: &Lock, upgrade: &Upgrade) -> LockedRequireme } // Map each entry in the lockfile to a preference. - preferences.push(Preference::from_lock(package)); + preferences.push(Preference::from_lock(package, install_path)?); // Map each entry in the lockfile to a Git SHA. if let Some(git_ref) = package.as_git_ref() { @@ -82,5 +86,5 @@ pub fn read_lock_requirements(lock: &Lock, upgrade: &Upgrade) -> LockedRequireme } } - LockedRequirements { preferences, git } + Ok(LockedRequirements { preferences, git }) } diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index cd66e4fba..a3cbca992 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -4,7 +4,7 @@ use std::fmt::{Display, Formatter}; use tracing::{debug, trace}; use uv_configuration::IndexStrategy; -use uv_distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource}; +use uv_distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource, IndexUrl}; use uv_distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist}; use uv_normalize::PackageName; use uv_pep440::Version; @@ -80,11 +80,12 @@ impl CandidateSelector { preferences: &'a Preferences, installed_packages: &'a InstalledPackages, exclusions: &'a Exclusions, + index: Option<&'a IndexUrl>, env: &ResolverEnvironment, ) -> Option> { let is_excluded = exclusions.contains(package_name); - // Check for a preference from a lockfile or a previous fork that satisfies the range and + // Check for a preference from a lockfile or a previous fork that satisfies the range and // is allowed. if let Some(preferred) = self.get_preferred( package_name, @@ -93,6 +94,7 @@ impl CandidateSelector { preferences, installed_packages, is_excluded, + index, env, ) { trace!("Using preference {} {}", preferred.name, preferred.version); @@ -131,23 +133,39 @@ impl CandidateSelector { preferences: &'a Preferences, installed_packages: &'a InstalledPackages, is_excluded: bool, + index: Option<&'a IndexUrl>, env: &ResolverEnvironment, ) -> Option { // In the branches, we "sort" the preferences by marker-matching through an iterator that // first has the matching half and then the mismatching half. - let preferences_match = preferences.get(package_name).filter(|(marker, _version)| { - // `.unwrap_or(true)` because the universal marker is considered matching. - marker - .map(|marker| env.included_by_marker(marker)) - .unwrap_or(true) - }); - let preferences_mismatch = preferences.get(package_name).filter(|(marker, _version)| { - marker - .map(|marker| !env.included_by_marker(marker)) - .unwrap_or(false) - }); + let preferences_match = + preferences + .get(package_name) + .filter(|(marker, _index, _version)| { + // `.unwrap_or(true)` because the universal marker is considered matching. + marker + .map(|marker| env.included_by_marker(marker)) + .unwrap_or(true) + }); + let preferences_mismatch = + preferences + .get(package_name) + .filter(|(marker, _index, _version)| { + marker + .map(|marker| !env.included_by_marker(marker)) + .unwrap_or(false) + }); + let preferences = preferences_match.chain(preferences_mismatch).filter_map( + |(marker, source, version)| { + // If the package is mapped to an explicit index, only consider preferences that + // match the index. + index + .map_or(true, |index| source == Some(index)) + .then_some((marker, version)) + }, + ); self.get_preferred_from_iter( - preferences_match.chain(preferences_mismatch), + preferences, package_name, range, version_maps, diff --git a/crates/uv-resolver/src/preferences.rs b/crates/uv-resolver/src/preferences.rs index 42be41b03..c8d571b58 100644 --- a/crates/uv-resolver/src/preferences.rs +++ b/crates/uv-resolver/src/preferences.rs @@ -1,16 +1,17 @@ +use std::path::Path; use std::str::FromStr; use rustc_hash::FxHashMap; use tracing::trace; -use uv_distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Name}; +use uv_distribution_types::{IndexUrl, InstalledDist, InstalledMetadata, InstalledVersion, Name}; use uv_normalize::PackageName; use uv_pep440::{Operator, Version}; use uv_pep508::{MarkerTree, VersionOrUrl}; use uv_pypi_types::{HashDigest, HashError}; use uv_requirements_txt::{RequirementEntry, RequirementsTxtRequirement}; -use crate::ResolverEnvironment; +use crate::{LockError, ResolverEnvironment}; #[derive(thiserror::Error, Debug)] pub enum PreferenceError { @@ -25,6 +26,8 @@ pub struct Preference { version: Version, /// The markers on the requirement itself (those after the semicolon). marker: MarkerTree, + /// The index URL of the package, if any. + index: Option, /// If coming from a package with diverging versions, the markers of the forks this preference /// is part of, otherwise `None`. fork_markers: Vec, @@ -60,6 +63,7 @@ impl Preference { marker: requirement.marker, // requirements.txt doesn't have fork annotations. fork_markers: vec![], + index: None, hashes: entry .hashes .iter() @@ -79,6 +83,7 @@ impl Preference { name: dist.name().clone(), version: version.clone(), marker: MarkerTree::TRUE, + index: None, // Installed distributions don't have fork annotations. fork_markers: vec![], hashes: Vec::new(), @@ -86,14 +91,18 @@ impl Preference { } /// Create a [`Preference`] from a locked distribution. - pub fn from_lock(package: &crate::lock::Package) -> Self { - Self { + pub fn from_lock( + package: &crate::lock::Package, + install_path: &Path, + ) -> Result { + Ok(Self { name: package.id.name.clone(), version: package.id.version.clone(), marker: MarkerTree::TRUE, + index: package.index(install_path)?, fork_markers: package.fork_markers().to_vec(), hashes: Vec::new(), - } + }) } /// Return the [`PackageName`] of the package for this [`Preference`]. @@ -107,6 +116,13 @@ impl Preference { } } +#[derive(Debug, Clone)] +struct Entry { + marker: Option, + index: Option, + pin: Pin, +} + /// A set of pinned packages that should be preserved during resolution, if possible. /// /// The marker is the marker of the fork that resolved to the pin, if any. @@ -114,15 +130,15 @@ impl Preference { /// Preferences should be prioritized first by whether their marker matches and then by the order /// they are stored, so that a lockfile has higher precedence than sibling forks. #[derive(Debug, Clone, Default)] -pub struct Preferences(FxHashMap, Pin)>>); +pub struct Preferences(FxHashMap>); impl Preferences { /// Create a map of pinned packages from an iterator of [`Preference`] entries. /// /// The provided [`ResolverEnvironment`] will be used to filter the preferences /// to an applicable subset. - pub fn from_iter>( - preferences: PreferenceIterator, + pub fn from_iter( + preferences: impl IntoIterator, env: &ResolverEnvironment, ) -> Self { let mut slf = Self::default(); @@ -152,6 +168,7 @@ impl Preferences { if preference.fork_markers.is_empty() { slf.insert( preference.name, + preference.index, None, Pin { version: preference.version, @@ -162,6 +179,7 @@ impl Preferences { for fork_marker in preference.fork_markers { slf.insert( preference.name.clone(), + preference.index.clone(), Some(fork_marker), Pin { version: preference.version.clone(), @@ -179,13 +197,15 @@ impl Preferences { pub(crate) fn insert( &mut self, package_name: PackageName, + index: Option, markers: Option, pin: impl Into, ) { - self.0 - .entry(package_name) - .or_default() - .push((markers, pin.into())); + self.0.entry(package_name).or_default().push(Entry { + marker: markers, + index, + pin: pin.into(), + }); } /// Returns an iterator over the preferences. @@ -194,15 +214,19 @@ impl Preferences { ) -> impl Iterator< Item = ( &PackageName, - impl Iterator, &Version)>, + impl Iterator, Option<&IndexUrl>, &Version)>, ), > { self.0.iter().map(|(name, preferences)| { ( name, - preferences - .iter() - .map(|(markers, pin)| (markers.as_ref(), pin.version())), + preferences.iter().map(|entry| { + ( + entry.marker.as_ref(), + entry.index.as_ref(), + entry.pin.version(), + ) + }), ) }) } @@ -211,12 +235,14 @@ impl Preferences { pub(crate) fn get( &self, package_name: &PackageName, - ) -> impl Iterator, &Version)> { - self.0 - .get(package_name) - .into_iter() - .flatten() - .map(|(markers, pin)| (markers.as_ref(), pin.version())) + ) -> impl Iterator, Option<&IndexUrl>, &Version)> { + self.0.get(package_name).into_iter().flatten().map(|entry| { + ( + entry.marker.as_ref(), + entry.index.as_ref(), + entry.pin.version(), + ) + }) } /// Return the hashes for a package, if the version matches that of the pin. @@ -229,8 +255,8 @@ impl Preferences { .get(package_name) .into_iter() .flatten() - .find(|(_markers, pin)| pin.version() == version) - .map(|(_markers, pin)| pin.hashes()) + .find(|entry| entry.pin.version() == version) + .map(|entry| entry.pin.hashes()) } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 9371dec9d..35256200c 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -381,6 +381,7 @@ impl ResolverState ResolverState impl Iterator> + 'a { debug!( - "Splitting resolution on {}=={} over {} into {} resolution with separate markers", + "Splitting resolution on {}=={} over {} into {} resolution{} with separate markers", current_state.next, version, diverging_packages .iter() .map(ToString::to_string) .join(", "), - forks.len() + forks.len(), + if forks.len() == 1 { "" } else { "s" } ); assert!(forks.len() >= 2); // This is a somewhat tortured technique to ensure @@ -1075,6 +1077,7 @@ impl ResolverState ResolverState { /// The requirements to include in the environment. requirements: RequirementsSpecification, - /// The lockfile from which to extract preferences. - lock: Option<&'lock Lock>, + /// The lockfile from which to extract preferences, along with the install path. + lock: Option<(&'lock Lock, &'lock Path)>, } impl From for EnvironmentSpecification<'_> { @@ -950,7 +950,7 @@ impl From for EnvironmentSpecification<'_> { impl<'lock> EnvironmentSpecification<'lock> { #[must_use] - pub(crate) fn with_lock(self, lock: Option<&'lock Lock>) -> Self { + pub(crate) fn with_lock(self, lock: Option<(&'lock Lock, &'lock Path)>) -> Self { Self { lock, ..self } } } @@ -1057,7 +1057,8 @@ pub(crate) async fn resolve_environment<'a>( // If an existing lockfile exists, build up a set of preferences. let LockedRequirements { preferences, git } = spec .lock - .map(|lock| read_lock_requirements(lock, &upgrade)) + .map(|(lock, install_path)| read_lock_requirements(lock, install_path, &upgrade)) + .transpose()? .unwrap_or_default(); // Populate the Git resolver. diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index ad5271f43..e9c038c50 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -372,7 +372,7 @@ pub(crate) async fn run( }; // The lockfile used for the base environment. - let mut lock: Option = None; + let mut lock: Option<(Lock, PathBuf)> = None; // Discover and sync the base environment. let temp_dir; @@ -609,7 +609,8 @@ pub(crate) async fn run( lock = project::lock::read(project.workspace()) .await .ok() - .flatten(); + .flatten() + .map(|lock| (lock, project.workspace().install_path().to_owned())); } } else { // Validate that any referenced dependency groups are defined in the workspace. @@ -749,7 +750,10 @@ pub(crate) async fn run( Err(err) => return Err(err.into()), } - lock = Some(result.into_lock()); + lock = Some(( + result.into_lock(), + project.workspace().install_path().to_owned(), + )); } venv.into_interpreter() @@ -861,7 +865,10 @@ pub(crate) async fn run( debug!("Syncing ephemeral requirements"); let result = CachedEnvironment::get_or_create( - EnvironmentSpecification::from(spec).with_lock(lock.as_ref()), + EnvironmentSpecification::from(spec).with_lock( + lock.as_ref() + .map(|(lock, install_path)| (lock, install_path.as_ref())), + ), base_interpreter.clone(), &settings, &state, diff --git a/crates/uv/tests/it/edit.rs b/crates/uv/tests/it/edit.rs index 5324bde59..6506b0cc9 100644 --- a/crates/uv/tests/it/edit.rs +++ b/crates/uv/tests/it/edit.rs @@ -6458,7 +6458,11 @@ fn add_index() -> Result<()> { ----- stderr ----- Resolved 4 packages in [TIME] - Audited 3 packages in [TIME] + Prepared 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - jinja2==3.1.3 + + jinja2==3.1.4 "###); let pyproject_toml = fs_err::read_to_string(context.temp_dir.join("pyproject.toml"))?; @@ -6517,14 +6521,14 @@ fn add_index() -> Result<()> { [[package]] name = "jinja2" - version = "3.1.3" + version = "3.1.4" source = { registry = "https://test.pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] - sdist = { url = "https://test-files.pythonhosted.org/packages/3e/f0/69ae37cced6b277dc0419dbb1c6e4fb259e5e319a1a971061a2776316bec/Jinja2-3.1.3.tar.gz", hash = "sha256:27fb536952e578492fa66d8681d8967d8bdf1eb36368b1f842b53251c9f0bfe1", size = 268254 } + sdist = { url = "https://test-files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ - { url = "https://test-files.pythonhosted.org/packages/47/dc/9d1c0f1ddbedb1e67f7d00e91819b5a9157056ad83bfa64c12ecef8a4f4e/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:ddd11470e8a1dc4c30e3146400f0130fed7d85886c5f8082f309355b4b0c1128", size = 133236 }, + { url = "https://test-files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]] @@ -6633,14 +6637,14 @@ fn add_index() -> Result<()> { [[package]] name = "jinja2" - version = "3.1.3" + version = "3.1.4" source = { registry = "https://test.pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] - sdist = { url = "https://test-files.pythonhosted.org/packages/3e/f0/69ae37cced6b277dc0419dbb1c6e4fb259e5e319a1a971061a2776316bec/Jinja2-3.1.3.tar.gz", hash = "sha256:27fb536952e578492fa66d8681d8967d8bdf1eb36368b1f842b53251c9f0bfe1", size = 268254 } + sdist = { url = "https://test-files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ - { url = "https://test-files.pythonhosted.org/packages/47/dc/9d1c0f1ddbedb1e67f7d00e91819b5a9157056ad83bfa64c12ecef8a4f4e/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:ddd11470e8a1dc4c30e3146400f0130fed7d85886c5f8082f309355b4b0c1128", size = 133236 }, + { url = "https://test-files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]] @@ -6758,14 +6762,14 @@ fn add_index() -> Result<()> { [[package]] name = "jinja2" - version = "3.1.3" + version = "3.1.4" source = { registry = "https://test.pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] - sdist = { url = "https://test-files.pythonhosted.org/packages/3e/f0/69ae37cced6b277dc0419dbb1c6e4fb259e5e319a1a971061a2776316bec/Jinja2-3.1.3.tar.gz", hash = "sha256:27fb536952e578492fa66d8681d8967d8bdf1eb36368b1f842b53251c9f0bfe1", size = 268254 } + sdist = { url = "https://test-files.pythonhosted.org/packages/ed/55/39036716d19cab0747a5020fc7e907f362fbf48c984b14e62127f7e68e5d/jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", size = 240245 } wheels = [ - { url = "https://test-files.pythonhosted.org/packages/47/dc/9d1c0f1ddbedb1e67f7d00e91819b5a9157056ad83bfa64c12ecef8a4f4e/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:ddd11470e8a1dc4c30e3146400f0130fed7d85886c5f8082f309355b4b0c1128", size = 133236 }, + { url = "https://test-files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d", size = 133271 }, ] [[package]]