blob: b401e923275ef6fcd0090aa96da7944fbc792ac1 [file] [log] [blame]
use super::encode::Metadata;
use crate::core::dependency::DepKind;
use crate::core::{Dependency, PackageId, PackageIdSpec, Summary, Target};
use crate::util::errors::CargoResult;
use crate::util::interning::InternedString;
use crate::util::Graph;
use std::borrow::Borrow;
use std::collections::{HashMap, HashSet};
use std::fmt;
/// Represents a fully-resolved package dependency graph. Each node in the graph
/// is a package and edges represent dependencies between packages.
///
/// Each instance of `Resolve` also understands the full set of features used
/// for each package.
pub struct Resolve {
/// A graph, whose vertices are packages and edges are dependency specifications
/// from `Cargo.toml`. We need a `HashSet` here because the same package
/// might be present in both `[dependencies]` and `[build-dependencies]`.
graph: Graph<PackageId, HashSet<Dependency>>,
/// Replacements from the `[replace]` table.
replacements: HashMap<PackageId, PackageId>,
/// Inverted version of `replacements`.
reverse_replacements: HashMap<PackageId, PackageId>,
/// Features enabled for a given package.
features: HashMap<PackageId, Vec<InternedString>>,
/// Checksum for each package. A SHA256 hash of the `.crate` file used to
/// validate the correct crate file is used. This is `None` for sources
/// that do not use `.crate` files, like path or git dependencies.
checksums: HashMap<PackageId, Option<String>>,
/// "Unknown" metadata. This is a collection of extra, unrecognized data
/// found in the `[metadata]` section of `Cargo.lock`, preserved for
/// forwards compatibility.
metadata: Metadata,
/// `[patch]` entries that did not match anything, preserved in
/// `Cargo.lock` as the `[[patch.unused]]` table array. Tracking unused
/// patches helps prevent Cargo from being forced to re-update the
/// registry every time it runs, and keeps the resolve in a locked state
/// so it doesn't re-resolve the unused entries.
unused_patches: Vec<PackageId>,
/// A map from packages to a set of their public dependencies
public_dependencies: HashMap<PackageId, HashSet<PackageId>>,
/// Version of the `Cargo.lock` format, see
/// `cargo::core::resolver::encode` for more.
version: ResolveVersion,
summaries: HashMap<PackageId, Summary>,
}
/// A version to indicate how a `Cargo.lock` should be serialized.
///
/// When creating a new lockfile, the version with `#[default]` is used.
/// If an old version of lockfile already exists, it will stay as-is.
///
/// It's important that if a new version is added that this is not updated
/// until *at least* the support for the version is in the stable release of Rust.
///
/// This resolve version will be used for all new lock files, for example
/// those generated by `cargo update` (update everything) or building after
/// a `cargo new` (where no lock file previously existed). This is also used
/// for *updated* lock files such as when a dependency is added or when a
/// version requirement changes. In this situation Cargo's updating the lock
/// file anyway so it takes the opportunity to bump the lock file version
/// forward.
///
/// It's theorized that we can add more here over time to track larger changes
/// to the `Cargo.lock` format, but we've yet to see how that strategy pans out.
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord)]
pub enum ResolveVersion {
/// Historical baseline for when this abstraction was added.
V1,
/// A more compact format, more amenable to avoiding source-control merge
/// conflicts. The `dependencies` arrays are compressed and checksums are
/// listed inline. Introduced in 2019 in version 1.38. New lockfiles use
/// V2 by default from 1.41 to 1.52.
V2,
/// A format that explicitly lists a `version` at the top of the file as
/// well as changing how git dependencies are encoded. Dependencies with
/// `branch = "master"` are no longer encoded the same way as those without
/// branch specifiers. Introduced in 2020 in version 1.47. New lockfiles use
/// V3 by default staring in 1.53.
#[default]
V3,
/// Unstable. Will collect a certain amount of changes and then go.
///
/// Changes made:
///
/// * SourceId URL serialization is aware of URL encoding.
V4,
}
impl ResolveVersion {
/// The maximum version of lockfile made into the stable channel.
///
/// Any version larger than this needs `-Znext-lockfile-bump` to enable.
///
/// Update this when you're going to stabilize a new lockfile format.
pub fn max_stable() -> ResolveVersion {
ResolveVersion::V3
}
}
impl Resolve {
pub fn new(
graph: Graph<PackageId, HashSet<Dependency>>,
replacements: HashMap<PackageId, PackageId>,
features: HashMap<PackageId, Vec<InternedString>>,
checksums: HashMap<PackageId, Option<String>>,
metadata: Metadata,
unused_patches: Vec<PackageId>,
version: ResolveVersion,
summaries: HashMap<PackageId, Summary>,
) -> Resolve {
let reverse_replacements = replacements.iter().map(|(&p, &r)| (r, p)).collect();
let public_dependencies = graph
.iter()
.map(|p| {
let public_deps = graph
.edges(p)
.filter(|(_, deps)| {
deps.iter()
.any(|d| d.kind() == DepKind::Normal && d.is_public())
})
.map(|(dep_package, _)| *dep_package)
.collect::<HashSet<PackageId>>();
(*p, public_deps)
})
.collect();
Resolve {
graph,
replacements,
features,
checksums,
metadata,
unused_patches,
reverse_replacements,
public_dependencies,
version,
summaries,
}
}
/// Resolves one of the paths from the given dependent package up to
/// the root.
pub fn path_to_top<'a>(
&'a self,
pkg: &'a PackageId,
) -> Vec<(&'a PackageId, Option<&'a HashSet<Dependency>>)> {
self.graph.path_to_top(pkg)
}
pub fn register_used_patches(&mut self, patches: &[Summary]) {
for summary in patches {
if !self.graph.contains(&summary.package_id()) {
self.unused_patches.push(summary.package_id())
};
}
}
pub fn merge_from(&mut self, previous: &Resolve) -> CargoResult<()> {
// Given a previous instance of resolve, it should be forbidden to ever
// have a checksums which *differ*. If the same package ID has differing
// checksums, then something has gone wrong such as:
//
// * Something got seriously corrupted
// * A "mirror" isn't actually a mirror as some changes were made
// * A replacement source wasn't actually a replacement, some changes
// were made
//
// In all of these cases, we want to report an error to indicate that
// something is awry. Normal execution (esp just using crates.io) should
// never run into this.
for (id, cksum) in previous.checksums.iter() {
if let Some(mine) = self.checksums.get(id) {
if mine == cksum {
continue;
}
// If the previous checksum wasn't calculated, the current
// checksum is `Some`. This may indicate that a source was
// erroneously replaced or was replaced with something that
// desires stronger checksum guarantees than can be afforded
// elsewhere.
if cksum.is_none() {
anyhow::bail!(
"\
checksum for `{}` was not previously calculated, but a checksum could now \
be calculated
this could be indicative of a few possible situations:
* the source `{}` did not previously support checksums,
but was replaced with one that does
* newer Cargo implementations know how to checksum this source, but this
older implementation does not
* the lock file is corrupt
",
id,
id.source_id()
)
// If our checksum hasn't been calculated, then it could mean
// that future Cargo figured out how to checksum something or
// more realistically we were overridden with a source that does
// not have checksums.
} else if mine.is_none() {
anyhow::bail!(
"\
checksum for `{}` could not be calculated, but a checksum is listed in \
the existing lock file
this could be indicative of a few possible situations:
* the source `{}` supports checksums,
but was replaced with one that doesn't
* the lock file is corrupt
unable to verify that `{0}` is the same as when the lockfile was generated
",
id,
id.source_id()
)
// If the checksums aren't equal, and neither is None, then they
// must both be Some, in which case the checksum now differs.
// That's quite bad!
} else {
anyhow::bail!(
"\
checksum for `{}` changed between lock files
this could be indicative of a few possible errors:
* the lock file is corrupt
* a replacement source in use (e.g., a mirror) returned a different checksum
* the source itself may be corrupt in one way or another
unable to verify that `{0}` is the same as when the lockfile was generated
",
id
);
}
}
}
// Be sure to just copy over any unknown metadata.
self.metadata = previous.metadata.clone();
// Preserve the lockfile encoding where possible to avoid lockfile churn
self.version = previous.version;
Ok(())
}
pub fn contains<Q: ?Sized>(&self, k: &Q) -> bool
where
PackageId: Borrow<Q>,
Q: Ord + Eq,
{
self.graph.contains(k)
}
pub fn sort(&self) -> Vec<PackageId> {
self.graph.sort()
}
pub fn iter(&self) -> impl Iterator<Item = PackageId> + '_ {
self.graph.iter().cloned()
}
pub fn deps(&self, pkg: PackageId) -> impl Iterator<Item = (PackageId, &HashSet<Dependency>)> {
self.deps_not_replaced(pkg)
.map(move |(id, deps)| (self.replacement(id).unwrap_or(id), deps))
}
pub fn deps_not_replaced(
&self,
pkg: PackageId,
) -> impl Iterator<Item = (PackageId, &HashSet<Dependency>)> {
self.graph.edges(&pkg).map(|(id, deps)| (*id, deps))
}
pub fn replacement(&self, pkg: PackageId) -> Option<PackageId> {
self.replacements.get(&pkg).cloned()
}
pub fn replacements(&self) -> &HashMap<PackageId, PackageId> {
&self.replacements
}
pub fn features(&self, pkg: PackageId) -> &[InternedString] {
self.features.get(&pkg).map(|v| &**v).unwrap_or(&[])
}
/// This is only here for legacy support, it will be removed when
/// switching to the new feature resolver.
pub fn features_clone(&self) -> HashMap<PackageId, Vec<InternedString>> {
self.features.clone()
}
pub fn is_public_dep(&self, pkg: PackageId, dep: PackageId) -> bool {
self.public_dependencies
.get(&pkg)
.map(|public_deps| public_deps.contains(&dep))
.unwrap_or_else(|| panic!("Unknown dependency {:?} for package {:?}", dep, pkg))
}
pub fn query(&self, spec: &str) -> CargoResult<PackageId> {
PackageIdSpec::query_str(spec, self.iter())
}
pub fn specs_to_ids(&self, specs: &[PackageIdSpec]) -> CargoResult<Vec<PackageId>> {
specs.iter().map(|s| s.query(self.iter())).collect()
}
pub fn unused_patches(&self) -> &[PackageId] {
&self.unused_patches
}
pub fn checksums(&self) -> &HashMap<PackageId, Option<String>> {
&self.checksums
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
pub fn extern_crate_name_and_dep_name(
&self,
from: PackageId,
to: PackageId,
to_target: &Target,
) -> CargoResult<(InternedString, Option<InternedString>)> {
let empty_set: HashSet<Dependency> = HashSet::new();
let deps = if from == to {
&empty_set
} else {
self.dependencies_listed(from, to)
};
let target_crate_name = || (to_target.crate_name(), None);
let mut name_pairs = deps.iter().map(|d| {
d.explicit_name_in_toml()
.map(|s| (s.as_str().replace("-", "_"), Some(s)))
.unwrap_or_else(target_crate_name)
});
let (extern_crate_name, dep_name) = name_pairs.next().unwrap_or_else(target_crate_name);
for (n, _) in name_pairs {
anyhow::ensure!(
n == extern_crate_name,
"the crate `{}` depends on crate `{}` multiple times with different names",
from,
to,
);
}
Ok((extern_crate_name.into(), dep_name))
}
fn dependencies_listed(&self, from: PackageId, to: PackageId) -> &HashSet<Dependency> {
// We've got a dependency on `from` to `to`, but this dependency edge
// may be affected by [replace]. If the `to` package is listed as the
// target of a replacement (aka the key of a reverse replacement map)
// then we try to find our dependency edge through that. If that fails
// then we go down below assuming it's not replaced.
//
// Note that we don't treat `from` as if it's been replaced because
// that's where the dependency originates from, and we only replace
// targets of dependencies not the originator.
if let Some(replace) = self.reverse_replacements.get(&to) {
if let Some(deps) = self.graph.edge(&from, replace) {
return deps;
}
}
match self.graph.edge(&from, &to) {
Some(ret) => ret,
None => panic!("no Dependency listed for `{}` => `{}`", from, to),
}
}
/// Returns the version of the encoding that's being used for this lock
/// file.
pub fn version(&self) -> ResolveVersion {
self.version
}
pub fn set_version(&mut self, version: ResolveVersion) {
self.version = version;
}
pub fn summary(&self, pkg_id: PackageId) -> &Summary {
&self.summaries[&pkg_id]
}
}
impl PartialEq for Resolve {
fn eq(&self, other: &Resolve) -> bool {
macro_rules! compare {
($($fields:ident)* | $($ignored:ident)*) => {
let Resolve { $($fields,)* $($ignored: _,)* } = self;
$($fields == &other.$fields)&&*
}
}
compare! {
// fields to compare
graph replacements reverse_replacements features
checksums metadata unused_patches public_dependencies summaries
|
// fields to ignore
version
}
}
}
impl fmt::Debug for Resolve {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(fmt, "graph: {:?}", self.graph)?;
writeln!(fmt, "\nfeatures: {{")?;
for (pkg, features) in &self.features {
writeln!(fmt, " {}: {:?}", pkg, features)?;
}
write!(fmt, "}}")
}
}