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 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(

View file

@ -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;

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> {
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,
)?;

View file

@ -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<Plan> {
// 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;
}

View file

@ -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<Vec<SitePackagesDiagnostic>> {
pub fn diagnostics(
&self,
markers: &ResolverMarkerEnvironment,
) -> Result<Vec<SitePackagesDiagnostic>> {
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<SatisfiesResult> {
// 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,

View file

@ -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(

View file

@ -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"`

View file

@ -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(

View file

@ -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<PackageName>,
package: Vec<PackageName>,

View file

@ -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<Item = Cow<'a, Requirement>> + '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<Item = Cow<'a, Requirement>> + '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<Item = Cow<'a, Requirement>> + '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<Item = Cow<'a, Requirement>> + '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<Item = Cow<'a, Requirement>> + '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.

View file

@ -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<PreferenceIterator: IntoIterator<Item = Preference>>(
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;

View file

@ -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();

View file

@ -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 {

View file

@ -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<PackageName, Vec<GroupName>>);
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

View file

@ -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();

View file

@ -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<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
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,

View file

@ -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<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 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<Self, ResolveError> {

View file

@ -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<FxHashMap<PackageName, FxHashSet<Version>>>);
impl AllowedYanks {
pub fn from_manifest(
manifest: &Manifest,
markers: Option<&MarkerEnvironment>,
markers: &ResolverMarkers,
dependencies: DependencyMode,
) -> Self {
let mut allowed_yanks = FxHashMap::<PackageName, FxHashSet<Version>>::default();

View file

@ -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<Item = (&'a UnresolvedRequirement, &'a [String])>,
markers: Option<&MarkerEnvironment>,
marker_env: Option<&ResolverMarkerEnvironment>,
mode: HashCheckingMode,
) -> Result<Self, HashStrategyError> {
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
// package.
for (requirement, digests) in requirements {
if !requirement.evaluate_markers(markers, &[]) {
if !requirement
.evaluate_markers(marker_env.map(ResolverMarkerEnvironment::markers), &[])
{
continue;
}

View file

@ -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<SitePackagesDiagnostic> =
site_packages.diagnostics()?.into_iter().collect();
site_packages.diagnostics(&markers)?.into_iter().collect();
if diagnostics.is_empty() {
writeln!(

View file

@ -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.

View file

@ -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(),
"{}{} {}",

View file

@ -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)

View file

@ -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(),
"{}{} {}",

View file

@ -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<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.
pub(crate) fn resolution_environment(
python_version: Option<PythonVersion>,
python_platform: Option<TargetTriple>,
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))

View file

@ -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<InstalledPackages: InstalledPackagesProvider>(
.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()

View file

@ -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(),
"{}{} {}",

View file

@ -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)

View file

@ -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<PackageName, Vec<Metadata>>,
) -> Self {
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.
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,

View file

@ -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,

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.
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,

View file

@ -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,

View file

@ -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);

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(())
}