Use separate types to represent raw vs. resolver markers (#6646)

## Summary

This is similar to https://github.com/astral-sh/uv/pull/6171 but more
expansive... _Anywhere_ that we test requirements for platform
compatibility, we _need_ to respect the resolver-friendly markers. In
fixing the motivating issue (#6621), I also realized that we had a bunch
of bugs here around `pip install` with `--python-platform` and
`--python-version`, because we always performed our `satisfy` and `Plan`
operations on the interpreter's markers, not the adjusted markers!

Closes https://github.com/astral-sh/uv/issues/6621.
This commit is contained in:
Charlie Marsh 2024-08-26 14:00:21 -04:00 committed by GitHub
parent 6220532373
commit a7850d2a1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 422 additions and 247 deletions

View File

@ -88,6 +88,7 @@ mod resolver {
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder}; use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder};
use platform_tags::{Arch, Os, Platform, Tags}; use platform_tags::{Arch, Os, Platform, Tags};
use pypi_types::ResolverMarkerEnvironment;
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::RegistryClient; use uv_client::RegistryClient;
use uv_configuration::{ use uv_configuration::{
@ -192,7 +193,7 @@ mod resolver {
let markers = if universal { let markers = if universal {
ResolverMarkers::universal(vec![]) ResolverMarkers::universal(vec![])
} else { } else {
ResolverMarkers::specific_environment(MARKERS.clone()) ResolverMarkers::specific_environment(ResolverMarkerEnvironment::from(MARKERS.clone()))
}; };
let resolver = Resolver::new( let resolver = Resolver::new(

View File

@ -1,6 +1,7 @@
pub use base_url::*; pub use base_url::*;
pub use direct_url::*; pub use direct_url::*;
pub use lenient_requirement::*; pub use lenient_requirement::*;
pub use marker_environment::*;
pub use metadata::*; pub use metadata::*;
pub use parsed_url::*; pub use parsed_url::*;
pub use requirement::*; pub use requirement::*;
@ -10,6 +11,7 @@ pub use simple_json::*;
mod base_url; mod base_url;
mod direct_url; mod direct_url;
mod lenient_requirement; mod lenient_requirement;
mod marker_environment;
mod metadata; mod metadata;
mod parsed_url; mod parsed_url;
mod requirement; mod requirement;

View File

@ -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<MarkerEnvironment> 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
}
}

View File

@ -138,7 +138,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result<Resolution> { async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result<Resolution> {
let python_requirement = PythonRequirement::from_interpreter(self.interpreter); 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 tags = self.interpreter.tags()?;
let resolver = Resolver::new( let resolver = Resolver::new(
@ -148,7 +148,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
.index_strategy(self.index_strategy) .index_strategy(self.index_strategy)
.build(), .build(),
&python_requirement, &python_requirement,
ResolverMarkers::specific_environment(markers.clone()), ResolverMarkers::specific_environment(markers),
Some(tags), Some(tags),
self.flat_index, self.flat_index,
self.index, self.index,
@ -189,6 +189,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
// Determine the current environment markers. // Determine the current environment markers.
let tags = self.interpreter.tags()?; let tags = self.interpreter.tags()?;
let markers = self.interpreter.resolver_markers();
// Determine the set of installed packages. // Determine the set of installed packages.
let site_packages = SitePackages::from_environment(venv)?; let site_packages = SitePackages::from_environment(venv)?;
@ -208,6 +209,7 @@ impl<'a> BuildContext for BuildDispatch<'a> {
self.index_locations, self.index_locations,
self.cache(), self.cache(),
venv, venv,
&markers,
tags, tags,
)?; )?;

View File

@ -12,7 +12,7 @@ use distribution_types::{
PathSourceDist, RemoteSource, Verbatim, PathSourceDist, RemoteSource, Verbatim,
}; };
use platform_tags::Tags; use platform_tags::Tags;
use pypi_types::{Requirement, RequirementSource}; use pypi_types::{Requirement, RequirementSource, ResolverMarkerEnvironment};
use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache}; use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, WheelCache};
use uv_configuration::{BuildOptions, Reinstall}; use uv_configuration::{BuildOptions, Reinstall};
use uv_distribution::{ use uv_distribution::{
@ -58,6 +58,7 @@ impl<'a> Planner<'a> {
index_locations: &IndexLocations, index_locations: &IndexLocations,
cache: &Cache, cache: &Cache,
venv: &PythonEnvironment, venv: &PythonEnvironment,
markers: &ResolverMarkerEnvironment,
tags: &Tags, tags: &Tags,
) -> Result<Plan> { ) -> Result<Plan> {
// Index all the already-downloaded wheels in the cache. // Index all the already-downloaded wheels in the cache.
@ -72,7 +73,7 @@ impl<'a> Planner<'a> {
for requirement in self.requirements { for requirement in self.requirements {
// Filter out incompatible requirements. // Filter out incompatible requirements.
if !requirement.evaluate_markers(Some(venv.interpreter().markers()), &[]) { if !requirement.evaluate_markers(Some(markers), &[]) {
continue; continue;
} }

View File

@ -11,7 +11,7 @@ use distribution_types::{
Diagnostic, InstalledDist, Name, UnresolvedRequirement, UnresolvedRequirementSpecification, Diagnostic, InstalledDist, Name, UnresolvedRequirement, UnresolvedRequirementSpecification,
}; };
use pep440_rs::{Version, VersionSpecifiers}; use pep440_rs::{Version, VersionSpecifiers};
use pypi_types::{Requirement, VerbatimParsedUrl}; use pypi_types::{Requirement, ResolverMarkerEnvironment, VerbatimParsedUrl};
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_python::{Interpreter, PythonEnvironment}; use uv_python::{Interpreter, PythonEnvironment};
@ -181,7 +181,10 @@ impl SitePackages {
} }
/// Validate the installed packages in the virtual environment. /// Validate the installed packages in the virtual environment.
pub fn diagnostics(&self) -> Result<Vec<SitePackagesDiagnostic>> { pub fn diagnostics(
&self,
markers: &ResolverMarkerEnvironment,
) -> Result<Vec<SitePackagesDiagnostic>> {
let mut diagnostics = Vec::new(); let mut diagnostics = Vec::new();
for (package, indexes) in &self.by_name { for (package, indexes) in &self.by_name {
@ -220,7 +223,7 @@ impl SitePackages {
// Verify that the package is compatible with the current Python version. // Verify that the package is compatible with the current Python version.
if let Some(requires_python) = metadata.requires_python.as_ref() { 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 { diagnostics.push(SitePackagesDiagnostic::IncompatiblePythonVersion {
package: package.clone(), package: package.clone(),
version: self.interpreter.python_version().clone(), version: self.interpreter.python_version().clone(),
@ -231,7 +234,7 @@ impl SitePackages {
// Verify that the dependencies are installed. // Verify that the dependencies are installed.
for dependency in &metadata.requires_dist { for dependency in &metadata.requires_dist {
if !dependency.evaluate_markers(self.interpreter.markers(), &[]) { if !dependency.evaluate_markers(markers, &[]) {
continue; continue;
} }
@ -281,6 +284,7 @@ impl SitePackages {
&self, &self,
requirements: &[UnresolvedRequirementSpecification], requirements: &[UnresolvedRequirementSpecification],
constraints: &[Requirement], constraints: &[Requirement],
markers: &ResolverMarkerEnvironment,
) -> Result<SatisfiesResult> { ) -> Result<SatisfiesResult> {
// Collect the constraints. // Collect the constraints.
let constraints: FxHashMap<&PackageName, Vec<&Requirement>> = let constraints: FxHashMap<&PackageName, Vec<&Requirement>> =
@ -299,10 +303,7 @@ impl SitePackages {
// Add the direct requirements to the queue. // Add the direct requirements to the queue.
for entry in requirements { for entry in requirements {
if entry if entry.requirement.evaluate_markers(Some(markers), &[]) {
.requirement
.evaluate_markers(Some(self.interpreter.markers()), &[])
{
if seen.insert(entry.clone()) { if seen.insert(entry.clone()) {
stack.push(entry.clone()); stack.push(entry.clone());
} }
@ -353,10 +354,7 @@ impl SitePackages {
// Add the dependencies to the queue. // Add the dependencies to the queue.
for dependency in metadata.requires_dist { for dependency in metadata.requires_dist {
if dependency.evaluate_markers( if dependency.evaluate_markers(markers, entry.requirement.extras()) {
self.interpreter.markers(),
entry.requirement.extras(),
) {
let dependency = UnresolvedRequirementSpecification { let dependency = UnresolvedRequirementSpecification {
requirement: UnresolvedRequirement::Named(Requirement::from( requirement: UnresolvedRequirement::Named(Requirement::from(
dependency, dependency,

View File

@ -17,7 +17,7 @@ use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, StringVersion}; use pep508_rs::{MarkerEnvironment, StringVersion};
use platform_tags::Platform; use platform_tags::Platform;
use platform_tags::{Tags, TagsError}; use platform_tags::{Tags, TagsError};
use pypi_types::Scheme; use pypi_types::{ResolverMarkerEnvironment, Scheme};
use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp}; use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
use uv_fs::{write_atomic_sync, PythonExt, Simplified}; use uv_fs::{write_atomic_sync, PythonExt, Simplified};
@ -142,6 +142,11 @@ impl Interpreter {
&self.markers &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. /// Returns the [`PythonInstallationKey`] for this interpreter.
pub fn key(&self) -> PythonInstallationKey { pub fn key(&self) -> PythonInstallationKey {
PythonInstallationKey::new( PythonInstallationKey::new(

View File

@ -84,7 +84,7 @@ impl PythonVersion {
/// ///
/// The returned [`MarkerEnvironment`] will preserve the base environment's platform markers, /// The returned [`MarkerEnvironment`] will preserve the base environment's platform markers,
/// but override its Python version 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(); let mut markers = base.clone();
// Ex) `implementation_version == "3.12.0"` // Ex) `implementation_version == "3.12.0"`

View File

@ -6,7 +6,7 @@ use tracing::{debug, trace};
use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource}; use distribution_types::{CompatibleDist, IncompatibleDist, IncompatibleSource};
use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist}; use distribution_types::{DistributionMetadata, IncompatibleWheel, Name, PrioritizedDist};
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::{MarkerEnvironment, MarkerTree}; use pep508_rs::MarkerTree;
use uv_configuration::IndexStrategy; use uv_configuration::IndexStrategy;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_types::InstalledPackagesProvider; use uv_types::InstalledPackagesProvider;
@ -30,7 +30,7 @@ impl CandidateSelector {
pub(crate) fn for_resolution( pub(crate) fn for_resolution(
options: Options, options: Options,
manifest: &Manifest, manifest: &Manifest,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
) -> Self { ) -> Self {
Self { Self {
resolution_strategy: ResolutionStrategy::from_mode( resolution_strategy: ResolutionStrategy::from_mode(

View File

@ -27,7 +27,7 @@ use pep508_rs::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, Verbat
use platform_tags::{TagCompatibility, TagPriority, Tags}; use platform_tags::{TagCompatibility, TagPriority, Tags};
use pypi_types::{ use pypi_types::{
redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement, redact_git_credentials, HashDigest, ParsedArchiveUrl, ParsedGitUrl, Requirement,
RequirementSource, RequirementSource, ResolverMarkerEnvironment,
}; };
use uv_configuration::ExtrasSpecification; use uv_configuration::ExtrasSpecification;
use uv_distribution::DistributionDatabase; use uv_distribution::DistributionDatabase;
@ -419,7 +419,7 @@ impl Lock {
pub fn to_resolution( pub fn to_resolution(
&self, &self,
project: &VirtualProject, project: &VirtualProject,
marker_env: &MarkerEnvironment, marker_env: &ResolverMarkerEnvironment,
tags: &Tags, tags: &Tags,
extras: &ExtrasSpecification, extras: &ExtrasSpecification,
dev: &[GroupName], dev: &[GroupName],
@ -3641,7 +3641,7 @@ impl<'env> TreeDisplay<'env> {
/// Create a new [`DisplayDependencyGraph`] for the set of installed packages. /// Create a new [`DisplayDependencyGraph`] for the set of installed packages.
pub fn new( pub fn new(
lock: &'env Lock, lock: &'env Lock,
markers: Option<&'env MarkerEnvironment>, markers: Option<&'env ResolverMarkerEnvironment>,
depth: usize, depth: usize,
prune: Vec<PackageName>, prune: Vec<PackageName>,
package: Vec<PackageName>, package: Vec<PackageName>,

View File

@ -1,15 +1,15 @@
use either::Either;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use pep508_rs::MarkerEnvironment; use either::Either;
use pypi_types::Requirement; use pypi_types::Requirement;
use uv_configuration::{Constraints, Overrides}; use uv_configuration::{Constraints, Overrides};
use uv_normalize::{GroupName, PackageName}; use uv_normalize::{GroupName, PackageName};
use uv_types::RequestedRequirements; use uv_types::RequestedRequirements;
use crate::preferences::Preferences; use crate::preferences::Preferences;
use crate::{DependencyMode, Exclusions}; use crate::{DependencyMode, Exclusions, ResolverMarkers};
/// A manifest of requirements, constraints, and preferences. /// A manifest of requirements, constraints, and preferences.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -109,7 +109,7 @@ impl Manifest {
/// - Determining which requirements should allow local version specifiers (e.g., `torch==2.2.0+cpu`). /// - Determining which requirements should allow local version specifiers (e.g., `torch==2.2.0+cpu`).
pub fn requirements<'a>( pub fn requirements<'a>(
&'a self, &'a self,
markers: Option<&'a MarkerEnvironment>, markers: &'a ResolverMarkers,
mode: DependencyMode, mode: DependencyMode,
) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a { ) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a {
self.requirements_no_overrides(markers, mode) self.requirements_no_overrides(markers, mode)
@ -119,39 +119,48 @@ impl Manifest {
/// Like [`Self::requirements`], but without the overrides. /// Like [`Self::requirements`], but without the overrides.
pub fn requirements_no_overrides<'a>( pub fn requirements_no_overrides<'a>(
&'a self, &'a self,
markers: Option<&'a MarkerEnvironment>, markers: &'a ResolverMarkers,
mode: DependencyMode, mode: DependencyMode,
) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a { ) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a {
match mode { match mode {
// Include all direct and transitive requirements, with constraints and overrides applied. // Include all direct and transitive requirements, with constraints and overrides applied.
DependencyMode::Transitive => Either::Left( DependencyMode::Transitive => {
self.lookaheads Either::Left(
.iter() self.lookaheads
.flat_map(move |lookahead| { .iter()
self.overrides .flat_map(move |lookahead| {
.apply(lookahead.requirements()) self.overrides.apply(lookahead.requirements()).filter(
.filter(move |requirement| { move |requirement| {
requirement.evaluate_markers(markers, lookahead.extras()) requirement.evaluate_markers(
}) markers.marker_environment(),
}) lookahead.extras(),
.chain( )
self.overrides },
.apply(&self.requirements) )
.filter(move |requirement| requirement.evaluate_markers(markers, &[])), })
) .chain(self.overrides.apply(&self.requirements).filter(
.chain( move |requirement| {
self.constraints requirement.evaluate_markers(markers.marker_environment(), &[])
.requirements() },
.filter(move |requirement| requirement.evaluate_markers(markers, &[])) ))
.map(Cow::Borrowed), .chain(
), self.constraints
), .requirements()
.filter(move |requirement| {
requirement.evaluate_markers(markers.marker_environment(), &[])
})
.map(Cow::Borrowed),
),
)
}
// Include direct requirements, with constraints and overrides applied. // Include direct requirements, with constraints and overrides applied.
DependencyMode::Direct => Either::Right( DependencyMode::Direct => Either::Right(
self.overrides self.overrides
.apply(&self.requirements) .apply(&self.requirements)
.chain(self.constraints.requirements().map(Cow::Borrowed)) .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`]. /// Only the overrides from [`Self::requirements`].
pub fn overrides<'a>( pub fn overrides<'a>(
&'a self, &'a self,
markers: Option<&'a MarkerEnvironment>, markers: &'a ResolverMarkers,
mode: DependencyMode, mode: DependencyMode,
) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a { ) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a {
match mode { match mode {
@ -167,14 +176,18 @@ impl Manifest {
DependencyMode::Transitive => Either::Left( DependencyMode::Transitive => Either::Left(
self.overrides self.overrides
.requirements() .requirements()
.filter(move |requirement| requirement.evaluate_markers(markers, &[])) .filter(move |requirement| {
requirement.evaluate_markers(markers.marker_environment(), &[])
})
.map(Cow::Borrowed), .map(Cow::Borrowed),
), ),
// Include direct requirements, with constraints and overrides applied. // Include direct requirements, with constraints and overrides applied.
DependencyMode::Direct => Either::Right( DependencyMode::Direct => Either::Right(
self.overrides self.overrides
.requirements() .requirements()
.filter(move |requirement| requirement.evaluate_markers(markers, &[])) .filter(move |requirement| {
requirement.evaluate_markers(markers.marker_environment(), &[])
})
.map(Cow::Borrowed), .map(Cow::Borrowed),
), ),
} }
@ -192,36 +205,43 @@ impl Manifest {
/// the `lowest-direct` strategy is in use. /// the `lowest-direct` strategy is in use.
pub fn user_requirements<'a>( pub fn user_requirements<'a>(
&'a self, &'a self,
markers: Option<&'a MarkerEnvironment>, markers: &'a ResolverMarkers,
mode: DependencyMode, mode: DependencyMode,
) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a { ) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a {
match mode { match mode {
// Include direct requirements, dependencies of editables, and transitive dependencies // Include direct requirements, dependencies of editables, and transitive dependencies
// of local packages. // of local packages.
DependencyMode::Transitive => Either::Left( DependencyMode::Transitive => {
self.lookaheads Either::Left(
.iter() self.lookaheads
.filter(|lookahead| lookahead.direct()) .iter()
.flat_map(move |lookahead| { .filter(|lookahead| lookahead.direct())
self.overrides .flat_map(move |lookahead| {
.apply(lookahead.requirements()) self.overrides.apply(lookahead.requirements()).filter(
.filter(move |requirement| { move |requirement| {
requirement.evaluate_markers(markers, lookahead.extras()) requirement.evaluate_markers(
}) markers.marker_environment(),
}) lookahead.extras(),
.chain( )
self.overrides },
.apply(&self.requirements) )
.filter(move |requirement| requirement.evaluate_markers(markers, &[])), })
), .chain(self.overrides.apply(&self.requirements).filter(
), move |requirement| {
requirement.evaluate_markers(markers.marker_environment(), &[])
},
)),
)
}
// Restrict to the direct requirements. // Restrict to the direct requirements.
DependencyMode::Direct => Either::Right( DependencyMode::Direct => {
self.overrides Either::Right(self.overrides.apply(self.requirements.iter()).filter(
.apply(self.requirements.iter()) move |requirement| {
.filter(move |requirement| requirement.evaluate_markers(markers, &[])), requirement.evaluate_markers(markers.marker_environment(), &[])
), },
))
}
} }
} }
@ -232,11 +252,13 @@ impl Manifest {
/// resolution (assuming the user enabled development dependencies). /// resolution (assuming the user enabled development dependencies).
pub fn direct_requirements<'a>( pub fn direct_requirements<'a>(
&'a self, &'a self,
markers: Option<&'a MarkerEnvironment>, markers: &'a ResolverMarkers,
) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a { ) -> impl Iterator<Item = Cow<'a, Requirement>> + 'a {
self.overrides self.overrides
.apply(self.requirements.iter()) .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. /// Apply the overrides and constraints to a set of requirements.

View File

@ -5,11 +5,13 @@ use tracing::trace;
use distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Name}; use distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Name};
use pep440_rs::{Operator, Version}; use pep440_rs::{Operator, Version};
use pep508_rs::{MarkerEnvironment, MarkerTree, VersionOrUrl}; use pep508_rs::{MarkerTree, VersionOrUrl};
use pypi_types::{HashDigest, HashError}; use pypi_types::{HashDigest, HashError};
use requirements_txt::{RequirementEntry, RequirementsTxtRequirement}; use requirements_txt::{RequirementEntry, RequirementsTxtRequirement};
use uv_normalize::PackageName; use uv_normalize::PackageName;
use crate::ResolverMarkers;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
pub enum PreferenceError { pub enum PreferenceError {
#[error(transparent)] #[error(transparent)]
@ -121,12 +123,12 @@ impl Preferences {
/// to an applicable subset. /// to an applicable subset.
pub fn from_iter<PreferenceIterator: IntoIterator<Item = Preference>>( pub fn from_iter<PreferenceIterator: IntoIterator<Item = Preference>>(
preferences: PreferenceIterator, preferences: PreferenceIterator,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
) -> Self { ) -> Self {
let mut slf = Self::default(); let mut slf = Self::default();
for preference in preferences { for preference in preferences {
// Filter non-matching preferences when resolving for an environment. // 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, &[]) { if !preference.marker.evaluate(markers, &[]) {
trace!("Excluding {preference} from preferences due to unmatched markers"); trace!("Excluding {preference} from preferences due to unmatched markers");
continue; continue;

View File

@ -1,6 +1,5 @@
use pypi_types::RequirementSource; use pypi_types::RequirementSource;
use pep508_rs::MarkerEnvironment;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use crate::resolver::ForkSet; use crate::resolver::ForkSet;
@ -68,7 +67,7 @@ impl PrereleaseStrategy {
pub(crate) fn from_mode( pub(crate) fn from_mode(
mode: PrereleaseMode, mode: PrereleaseMode,
manifest: &Manifest, manifest: &Manifest,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
dependencies: DependencyMode, dependencies: DependencyMode,
) -> Self { ) -> Self {
let mut packages = ForkSet::default(); let mut packages = ForkSet::default();

View File

@ -1,9 +1,8 @@
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use pep508_rs::MarkerEnvironment;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use crate::{DependencyMode, Manifest}; use crate::{DependencyMode, Manifest, ResolverMarkers};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)] #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")] #[serde(deny_unknown_fields, rename_all = "kebab-case")]
@ -47,7 +46,7 @@ impl ResolutionStrategy {
pub(crate) fn from_mode( pub(crate) fn from_mode(
mode: ResolutionMode, mode: ResolutionMode,
manifest: &Manifest, manifest: &Manifest,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
dependencies: DependencyMode, dependencies: DependencyMode,
) -> Self { ) -> Self {
match mode { match mode {

View File

@ -1,9 +1,8 @@
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use pep508_rs::MarkerEnvironment;
use uv_normalize::{GroupName, PackageName}; use uv_normalize::{GroupName, PackageName};
use crate::Manifest; use crate::{Manifest, ResolverMarkers};
/// A map of package names to their activated dependency groups. /// A map of package names to their activated dependency groups.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
@ -11,7 +10,7 @@ pub(crate) struct Groups(FxHashMap<PackageName, Vec<GroupName>>);
impl Groups { impl Groups {
/// Determine the set of enabled dependency groups in the [`Manifest`]. /// 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(); let mut groups = FxHashMap::default();
// Enable the groups for all direct dependencies. In practice, this tends to mean: when // Enable the groups for all direct dependencies. In practice, this tends to mean: when

View File

@ -3,7 +3,7 @@ use std::str::FromStr;
use distribution_filename::{SourceDistFilename, WheelFilename}; use distribution_filename::{SourceDistFilename, WheelFilename};
use distribution_types::RemoteSource; use distribution_types::RemoteSource;
use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError}; use pep440_rs::{Operator, Version, VersionSpecifier, VersionSpecifierBuildError};
use pep508_rs::{MarkerEnvironment, PackageName}; use pep508_rs::PackageName;
use pypi_types::RequirementSource; use pypi_types::RequirementSource;
use crate::resolver::ForkMap; use crate::resolver::ForkMap;
@ -17,7 +17,7 @@ impl Locals {
/// Determine the set of permitted local versions in the [`Manifest`]. /// Determine the set of permitted local versions in the [`Manifest`].
pub(crate) fn from_manifest( pub(crate) fn from_manifest(
manifest: &Manifest, manifest: &Manifest,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
dependencies: DependencyMode, dependencies: DependencyMode,
) -> Self { ) -> Self {
let mut locals = ForkMap::default(); let mut locals = ForkMap::default();

View File

@ -157,11 +157,7 @@ impl<'a, Context: BuildContext, InstalledPackages: InstalledPackagesProvider>
python_requirement python_requirement
.target() .target()
.and_then(|target| target.as_requires_python()), .and_then(|target| target.as_requires_python()),
AllowedYanks::from_manifest( AllowedYanks::from_manifest(&manifest, &markers, options.dependency_mode),
&manifest,
markers.marker_environment(),
options.dependency_mode,
),
hasher, hasher,
options.exclude_newer, options.exclude_newer,
build_context.build_options(), build_context.build_options(),
@ -199,24 +195,11 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
let state = ResolverState { let state = ResolverState {
index: index.clone(), index: index.clone(),
git: git.clone(), git: git.clone(),
selector: CandidateSelector::for_resolution( selector: CandidateSelector::for_resolution(options, &manifest, &markers),
options,
&manifest,
markers.marker_environment(),
),
dependency_mode: options.dependency_mode, dependency_mode: options.dependency_mode,
urls: Urls::from_manifest( urls: Urls::from_manifest(&manifest, &markers, git, options.dependency_mode)?,
&manifest, locals: Locals::from_manifest(&manifest, &markers, options.dependency_mode),
markers.marker_environment(), groups: Groups::from_manifest(&manifest, &markers),
git,
options.dependency_mode,
)?,
locals: Locals::from_manifest(
&manifest,
markers.marker_environment(),
options.dependency_mode,
),
groups: Groups::from_manifest(&manifest, markers.marker_environment()),
project: manifest.project, project: manifest.project,
workspace_members: manifest.workspace_members, workspace_members: manifest.workspace_members,
requirements: manifest.requirements, requirements: manifest.requirements,

View File

@ -1,7 +1,7 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use tracing::debug;
use pep508_rs::{MarkerEnvironment, MarkerTree}; use pep508_rs::{MarkerEnvironment, MarkerTree};
use pypi_types::ResolverMarkerEnvironment;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
/// Whether we're solving for a specific environment, universally or for a specific fork. /// Whether we're solving for a specific environment, universally or for a specific fork.
@ -20,8 +20,8 @@ pub enum ResolverMarkers {
impl ResolverMarkers { impl ResolverMarkers {
/// Set the resolver to perform a resolution for a specific environment. /// Set the resolver to perform a resolution for a specific environment.
pub fn specific_environment(markers: MarkerEnvironment) -> Self { pub fn specific_environment(markers: ResolverMarkerEnvironment) -> Self {
Self::SpecificEnvironment(ResolverMarkerEnvironment::from(markers)) Self::SpecificEnvironment(markers)
} }
/// Set the resolver to perform a universal resolution. /// 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<MarkerEnvironment> 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
}
}

View File

@ -6,12 +6,12 @@ use tracing::debug;
use cache_key::CanonicalUrl; use cache_key::CanonicalUrl;
use distribution_types::Verbatim; use distribution_types::Verbatim;
use pep508_rs::{MarkerEnvironment, VerbatimUrl}; use pep508_rs::VerbatimUrl;
use pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl}; use pypi_types::{ParsedDirectoryUrl, ParsedUrl, VerbatimParsedUrl};
use uv_git::GitResolver; use uv_git::GitResolver;
use uv_normalize::PackageName; use uv_normalize::PackageName;
use crate::{DependencyMode, Manifest, ResolveError}; use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers};
/// The URLs that are allowed for packages. /// The URLs that are allowed for packages.
/// ///
@ -36,7 +36,7 @@ pub(crate) struct Urls {
impl Urls { impl Urls {
pub(crate) fn from_manifest( pub(crate) fn from_manifest(
manifest: &Manifest, manifest: &Manifest,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
git: &GitResolver, git: &GitResolver,
dependencies: DependencyMode, dependencies: DependencyMode,
) -> Result<Self, ResolveError> { ) -> Result<Self, ResolveError> {

View File

@ -1,12 +1,12 @@
use pypi_types::RequirementSource;
use rustc_hash::{FxHashMap, FxHashSet};
use std::sync::Arc; use std::sync::Arc;
use rustc_hash::{FxHashMap, FxHashSet};
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::MarkerEnvironment; use pypi_types::RequirementSource;
use uv_normalize::PackageName; 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 /// A set of package versions that are permitted, even if they're marked as yanked by the
/// relevant index. /// relevant index.
@ -16,7 +16,7 @@ pub struct AllowedYanks(Arc<FxHashMap<PackageName, FxHashSet<Version>>>);
impl AllowedYanks { impl AllowedYanks {
pub fn from_manifest( pub fn from_manifest(
manifest: &Manifest, manifest: &Manifest,
markers: Option<&MarkerEnvironment>, markers: &ResolverMarkers,
dependencies: DependencyMode, dependencies: DependencyMode,
) -> Self { ) -> Self {
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default(); let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();

View File

@ -7,8 +7,9 @@ use distribution_types::{
DistributionMetadata, HashPolicy, Name, Resolution, UnresolvedRequirement, VersionId, DistributionMetadata, HashPolicy, Name, Resolution, UnresolvedRequirement, VersionId,
}; };
use pep440_rs::Version; use pep440_rs::Version;
use pep508_rs::MarkerEnvironment; use pypi_types::{
use pypi_types::{HashDigest, HashError, Requirement, RequirementSource}; HashDigest, HashError, Requirement, RequirementSource, ResolverMarkerEnvironment,
};
use uv_configuration::HashCheckingMode; use uv_configuration::HashCheckingMode;
use uv_normalize::PackageName; use uv_normalize::PackageName;
@ -125,7 +126,7 @@ impl HashStrategy {
/// to "only evaluate marker expressions that reference an extra name.") /// to "only evaluate marker expressions that reference an extra name.")
pub fn from_requirements<'a>( pub fn from_requirements<'a>(
requirements: impl Iterator<Item = (&'a UnresolvedRequirement, &'a [String])>, requirements: impl Iterator<Item = (&'a UnresolvedRequirement, &'a [String])>,
markers: Option<&MarkerEnvironment>, marker_env: Option<&ResolverMarkerEnvironment>,
mode: HashCheckingMode, mode: HashCheckingMode,
) -> Result<Self, HashStrategyError> { ) -> Result<Self, HashStrategyError> {
let mut hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default(); let mut hashes = FxHashMap::<VersionId, Vec<HashDigest>>::default();
@ -133,7 +134,9 @@ impl HashStrategy {
// For each requirement, map from name to allowed hashes. We use the last entry for each // For each requirement, map from name to allowed hashes. We use the last entry for each
// package. // package.
for (requirement, digests) in requirements { for (requirement, digests) in requirements {
if !requirement.evaluate_markers(markers, &[]) { if !requirement
.evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[])
{
continue; continue;
} }

View File

@ -52,8 +52,12 @@ pub(crate) fn pip_check(
.dimmed() .dimmed()
)?; )?;
// Determine the markers to use for resolution.
let markers = environment.interpreter().resolver_markers();
// Run the diagnostics.
let diagnostics: Vec<SitePackagesDiagnostic> = let diagnostics: Vec<SitePackagesDiagnostic> =
site_packages.diagnostics()?.into_iter().collect(); site_packages.diagnostics(&markers)?.into_iter().collect();
if diagnostics.is_empty() { if diagnostics.is_empty() {
writeln!( writeln!(

View File

@ -248,10 +248,7 @@ pub(crate) async fn pip_compile(
} else { } else {
let (tags, markers) = let (tags, markers) =
resolution_environment(python_version, python_platform, &interpreter)?; resolution_environment(python_version, python_platform, &interpreter)?;
( (Some(tags), ResolverMarkers::specific_environment(markers))
Some(tags),
ResolverMarkers::specific_environment((*markers).clone()),
)
}; };
// Generate, but don't enforce hashes for the requirements. // Generate, but don't enforce hashes for the requirements.

View File

@ -68,7 +68,10 @@ pub(crate) fn pip_freeze(
// Validate that the environment is consistent. // Validate that the environment is consistent.
if strict { 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!( writeln!(
printer.stderr(), printer.stderr(),
"{}{} {}", "{}{} {}",

View File

@ -32,7 +32,7 @@ use uv_types::{BuildIsolation, HashStrategy};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger, InstallLogger};
use crate::commands::pip::operations::Modifications; 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::commands::{ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
@ -183,6 +183,14 @@ pub(crate) async fn pip_install(
let _lock = environment.lock()?; 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. // Determine the set of installed packages.
let site_packages = SitePackages::from_environment(&environment)?; 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, // 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. // 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() { 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. // If the requirements are already satisfied, we're done.
SatisfiesResult::Fresh { SatisfiesResult::Fresh {
recursive_requirements, 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. // Determine the Python requirement, if the user requested a specific version.
let python_requirement = if let Some(python_version) = python_version.as_ref() { let python_requirement = if let Some(python_version) = python_version.as_ref() {
PythonRequirement::from_python_version(interpreter, python_version) PythonRequirement::from_python_version(interpreter, python_version)
@ -225,8 +231,12 @@ pub(crate) async fn pip_install(
PythonRequirement::from_interpreter(interpreter) PythonRequirement::from_interpreter(interpreter)
}; };
// Determine the environment for the resolution. // Determine the tags to use for the resolution.
let (tags, markers) = resolution_environment(python_version, python_platform, interpreter)?; let tags = resolution_tags(
python_version.as_ref(),
python_platform.as_ref(),
interpreter,
)?;
// Collect the set of required hashes. // Collect the set of required hashes.
let hasher = if let Some(hash_checking) = hash_checking { let hasher = if let Some(hash_checking) = hash_checking {
@ -262,7 +272,7 @@ pub(crate) async fn pip_install(
.cache(cache.clone()) .cache(cache.clone())
.index_urls(index_locations.index_urls()) .index_urls(index_locations.index_urls())
.index_strategy(index_strategy) .index_strategy(index_strategy)
.markers(&markers) .markers(interpreter.markers())
.platform(interpreter.platform()) .platform(interpreter.platform())
.build(); .build();
@ -333,7 +343,7 @@ pub(crate) async fn pip_install(
&reinstall, &reinstall,
&upgrade, &upgrade,
Some(&tags), Some(&tags),
ResolverMarkers::specific_environment((*markers).clone()), ResolverMarkers::specific_environment(markers.clone()),
python_requirement, python_requirement,
&client, &client,
&flat_index, &flat_index,
@ -366,6 +376,7 @@ pub(crate) async fn pip_install(
compile, compile,
&index_locations, &index_locations,
&hasher, &hasher,
&markers,
&tags, &tags,
&client, &client,
&state.in_flight, &state.in_flight,
@ -384,7 +395,7 @@ pub(crate) async fn pip_install(
// Notify the user of any environment diagnostics. // Notify the user of any environment diagnostics.
if strict && !dry_run { if strict && !dry_run {
operations::diagnose_environment(&resolution, &environment, printer)?; operations::diagnose_environment(&resolution, &environment, &markers, printer)?;
} }
Ok(ExitStatus::Success) Ok(ExitStatus::Success)

View File

@ -119,7 +119,10 @@ pub(crate) fn pip_list(
// Validate that the environment is consistent. // Validate that the environment is consistent.
if strict { 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!( writeln!(
printer.stderr(), printer.stderr(),
"{}{} {}", "{}{} {}",

View File

@ -1,7 +1,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use pep508_rs::MarkerEnvironment;
use platform_tags::{Tags, TagsError}; use platform_tags::{Tags, TagsError};
use pypi_types::ResolverMarkerEnvironment;
use uv_configuration::TargetTriple; use uv_configuration::TargetTriple;
use uv_python::{Interpreter, PythonVersion}; use uv_python::{Interpreter, PythonVersion};
@ -17,12 +17,65 @@ pub(crate) mod sync;
pub(crate) mod tree; pub(crate) mod tree;
pub(crate) mod uninstall; 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<Cow<'env, Tags>, 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. /// Determine the tags, markers, and interpreter to use for resolution.
pub(crate) fn resolution_environment( pub(crate) fn resolution_environment(
python_version: Option<PythonVersion>, python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>, python_platform: Option<TargetTriple>,
interpreter: &Interpreter, interpreter: &Interpreter,
) -> Result<(Cow<'_, Tags>, Cow<'_, MarkerEnvironment>), TagsError> { ) -> Result<(Cow<'_, Tags>, ResolverMarkerEnvironment), TagsError> {
let tags = match (python_platform, python_version.as_ref()) { let tags = match (python_platform, python_version.as_ref()) {
(Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env( (Some(python_platform), Some(python_version)) => Cow::Owned(Tags::from_env(
&python_platform.platform(), &python_platform.platform(),
@ -53,12 +106,16 @@ pub(crate) fn resolution_environment(
// Apply the platform tags to the markers. // Apply the platform tags to the markers.
let markers = match (python_platform, python_version) { let markers = match (python_platform, python_version) {
(Some(python_platform), Some(python_version)) => { (Some(python_platform), Some(python_version)) => ResolverMarkerEnvironment::from(
Cow::Owned(python_version.markers(&python_platform.markers(interpreter.markers()))) 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)) => {
(None, Some(python_version)) => Cow::Owned(python_version.markers(interpreter.markers())), ResolverMarkerEnvironment::from(python_version.markers(interpreter.markers()))
(None, None) => Cow::Borrowed(interpreter.markers()), }
(None, None) => interpreter.resolver_markers(),
}; };
Ok((tags, markers)) Ok((tags, markers))

View File

@ -17,7 +17,7 @@ use distribution_types::{
}; };
use install_wheel_rs::linker::LinkMode; use install_wheel_rs::linker::LinkMode;
use platform_tags::Tags; use platform_tags::Tags;
use pypi_types::Requirement; use pypi_types::{Requirement, ResolverMarkerEnvironment};
use uv_cache::Cache; use uv_cache::Cache;
use uv_client::{BaseClientBuilder, RegistryClient}; use uv_client::{BaseClientBuilder, RegistryClient};
use uv_configuration::{ use uv_configuration::{
@ -199,7 +199,7 @@ pub(crate) async fn resolve<InstalledPackages: InstalledPackagesProvider>(
.chain(upgrade.constraints().cloned()), .chain(upgrade.constraints().cloned()),
); );
let overrides = Overrides::from_requirements(overrides); 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. // Determine any lookahead requirements.
let lookaheads = match options.dependency_mode { let lookaheads = match options.dependency_mode {
@ -349,6 +349,7 @@ pub(crate) async fn install(
compile: bool, compile: bool,
index_urls: &IndexLocations, index_urls: &IndexLocations,
hasher: &HashStrategy, hasher: &HashStrategy,
markers: &ResolverMarkerEnvironment,
tags: &Tags, tags: &Tags,
client: &RegistryClient, client: &RegistryClient,
in_flight: &InFlight, in_flight: &InFlight,
@ -376,6 +377,7 @@ pub(crate) async fn install(
index_urls, index_urls,
cache, cache,
venv, venv,
markers,
tags, tags,
) )
.context("Failed to determine installation plan")?; .context("Failed to determine installation plan")?;
@ -661,10 +663,11 @@ pub(crate) fn diagnose_resolution(
pub(crate) fn diagnose_environment( pub(crate) fn diagnose_environment(
resolution: &Resolution, resolution: &Resolution,
venv: &PythonEnvironment, venv: &PythonEnvironment,
markers: &ResolverMarkerEnvironment,
printer: Printer, printer: Printer,
) -> Result<(), Error> { ) -> Result<(), Error> {
let site_packages = SitePackages::from_environment(venv)?; 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. // Only surface diagnostics that are "relevant" to the current resolution.
if resolution if resolution
.packages() .packages()

View File

@ -55,7 +55,7 @@ pub(crate) fn pip_show(
let site_packages = SitePackages::from_environment(&environment)?; let site_packages = SitePackages::from_environment(&environment)?;
// Determine the markers to use for resolution. // 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. // Sort and deduplicate the packages, which are keyed by name.
packages.sort_unstable(); packages.sort_unstable();
@ -101,7 +101,7 @@ pub(crate) fn pip_show(
metadata metadata
.requires_dist .requires_dist
.into_iter() .into_iter()
.filter(|req| req.evaluate_markers(markers, &[])) .filter(|req| req.evaluate_markers(&markers, &[]))
.map(|req| req.name) .map(|req| req.name)
.sorted_unstable() .sorted_unstable()
.dedup() .dedup()
@ -119,7 +119,7 @@ pub(crate) fn pip_show(
let requires = metadata let requires = metadata
.requires_dist .requires_dist
.into_iter() .into_iter()
.filter(|req| req.evaluate_markers(markers, &[])) .filter(|req| req.evaluate_markers(&markers, &[]))
.map(|req| req.name) .map(|req| req.name)
.collect_vec(); .collect_vec();
if !requires.is_empty() { if !requires.is_empty() {
@ -192,7 +192,7 @@ pub(crate) fn pip_show(
// Validate that the environment is consistent. // Validate that the environment is consistent.
if strict { if strict {
for diagnostic in site_packages.diagnostics()? { for diagnostic in site_packages.diagnostics(&markers)? {
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{}{} {}", "{}{} {}",

View File

@ -31,7 +31,7 @@ use uv_types::{BuildIsolation, HashStrategy};
use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger}; use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::pip::operations::Modifications; 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::commands::{ExitStatus, SharedState};
use crate::printer::Printer; use crate::printer::Printer;
@ -183,8 +183,17 @@ pub(crate) async fn pip_sync(
PythonRequirement::from_interpreter(interpreter) PythonRequirement::from_interpreter(interpreter)
}; };
// Determine the environment for the resolution. // Determine the markers and tags to use for resolution.
let (tags, markers) = resolution_environment(python_version, python_platform, interpreter)?; 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. // Collect the set of required hashes.
let hasher = if let Some(hash_checking) = hash_checking { let hasher = if let Some(hash_checking) = hash_checking {
@ -213,7 +222,7 @@ pub(crate) async fn pip_sync(
.cache(cache.clone()) .cache(cache.clone())
.index_urls(index_locations.index_urls()) .index_urls(index_locations.index_urls())
.index_strategy(index_strategy) .index_strategy(index_strategy)
.markers(&markers) .markers(interpreter.markers())
.platform(interpreter.platform()) .platform(interpreter.platform())
.build(); .build();
@ -292,7 +301,7 @@ pub(crate) async fn pip_sync(
&reinstall, &reinstall,
&upgrade, &upgrade,
Some(&tags), Some(&tags),
ResolverMarkers::specific_environment((*markers).clone()), ResolverMarkers::specific_environment(markers.clone()),
python_requirement, python_requirement,
&client, &client,
&flat_index, &flat_index,
@ -325,6 +334,7 @@ pub(crate) async fn pip_sync(
compile, compile,
&index_locations, &index_locations,
&hasher, &hasher,
&markers,
&tags, &tags,
&client, &client,
&state.in_flight, &state.in_flight,
@ -343,7 +353,7 @@ pub(crate) async fn pip_sync(
// Notify the user of any environment diagnostics. // Notify the user of any environment diagnostics.
if strict && !dry_run { if strict && !dry_run {
operations::diagnose_environment(&resolution, &environment, printer)?; operations::diagnose_environment(&resolution, &environment, &markers, printer)?;
} }
Ok(ExitStatus::Success) Ok(ExitStatus::Success)

View File

@ -7,8 +7,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use tracing::debug; use tracing::debug;
use distribution_types::{Diagnostic, Name}; use distribution_types::{Diagnostic, Name};
use pep508_rs::MarkerEnvironment; use pypi_types::{RequirementSource, ResolverMarkerEnvironment};
use pypi_types::RequirementSource;
use uv_cache::Cache; use uv_cache::Cache;
use uv_distribution::Metadata; use uv_distribution::Metadata;
use uv_fs::Simplified; use uv_fs::Simplified;
@ -60,6 +59,9 @@ pub(crate) fn pip_tree(
.push(metadata); .push(metadata);
} }
// Determine the markers to use for the resolution.
let markers = environment.interpreter().resolver_markers();
// Render the tree. // Render the tree.
let rendered_tree = DisplayDependencyGraph::new( let rendered_tree = DisplayDependencyGraph::new(
depth.into(), depth.into(),
@ -68,7 +70,7 @@ pub(crate) fn pip_tree(
no_dedupe, no_dedupe,
invert, invert,
show_version_specifiers, show_version_specifiers,
environment.interpreter().markers(), &markers,
packages, packages,
) )
.render() .render()
@ -87,7 +89,7 @@ pub(crate) fn pip_tree(
// Validate that the environment is consistent. // Validate that the environment is consistent.
if strict { if strict {
for diagnostic in site_packages.diagnostics()? { for diagnostic in site_packages.diagnostics(&markers)? {
writeln!( writeln!(
printer.stderr(), printer.stderr(),
"{}{} {}", "{}{} {}",
@ -129,7 +131,7 @@ impl DisplayDependencyGraph {
no_dedupe: bool, no_dedupe: bool,
invert: bool, invert: bool,
show_version_specifiers: bool, show_version_specifiers: bool,
markers: &MarkerEnvironment, markers: &ResolverMarkerEnvironment,
packages: IndexMap<PackageName, Vec<Metadata>>, packages: IndexMap<PackageName, Vec<Metadata>>,
) -> Self { ) -> Self {
let mut requirements: FxHashMap<_, Vec<_>> = FxHashMap::default(); let mut requirements: FxHashMap<_, Vec<_>> = FxHashMap::default();

View File

@ -534,7 +534,7 @@ pub(crate) async fn resolve_environment<'a>(
// Determine the tags, markers, and interpreter to use for resolution. // Determine the tags, markers, and interpreter to use for resolution.
let tags = interpreter.tags()?; let tags = interpreter.tags()?;
let markers = interpreter.markers(); let markers = interpreter.resolver_markers();
let python_requirement = PythonRequirement::from_interpreter(interpreter); let python_requirement = PythonRequirement::from_interpreter(interpreter);
// Add all authenticated sources to the cache. // 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_urls(index_locations.index_urls())
.index_strategy(index_strategy) .index_strategy(index_strategy)
.keyring(keyring_provider) .keyring(keyring_provider)
.markers(markers) .markers(interpreter.markers())
.platform(interpreter.platform()) .platform(interpreter.platform())
.build(); .build();
@ -629,7 +629,7 @@ pub(crate) async fn resolve_environment<'a>(
&reinstall, &reinstall,
&upgrade, &upgrade,
Some(tags), Some(tags),
ResolverMarkers::specific_environment(markers.clone()), ResolverMarkers::specific_environment(markers),
python_requirement, python_requirement,
&client, &client,
&flat_index, &flat_index,
@ -673,10 +673,10 @@ pub(crate) async fn sync_environment(
let site_packages = SitePackages::from_environment(&venv)?; 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 interpreter = venv.interpreter();
let tags = venv.interpreter().tags()?; let tags = venv.interpreter().tags()?;
let markers = venv.interpreter().markers(); let markers = interpreter.resolver_markers();
// Add all authenticated sources to the cache. // Add all authenticated sources to the cache.
for url in index_locations.urls() { for url in index_locations.urls() {
@ -690,7 +690,7 @@ pub(crate) async fn sync_environment(
.index_urls(index_locations.index_urls()) .index_urls(index_locations.index_urls())
.index_strategy(index_strategy) .index_strategy(index_strategy)
.keyring(keyring_provider) .keyring(keyring_provider)
.markers(markers) .markers(interpreter.markers())
.platform(interpreter.platform()) .platform(interpreter.platform())
.build(); .build();
@ -748,6 +748,7 @@ pub(crate) async fn sync_environment(
compile_bytecode, compile_bytecode,
index_locations, index_locations,
&hasher, &hasher,
&markers,
tags, tags,
&client, &client,
&state.in_flight, &state.in_flight,
@ -827,10 +828,14 @@ pub(crate) async fn update_environment(
.. ..
} = spec; } = 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 // Check if the current environment satisfies the requirements
let site_packages = SitePackages::from_environment(&venv)?; let site_packages = SitePackages::from_environment(&venv)?;
if source_trees.is_empty() && reinstall.is_none() && upgrade.is_none() && overrides.is_empty() { 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. // If the requirements are already satisfied, we're done.
SatisfiesResult::Fresh { SatisfiesResult::Fresh {
recursive_requirements, 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. // Add all authenticated sources to the cache.
for url in index_locations.urls() { for url in index_locations.urls() {
store_credentials_from_url(url); store_credentials_from_url(url);
@ -872,7 +871,7 @@ pub(crate) async fn update_environment(
.index_urls(index_locations.index_urls()) .index_urls(index_locations.index_urls())
.index_strategy(*index_strategy) .index_strategy(*index_strategy)
.keyring(*keyring_provider) .keyring(*keyring_provider)
.markers(markers) .markers(interpreter.markers())
.platform(interpreter.platform()) .platform(interpreter.platform())
.build(); .build();
@ -901,6 +900,10 @@ pub(crate) async fn update_environment(
let hasher = HashStrategy::default(); let hasher = HashStrategy::default();
let preferences = Vec::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`. // Resolve the flat indexes from `--find-links`.
let flat_index = { let flat_index = {
let client = FlatIndexClient::new(&client, cache); let client = FlatIndexClient::new(&client, cache);
@ -973,6 +976,7 @@ pub(crate) async fn update_environment(
*compile_bytecode, *compile_bytecode,
index_locations, index_locations,
&hasher, &hasher,
&markers,
tags, tags,
&client, &client,
&state.in_flight, &state.in_flight,

View File

@ -11,7 +11,6 @@ use itertools::Itertools;
use owo_colors::OwoColorize; use owo_colors::OwoColorize;
use tokio::process::Command; use tokio::process::Command;
use tracing::{debug, warn}; use tracing::{debug, warn};
use uv_cache::Cache; use uv_cache::Cache;
use uv_cli::ExternalCommand; use uv_cli::ExternalCommand;
use uv_client::{BaseClientBuilder, Connectivity}; use uv_client::{BaseClientBuilder, Connectivity};
@ -692,7 +691,11 @@ fn can_skip_ephemeral(
return false; 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. // If the requirements are already satisfied, we're done.
Ok(SatisfiesResult::Fresh { Ok(SatisfiesResult::Fresh {
recursive_requirements, recursive_requirements,

View File

@ -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. // Validate that the platform is supported by the lockfile.
let environments = lock.supported_environments(); let environments = lock.supported_environments();
if !environments.is_empty() { if !environments.is_empty() {
let platform = venv.interpreter().markers(); if !environments.iter().any(|env| env.evaluate(&markers, &[])) {
if !environments.iter().any(|env| env.evaluate(platform, &[])) {
return Err(ProjectError::LockedPlatformIncompatibility( return Err(ProjectError::LockedPlatformIncompatibility(
environments environments
.iter() .iter()
@ -195,11 +197,11 @@ pub(super) async fn do_sync(
vec![] vec![]
}; };
let markers = venv.interpreter().markers(); // Determine the tags to use for resolution.
let tags = venv.interpreter().tags()?; let tags = venv.interpreter().tags()?;
// Read the lockfile. // 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. // If `--no-install-project` is set, remove the project itself.
let resolution = apply_no_install_project(no_install_project, resolution, project); 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_urls(index_locations.index_urls())
.index_strategy(index_strategy) .index_strategy(index_strategy)
.keyring(keyring_provider) .keyring(keyring_provider)
.markers(markers) .markers(venv.interpreter().markers())
.platform(venv.interpreter().platform()) .platform(venv.interpreter().platform())
.build(); .build();
@ -284,6 +286,7 @@ pub(super) async fn do_sync(
compile_bytecode, compile_bytecode,
index_locations, index_locations,
&hasher, &hasher,
&markers,
tags, tags,
&client, &client,
&state.in_flight, &state.in_flight,

View File

@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::fmt::Write; use std::fmt::Write;
use anyhow::Result; use anyhow::Result;
@ -13,6 +12,7 @@ use uv_resolver::TreeDisplay;
use uv_workspace::{DiscoveryOptions, Workspace}; use uv_workspace::{DiscoveryOptions, Workspace};
use crate::commands::pip::loggers::DefaultResolveLogger; use crate::commands::pip::loggers::DefaultResolveLogger;
use crate::commands::pip::resolution_markers;
use crate::commands::project::FoundInterpreter; use crate::commands::project::FoundInterpreter;
use crate::commands::{project, ExitStatus}; use crate::commands::{project, ExitStatus};
use crate::printer::Printer; use crate::printer::Printer;
@ -75,20 +75,17 @@ pub(crate) async fn tree(
.await? .await?
.into_lock(); .into_lock();
// Apply the platform tags to the markers. // Determine the markers to use for resolution.
let markers = match (python_platform, python_version) { let markers = resolution_markers(
(Some(python_platform), Some(python_version)) => { python_version.as_ref(),
Cow::Owned(python_version.markers(&python_platform.markers(interpreter.markers()))) python_platform.as_ref(),
} &interpreter,
(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()),
};
// Render the tree. // Render the tree.
let tree = TreeDisplay::new( let tree = TreeDisplay::new(
&lock, &lock,
(!universal).then(|| markers.as_ref()), (!universal).then_some(&markers),
depth.into(), depth.into(),
prune, prune,
package, package,

View File

@ -514,7 +514,11 @@ async fn get_or_create_environment(
let constraints = []; let constraints = [];
if matches!( if matches!(
site_packages.satisfies(&requirements, &constraints), site_packages.satisfies(
&requirements,
&constraints,
&interpreter.resolver_markers()
),
Ok(SatisfiesResult::Fresh { .. }) Ok(SatisfiesResult::Fresh { .. })
) { ) {
debug!("Using existing tool `{}`", from.name); debug!("Using existing tool `{}`", from.name);

View File

@ -6338,3 +6338,51 @@ fn no_extension() {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"###); "###);
} }
/// Regression test for: <https://github.com/astral-sh/uv/pull/6646>
#[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(())
}