guppy/graph/cargo/cargo_api.rs
1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5 Error, PackageId,
6 graph::{
7 DependencyDirection, PackageGraph, PackageIx, PackageLink, PackageResolver, PackageSet,
8 cargo::build::CargoSetBuildState,
9 feature::{FeatureGraph, FeatureSet},
10 },
11 platform::PlatformSpec,
12 sorted_set::SortedSet,
13};
14use petgraph::prelude::*;
15use serde::{Deserialize, Serialize};
16use std::{collections::HashSet, fmt};
17
18/// Options for queries which simulate what Cargo does.
19///
20/// This provides control over the resolution algorithm used by `guppy`'s simulation of Cargo.
21#[derive(Clone, Debug)]
22pub struct CargoOptions<'a> {
23 pub(crate) resolver: CargoResolverVersion,
24 pub(crate) include_dev: bool,
25 pub(crate) initials_platform: InitialsPlatform,
26 // Use Supercow here to ensure that owned Platform instances are boxed, to reduce stack size.
27 pub(crate) host_platform: PlatformSpec,
28 pub(crate) target_platform: PlatformSpec,
29 pub(crate) omitted_packages: HashSet<&'a PackageId>,
30}
31
32impl<'a> CargoOptions<'a> {
33 /// Creates a new `CargoOptions` with this resolver version and default settings.
34 ///
35 /// The default settings are similar to what a plain `cargo build` does:
36 ///
37 /// * use version 1 of the Cargo resolver
38 /// * exclude dev-dependencies
39 /// * do not build proc macros specified in the query on the target platform
40 /// * resolve dependencies assuming any possible host or target platform
41 /// * do not omit any packages.
42 pub fn new() -> Self {
43 Self {
44 resolver: CargoResolverVersion::V1,
45 include_dev: false,
46 initials_platform: InitialsPlatform::Standard,
47 host_platform: PlatformSpec::Any,
48 target_platform: PlatformSpec::Any,
49 omitted_packages: HashSet::new(),
50 }
51 }
52
53 /// Sets the Cargo feature resolver version.
54 ///
55 /// For more about feature resolution, see the documentation for `CargoResolverVersion`.
56 pub fn set_resolver(&mut self, resolver: CargoResolverVersion) -> &mut Self {
57 self.resolver = resolver;
58 self
59 }
60
61 /// If set to true, causes dev-dependencies of the initial set to be followed.
62 ///
63 /// This does not affect transitive dependencies -- for example, a build or dev-dependency's
64 /// further dev-dependencies are never followed.
65 ///
66 /// The default is false, which matches what a plain `cargo build` does.
67 pub fn set_include_dev(&mut self, include_dev: bool) -> &mut Self {
68 self.include_dev = include_dev;
69 self
70 }
71
72 /// Configures the way initials are treated on the target and the host.
73 ///
74 /// The default is a "standard" build and this does not usually need to be set, but some
75 /// advanced use cases may require it. For more about this option, see the documentation for
76 /// [`InitialsPlatform`](InitialsPlatform).
77 pub fn set_initials_platform(&mut self, initials_platform: InitialsPlatform) -> &mut Self {
78 self.initials_platform = initials_platform;
79 self
80 }
81
82 /// Sets both the target and host platforms to the provided spec.
83 pub fn set_platform(&mut self, platform_spec: impl Into<PlatformSpec>) -> &mut Self {
84 let platform_spec = platform_spec.into();
85 self.target_platform = platform_spec.clone();
86 self.host_platform = platform_spec;
87 self
88 }
89
90 /// Sets the target platform to the provided spec.
91 pub fn set_target_platform(&mut self, target_platform: impl Into<PlatformSpec>) -> &mut Self {
92 self.target_platform = target_platform.into();
93 self
94 }
95
96 /// Sets the host platform to the provided spec.
97 pub fn set_host_platform(&mut self, host_platform: impl Into<PlatformSpec>) -> &mut Self {
98 self.host_platform = host_platform.into();
99 self
100 }
101
102 /// Omits edges into the given packages.
103 ///
104 /// This may be useful in order to figure out what additional dependencies or features a
105 /// particular set of packages pulls in.
106 ///
107 /// This method is additive.
108 pub fn add_omitted_packages(
109 &mut self,
110 package_ids: impl IntoIterator<Item = &'a PackageId>,
111 ) -> &mut Self {
112 self.omitted_packages.extend(package_ids);
113 self
114 }
115}
116
117impl Default for CargoOptions<'_> {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123/// The version of Cargo's feature resolver to use.
124#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
125#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
126#[serde(rename_all = "kebab-case")]
127#[non_exhaustive]
128pub enum CargoResolverVersion {
129 /// The "classic" feature resolver in Rust.
130 ///
131 /// This feature resolver unifies features across inactive platforms, and also unifies features
132 /// across normal, build and dev dependencies for initials. This may produce results that are
133 /// surprising at times.
134 #[serde(rename = "1", alias = "v1")]
135 V1,
136
137 /// The "classic" feature resolver in Rust, as used by commands like `cargo install`.
138 ///
139 /// This resolver is the same as `V1`, except it doesn't unify features across dev dependencies
140 /// for initials. However, if `CargoOptions::with_dev_deps` is set to true, it behaves
141 /// identically to the V1 resolver.
142 ///
143 /// For more, see
144 /// [avoid-dev-deps](https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#avoid-dev-deps)
145 /// in the Cargo reference.
146 #[serde(rename = "install", alias = "v1-install")]
147 V1Install,
148
149 /// [Version 2 of the feature resolver](https://doc.rust-lang.org/cargo/reference/resolver.html#feature-resolver-version-2),
150 /// available since Rust 1.51. This feature resolver does not unify features:
151 ///
152 /// * across host (build) and target (regular) dependencies
153 /// * with dev-dependencies for initials, if tests aren't currently being built
154 /// * with [platform-specific dependencies](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies) that are currently inactive
155 ///
156 /// Version 2 of the feature resolver can be enabled by specifying `resolver
157 /// = "2"` in the workspace's `Cargo.toml`. It is also [the default resolver
158 /// version](https://doc.rust-lang.org/beta/edition-guide/rust-2021/default-cargo-resolver.html)
159 /// for [the Rust 2021
160 /// edition](https://doc.rust-lang.org/edition-guide/rust-2021/index.html).
161 #[serde(rename = "2", alias = "v2")]
162 V2,
163
164 /// [Version 3 of the dependency
165 /// resolver](https://doc.rust-lang.org/beta/cargo/reference/resolver.html#resolver-versions),
166 /// available since Rust 1.84.
167 ///
168 /// Version 3 of the resolver enables [MSRV-aware dependency
169 /// resolution](https://doc.rust-lang.org/beta/cargo/reference/config.html#resolverincompatible-rust-versions).
170 /// There are no changes to feature resolution compared to version 2.
171 ///
172 /// Version 3 of the feature resolver can be enabled by specifying `resolver
173 /// = "3"` in the workspace's `Cargo.toml`. It is also [the default resolver
174 /// version](https://doc.rust-lang.org/beta/edition-guide/rust-2024/cargo-resolver.html)
175 /// for [the Rust 2024
176 /// edition](https://doc.rust-lang.org/beta/edition-guide/rust-2024/index.html).
177 #[serde(rename = "3", alias = "v3")]
178 V3,
179}
180
181/// For a given Cargo build simulation, what platform to assume the initials are being built on.
182#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
183#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
184#[serde(rename_all = "kebab-case")]
185pub enum InitialsPlatform {
186 /// Assume that the initials are being built on the host platform.
187 ///
188 /// This is most useful for "continuing" simulations, where it is already known that some
189 /// packages are being built on the host and one wishes to find their dependencies.
190 Host,
191
192 /// Assume a standard build.
193 ///
194 /// In this mode, all initials other than proc-macros are built on the target platform. Proc-
195 /// macros, being compiler plugins, are built on the host.
196 ///
197 /// This is the default for `InitialsPlatform`.
198 Standard,
199
200 /// Perform a standard build, and also build proc-macros on the target.
201 ///
202 /// Proc-macro crates may include tests, which are run on the target platform. This option is
203 /// most useful for such situations.
204 ProcMacrosOnTarget,
205}
206
207/// The default for `InitialsPlatform`: the `Standard` option.
208impl Default for InitialsPlatform {
209 fn default() -> Self {
210 InitialsPlatform::Standard
211 }
212}
213
214/// A set of packages and features, as would be built by Cargo.
215///
216/// Cargo implements a set of algorithms to figure out which packages or features are built in
217/// a given situation. `guppy` implements those algorithms.
218#[derive(Clone, Debug)]
219pub struct CargoSet<'g> {
220 pub(super) initials: FeatureSet<'g>,
221 pub(super) features_only: FeatureSet<'g>,
222 pub(super) target_features: FeatureSet<'g>,
223 pub(super) host_features: FeatureSet<'g>,
224 pub(super) target_direct_deps: PackageSet<'g>,
225 pub(super) host_direct_deps: PackageSet<'g>,
226 pub(super) proc_macro_edge_ixs: SortedSet<EdgeIndex<PackageIx>>,
227 pub(super) build_dep_edge_ixs: SortedSet<EdgeIndex<PackageIx>>,
228 pub(super) target_edge_ixs: SortedSet<EdgeIndex<PackageIx>>,
229 pub(super) host_edge_ixs: SortedSet<EdgeIndex<PackageIx>>,
230}
231
232assert_covariant!(CargoSet);
233
234impl<'g> CargoSet<'g> {
235 /// Simulates a Cargo build of this feature set, with the given options.
236 ///
237 /// The feature sets are expected to be entirely within the workspace. Its behavior outside the
238 /// workspace isn't defined and may be surprising.
239 ///
240 /// `CargoSet::new` takes two `FeatureSet` instances:
241 /// * `initials`, from which dependencies are followed to build the `CargoSet`.
242 /// * `features_only`, which are additional inputs that are only used for feature
243 /// unification. This may be used to simulate, e.g. `cargo build --package foo --package bar`,
244 /// when you only care about the results of `foo` but specifying `bar` influences the build.
245 ///
246 /// Note that even if a package is in `features_only`, it may be included in the final build set
247 /// through other means (for example, if it is also in `initials` or it is a dependency of one
248 /// of them).
249 ///
250 /// In many cases `features_only` is empty -- in that case you may wish to use
251 /// `FeatureSet::into_cargo_set()`, and it may be more convenient to use that if the code is
252 /// written in a "fluent" style.
253 ///
254 ///
255 pub fn new(
256 initials: FeatureSet<'g>,
257 features_only: FeatureSet<'g>,
258 opts: &CargoOptions<'_>,
259 ) -> Result<Self, Error> {
260 Self::new_internal(initials, features_only, None, opts)
261 }
262
263 /// Like `Cargo.new`, but takes an additional [`PackageResolver`] which can
264 /// be used to filter out some dependency edges, or to collect additional
265 /// information.
266 ///
267 /// [`resolver.accept`] is called for both target and host dependencies. It
268 /// is called after static filtering through
269 /// [`CargoOptions::add_omitted_packages`], but before any other decisions
270 /// are made.
271 ///
272 /// [`resolver.accept`]: PackageResolver::accept
273 pub fn with_package_resolver(
274 initials: FeatureSet<'g>,
275 features_only: FeatureSet<'g>,
276 mut resolver: impl PackageResolver<'g>,
277 opts: &CargoOptions<'_>,
278 ) -> Result<Self, Error> {
279 Self::new_internal(initials, features_only, Some(&mut resolver), opts)
280 }
281
282 /// Internal helper to deduplicate code across `CargoSet::new` and `CargoSet::with_resolver`.
283 fn new_internal(
284 initials: FeatureSet<'g>,
285 features_only: FeatureSet<'g>,
286 resolver: Option<&mut dyn PackageResolver<'g>>,
287 opts: &CargoOptions<'_>,
288 ) -> Result<Self, Error> {
289 let build_state = CargoSetBuildState::new(initials.graph().package_graph, opts)?;
290 Ok(build_state.build(initials, features_only, resolver))
291 }
292
293 /// Creates a new `CargoIntermediateSet` based on the given query and options.
294 ///
295 /// This set contains an over-estimate of targets and features.
296 ///
297 /// Not part of the stable API, exposed for testing.
298 #[doc(hidden)]
299 pub fn new_intermediate(
300 initials: &FeatureSet<'g>,
301 opts: &CargoOptions<'_>,
302 ) -> Result<CargoIntermediateSet<'g>, Error> {
303 let build_state = CargoSetBuildState::new(initials.graph().package_graph, opts)?;
304 Ok(build_state.build_intermediate(initials.to_feature_query(DependencyDirection::Forward)))
305 }
306
307 /// Returns the feature graph for this `CargoSet` instance.
308 pub fn feature_graph(&self) -> &FeatureGraph<'g> {
309 self.initials.graph()
310 }
311
312 /// Returns the package graph for this `CargoSet` instance.
313 pub fn package_graph(&self) -> &'g PackageGraph {
314 self.feature_graph().package_graph
315 }
316
317 /// Returns the initial packages and features from which the `CargoSet` instance was
318 /// constructed.
319 pub fn initials(&self) -> &FeatureSet<'g> {
320 &self.initials
321 }
322
323 /// Returns the packages and features that took part in feature unification but were not
324 /// considered part of the final result.
325 ///
326 /// For more about `features_only` and how it influences the build, see the documentation for
327 /// [`CargoSet::new`](CargoSet::new).
328 pub fn features_only(&self) -> &FeatureSet<'g> {
329 &self.features_only
330 }
331
332 /// Returns the feature set enabled on the target platform.
333 ///
334 /// This represents the packages and features that are included as code in the final build
335 /// artifacts. This is relevant for both cross-compilation and auditing.
336 pub fn target_features(&self) -> &FeatureSet<'g> {
337 &self.target_features
338 }
339
340 /// Returns the feature set enabled on the host platform.
341 ///
342 /// This represents the packages and features that influence the final build artifacts, but
343 /// whose code is generally not directly included.
344 ///
345 /// This includes all procedural macros, including those specified in the initial query.
346 pub fn host_features(&self) -> &FeatureSet<'g> {
347 &self.host_features
348 }
349
350 /// Returns the feature set enabled on the specified build platform.
351 pub fn platform_features(&self, build_platform: BuildPlatform) -> &FeatureSet<'g> {
352 match build_platform {
353 BuildPlatform::Target => self.target_features(),
354 BuildPlatform::Host => self.host_features(),
355 }
356 }
357
358 /// Returns the feature sets across the target and host build platforms.
359 pub fn all_features(&self) -> [(BuildPlatform, &FeatureSet<'g>); 2] {
360 [
361 (BuildPlatform::Target, self.target_features()),
362 (BuildPlatform::Host, self.host_features()),
363 ]
364 }
365
366 /// Returns the set of workspace and direct dependency packages on the target platform.
367 ///
368 /// The packages in this set are a subset of the packages in `target_features`.
369 pub fn target_direct_deps(&self) -> &PackageSet<'g> {
370 &self.target_direct_deps
371 }
372
373 /// Returns the set of workspace and direct dependency packages on the host platform.
374 ///
375 /// The packages in this set are a subset of the packages in `host_features`.
376 pub fn host_direct_deps(&self) -> &PackageSet<'g> {
377 &self.host_direct_deps
378 }
379
380 /// Returns the set of workspace and direct dependency packages on the specified build platform.
381 pub fn platform_direct_deps(&self, build_platform: BuildPlatform) -> &PackageSet<'g> {
382 match build_platform {
383 BuildPlatform::Target => self.target_direct_deps(),
384 BuildPlatform::Host => self.host_direct_deps(),
385 }
386 }
387
388 /// Returns the set of workspace and direct dependency packages across the target and host
389 /// build platforms.
390 pub fn all_direct_deps(&self) -> [(BuildPlatform, &PackageSet<'g>); 2] {
391 [
392 (BuildPlatform::Target, self.target_direct_deps()),
393 (BuildPlatform::Host, self.host_direct_deps()),
394 ]
395 }
396
397 /// Returns `PackageLink` instances for procedural macro dependencies from target packages.
398 ///
399 /// Procedural macros straddle the line between target and host: they're built for the host
400 /// but generate code that is compiled for the target platform.
401 ///
402 /// ## Notes
403 ///
404 /// Procedural macro packages will be included in the *host* feature set.
405 /// See also [`Self::host_features`].
406 ///
407 /// The returned iterator will include proc macros that are depended on normally or in dev
408 /// builds from initials (if `include_dev` is set), but not the ones in the
409 /// `[build-dependencies]` section.
410 pub fn proc_macro_links<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageLink<'g>> + 'a {
411 let package_graph = self.target_features.graph().package_graph;
412 self.proc_macro_edge_ixs
413 .iter()
414 .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix))
415 }
416
417 /// Returns `PackageLink` instances for build dependencies from target packages.
418 ///
419 /// ## Notes
420 ///
421 /// For each link, the `from` is built on the target while the `to` is built on the host.
422 /// It is possible (though rare) that a build dependency is also included as a normal
423 /// dependency, or as a dev dependency in which case it will also be built on the target.
424 ///
425 /// The returned iterators will not include build dependencies of host packages -- those are
426 /// also built on the host.
427 pub fn build_dep_links<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageLink<'g>> + 'a {
428 let package_graph = self.target_features.graph().package_graph;
429 self.build_dep_edge_ixs
430 .iter()
431 .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix))
432 }
433
434 /// Returns `PackageLink` instances for normal dependencies between target packages.
435 ///
436 /// ## Notes
437 ///
438 /// For each link, both the `from` and the `to` package are built on the target.
439 ///
440 /// Target packages will be included in the *target* feature set.
441 /// See also [`Self::target_features`].
442 pub fn target_links<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageLink<'g>> + 'a {
443 let package_graph = self.target_features.graph().package_graph;
444 self.target_edge_ixs
445 .iter()
446 .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix))
447 }
448
449 /// Returns `PackageLink` instances for dependencies between host packages.
450 ///
451 /// ## Notes
452 ///
453 /// For each link, both the `from` and the `to` package are built on the host.
454 /// Typically most links are normal dependencies, but it is possible to have
455 /// build dependencies as well (e.g. dependencies of a build script used
456 /// in a proc-macro package).
457 ///
458 /// Host packages will be included in the *host* feature set.
459 /// See also [`Self::host_features`].
460 pub fn host_links<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageLink<'g>> + 'a {
461 let package_graph = self.target_features.graph().package_graph;
462 self.host_edge_ixs
463 .iter()
464 .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix))
465 }
466}
467
468/// Either the target or the host platform.
469///
470/// When Cargo computes the platforms it is building on, it computes two separate build graphs: one
471/// for the target platform and one for the host. This is most useful in cross-compilation
472/// situations where the target is different from the host, but the separate graphs are computed
473/// whether or not a build cross-compiles.
474///
475/// A `cargo check` can be looked at as a kind of cross-compilation as well--machine code is
476/// generated and run for the host platform but not the target platform. This is why `cargo check`
477/// output usually has some lines that say `Compiling` (for the host platform) and some that say
478/// `Checking` (for the target platform).
479#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
480pub enum BuildPlatform {
481 /// The target platform.
482 ///
483 /// This represents the packages and features that are included as code in the final build
484 /// artifacts.
485 Target,
486
487 /// The host platform.
488 ///
489 /// This represents build scripts, proc macros and other code that is run on the machine doing
490 /// the compiling.
491 Host,
492}
493
494impl BuildPlatform {
495 /// A list of all possible variants of `BuildPlatform`.
496 pub const VALUES: &'static [Self; 2] = &[BuildPlatform::Target, BuildPlatform::Host];
497
498 /// Returns the build platform that's not `self`.
499 pub fn flip(self) -> Self {
500 match self {
501 BuildPlatform::Host => BuildPlatform::Target,
502 BuildPlatform::Target => BuildPlatform::Host,
503 }
504 }
505}
506
507impl fmt::Display for BuildPlatform {
508 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
509 match self {
510 BuildPlatform::Target => write!(f, "target"),
511 BuildPlatform::Host => write!(f, "host"),
512 }
513 }
514}
515
516/// An intermediate set representing an overestimate of what packages are built, but an accurate
517/// summary of what features are built given a particular package.
518///
519/// Not part of the stable API, exposed for cargo-compare.
520#[doc(hidden)]
521#[derive(Debug)]
522pub enum CargoIntermediateSet<'g> {
523 Unified(FeatureSet<'g>),
524 TargetHost {
525 target: FeatureSet<'g>,
526 host: FeatureSet<'g>,
527 },
528}
529
530impl<'g> CargoIntermediateSet<'g> {
531 #[doc(hidden)]
532 pub fn target_host_sets(&self) -> (&FeatureSet<'g>, &FeatureSet<'g>) {
533 match self {
534 CargoIntermediateSet::Unified(set) => (set, set),
535 CargoIntermediateSet::TargetHost { target, host } => (target, host),
536 }
537 }
538}