blob: 91df17b2e1818ef7e22e9a78d8da5cdae5146279 [file] [log] [blame]
use super::encode::Metadata;
use crate::core::dependency::DepKind;
use crate::core::interning::InternedString;
use crate::core::{Dependency, PackageId, PackageIdSpec, Summary, Target};
use crate::util::errors::CargoResult;
use crate::util::Graph;
use std::borrow::Borrow;
use std::cmp;
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 `Vec` here because the same package
/// might be present in both `[dependencies]` and `[build-dependencies]`.
graph: Graph<PackageId, Vec<Dependency>>,
/// Replacements from the `[replace]` table.
replacements: HashMap<PackageId, PackageId>,
/// Inverted version of `replacements`.
reverse_replacements: HashMap<PackageId, PackageId>,
/// An empty `HashSet` to avoid creating a new `HashSet` for every package
/// that does not have any features, and to avoid using `Option` to
/// simplify the API.
empty_features: Vec<InternedString>,
/// 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. Currently
/// V2 is the default when creating a new lockfile. If a V1 lockfile already
/// exists, it will stay as V1.
///
/// 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(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 starting in 1.41.
V2,
}
impl Resolve {
pub fn new(
graph: Graph<PackageId, Vec<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,
empty_features: Vec::new(),
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> {
self.graph.path_to_top(pkg)
}
pub fn register_used_patches(&mut self, patches: &[Summary]) {
for summary in patches {
if self.iter().any(|id| id == summary.package_id()) {
continue;
}
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();
// The goal of Cargo is largely to preserve the encoding of `Cargo.lock`
// that it finds on the filesystem. Sometimes `Cargo.lock` changes are
// in the works where they haven't been set as the default yet but will
// become the default soon.
//
// The scenarios we could be in are:
//
// * This is a brand new lock file with nothing previous. In that case
// this method isn't actually called at all, but instead
// `default_for_new_lockfiles` called below was encoded during the
// resolution step, so that's what we're gonna use.
//
// * We have an old lock file. In this case we want to switch the
// version to `default_for_old_lockfiles`. That puts us in one of
// three cases:
//
// * Our version is older than the default. This means that we're
// migrating someone forward, so we switch the encoding.
// * Our version is equal to the default, nothing to do!
// * Our version is *newer* than the default. This is where we
// critically keep the new version of encoding.
//
// This strategy should get new lockfiles into the pipeline more quickly
// while ensuring that any time an old cargo sees a future lock file it
// keeps the future lockfile encoding.
self.version = cmp::max(
previous.version,
ResolveVersion::default_for_old_lockfiles(),
);
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<'a>(&'a self) -> impl Iterator<Item = PackageId> + 'a {
self.graph.iter().cloned()
}
pub fn deps(&self, pkg: PackageId) -> impl Iterator<Item = (PackageId, &[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, &[Dependency])> {
self.graph
.edges(&pkg)
.map(|(id, deps)| (*id, deps.as_slice()))
}
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).unwrap_or(&self.empty_features)
}
/// 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 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(
&self,
from: PackageId,
to: PackageId,
to_target: &Target,
) -> CargoResult<String> {
let deps = if from == to {
&[]
} else {
self.dependencies_listed(from, to)
};
let crate_name = to_target.crate_name();
let mut names = deps.iter().map(|d| {
d.explicit_name_in_toml()
.map(|s| s.as_str().replace("-", "_"))
.unwrap_or_else(|| crate_name.clone())
});
let name = names.next().unwrap_or_else(|| crate_name.clone());
for n in names {
anyhow::ensure!(
n == name,
"the crate `{}` depends on crate `{}` multiple times with different names",
from,
to,
);
}
Ok(name)
}
fn dependencies_listed(&self, from: PackageId, to: PackageId) -> &[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 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 empty_features 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, "}}")
}
}
impl ResolveVersion {
/// The default way to encode new `Cargo.lock` files.
///
/// It's important that if a new version of `ResolveVersion` is added that
/// this is not updated until *at least* the support for the version is in
/// the stable release of Rust. It's ok for this to be newer than
/// `default_for_old_lockfiles` below.
pub fn default_for_new_lockfiles() -> ResolveVersion {
ResolveVersion::V2
}
/// The default way to encode old preexisting `Cargo.lock` files. This is
/// often trailing the new lockfiles one above to give older projects a
/// longer time to catch up.
///
/// It's important that this trails behind `default_for_new_lockfiles` for
/// quite some time. This gives projects a quite large window to update in
/// where we don't force updates, so if projects span many versions of Cargo
/// all those versions of Cargo will have support for a new version of the
/// lock file.
pub fn default_for_old_lockfiles() -> ResolveVersion {
ResolveVersion::V1
}
}