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]
256 pub fn is_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
257 Ok(self.is_traversal_excluded(package_id)? || self.is_final_excluded(package_id)?)
258 }
259
260 pub(crate) fn is_managed_member(&self, package: &PackageMetadata<'g>) -> bool {
273 debug_assert!(
274 std::ptr::eq(package.graph(), *self.graph),
275 "package is from the same graph as this builder"
276 );
277 let Some(hakari_package) = self.hakari_package else {
278 return false;
279 };
280 let is_hakari_package = hakari_package.id() == package.id();
284 package.in_workspace()
285 && !is_hakari_package
286 && !self
287 .is_excluded(package.id())
288 .expect("package is from the same graph as this builder")
289 }
290
291 pub fn add_registries(
296 &mut self,
297 registries: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
298 ) -> &mut Self {
299 self.registries
300 .extend(registries.into_iter().map(|(name, url)| Registry {
301 name: name.into(),
302 url: url.into(),
303 }));
304 self
305 }
306
307 pub fn set_unify_target_host(&mut self, unify_target_host: UnifyTargetHost) -> &mut Self {
312 self.unify_target_host = unify_target_host;
313 self
314 }
315
316 pub fn unify_target_host(&self) -> UnifyTargetHost {
318 self.unify_target_host
319 }
320
321 pub fn set_output_single_feature(&mut self, output_single_feature: bool) -> &mut Self {
329 self.output_single_feature = output_single_feature;
330 self
331 }
332
333 pub fn output_single_feature(&self) -> bool {
335 self.output_single_feature
336 }
337
338 pub fn set_dep_format_version(&mut self, dep_format_version: DepFormatVersion) -> &mut Self {
342 self.dep_format_version = dep_format_version;
343 self
344 }
345
346 pub fn dep_format_version(&self) -> DepFormatVersion {
348 self.dep_format_version
349 }
350
351 pub fn set_workspace_hack_line_style(
355 &mut self,
356 line_style: WorkspaceHackLineStyle,
357 ) -> &mut Self {
358 self.workspace_hack_line_style = line_style;
359 self
360 }
361
362 pub fn workspace_hack_line_style(&self) -> WorkspaceHackLineStyle {
364 self.workspace_hack_line_style
365 }
366
367 pub fn compute(self) -> Hakari<'g> {
369 Hakari::build(self)
370 }
371
372 #[cfg(feature = "cli-support")]
377 pub(crate) fn traversal_excludes_only<'b>(
378 &'b self,
379 ) -> impl Iterator<Item = &'g PackageId> + 'b {
380 self.traversal_excludes.iter().copied()
381 }
382
383 fn make_traversal_excludes<'b>(&'b self) -> TraversalExcludes<'g, 'b> {
384 let hakari_package = if self.verify_mode {
385 None
386 } else {
387 self.hakari_package.map(|package| package.id())
388 };
389
390 TraversalExcludes {
391 excludes: &self.traversal_excludes,
392 hakari_package,
393 }
394 }
395
396 pub(crate) fn make_structural_excludes(&self) -> StructuralExcludes<'g> {
397 let cycle_forming = match &self.hakari_package {
398 Some(hakari_package) => {
399 let roots = std::iter::once(hakari_package.id()).chain(
400 self.graph
401 .workspace()
402 .iter()
403 .filter(|member| self.is_managed_member(member))
404 .map(|member| member.id()),
405 );
406 self.graph
407 .query_reverse(roots)
408 .expect("roots are package IDs from this graph")
409 .resolve_with_fn(|_, link| !link.dev_only())
410 .packages(DependencyDirection::Reverse)
413 .filter(|package| !package.in_workspace())
414 .map(|package| package.id())
415 .collect()
416 }
417 None => BTreeSet::new(),
418 };
419
420 StructuralExcludes { cycle_forming }
421 }
422
423 fn make_features_only<'b>(&'b self) -> FeatureSet<'g> {
424 if self.verify_mode {
425 match &self.hakari_package {
426 Some(package) => package.to_package_set(),
427 None => self.graph.resolve_none(),
428 }
429 .to_feature_set(StandardFeatures::Default)
430 } else {
431 self.graph.feature_graph().resolve_none()
432 }
433 }
434}
435
436#[cfg(feature = "cli-support")]
437mod summaries {
438 use super::*;
439 use crate::summaries::HakariBuilderSummary;
440 use guppy::platform::TargetFeatures;
441
442 impl<'g> HakariBuilder<'g> {
443 pub fn from_summary(
450 graph: &'g PackageGraph,
451 summary: &HakariBuilderSummary,
452 ) -> Result<Self, guppy::Error> {
453 let hakari_package = summary
454 .hakari_package
455 .as_ref()
456 .map(|name| graph.workspace().member_by_name(name))
457 .transpose()?;
458 let platforms = summary
459 .platforms
460 .iter()
461 .map(|triple_str| {
462 let platform = Platform::new(triple_str.clone(), TargetFeatures::Unknown)
463 .map_err(|err| {
464 guppy::Error::TargetSpecError(
465 "while resolving hakari config or summary".to_owned(),
466 err,
467 )
468 })?;
469 Ok(platform.into())
470 })
471 .collect::<Result<Vec<_>, _>>()?;
472
473 let registries: BiHashMap<_, ahash::RandomState> = summary
474 .registries
475 .iter()
476 .map(|(name, url)| Registry {
477 name: name.clone(),
478 url: url.clone(),
479 })
480 .collect();
481
482 let traversal_excludes = summary
483 .traversal_excludes
484 .to_package_set_registry(
485 graph,
486 |name| registries.get1(name).map(|registry| registry.url.as_str()),
487 "resolving hakari traversal-excludes",
488 )?
489 .package_ids(DependencyDirection::Forward)
490 .collect();
491 let final_excludes = summary
492 .final_excludes
493 .to_package_set_registry(
494 graph,
495 |name| registries.get1(name).map(|registry| registry.url.as_str()),
496 "resolving hakari final-excludes",
497 )?
498 .package_ids(DependencyDirection::Forward)
499 .collect();
500
501 Ok(Self {
502 graph: DebugIgnore(graph),
503 hakari_package,
504 resolver: summary.resolver,
505 verify_mode: false,
506 unify_target_host: summary.unify_target_host,
507 output_single_feature: summary.output_single_feature,
508 dep_format_version: summary.dep_format_version,
509 workspace_hack_line_style: summary.workspace_hack_line_style,
510 platforms,
511 registries,
512 traversal_excludes,
513 final_excludes,
514 })
515 }
516 }
517}
518
519#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
544#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
545#[cfg_attr(feature = "cli-support", derive(serde::Serialize, serde::Deserialize))]
546#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
547#[non_exhaustive]
548pub enum UnifyTargetHost {
549 None,
555
556 Auto,
564
565 UnifyIfBoth,
571
572 ReplicateTargetOnHost,
578}
579
580impl Default for UnifyTargetHost {
583 #[inline]
584 fn default() -> Self {
585 UnifyTargetHost::Auto
586 }
587}
588
589#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
593#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
594#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
595#[non_exhaustive]
596#[derive(Default)]
597pub enum DepFormatVersion {
598 #[cfg_attr(feature = "cli-support", serde(rename = "1"))]
602 #[default]
603 V1,
604
605 #[cfg_attr(feature = "cli-support", serde(rename = "2"))]
608 V2,
609
610 #[cfg_attr(feature = "cli-support", serde(rename = "3"))]
612 V3,
613
614 #[cfg_attr(feature = "cli-support", serde(rename = "4"))]
620 V4,
621}
622
623impl DepFormatVersion {
624 #[inline]
626 pub fn latest() -> Self {
627 DepFormatVersion::V4
628 }
629}
630
631impl fmt::Display for DepFormatVersion {
632 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
633 match self {
634 DepFormatVersion::V1 => write!(f, "1"),
635 DepFormatVersion::V2 => write!(f, "2"),
636 DepFormatVersion::V3 => write!(f, "3"),
637 DepFormatVersion::V4 => write!(f, "4"),
638 }
639 }
640}
641
642#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
644#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
645#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
646#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
647#[non_exhaustive]
648#[derive(Default)]
649pub enum WorkspaceHackLineStyle {
650 #[default]
652 Full,
653
654 VersionOnly,
656
657 WorkspaceDotted,
659}
660
661#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
663pub struct OutputKey {
664 pub platform_idx: Option<usize>,
667
668 pub build_platform: BuildPlatform,
670}
671
672#[derive(Clone, Debug)]
678#[non_exhaustive]
679pub struct Hakari<'g> {
680 pub(crate) builder: HakariBuilder<'g>,
681
682 pub output_map: OutputMap<'g>,
687
688 pub computed_map: ComputedMap<'g>,
695
696 structural_excludes: StructuralExcludes<'g>,
697}
698
699impl<'g> Hakari<'g> {
700 pub fn builder(&self) -> &HakariBuilder<'g> {
702 &self.builder
703 }
704
705 pub fn structural_excludes(&self) -> impl Iterator<Item = &'g PackageId> + '_ {
733 self.structural_excludes.cycle_forming.iter().copied()
734 }
735
736 pub fn is_structural_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
744 self.builder.graph().metadata(package_id)?;
745 Ok(self.structural_excludes.cycle_forming.contains(package_id))
746 }
747
748 pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
757 self.builder.read_toml()
758 }
759
760 pub fn write_toml(
764 &self,
765 options: &HakariOutputOptions,
766 out: impl fmt::Write,
767 ) -> Result<(), TomlOutError> {
768 write_toml(
769 &self.builder,
770 &self.output_map,
771 options,
772 self.builder.dep_format_version,
773 out,
774 )
775 }
776
777 pub fn toml_name_map(&self) -> AHashMap<Cow<'g, str>, PackageMetadata<'g>> {
783 toml_name_map(&self.output_map, self.builder.dep_format_version)
784 }
785
786 pub fn explain(
791 &self,
792 package_id: &'g PackageId,
793 ) -> Result<HakariExplain<'g, '_>, guppy::Error> {
794 HakariExplain::new(self, package_id)
795 }
796
797 pub fn to_toml_string(&self, options: &HakariOutputOptions) -> Result<String, TomlOutError> {
802 let mut out = String::new();
803 self.write_toml(options, &mut out)?;
804 Ok(out)
805 }
806
807 fn build(builder: HakariBuilder<'g>) -> Self {
812 let graph = *builder.graph;
813 let mut computed_map_build = ComputedMapBuild::new(&builder);
814 let platform_specs: Vec<_> = builder
815 .platforms
816 .iter()
817 .map(|platform| PlatformSpec::from(platform.clone()))
818 .collect();
819
820 let unify_target_host = builder.unify_target_host.to_impl(graph);
821
822 let mut map_build: OutputMapBuild<'g> = OutputMapBuild::new(graph);
824 map_build.insert_all(
825 computed_map_build.iter(),
826 builder.output_single_feature,
827 unify_target_host,
828 );
829
830 if !builder.output_single_feature {
831 loop {
835 let mut add_extra = HashSet::new();
836 for (output_key, features) in map_build.iter_feature_sets() {
837 let initials_platform = match output_key.build_platform {
838 BuildPlatform::Target => InitialsPlatform::Standard,
839 BuildPlatform::Host => InitialsPlatform::Host,
840 };
841
842 let mut cargo_opts = CargoOptions::new();
843 let platform_spec = match output_key.platform_idx {
844 Some(idx) => platform_specs[idx].clone(),
845 None => PlatformSpec::Always,
846 };
847 cargo_opts
849 .set_include_dev(false)
850 .set_initials_platform(initials_platform)
851 .set_platform(platform_spec)
852 .set_resolver(builder.resolver)
853 .add_omitted_packages(computed_map_build.traversal_excludes.iter());
854 let cargo_set = features
855 .into_cargo_set(&cargo_opts)
856 .expect("into_cargo_set processed successfully");
857
858 for &(build_platform, feature_set) in cargo_set.all_features().iter() {
862 for feature_list in
863 feature_set.packages_with_features(DependencyDirection::Forward)
864 {
865 let dep = feature_list.package();
866 if computed_map_build.structural_excludes.never_unified(dep) {
867 continue;
868 }
869 let dep_id = dep.id();
870 let v_mut = computed_map_build
874 .get_or_insert_mut(output_key.platform_idx, dep_id);
875
876 let new_key = OutputKey {
878 platform_idx: output_key.platform_idx,
879 build_platform,
880 };
881
882 if map_build.is_inserted(new_key, dep_id) {
883 continue;
884 }
885
886 let this_list: BTreeSet<_> = feature_list.named_features().collect();
887
888 let already_present = v_mut.contains(build_platform, &this_list);
889 if !already_present {
890 v_mut.mark_fixed_up(build_platform, this_list);
892 add_extra.insert((output_key.platform_idx, dep_id));
893 }
894 }
895 }
896 }
897
898 if add_extra.is_empty() {
899 break;
900 }
901
902 map_build.insert_all(
903 add_extra.iter().map(|&(platform_idx, dep_id)| {
904 let v = computed_map_build
905 .get(platform_idx, dep_id)
906 .expect("full value should be present");
907 (platform_idx, dep_id, v)
908 }),
909 builder.output_single_feature,
910 unify_target_host,
911 );
912 }
913 }
914
915 let ComputedMapBuild {
916 structural_excludes,
917 computed_map,
918 ..
919 } = computed_map_build;
920 let output_map = map_build.finish(
921 &builder.final_excludes,
922 builder.dep_format_version,
923 builder.output_single_feature,
924 );
925
926 Self {
927 builder,
928 output_map,
929 computed_map,
930 structural_excludes,
931 }
932 }
933}
934
935pub type OutputMap<'g> =
945 BTreeMap<OutputKey, BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>>;
946
947pub type ComputedMap<'g> = BTreeMap<(Option<usize>, &'g PackageId), ComputedValue<'g>>;
958
959#[derive(Clone, Debug, Default)]
965pub struct ComputedValue<'g> {
966 pub target_inner: ComputedInnerMap<'g>,
968
969 pub host_inner: ComputedInnerMap<'g>,
971}
972
973pub type ComputedInnerMap<'g> = BTreeMap<BTreeSet<&'g str>, ComputedInnerValue<'g>>;
978
979#[derive(Clone, Debug, Default)]
981pub struct ComputedInnerValue<'g> {
982 pub workspace_packages: Vec<(PackageMetadata<'g>, StandardFeatures, bool)>,
986
987 pub fixed_up: bool,
989}
990
991impl<'g> ComputedInnerValue<'g> {
992 fn extend(&mut self, other: ComputedInnerValue<'g>) {
993 self.workspace_packages.extend(other.workspace_packages);
994 self.fixed_up |= other.fixed_up;
995 }
996
997 #[inline]
998 fn push(
999 &mut self,
1000 package: PackageMetadata<'g>,
1001 features: StandardFeatures,
1002 include_dev: bool,
1003 ) {
1004 self.workspace_packages
1005 .push((package, features, include_dev));
1006 }
1007}
1008
1009#[derive(Debug)]
1010struct TraversalExcludes<'g, 'b> {
1011 excludes: &'b HashSet<&'g PackageId>,
1012 hakari_package: Option<&'g PackageId>,
1013}
1014
1015impl<'g, 'b> TraversalExcludes<'g, 'b> {
1016 fn iter(&self) -> impl Iterator<Item = &'g PackageId> + 'b + use<'g, 'b> {
1017 self.excludes.iter().copied().chain(self.hakari_package)
1018 }
1019
1020 fn is_excluded(&self, package_id: &PackageId) -> bool {
1021 self.hakari_package == Some(package_id) || self.excludes.contains(package_id)
1022 }
1023}
1024
1025#[derive(Clone, Debug)]
1031pub(crate) struct StructuralExcludes<'g> {
1032 pub(crate) cycle_forming: BTreeSet<&'g PackageId>,
1037}
1038
1039impl<'g> StructuralExcludes<'g> {
1040 fn never_unified(&self, package: &PackageMetadata<'g>) -> bool {
1044 package.in_workspace() || self.cycle_forming.contains(package.id())
1045 }
1046}
1047
1048#[derive(Debug)]
1050struct ComputedMapBuild<'g, 'b> {
1051 traversal_excludes: TraversalExcludes<'g, 'b>,
1052 structural_excludes: StructuralExcludes<'g>,
1053 computed_map: ComputedMap<'g>,
1054}
1055
1056impl<'g, 'b> ComputedMapBuild<'g, 'b> {
1057 fn new(builder: &'b HakariBuilder<'g>) -> Self {
1058 let features_include_dev = [
1089 (StandardFeatures::None, false),
1090 (StandardFeatures::None, true),
1091 (StandardFeatures::Default, false),
1092 (StandardFeatures::Default, true),
1093 (StandardFeatures::All, false),
1094 (StandardFeatures::All, true),
1095 ];
1096
1097 let always_features = features_include_dev
1099 .iter()
1100 .map(|&(features, include_dev)| (None, PlatformSpec::Always, features, include_dev));
1101
1102 let specified_features =
1104 features_include_dev
1105 .iter()
1106 .flat_map(|&(features, include_dev)| {
1107 builder
1108 .platforms
1109 .iter()
1110 .enumerate()
1111 .map(move |(idx, platform)| {
1112 (
1113 Some(idx),
1114 PlatformSpec::from(platform.clone()),
1115 features,
1116 include_dev,
1117 )
1118 })
1119 });
1120 let platforms_features: Vec<_> = always_features.chain(specified_features).collect();
1121
1122 let workspace = builder.graph.workspace();
1123 let traversal_excludes = builder.make_traversal_excludes();
1124 let structural_excludes = builder.make_structural_excludes();
1125 let features_only = builder.make_features_only();
1126 let traversal_excludes_ref = &traversal_excludes;
1127 let structural_excludes_ref = &structural_excludes;
1128 let features_only_ref = &features_only;
1129
1130 let computed_map: ComputedMap<'g> = platforms_features
1131 .into_par_iter()
1132 .flat_map(|(idx, platform_spec, feature_filter, include_dev)| {
1135 let mut cargo_options = CargoOptions::new();
1136 cargo_options
1137 .set_include_dev(include_dev)
1138 .set_resolver(builder.resolver)
1139 .set_platform(platform_spec)
1140 .add_omitted_packages(traversal_excludes.iter());
1141
1142 workspace.par_iter().map(move |workspace_package| {
1143 if traversal_excludes_ref.is_excluded(workspace_package.id()) {
1144 return BTreeMap::new();
1146 }
1147
1148 let initials = workspace_package
1149 .to_package_set()
1150 .to_feature_set(feature_filter);
1151 let cargo_set =
1152 CargoSet::new(initials, features_only_ref.clone(), &cargo_options)
1153 .expect("cargo resolution should succeed");
1154
1155 let all_features = cargo_set.all_features();
1156
1157 let values = all_features.iter().flat_map(|&(build_platform, features)| {
1158 features
1159 .packages_with_features(DependencyDirection::Forward)
1160 .filter_map(move |feature_list| {
1161 let dep = feature_list.package();
1162 if structural_excludes_ref.never_unified(dep) {
1163 return None;
1164 }
1165
1166 let features: BTreeSet<&'g str> =
1167 feature_list.named_features().collect();
1168 Some((
1169 idx,
1170 build_platform,
1171 dep.id(),
1172 features,
1173 workspace_package,
1174 feature_filter,
1175 include_dev,
1176 ))
1177 })
1178 });
1179
1180 let mut map = ComputedMap::new();
1181 for (
1182 platform_idx,
1183 build_platform,
1184 package_id,
1185 features,
1186 package,
1187 feature_filter,
1188 include_dev,
1189 ) in values
1190 {
1191 map.entry((platform_idx, package_id)).or_default().insert(
1193 build_platform,
1194 features,
1195 package,
1196 feature_filter,
1197 include_dev,
1198 );
1199 }
1200
1201 map
1202 })
1203 })
1204 .reduce(ComputedMap::new, |mut acc, map| {
1205 for (k, v) in map {
1207 acc.entry(k).or_default().merge(v);
1208 }
1209 acc
1210 });
1211
1212 Self {
1213 traversal_excludes,
1214 structural_excludes,
1215 computed_map,
1216 }
1217 }
1218
1219 fn get(
1220 &self,
1221 platform_idx: Option<usize>,
1222 package_id: &'g PackageId,
1223 ) -> Option<&ComputedValue<'g>> {
1224 self.computed_map.get(&(platform_idx, package_id))
1225 }
1226
1227 fn get_or_insert_mut(
1228 &mut self,
1229 platform_idx: Option<usize>,
1230 package_id: &'g PackageId,
1231 ) -> &mut ComputedValue<'g> {
1232 self.computed_map
1233 .entry((platform_idx, package_id))
1234 .or_default()
1235 }
1236
1237 fn iter<'a>(
1238 &'a self,
1239 ) -> impl Iterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)> + 'a {
1240 self.computed_map
1241 .iter()
1242 .map(move |(&(platform_idx, package_id), v)| (platform_idx, package_id, v))
1243 }
1244}
1245
1246impl<'g> ComputedValue<'g> {
1247 pub fn inner_maps(&self) -> [(BuildPlatform, &ComputedInnerMap<'g>); 2] {
1249 [
1250 (BuildPlatform::Target, &self.target_inner),
1251 (BuildPlatform::Host, &self.host_inner),
1252 ]
1253 }
1254
1255 pub fn into_inner_maps(self) -> [(BuildPlatform, ComputedInnerMap<'g>); 2] {
1258 [
1259 (BuildPlatform::Target, self.target_inner),
1260 (BuildPlatform::Host, self.host_inner),
1261 ]
1262 }
1263
1264 pub fn get_inner(&self, build_platform: BuildPlatform) -> &ComputedInnerMap<'g> {
1266 match build_platform {
1267 BuildPlatform::Target => &self.target_inner,
1268 BuildPlatform::Host => &self.host_inner,
1269 }
1270 }
1271
1272 pub fn get_inner_mut(&mut self, build_platform: BuildPlatform) -> &mut ComputedInnerMap<'g> {
1274 match build_platform {
1275 BuildPlatform::Target => &mut self.target_inner,
1276 BuildPlatform::Host => &mut self.host_inner,
1277 }
1278 }
1279
1280 fn merge(&mut self, other: ComputedValue<'g>) {
1282 for (features, details) in other.target_inner {
1283 self.target_inner
1284 .entry(features)
1285 .or_default()
1286 .extend(details);
1287 }
1288 for (features, details) in other.host_inner {
1289 self.host_inner.entry(features).or_default().extend(details);
1290 }
1291 }
1292
1293 fn contains(&mut self, build_platform: BuildPlatform, features: &BTreeSet<&'g str>) -> bool {
1294 self.get_inner(build_platform).contains_key(features)
1295 }
1296
1297 fn insert(
1298 &mut self,
1299 build_platform: BuildPlatform,
1300 features: BTreeSet<&'g str>,
1301 package: PackageMetadata<'g>,
1302 feature_filter: StandardFeatures,
1303 include_dev: bool,
1304 ) {
1305 self.get_inner_mut(build_platform)
1306 .entry(features)
1307 .or_default()
1308 .push(package, feature_filter, include_dev);
1309 }
1310
1311 fn mark_fixed_up(&mut self, build_platform: BuildPlatform, features: BTreeSet<&'g str>) {
1312 self.get_inner_mut(build_platform)
1313 .entry(features)
1314 .or_default()
1315 .fixed_up = true;
1316 }
1317
1318 fn describe<'a>(&'a self) -> ValueDescribe<'g, 'a> {
1319 match (self.target_inner.len(), self.host_inner.len()) {
1320 (0, 0) => ValueDescribe::None,
1321 (0, 1) => ValueDescribe::SingleHost(&self.host_inner),
1322 (1, 0) => ValueDescribe::SingleTarget(&self.target_inner),
1323 (1, 1) => {
1324 let target_features = self.target_inner.keys().next().expect("1 element");
1325 let host_features = self.host_inner.keys().next().expect("1 element");
1326 if target_features == host_features {
1327 ValueDescribe::SingleMatchingBoth {
1328 target_inner: &self.target_inner,
1329 host_inner: &self.host_inner,
1330 }
1331 } else {
1332 ValueDescribe::SingleNonMatchingBoth {
1333 target_inner: &self.target_inner,
1334 host_inner: &self.host_inner,
1335 }
1336 }
1337 }
1338 (_m, 0) => ValueDescribe::MultiTarget(&self.target_inner),
1339 (_m, 1) => ValueDescribe::MultiTargetSingleHost {
1340 target_inner: &self.target_inner,
1341 host_inner: &self.host_inner,
1342 },
1343 (0, _n) => ValueDescribe::MultiHost(&self.host_inner),
1344 (1, _n) => ValueDescribe::MultiHostSingleTarget {
1345 target_inner: &self.target_inner,
1346 host_inner: &self.host_inner,
1347 },
1348 (_m, _n) => ValueDescribe::MultiBoth {
1349 target_inner: &self.target_inner,
1350 host_inner: &self.host_inner,
1351 },
1352 }
1353 }
1354}
1355
1356#[derive(Copy, Clone, Debug)]
1357enum ValueDescribe<'g, 'a> {
1358 None,
1359 SingleTarget(&'a ComputedInnerMap<'g>),
1360 SingleHost(&'a ComputedInnerMap<'g>),
1361 MultiTarget(&'a ComputedInnerMap<'g>),
1362 MultiHost(&'a ComputedInnerMap<'g>),
1363 SingleMatchingBoth {
1364 target_inner: &'a ComputedInnerMap<'g>,
1365 host_inner: &'a ComputedInnerMap<'g>,
1366 },
1367 SingleNonMatchingBoth {
1368 target_inner: &'a ComputedInnerMap<'g>,
1369 host_inner: &'a ComputedInnerMap<'g>,
1370 },
1371 MultiTargetSingleHost {
1372 target_inner: &'a ComputedInnerMap<'g>,
1373 host_inner: &'a ComputedInnerMap<'g>,
1374 },
1375 MultiHostSingleTarget {
1376 target_inner: &'a ComputedInnerMap<'g>,
1377 host_inner: &'a ComputedInnerMap<'g>,
1378 },
1379 MultiBoth {
1380 target_inner: &'a ComputedInnerMap<'g>,
1381 host_inner: &'a ComputedInnerMap<'g>,
1382 },
1383}
1384
1385impl<'g, 'a> ValueDescribe<'g, 'a> {
1386 #[allow(dead_code)]
1387 fn description(self) -> &'static str {
1388 match self {
1389 ValueDescribe::None => "None",
1390 ValueDescribe::SingleTarget(_) => "SingleTarget",
1391 ValueDescribe::SingleHost(_) => "SingleHost",
1392 ValueDescribe::MultiTarget(_) => "MultiTarget",
1393 ValueDescribe::MultiHost(_) => "MultiHost",
1394 ValueDescribe::SingleMatchingBoth { .. } => "SingleMatchingBoth",
1395 ValueDescribe::SingleNonMatchingBoth { .. } => "SingleNonMatchingBoth",
1396 ValueDescribe::MultiTargetSingleHost { .. } => "MultiTargetSingleHost",
1397 ValueDescribe::MultiHostSingleTarget { .. } => "MultiHostSingleTarget",
1398 ValueDescribe::MultiBoth { .. } => "MultiBoth",
1399 }
1400 }
1401
1402 fn insert(
1403 self,
1404 output_single_feature: bool,
1405 unify_target_host: UnifyTargetHostImpl,
1406 mut insert_cb: impl FnMut(BuildPlatform, &'a ComputedInnerMap<'g>),
1407 ) {
1408 use BuildPlatform::*;
1409
1410 match self {
1411 ValueDescribe::None => {
1412 }
1414 ValueDescribe::SingleTarget(target_inner) => {
1415 if output_single_feature {
1417 insert_cb(Target, target_inner);
1418 if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1419 insert_cb(Host, target_inner);
1420 }
1421 }
1422 }
1423 ValueDescribe::SingleHost(host_inner) => {
1424 if output_single_feature {
1426 insert_cb(Host, host_inner);
1427 }
1428 }
1429 ValueDescribe::MultiTarget(target_inner) => {
1430 insert_cb(Target, target_inner);
1432 if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1433 insert_cb(Host, target_inner);
1434 }
1435 }
1436 ValueDescribe::MultiHost(host_inner) => {
1437 insert_cb(Host, host_inner);
1439 }
1440 ValueDescribe::SingleMatchingBoth {
1441 target_inner,
1442 host_inner,
1443 } => {
1444 if output_single_feature {
1446 insert_cb(Target, target_inner);
1447 insert_cb(Host, host_inner);
1448 }
1449 }
1450 ValueDescribe::SingleNonMatchingBoth {
1451 target_inner,
1452 host_inner,
1453 } => {
1454 insert_cb(Target, target_inner);
1456 insert_cb(Host, host_inner);
1457 if unify_target_host != UnifyTargetHostImpl::None {
1458 insert_cb(Target, host_inner);
1459 insert_cb(Host, target_inner);
1460 }
1461 }
1462 ValueDescribe::MultiTargetSingleHost {
1463 target_inner,
1464 host_inner,
1465 } => {
1466 insert_cb(Target, target_inner);
1468 insert_cb(Host, host_inner);
1469 if unify_target_host != UnifyTargetHostImpl::None {
1470 insert_cb(Target, host_inner);
1471 insert_cb(Host, target_inner);
1472 }
1473 }
1474 ValueDescribe::MultiHostSingleTarget {
1475 target_inner,
1476 host_inner,
1477 } => {
1478 insert_cb(Target, target_inner);
1480 insert_cb(Host, host_inner);
1481 if unify_target_host != UnifyTargetHostImpl::None {
1482 insert_cb(Target, host_inner);
1483 insert_cb(Host, target_inner);
1484 }
1485 }
1486 ValueDescribe::MultiBoth {
1487 target_inner,
1488 host_inner,
1489 } => {
1490 insert_cb(Target, target_inner);
1492 insert_cb(Host, host_inner);
1493 if unify_target_host != UnifyTargetHostImpl::None {
1494 insert_cb(Target, host_inner);
1495 insert_cb(Host, target_inner);
1496 }
1497 }
1498 }
1499 }
1500}
1501
1502#[derive(Debug)]
1503struct OutputMapBuild<'g> {
1504 graph: &'g PackageGraph,
1505 output_map: OutputMap<'g>,
1506}
1507
1508impl<'g> OutputMapBuild<'g> {
1509 fn new(graph: &'g PackageGraph) -> Self {
1510 Self {
1511 graph,
1512 output_map: OutputMap::new(),
1513 }
1514 }
1515
1516 fn is_inserted(&self, output_key: OutputKey, package_id: &'g PackageId) -> bool {
1517 match self.output_map.get(&output_key) {
1518 Some(inner_map) => inner_map.contains_key(package_id),
1519 None => false,
1520 }
1521 }
1522
1523 #[allow(dead_code)]
1524 fn get(
1525 &self,
1526 output_key: OutputKey,
1527 package_id: &'g PackageId,
1528 ) -> Option<&(PackageMetadata<'g>, BTreeSet<&'g str>)> {
1529 match self.output_map.get(&output_key) {
1530 Some(inner_map) => inner_map.get(package_id),
1531 None => None,
1532 }
1533 }
1534
1535 fn insert_all<'a>(
1536 &mut self,
1537 values: impl IntoIterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)>,
1538 output_single_feature: bool,
1539 unify_target_host: UnifyTargetHostImpl,
1540 ) where
1541 'g: 'a,
1542 {
1543 for (platform_idx, dep_id, v) in values {
1544 let describe = v.describe();
1545 describe.insert(
1546 output_single_feature,
1547 unify_target_host,
1548 |build_platform, inner| {
1549 self.insert_inner(platform_idx, build_platform, dep_id, inner);
1550 },
1551 );
1552 }
1553 }
1554
1555 fn insert_inner(
1556 &mut self,
1557 platform_idx: Option<usize>,
1558 build_platform: BuildPlatform,
1559 package_id: &'g PackageId,
1560 inner: &ComputedInnerMap<'g>,
1561 ) {
1562 let output_key = OutputKey {
1563 platform_idx,
1564 build_platform,
1565 };
1566 self.insert(
1567 output_key,
1568 package_id,
1569 inner.keys().flat_map(|f| f.iter().copied()),
1570 )
1571 }
1572
1573 fn insert(
1574 &mut self,
1575 output_key: OutputKey,
1576 package_id: &'g PackageId,
1577 features: impl IntoIterator<Item = &'g str>,
1578 ) {
1579 let map = self.output_map.entry(output_key).or_default();
1580 let graph = self.graph;
1581 let (_, inner) = map.entry(package_id).or_insert_with(|| {
1582 (
1583 graph.metadata(package_id).expect("valid package ID"),
1584 BTreeSet::new(),
1585 )
1586 });
1587 inner.extend(features);
1588 }
1589
1590 fn iter_feature_sets<'a>(&'a self) -> impl Iterator<Item = (OutputKey, FeatureSet<'g>)> + 'a {
1591 self.output_map.iter().map(move |(&output_key, deps)| {
1592 let feature_ids = deps.iter().flat_map(|(&package_id, (_, features))| {
1593 features
1594 .iter()
1595 .map(move |&feature| FeatureId::new(package_id, FeatureLabel::Named(feature)))
1596 });
1597 (
1598 output_key,
1599 self.graph
1600 .feature_graph()
1601 .resolve_ids(feature_ids)
1602 .expect("specified feature IDs are valid"),
1603 )
1604 })
1605 }
1606
1607 fn finish(
1608 mut self,
1609 final_excludes: &HashSet<&'g PackageId>,
1610 dep_format: DepFormatVersion,
1611 output_single_feature: bool,
1612 ) -> OutputMap<'g> {
1613 for &build_platform in BuildPlatform::VALUES {
1615 let always_key = OutputKey {
1616 platform_idx: None,
1617 build_platform,
1618 };
1619
1620 let mut always_map = match self.output_map.remove(&always_key) {
1622 Some(always_map) => always_map,
1623 None => {
1624 continue;
1626 }
1627 };
1628
1629 if dep_format >= DepFormatVersion::V3 {
1630 Self::filter_root_features(&mut always_map, output_single_feature);
1631 }
1632
1633 for (key, inner_map) in &mut self.output_map {
1634 if key.build_platform != build_platform {
1636 continue;
1637 }
1638 if dep_format >= DepFormatVersion::V3 {
1639 Self::filter_root_features(inner_map, output_single_feature);
1640 }
1641
1642 for (package_id, (_always_package, always_features)) in &always_map {
1643 let (package, remaining_features) = {
1644 let (package, features) = match inner_map.get(package_id) {
1645 Some(v) => v,
1646 None => {
1647 continue;
1650 }
1651 };
1652 (*package, features - always_features)
1653 };
1654 if remaining_features.is_empty() {
1655 inner_map.remove(package_id);
1657 } else {
1658 inner_map.insert(package_id, (package, remaining_features));
1659 }
1660 }
1661 }
1662
1663 self.output_map.insert(always_key, always_map);
1665 }
1666
1667 self.output_map.retain(|_, inner_map| {
1669 for package_id in final_excludes {
1670 inner_map.remove(package_id);
1671 }
1672 !inner_map.is_empty()
1673 });
1674
1675 self.output_map
1676 }
1677
1678 fn filter_root_features(
1688 inner_map: &mut BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>,
1689 output_single_feature: bool,
1690 ) {
1691 inner_map.retain(|_, (package, features)| {
1692 let feature_set = package.to_feature_set(named_feature_filter(
1693 StandardFeatures::None,
1694 features.iter().copied(),
1695 ));
1696
1697 let root_features: BTreeSet<_> = feature_set
1698 .root_ids(DependencyDirection::Forward)
1699 .filter_map(|f| match f.label() {
1700 FeatureLabel::Named(name) => Some(name),
1701 FeatureLabel::Base => None,
1702 FeatureLabel::OptionalDependency(name) => {
1703 debug_assert!(
1704 false,
1705 "root features must be named or base, found optional dependency {name}",
1706 );
1707 None
1708 }
1709 })
1710 .collect();
1711
1712 if root_features.is_empty() {
1713 output_single_feature && features.is_empty()
1717 } else {
1718 *features = root_features;
1719 true
1720 }
1721 });
1722 }
1723}
1724
1725#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1726enum UnifyTargetHostImpl {
1727 None,
1728 UnifyIfBoth,
1729 ReplicateTargetOnHost,
1730}
1731
1732impl UnifyTargetHost {
1733 fn to_impl(self, graph: &PackageGraph) -> UnifyTargetHostImpl {
1734 match self {
1735 UnifyTargetHost::None => UnifyTargetHostImpl::None,
1736 UnifyTargetHost::UnifyIfBoth => UnifyTargetHostImpl::UnifyIfBoth,
1737 UnifyTargetHost::ReplicateTargetOnHost => UnifyTargetHostImpl::ReplicateTargetOnHost,
1738 UnifyTargetHost::Auto => {
1739 let workspace_set = graph.resolve_workspace();
1740 if workspace_set
1742 .packages(DependencyDirection::Forward)
1743 .any(|package| package.is_proc_macro())
1744 {
1745 return UnifyTargetHostImpl::ReplicateTargetOnHost;
1746 }
1747
1748 if workspace_set
1750 .links(DependencyDirection::Forward)
1751 .any(|link| link.build().is_present())
1752 {
1753 return UnifyTargetHostImpl::ReplicateTargetOnHost;
1754 }
1755
1756 UnifyTargetHostImpl::UnifyIfBoth
1757 }
1758 }
1759 }
1760}
1761
1762#[cfg(test)]
1763mod tests {
1764 use super::*;
1765 use crate::UnifyTargetHost;
1766 use fixtures::json::*;
1767
1768 #[test]
1769 fn unify_target_host_auto() {
1770 let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_guppy_78cb7e8().graph());
1773 assert_eq!(
1774 res,
1775 UnifyTargetHostImpl::UnifyIfBoth,
1776 "no proc macros => unify if both"
1777 );
1778
1779 let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_libra_9ffd93b().graph());
1782 assert_eq!(
1783 res,
1784 UnifyTargetHostImpl::ReplicateTargetOnHost,
1785 "proc macros => replicate target on host"
1786 );
1787
1788 let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_builddep().graph());
1791 assert_eq!(
1792 res,
1793 UnifyTargetHostImpl::ReplicateTargetOnHost,
1794 "internal build deps => replicate target on host"
1795 );
1796 }
1797
1798 #[test]
1799 fn fixpoint_never_adds_workspace_packages() {
1800 let mut builder = reverse_dep_builder();
1808 let graph = builder.graph();
1809 builder.set_output_single_feature(false);
1812
1813 let member_dev_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV);
1814 let member_unlinked_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_UNLINKED);
1815 builder
1816 .add_final_excludes([&member_dev_id, &member_unlinked_id])
1817 .expect("final excludes are known to the graph");
1818 let hakari = builder.compute();
1819
1820 let target_key = OutputKey {
1824 platform_idx: None,
1825 build_platform: BuildPlatform::Target,
1826 };
1827 let output_ids: BTreeSet<&PackageId> =
1828 hakari.output_map[&target_key].keys().copied().collect();
1829 for bridge_id in [
1830 METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_DEV,
1831 METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_UNLINKED,
1832 ] {
1833 assert!(
1834 output_ids.contains(&PackageId::new(bridge_id)),
1835 "{bridge_id} depends on a workspace member and is in the output \
1836 map, so the fixpoint loop reaches that member"
1837 );
1838 }
1839
1840 for &(platform_idx, package_id) in hakari.computed_map.keys() {
1841 let package = graph.metadata(package_id).expect("package is in the graph");
1842 assert!(
1843 !package.in_workspace(),
1844 "the computed map only has third-party packages, but {} \
1845 is a workspace member (platform_idx: {platform_idx:?})",
1846 package.name()
1847 );
1848 }
1849
1850 for (output_key, inner_map) in &hakari.output_map {
1851 for (package, _) in inner_map.values() {
1852 assert!(
1853 !package.in_workspace(),
1854 "the output map only has third-party packages, but {} \
1855 is a workspace member (output key: {output_key:?})",
1856 package.name()
1857 );
1858 }
1859 }
1860 }
1861
1862 #[test]
1863 fn hakari_reverse_deps_not_unified() {
1864 let builder = reverse_dep_builder();
1865 let graph = builder.graph();
1866 let leaf_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_LEAF);
1867 let hakari = builder.compute();
1868
1869 let expected_excludes = expected_structural_excludes();
1870 let structural_excludes: BTreeSet<PackageId> =
1871 hakari.structural_excludes().cloned().collect();
1872 assert_eq!(
1873 structural_excludes, expected_excludes,
1874 "structural excludes are exactly the third-party packages that would form a cycle"
1875 );
1876 for package_id in &expected_excludes {
1877 assert!(
1878 hakari
1879 .is_structural_excluded(package_id)
1880 .expect("package ID is known"),
1881 "{package_id} is structurally excluded"
1882 );
1883 }
1884 assert!(
1885 !hakari
1886 .is_structural_excluded(&leaf_id)
1887 .expect("package ID is known"),
1888 "{leaf_id} is not structurally excluded"
1889 );
1890 for package in graph.workspace().iter() {
1891 assert!(
1892 !hakari
1893 .is_structural_excluded(package.id())
1894 .expect("package ID is known"),
1895 "workspace package {} is not structurally excluded (workspace packages are \
1896 handled separately)",
1897 package.name()
1898 );
1899 }
1900 assert!(
1901 hakari
1902 .is_structural_excluded(&PackageId::new("unknown-package 0.1.0"))
1903 .is_err(),
1904 "unknown package IDs are an error"
1905 );
1906
1907 let computed_ids: BTreeSet<&PackageId> = hakari
1908 .computed_map
1909 .keys()
1910 .map(|(_, package_id)| *package_id)
1911 .collect();
1912 for package_id in &expected_excludes {
1913 assert!(
1914 !computed_ids.contains(package_id),
1915 "{package_id} is structurally excluded so it is never computed: {computed_ids:?}"
1916 );
1917 }
1918 for package in graph.workspace().iter() {
1919 assert!(
1920 !computed_ids.contains(package.id()),
1921 "workspace package {} is never computed",
1922 package.name()
1923 );
1924 }
1925
1926 let target_key = OutputKey {
1927 platform_idx: None,
1928 build_platform: BuildPlatform::Target,
1929 };
1930 let host_key = OutputKey {
1931 platform_idx: None,
1932 build_platform: BuildPlatform::Host,
1933 };
1934 let output_keys: BTreeSet<OutputKey> = hakari.output_map.keys().copied().collect();
1935 assert_eq!(
1936 output_keys,
1937 [target_key, host_key].into_iter().collect(),
1938 "hrd-member-build's build-dependency on the hakari package makes \
1939 UnifyTargetHost::Auto resolve to replicate-target-on-host, so leaf's target \
1940 entry is replicated under the always/host key alongside always/target"
1941 );
1942 for key in [target_key, host_key] {
1943 let map = &hakari.output_map[&key];
1944 let output_ids: BTreeSet<&PackageId> = map.keys().copied().collect();
1945 let expected_ids: BTreeSet<&PackageId> = [&leaf_id].into_iter().collect();
1946 assert_eq!(
1947 output_ids, expected_ids,
1948 "[{key:?}] only leaf is unified: its features differ across workspace \
1949 members, and every other third-party package would form a cycle"
1950 );
1951 assert_eq!(
1952 map[&leaf_id].1,
1953 ["feat1", "feat2"].into_iter().collect::<BTreeSet<_>>(),
1954 "[{key:?}] leaf features unified"
1955 );
1956 }
1957
1958 if let Err(errs) = hakari.builder().clone().verify() {
1960 panic!("verify failed for packages: {:?}", errs.dependency_ids);
1961 }
1962 }
1963
1964 #[test]
1965 fn unmanaged_members_are_not_cycle_roots() {
1966 let member_dev_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_DEV);
1967 let member_unlinked_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_MEMBER_UNLINKED);
1968
1969 let mut final_excluded = reverse_dep_builder();
1970 final_excluded
1971 .add_final_excludes([&member_dev_id, &member_unlinked_id])
1972 .expect("final excludes are known to the graph");
1973 assert_unmanaged_members_unified(final_excluded, "final-excluded");
1974
1975 let mut traversal_excluded = reverse_dep_builder();
1976 traversal_excluded
1977 .add_traversal_excludes([&member_dev_id, &member_unlinked_id])
1978 .expect("traversal excludes are known to the graph");
1979 assert_unmanaged_members_unified(traversal_excluded, "traversal-excluded");
1980 }
1981
1982 fn assert_unmanaged_members_unified(builder: HakariBuilder<'_>, scenario: &str) {
1983 let via_member_published_id =
1984 PackageId::new(METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_PUBLISHED);
1985 let via_member_dev_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_DEV);
1986 let via_member_unlinked_id =
1987 PackageId::new(METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_UNLINKED);
1988 let leaf_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_LEAF);
1989 let hakari = builder.compute();
1990
1991 for package_id in [&via_member_dev_id, &via_member_unlinked_id] {
1995 assert!(
1996 !hakari
1997 .is_structural_excluded(package_id)
1998 .expect("package ID is known"),
1999 "[{scenario}] {package_id} only reaches unmanaged members, so it is \
2000 not structurally excluded"
2001 );
2002 }
2003 assert!(
2006 hakari
2007 .is_structural_excluded(&via_member_published_id)
2008 .expect("package ID is known"),
2009 "[{scenario}] {via_member_published_id} reaches managed member hrd-member-published, so it \
2010 is structurally excluded"
2011 );
2012
2013 let target_key = OutputKey {
2014 platform_idx: None,
2015 build_platform: BuildPlatform::Target,
2016 };
2017 let target_map = &hakari.output_map[&target_key];
2018 let output_ids: BTreeSet<&PackageId> = target_map.keys().copied().collect();
2019 let expected_ids: BTreeSet<&PackageId> =
2020 [&leaf_id, &via_member_dev_id, &via_member_unlinked_id]
2021 .into_iter()
2022 .collect();
2023 assert_eq!(
2024 output_ids, expected_ids,
2025 "[{scenario}] via-member-dev and via-member-unlinked are unified now that they can't \
2026 form a cycle"
2027 );
2028 }
2029
2030 #[test]
2031 fn hakari_reverse_deps_verify_failure() {
2032 let fixture = JsonFixture::metadata_hakari_reverse_dep();
2033 let hakari_id = fixture
2034 .details()
2035 .hakari_package()
2036 .expect("fixture declares a hakari package");
2037
2038 let leaf_dep_unified = r#""name":"hrd-leaf","source":null,"req":"*","kind":null,"rename":null,"optional":false,"uses_default_features":true,"features":["feat1","feat2"]"#;
2041 let leaf_dep_stale = leaf_dep_unified.replace(r#"["feat1","feat2"]"#, "[]");
2042 let json = fixture.json();
2043 assert_eq!(
2044 json.matches(leaf_dep_unified).count(),
2045 1,
2046 "hakari package's dependency on hrd-leaf occurs exactly once"
2047 );
2048 let stale_json = json.replace(leaf_dep_unified, &leaf_dep_stale);
2049 let graph = PackageGraph::from_json(stale_json).expect("stale fixture parsed");
2050
2051 let builder = HakariBuilder::new(&graph, Some(hakari_id)).expect("builder created");
2052 let structural_excludes: BTreeSet<PackageId> = builder
2053 .clone()
2054 .compute()
2055 .structural_excludes()
2056 .cloned()
2057 .collect();
2058 let errs = builder
2059 .verify()
2060 .expect_err("stale hakari package fails verification");
2061
2062 let leaf_id = PackageId::new(METADATA_HAKARI_REVERSE_DEP_LEAF);
2063 assert_eq!(
2064 errs.dependency_ids,
2065 [&leaf_id].into_iter().collect::<BTreeSet<_>>(),
2066 "only hrd-leaf is reported: the structurally excluded packages are \
2067 built with more than one feature set too, but verify skips them"
2068 );
2069 assert_eq!(
2070 structural_excludes,
2071 expected_structural_excludes(),
2072 "the cycle-forming packages really are structurally excluded here, so the check \
2073 above isn't tautological"
2074 );
2075 }
2076
2077 fn expected_structural_excludes() -> BTreeSet<PackageId> {
2081 [
2082 METADATA_HAKARI_REVERSE_DEP_NORMAL_ON_HACK,
2083 METADATA_HAKARI_REVERSE_DEP_VIA_NORMAL_ON_HACK,
2084 METADATA_HAKARI_REVERSE_DEP_BUILD_ON_HACK,
2085 METADATA_HAKARI_REVERSE_DEP_CFG_ON_HACK,
2086 METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_PUBLISHED,
2087 METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_DEV,
2088 METADATA_HAKARI_REVERSE_DEP_VIA_MEMBER_UNLINKED,
2089 ]
2090 .into_iter()
2091 .map(PackageId::new)
2092 .collect()
2093 }
2094
2095 fn reverse_dep_builder() -> HakariBuilder<'static> {
2096 let fixture = JsonFixture::metadata_hakari_reverse_dep();
2097 let hakari_id = fixture
2098 .details()
2099 .hakari_package()
2100 .expect("hakari-reverse-dep fixture names a hakari package");
2101 HakariBuilder::new(fixture.graph(), Some(hakari_id)).expect("hakari builder is created")
2102 }
2103}