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, 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}
229
230assert_covariant!(CargoSet);
231
232impl<'g> CargoSet<'g> {
233    /// Simulates a Cargo build of this feature set, with the given options.
234    ///
235    /// The feature sets are expected to be entirely within the workspace. Its behavior outside the
236    /// workspace isn't defined and may be surprising.
237    ///
238    /// `CargoSet::new` takes two `FeatureSet` instances:
239    /// * `initials`, from which dependencies are followed to build the `CargoSet`.
240    /// * `features_only`, which are additional inputs that are only used for feature
241    ///   unification. This may be used to simulate, e.g. `cargo build --package foo --package bar`,
242    ///   when you only care about the results of `foo` but specifying `bar` influences the build.
243    ///
244    /// Note that even if a package is in `features_only`, it may be included in the final build set
245    /// through other means (for example, if it is also in `initials` or it is a dependency of one
246    /// of them).
247    ///
248    /// In many cases `features_only` is empty -- in that case you may wish to use
249    /// `FeatureSet::into_cargo_set()`, and it may be more convenient to use that if the code is
250    /// written in a "fluent" style.
251    ///
252    ///
253    pub fn new(
254        initials: FeatureSet<'g>,
255        features_only: FeatureSet<'g>,
256        opts: &CargoOptions<'_>,
257    ) -> Result<Self, Error> {
258        let build_state = CargoSetBuildState::new(initials.graph().package_graph, opts)?;
259        Ok(build_state.build(initials, features_only))
260    }
261
262    /// Creates a new `CargoIntermediateSet` based on the given query and options.
263    ///
264    /// This set contains an over-estimate of targets and features.
265    ///
266    /// Not part of the stable API, exposed for testing.
267    #[doc(hidden)]
268    pub fn new_intermediate(
269        initials: &FeatureSet<'g>,
270        opts: &CargoOptions<'_>,
271    ) -> Result<CargoIntermediateSet<'g>, Error> {
272        let build_state = CargoSetBuildState::new(initials.graph().package_graph, opts)?;
273        Ok(build_state.build_intermediate(initials.to_feature_query(DependencyDirection::Forward)))
274    }
275
276    /// Returns the feature graph for this `CargoSet` instance.
277    pub fn feature_graph(&self) -> &FeatureGraph<'g> {
278        self.initials.graph()
279    }
280
281    /// Returns the package graph for this `CargoSet` instance.
282    pub fn package_graph(&self) -> &'g PackageGraph {
283        self.feature_graph().package_graph
284    }
285
286    /// Returns the initial packages and features from which the `CargoSet` instance was
287    /// constructed.
288    pub fn initials(&self) -> &FeatureSet<'g> {
289        &self.initials
290    }
291
292    /// Returns the packages and features that took part in feature unification but were not
293    /// considered part of the final result.
294    ///
295    /// For more about `features_only` and how it influences the build, see the documentation for
296    /// [`CargoSet::new`](CargoSet::new).
297    pub fn features_only(&self) -> &FeatureSet<'g> {
298        &self.features_only
299    }
300
301    /// Returns the feature set enabled on the target platform.
302    ///
303    /// This represents the packages and features that are included as code in the final build
304    /// artifacts. This is relevant for both cross-compilation and auditing.
305    pub fn target_features(&self) -> &FeatureSet<'g> {
306        &self.target_features
307    }
308
309    /// Returns the feature set enabled on the host platform.
310    ///
311    /// This represents the packages and features that influence the final build artifacts, but
312    /// whose code is generally not directly included.
313    ///
314    /// This includes all procedural macros, including those specified in the initial query.
315    pub fn host_features(&self) -> &FeatureSet<'g> {
316        &self.host_features
317    }
318
319    /// Returns the feature set enabled on the specified build platform.
320    pub fn platform_features(&self, build_platform: BuildPlatform) -> &FeatureSet<'g> {
321        match build_platform {
322            BuildPlatform::Target => self.target_features(),
323            BuildPlatform::Host => self.host_features(),
324        }
325    }
326
327    /// Returns the feature sets across the target and host build platforms.
328    pub fn all_features(&self) -> [(BuildPlatform, &FeatureSet<'g>); 2] {
329        [
330            (BuildPlatform::Target, self.target_features()),
331            (BuildPlatform::Host, self.host_features()),
332        ]
333    }
334
335    /// Returns the set of workspace and direct dependency packages on the target platform.
336    ///
337    /// The packages in this set are a subset of the packages in `target_features`.
338    pub fn target_direct_deps(&self) -> &PackageSet<'g> {
339        &self.target_direct_deps
340    }
341
342    /// Returns the set of workspace and direct dependency packages on the host platform.
343    ///
344    /// The packages in this set are a subset of the packages in `host_features`.
345    pub fn host_direct_deps(&self) -> &PackageSet<'g> {
346        &self.host_direct_deps
347    }
348
349    /// Returns the set of workspace and direct dependency packages on the specified build platform.
350    pub fn platform_direct_deps(&self, build_platform: BuildPlatform) -> &PackageSet<'g> {
351        match build_platform {
352            BuildPlatform::Target => self.target_direct_deps(),
353            BuildPlatform::Host => self.host_direct_deps(),
354        }
355    }
356
357    /// Returns the set of workspace and direct dependency packages across the target and host
358    /// build platforms.
359    pub fn all_direct_deps(&self) -> [(BuildPlatform, &PackageSet<'g>); 2] {
360        [
361            (BuildPlatform::Target, self.target_direct_deps()),
362            (BuildPlatform::Host, self.host_direct_deps()),
363        ]
364    }
365
366    /// Returns `PackageLink` instances for procedural macro dependencies from target packages.
367    ///
368    /// Procedural macros straddle the line between target and host: they're built for the host
369    /// but generate code that is compiled for the target platform.
370    ///
371    /// ## Notes
372    ///
373    /// Procedural macro packages will be included in the *host* feature set.
374    ///
375    /// The returned iterator will include proc macros that are depended on normally or in dev
376    /// builds from initials (if `include_dev` is set), but not the ones in the
377    /// `[build-dependencies]` section.
378    pub fn proc_macro_links<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageLink<'g>> + 'a {
379        let package_graph = self.target_features.graph().package_graph;
380        self.proc_macro_edge_ixs
381            .iter()
382            .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix))
383    }
384
385    /// Returns `PackageLink` instances for build dependencies from target packages.
386    ///
387    /// ## Notes
388    ///
389    /// For each link, the `from` is built on the target while the `to` is built on the host.
390    /// It is possible (though rare) that a build dependency is also included as a normal
391    /// dependency, or as a dev dependency in which case it will also be built on the target.
392    ///
393    /// The returned iterators will not include build dependencies of host packages -- those are
394    /// also built on the host.
395    pub fn build_dep_links<'a>(&'a self) -> impl ExactSizeIterator<Item = PackageLink<'g>> + 'a {
396        let package_graph = self.target_features.graph().package_graph;
397        self.build_dep_edge_ixs
398            .iter()
399            .map(move |edge_ix| package_graph.edge_ix_to_link(*edge_ix))
400    }
401}
402
403/// Either the target or the host platform.
404///
405/// When Cargo computes the platforms it is building on, it computes two separate build graphs: one
406/// for the target platform and one for the host. This is most useful in cross-compilation
407/// situations where the target is different from the host, but the separate graphs are computed
408/// whether or not a build cross-compiles.
409///
410/// A `cargo check` can be looked at as a kind of cross-compilation as well--machine code is
411/// generated and run for the host platform but not the target platform. This is why `cargo check`
412/// output usually has some lines that say `Compiling` (for the host platform) and some that say
413/// `Checking` (for the target platform).
414#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
415pub enum BuildPlatform {
416    /// The target platform.
417    ///
418    /// This represents the packages and features that are included as code in the final build
419    /// artifacts.
420    Target,
421
422    /// The host platform.
423    ///
424    /// This represents build scripts, proc macros and other code that is run on the machine doing
425    /// the compiling.
426    Host,
427}
428
429impl BuildPlatform {
430    /// A list of all possible variants of `BuildPlatform`.
431    pub const VALUES: &'static [Self; 2] = &[BuildPlatform::Target, BuildPlatform::Host];
432
433    /// Returns the build platform that's not `self`.
434    pub fn flip(self) -> Self {
435        match self {
436            BuildPlatform::Host => BuildPlatform::Target,
437            BuildPlatform::Target => BuildPlatform::Host,
438        }
439    }
440}
441
442impl fmt::Display for BuildPlatform {
443    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
444        match self {
445            BuildPlatform::Target => write!(f, "target"),
446            BuildPlatform::Host => write!(f, "host"),
447        }
448    }
449}
450
451/// An intermediate set representing an overestimate of what packages are built, but an accurate
452/// summary of what features are built given a particular package.
453///
454/// Not part of the stable API, exposed for cargo-compare.
455#[doc(hidden)]
456#[derive(Debug)]
457pub enum CargoIntermediateSet<'g> {
458    Unified(FeatureSet<'g>),
459    TargetHost {
460        target: FeatureSet<'g>,
461        host: FeatureSet<'g>,
462    },
463}
464
465impl<'g> CargoIntermediateSet<'g> {
466    #[doc(hidden)]
467    pub fn target_host_sets(&self) -> (&FeatureSet<'g>, &FeatureSet<'g>) {
468        match self {
469            CargoIntermediateSet::Unified(set) => (set, set),
470            CargoIntermediateSet::TargetHost { target, host } => (target, host),
471        }
472    }
473}