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}