1use 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#[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 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 pub fn graph(&self) -> &'g PackageGraph {
93 #[allow(clippy::explicit_auto_deref)]
95 *self.graph
96 }
97
98 pub fn hakari_package(&self) -> Option<&PackageMetadata<'g>> {
100 self.hakari_package.as_ref()
101 }
102
103 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 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 pub fn platforms(&self) -> impl ExactSizeIterator<Item = &str> + '_ {
151 self.platforms.iter().map(|platform| platform.triple_str())
152 }
153
154 pub fn set_resolver(&mut self, resolver: CargoResolverVersion) -> &mut Self {
160 self.resolver = resolver;
161 self
162 }
163
164 pub fn resolver(&self) -> CargoResolverVersion {
166 self.resolver
167 }
168
169 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 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 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 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 pub fn final_excludes<'b>(&'b self) -> impl Iterator<Item = &'g PackageId> + 'b {
236 self.final_excludes.iter().copied()
237 }
238
239 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 #[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 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 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 pub fn unify_target_host(&self) -> UnifyTargetHost {
285 self.unify_target_host
286 }
287
288 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 pub fn output_single_feature(&self) -> bool {
302 self.output_single_feature
303 }
304
305 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 pub fn dep_format_version(&self) -> DepFormatVersion {
315 self.dep_format_version
316 }
317
318 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 pub fn workspace_hack_line_style(&self) -> WorkspaceHackLineStyle {
331 self.workspace_hack_line_style
332 }
333
334 pub fn compute(self) -> Hakari<'g> {
336 Hakari::build(self)
337 }
338
339 #[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 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#[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 None,
495
496 Auto,
504
505 UnifyIfBoth,
511
512 ReplicateTargetOnHost,
518}
519
520impl Default for UnifyTargetHost {
523 #[inline]
524 fn default() -> Self {
525 UnifyTargetHost::Auto
526 }
527}
528
529#[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 #[cfg_attr(feature = "cli-support", serde(rename = "1"))]
542 #[default]
543 V1,
544
545 #[cfg_attr(feature = "cli-support", serde(rename = "2"))]
548 V2,
549
550 #[cfg_attr(feature = "cli-support", serde(rename = "3"))]
552 V3,
553
554 #[cfg_attr(feature = "cli-support", serde(rename = "4"))]
560 V4,
561}
562
563impl DepFormatVersion {
564 #[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#[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 #[default]
592 Full,
593
594 VersionOnly,
596
597 WorkspaceDotted,
599}
600
601#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
603pub struct OutputKey {
604 pub platform_idx: Option<usize>,
607
608 pub build_platform: BuildPlatform,
610}
611
612#[derive(Clone, Debug)]
618#[non_exhaustive]
619pub struct Hakari<'g> {
620 pub(crate) builder: HakariBuilder<'g>,
621
622 pub output_map: OutputMap<'g>,
627
628 pub computed_map: ComputedMap<'g>,
632}
633
634impl<'g> Hakari<'g> {
635 pub fn builder(&self) -> &HakariBuilder<'g> {
637 &self.builder
638 }
639
640 pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
649 self.builder.read_toml()
650 }
651
652 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 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 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 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 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 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 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 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 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 let v_mut = computed_map_build
763 .get_or_insert_mut(output_key.platform_idx, dep_id);
764
765 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 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
819pub type OutputMap<'g> =
829 BTreeMap<OutputKey, BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>>;
830
831pub type ComputedMap<'g> = BTreeMap<(Option<usize>, &'g PackageId), ComputedValue<'g>>;
842
843#[derive(Clone, Debug, Default)]
849pub struct ComputedValue<'g> {
850 pub target_inner: ComputedInnerMap<'g>,
852
853 pub host_inner: ComputedInnerMap<'g>,
855}
856
857pub type ComputedInnerMap<'g> = BTreeMap<BTreeSet<&'g str>, ComputedInnerValue<'g>>;
862
863#[derive(Clone, Debug, Default)]
865pub struct ComputedInnerValue<'g> {
866 pub workspace_packages: Vec<(PackageMetadata<'g>, StandardFeatures, bool)>,
870
871 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#[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 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 let always_features = features_include_dev
959 .iter()
960 .map(|&(features, include_dev)| (None, PlatformSpec::Always, features, include_dev));
961
962 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 .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 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 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 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 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 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 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 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 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 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 }
1272 ValueDescribe::SingleTarget(target_inner) => {
1273 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 if output_single_feature {
1284 insert_cb(Host, host_inner);
1285 }
1286 }
1287 ValueDescribe::MultiTarget(target_inner) => {
1288 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 insert_cb(Host, host_inner);
1297 }
1298 ValueDescribe::SingleMatchingBoth {
1299 target_inner,
1300 host_inner,
1301 } => {
1302 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 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 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 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 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 for &build_platform in BuildPlatform::VALUES {
1473 let always_key = OutputKey {
1474 platform_idx: None,
1475 build_platform,
1476 };
1477
1478 let mut always_map = match self.output_map.remove(&always_key) {
1480 Some(always_map) => always_map,
1481 None => {
1482 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 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 continue;
1508 }
1509 };
1510 (*package, features - always_features)
1511 };
1512 if remaining_features.is_empty() {
1513 inner_map.remove(package_id);
1515 } else {
1516 inner_map.insert(package_id, (package, remaining_features));
1517 }
1518 }
1519 }
1520
1521 self.output_map.insert(always_key, always_map);
1523 }
1524
1525 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 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 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 if workspace_set
1601 .packages(DependencyDirection::Forward)
1602 .any(|package| package.is_proc_macro())
1603 {
1604 return UnifyTargetHostImpl::ReplicateTargetOnHost;
1605 }
1606
1607 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 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 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 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}