hakari/
hakari.rs

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