diff --git a/crates/bench/benches/uv.rs b/crates/bench/benches/uv.rs index b7d3accee..dae7558cf 100644 --- a/crates/bench/benches/uv.rs +++ b/crates/bench/benches/uv.rs @@ -88,6 +88,7 @@ mod resolver { use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder}; use platform_tags::{Arch, Os, Platform, Tags}; + use pypi_types::ResolverMarkerEnvironment; use uv_cache::Cache; use uv_client::RegistryClient; use uv_configuration::{ @@ -192,7 +193,7 @@ mod resolver { let markers = if universal { ResolverMarkers::universal(vec![]) } else { - ResolverMarkers::specific_environment(MARKERS.clone()) + ResolverMarkers::specific_environment(ResolverMarkerEnvironment::from(MARKERS.clone())) }; let resolver = Resolver::new( diff --git a/crates/pypi-types/src/lib.rs b/crates/pypi-types/src/lib.rs index 73efeba94..a38dc2487 100644 --- a/crates/pypi-types/src/lib.rs +++ b/crates/pypi-types/src/lib.rs @@ -1,6 +1,7 @@ pub use base_url::*; pub use direct_url::*; pub use lenient_requirement::*; +pub use marker_environment::*; pub use metadata::*; pub use parsed_url::*; pub use requirement::*; @@ -10,6 +11,7 @@ pub use simple_json::*; mod base_url; mod direct_url; mod lenient_requirement; +mod marker_environment; mod metadata; mod parsed_url; mod requirement; diff --git a/crates/pypi-types/src/marker_environment.rs b/crates/pypi-types/src/marker_environment.rs new file mode 100644 index 000000000..063ceb41c --- /dev/null +++ b/crates/pypi-types/src/marker_environment.rs @@ -0,0 +1,53 @@ +use tracing::debug; + +use pep508_rs::MarkerEnvironment; + +/// A wrapper type around [`MarkerEnvironment`] that ensures the Python version markers are +/// release-only, to match the resolver's semantics. +#[derive(Debug, Clone)] +pub struct ResolverMarkerEnvironment(MarkerEnvironment); + +impl ResolverMarkerEnvironment { + /// Returns the underlying [`MarkerEnvironment`]. + pub fn markers(&self) -> &MarkerEnvironment { + &self.0 + } +} + +impl From for ResolverMarkerEnvironment { + fn from(value: MarkerEnvironment) -> Self { + // Strip `python_version`. + let python_version = value.python_version().only_release(); + let value = if python_version == **value.python_version() { + value + } else { + debug!( + "Stripping pre-release from `python_version`: {}", + value.python_version() + ); + value.with_python_version(python_version) + }; + + // Strip `python_full_version`. + let python_full_version = value.python_full_version().only_release(); + let value = if python_full_version == **value.python_full_version() { + value + } else { + debug!( + "Stripping pre-release from `python_full_version`: {}", + value.python_full_version() + ); + value.with_python_full_version(python_full_version) + }; + + Self(value) + } +} + +impl std::ops::Deref for ResolverMarkerEnvironment { + type Target = MarkerEnvironment; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 4a95d4554..b44837b4d 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -138,7 +138,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result { let python_requirement = PythonRequirement::from_interpreter(self.interpreter); - let markers = self.interpreter.markers(); + let markers = self.interpreter.resolver_markers(); let tags = self.interpreter.tags()?; let resolver = Resolver::new( @@ -148,7 +148,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { .index_strategy(self.index_strategy) .build(), &python_requirement, - ResolverMarkers::specific_environment(markers.clone()), + ResolverMarkers::specific_environment(markers), Some(tags), self.flat_index, self.index, @@ -189,6 +189,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { // Determine the current environment markers. let tags = self.interpreter.tags()?; + let markers = self.interpreter.resolver_markers(); // Determine the set of installed packages. let site_packages = SitePackages::from_environment(venv)?; @@ -208,6 +209,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.index_locations, self.cache(), venv, + &markers, tags, )?; diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 8f396c8e0..e0340cf25 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -12,7 +12,7 @@ use distribution_types::{ PathSourceDist, RemoteSource, Verbatim, }; use platform_tags::Tags; -use pypi_types::{Requirement, RequirementSource}; +use pypi_types::{Requirement, RequirementSource, ResolverMarkerEnvironment}; use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache}; use uv_configuration::{BuildOptions, Reinstall}; use uv_distribution::{ @@ -58,6 +58,7 @@ impl<'a> Planner<'a> { index_locations: &IndexLocations, cache: &Cache, venv: &PythonEnvironment, + markers: &ResolverMarkerEnvironment, tags: &Tags, ) -> Result { // Index all the already-downloaded wheels in the cache. @@ -72,7 +73,7 @@ impl<'a> Planner<'a> { for requirement in self.requirements { // Filter out incompatible requirements. - if !requirement.evaluate_markers(Some(venv.interpreter().markers()), &[]) { + if !requirement.evaluate_markers(Some(markers), &[]) { continue; } diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 003c3c5d3..b6de14853 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -11,7 +11,7 @@ use distribution_types::{ Diagnostic, InstalledDist, Name, UnresolvedRequirement, UnresolvedRequirementSpecification, }; use pep440_rs::{Version, VersionSpecifiers}; -use pypi_types::{Requirement, VerbatimParsedUrl}; +use pypi_types::{Requirement, ResolverMarkerEnvironment, VerbatimParsedUrl}; use uv_fs::Simplified; use uv_normalize::PackageName; use uv_python::{Interpreter, PythonEnvironment}; @@ -181,7 +181,10 @@ impl SitePackages { } /// Validate the installed packages in the virtual environment. - pub fn diagnostics(&self) -> Result> { + pub fn diagnostics( + &self, + markers: &ResolverMarkerEnvironment, + ) -> Result> { let mut diagnostics = Vec::new(); for (package, indexes) in &self.by_name { @@ -220,7 +223,7 @@ impl SitePackages { // Verify that the package is compatible with the current Python version. if let Some(requires_python) = metadata.requires_python.as_ref() { - if !requires_python.contains(self.interpreter.python_version()) { + if !requires_python.contains(markers.python_full_version()) { diagnostics.push(SitePackagesDiagnostic::IncompatiblePythonVersion { package: package.clone(), version: self.interpreter.python_version().clone(), @@ -231,7 +234,7 @@ impl SitePackages { // Verify that the dependencies are installed. for dependency in &metadata.requires_dist { - if !dependency.evaluate_markers(self.interpreter.markers(), &[]) { + if !dependency.evaluate_markers(markers, &[]) { continue; } @@ -281,6 +284,7 @@ impl SitePackages { &self, requirements: &[UnresolvedRequirementSpecification], constraints: &[Requirement], + markers: &ResolverMarkerEnvironment, ) -> Result { // Collect the constraints. let constraints: FxHashMap<&PackageName, Vec<&Requirement>> = @@ -299,10 +303,7 @@ impl SitePackages { // Add the direct requirements to the queue. for entry in requirements { - if entry - .requirement - .evaluate_markers(Some(self.interpreter.markers()), &[]) - { + if entry.requirement.evaluate_markers(Some(markers), &[]) { if seen.insert(entry.clone()) { stack.push(entry.clone()); } @@ -353,10 +354,7 @@ impl SitePackages { // Add the dependencies to the queue. for dependency in metadata.requires_dist { - if dependency.evaluate_markers( - self.interpreter.markers(), - entry.requirement.extras(), - ) { + if dependency.evaluate_markers(markers, entry.requirement.extras()) { let dependency = UnresolvedRequirementSpecification { requirement: UnresolvedRequirement::Named(Requirement::from( dependency, diff --git a/crates/uv-python/src/interpreter.rs b/crates/uv-python/src/interpreter.rs index e1405fc88..0f7d4aead 100644 --- a/crates/uv-python/src/interpreter.rs +++ b/crates/uv-python/src/interpreter.rs @@ -17,7 +17,7 @@ use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, StringVersion}; use platform_tags::Platform; use platform_tags::{Tags, TagsError}; -use pypi_types::Scheme; +use pypi_types::{ResolverMarkerEnvironment, Scheme}; use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp}; use uv_fs::{write_atomic_sync, PythonExt, Simplified}; @@ -142,6 +142,11 @@ impl Interpreter { &self.markers } + /// Return the [`ResolverMarkerEnvironment`] for this Python executable. + pub fn resolver_markers(&self) -> ResolverMarkerEnvironment { + ResolverMarkerEnvironment::from(self.markers().clone()) + } + /// Returns the [`PythonInstallationKey`] for this interpreter. pub fn key(&self) -> PythonInstallationKey { PythonInstallationKey::new( diff --git a/crates/uv-python/src/python_version.rs b/crates/uv-python/src/python_version.rs index d261e2564..07ee3edac 100644 --- a/crates/uv-python/src/python_version.rs +++ b/crates/uv-python/src/python_version.rs @@ -84,7 +84,7 @@ impl PythonVersion { /// /// The returned [`MarkerEnvironment`] will preserve the base environment's platform markers, /// but override its Python version markers. - pub fn markers(self, base: &MarkerEnvironment) -> MarkerEnvironment { + pub fn markers(&self, base: &MarkerEnvironment) -> MarkerEnvironment { let mut markers = base.clone(); // Ex) `implementation_version == "3.12.0"` diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index f4d17b813..2146f7e7e 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -6,7 +6,7 @@ use tracing::{debug, trace}; use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource}; use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist}; use pep440_rs::Version; -use pep508_rs::{MarkerEnvironment, MarkerTree}; +use pep508_rs::MarkerTree; use uv_configuration::IndexStrategy; use uv_normalize::PackageName; use uv_types::InstalledPackagesProvider; @@ -30,7 +30,7 @@ impl CandidateSelector { pub(crate) fn for_resolution( options: Options, manifest: &Manifest, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, ) -> Self { Self { resolution_strategy: ResolutionStrategy::from_mode( diff --git a/crates/uv-resolver/src/lock.rs b/crates/uv-resolver/src/lock.rs index 5f99c4ee9..b3b014ba3 100644 --- a/crates/uv-resolver/src/lock.rs +++ b/crates/uv-resolver/src/lock.rs @@ -27,7 +27,7 @@ use pep508_rs::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, Verbat use platform_tags::{TagCompatibility, TagPriority, Tags}; use pypi_types::{ redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, - RequirementSource, + RequirementSource, ResolverMarkerEnvironment, }; use uv_configuration::ExtrasSpecification; use uv_distribution::DistributionDatabase; @@ -419,7 +419,7 @@ impl Lock { pub fn to_resolution( &self, project: &VirtualProject, - marker_env: &MarkerEnvironment, + marker_env: &ResolverMarkerEnvironment, tags: &Tags, extras: &ExtrasSpecification, dev: &[GroupName], @@ -3641,7 +3641,7 @@ impl<'env> TreeDisplay<'env> { /// Create a new [`DisplayDependencyGraph`] for the set of installed packages. pub fn new( lock: &'env Lock, - markers: Option<&'env MarkerEnvironment>, + markers: Option<&'env ResolverMarkerEnvironment>, depth: usize, prune: Vec, package: Vec, diff --git a/crates/uv-resolver/src/manifest.rs b/crates/uv-resolver/src/manifest.rs index 741ae98d0..82de403ed 100644 --- a/crates/uv-resolver/src/manifest.rs +++ b/crates/uv-resolver/src/manifest.rs @@ -1,15 +1,15 @@ -use either::Either; use std::borrow::Cow; use std::collections::BTreeSet; -use pep508_rs::MarkerEnvironment; +use either::Either; + use pypi_types::Requirement; use uv_configuration::{Constraints, Overrides}; use uv_normalize::{GroupName, PackageName}; use uv_types::RequestedRequirements; use crate::preferences::Preferences; -use crate::{DependencyMode, Exclusions}; +use crate::{DependencyMode, Exclusions, ResolverMarkers}; /// A manifest of requirements, constraints, and preferences. #[derive(Clone, Debug)] @@ -109,7 +109,7 @@ impl Manifest { /// - Determining which requirements should allow local version specifiers (e.g., `torch==2.2.0+cpu`). pub fn requirements<'a>( &'a self, - markers: Option<&'a MarkerEnvironment>, + markers: &'a ResolverMarkers, mode: DependencyMode, ) -> impl Iterator> + 'a { self.requirements_no_overrides(markers, mode) @@ -119,39 +119,48 @@ impl Manifest { /// Like [`Self::requirements`], but without the overrides. pub fn requirements_no_overrides<'a>( &'a self, - markers: Option<&'a MarkerEnvironment>, + markers: &'a ResolverMarkers, mode: DependencyMode, ) -> impl Iterator> + 'a { match mode { // Include all direct and transitive requirements, with constraints and overrides applied. - DependencyMode::Transitive => Either::Left( - self.lookaheads - .iter() - .flat_map(move |lookahead| { - self.overrides - .apply(lookahead.requirements()) - .filter(move |requirement| { - requirement.evaluate_markers(markers, lookahead.extras()) - }) - }) - .chain( - self.overrides - .apply(&self.requirements) - .filter(move |requirement| requirement.evaluate_markers(markers, &[])), - ) - .chain( - self.constraints - .requirements() - .filter(move |requirement| requirement.evaluate_markers(markers, &[])) - .map(Cow::Borrowed), - ), - ), + DependencyMode::Transitive => { + Either::Left( + self.lookaheads + .iter() + .flat_map(move |lookahead| { + self.overrides.apply(lookahead.requirements()).filter( + move |requirement| { + requirement.evaluate_markers( + markers.marker_environment(), + lookahead.extras(), + ) + }, + ) + }) + .chain(self.overrides.apply(&self.requirements).filter( + move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }, + )) + .chain( + self.constraints + .requirements() + .filter(move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }) + .map(Cow::Borrowed), + ), + ) + } // Include direct requirements, with constraints and overrides applied. DependencyMode::Direct => Either::Right( self.overrides .apply(&self.requirements) .chain(self.constraints.requirements().map(Cow::Borrowed)) - .filter(move |requirement| requirement.evaluate_markers(markers, &[])), + .filter(move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }), ), } } @@ -159,7 +168,7 @@ impl Manifest { /// Only the overrides from [`Self::requirements`]. pub fn overrides<'a>( &'a self, - markers: Option<&'a MarkerEnvironment>, + markers: &'a ResolverMarkers, mode: DependencyMode, ) -> impl Iterator> + 'a { match mode { @@ -167,14 +176,18 @@ impl Manifest { DependencyMode::Transitive => Either::Left( self.overrides .requirements() - .filter(move |requirement| requirement.evaluate_markers(markers, &[])) + .filter(move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }) .map(Cow::Borrowed), ), // Include direct requirements, with constraints and overrides applied. DependencyMode::Direct => Either::Right( self.overrides .requirements() - .filter(move |requirement| requirement.evaluate_markers(markers, &[])) + .filter(move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }) .map(Cow::Borrowed), ), } @@ -192,36 +205,43 @@ impl Manifest { /// the `lowest-direct` strategy is in use. pub fn user_requirements<'a>( &'a self, - markers: Option<&'a MarkerEnvironment>, + markers: &'a ResolverMarkers, mode: DependencyMode, ) -> impl Iterator> + 'a { match mode { // Include direct requirements, dependencies of editables, and transitive dependencies // of local packages. - DependencyMode::Transitive => Either::Left( - self.lookaheads - .iter() - .filter(|lookahead| lookahead.direct()) - .flat_map(move |lookahead| { - self.overrides - .apply(lookahead.requirements()) - .filter(move |requirement| { - requirement.evaluate_markers(markers, lookahead.extras()) - }) - }) - .chain( - self.overrides - .apply(&self.requirements) - .filter(move |requirement| requirement.evaluate_markers(markers, &[])), - ), - ), + DependencyMode::Transitive => { + Either::Left( + self.lookaheads + .iter() + .filter(|lookahead| lookahead.direct()) + .flat_map(move |lookahead| { + self.overrides.apply(lookahead.requirements()).filter( + move |requirement| { + requirement.evaluate_markers( + markers.marker_environment(), + lookahead.extras(), + ) + }, + ) + }) + .chain(self.overrides.apply(&self.requirements).filter( + move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }, + )), + ) + } // Restrict to the direct requirements. - DependencyMode::Direct => Either::Right( - self.overrides - .apply(self.requirements.iter()) - .filter(move |requirement| requirement.evaluate_markers(markers, &[])), - ), + DependencyMode::Direct => { + Either::Right(self.overrides.apply(self.requirements.iter()).filter( + move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }, + )) + } } } @@ -232,11 +252,13 @@ impl Manifest { /// resolution (assuming the user enabled development dependencies). pub fn direct_requirements<'a>( &'a self, - markers: Option<&'a MarkerEnvironment>, + markers: &'a ResolverMarkers, ) -> impl Iterator> + 'a { self.overrides .apply(self.requirements.iter()) - .filter(move |requirement| requirement.evaluate_markers(markers, &[])) + .filter(move |requirement| { + requirement.evaluate_markers(markers.marker_environment(), &[]) + }) } /// Apply the overrides and constraints to a set of requirements. diff --git a/crates/uv-resolver/src/preferences.rs b/crates/uv-resolver/src/preferences.rs index 4631ac595..b031045fe 100644 --- a/crates/uv-resolver/src/preferences.rs +++ b/crates/uv-resolver/src/preferences.rs @@ -5,11 +5,13 @@ use tracing::trace; use distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Name}; use pep440_rs::{Operator, Version}; -use pep508_rs::{MarkerEnvironment, MarkerTree, VersionOrUrl}; +use pep508_rs::{MarkerTree, VersionOrUrl}; use pypi_types::{HashDigest, HashError}; use requirements_txt::{RequirementEntry, RequirementsTxtRequirement}; use uv_normalize::PackageName; +use crate::ResolverMarkers; + #[derive(thiserror::Error, Debug)] pub enum PreferenceError { #[error(transparent)] @@ -121,12 +123,12 @@ impl Preferences { /// to an applicable subset. pub fn from_iter>( preferences: PreferenceIterator, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, ) -> Self { let mut slf = Self::default(); for preference in preferences { // Filter non-matching preferences when resolving for an environment. - if let Some(markers) = markers { + if let Some(markers) = markers.marker_environment() { if !preference.marker.evaluate(markers, &[]) { trace!("Excluding {preference} from preferences due to unmatched markers"); continue; diff --git a/crates/uv-resolver/src/prerelease.rs b/crates/uv-resolver/src/prerelease.rs index dc1c216fe..a0e26fc84 100644 --- a/crates/uv-resolver/src/prerelease.rs +++ b/crates/uv-resolver/src/prerelease.rs @@ -1,6 +1,5 @@ use pypi_types::RequirementSource; -use pep508_rs::MarkerEnvironment; use uv_normalize::PackageName; use crate::resolver::ForkSet; @@ -68,7 +67,7 @@ impl PrereleaseStrategy { pub(crate) fn from_mode( mode: PrereleaseMode, manifest: &Manifest, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, dependencies: DependencyMode, ) -> Self { let mut packages = ForkSet::default(); diff --git a/crates/uv-resolver/src/resolution_mode.rs b/crates/uv-resolver/src/resolution_mode.rs index eb433212f..cb0f66bd3 100644 --- a/crates/uv-resolver/src/resolution_mode.rs +++ b/crates/uv-resolver/src/resolution_mode.rs @@ -1,9 +1,8 @@ use rustc_hash::FxHashSet; -use pep508_rs::MarkerEnvironment; use uv_normalize::PackageName; -use crate::{DependencyMode, Manifest}; +use crate::{DependencyMode, Manifest, ResolverMarkers}; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] @@ -47,7 +46,7 @@ impl ResolutionStrategy { pub(crate) fn from_mode( mode: ResolutionMode, manifest: &Manifest, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, dependencies: DependencyMode, ) -> Self { match mode { diff --git a/crates/uv-resolver/src/resolver/groups.rs b/crates/uv-resolver/src/resolver/groups.rs index a10314222..c2a5b77e8 100644 --- a/crates/uv-resolver/src/resolver/groups.rs +++ b/crates/uv-resolver/src/resolver/groups.rs @@ -1,9 +1,8 @@ use rustc_hash::FxHashMap; -use pep508_rs::MarkerEnvironment; use uv_normalize::{GroupName, PackageName}; -use crate::Manifest; +use crate::{Manifest, ResolverMarkers}; /// A map of package names to their activated dependency groups. #[derive(Debug, Default, Clone)] @@ -11,7 +10,7 @@ pub(crate) struct Groups(FxHashMap>); impl Groups { /// Determine the set of enabled dependency groups in the [`Manifest`]. - pub(crate) fn from_manifest(manifest: &Manifest, markers: Option<&MarkerEnvironment>) -> Self { + pub(crate) fn from_manifest(manifest: &Manifest, markers: &ResolverMarkers) -> Self { let mut groups = FxHashMap::default(); // Enable the groups for all direct dependencies. In practice, this tends to mean: when diff --git a/crates/uv-resolver/src/resolver/locals.rs b/crates/uv-resolver/src/resolver/locals.rs index e2e034603..b4a2a2666 100644 --- a/crates/uv-resolver/src/resolver/locals.rs +++ b/crates/uv-resolver/src/resolver/locals.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use distribution_filename::{SourceDistFilename, WheelFilename}; use distribution_types::RemoteSource; use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError}; -use pep508_rs::{MarkerEnvironment, PackageName}; +use pep508_rs::PackageName; use pypi_types::RequirementSource; use crate::resolver::ForkMap; @@ -17,7 +17,7 @@ impl Locals { /// Determine the set of permitted local versions in the [`Manifest`]. pub(crate) fn from_manifest( manifest: &Manifest, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, dependencies: DependencyMode, ) -> Self { let mut locals = ForkMap::default(); diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index af435f871..1d755f466 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -157,11 +157,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider> python_requirement .target() .and_then(|target| target.as_requires_python()), - AllowedYanks::from_manifest( - &manifest, - markers.marker_environment(), - options.dependency_mode, - ), + AllowedYanks::from_manifest(&manifest, &markers, options.dependency_mode), hasher, options.exclude_newer, build_context.build_options(), @@ -199,24 +195,11 @@ impl let state = ResolverState { index: index.clone(), git: git.clone(), - selector: CandidateSelector::for_resolution( - options, - &manifest, - markers.marker_environment(), - ), + selector: CandidateSelector::for_resolution(options, &manifest, &markers), dependency_mode: options.dependency_mode, - urls: Urls::from_manifest( - &manifest, - markers.marker_environment(), - git, - options.dependency_mode, - )?, - locals: Locals::from_manifest( - &manifest, - markers.marker_environment(), - options.dependency_mode, - ), - groups: Groups::from_manifest(&manifest, markers.marker_environment()), + urls: Urls::from_manifest(&manifest, &markers, git, options.dependency_mode)?, + locals: Locals::from_manifest(&manifest, &markers, options.dependency_mode), + groups: Groups::from_manifest(&manifest, &markers), project: manifest.project, workspace_members: manifest.workspace_members, requirements: manifest.requirements, diff --git a/crates/uv-resolver/src/resolver/resolver_markers.rs b/crates/uv-resolver/src/resolver/resolver_markers.rs index 6832eee92..da38f0563 100644 --- a/crates/uv-resolver/src/resolver/resolver_markers.rs +++ b/crates/uv-resolver/src/resolver/resolver_markers.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; -use tracing::debug; use pep508_rs::{MarkerEnvironment, MarkerTree}; +use pypi_types::ResolverMarkerEnvironment; #[derive(Debug, Clone)] /// Whether we're solving for a specific environment, universally or for a specific fork. @@ -20,8 +20,8 @@ pub enum ResolverMarkers { impl ResolverMarkers { /// Set the resolver to perform a resolution for a specific environment. - pub fn specific_environment(markers: MarkerEnvironment) -> Self { - Self::SpecificEnvironment(ResolverMarkerEnvironment::from(markers)) + pub fn specific_environment(markers: ResolverMarkerEnvironment) -> Self { + Self::SpecificEnvironment(markers) } /// Set the resolver to perform a universal resolution. @@ -71,46 +71,3 @@ impl Display for ResolverMarkers { } } } - -/// A wrapper type around [`MarkerEnvironment`] that ensures the Python version markers are -/// release-only, to match the resolver's semantics. -#[derive(Debug, Clone)] -pub struct ResolverMarkerEnvironment(MarkerEnvironment); - -impl From for ResolverMarkerEnvironment { - fn from(value: MarkerEnvironment) -> Self { - // Strip `python_version`. - let python_version = value.python_version().only_release(); - let value = if python_version == **value.python_version() { - value - } else { - debug!( - "Stripping pre-release from `python_version`: {}", - value.python_version() - ); - value.with_python_version(python_version) - }; - - // Strip `python_full_version`. - let python_full_version = value.python_full_version().only_release(); - let value = if python_full_version == **value.python_full_version() { - value - } else { - debug!( - "Stripping pre-release from `python_full_version`: {}", - value.python_full_version() - ); - value.with_python_full_version(python_full_version) - }; - - Self(value) - } -} - -impl std::ops::Deref for ResolverMarkerEnvironment { - type Target = MarkerEnvironment; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/crates/uv-resolver/src/resolver/urls.rs b/crates/uv-resolver/src/resolver/urls.rs index 2ddf482c6..fe8f09040 100644 --- a/crates/uv-resolver/src/resolver/urls.rs +++ b/crates/uv-resolver/src/resolver/urls.rs @@ -6,12 +6,12 @@ use tracing::debug; use cache_key::CanonicalUrl; use distribution_types::Verbatim; -use pep508_rs::{MarkerEnvironment, VerbatimUrl}; +use pep508_rs::VerbatimUrl; use pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl}; use uv_git::GitResolver; use uv_normalize::PackageName; -use crate::{DependencyMode, Manifest, ResolveError}; +use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers}; /// The URLs that are allowed for packages. /// @@ -36,7 +36,7 @@ pub(crate) struct Urls { impl Urls { pub(crate) fn from_manifest( manifest: &Manifest, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, git: &GitResolver, dependencies: DependencyMode, ) -> Result { diff --git a/crates/uv-resolver/src/yanks.rs b/crates/uv-resolver/src/yanks.rs index 34772e01d..59f27cd18 100644 --- a/crates/uv-resolver/src/yanks.rs +++ b/crates/uv-resolver/src/yanks.rs @@ -1,12 +1,12 @@ -use pypi_types::RequirementSource; -use rustc_hash::{FxHashMap, FxHashSet}; use std::sync::Arc; +use rustc_hash::{FxHashMap, FxHashSet}; + use pep440_rs::Version; -use pep508_rs::MarkerEnvironment; +use pypi_types::RequirementSource; use uv_normalize::PackageName; -use crate::{DependencyMode, Manifest}; +use crate::{DependencyMode, Manifest, ResolverMarkers}; /// A set of package versions that are permitted, even if they're marked as yanked by the /// relevant index. @@ -16,7 +16,7 @@ pub struct AllowedYanks(Arc>>); impl AllowedYanks { pub fn from_manifest( manifest: &Manifest, - markers: Option<&MarkerEnvironment>, + markers: &ResolverMarkers, dependencies: DependencyMode, ) -> Self { let mut allowed_yanks = FxHashMap::>::default(); diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 8a9d80e62..1e8faf378 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -7,8 +7,9 @@ use distribution_types::{ DistributionMetadata, HashPolicy, Name, Resolution, UnresolvedRequirement, VersionId, }; use pep440_rs::Version; -use pep508_rs::MarkerEnvironment; -use pypi_types::{HashDigest, HashError, Requirement, RequirementSource}; +use pypi_types::{ + HashDigest, HashError, Requirement, RequirementSource, ResolverMarkerEnvironment, +}; use uv_configuration::HashCheckingMode; use uv_normalize::PackageName; @@ -125,7 +126,7 @@ impl HashStrategy { /// to "only evaluate marker expressions that reference an extra name.") pub fn from_requirements<'a>( requirements: impl Iterator, - markers: Option<&MarkerEnvironment>, + marker_env: Option<&ResolverMarkerEnvironment>, mode: HashCheckingMode, ) -> Result { let mut hashes = FxHashMap::>::default(); @@ -133,7 +134,9 @@ impl HashStrategy { // For each requirement, map from name to allowed hashes. We use the last entry for each // package. for (requirement, digests) in requirements { - if !requirement.evaluate_markers(markers, &[]) { + if !requirement + .evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[]) + { continue; } diff --git a/crates/uv/src/commands/pip/check.rs b/crates/uv/src/commands/pip/check.rs index b8b28bba7..28a7edb21 100644 --- a/crates/uv/src/commands/pip/check.rs +++ b/crates/uv/src/commands/pip/check.rs @@ -52,8 +52,12 @@ pub(crate) fn pip_check( .dimmed() )?; + // Determine the markers to use for resolution. + let markers = environment.interpreter().resolver_markers(); + + // Run the diagnostics. let diagnostics: Vec = - site_packages.diagnostics()?.into_iter().collect(); + site_packages.diagnostics(&markers)?.into_iter().collect(); if diagnostics.is_empty() { writeln!( diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 29c870204..896135641 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -248,10 +248,7 @@ pub(crate) async fn pip_compile( } else { let (tags, markers) = resolution_environment(python_version, python_platform, &interpreter)?; - ( - Some(tags), - ResolverMarkers::specific_environment((*markers).clone()), - ) + (Some(tags), ResolverMarkers::specific_environment(markers)) }; // Generate, but don't enforce hashes for the requirements. diff --git a/crates/uv/src/commands/pip/freeze.rs b/crates/uv/src/commands/pip/freeze.rs index 5aae401f0..97fce7cc3 100644 --- a/crates/uv/src/commands/pip/freeze.rs +++ b/crates/uv/src/commands/pip/freeze.rs @@ -68,7 +68,10 @@ pub(crate) fn pip_freeze( // Validate that the environment is consistent. if strict { - for diagnostic in site_packages.diagnostics()? { + // Determine the markers to use for resolution. + let markers = environment.interpreter().resolver_markers(); + + for diagnostic in site_packages.diagnostics(&markers)? { writeln!( printer.stderr(), "{}{} {}", diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 0ca873e1d..a52fb7ce3 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -32,7 +32,7 @@ use uv_types::{BuildIsolation, HashStrategy}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::operations::Modifications; -use crate::commands::pip::{operations, resolution_environment}; +use crate::commands::pip::{operations, resolution_markers, resolution_tags}; use crate::commands::{ExitStatus, SharedState}; use crate::printer::Printer; @@ -183,6 +183,14 @@ pub(crate) async fn pip_install( let _lock = environment.lock()?; + // Determine the markers to use for the resolution. + let interpreter = environment.interpreter(); + let markers = resolution_markers( + python_version.as_ref(), + python_platform.as_ref(), + interpreter, + ); + // Determine the set of installed packages. let site_packages = SitePackages::from_environment(&environment)?; @@ -190,7 +198,7 @@ pub(crate) async fn pip_install( // Ideally, the resolver would be fast enough to let us remove this check. But right now, for large environments, // it's an order of magnitude faster to validate the environment than to resolve the requirements. if reinstall.is_none() && upgrade.is_none() && source_trees.is_empty() && overrides.is_empty() { - match site_packages.satisfies(&requirements, &constraints)? { + match site_packages.satisfies(&requirements, &constraints, &markers)? { // If the requirements are already satisfied, we're done. SatisfiesResult::Fresh { recursive_requirements, @@ -216,8 +224,6 @@ pub(crate) async fn pip_install( } } - let interpreter = environment.interpreter(); - // Determine the Python requirement, if the user requested a specific version. let python_requirement = if let Some(python_version) = python_version.as_ref() { PythonRequirement::from_python_version(interpreter, python_version) @@ -225,8 +231,12 @@ pub(crate) async fn pip_install( PythonRequirement::from_interpreter(interpreter) }; - // Determine the environment for the resolution. - let (tags, markers) = resolution_environment(python_version, python_platform, interpreter)?; + // Determine the tags to use for the resolution. + let tags = resolution_tags( + python_version.as_ref(), + python_platform.as_ref(), + interpreter, + )?; // Collect the set of required hashes. let hasher = if let Some(hash_checking) = hash_checking { @@ -262,7 +272,7 @@ pub(crate) async fn pip_install( .cache(cache.clone()) .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) - .markers(&markers) + .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -333,7 +343,7 @@ pub(crate) async fn pip_install( &reinstall, &upgrade, Some(&tags), - ResolverMarkers::specific_environment((*markers).clone()), + ResolverMarkers::specific_environment(markers.clone()), python_requirement, &client, &flat_index, @@ -366,6 +376,7 @@ pub(crate) async fn pip_install( compile, &index_locations, &hasher, + &markers, &tags, &client, &state.in_flight, @@ -384,7 +395,7 @@ pub(crate) async fn pip_install( // Notify the user of any environment diagnostics. if strict && !dry_run { - operations::diagnose_environment(&resolution, &environment, printer)?; + operations::diagnose_environment(&resolution, &environment, &markers, printer)?; } Ok(ExitStatus::Success) diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 1c629b194..e10c47062 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -119,7 +119,10 @@ pub(crate) fn pip_list( // Validate that the environment is consistent. if strict { - for diagnostic in site_packages.diagnostics()? { + // Determine the markers to use for resolution. + let markers = environment.interpreter().resolver_markers(); + + for diagnostic in site_packages.diagnostics(&markers)? { writeln!( printer.stderr(), "{}{} {}", diff --git a/crates/uv/src/commands/pip/mod.rs b/crates/uv/src/commands/pip/mod.rs index 4821ad545..d2a2fce00 100644 --- a/crates/uv/src/commands/pip/mod.rs +++ b/crates/uv/src/commands/pip/mod.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; -use pep508_rs::MarkerEnvironment; use platform_tags::{Tags, TagsError}; +use pypi_types::ResolverMarkerEnvironment; use uv_configuration::TargetTriple; use uv_python::{Interpreter, PythonVersion}; @@ -17,12 +17,65 @@ pub(crate) mod sync; pub(crate) mod tree; pub(crate) mod uninstall; +pub(crate) fn resolution_markers( + python_version: Option<&PythonVersion>, + python_platform: Option<&TargetTriple>, + interpreter: &Interpreter, +) -> ResolverMarkerEnvironment { + match (python_platform, python_version) { + (Some(python_platform), Some(python_version)) => ResolverMarkerEnvironment::from( + python_version.markers(&python_platform.markers(interpreter.markers())), + ), + (Some(python_platform), None) => { + ResolverMarkerEnvironment::from(python_platform.markers(interpreter.markers())) + } + (None, Some(python_version)) => { + ResolverMarkerEnvironment::from(python_version.markers(interpreter.markers())) + } + (None, None) => interpreter.resolver_markers(), + } +} + +pub(crate) fn resolution_tags<'env>( + python_version: Option<&PythonVersion>, + python_platform: Option<&TargetTriple>, + interpreter: &'env Interpreter, +) -> Result, TagsError> { + Ok(match (python_platform, python_version.as_ref()) { + (Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env( + &python_platform.platform(), + (python_version.major(), python_version.minor()), + interpreter.implementation_name(), + interpreter.implementation_tuple(), + interpreter.manylinux_compatible(), + interpreter.gil_disabled(), + )?), + (Some(python_platform), None) => Cow::Owned(Tags::from_env( + &python_platform.platform(), + interpreter.python_tuple(), + interpreter.implementation_name(), + interpreter.implementation_tuple(), + interpreter.manylinux_compatible(), + interpreter.gil_disabled(), + )?), + (None, Some(python_version)) => Cow::Owned(Tags::from_env( + interpreter.platform(), + (python_version.major(), python_version.minor()), + interpreter.implementation_name(), + interpreter.implementation_tuple(), + interpreter.manylinux_compatible(), + interpreter.gil_disabled(), + )?), + (None, None) => Cow::Borrowed(interpreter.tags()?), + }) +} + /// Determine the tags, markers, and interpreter to use for resolution. pub(crate) fn resolution_environment( python_version: Option, python_platform: Option, interpreter: &Interpreter, -) -> Result<(Cow<'_, Tags>, Cow<'_, MarkerEnvironment>), TagsError> { +) -> Result<(Cow<'_, Tags>, ResolverMarkerEnvironment), TagsError> { let tags = match (python_platform, python_version.as_ref()) { (Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env( &python_platform.platform(), @@ -53,12 +106,16 @@ pub(crate) fn resolution_environment( // Apply the platform tags to the markers. let markers = match (python_platform, python_version) { - (Some(python_platform), Some(python_version)) => { - Cow::Owned(python_version.markers(&python_platform.markers(interpreter.markers()))) + (Some(python_platform), Some(python_version)) => ResolverMarkerEnvironment::from( + python_version.markers(&python_platform.markers(interpreter.markers())), + ), + (Some(python_platform), None) => { + ResolverMarkerEnvironment::from(python_platform.markers(interpreter.markers())) } - (Some(python_platform), None) => Cow::Owned(python_platform.markers(interpreter.markers())), - (None, Some(python_version)) => Cow::Owned(python_version.markers(interpreter.markers())), - (None, None) => Cow::Borrowed(interpreter.markers()), + (None, Some(python_version)) => { + ResolverMarkerEnvironment::from(python_version.markers(interpreter.markers())) + } + (None, None) => interpreter.resolver_markers(), }; Ok((tags, markers)) diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 7f236d3f1..4e2bd312b 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -17,7 +17,7 @@ use distribution_types::{ }; use install_wheel_rs::linker::LinkMode; use platform_tags::Tags; -use pypi_types::Requirement; +use pypi_types::{Requirement, ResolverMarkerEnvironment}; use uv_cache::Cache; use uv_client::{BaseClientBuilder, RegistryClient}; use uv_configuration::{ @@ -199,7 +199,7 @@ pub(crate) async fn resolve( .chain(upgrade.constraints().cloned()), ); let overrides = Overrides::from_requirements(overrides); - let preferences = Preferences::from_iter(preferences, markers.marker_environment()); + let preferences = Preferences::from_iter(preferences, &markers); // Determine any lookahead requirements. let lookaheads = match options.dependency_mode { @@ -349,6 +349,7 @@ pub(crate) async fn install( compile: bool, index_urls: &IndexLocations, hasher: &HashStrategy, + markers: &ResolverMarkerEnvironment, tags: &Tags, client: &RegistryClient, in_flight: &InFlight, @@ -376,6 +377,7 @@ pub(crate) async fn install( index_urls, cache, venv, + markers, tags, ) .context("Failed to determine installation plan")?; @@ -661,10 +663,11 @@ pub(crate) fn diagnose_resolution( pub(crate) fn diagnose_environment( resolution: &Resolution, venv: &PythonEnvironment, + markers: &ResolverMarkerEnvironment, printer: Printer, ) -> Result<(), Error> { let site_packages = SitePackages::from_environment(venv)?; - for diagnostic in site_packages.diagnostics()? { + for diagnostic in site_packages.diagnostics(markers)? { // Only surface diagnostics that are "relevant" to the current resolution. if resolution .packages() diff --git a/crates/uv/src/commands/pip/show.rs b/crates/uv/src/commands/pip/show.rs index 3119f635d..f7f62307c 100644 --- a/crates/uv/src/commands/pip/show.rs +++ b/crates/uv/src/commands/pip/show.rs @@ -55,7 +55,7 @@ pub(crate) fn pip_show( let site_packages = SitePackages::from_environment(&environment)?; // Determine the markers to use for resolution. - let markers = environment.interpreter().markers(); + let markers = environment.interpreter().resolver_markers(); // Sort and deduplicate the packages, which are keyed by name. packages.sort_unstable(); @@ -101,7 +101,7 @@ pub(crate) fn pip_show( metadata .requires_dist .into_iter() - .filter(|req| req.evaluate_markers(markers, &[])) + .filter(|req| req.evaluate_markers(&markers, &[])) .map(|req| req.name) .sorted_unstable() .dedup() @@ -119,7 +119,7 @@ pub(crate) fn pip_show( let requires = metadata .requires_dist .into_iter() - .filter(|req| req.evaluate_markers(markers, &[])) + .filter(|req| req.evaluate_markers(&markers, &[])) .map(|req| req.name) .collect_vec(); if !requires.is_empty() { @@ -192,7 +192,7 @@ pub(crate) fn pip_show( // Validate that the environment is consistent. if strict { - for diagnostic in site_packages.diagnostics()? { + for diagnostic in site_packages.diagnostics(&markers)? { writeln!( printer.stderr(), "{}{} {}", diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 85f502187..f62461062 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -31,7 +31,7 @@ use uv_types::{BuildIsolation, HashStrategy}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::operations::Modifications; -use crate::commands::pip::{operations, resolution_environment}; +use crate::commands::pip::{operations, resolution_markers, resolution_tags}; use crate::commands::{ExitStatus, SharedState}; use crate::printer::Printer; @@ -183,8 +183,17 @@ pub(crate) async fn pip_sync( PythonRequirement::from_interpreter(interpreter) }; - // Determine the environment for the resolution. - let (tags, markers) = resolution_environment(python_version, python_platform, interpreter)?; + // Determine the markers and tags to use for resolution. + let markers = resolution_markers( + python_version.as_ref(), + python_platform.as_ref(), + interpreter, + ); + let tags = resolution_tags( + python_version.as_ref(), + python_platform.as_ref(), + interpreter, + )?; // Collect the set of required hashes. let hasher = if let Some(hash_checking) = hash_checking { @@ -213,7 +222,7 @@ pub(crate) async fn pip_sync( .cache(cache.clone()) .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) - .markers(&markers) + .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -292,7 +301,7 @@ pub(crate) async fn pip_sync( &reinstall, &upgrade, Some(&tags), - ResolverMarkers::specific_environment((*markers).clone()), + ResolverMarkers::specific_environment(markers.clone()), python_requirement, &client, &flat_index, @@ -325,6 +334,7 @@ pub(crate) async fn pip_sync( compile, &index_locations, &hasher, + &markers, &tags, &client, &state.in_flight, @@ -343,7 +353,7 @@ pub(crate) async fn pip_sync( // Notify the user of any environment diagnostics. if strict && !dry_run { - operations::diagnose_environment(&resolution, &environment, printer)?; + operations::diagnose_environment(&resolution, &environment, &markers, printer)?; } Ok(ExitStatus::Success) diff --git a/crates/uv/src/commands/pip/tree.rs b/crates/uv/src/commands/pip/tree.rs index 2e2442e90..d106b18ac 100644 --- a/crates/uv/src/commands/pip/tree.rs +++ b/crates/uv/src/commands/pip/tree.rs @@ -7,8 +7,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use tracing::debug; use distribution_types::{Diagnostic, Name}; -use pep508_rs::MarkerEnvironment; -use pypi_types::RequirementSource; +use pypi_types::{RequirementSource, ResolverMarkerEnvironment}; use uv_cache::Cache; use uv_distribution::Metadata; use uv_fs::Simplified; @@ -60,6 +59,9 @@ pub(crate) fn pip_tree( .push(metadata); } + // Determine the markers to use for the resolution. + let markers = environment.interpreter().resolver_markers(); + // Render the tree. let rendered_tree = DisplayDependencyGraph::new( depth.into(), @@ -68,7 +70,7 @@ pub(crate) fn pip_tree( no_dedupe, invert, show_version_specifiers, - environment.interpreter().markers(), + &markers, packages, ) .render() @@ -87,7 +89,7 @@ pub(crate) fn pip_tree( // Validate that the environment is consistent. if strict { - for diagnostic in site_packages.diagnostics()? { + for diagnostic in site_packages.diagnostics(&markers)? { writeln!( printer.stderr(), "{}{} {}", @@ -129,7 +131,7 @@ impl DisplayDependencyGraph { no_dedupe: bool, invert: bool, show_version_specifiers: bool, - markers: &MarkerEnvironment, + markers: &ResolverMarkerEnvironment, packages: IndexMap>, ) -> Self { let mut requirements: FxHashMap<_, Vec<_>> = FxHashMap::default(); diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 223142d04..ed56bd9ec 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -534,7 +534,7 @@ pub(crate) async fn resolve_environment<'a>( // Determine the tags, markers, and interpreter to use for resolution. let tags = interpreter.tags()?; - let markers = interpreter.markers(); + let markers = interpreter.resolver_markers(); let python_requirement = PythonRequirement::from_interpreter(interpreter); // Add all authenticated sources to the cache. @@ -549,7 +549,7 @@ pub(crate) async fn resolve_environment<'a>( .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) .keyring(keyring_provider) - .markers(markers) + .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -629,7 +629,7 @@ pub(crate) async fn resolve_environment<'a>( &reinstall, &upgrade, Some(tags), - ResolverMarkers::specific_environment(markers.clone()), + ResolverMarkers::specific_environment(markers), python_requirement, &client, &flat_index, @@ -673,10 +673,10 @@ pub(crate) async fn sync_environment( let site_packages = SitePackages::from_environment(&venv)?; - // Determine the tags, markers, and interpreter to use for resolution. + // Determine the markers tags to use for resolution. let interpreter = venv.interpreter(); let tags = venv.interpreter().tags()?; - let markers = venv.interpreter().markers(); + let markers = interpreter.resolver_markers(); // Add all authenticated sources to the cache. for url in index_locations.urls() { @@ -690,7 +690,7 @@ pub(crate) async fn sync_environment( .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) .keyring(keyring_provider) - .markers(markers) + .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -748,6 +748,7 @@ pub(crate) async fn sync_environment( compile_bytecode, index_locations, &hasher, + &markers, tags, &client, &state.in_flight, @@ -827,10 +828,14 @@ pub(crate) async fn update_environment( .. } = spec; + // Determine markers to use for resolution. + let interpreter = venv.interpreter(); + let markers = venv.interpreter().resolver_markers(); + // Check if the current environment satisfies the requirements let site_packages = SitePackages::from_environment(&venv)?; if source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() && overrides.is_empty() { - match site_packages.satisfies(&requirements, &constraints)? { + match site_packages.satisfies(&requirements, &constraints, &markers)? { // If the requirements are already satisfied, we're done. SatisfiesResult::Fresh { recursive_requirements, @@ -854,12 +859,6 @@ pub(crate) async fn update_environment( } } - // Determine the tags, markers, and interpreter to use for resolution. - let interpreter = venv.interpreter(); - let tags = venv.interpreter().tags()?; - let markers = venv.interpreter().markers(); - let python_requirement = PythonRequirement::from_interpreter(interpreter); - // Add all authenticated sources to the cache. for url in index_locations.urls() { store_credentials_from_url(url); @@ -872,7 +871,7 @@ pub(crate) async fn update_environment( .index_urls(index_locations.index_urls()) .index_strategy(*index_strategy) .keyring(*keyring_provider) - .markers(markers) + .markers(interpreter.markers()) .platform(interpreter.platform()) .build(); @@ -901,6 +900,10 @@ pub(crate) async fn update_environment( let hasher = HashStrategy::default(); let preferences = Vec::default(); + // Determine the tags to use for resolution. + let tags = venv.interpreter().tags()?; + let python_requirement = PythonRequirement::from_interpreter(interpreter); + // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, cache); @@ -973,6 +976,7 @@ pub(crate) async fn update_environment( *compile_bytecode, index_locations, &hasher, + &markers, tags, &client, &state.in_flight, diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 16c635085..59d13c203 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -11,7 +11,6 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tokio::process::Command; use tracing::{debug, warn}; - use uv_cache::Cache; use uv_cli::ExternalCommand; use uv_client::{BaseClientBuilder, Connectivity}; @@ -692,7 +691,11 @@ fn can_skip_ephemeral( return false; } - match site_packages.satisfies(&spec.requirements, &spec.constraints) { + match site_packages.satisfies( + &spec.requirements, + &spec.constraints, + &base_interpreter.resolver_markers(), + ) { // If the requirements are already satisfied, we're done. Ok(SatisfiesResult::Fresh { recursive_requirements, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 934d67f3c..01b5c1fa5 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -173,11 +173,13 @@ pub(super) async fn do_sync( } } + // Determine the markers to use for resolution. + let markers = venv.interpreter().resolver_markers(); + // Validate that the platform is supported by the lockfile. let environments = lock.supported_environments(); if !environments.is_empty() { - let platform = venv.interpreter().markers(); - if !environments.iter().any(|env| env.evaluate(platform, &[])) { + if !environments.iter().any(|env| env.evaluate(&markers, &[])) { return Err(ProjectError::LockedPlatformIncompatibility( environments .iter() @@ -195,11 +197,11 @@ pub(super) async fn do_sync( vec![] }; - let markers = venv.interpreter().markers(); + // Determine the tags to use for resolution. let tags = venv.interpreter().tags()?; // Read the lockfile. - let resolution = lock.to_resolution(project, markers, tags, extras, &dev)?; + let resolution = lock.to_resolution(project, &markers, tags, extras, &dev)?; // If `--no-install-project` is set, remove the project itself. let resolution = apply_no_install_project(no_install_project, resolution, project); @@ -222,7 +224,7 @@ pub(super) async fn do_sync( .index_urls(index_locations.index_urls()) .index_strategy(index_strategy) .keyring(keyring_provider) - .markers(markers) + .markers(venv.interpreter().markers()) .platform(venv.interpreter().platform()) .build(); @@ -284,6 +286,7 @@ pub(super) async fn do_sync( compile_bytecode, index_locations, &hasher, + &markers, tags, &client, &state.in_flight, diff --git a/crates/uv/src/commands/project/tree.rs b/crates/uv/src/commands/project/tree.rs index effe498c8..d9e512079 100644 --- a/crates/uv/src/commands/project/tree.rs +++ b/crates/uv/src/commands/project/tree.rs @@ -1,4 +1,3 @@ -use std::borrow::Cow; use std::fmt::Write; use anyhow::Result; @@ -13,6 +12,7 @@ use uv_resolver::TreeDisplay; use uv_workspace::{DiscoveryOptions, Workspace}; use crate::commands::pip::loggers::DefaultResolveLogger; +use crate::commands::pip::resolution_markers; use crate::commands::project::FoundInterpreter; use crate::commands::{project, ExitStatus}; use crate::printer::Printer; @@ -75,20 +75,17 @@ pub(crate) async fn tree( .await? .into_lock(); - // Apply the platform tags to the markers. - let markers = match (python_platform, python_version) { - (Some(python_platform), Some(python_version)) => { - Cow::Owned(python_version.markers(&python_platform.markers(interpreter.markers()))) - } - (Some(python_platform), None) => Cow::Owned(python_platform.markers(interpreter.markers())), - (None, Some(python_version)) => Cow::Owned(python_version.markers(interpreter.markers())), - (None, None) => Cow::Borrowed(interpreter.markers()), - }; + // Determine the markers to use for resolution. + let markers = resolution_markers( + python_version.as_ref(), + python_platform.as_ref(), + &interpreter, + ); // Render the tree. let tree = TreeDisplay::new( &lock, - (!universal).then(|| markers.as_ref()), + (!universal).then_some(&markers), depth.into(), prune, package, diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index 1a82267fe..aa7089bef 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -514,7 +514,11 @@ async fn get_or_create_environment( let constraints = []; if matches!( - site_packages.satisfies(&requirements, &constraints), + site_packages.satisfies( + &requirements, + &constraints, + &interpreter.resolver_markers() + ), Ok(SatisfiesResult::Fresh { .. }) ) { debug!("Using existing tool `{}`", from.name); diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index ba96362e5..df5858c1d 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -6338,3 +6338,51 @@ fn no_extension() { ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ "###); } + +/// Regression test for: +#[test] +fn switch_platform() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("iniconfig ; python_version == '3.12'")?; + + // Install `iniconfig`. + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "###); + + requirements_txt + .write_str("iniconfig ; python_version == '3.12'\nanyio ; python_version < '3.12'")?; + + // Add `anyio`, though it's only installed because of `--python-version`. + uv_snapshot!(context.pip_install() + .arg("-r") + .arg("requirements.txt") + .arg("--python-version") + .arg("3.11"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.3.0 + + idna==3.6 + + sniffio==1.3.1 + "###); + + Ok(()) +}