1use crate::{
5 explain::HakariExplain,
6 toml_name_map,
7 toml_out::{write_toml, HakariOutputOptions},
8 CargoTomlError, HakariCargoToml, TomlOutError,
9};
10use ahash::AHashMap;
11use bimap::BiHashMap;
12use debug_ignore::DebugIgnore;
13use guppy::{
14 errors::TargetSpecError,
15 graph::{
16 cargo::{BuildPlatform, CargoOptions, CargoResolverVersion, CargoSet, InitialsPlatform},
17 feature::{named_feature_filter, FeatureId, FeatureLabel, FeatureSet, StandardFeatures},
18 DependencyDirection, PackageGraph, PackageMetadata,
19 },
20 platform::{Platform, PlatformSpec, TargetFeatures},
21 PackageId,
22};
23use rayon::prelude::*;
24use std::{
25 borrow::Cow,
26 collections::{BTreeMap, BTreeSet, HashSet},
27 fmt,
28 sync::Arc,
29};
30
31#[derive(Clone, Debug)]
35pub struct HakariBuilder<'g> {
36 graph: DebugIgnore<&'g PackageGraph>,
37 hakari_package: Option<PackageMetadata<'g>>,
38 pub(crate) platforms: Vec<Arc<Platform>>,
39 resolver: CargoResolverVersion,
40 pub(crate) verify_mode: bool,
41 pub(crate) traversal_excludes: HashSet<&'g PackageId>,
42 final_excludes: HashSet<&'g PackageId>,
43 pub(crate) registries: BiHashMap<String, String, ahash::RandomState, ahash::RandomState>,
44 unify_target_host: UnifyTargetHost,
45 output_single_feature: bool,
46 pub(crate) dep_format_version: DepFormatVersion,
47 pub(crate) workspace_hack_line_style: WorkspaceHackLineStyle,
48}
49
50impl<'g> HakariBuilder<'g> {
51 pub fn new(
59 graph: &'g PackageGraph,
60 hakari_id: Option<&PackageId>,
61 ) -> Result<Self, guppy::Error> {
62 let hakari_package = hakari_id
63 .map(|package_id| {
64 let package = graph.metadata(package_id)?;
65 if !package.in_workspace() {
66 return Err(guppy::Error::UnknownWorkspaceName(
67 package.name().to_string(),
68 ));
69 }
70 Ok(package)
71 })
72 .transpose()?;
73
74 Ok(Self {
75 graph: DebugIgnore(graph),
76 hakari_package,
77 platforms: vec![],
78 resolver: CargoResolverVersion::V2,
79 verify_mode: false,
80 traversal_excludes: HashSet::new(),
81 final_excludes: HashSet::new(),
82 registries: BiHashMap::with_hashers(Default::default(), Default::default()),
83 unify_target_host: UnifyTargetHost::default(),
84 output_single_feature: false,
85 dep_format_version: DepFormatVersion::default(),
86 workspace_hack_line_style: WorkspaceHackLineStyle::default(),
87 })
88 }
89
90 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]
253 pub fn is_excluded(&self, package_id: &PackageId) -> Result<bool, guppy::Error> {
254 Ok(self.is_traversal_excluded(package_id)? || self.is_final_excluded(package_id)?)
255 }
256
257 pub fn add_registries(
262 &mut self,
263 registries: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
264 ) -> &mut Self {
265 self.registries.extend(
266 registries
267 .into_iter()
268 .map(|(name, url)| (name.into(), url.into())),
269 );
270 self
271 }
272
273 pub fn set_unify_target_host(&mut self, unify_target_host: UnifyTargetHost) -> &mut Self {
278 self.unify_target_host = unify_target_host;
279 self
280 }
281
282 pub fn unify_target_host(&self) -> UnifyTargetHost {
284 self.unify_target_host
285 }
286
287 pub fn set_output_single_feature(&mut self, output_single_feature: bool) -> &mut Self {
295 self.output_single_feature = output_single_feature;
296 self
297 }
298
299 pub fn output_single_feature(&self) -> bool {
301 self.output_single_feature
302 }
303
304 pub fn set_dep_format_version(&mut self, dep_format_version: DepFormatVersion) -> &mut Self {
308 self.dep_format_version = dep_format_version;
309 self
310 }
311
312 pub fn dep_format_version(&self) -> DepFormatVersion {
314 self.dep_format_version
315 }
316
317 pub fn set_workspace_hack_line_style(
321 &mut self,
322 line_style: WorkspaceHackLineStyle,
323 ) -> &mut Self {
324 self.workspace_hack_line_style = line_style;
325 self
326 }
327
328 pub fn workspace_hack_line_style(&self) -> WorkspaceHackLineStyle {
330 self.workspace_hack_line_style
331 }
332
333 pub fn compute(self) -> Hakari<'g> {
335 Hakari::build(self)
336 }
337
338 #[cfg(feature = "cli-support")]
343 pub(crate) fn traversal_excludes_only<'b>(
344 &'b self,
345 ) -> impl Iterator<Item = &'g PackageId> + 'b {
346 self.traversal_excludes.iter().copied()
347 }
348
349 fn make_traversal_excludes<'b>(&'b self) -> TraversalExcludes<'g, 'b> {
350 let hakari_package = if self.verify_mode {
351 None
352 } else {
353 self.hakari_package.map(|package| package.id())
354 };
355
356 TraversalExcludes {
357 excludes: &self.traversal_excludes,
358 hakari_package,
359 }
360 }
361
362 fn make_features_only<'b>(&'b self) -> FeatureSet<'g> {
363 if self.verify_mode {
364 match &self.hakari_package {
365 Some(package) => package.to_package_set(),
366 None => self.graph.resolve_none(),
367 }
368 .to_feature_set(StandardFeatures::Default)
369 } else {
370 self.graph.feature_graph().resolve_none()
371 }
372 }
373}
374
375#[cfg(feature = "cli-support")]
376mod summaries {
377 use super::*;
378 use crate::summaries::HakariBuilderSummary;
379 use guppy::platform::TargetFeatures;
380
381 impl<'g> HakariBuilder<'g> {
382 pub fn from_summary(
389 graph: &'g PackageGraph,
390 summary: &HakariBuilderSummary,
391 ) -> Result<Self, guppy::Error> {
392 let hakari_package = summary
393 .hakari_package
394 .as_ref()
395 .map(|name| graph.workspace().member_by_name(name))
396 .transpose()?;
397 let platforms = summary
398 .platforms
399 .iter()
400 .map(|triple_str| {
401 let platform = Platform::new(triple_str.clone(), TargetFeatures::Unknown)
402 .map_err(|err| {
403 guppy::Error::TargetSpecError(
404 "while resolving hakari config or summary".to_owned(),
405 err,
406 )
407 })?;
408 Ok(platform.into())
409 })
410 .collect::<Result<Vec<_>, _>>()?;
411
412 let registries: BiHashMap<_, _, ahash::RandomState, ahash::RandomState> = summary
413 .registries
414 .iter()
415 .map(|(name, url)| (name.clone(), url.clone()))
416 .collect();
417
418 let traversal_excludes = summary
419 .traversal_excludes
420 .to_package_set_registry(
421 graph,
422 |name| registries.get_by_left(name).map(|s| s.as_str()),
423 "resolving hakari traversal-excludes",
424 )?
425 .package_ids(DependencyDirection::Forward)
426 .collect();
427 let final_excludes = summary
428 .final_excludes
429 .to_package_set_registry(
430 graph,
431 |name| registries.get_by_left(name).map(|s| s.as_str()),
432 "resolving hakari final-excludes",
433 )?
434 .package_ids(DependencyDirection::Forward)
435 .collect();
436
437 Ok(Self {
438 graph: DebugIgnore(graph),
439 hakari_package,
440 resolver: summary.resolver,
441 verify_mode: false,
442 unify_target_host: summary.unify_target_host,
443 output_single_feature: summary.output_single_feature,
444 dep_format_version: summary.dep_format_version,
445 workspace_hack_line_style: summary.workspace_hack_line_style,
446 platforms,
447 registries,
448 traversal_excludes,
449 final_excludes,
450 })
451 }
452 }
453}
454
455#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
480#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
481#[cfg_attr(feature = "cli-support", derive(serde::Serialize, serde::Deserialize))]
482#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
483#[non_exhaustive]
484pub enum UnifyTargetHost {
485 None,
491
492 Auto,
500
501 UnifyIfBoth,
507
508 ReplicateTargetOnHost,
514}
515
516impl Default for UnifyTargetHost {
519 #[inline]
520 fn default() -> Self {
521 UnifyTargetHost::Auto
522 }
523}
524
525#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
529#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
530#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
531#[non_exhaustive]
532#[derive(Default)]
533pub enum DepFormatVersion {
534 #[cfg_attr(feature = "cli-support", serde(rename = "1"))]
538 #[default]
539 V1,
540
541 #[cfg_attr(feature = "cli-support", serde(rename = "2"))]
544 V2,
545
546 #[cfg_attr(feature = "cli-support", serde(rename = "3"))]
548 V3,
549
550 #[cfg_attr(feature = "cli-support", serde(rename = "4"))]
556 V4,
557}
558
559impl DepFormatVersion {
560 #[inline]
562 pub fn latest() -> Self {
563 DepFormatVersion::V4
564 }
565}
566
567impl fmt::Display for DepFormatVersion {
568 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569 match self {
570 DepFormatVersion::V1 => write!(f, "1"),
571 DepFormatVersion::V2 => write!(f, "2"),
572 DepFormatVersion::V3 => write!(f, "3"),
573 DepFormatVersion::V4 => write!(f, "4"),
574 }
575 }
576}
577
578#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
580#[cfg_attr(feature = "cli-support", derive(serde::Deserialize, serde::Serialize))]
581#[cfg_attr(feature = "cli-support", serde(rename_all = "kebab-case"))]
582#[cfg_attr(feature = "proptest1", derive(proptest_derive::Arbitrary))]
583#[non_exhaustive]
584#[derive(Default)]
585pub enum WorkspaceHackLineStyle {
586 #[default]
588 Full,
589
590 VersionOnly,
592
593 WorkspaceDotted,
595}
596
597#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
599pub struct OutputKey {
600 pub platform_idx: Option<usize>,
603
604 pub build_platform: BuildPlatform,
606}
607
608#[derive(Clone, Debug)]
614#[non_exhaustive]
615pub struct Hakari<'g> {
616 pub(crate) builder: HakariBuilder<'g>,
617
618 pub output_map: OutputMap<'g>,
623
624 pub computed_map: ComputedMap<'g>,
628}
629
630impl<'g> Hakari<'g> {
631 pub fn builder(&self) -> &HakariBuilder<'g> {
633 &self.builder
634 }
635
636 pub fn read_toml(&self) -> Option<Result<HakariCargoToml, CargoTomlError>> {
645 self.builder.read_toml()
646 }
647
648 pub fn write_toml(
652 &self,
653 options: &HakariOutputOptions,
654 out: impl fmt::Write,
655 ) -> Result<(), TomlOutError> {
656 write_toml(
657 &self.builder,
658 &self.output_map,
659 options,
660 self.builder.dep_format_version,
661 out,
662 )
663 }
664
665 pub fn toml_name_map(&self) -> AHashMap<Cow<'g, str>, PackageMetadata<'g>> {
671 toml_name_map(&self.output_map, self.builder.dep_format_version)
672 }
673
674 pub fn explain(
679 &self,
680 package_id: &'g PackageId,
681 ) -> Result<HakariExplain<'g, '_>, guppy::Error> {
682 HakariExplain::new(self, package_id)
683 }
684
685 pub fn to_toml_string(&self, options: &HakariOutputOptions) -> Result<String, TomlOutError> {
690 let mut out = String::new();
691 self.write_toml(options, &mut out)?;
692 Ok(out)
693 }
694
695 fn build(builder: HakariBuilder<'g>) -> Self {
700 let graph = *builder.graph;
701 let mut computed_map_build = ComputedMapBuild::new(&builder);
702 let platform_specs: Vec<_> = builder
703 .platforms
704 .iter()
705 .map(|platform| PlatformSpec::Platform(platform.clone()))
706 .collect();
707
708 let unify_target_host = builder.unify_target_host.to_impl(graph);
709
710 let mut map_build: OutputMapBuild<'g> = OutputMapBuild::new(graph);
712 map_build.insert_all(
713 computed_map_build.iter(),
714 builder.output_single_feature,
715 unify_target_host,
716 );
717
718 if !builder.output_single_feature {
719 loop {
723 let mut add_extra = HashSet::new();
724 for (output_key, features) in map_build.iter_feature_sets() {
725 let initials_platform = match output_key.build_platform {
726 BuildPlatform::Target => InitialsPlatform::Standard,
727 BuildPlatform::Host => InitialsPlatform::Host,
728 };
729
730 let mut cargo_opts = CargoOptions::new();
731 let platform_spec = match output_key.platform_idx {
732 Some(idx) => platform_specs[idx].clone(),
733 None => PlatformSpec::Always,
734 };
735 cargo_opts
737 .set_include_dev(false)
738 .set_initials_platform(initials_platform)
739 .set_platform(platform_spec)
740 .set_resolver(builder.resolver)
741 .add_omitted_packages(computed_map_build.excludes.iter());
742 let cargo_set = features
743 .into_cargo_set(&cargo_opts)
744 .expect("into_cargo_set processed successfully");
745
746 for &(build_platform, feature_set) in cargo_set.all_features().iter() {
750 for feature_list in
751 feature_set.packages_with_features(DependencyDirection::Forward)
752 {
753 let dep = feature_list.package();
754 let dep_id = dep.id();
755 let v_mut = computed_map_build
759 .get_or_insert_mut(output_key.platform_idx, dep_id);
760
761 let new_key = OutputKey {
763 platform_idx: output_key.platform_idx,
764 build_platform,
765 };
766
767 if map_build.is_inserted(new_key, dep_id) {
768 continue;
769 }
770
771 let this_list: BTreeSet<_> = feature_list.named_features().collect();
772
773 let already_present = v_mut.contains(build_platform, &this_list);
774 if !already_present {
775 v_mut.mark_fixed_up(build_platform, this_list);
777 add_extra.insert((output_key.platform_idx, dep_id));
778 }
779 }
780 }
781 }
782
783 if add_extra.is_empty() {
784 break;
785 }
786
787 map_build.insert_all(
788 add_extra.iter().map(|&(platform_idx, dep_id)| {
789 let v = computed_map_build
790 .get(platform_idx, dep_id)
791 .expect("full value should be present");
792 (platform_idx, dep_id, v)
793 }),
794 builder.output_single_feature,
795 unify_target_host,
796 );
797 }
798 }
799
800 let computed_map = computed_map_build.computed_map;
801 let output_map = map_build.finish(
802 &builder.final_excludes,
803 builder.dep_format_version,
804 builder.output_single_feature,
805 );
806
807 Self {
808 builder,
809 output_map,
810 computed_map,
811 }
812 }
813}
814
815pub type OutputMap<'g> =
825 BTreeMap<OutputKey, BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>>;
826
827pub type ComputedMap<'g> = BTreeMap<(Option<usize>, &'g PackageId), ComputedValue<'g>>;
838
839#[derive(Clone, Debug, Default)]
845pub struct ComputedValue<'g> {
846 pub target_inner: ComputedInnerMap<'g>,
848
849 pub host_inner: ComputedInnerMap<'g>,
851}
852
853pub type ComputedInnerMap<'g> = BTreeMap<BTreeSet<&'g str>, ComputedInnerValue<'g>>;
858
859#[derive(Clone, Debug, Default)]
861pub struct ComputedInnerValue<'g> {
862 pub workspace_packages: Vec<(PackageMetadata<'g>, StandardFeatures, bool)>,
866
867 pub fixed_up: bool,
869}
870
871impl<'g> ComputedInnerValue<'g> {
872 fn extend(&mut self, other: ComputedInnerValue<'g>) {
873 self.workspace_packages.extend(other.workspace_packages);
874 self.fixed_up |= other.fixed_up;
875 }
876
877 #[inline]
878 fn push(
879 &mut self,
880 package: PackageMetadata<'g>,
881 features: StandardFeatures,
882 include_dev: bool,
883 ) {
884 self.workspace_packages
885 .push((package, features, include_dev));
886 }
887}
888
889#[derive(Debug)]
890struct TraversalExcludes<'g, 'b> {
891 excludes: &'b HashSet<&'g PackageId>,
892 hakari_package: Option<&'g PackageId>,
893}
894
895impl<'g, 'b> TraversalExcludes<'g, 'b> {
896 fn iter(&self) -> impl Iterator<Item = &'g PackageId> + 'b {
897 self.excludes.iter().copied().chain(self.hakari_package)
898 }
899
900 fn is_excluded(&self, package_id: &PackageId) -> bool {
901 self.hakari_package == Some(package_id) || self.excludes.contains(package_id)
902 }
903}
904
905#[derive(Debug)]
907struct ComputedMapBuild<'g, 'b> {
908 excludes: TraversalExcludes<'g, 'b>,
909 computed_map: ComputedMap<'g>,
910}
911
912impl<'g, 'b> ComputedMapBuild<'g, 'b> {
913 fn new(builder: &'b HakariBuilder<'g>) -> Self {
914 let features_include_dev = [
945 (StandardFeatures::None, false),
946 (StandardFeatures::None, true),
947 (StandardFeatures::Default, false),
948 (StandardFeatures::Default, true),
949 (StandardFeatures::All, false),
950 (StandardFeatures::All, true),
951 ];
952
953 let always_features = features_include_dev
955 .iter()
956 .map(|&(features, include_dev)| (None, PlatformSpec::Always, features, include_dev));
957
958 let specified_features =
960 features_include_dev
961 .iter()
962 .flat_map(|&(features, include_dev)| {
963 builder
964 .platforms
965 .iter()
966 .enumerate()
967 .map(move |(idx, platform)| {
968 (
969 Some(idx),
970 PlatformSpec::Platform(platform.clone()),
971 features,
972 include_dev,
973 )
974 })
975 });
976 let platforms_features: Vec<_> = always_features.chain(specified_features).collect();
977
978 let workspace = builder.graph.workspace();
979 let excludes = builder.make_traversal_excludes();
980 let features_only = builder.make_features_only();
981 let excludes_ref = &excludes;
982 let features_only_ref = &features_only;
983
984 let computed_map: ComputedMap<'g> = platforms_features
985 .into_par_iter()
986 .flat_map(|(idx, platform_spec, feature_filter, include_dev)| {
989 let mut cargo_options = CargoOptions::new();
990 cargo_options
991 .set_include_dev(include_dev)
992 .set_resolver(builder.resolver)
993 .set_platform(platform_spec)
994 .add_omitted_packages(excludes.iter());
995
996 workspace.par_iter().map(move |workspace_package| {
997 if excludes_ref.is_excluded(workspace_package.id()) {
998 return BTreeMap::new();
1000 }
1001
1002 let initials = workspace_package
1003 .to_package_set()
1004 .to_feature_set(feature_filter);
1005 let cargo_set =
1006 CargoSet::new(initials, features_only_ref.clone(), &cargo_options)
1007 .expect("cargo resolution should succeed");
1008
1009 let all_features = cargo_set.all_features();
1010
1011 let values = all_features.iter().flat_map(|&(build_platform, features)| {
1012 features
1013 .packages_with_features(DependencyDirection::Forward)
1014 .filter_map(move |feature_list| {
1015 let dep = feature_list.package();
1016 if dep.in_workspace() {
1017 return None;
1019 }
1020
1021 let features: BTreeSet<&'g str> =
1022 feature_list.named_features().collect();
1023 Some((
1024 idx,
1025 build_platform,
1026 dep.id(),
1027 features,
1028 workspace_package,
1029 feature_filter,
1030 include_dev,
1031 ))
1032 })
1033 });
1034
1035 let mut map = ComputedMap::new();
1036 for (
1037 platform_idx,
1038 build_platform,
1039 package_id,
1040 features,
1041 package,
1042 feature_filter,
1043 include_dev,
1044 ) in values
1045 {
1046 map.entry((platform_idx, package_id)).or_default().insert(
1048 build_platform,
1049 features,
1050 package,
1051 feature_filter,
1052 include_dev,
1053 );
1054 }
1055
1056 map
1057 })
1058 })
1059 .reduce(ComputedMap::new, |mut acc, map| {
1060 for (k, v) in map {
1062 acc.entry(k).or_default().merge(v);
1063 }
1064 acc
1065 });
1066
1067 Self {
1068 excludes,
1069 computed_map,
1070 }
1071 }
1072
1073 fn get(
1074 &self,
1075 platform_idx: Option<usize>,
1076 package_id: &'g PackageId,
1077 ) -> Option<&ComputedValue<'g>> {
1078 self.computed_map.get(&(platform_idx, package_id))
1079 }
1080
1081 fn get_or_insert_mut(
1082 &mut self,
1083 platform_idx: Option<usize>,
1084 package_id: &'g PackageId,
1085 ) -> &mut ComputedValue<'g> {
1086 self.computed_map
1087 .entry((platform_idx, package_id))
1088 .or_default()
1089 }
1090
1091 fn iter<'a>(
1092 &'a self,
1093 ) -> impl Iterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)> + 'a {
1094 self.computed_map
1095 .iter()
1096 .map(move |(&(platform_idx, package_id), v)| (platform_idx, package_id, v))
1097 }
1098}
1099
1100impl<'g> ComputedValue<'g> {
1101 pub fn inner_maps(&self) -> [(BuildPlatform, &ComputedInnerMap<'g>); 2] {
1103 [
1104 (BuildPlatform::Target, &self.target_inner),
1105 (BuildPlatform::Host, &self.host_inner),
1106 ]
1107 }
1108
1109 pub fn into_inner_maps(self) -> [(BuildPlatform, ComputedInnerMap<'g>); 2] {
1112 [
1113 (BuildPlatform::Target, self.target_inner),
1114 (BuildPlatform::Host, self.host_inner),
1115 ]
1116 }
1117
1118 pub fn get_inner(&self, build_platform: BuildPlatform) -> &ComputedInnerMap<'g> {
1120 match build_platform {
1121 BuildPlatform::Target => &self.target_inner,
1122 BuildPlatform::Host => &self.host_inner,
1123 }
1124 }
1125
1126 pub fn get_inner_mut(&mut self, build_platform: BuildPlatform) -> &mut ComputedInnerMap<'g> {
1128 match build_platform {
1129 BuildPlatform::Target => &mut self.target_inner,
1130 BuildPlatform::Host => &mut self.host_inner,
1131 }
1132 }
1133
1134 fn merge(&mut self, other: ComputedValue<'g>) {
1136 for (features, details) in other.target_inner {
1137 self.target_inner
1138 .entry(features)
1139 .or_default()
1140 .extend(details);
1141 }
1142 for (features, details) in other.host_inner {
1143 self.host_inner.entry(features).or_default().extend(details);
1144 }
1145 }
1146
1147 fn contains(&mut self, build_platform: BuildPlatform, features: &BTreeSet<&'g str>) -> bool {
1148 self.get_inner(build_platform).contains_key(features)
1149 }
1150
1151 fn insert(
1152 &mut self,
1153 build_platform: BuildPlatform,
1154 features: BTreeSet<&'g str>,
1155 package: PackageMetadata<'g>,
1156 feature_filter: StandardFeatures,
1157 include_dev: bool,
1158 ) {
1159 self.get_inner_mut(build_platform)
1160 .entry(features)
1161 .or_default()
1162 .push(package, feature_filter, include_dev);
1163 }
1164
1165 fn mark_fixed_up(&mut self, build_platform: BuildPlatform, features: BTreeSet<&'g str>) {
1166 self.get_inner_mut(build_platform)
1167 .entry(features)
1168 .or_default()
1169 .fixed_up = true;
1170 }
1171
1172 fn describe<'a>(&'a self) -> ValueDescribe<'g, 'a> {
1173 match (self.target_inner.len(), self.host_inner.len()) {
1174 (0, 0) => ValueDescribe::None,
1175 (0, 1) => ValueDescribe::SingleHost(&self.host_inner),
1176 (1, 0) => ValueDescribe::SingleTarget(&self.target_inner),
1177 (1, 1) => {
1178 let target_features = self.target_inner.keys().next().expect("1 element");
1179 let host_features = self.host_inner.keys().next().expect("1 element");
1180 if target_features == host_features {
1181 ValueDescribe::SingleMatchingBoth {
1182 target_inner: &self.target_inner,
1183 host_inner: &self.host_inner,
1184 }
1185 } else {
1186 ValueDescribe::SingleNonMatchingBoth {
1187 target_inner: &self.target_inner,
1188 host_inner: &self.host_inner,
1189 }
1190 }
1191 }
1192 (_m, 0) => ValueDescribe::MultiTarget(&self.target_inner),
1193 (_m, 1) => ValueDescribe::MultiTargetSingleHost {
1194 target_inner: &self.target_inner,
1195 host_inner: &self.host_inner,
1196 },
1197 (0, _n) => ValueDescribe::MultiHost(&self.host_inner),
1198 (1, _n) => ValueDescribe::MultiHostSingleTarget {
1199 target_inner: &self.target_inner,
1200 host_inner: &self.host_inner,
1201 },
1202 (_m, _n) => ValueDescribe::MultiBoth {
1203 target_inner: &self.target_inner,
1204 host_inner: &self.host_inner,
1205 },
1206 }
1207 }
1208}
1209
1210#[derive(Copy, Clone, Debug)]
1211enum ValueDescribe<'g, 'a> {
1212 None,
1213 SingleTarget(&'a ComputedInnerMap<'g>),
1214 SingleHost(&'a ComputedInnerMap<'g>),
1215 MultiTarget(&'a ComputedInnerMap<'g>),
1216 MultiHost(&'a ComputedInnerMap<'g>),
1217 SingleMatchingBoth {
1218 target_inner: &'a ComputedInnerMap<'g>,
1219 host_inner: &'a ComputedInnerMap<'g>,
1220 },
1221 SingleNonMatchingBoth {
1222 target_inner: &'a ComputedInnerMap<'g>,
1223 host_inner: &'a ComputedInnerMap<'g>,
1224 },
1225 MultiTargetSingleHost {
1226 target_inner: &'a ComputedInnerMap<'g>,
1227 host_inner: &'a ComputedInnerMap<'g>,
1228 },
1229 MultiHostSingleTarget {
1230 target_inner: &'a ComputedInnerMap<'g>,
1231 host_inner: &'a ComputedInnerMap<'g>,
1232 },
1233 MultiBoth {
1234 target_inner: &'a ComputedInnerMap<'g>,
1235 host_inner: &'a ComputedInnerMap<'g>,
1236 },
1237}
1238
1239impl<'g, 'a> ValueDescribe<'g, 'a> {
1240 #[allow(dead_code)]
1241 fn description(self) -> &'static str {
1242 match self {
1243 ValueDescribe::None => "None",
1244 ValueDescribe::SingleTarget(_) => "SingleTarget",
1245 ValueDescribe::SingleHost(_) => "SingleHost",
1246 ValueDescribe::MultiTarget(_) => "MultiTarget",
1247 ValueDescribe::MultiHost(_) => "MultiHost",
1248 ValueDescribe::SingleMatchingBoth { .. } => "SingleMatchingBoth",
1249 ValueDescribe::SingleNonMatchingBoth { .. } => "SingleNonMatchingBoth",
1250 ValueDescribe::MultiTargetSingleHost { .. } => "MultiTargetSingleHost",
1251 ValueDescribe::MultiHostSingleTarget { .. } => "MultiHostSingleTarget",
1252 ValueDescribe::MultiBoth { .. } => "MultiBoth",
1253 }
1254 }
1255
1256 fn insert(
1257 self,
1258 output_single_feature: bool,
1259 unify_target_host: UnifyTargetHostImpl,
1260 mut insert_cb: impl FnMut(BuildPlatform, &'a ComputedInnerMap<'g>),
1261 ) {
1262 use BuildPlatform::*;
1263
1264 match self {
1265 ValueDescribe::None => {
1266 }
1268 ValueDescribe::SingleTarget(target_inner) => {
1269 if output_single_feature {
1271 insert_cb(Target, target_inner);
1272 if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1273 insert_cb(Host, target_inner);
1274 }
1275 }
1276 }
1277 ValueDescribe::SingleHost(host_inner) => {
1278 if output_single_feature {
1280 insert_cb(Host, host_inner);
1281 }
1282 }
1283 ValueDescribe::MultiTarget(target_inner) => {
1284 insert_cb(Target, target_inner);
1286 if unify_target_host == UnifyTargetHostImpl::ReplicateTargetOnHost {
1287 insert_cb(Host, target_inner);
1288 }
1289 }
1290 ValueDescribe::MultiHost(host_inner) => {
1291 insert_cb(Host, host_inner);
1293 }
1294 ValueDescribe::SingleMatchingBoth {
1295 target_inner,
1296 host_inner,
1297 } => {
1298 if output_single_feature {
1300 insert_cb(Target, target_inner);
1301 insert_cb(Host, host_inner);
1302 }
1303 }
1304 ValueDescribe::SingleNonMatchingBoth {
1305 target_inner,
1306 host_inner,
1307 } => {
1308 insert_cb(Target, target_inner);
1310 insert_cb(Host, host_inner);
1311 if unify_target_host != UnifyTargetHostImpl::None {
1312 insert_cb(Target, host_inner);
1313 insert_cb(Host, target_inner);
1314 }
1315 }
1316 ValueDescribe::MultiTargetSingleHost {
1317 target_inner,
1318 host_inner,
1319 } => {
1320 insert_cb(Target, target_inner);
1322 insert_cb(Host, host_inner);
1323 if unify_target_host != UnifyTargetHostImpl::None {
1324 insert_cb(Target, host_inner);
1325 insert_cb(Host, target_inner);
1326 }
1327 }
1328 ValueDescribe::MultiHostSingleTarget {
1329 target_inner,
1330 host_inner,
1331 } => {
1332 insert_cb(Target, target_inner);
1334 insert_cb(Host, host_inner);
1335 if unify_target_host != UnifyTargetHostImpl::None {
1336 insert_cb(Target, host_inner);
1337 insert_cb(Host, target_inner);
1338 }
1339 }
1340 ValueDescribe::MultiBoth {
1341 target_inner,
1342 host_inner,
1343 } => {
1344 insert_cb(Target, target_inner);
1346 insert_cb(Host, host_inner);
1347 if unify_target_host != UnifyTargetHostImpl::None {
1348 insert_cb(Target, host_inner);
1349 insert_cb(Host, target_inner);
1350 }
1351 }
1352 }
1353 }
1354}
1355
1356#[derive(Debug)]
1357struct OutputMapBuild<'g> {
1358 graph: &'g PackageGraph,
1359 output_map: OutputMap<'g>,
1360}
1361
1362impl<'g> OutputMapBuild<'g> {
1363 fn new(graph: &'g PackageGraph) -> Self {
1364 Self {
1365 graph,
1366 output_map: OutputMap::new(),
1367 }
1368 }
1369
1370 fn is_inserted(&self, output_key: OutputKey, package_id: &'g PackageId) -> bool {
1371 match self.output_map.get(&output_key) {
1372 Some(inner_map) => inner_map.contains_key(package_id),
1373 None => false,
1374 }
1375 }
1376
1377 #[allow(dead_code)]
1378 fn get(
1379 &self,
1380 output_key: OutputKey,
1381 package_id: &'g PackageId,
1382 ) -> Option<&(PackageMetadata<'g>, BTreeSet<&'g str>)> {
1383 match self.output_map.get(&output_key) {
1384 Some(inner_map) => inner_map.get(package_id),
1385 None => None,
1386 }
1387 }
1388
1389 fn insert_all<'a>(
1390 &mut self,
1391 values: impl IntoIterator<Item = (Option<usize>, &'g PackageId, &'a ComputedValue<'g>)>,
1392 output_single_feature: bool,
1393 unify_target_host: UnifyTargetHostImpl,
1394 ) where
1395 'g: 'a,
1396 {
1397 for (platform_idx, dep_id, v) in values {
1398 let describe = v.describe();
1399 describe.insert(
1400 output_single_feature,
1401 unify_target_host,
1402 |build_platform, inner| {
1403 self.insert_inner(platform_idx, build_platform, dep_id, inner);
1404 },
1405 );
1406 }
1407 }
1408
1409 fn insert_inner(
1410 &mut self,
1411 platform_idx: Option<usize>,
1412 build_platform: BuildPlatform,
1413 package_id: &'g PackageId,
1414 inner: &ComputedInnerMap<'g>,
1415 ) {
1416 let output_key = OutputKey {
1417 platform_idx,
1418 build_platform,
1419 };
1420 self.insert(
1421 output_key,
1422 package_id,
1423 inner.keys().flat_map(|f| f.iter().copied()),
1424 )
1425 }
1426
1427 fn insert(
1428 &mut self,
1429 output_key: OutputKey,
1430 package_id: &'g PackageId,
1431 features: impl IntoIterator<Item = &'g str>,
1432 ) {
1433 let map = self.output_map.entry(output_key).or_default();
1434 let graph = self.graph;
1435 let (_, inner) = map.entry(package_id).or_insert_with(|| {
1436 (
1437 graph.metadata(package_id).expect("valid package ID"),
1438 BTreeSet::new(),
1439 )
1440 });
1441 inner.extend(features);
1442 }
1443
1444 fn iter_feature_sets<'a>(&'a self) -> impl Iterator<Item = (OutputKey, FeatureSet<'g>)> + 'a {
1445 self.output_map.iter().map(move |(&output_key, deps)| {
1446 let feature_ids = deps.iter().flat_map(|(&package_id, (_, features))| {
1447 features
1448 .iter()
1449 .map(move |&feature| FeatureId::new(package_id, FeatureLabel::Named(feature)))
1450 });
1451 (
1452 output_key,
1453 self.graph
1454 .feature_graph()
1455 .resolve_ids(feature_ids)
1456 .expect("specified feature IDs are valid"),
1457 )
1458 })
1459 }
1460
1461 fn finish(
1462 mut self,
1463 final_excludes: &HashSet<&'g PackageId>,
1464 dep_format: DepFormatVersion,
1465 output_single_feature: bool,
1466 ) -> OutputMap<'g> {
1467 for &build_platform in BuildPlatform::VALUES {
1469 let always_key = OutputKey {
1470 platform_idx: None,
1471 build_platform,
1472 };
1473
1474 let mut always_map = match self.output_map.remove(&always_key) {
1476 Some(always_map) => always_map,
1477 None => {
1478 continue;
1480 }
1481 };
1482
1483 if dep_format >= DepFormatVersion::V3 {
1484 Self::filter_root_features(&mut always_map, output_single_feature);
1485 }
1486
1487 for (key, inner_map) in &mut self.output_map {
1488 if key.build_platform != build_platform {
1490 continue;
1491 }
1492 if dep_format >= DepFormatVersion::V3 {
1493 Self::filter_root_features(inner_map, output_single_feature);
1494 }
1495
1496 for (package_id, (_always_package, always_features)) in &always_map {
1497 let (package, remaining_features) = {
1498 let (package, features) = match inner_map.get(package_id) {
1499 Some(v) => v,
1500 None => {
1501 continue;
1504 }
1505 };
1506 (*package, features - always_features)
1507 };
1508 if remaining_features.is_empty() {
1509 inner_map.remove(package_id);
1511 } else {
1512 inner_map.insert(package_id, (package, remaining_features));
1513 }
1514 }
1515 }
1516
1517 self.output_map.insert(always_key, always_map);
1519 }
1520
1521 self.output_map.retain(|_, inner_map| {
1523 for package_id in final_excludes {
1524 inner_map.remove(package_id);
1525 }
1526 !inner_map.is_empty()
1527 });
1528
1529 self.output_map
1530 }
1531
1532 fn filter_root_features(
1542 inner_map: &mut BTreeMap<&'g PackageId, (PackageMetadata<'g>, BTreeSet<&'g str>)>,
1543 output_single_feature: bool,
1544 ) {
1545 inner_map.retain(|_, (package, features)| {
1546 let feature_set = package.to_feature_set(named_feature_filter(
1547 StandardFeatures::None,
1548 features.iter().copied(),
1549 ));
1550
1551 let root_features: BTreeSet<_> = feature_set
1552 .root_ids(DependencyDirection::Forward)
1553 .filter_map(|f| match f.label() {
1554 FeatureLabel::Named(name) => Some(name),
1555 FeatureLabel::Base => None,
1556 FeatureLabel::OptionalDependency(name) => {
1557 debug_assert!(
1558 false,
1559 "root features must be named or base, found optional dependency {}",
1560 name,
1561 );
1562 None
1563 }
1564 })
1565 .collect();
1566
1567 if root_features.is_empty() {
1568 output_single_feature && features.is_empty()
1572 } else {
1573 *features = root_features;
1574 true
1575 }
1576 });
1577 }
1578}
1579
1580#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1581enum UnifyTargetHostImpl {
1582 None,
1583 UnifyIfBoth,
1584 ReplicateTargetOnHost,
1585}
1586
1587impl UnifyTargetHost {
1588 fn to_impl(self, graph: &PackageGraph) -> UnifyTargetHostImpl {
1589 match self {
1590 UnifyTargetHost::None => UnifyTargetHostImpl::None,
1591 UnifyTargetHost::UnifyIfBoth => UnifyTargetHostImpl::UnifyIfBoth,
1592 UnifyTargetHost::ReplicateTargetOnHost => UnifyTargetHostImpl::ReplicateTargetOnHost,
1593 UnifyTargetHost::Auto => {
1594 let workspace_set = graph.resolve_workspace();
1595 if workspace_set
1597 .packages(DependencyDirection::Forward)
1598 .any(|package| package.is_proc_macro())
1599 {
1600 return UnifyTargetHostImpl::ReplicateTargetOnHost;
1601 }
1602
1603 if workspace_set
1605 .links(DependencyDirection::Forward)
1606 .any(|link| link.build().is_present())
1607 {
1608 return UnifyTargetHostImpl::ReplicateTargetOnHost;
1609 }
1610
1611 UnifyTargetHostImpl::UnifyIfBoth
1612 }
1613 }
1614 }
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620 use crate::UnifyTargetHost;
1621 use fixtures::json::JsonFixture;
1622
1623 #[test]
1624 fn unify_target_host_auto() {
1625 let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_guppy_78cb7e8().graph());
1628 assert_eq!(
1629 res,
1630 UnifyTargetHostImpl::UnifyIfBoth,
1631 "no proc macros => unify if both"
1632 );
1633
1634 let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_libra_9ffd93b().graph());
1637 assert_eq!(
1638 res,
1639 UnifyTargetHostImpl::ReplicateTargetOnHost,
1640 "proc macros => replicate target on host"
1641 );
1642
1643 let res = UnifyTargetHost::Auto.to_impl(JsonFixture::metadata_builddep().graph());
1646 assert_eq!(
1647 res,
1648 UnifyTargetHostImpl::ReplicateTargetOnHost,
1649 "internal build deps => replicate target on host"
1650 );
1651 }
1652}