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}