hakari/
hakari.rs

1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use crate::{
5    explain::HakariExplain,
6    toml_name_map,
7    toml_out::{write_toml, HakariOutputOptions},
8    CargoTomlError, HakariCargoToml, TomlOutError,
9};
10use ahash::AHashMap;
11use bimap::BiHashMap;
12use debug_ignore::DebugIgnore;
13use guppy::{
14    errors::TargetSpecError,
15    graph::{
16        cargo::{BuildPlatform, CargoOptions, CargoResolverVersion, CargoSet, InitialsPlatform},
17        feature::{named_feature_filter, FeatureId, FeatureLabel, FeatureSet, StandardFeatures},
18        DependencyDirection, PackageGraph, PackageMetadata,
19    },
20    platform::{Platform, PlatformSpec, TargetFeatures},
21    PackageId,
22};
23use rayon::prelude::*;
24use std::{
25    borrow::Cow,
26    collections::{BTreeMap, BTreeSet, HashSet},
27    fmt,
28    sync::Arc,
29};
30
31/// Configures and constructs [`Hakari`](Hakari) instances.
32///
33/// This struct provides a number of options that determine how `Hakari` instances are generated.
34#[derive(Clone, Debug)]
35pub struct HakariBuilder<'g> {
36    graph: DebugIgnore<&'g PackageGraph>,
37    hakari_package: Option<PackageMetadata<'g>>,
38    pub(crate) platforms: Vec<Arc<Platform>>,
39    resolver: CargoResolverVersion,
40    pub(crate) verify_mode: bool,
41    pub(crate) traversal_excludes: HashSet<&'g PackageId>,
42    final_excludes: HashSet<&'g PackageId>,
43    pub(crate) registries: BiHashMap<String, String, ahash::RandomState, ahash::RandomState>,
44    unify_target_host: UnifyTargetHost,
45    output_single_feature: bool,
46    pub(crate) dep_format_version: DepFormatVersion,
47    pub(crate) workspace_hack_line_style: WorkspaceHackLineStyle,
48}
49
50impl<'g> HakariBuilder<'g> {
51    /// Creates a new `HakariBuilder` instance from a `PackageGraph`.
52    ///
53    /// The Hakari package itself is usually present in the workspace. If so, specify its
54    /// package ID, otherwise pass in `None`.
55    ///
56    /// Returns an error if a Hakari package ID is specified but it isn't known to the graph, or
57    /// isn't in the workspace.
58    pub fn new(
59        graph: &'g PackageGraph,
60        hakari_id: Option<&PackageId>,
61    ) -> Result<Self, guppy::Error> {
62        let hakari_package = hakari_id
63            .map(|package_id| {
64                let package = graph.metadata(package_id)?;
65                if !package.in_workspace() {
66                    return Err(guppy::Error::UnknownWorkspaceName(
67                        package.name().to_string(),
68                    ));
69                }
70                Ok(package)
71            })
72            .transpose()?;
73
74        Ok(Self {
75            graph: DebugIgnore(graph),
76            hakari_package,
77            platforms: vec![],
78            resolver: CargoResolverVersion::V2,
79            verify_mode: false,
80            traversal_excludes: HashSet::new(),
81            final_excludes: HashSet::new(),
82            registries: BiHashMap::with_hashers(Default::default(), Default::default()),
83            unify_target_host: UnifyTargetHost::default(),
84            output_single_feature: false,
85            dep_format_version: DepFormatVersion::default(),
86            workspace_hack_line_style: WorkspaceHackLineStyle::default(),
87        })
88    }
89
90    /// Returns the `PackageGraph` used to construct this `Hakari` instance.
91    pub fn graph(&self) -> &'g PackageGraph {
92        // This is a spurious clippy lint on Rust 1.65.0
93        #[allow(clippy::explicit_auto_deref)]
94        *self.graph
95    }
96
97    /// Returns the Hakari package, or `None` if it wasn't passed into [`new`](Self::new).
98    pub fn hakari_package(&self) -> Option<&PackageMetadata<'g>> {
99        self.hakari_package.as_ref()
100    }
101
102    /// Reads the existing TOML file for the Hakari package from disk, returning a
103    /// `HakariCargoToml`.
104    ///
105    /// This can be used with [`Hakari::to_toml_string`](Hakari::to_toml_string) to manage the
106    /// contents of the Hakari package's TOML file on disk.
107    ///
108    /// Returns an error if there was an issue reading the TOML file from disk, or `None` if
109    /// this builder was created without a Hakari package.
110    pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
111        let hakari_package = self.hakari_package()?;
112        let workspace_path = hakari_package
113            .source()
114            .workspace_path()
115            .expect("hakari_package is in workspace");
116        Some(HakariCargoToml::new_relative(
117            self.graph.workspace().root(),
118            workspace_path,
119        ))
120    }
121
122    /// Sets a list of platforms for `hakari` to use.
123    ///
124    /// By default, `hakari` unifies features that are always enabled across all platforms. If
125    /// builds are commonly performed on a few platforms, `hakari` can output platform-specific
126    /// instructions for those builds.
127    ///
128    /// This currently supports target triples only, without further customization around
129    /// target features or flags. In the future, this may support `cfg()` expressions using
130    /// an [SMT solver](https://en.wikipedia.org/wiki/Satisfiability_modulo_theories).
131    ///
132    /// Call `set_platforms` with an empty list to reset to default behavior.
133    ///
134    /// Returns an error if a platform wasn't known to [`target_spec`], the library `hakari` uses
135    /// to resolve platforms.
136    pub fn set_platforms(
137        &mut self,
138        platforms: impl IntoIterator<Item = impl Into<Cow<'static, str>>>,
139    ) -> Result<&mut Self, TargetSpecError> {
140        self.platforms = platforms
141            .into_iter()
142            .map(|s| Ok(Arc::new(Platform::new(s.into(), TargetFeatures::Unknown)?)))
143            .collect::<Result<Vec<_>, _>>()?;
144        Ok(self)
145    }
146
147    /// Returns the platforms set through `set_platforms`, or an empty list if no platforms are
148    /// set.
149    pub fn platforms(&self) -> impl ExactSizeIterator<Item = &str> + '_ {
150        self.platforms.iter().map(|platform| platform.triple_str())
151    }
152
153    /// Sets the Cargo resolver version.
154    ///
155    /// By default, `HakariBuilder` uses [version 2](CargoResolverVersion::V2) of the Cargo
156    /// resolver. For more about Cargo resolvers, see the documentation for
157    /// [`CargoResolverVersion`](CargoResolverVersion).
158    pub fn set_resolver(&mut self, resolver: CargoResolverVersion) -> &mut Self {
159        self.resolver = resolver;
160        self
161    }
162
163    /// Returns the current Cargo resolver version.
164    pub fn resolver(&self) -> CargoResolverVersion {
165        self.resolver
166    }
167
168    /// Pretends that the provided packages don't exist during graph traversals.
169    ///
170    /// Users may wish to not consider certain packages while figuring out the unified feature set.
171    /// Setting this option prevents those packages from being considered.
172    ///
173    /// Practically, this means that:
174    /// * If a workspace package is specified, Cargo build simulations for it will not be run.
175    /// * If a third-party package is specified, it will not be present in the output, nor will
176    ///   any transitive dependencies or features enabled by it that aren't enabled any other way.
177    ///   In other words, any packages excluded during traversal are also [excluded from the final
178    ///   output](Self::add_final_excludes).
179    ///
180    /// Returns an error if any package IDs specified aren't known to the graph.
181    pub fn add_traversal_excludes<'b>(
182        &mut self,
183        excludes: impl IntoIterator<Item = &'b PackageId>,
184    ) -> Result<&mut Self, guppy::Error> {
185        let traversal_exclude: Vec<&'g PackageId> = excludes
186            .into_iter()
187            .map(|package_id| Ok(self.graph.metadata(package_id)?.id()))
188            .collect::<Result<_, _>>()?;
189        self.traversal_excludes.extend(traversal_exclude);
190        Ok(self)
191    }
192
193    /// Returns the packages currently excluded during graph traversals.
194    ///
195    /// Also returns the Hakari package if specified. This is because the Hakari package is treated
196    /// as excluded while performing unification.
197    pub fn traversal_excludes<'b>(&'b self) -> impl Iterator<Item = &'g PackageId> + 'b {
198        let excludes = self.make_traversal_excludes();
199        excludes.iter()
200    }
201
202    /// Returns true if a package ID is currently excluded during traversal.
203    ///
204    /// Also returns true for the Hakari package if specified. This is because the Hakari package is
205    /// treated as excluded by the algorithm.
206    ///
207    /// Returns an error if this package ID isn't known to the underlying graph.
208    pub fn is_traversal_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
209        self.graph.metadata(package_id)?;
210
211        let excludes = self.make_traversal_excludes();
212        Ok(excludes.is_excluded(package_id))
213    }
214
215    /// Adds packages to be removed from the final output.
216    ///
217    /// Unlike [`traversal_excludes`](Self::traversal_excludes), these packages are considered
218    /// during traversals, but removed at the end.
219    ///
220    /// Returns an error if any package IDs specified aren't known to the graph.
221    pub fn add_final_excludes<'b>(
222        &mut self,
223        excludes: impl IntoIterator<Item = &'b PackageId>,
224    ) -> Result<&mut Self, guppy::Error> {
225        let final_excludes: Vec<&'g PackageId> = excludes
226            .into_iter()
227            .map(|package_id| Ok(self.graph.metadata(package_id)?.id()))
228            .collect::<Result<_, _>>()?;
229        self.final_excludes.extend(final_excludes);
230        Ok(self)
231    }
232
233    /// Returns the packages to be removed from the final output.
234    pub fn final_excludes<'b>(&'b self) -> impl Iterator<Item = &'g PackageId> + 'b {
235        self.final_excludes.iter().copied()
236    }
237
238    /// Returns true if a package ID is currently excluded from the final output.
239    ///
240    /// Returns an error if this package ID isn't known to the underlying graph.
241    pub fn is_final_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
242        self.graph.metadata(package_id)?;
243        Ok(self.final_excludes.contains(package_id))
244    }
245
246    /// Returns true if a package ID is excluded from either the traversal or the final output.
247    ///
248    /// Also returns true for the Hakari package if specified. This is because the Hakari package is
249    /// treated as excluded by the algorithm.
250    ///
251    /// Returns an error if this package ID isn't known to the underlying graph.
252    #[inline]
253    pub fn is_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
254        Ok(self.is_traversal_excluded(package_id)? || self.is_final_excluded(package_id)?)
255    }
256
257    /// Add alternate registries by (name, URL) pairs.
258    ///
259    /// This is a temporary workaround until [Cargo issue #9052](https://github.com/rust-lang/cargo/issues/9052)
260    /// is resolved.
261    pub fn add_registries(
262        &mut self,
263        registries: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
264    ) -> &mut Self {
265        self.registries.extend(
266            registries
267                .into_iter()
268                .map(|(name, url)| (name.into(), url.into())),
269        );
270        self
271    }
272
273    /// Whether and how to unify feature sets across target and host platforms.
274    ///
275    /// This is an advanced feature that most users don't need to set. For more information about
276    /// this option, see the documentation for [`UnifyTargetHost`](UnifyTargetHost).
277    pub fn set_unify_target_host(&mut self, unify_target_host: UnifyTargetHost) -> &mut Self {
278        self.unify_target_host = unify_target_host;
279        self
280    }
281
282    /// Returns the current value of `unify_target_host`.
283    pub fn unify_target_host(&self) -> UnifyTargetHost {
284        self.unify_target_host
285    }
286
287    /// Whether to unify feature sets for all dependencies.
288    ///
289    /// By default, Hakari only produces output for dependencies that are built with more
290    /// than one feature set. If set to true, Hakari will produce outputs for all dependencies,
291    /// including those that don't need to be unified.
292    ///
293    /// This is rarely needed in production, and is most useful for testing and debugging scenarios.
294    pub fn set_output_single_feature(&mut self, output_single_feature: bool) -> &mut Self {
295        self.output_single_feature = output_single_feature;
296        self
297    }
298
299    /// Returns the current value of `output_single_feature`.
300    pub fn output_single_feature(&self) -> bool {
301        self.output_single_feature
302    }
303
304    /// Version of hakari data to output.
305    ///
306    /// For more, see the documentation for [`DepFormatVersion`](DepFormatVersion).
307    pub fn set_dep_format_version(&mut self, dep_format_version: DepFormatVersion) -> &mut Self {
308        self.dep_format_version = dep_format_version;
309        self
310    }
311
312    /// Returns the current value of `dep_format_version`.
313    pub fn dep_format_version(&self) -> DepFormatVersion {
314        self.dep_format_version
315    }
316
317    /// Kind of `workspace-hack = ...` lines to output.
318    ///
319    /// For more, see the documentation for [`WorkspaceHackLineStyle`].
320    pub fn set_workspace_hack_line_style(
321        &mut self,
322        line_style: WorkspaceHackLineStyle,
323    ) -> &mut Self {
324        self.workspace_hack_line_style = line_style;
325        self
326    }
327
328    /// Returns the current value of `workspace_hack_line_style`.
329    pub fn workspace_hack_line_style(&self) -> WorkspaceHackLineStyle {
330        self.workspace_hack_line_style
331    }
332
333    /// Computes the `Hakari` for this builder.
334    pub fn compute(self) -> Hakari<'g> {
335        Hakari::build(self)
336    }
337
338    // ---
339    // Helper methods
340    // ---
341
342    #[cfg(feature = "cli-support")]
343    pub(crate) fn traversal_excludes_only<'b>(
344        &'b self,
345    ) -> impl Iterator<Item = &'g PackageId> + 'b {
346        self.traversal_excludes.iter().copied()
347    }
348
349    fn make_traversal_excludes<'b>(&'b self) -> TraversalExcludes<'g, 'b> {
350        let hakari_package = if self.verify_mode {
351            None
352        } else {
353            self.hakari_package.map(|package| package.id())
354        };
355
356        TraversalExcludes {
357            excludes: &self.traversal_excludes,
358            hakari_package,
359        }
360    }
361
362    fn make_features_only<'b>(&'b self) -> FeatureSet<'g> {
363        if self.verify_mode {
364            match &self.hakari_package {
365                Some(package) => package.to_package_set(),
366                None => self.graph.resolve_none(),
367            }
368            .to_feature_set(StandardFeatures::Default)
369        } else {
370            self.graph.feature_graph().resolve_none()
371        }
372    }
373}
374
375#[cfg(feature = "cli-support")]
376mod summaries {
377    use super::*;
378    use crate::summaries::HakariBuilderSummary;
379    use guppy::platform::TargetFeatures;
380
381    impl<'g> HakariBuilder<'g> {
382        /// Constructs a `HakariBuilder` from a `PackageGraph` and a serialized summary.
383        ///
384        /// Requires the `cli-support` feature to be enabled.
385        ///
386        /// Returns an error if the summary references a package that's not present, or if there was
387        /// some other issue while creating a `HakariBuilder` from the summary.
388        pub fn from_summary(
389            graph: &'g PackageGraph,
390            summary: &HakariBuilderSummary,
391        ) -> Result<Self, guppy::Error> {
392            let hakari_package = summary
393                .hakari_package
394                .as_ref()
395                .map(|name| graph.workspace().member_by_name(name))
396                .transpose()?;
397            let platforms = summary
398                .platforms
399                .iter()
400                .map(|triple_str| {
401                    let platform = Platform::new(triple_str.clone(), TargetFeatures::Unknown)
402                        .map_err(|err| {
403                            guppy::Error::TargetSpecError(
404                                "while resolving hakari config or summary".to_owned(),
405                                err,
406                            )
407                        })?;
408                    Ok(platform.into())
409                })
410                .collect::<Result<Vec<_>, _>>()?;
411
412            let registries: BiHashMap<_, _, ahash::RandomState, ahash::RandomState> = summary
413                .registries
414                .iter()
415                .map(|(name, url)| (name.clone(), url.clone()))
416                .collect();
417
418            let traversal_excludes = summary
419                .traversal_excludes
420                .to_package_set_registry(
421                    graph,
422                    |name| registries.get_by_left(name).map(|s| s.as_str()),
423                    "resolving hakari traversal-excludes",
424                )?
425                .package_ids(DependencyDirection::Forward)
426                .collect();
427            let final_excludes = summary
428                .final_excludes
429                .to_package_set_registry(
430                    graph,
431                    |name| registries.get_by_left(name).map(|s| s.as_str()),
432                    "resolving hakari final-excludes",
433                )?
434                .package_ids(DependencyDirection::Forward)
435                .collect();
436
437            Ok(Self {
438                graph: DebugIgnore(graph),
439                hakari_package,
440                resolver: summary.resolver,
441                verify_mode: false,
442                unify_target_host: summary.unify_target_host,
443                output_single_feature: summary.output_single_feature,
444                dep_format_version: summary.dep_format_version,
445                workspace_hack_line_style: summary.workspace_hack_line_style,
446                platforms,
447                registries,
448                traversal_excludes,
449                final_excludes,
450            })
451        }
452    }
453}
454
455/// Whether to unify feature sets for a given dependency across target and host platforms.
456///
457/// Consider a dependency that is built as both normally (on the target platform) and in a build
458/// script or proc macro. The normal dependency is considered to be built on the *target platform*,
459/// and is represented in the `[dependencies]` section in the generated `Cargo.toml`.
460/// The build dependency is built on the *host platform*, represented in the `[build-dependencies]`
461/// section.
462///
463/// Now consider that the target and host platforms need two different sets of features:
464///
465/// ```toml
466/// ## feature set on target platform
467/// [dependencies]
468/// my-dep = { version = "1.0", features = ["a", "b"] }
469///
470/// ## feature set on host platform
471/// [build-dependencies]
472/// my-dep = { version = "1.0", features = ["b", "c"] }
473/// ```
474///
475/// Should hakari unify the feature sets across the `[dependencies]` and `[build-dependencies]`
476/// feature sets?
477///
478/// Call `HakariBuilder::set_unify_target_host` to configure this option.
479#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
480#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
481#[cfg_attr(feature = "cli-support", derive(serde::Serialize, serde::Deserialize))]
482#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
483#[non_exhaustive]
484pub enum UnifyTargetHost {
485    /// Perform no unification across the target and host feature sets.
486    ///
487    /// This is the most conservative option, but it means that some dependencies may be built with
488    /// two different sets of features. In this mode, Hakari will likely be significantly less
489    /// efficient.
490    None,
491
492    /// Automatically choose between the [`UnifyIfBoth`](Self::UnifyIfBoth) and the
493    /// [`ReplicateTargetOnHost`](Self::ReplicateTargetOnHost) options:
494    /// * If the workspace contains proc macros, or crates that are build dependencies of other
495    ///   crates, choose the `ReplicateTargetAsHost` strategy.
496    /// * Otherwise, choose the `UnifyIfBoth` strategy.
497    ///
498    /// This is the default behavior.
499    Auto,
500
501    /// Perform unification across target and host feature sets, but only if a dependency is built
502    /// on both the target and the host.
503    ///
504    /// This is useful if cross-compilations are uncommon and one wishes to avoid the same package
505    /// being built two different ways: once for the target and once for the host.
506    UnifyIfBoth,
507
508    /// Perform unification across target and host feature sets, and also replicate all target-only
509    /// lines to the host.
510    ///
511    /// This is most useful if some workspace packages are proc macros or build dependencies
512    /// used by other packages.
513    ReplicateTargetOnHost,
514}
515
516/// The default for `UnifyTargetHost`: automatically choose unification strategy based on the
517/// workspace.
518impl Default for UnifyTargetHost {
519    #[inline]
520    fn default() -> Self {
521        UnifyTargetHost::Auto
522    }
523}
524
525/// Format version for hakari.
526///
527/// Older versions are kept around for backwards compatibility.
528#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
529#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
530#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
531#[non_exhaustive]
532#[derive(Default)]
533pub enum DepFormatVersion {
534    /// `workspace-hack = { path = ...}`. (Note the lack of a trailing space.)
535    ///
536    /// This was used until `cargo hakari 0.9.6`.
537    #[cfg_attr(feature = "cli-support", serde(rename = "1"))]
538    #[default]
539    V1,
540
541    /// `workspace-hack = { version = "0.1", path = ... }`. This was introduced in
542    /// `cargo hakari 0.9.8`.
543    #[cfg_attr(feature = "cli-support", serde(rename = "2"))]
544    V2,
545
546    /// Elides build metadata. This was introduced in `cargo hakari 0.9.18`.
547    #[cfg_attr(feature = "cli-support", serde(rename = "3"))]
548    V3,
549
550    /// Sorts dependency names alphabetically. This was introduced in `cargo hakari 0.9.22`.
551    ///
552    /// (Dependency names were usually produced in sorted order before V4, but there are
553    /// some edge cases where they weren't: see [issue
554    /// #65](https://github.com/guppy-rs/guppy/issues/65).
555    #[cfg_attr(feature = "cli-support", serde(rename = "4"))]
556    V4,
557}
558
559impl DepFormatVersion {
560    /// Returns the highest format version supported by this version of `cargo hakari`.
561    #[inline]
562    pub fn latest() -> Self {
563        DepFormatVersion::V4
564    }
565}
566
567impl fmt::Display for DepFormatVersion {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        match self {
570            DepFormatVersion::V1 => write!(f, "1"),
571            DepFormatVersion::V2 => write!(f, "2"),
572            DepFormatVersion::V3 => write!(f, "3"),
573            DepFormatVersion::V4 => write!(f, "4"),
574        }
575    }
576}
577
578/// Style of `workspace-hack = ...` lines to output.
579#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
580#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
581#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
582#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
583#[non_exhaustive]
584#[derive(Default)]
585pub enum WorkspaceHackLineStyle {
586    /// `workspace-hack = { version = "0.1", path = ... }`.
587    #[default]
588    Full,
589
590    /// `workspace-hack = { version = "0.1" }`.
591    VersionOnly,
592
593    /// `workspace-hack.workspace = true`
594    WorkspaceDotted,
595}
596
597/// A key representing a platform and host/target. Returned by `Hakari`.
598#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
599pub struct OutputKey {
600    /// The index of the build platform for this key, or `None` if the computation was done in a
601    /// platform-independent manner.
602    pub platform_idx: Option<usize>,
603
604    /// The build platform: target or host.
605    pub build_platform: BuildPlatform,
606}
607
608/// The result of a Hakari computation.
609///
610/// This contains all the data required to generate a workspace package.
611///
612/// Produced by [`HakariBuilder::compute`](HakariBuilder::compute).
613#[derive(Clone, Debug)]
614#[non_exhaustive]
615pub struct Hakari<'g> {
616    pub(crate) builder: HakariBuilder<'g>,
617
618    /// The map built by Hakari of dependencies that need to be unified.
619    ///
620    /// This map is used to construct the TOML output. Public access is provided in case some
621    /// post-processing needs to be done.
622    pub output_map: OutputMap<'g>,
623
624    /// The complete map of dependency build results built by Hakari.
625    ///
626    /// This map is not used to generate the TOML output.
627    pub computed_map: ComputedMap<'g>,
628}
629
630impl<'g> Hakari<'g> {
631    /// Returns the `HakariBuilder` used to create this instance.
632    pub fn builder(&self) -> &HakariBuilder<'g> {
633        &self.builder
634    }
635
636    /// Reads the existing TOML file for the Hakari package from disk, returning a
637    /// `HakariCargoToml`.
638    ///
639    /// This can be used with [`to_toml_string`](Self::to_toml_string) to manage the contents of
640    /// the given TOML file on disk.
641    ///
642    /// Returns an error if there was an issue reading the TOML file from disk, or `None` if
643    /// the builder's [`hakari_package`](HakariBuilder::hakari_package) is `None`.
644    pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
645        self.builder.read_toml()
646    }
647
648    /// Writes `[dependencies]` and other `Cargo.toml` lines to the given `fmt::Write` instance.
649    ///
650    /// `&mut String` and `fmt::Formatter` both implement `fmt::Write`.
651    pub fn write_toml(
652        &self,
653        options: &HakariOutputOptions,
654        out: impl fmt::Write,
655    ) -> Result<(), TomlOutError> {
656        write_toml(
657            &self.builder,
658            &self.output_map,
659            options,
660            self.builder.dep_format_version,
661            out,
662        )
663    }
664
665    /// Returns a map of dependency names as present in the workspace-hack's `Cargo.toml` to their
666    /// corresponding [`PackageMetadata`].
667    ///
668    /// Packages which have one version are present as their original names, while packages with
669    /// more than one version have a hash appended to them.
670    pub fn toml_name_map(&self) -> AHashMap<Cow<'g, str>, PackageMetadata<'g>> {
671        toml_name_map(&self.output_map, self.builder.dep_format_version)
672    }
673
674    /// Returns a `HakariExplain`, which can be used to print out why a specific package is
675    /// in the workspace-hack's `Cargo.toml`.
676    ///
677    /// Returns an error if the package ID was not found in the output.
678    pub fn explain(
679        &self,
680        package_id: &'g PackageId,
681    ) -> Result<HakariExplain<'g, '_>, guppy::Error> {
682        HakariExplain::new(self, package_id)
683    }
684
685    /// A convenience method around `write_toml` that returns a new string with `Cargo.toml` lines.
686    ///
687    /// The returned string is guaranteed to be valid TOML, and can be provided to
688    /// a [`HakariCargoToml`](crate::HakariCargoToml) obtained from [`read_toml`](Self::read_toml).
689    pub fn to_toml_string(&self, options: &HakariOutputOptions) -> Result<String, TomlOutError> {
690        let mut out = String::new();
691        self.write_toml(options, &mut out)?;
692        Ok(out)
693    }
694
695    // ---
696    // Helper methods
697    // ---
698
699    fn build(builder: HakariBuilder<'g>) -> Self {
700        let graph = *builder.graph;
701        let mut computed_map_build = ComputedMapBuild::new(&builder);
702        let platform_specs: Vec<_> = builder
703            .platforms
704            .iter()
705            .map(|platform| PlatformSpec::Platform(platform.clone()))
706            .collect();
707
708        let unify_target_host = builder.unify_target_host.to_impl(graph);
709
710        // Collect all the dependencies that need to be unified, by platform and build type.
711        let mut map_build: OutputMapBuild<'g> = OutputMapBuild::new(graph);
712        map_build.insert_all(
713            computed_map_build.iter(),
714            builder.output_single_feature,
715            unify_target_host,
716        );
717
718        if !builder.output_single_feature {
719            // Adding packages might cause different feature sets for some dependencies. Simulate
720            // further builds with the given target and host features, and use that to add in any
721            // extra features that need to be considered.
722            loop {
723                let mut add_extra = HashSet::new();
724                for (output_key, features) in map_build.iter_feature_sets() {
725                    let initials_platform = match output_key.build_platform {
726                        BuildPlatform::Target => InitialsPlatform::Standard,
727                        BuildPlatform::Host => InitialsPlatform::Host,
728                    };
729
730                    let mut cargo_opts = CargoOptions::new();
731                    let platform_spec = match output_key.platform_idx {
732                        Some(idx) => platform_specs[idx].clone(),
733                        None => PlatformSpec::Always,
734                    };
735                    // Third-party dependencies are built without including dev.
736                    cargo_opts
737                        .set_include_dev(false)
738                        .set_initials_platform(initials_platform)
739                        .set_platform(platform_spec)
740                        .set_resolver(builder.resolver)
741                        .add_omitted_packages(computed_map_build.excludes.iter());
742                    let cargo_set = features
743                        .into_cargo_set(&cargo_opts)
744                        .expect("into_cargo_set processed successfully");
745
746                    // Check the features for the cargo set to see if any further dependencies were
747                    // built with a different result and weren't included in the hakari map
748                    // originally.
749                    for &(build_platform, feature_set) in cargo_set.all_features().iter() {
750                        for feature_list in
751                            feature_set.packages_with_features(DependencyDirection::Forward)
752                        {
753                            let dep = feature_list.package();
754                            let dep_id = dep.id();
755                            // This is "get or insert" because we could be adding whole new
756                            // dependencies here rather than just new features to existing
757                            // dependencies.
758                            let v_mut = computed_map_build
759                                .get_or_insert_mut(output_key.platform_idx, dep_id);
760
761                            // Is it already present in the output?
762                            let new_key = OutputKey {
763                                platform_idx: output_key.platform_idx,
764                                build_platform,
765                            };
766
767                            if map_build.is_inserted(new_key, dep_id) {
768                                continue;
769                            }
770
771                            let this_list: BTreeSet<_> = feature_list.named_features().collect();
772
773                            let already_present = v_mut.contains(build_platform, &this_list);
774                            if !already_present {
775                                // The feature list added by this dependency is non-unique.
776                                v_mut.mark_fixed_up(build_platform, this_list);
777                                add_extra.insert((output_key.platform_idx, dep_id));
778                            }
779                        }
780                    }
781                }
782
783                if add_extra.is_empty() {
784                    break;
785                }
786
787                map_build.insert_all(
788                    add_extra.iter().map(|&(platform_idx, dep_id)| {
789                        let v = computed_map_build
790                            .get(platform_idx, dep_id)
791                            .expect("full value should be present");
792                        (platform_idx, dep_id, v)
793                    }),
794                    builder.output_single_feature,
795                    unify_target_host,
796                );
797            }
798        }
799
800        let computed_map = computed_map_build.computed_map;
801        let output_map = map_build.finish(
802            &builder.final_excludes,
803            builder.dep_format_version,
804            builder.output_single_feature,
805        );
806
807        Self {
808            builder,
809            output_map,
810            computed_map,
811        }
812    }
813}
814
815/// The map used by Hakari to generate output TOML.
816///
817/// This is a two-level `BTreeMap`, where:
818/// * the top-level keys are [`OutputKey`](OutputKey) instances.
819/// * the inner map is keyed by dependency [`PackageId`](PackageId) instances, and the values are
820///   the corresponding [`PackageMetadata`](PackageMetadata) for this dependency, and the set of
821///   features enabled for this package.
822///
823/// This is an alias for the type of [`Hakari::output_map`](Hakari::output_map).
824pub type OutputMap<'g> =
825    BTreeMap<OutputKey, BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>>;
826
827/// The map of all build results computed by Hakari.
828///
829/// The keys are the platform index and the dependency's package ID, and the values are
830/// [`ComputedValue`](ComputedValue) instances that represent the different feature sets this
831/// dependency is built with on both the host and target platforms.
832///
833/// The values that are most interesting are the ones where maps have two elements or more: they
834/// indicate dependencies with features that need to be unified.
835///
836/// This is an alias for the type of [`Hakari::computed_map`](Hakari::computed_map).
837pub type ComputedMap<'g> = BTreeMap<(Option<usize>, &'g PackageId), ComputedValue<'g>>;
838
839/// The values of a [`ComputedMap`](ComputedMap).
840///
841/// This represents a pair of `ComputedInnerMap` instances: one for the target platform and one for
842/// the host. For more about the values, see the documentation for
843/// [`ComputedInnerMap`](ComputedInnerMap).
844#[derive(Clone, Debug, Default)]
845pub struct ComputedValue<'g> {
846    /// The feature sets built on the target platform.
847    pub target_inner: ComputedInnerMap<'g>,
848
849    /// The feature sets built on the host platform.
850    pub host_inner: ComputedInnerMap<'g>,
851}
852
853/// A target map or a host map in a [`ComputedValue`](ComputedValue).
854///
855/// * The keys are sets of feature names (or empty for no features).
856/// * The values are [`ComputedInnerValue`] instances.
857pub type ComputedInnerMap<'g> = BTreeMap<BTreeSet<&'g str>, ComputedInnerValue<'g>>;
858
859/// The values of [`ComputedInnerMap`].
860#[derive(Clone, Debug, Default)]
861pub struct ComputedInnerValue<'g> {
862    /// The workspace packages, selected features, and include dev that cause the key in
863    /// `ComputedMap` to be built with the feature set that forms the key of `ComputedInnerMap`.
864    /// They are not defined to be in any particular order.
865    pub workspace_packages: Vec<(PackageMetadata<'g>, StandardFeatures, bool)>,
866
867    /// Whether at least one post-computation fixup was performed with this feature set.
868    pub fixed_up: bool,
869}
870
871impl<'g> ComputedInnerValue<'g> {
872    fn extend(&mut self, other: ComputedInnerValue<'g>) {
873        self.workspace_packages.extend(other.workspace_packages);
874        self.fixed_up |= other.fixed_up;
875    }
876
877    #[inline]
878    fn push(
879        &mut self,
880        package: PackageMetadata<'g>,
881        features: StandardFeatures,
882        include_dev: bool,
883    ) {
884        self.workspace_packages
885            .push((package, features, include_dev));
886    }
887}
888
889#[derive(Debug)]
890struct TraversalExcludes<'g, 'b> {
891    excludes: &'b HashSet<&'g PackageId>,
892    hakari_package: Option<&'g PackageId>,
893}
894
895impl<'g, 'b> TraversalExcludes<'g, 'b> {
896    fn iter(&self) -> impl Iterator<Item = &'g PackageId> + 'b {
897        self.excludes.iter().copied().chain(self.hakari_package)
898    }
899
900    fn is_excluded(&self, package_id: &PackageId) -> bool {
901        self.hakari_package == Some(package_id) || self.excludes.contains(package_id)
902    }
903}
904
905/// Intermediate build state used by Hakari.
906#[derive(Debug)]
907struct ComputedMapBuild<'g, 'b> {
908    excludes: TraversalExcludes<'g, 'b>,
909    computed_map: ComputedMap<'g>,
910}
911
912impl<'g, 'b> ComputedMapBuild<'g, 'b> {
913    fn new(builder: &'b HakariBuilder<'g>) -> Self {
914        // This was just None or All for a bit under the theory that feature sets are additive only,
915        // but unfortunately we cannot exploit this property because it doesn't account for the fact
916        // that some dependencies might not be built *at all*, under certain feature combinations.
917        //
918        // That's also why we simulate builds with and without dev-only dependencies in all cases.
919        //
920        // For example, for:
921        //
922        // ```toml
923        // [dependencies]
924        // dep = { version = "1", optional = true }
925        //
926        // [dev-dependencies]
927        // dep = { version = "1", optional = true, features = ["dev-feature"] }
928        //
929        // [features]
930        // default = ["dep"]
931        // extra = ["dep/extra", "dep/dev-feature"]
932        // ```
933        //
934        // | feature set | include dev | dep status         |
935        // | ----------- | ----------- | ------------------ |
936        // | none        | no          | not built          |
937        // | none        | yes         | not built          |
938        // | default     | no          | no features        |
939        // | default     | yes         | dev-feature        |
940        // | all         | no          | extra, dev-feature |
941        // | all         | yes         | extra, dev-feature |
942        //
943        // (And there's further complexity possible with transitive deps as well.)
944        let features_include_dev = [
945            (StandardFeatures::None, false),
946            (StandardFeatures::None, true),
947            (StandardFeatures::Default, false),
948            (StandardFeatures::Default, true),
949            (StandardFeatures::All, false),
950            (StandardFeatures::All, true),
951        ];
952
953        // Features for the "always" platform spec.
954        let always_features = features_include_dev
955            .iter()
956            .map(|&(features, include_dev)| (None, PlatformSpec::Always, features, include_dev));
957
958        // Features for specified platforms.
959        let specified_features =
960            features_include_dev
961                .iter()
962                .flat_map(|&(features, include_dev)| {
963                    builder
964                        .platforms
965                        .iter()
966                        .enumerate()
967                        .map(move |(idx, platform)| {
968                            (
969                                Some(idx),
970                                PlatformSpec::Platform(platform.clone()),
971                                features,
972                                include_dev,
973                            )
974                        })
975                });
976        let platforms_features: Vec<_> = always_features.chain(specified_features).collect();
977
978        let workspace = builder.graph.workspace();
979        let excludes = builder.make_traversal_excludes();
980        let features_only = builder.make_features_only();
981        let excludes_ref = &excludes;
982        let features_only_ref = &features_only;
983
984        let computed_map: ComputedMap<'g> = platforms_features
985            .into_par_iter()
986            // The cargo_set computation in the inner iterator is the most expensive part of the
987            // process, so use flat_map instead of flat_map_iter.
988            .flat_map(|(idx, platform_spec, feature_filter, include_dev)| {
989                let mut cargo_options = CargoOptions::new();
990                cargo_options
991                    .set_include_dev(include_dev)
992                    .set_resolver(builder.resolver)
993                    .set_platform(platform_spec)
994                    .add_omitted_packages(excludes.iter());
995
996                workspace.par_iter().map(move |workspace_package| {
997                    if excludes_ref.is_excluded(workspace_package.id()) {
998                        // Skip this package since it was excluded during traversal.
999                        return BTreeMap::new();
1000                    }
1001
1002                    let initials = workspace_package
1003                        .to_package_set()
1004                        .to_feature_set(feature_filter);
1005                    let cargo_set =
1006                        CargoSet::new(initials, features_only_ref.clone(), &cargo_options)
1007                            .expect("cargo resolution should succeed");
1008
1009                    let all_features = cargo_set.all_features();
1010
1011                    let values = all_features.iter().flat_map(|&(build_platform, features)| {
1012                        features
1013                            .packages_with_features(DependencyDirection::Forward)
1014                            .filter_map(move |feature_list| {
1015                                let dep = feature_list.package();
1016                                if dep.in_workspace() {
1017                                    // Only looking at third-party packages for hakari.
1018                                    return None;
1019                                }
1020
1021                                let features: BTreeSet<&'g str> =
1022                                    feature_list.named_features().collect();
1023                                Some((
1024                                    idx,
1025                                    build_platform,
1026                                    dep.id(),
1027                                    features,
1028                                    workspace_package,
1029                                    feature_filter,
1030                                    include_dev,
1031                                ))
1032                            })
1033                    });
1034
1035                    let mut map = ComputedMap::new();
1036                    for (
1037                        platform_idx,
1038                        build_platform,
1039                        package_id,
1040                        features,
1041                        package,
1042                        feature_filter,
1043                        include_dev,
1044                    ) in values
1045                    {
1046                        // Accumulate the features and package for each key.
1047                        map.entry((platform_idx, package_id)).or_default().insert(
1048                            build_platform,
1049                            features,
1050                            package,
1051                            feature_filter,
1052                            include_dev,
1053                        );
1054                    }
1055
1056                    map
1057                })
1058            })
1059            .reduce(ComputedMap::new, |mut acc, map| {
1060                // Accumulate across all threads.
1061                for (k, v) in map {
1062                    acc.entry(k).or_default().merge(v);
1063                }
1064                acc
1065            });
1066
1067        Self {
1068            excludes,
1069            computed_map,
1070        }
1071    }
1072
1073    fn get(
1074        &self,
1075        platform_idx: Option<usize>,
1076        package_id: &'g PackageId,
1077    ) -> Option<&ComputedValue<'g>> {
1078        self.computed_map.get(&(platform_idx, package_id))
1079    }
1080
1081    fn get_or_insert_mut(
1082        &mut self,
1083        platform_idx: Option<usize>,
1084        package_id: &'g PackageId,
1085    ) -> &mut ComputedValue<'g> {
1086        self.computed_map
1087            .entry((platform_idx, package_id))
1088            .or_default()
1089    }
1090
1091    fn iter<'a>(
1092        &'a self,
1093    ) -> impl Iterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)> + 'a {
1094        self.computed_map
1095            .iter()
1096            .map(move |(&(platform_idx, package_id), v)| (platform_idx, package_id, v))
1097    }
1098}
1099
1100impl<'g> ComputedValue<'g> {
1101    /// Returns both the inner maps along with the build platforms they represent.
1102    pub fn inner_maps(&self) -> [(BuildPlatform, &ComputedInnerMap<'g>); 2] {
1103        [
1104            (BuildPlatform::Target, &self.target_inner),
1105            (BuildPlatform::Host, &self.host_inner),
1106        ]
1107    }
1108
1109    /// Converts `self` into [`ComputedInnerMap`] instances, along with the build platforms they
1110    /// represent.
1111    pub fn into_inner_maps(self) -> [(BuildPlatform, ComputedInnerMap<'g>); 2] {
1112        [
1113            (BuildPlatform::Target, self.target_inner),
1114            (BuildPlatform::Host, self.host_inner),
1115        ]
1116    }
1117
1118    /// Returns a reference to the inner map corresponding to the given build platform.
1119    pub fn get_inner(&self, build_platform: BuildPlatform) -> &ComputedInnerMap<'g> {
1120        match build_platform {
1121            BuildPlatform::Target => &self.target_inner,
1122            BuildPlatform::Host => &self.host_inner,
1123        }
1124    }
1125
1126    /// Returns a mutable reference to the inner map corresponding to the given build platform.
1127    pub fn get_inner_mut(&mut self, build_platform: BuildPlatform) -> &mut ComputedInnerMap<'g> {
1128        match build_platform {
1129            BuildPlatform::Target => &mut self.target_inner,
1130            BuildPlatform::Host => &mut self.host_inner,
1131        }
1132    }
1133
1134    /// Adds all the instances in `other` to `self`.
1135    fn merge(&mut self, other: ComputedValue<'g>) {
1136        for (features, details) in other.target_inner {
1137            self.target_inner
1138                .entry(features)
1139                .or_default()
1140                .extend(details);
1141        }
1142        for (features, details) in other.host_inner {
1143            self.host_inner.entry(features).or_default().extend(details);
1144        }
1145    }
1146
1147    fn contains(&mut self, build_platform: BuildPlatform, features: &BTreeSet<&'g str>) -> bool {
1148        self.get_inner(build_platform).contains_key(features)
1149    }
1150
1151    fn insert(
1152        &mut self,
1153        build_platform: BuildPlatform,
1154        features: BTreeSet<&'g str>,
1155        package: PackageMetadata<'g>,
1156        feature_filter: StandardFeatures,
1157        include_dev: bool,
1158    ) {
1159        self.get_inner_mut(build_platform)
1160            .entry(features)
1161            .or_default()
1162            .push(package, feature_filter, include_dev);
1163    }
1164
1165    fn mark_fixed_up(&mut self, build_platform: BuildPlatform, features: BTreeSet<&'g str>) {
1166        self.get_inner_mut(build_platform)
1167            .entry(features)
1168            .or_default()
1169            .fixed_up = true;
1170    }
1171
1172    fn describe<'a>(&'a self) -> ValueDescribe<'g, 'a> {
1173        match (self.target_inner.len(), self.host_inner.len()) {
1174            (0, 0) => ValueDescribe::None,
1175            (0, 1) => ValueDescribe::SingleHost(&self.host_inner),
1176            (1, 0) => ValueDescribe::SingleTarget(&self.target_inner),
1177            (1, 1) => {
1178                let target_features = self.target_inner.keys().next().expect("1 element");
1179                let host_features = self.host_inner.keys().next().expect("1 element");
1180                if target_features == host_features {
1181                    ValueDescribe::SingleMatchingBoth {
1182                        target_inner: &self.target_inner,
1183                        host_inner: &self.host_inner,
1184                    }
1185                } else {
1186                    ValueDescribe::SingleNonMatchingBoth {
1187                        target_inner: &self.target_inner,
1188                        host_inner: &self.host_inner,
1189                    }
1190                }
1191            }
1192            (_m, 0) => ValueDescribe::MultiTarget(&self.target_inner),
1193            (_m, 1) => ValueDescribe::MultiTargetSingleHost {
1194                target_inner: &self.target_inner,
1195                host_inner: &self.host_inner,
1196            },
1197            (0, _n) => ValueDescribe::MultiHost(&self.host_inner),
1198            (1, _n) => ValueDescribe::MultiHostSingleTarget {
1199                target_inner: &self.target_inner,
1200                host_inner: &self.host_inner,
1201            },
1202            (_m, _n) => ValueDescribe::MultiBoth {
1203                target_inner: &self.target_inner,
1204                host_inner: &self.host_inner,
1205            },
1206        }
1207    }
1208}
1209
1210#[derive(Copy, Clone, Debug)]
1211enum ValueDescribe<'g, 'a> {
1212    None,
1213    SingleTarget(&'a ComputedInnerMap<'g>),
1214    SingleHost(&'a ComputedInnerMap<'g>),
1215    MultiTarget(&'a ComputedInnerMap<'g>),
1216    MultiHost(&'a ComputedInnerMap<'g>),
1217    SingleMatchingBoth {
1218        target_inner: &'a ComputedInnerMap<'g>,
1219        host_inner: &'a ComputedInnerMap<'g>,
1220    },
1221    SingleNonMatchingBoth {
1222        target_inner: &'a ComputedInnerMap<'g>,
1223        host_inner: &'a ComputedInnerMap<'g>,
1224    },
1225    MultiTargetSingleHost {
1226        target_inner: &'a ComputedInnerMap<'g>,
1227        host_inner: &'a ComputedInnerMap<'g>,
1228    },
1229    MultiHostSingleTarget {
1230        target_inner: &'a ComputedInnerMap<'g>,
1231        host_inner: &'a ComputedInnerMap<'g>,
1232    },
1233    MultiBoth {
1234        target_inner: &'a ComputedInnerMap<'g>,
1235        host_inner: &'a ComputedInnerMap<'g>,
1236    },
1237}
1238
1239impl<'g, 'a> ValueDescribe<'g, 'a> {
1240    #[allow(dead_code)]
1241    fn description(self) -> &'static str {
1242        match self {
1243            ValueDescribe::None => "None",
1244            ValueDescribe::SingleTarget(_) => "SingleTarget",
1245            ValueDescribe::SingleHost(_) => "SingleHost",
1246            ValueDescribe::MultiTarget(_) => "MultiTarget",
1247            ValueDescribe::MultiHost(_) => "MultiHost",
1248            ValueDescribe::SingleMatchingBoth { .. } => "SingleMatchingBoth",
1249            ValueDescribe::SingleNonMatchingBoth { .. } => "SingleNonMatchingBoth",
1250            ValueDescribe::MultiTargetSingleHost { .. } => "MultiTargetSingleHost",
1251            ValueDescribe::MultiHostSingleTarget { .. } => "MultiHostSingleTarget",
1252            ValueDescribe::MultiBoth { .. } => "MultiBoth",
1253        }
1254    }
1255
1256    fn insert(
1257        self,
1258        output_single_feature: bool,
1259        unify_target_host: UnifyTargetHostImpl,
1260        mut insert_cb: impl FnMut(BuildPlatform, &'a ComputedInnerMap<'g>),
1261    ) {
1262        use BuildPlatform::*;
1263
1264        match self {
1265            ValueDescribe::None => {
1266                // Empty, ignore. (This should probably never happen anyway.)
1267            }
1268            ValueDescribe::SingleTarget(target_inner) => {
1269                // Just one way to unify these.
1270                if output_single_feature {
1271                    insert_cb(Target, target_inner);
1272                    if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1273                        insert_cb(Host, target_inner);
1274                    }
1275                }
1276            }
1277            ValueDescribe::SingleHost(host_inner) => {
1278                // Just one way to unify other.
1279                if output_single_feature {
1280                    insert_cb(Host, host_inner);
1281                }
1282            }
1283            ValueDescribe::MultiTarget(target_inner) => {
1284                // Unify features for target.
1285                insert_cb(Target, target_inner);
1286                if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1287                    insert_cb(Host, target_inner);
1288                }
1289            }
1290            ValueDescribe::MultiHost(host_inner) => {
1291                // Unify features for host.
1292                insert_cb(Host, host_inner);
1293            }
1294            ValueDescribe::SingleMatchingBoth {
1295                target_inner,
1296                host_inner,
1297            } => {
1298                // Just one way to unify across both.
1299                if output_single_feature {
1300                    insert_cb(Target, target_inner);
1301                    insert_cb(Host, host_inner);
1302                }
1303            }
1304            ValueDescribe::SingleNonMatchingBoth {
1305                target_inner,
1306                host_inner,
1307            } => {
1308                // Unify features for both across both.
1309                insert_cb(Target, target_inner);
1310                insert_cb(Host, host_inner);
1311                if unify_target_host != UnifyTargetHostImpl::None {
1312                    insert_cb(Target, host_inner);
1313                    insert_cb(Host, target_inner);
1314                }
1315            }
1316            ValueDescribe::MultiTargetSingleHost {
1317                target_inner,
1318                host_inner,
1319            } => {
1320                // Unify features for both across both.
1321                insert_cb(Target, target_inner);
1322                insert_cb(Host, host_inner);
1323                if unify_target_host != UnifyTargetHostImpl::None {
1324                    insert_cb(Target, host_inner);
1325                    insert_cb(Host, target_inner);
1326                }
1327            }
1328            ValueDescribe::MultiHostSingleTarget {
1329                target_inner,
1330                host_inner,
1331            } => {
1332                // Unify features for both across both.
1333                insert_cb(Target, target_inner);
1334                insert_cb(Host, host_inner);
1335                if unify_target_host != UnifyTargetHostImpl::None {
1336                    insert_cb(Target, host_inner);
1337                    insert_cb(Host, target_inner);
1338                }
1339            }
1340            ValueDescribe::MultiBoth {
1341                target_inner,
1342                host_inner,
1343            } => {
1344                // Unify features for both across both.
1345                insert_cb(Target, target_inner);
1346                insert_cb(Host, host_inner);
1347                if unify_target_host != UnifyTargetHostImpl::None {
1348                    insert_cb(Target, host_inner);
1349                    insert_cb(Host, target_inner);
1350                }
1351            }
1352        }
1353    }
1354}
1355
1356#[derive(Debug)]
1357struct OutputMapBuild<'g> {
1358    graph: &'g PackageGraph,
1359    output_map: OutputMap<'g>,
1360}
1361
1362impl<'g> OutputMapBuild<'g> {
1363    fn new(graph: &'g PackageGraph) -> Self {
1364        Self {
1365            graph,
1366            output_map: OutputMap::new(),
1367        }
1368    }
1369
1370    fn is_inserted(&self, output_key: OutputKey, package_id: &'g PackageId) -> bool {
1371        match self.output_map.get(&output_key) {
1372            Some(inner_map) => inner_map.contains_key(package_id),
1373            None => false,
1374        }
1375    }
1376
1377    #[allow(dead_code)]
1378    fn get(
1379        &self,
1380        output_key: OutputKey,
1381        package_id: &'g PackageId,
1382    ) -> Option<&(PackageMetadata<'g>, BTreeSet<&'g str>)> {
1383        match self.output_map.get(&output_key) {
1384            Some(inner_map) => inner_map.get(package_id),
1385            None => None,
1386        }
1387    }
1388
1389    fn insert_all<'a>(
1390        &mut self,
1391        values: impl IntoIterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)>,
1392        output_single_feature: bool,
1393        unify_target_host: UnifyTargetHostImpl,
1394    ) where
1395        'g: 'a,
1396    {
1397        for (platform_idx, dep_id, v) in values {
1398            let describe = v.describe();
1399            describe.insert(
1400                output_single_feature,
1401                unify_target_host,
1402                |build_platform, inner| {
1403                    self.insert_inner(platform_idx, build_platform, dep_id, inner);
1404                },
1405            );
1406        }
1407    }
1408
1409    fn insert_inner(
1410        &mut self,
1411        platform_idx: Option<usize>,
1412        build_platform: BuildPlatform,
1413        package_id: &'g PackageId,
1414        inner: &ComputedInnerMap<'g>,
1415    ) {
1416        let output_key = OutputKey {
1417            platform_idx,
1418            build_platform,
1419        };
1420        self.insert(
1421            output_key,
1422            package_id,
1423            inner.keys().flat_map(|f| f.iter().copied()),
1424        )
1425    }
1426
1427    fn insert(
1428        &mut self,
1429        output_key: OutputKey,
1430        package_id: &'g PackageId,
1431        features: impl IntoIterator<Item = &'g str>,
1432    ) {
1433        let map = self.output_map.entry(output_key).or_default();
1434        let graph = self.graph;
1435        let (_, inner) = map.entry(package_id).or_insert_with(|| {
1436            (
1437                graph.metadata(package_id).expect("valid package ID"),
1438                BTreeSet::new(),
1439            )
1440        });
1441        inner.extend(features);
1442    }
1443
1444    fn iter_feature_sets<'a>(&'a self) -> impl Iterator<Item = (OutputKey, FeatureSet<'g>)> + 'a {
1445        self.output_map.iter().map(move |(&output_key, deps)| {
1446            let feature_ids = deps.iter().flat_map(|(&package_id, (_, features))| {
1447                features
1448                    .iter()
1449                    .map(move |&feature| FeatureId::new(package_id, FeatureLabel::Named(feature)))
1450            });
1451            (
1452                output_key,
1453                self.graph
1454                    .feature_graph()
1455                    .resolve_ids(feature_ids)
1456                    .expect("specified feature IDs are valid"),
1457            )
1458        })
1459    }
1460
1461    fn finish(
1462        mut self,
1463        final_excludes: &HashSet<&'g PackageId>,
1464        dep_format: DepFormatVersion,
1465        output_single_feature: bool,
1466    ) -> OutputMap<'g> {
1467        // Remove all features that are already unified in the "always" set.
1468        for &build_platform in BuildPlatform::VALUES {
1469            let always_key = OutputKey {
1470                platform_idx: None,
1471                build_platform,
1472            };
1473
1474            // Temporarily remove the set to avoid &mut issues.
1475            let mut always_map = match self.output_map.remove(&always_key) {
1476                Some(always_map) => always_map,
1477                None => {
1478                    // No features unified for the always set.
1479                    continue;
1480                }
1481            };
1482
1483            if dep_format >= DepFormatVersion::V3 {
1484                Self::filter_root_features(&mut always_map, output_single_feature);
1485            }
1486
1487            for (key, inner_map) in &mut self.output_map {
1488                // Treat the host and target maps as separate.
1489                if key.build_platform != build_platform {
1490                    continue;
1491                }
1492                if dep_format >= DepFormatVersion::V3 {
1493                    Self::filter_root_features(inner_map, output_single_feature);
1494                }
1495
1496                for (package_id, (_always_package, always_features)) in &always_map {
1497                    let (package, remaining_features) = {
1498                        let (package, features) = match inner_map.get(package_id) {
1499                            Some(v) => v,
1500                            None => {
1501                                // The package ID isn't present in the platform-specific map --
1502                                // nothing to be done.
1503                                continue;
1504                            }
1505                        };
1506                        (*package, features - always_features)
1507                    };
1508                    if remaining_features.is_empty() {
1509                        // No features left.
1510                        inner_map.remove(package_id);
1511                    } else {
1512                        inner_map.insert(package_id, (package, remaining_features));
1513                    }
1514                }
1515            }
1516
1517            // Put always_map back into the output map.
1518            self.output_map.insert(always_key, always_map);
1519        }
1520
1521        // Remove final-excludes, and get rid of any maps that are empty.
1522        self.output_map.retain(|_, inner_map| {
1523            for package_id in final_excludes {
1524                inner_map.remove(package_id);
1525            }
1526            !inner_map.is_empty()
1527        });
1528
1529        self.output_map
1530    }
1531
1532    /// Removes all features from the map that aren't at the root of the provided feature graph.
1533    ///
1534    /// Many crates have a notion of public and private features. Private features are not
1535    /// intended to be used by consumers of the crate, and are only used by the crate itself.
1536    ///
1537    /// As a heuristic, we assume that all root features are public.
1538    ///
1539    /// There aren't any platform-related considerations here, because internal feature dependencies
1540    /// aren't platform-specific.
1541    fn filter_root_features(
1542        inner_map: &mut BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>,
1543        output_single_feature: bool,
1544    ) {
1545        inner_map.retain(|_, (package, features)| {
1546            let feature_set = package.to_feature_set(named_feature_filter(
1547                StandardFeatures::None,
1548                features.iter().copied(),
1549            ));
1550
1551            let root_features: BTreeSet<_> = feature_set
1552                .root_ids(DependencyDirection::Forward)
1553                .filter_map(|f| match f.label() {
1554                    FeatureLabel::Named(name) => Some(name),
1555                    FeatureLabel::Base => None,
1556                    FeatureLabel::OptionalDependency(name) => {
1557                        debug_assert!(
1558                            false,
1559                            "root features must be named or base, found optional dependency {}",
1560                            name,
1561                        );
1562                        None
1563                    }
1564                })
1565                .collect();
1566
1567            if root_features.is_empty() {
1568                // No features left -- remove it from the map if output_single_feature is false. If
1569                // it's true, then we might be tracking a feature set that was originally provided
1570                // as empty to us.
1571                output_single_feature && features.is_empty()
1572            } else {
1573                *features = root_features;
1574                true
1575            }
1576        });
1577    }
1578}
1579
1580#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1581enum UnifyTargetHostImpl {
1582    None,
1583    UnifyIfBoth,
1584    ReplicateTargetOnHost,
1585}
1586
1587impl UnifyTargetHost {
1588    fn to_impl(self, graph: &PackageGraph) -> UnifyTargetHostImpl {
1589        match self {
1590            UnifyTargetHost::None => UnifyTargetHostImpl::None,
1591            UnifyTargetHost::UnifyIfBoth => UnifyTargetHostImpl::UnifyIfBoth,
1592            UnifyTargetHost::ReplicateTargetOnHost => UnifyTargetHostImpl::ReplicateTargetOnHost,
1593            UnifyTargetHost::Auto => {
1594                let workspace_set = graph.resolve_workspace();
1595                // Is any package a proc macro?
1596                if workspace_set
1597                    .packages(DependencyDirection::Forward)
1598                    .any(|package| package.is_proc_macro())
1599                {
1600                    return UnifyTargetHostImpl::ReplicateTargetOnHost;
1601                }
1602
1603                // Is any package a build dependency of any other?
1604                if workspace_set
1605                    .links(DependencyDirection::Forward)
1606                    .any(|link| link.build().is_present())
1607                {
1608                    return UnifyTargetHostImpl::ReplicateTargetOnHost;
1609                }
1610
1611                UnifyTargetHostImpl::UnifyIfBoth
1612            }
1613        }
1614    }
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619    use super::*;
1620    use crate::UnifyTargetHost;
1621    use fixtures::json::JsonFixture;
1622
1623    #[test]
1624    fn unify_target_host_auto() {
1625        // Test that this "guppy" fixture (which does not have internal proc macros or build deps)
1626        // turns into "unify if both".
1627        let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_guppy_78cb7e8().graph());
1628        assert_eq!(
1629            res,
1630            UnifyTargetHostImpl::UnifyIfBoth,
1631            "no proc macros => unify if both"
1632        );
1633
1634        // Test that this "libra" fixture (which has internal proc macros) turns into "replicate
1635        // target on host".
1636        let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_libra_9ffd93b().graph());
1637        assert_eq!(
1638            res,
1639            UnifyTargetHostImpl::ReplicateTargetOnHost,
1640            "proc macros => replicate target on host"
1641        );
1642
1643        // Test that the "builddep" fixture (which has an internal build dependency) turns into
1644        // "replicate target on host".
1645        let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_builddep().graph());
1646        assert_eq!(
1647            res,
1648            UnifyTargetHostImpl::ReplicateTargetOnHost,
1649            "internal build deps => replicate target on host"
1650        );
1651    }
1652}