Skip to main content

cargo/core/resolver/
resolve.rs

1use super::encode::Metadata;
2use crate::core::dependency::DepKind;
3use crate::core::interning::InternedString;
4use crate::core::{Dependency, PackageId, PackageIdSpec, Summary, Target};
5use crate::util::errors::CargoResult;
6use crate::util::Graph;
7use std::borrow::Borrow;
8use std::cmp;
9use std::collections::{HashMap, HashSet};
10use std::fmt;
11
12/// Represents a fully-resolved package dependency graph. Each node in the graph
13/// is a package and edges represent dependencies between packages.
14///
15/// Each instance of `Resolve` also understands the full set of features used
16/// for each package.
17pub struct Resolve {
18    /// A graph, whose vertices are packages and edges are dependency specifications
19    /// from `Cargo.toml`. We need a `Vec` here because the same package
20    /// might be present in both `[dependencies]` and `[build-dependencies]`.
21    graph: Graph<PackageId, HashSet<Dependency>>,
22    /// Replacements from the `[replace]` table.
23    replacements: HashMap<PackageId, PackageId>,
24    /// Inverted version of `replacements`.
25    reverse_replacements: HashMap<PackageId, PackageId>,
26    /// An empty `HashSet` to avoid creating a new `HashSet` for every package
27    /// that does not have any features, and to avoid using `Option` to
28    /// simplify the API.
29    empty_features: Vec<InternedString>,
30    /// Features enabled for a given package.
31    features: HashMap<PackageId, Vec<InternedString>>,
32    /// Checksum for each package. A SHA256 hash of the `.crate` file used to
33    /// validate the correct crate file is used. This is `None` for sources
34    /// that do not use `.crate` files, like path or git dependencies.
35    checksums: HashMap<PackageId, Option<String>>,
36    /// "Unknown" metadata. This is a collection of extra, unrecognized data
37    /// found in the `[metadata]` section of `Cargo.lock`, preserved for
38    /// forwards compatibility.
39    metadata: Metadata,
40    /// `[patch]` entries that did not match anything, preserved in
41    /// `Cargo.lock` as the `[[patch.unused]]` table array. Tracking unused
42    /// patches helps prevent Cargo from being forced to re-update the
43    /// registry every time it runs, and keeps the resolve in a locked state
44    /// so it doesn't re-resolve the unused entries.
45    unused_patches: Vec<PackageId>,
46    /// A map from packages to a set of their public dependencies
47    public_dependencies: HashMap<PackageId, HashSet<PackageId>>,
48    /// Version of the `Cargo.lock` format, see
49    /// `cargo::core::resolver::encode` for more.
50    version: ResolveVersion,
51    summaries: HashMap<PackageId, Summary>,
52}
53
54/// A version to indicate how a `Cargo.lock` should be serialized. Currently
55/// V2 is the default when creating a new lockfile. If a V1 lockfile already
56/// exists, it will stay as V1.
57///
58/// It's theorized that we can add more here over time to track larger changes
59/// to the `Cargo.lock` format, but we've yet to see how that strategy pans out.
60#[derive(PartialEq, Eq, Clone, Copy, Debug, PartialOrd, Ord)]
61pub enum ResolveVersion {
62    /// Historical baseline for when this abstraction was added.
63    V1,
64    /// A more compact format, more amenable to avoiding source-control merge
65    /// conflicts. The `dependencies` arrays are compressed and checksums are
66    /// listed inline. Introduced in 2019 in version 1.38. New lockfiles use
67    /// V2 by default starting in 1.41.
68    V2,
69}
70
71impl Resolve {
72    pub fn new(
73        graph: Graph<PackageId, HashSet<Dependency>>,
74        replacements: HashMap<PackageId, PackageId>,
75        features: HashMap<PackageId, Vec<InternedString>>,
76        checksums: HashMap<PackageId, Option<String>>,
77        metadata: Metadata,
78        unused_patches: Vec<PackageId>,
79        version: ResolveVersion,
80        summaries: HashMap<PackageId, Summary>,
81    ) -> Resolve {
82        let reverse_replacements = replacements.iter().map(|(&p, &r)| (r, p)).collect();
83        let public_dependencies = graph
84            .iter()
85            .map(|p| {
86                let public_deps = graph
87                    .edges(p)
88                    .filter(|(_, deps)| {
89                        deps.iter()
90                            .any(|d| d.kind() == DepKind::Normal && d.is_public())
91                    })
92                    .map(|(dep_package, _)| *dep_package)
93                    .collect::<HashSet<PackageId>>();
94
95                (*p, public_deps)
96            })
97            .collect();
98
99        Resolve {
100            graph,
101            replacements,
102            features,
103            checksums,
104            metadata,
105            unused_patches,
106            empty_features: Vec::new(),
107            reverse_replacements,
108            public_dependencies,
109            version,
110            summaries,
111        }
112    }
113
114    /// Resolves one of the paths from the given dependent package up to
115    /// the root.
116    pub fn path_to_top<'a>(&'a self, pkg: &'a PackageId) -> Vec<&'a PackageId> {
117        self.graph.path_to_top(pkg)
118    }
119
120    pub fn register_used_patches(&mut self, patches: &[Summary]) {
121        for summary in patches {
122            if self.iter().any(|id| id == summary.package_id()) {
123                continue;
124            }
125            self.unused_patches.push(summary.package_id());
126        }
127    }
128
129    pub fn merge_from(&mut self, previous: &Resolve) -> CargoResult<()> {
130        // Given a previous instance of resolve, it should be forbidden to ever
131        // have a checksums which *differ*. If the same package ID has differing
132        // checksums, then something has gone wrong such as:
133        //
134        // * Something got seriously corrupted
135        // * A "mirror" isn't actually a mirror as some changes were made
136        // * A replacement source wasn't actually a replacement, some changes
137        //   were made
138        //
139        // In all of these cases, we want to report an error to indicate that
140        // something is awry. Normal execution (esp just using crates.io) should
141        // never run into this.
142        for (id, cksum) in previous.checksums.iter() {
143            if let Some(mine) = self.checksums.get(id) {
144                if mine == cksum {
145                    continue;
146                }
147
148                // If the previous checksum wasn't calculated, the current
149                // checksum is `Some`. This may indicate that a source was
150                // erroneously replaced or was replaced with something that
151                // desires stronger checksum guarantees than can be afforded
152                // elsewhere.
153                if cksum.is_none() {
154                    anyhow::bail!(
155                        "\
156checksum for `{}` was not previously calculated, but a checksum could now \
157be calculated
158
159this could be indicative of a few possible situations:
160
161    * the source `{}` did not previously support checksums,
162      but was replaced with one that does
163    * newer Cargo implementations know how to checksum this source, but this
164      older implementation does not
165    * the lock file is corrupt
166",
167                        id,
168                        id.source_id()
169                    )
170
171                // If our checksum hasn't been calculated, then it could mean
172                // that future Cargo figured out how to checksum something or
173                // more realistically we were overridden with a source that does
174                // not have checksums.
175                } else if mine.is_none() {
176                    anyhow::bail!(
177                        "\
178checksum for `{}` could not be calculated, but a checksum is listed in \
179the existing lock file
180
181this could be indicative of a few possible situations:
182
183    * the source `{}` supports checksums,
184      but was replaced with one that doesn't
185    * the lock file is corrupt
186
187unable to verify that `{0}` is the same as when the lockfile was generated
188",
189                        id,
190                        id.source_id()
191                    )
192
193                // If the checksums aren't equal, and neither is None, then they
194                // must both be Some, in which case the checksum now differs.
195                // That's quite bad!
196                } else {
197                    anyhow::bail!(
198                        "\
199checksum for `{}` changed between lock files
200
201this could be indicative of a few possible errors:
202
203    * the lock file is corrupt
204    * a replacement source in use (e.g., a mirror) returned a different checksum
205    * the source itself may be corrupt in one way or another
206
207unable to verify that `{0}` is the same as when the lockfile was generated
208",
209                        id
210                    );
211                }
212            }
213        }
214
215        // Be sure to just copy over any unknown metadata.
216        self.metadata = previous.metadata.clone();
217
218        // The goal of Cargo is largely to preserve the encoding of `Cargo.lock`
219        // that it finds on the filesystem. Sometimes `Cargo.lock` changes are
220        // in the works where they haven't been set as the default yet but will
221        // become the default soon.
222        //
223        // The scenarios we could be in are:
224        //
225        // * This is a brand new lock file with nothing previous. In that case
226        //   this method isn't actually called at all, but instead
227        //   `default_for_new_lockfiles` called below was encoded during the
228        //   resolution step, so that's what we're gonna use.
229        //
230        // * We have an old lock file. In this case we want to switch the
231        //   version to `default_for_old_lockfiles`. That puts us in one of
232        //   three cases:
233        //
234        //   * Our version is older than the default. This means that we're
235        //     migrating someone forward, so we switch the encoding.
236        //   * Our version is equal to the default, nothing to do!
237        //   * Our version is *newer* than the default. This is where we
238        //     critically keep the new version of encoding.
239        //
240        // This strategy should get new lockfiles into the pipeline more quickly
241        // while ensuring that any time an old cargo sees a future lock file it
242        // keeps the future lockfile encoding.
243        self.version = cmp::max(
244            previous.version,
245            ResolveVersion::default_for_old_lockfiles(),
246        );
247
248        Ok(())
249    }
250
251    pub fn contains<Q: ?Sized>(&self, k: &Q) -> bool
252    where
253        PackageId: Borrow<Q>,
254        Q: Ord + Eq,
255    {
256        self.graph.contains(k)
257    }
258
259    pub fn sort(&self) -> Vec<PackageId> {
260        self.graph.sort()
261    }
262
263    pub fn iter<'a>(&'a self) -> impl Iterator<Item = PackageId> + 'a {
264        self.graph.iter().cloned()
265    }
266
267    pub fn deps(&self, pkg: PackageId) -> impl Iterator<Item = (PackageId, &HashSet<Dependency>)> {
268        self.deps_not_replaced(pkg)
269            .map(move |(id, deps)| (self.replacement(id).unwrap_or(id), deps))
270    }
271
272    pub fn deps_not_replaced(
273        &self,
274        pkg: PackageId,
275    ) -> impl Iterator<Item = (PackageId, &HashSet<Dependency>)> {
276        self.graph.edges(&pkg).map(|(id, deps)| (*id, deps))
277    }
278
279    pub fn replacement(&self, pkg: PackageId) -> Option<PackageId> {
280        self.replacements.get(&pkg).cloned()
281    }
282
283    pub fn replacements(&self) -> &HashMap<PackageId, PackageId> {
284        &self.replacements
285    }
286
287    pub fn features(&self, pkg: PackageId) -> &[InternedString] {
288        self.features.get(&pkg).unwrap_or(&self.empty_features)
289    }
290
291    /// This is only here for legacy support, it will be removed when
292    /// switching to the new feature resolver.
293    pub fn features_clone(&self) -> HashMap<PackageId, Vec<InternedString>> {
294        self.features.clone()
295    }
296
297    pub fn is_public_dep(&self, pkg: PackageId, dep: PackageId) -> bool {
298        self.public_dependencies
299            .get(&pkg)
300            .map(|public_deps| public_deps.contains(&dep))
301            .unwrap_or_else(|| panic!("Unknown dependency {:?} for package {:?}", dep, pkg))
302    }
303
304    pub fn query(&self, spec: &str) -> CargoResult<PackageId> {
305        PackageIdSpec::query_str(spec, self.iter())
306    }
307
308    pub fn unused_patches(&self) -> &[PackageId] {
309        &self.unused_patches
310    }
311
312    pub fn checksums(&self) -> &HashMap<PackageId, Option<String>> {
313        &self.checksums
314    }
315
316    pub fn metadata(&self) -> &Metadata {
317        &self.metadata
318    }
319
320    pub fn extern_crate_name(
321        &self,
322        from: PackageId,
323        to: PackageId,
324        to_target: &Target,
325    ) -> CargoResult<String> {
326        let empty_set: HashSet<Dependency> = HashSet::new();
327        let deps = if from == to {
328            &empty_set
329        } else {
330            self.dependencies_listed(from, to)
331        };
332
333        let crate_name = to_target.crate_name();
334        let mut names = deps.iter().map(|d| {
335            d.explicit_name_in_toml()
336                .map(|s| s.as_str().replace("-", "_"))
337                .unwrap_or_else(|| crate_name.clone())
338        });
339        let name = names.next().unwrap_or_else(|| crate_name.clone());
340        for n in names {
341            anyhow::ensure!(
342                n == name,
343                "the crate `{}` depends on crate `{}` multiple times with different names",
344                from,
345                to,
346            );
347        }
348        Ok(name)
349    }
350
351    fn dependencies_listed(&self, from: PackageId, to: PackageId) -> &HashSet<Dependency> {
352        // We've got a dependency on `from` to `to`, but this dependency edge
353        // may be affected by [replace]. If the `to` package is listed as the
354        // target of a replacement (aka the key of a reverse replacement map)
355        // then we try to find our dependency edge through that. If that fails
356        // then we go down below assuming it's not replaced.
357        //
358        // Note that we don't treat `from` as if it's been replaced because
359        // that's where the dependency originates from, and we only replace
360        // targets of dependencies not the originator.
361        if let Some(replace) = self.reverse_replacements.get(&to) {
362            if let Some(deps) = self.graph.edge(&from, replace) {
363                return deps;
364            }
365        }
366        match self.graph.edge(&from, &to) {
367            Some(ret) => ret,
368            None => panic!("no Dependency listed for `{}` => `{}`", from, to),
369        }
370    }
371
372    /// Returns the version of the encoding that's being used for this lock
373    /// file.
374    pub fn version(&self) -> &ResolveVersion {
375        &self.version
376    }
377
378    pub fn summary(&self, pkg_id: PackageId) -> &Summary {
379        &self.summaries[&pkg_id]
380    }
381}
382
383impl PartialEq for Resolve {
384    fn eq(&self, other: &Resolve) -> bool {
385        macro_rules! compare {
386            ($($fields:ident)* | $($ignored:ident)*) => {
387                let Resolve { $($fields,)* $($ignored: _,)* } = self;
388                $($fields == &other.$fields)&&*
389            }
390        }
391        compare! {
392            // fields to compare
393            graph replacements reverse_replacements empty_features features
394            checksums metadata unused_patches public_dependencies summaries
395            |
396            // fields to ignore
397            version
398        }
399    }
400}
401
402impl fmt::Debug for Resolve {
403    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
404        writeln!(fmt, "graph: {:?}", self.graph)?;
405        writeln!(fmt, "\nfeatures: {{")?;
406        for (pkg, features) in &self.features {
407            writeln!(fmt, "  {}: {:?}", pkg, features)?;
408        }
409        write!(fmt, "}}")
410    }
411}
412
413impl ResolveVersion {
414    /// The default way to encode new `Cargo.lock` files.
415    ///
416    /// It's important that if a new version of `ResolveVersion` is added that
417    /// this is not updated until *at least* the support for the version is in
418    /// the stable release of Rust. It's ok for this to be newer than
419    /// `default_for_old_lockfiles` below.
420    pub fn default_for_new_lockfiles() -> ResolveVersion {
421        ResolveVersion::V2
422    }
423
424    /// The default way to encode old preexisting `Cargo.lock` files. This is
425    /// often trailing the new lockfiles one above to give older projects a
426    /// longer time to catch up.
427    ///
428    /// It's important that this trails behind `default_for_new_lockfiles` for
429    /// quite some time. This gives projects a quite large window to update in
430    /// where we don't force updates, so if projects span many versions of Cargo
431    /// all those versions of Cargo will have support for a new version of the
432    /// lock file.
433    pub fn default_for_old_lockfiles() -> ResolveVersion {
434        ResolveVersion::V1
435    }
436}