Skip to content

Commit

Permalink
Store resolution options in lockfile (#5264)
Browse files Browse the repository at this point in the history
## Summary

This PR modifies the lockfile to include the impactful resolution
settings, like the resolution and pre-release mode. If any of those
values change, we want to ignore the existing lockfile. Otherwise,
`--resolution lowest-direct` will typically have no effect, which is
really unintuitive.

Closes #5226.
  • Loading branch information
charliermarsh committed Jul 22, 2024
1 parent 178300d commit 5a23f05
Show file tree
Hide file tree
Showing 23 changed files with 406 additions and 16 deletions.
2 changes: 1 addition & 1 deletion crates/uv-configuration/src/build_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ impl NoBuild {
}
}

#[derive(Debug, Default, Clone, Copy, Hash, Eq, PartialEq, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/dependency_mode.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
pub enum DependencyMode {
/// Include all dependencies, whether direct or transitive.
#[default]
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/exclude_newer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::str::FromStr;
use chrono::{DateTime, Days, NaiveDate, NaiveTime, Utc};

/// A timestamp that excludes files newer than it.
#[derive(Debug, Copy, Clone, serde::Deserialize, serde::Serialize)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct ExcludeNewer(DateTime<Utc>);

impl ExcludeNewer {
Expand Down
75 changes: 70 additions & 5 deletions crates/uv-resolver/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ use uv_workspace::VirtualProject;
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
use crate::resolver::FxOnceMap;
use crate::{
InMemoryIndex, MetadataResponse, RequiresPython, ResolutionGraph, VersionMap, VersionsResponse,
ExcludeNewer, InMemoryIndex, MetadataResponse, PreReleaseMode, RequiresPython, ResolutionGraph,
ResolutionMode, VersionMap, VersionsResponse,
};

/// The current version of the lock file format.
Expand All @@ -55,6 +56,12 @@ pub struct Lock {
distributions: Vec<Distribution>,
/// The range of supported Python versions.
requires_python: Option<RequiresPython>,
/// The [`ResolutionMode`] used to generate this lock.
resolution_mode: ResolutionMode,
/// The [`PreReleaseMode`] used to generate this lock.
prerelease_mode: PreReleaseMode,
/// The [`ExcludeNewer`] used to generate this lock.
exclude_newer: Option<ExcludeNewer>,
/// A map from distribution ID to index in `distributions`.
///
/// This can be used to quickly lookup the full distribution for any ID
Expand Down Expand Up @@ -144,7 +151,15 @@ impl Lock {

let distributions = locked_dists.into_values().collect();
let requires_python = graph.requires_python.clone();
let lock = Self::new(VERSION, distributions, requires_python)?;
let options = graph.options;
let lock = Self::new(
VERSION,
distributions,
requires_python,
options.resolution_mode,
options.prerelease_mode,
options.exclude_newer,
)?;
Ok(lock)
}

Expand All @@ -153,6 +168,9 @@ impl Lock {
version: u32,
mut distributions: Vec<Distribution>,
requires_python: Option<RequiresPython>,
resolution_mode: ResolutionMode,
prerelease_mode: PreReleaseMode,
exclude_newer: Option<ExcludeNewer>,
) -> Result<Self, LockError> {
// Put all dependencies for each distribution in a canonical order and
// check for duplicates.
Expand Down Expand Up @@ -307,10 +325,13 @@ impl Lock {
}
}
}
Ok(Lock {
Ok(Self {
version,
distributions,
requires_python,
resolution_mode,
prerelease_mode,
exclude_newer,
by_id,
})
}
Expand All @@ -330,6 +351,21 @@ impl Lock {
self.requires_python.as_ref()
}

/// Returns the resolution mode used to generate this lock.
pub fn resolution_mode(&self) -> ResolutionMode {
self.resolution_mode
}

/// Returns the pre-release mode used to generate this lock.
pub fn prerelease_mode(&self) -> PreReleaseMode {
self.prerelease_mode
}

/// Returns the exclude newer setting used to generate this lock.
pub fn exclude_newer(&self) -> Option<ExcludeNewer> {
self.exclude_newer
}

/// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root.
pub fn to_resolution(
&self,
Expand Down Expand Up @@ -419,6 +455,19 @@ impl Lock {
doc.insert("requires-python", value(requires_python.to_string()));
}

// Write the settings that were used to generate the resolution.
// This enables us to invalidate the lockfile if the user changes
// their settings.
if self.resolution_mode != ResolutionMode::default() {
doc.insert("resolution-mode", value(self.resolution_mode.to_string()));
}
if self.prerelease_mode != PreReleaseMode::default() {
doc.insert("prerelease-mode", value(self.prerelease_mode.to_string()));
}
if let Some(exclude_newer) = self.exclude_newer {
doc.insert("exclude-newer", value(exclude_newer.to_string()));
}

// Count the number of distributions for each package name. When
// there's only one distribution for a particular package name (the
// overwhelmingly common case), we can omit some data (like source and
Expand Down Expand Up @@ -522,12 +571,18 @@ impl Lock {
}

#[derive(Clone, Debug, serde::Deserialize)]
#[serde(rename_all = "kebab-case")]
struct LockWire {
version: u32,
#[serde(rename = "distribution")]
distributions: Vec<DistributionWire>,
#[serde(rename = "requires-python")]
requires_python: Option<RequiresPython>,
#[serde(default)]
resolution_mode: ResolutionMode,
#[serde(default)]
prerelease_mode: PreReleaseMode,
#[serde(default)]
exclude_newer: Option<ExcludeNewer>,
}

impl From<Lock> for LockWire {
Expand All @@ -540,6 +595,9 @@ impl From<Lock> for LockWire {
.map(DistributionWire::from)
.collect(),
requires_python: lock.requires_python,
resolution_mode: lock.resolution_mode,
prerelease_mode: lock.prerelease_mode,
exclude_newer: lock.exclude_newer,
}
}
}
Expand Down Expand Up @@ -570,7 +628,14 @@ impl TryFrom<LockWire> for Lock {
.into_iter()
.map(|dist| dist.unwire(&unambiguous_dist_ids))
.collect::<Result<Vec<_>, _>>()?;
Lock::new(wire.version, distributions, wire.requires_python)
Lock::new(
wire.version,
distributions,
wire.requires_python,
wire.resolution_mode,
wire.prerelease_mode,
wire.exclude_newer,
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv-resolver/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use uv_configuration::IndexStrategy;
use crate::{DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};

/// Options for resolving a manifest.
#[derive(Debug, Default, Copy, Clone)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
pub struct Options {
pub resolution_mode: ResolutionMode,
pub prerelease_mode: PreReleaseMode,
Expand Down
12 changes: 12 additions & 0 deletions crates/uv-resolver/src/prerelease_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ pub enum PreReleaseMode {
IfNecessaryOrExplicit,
}

impl std::fmt::Display for PreReleaseMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Disallow => write!(f, "disallow"),
Self::Allow => write!(f, "allow"),
Self::IfNecessary => write!(f, "if-necessary"),
Self::Explicit => write!(f, "explicit"),
Self::IfNecessaryOrExplicit => write!(f, "if-necessary-or-explicit"),
}
}
}

/// Like [`PreReleaseMode`], but with any additional information required to select a candidate,
/// like the set of direct dependencies.
#[derive(Debug, Clone)]
Expand Down
8 changes: 6 additions & 2 deletions crates/uv-resolver/src/resolution/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ use crate::redirect::url_to_precise;
use crate::resolution::AnnotatedDist;
use crate::resolver::{Resolution, ResolutionPackage};
use crate::{
InMemoryIndex, MetadataResponse, PythonRequirement, RequiresPython, ResolveError,
InMemoryIndex, MetadataResponse, Options, PythonRequirement, RequiresPython, ResolveError,
VersionsResponse,
};

Expand All @@ -42,6 +42,8 @@ pub struct ResolutionGraph {
pub(crate) constraints: Constraints,
/// The overrides that were used to build the graph.
pub(crate) overrides: Overrides,
/// The options that were used to build the graph.
pub(crate) options: Options,
}

#[derive(Debug)]
Expand All @@ -53,14 +55,15 @@ pub(crate) enum ResolutionGraphNode {
impl ResolutionGraph {
/// Create a new graph from the resolved PubGrub state.
pub(crate) fn from_state(
resolution: Resolution,
requirements: &[Requirement],
constraints: &Constraints,
overrides: &Overrides,
preferences: &Preferences,
index: &InMemoryIndex,
git: &GitResolver,
python: &PythonRequirement,
resolution: Resolution,
options: Options,
) -> Result<Self, ResolveError> {
type NodeKey<'a> = (
&'a PackageName,
Expand Down Expand Up @@ -308,6 +311,7 @@ impl ResolutionGraph {
requirements: requirements.to_vec(),
constraints: constraints.clone(),
overrides: overrides.clone(),
options,
})
}

Expand Down
12 changes: 11 additions & 1 deletion crates/uv-resolver/src/resolution_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use uv_normalize::PackageName;

use crate::{DependencyMode, Manifest};

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
Expand All @@ -20,6 +20,16 @@ pub enum ResolutionMode {
LowestDirect,
}

impl std::fmt::Display for ResolutionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Highest => write!(f, "highest"),
Self::Lowest => write!(f, "lowest"),
Self::LowestDirect => write!(f, "lowest-direct"),
}
}
}

/// Like [`ResolutionMode`], but with any additional information required to select a candidate,
/// like the set of direct dependencies.
#[derive(Debug, Clone)]
Expand Down
13 changes: 9 additions & 4 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ struct ResolverState<InstalledPackages: InstalledPackagesProvider> {
unavailable_packages: DashMap<PackageName, UnavailablePackage>,
/// Incompatibilities for packages that are unavailable at specific versions.
incomplete_packages: DashMap<PackageName, DashMap<Version, IncompletePackage>>,
/// The options that were used to configure this resolver.
options: Options,
/// The reporter to use for this resolver.
reporter: Option<Arc<dyn Reporter>>,
}

Expand Down Expand Up @@ -202,8 +205,6 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
let state = ResolverState {
index: index.clone(),
git: git.clone(),
unavailable_packages: DashMap::default(),
incomplete_packages: DashMap::default(),
selector: CandidateSelector::for_resolution(
options,
&manifest,
Expand Down Expand Up @@ -236,8 +237,11 @@ impl<Provider: ResolverProvider, InstalledPackages: InstalledPackagesProvider>
},
markers,
python_requirement: python_requirement.clone(),
reporter: None,
installed_packages,
unavailable_packages: DashMap::default(),
incomplete_packages: DashMap::default(),
options,
reporter: None,
};
Ok(Self { state, provider })
}
Expand Down Expand Up @@ -677,14 +681,15 @@ impl<InstalledPackages: InstalledPackagesProvider> ResolverState<InstalledPackag
}
}
ResolutionGraph::from_state(
combined,
&self.requirements,
&self.constraints,
&self.overrides,
&self.preferences,
&self.index,
&self.git,
&self.python_requirement,
combined,
self.options,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ Ok(
},
],
requires_python: None,
resolution_mode: Highest,
prerelease_mode: IfNecessaryOrExplicit,
exclude_newer: None,
by_id: {
DistributionId {
name: PackageName(
Expand Down
Loading

0 comments on commit 5a23f05

Please sign in to comment.