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