1use crate::error::RailResult;
7use cargo_metadata::{Metadata, MetadataCommand, Package, PackageId};
8use rayon::prelude::*;
9use semver::Version;
10use std::collections::{HashMap, HashSet};
11use std::path::Path;
12
13#[derive(Clone)]
14struct TargetMetadataEntry {
15 metadata: Metadata,
16 package_id_index: HashMap<PackageId, usize>,
17}
18
19impl TargetMetadataEntry {
20 fn new(metadata: Metadata) -> Self {
21 let package_id_index = metadata
22 .packages
23 .iter()
24 .enumerate()
25 .map(|(idx, pkg)| (pkg.id.clone(), idx))
26 .collect();
27
28 Self {
29 metadata,
30 package_id_index,
31 }
32 }
33
34 fn package_by_id(&self, id: &PackageId) -> Option<&Package> {
35 self.package_id_index.get(id).map(|&idx| &self.metadata.packages[idx])
36 }
37}
38
39#[derive(Clone)]
45pub struct MultiTargetMetadata {
46 cache: HashMap<String, TargetMetadataEntry>,
48}
49
50impl MultiTargetMetadata {
51 pub fn load_parallel(workspace_root: &Path, targets: &[String]) -> RailResult<Self> {
53 let workspace_root = workspace_root.to_path_buf();
54
55 if targets.is_empty() {
57 let metadata = Self::load_single_target(&workspace_root, None)?;
58 let mut cache = HashMap::new();
59 cache.insert("default".to_string(), TargetMetadataEntry::new(metadata));
60 return Ok(Self { cache });
61 }
62
63 let results: Vec<RailResult<(String, Metadata)>> = targets
65 .par_iter()
66 .map(|target| {
67 let metadata = Self::load_single_target(&workspace_root, Some(target))?;
68 Ok((target.clone(), metadata))
69 })
70 .collect();
71
72 let mut cache = HashMap::new();
74 for result in results {
75 let (target, metadata) = result?;
76 cache.insert(target, TargetMetadataEntry::new(metadata));
77 }
78
79 Ok(Self { cache })
80 }
81
82 fn load_single_target(workspace_root: &Path, target: Option<&str>) -> RailResult<Metadata> {
84 let manifest_path = workspace_root.join("Cargo.toml");
85
86 let mut cmd = MetadataCommand::new();
87 cmd.manifest_path(&manifest_path);
88
89 if let Some(target_triple) = target {
91 cmd.other_options(vec!["--filter-platform".to_string(), target_triple.to_string()]);
92 }
93
94 let metadata = cmd.exec().map_err(|e| {
98 if let Some(t) = target {
99 let err_str = e.to_string();
100 if err_str.contains("error[E0463]")
102 || err_str.contains("can't find crate")
103 || err_str.contains("target may not be installed")
104 {
105 crate::error::RailError::with_help(
106 format!("Target '{}' is not installed on this machine", t),
107 format!(
108 "Install the target with: rustup target add {}\n\
109 Or remove it from rail.toml [targets] if not needed for this workspace.",
110 t
111 ),
112 )
113 } else {
114 crate::error::RailError::with_help(
115 format!("Failed to load cargo metadata for target '{}'", t),
116 format!("Error: {}\n\nCheck that the target is valid and installed.", e),
117 )
118 }
119 } else {
120 crate::error::RailError::with_help("Failed to load cargo metadata".to_string(), format!("Error: {}", e))
121 }
122 })?;
123
124 Ok(metadata)
125 }
126
127 pub fn get(&self, target: &str) -> Option<&Metadata> {
129 self.cache.get(target).map(|e| &e.metadata)
130 }
131
132 pub fn any(&self) -> Option<&Metadata> {
134 self.cache.values().next().map(|e| &e.metadata)
135 }
136
137 pub fn targets(&self) -> Vec<&str> {
139 let mut targets: Vec<_> = self.cache.keys().map(|s| s.as_str()).collect();
140 targets.sort_unstable();
141 targets
142 }
143
144 pub fn workspace_packages(&self) -> Vec<&Package> {
146 self.any().map(|m| m.workspace_packages()).unwrap_or_default()
147 }
148
149 pub fn all_versions(&self, dep_name: &str) -> HashMap<String, Version> {
153 let mut versions = HashMap::new();
154
155 for (target, entry) in &self.cache {
156 let metadata = &entry.metadata;
157 if let Some(resolve) = &metadata.resolve {
158 for node in &resolve.nodes {
160 if let Some(pkg) = entry.package_by_id(&node.id)
161 && pkg.name == dep_name
162 {
163 versions.insert(target.clone(), pkg.version.clone());
164 break; }
166 }
167 }
168 }
169
170 versions
171 }
172
173 pub fn direct_dep_versions(&self, dep_name: &str) -> HashMap<String, Version> {
181 let mut versions = HashMap::new();
182
183 for (target, entry) in &self.cache {
184 let metadata = &entry.metadata;
185 let workspace_member_ids: HashSet<_> = metadata.workspace_packages().iter().map(|p| &p.id).collect();
187
188 if let Some(resolve) = &metadata.resolve {
189 for node in &resolve.nodes {
191 if !workspace_member_ids.contains(&node.id) {
193 continue;
194 }
195
196 for dep in &node.deps {
198 if dep.name == dep_name {
199 if let Some(pkg) = entry.package_by_id(&dep.pkg) {
201 versions.insert(target.clone(), pkg.version.clone());
203 break; }
205 }
206 }
207 }
208 }
209 }
210
211 versions
212 }
213
214 pub fn is_transitive_only(&self, dep_name: &str) -> bool {
216 for entry in self.cache.values() {
218 let metadata = &entry.metadata;
219 for pkg in metadata.workspace_packages() {
220 for dep in &pkg.dependencies {
221 if dep.name == dep_name {
222 return false; }
224 }
225 }
226 }
227
228 for entry in self.cache.values() {
230 let metadata = &entry.metadata;
231 if let Some(resolve) = &metadata.resolve {
232 for node in &resolve.nodes {
233 if let Some(pkg) = entry.package_by_id(&node.id)
234 && pkg.name == dep_name
235 {
236 return true; }
238 }
239 }
240 }
241
242 false }
244
245 pub fn is_path_dependency(&self, dep_name: &str) -> bool {
251 for entry in self.cache.values() {
252 let metadata = &entry.metadata;
253 for pkg in &metadata.packages {
254 if pkg.name == dep_name {
255 return pkg.source.is_none();
258 }
259 }
260 }
261 false
262 }
263
264 pub fn all_features(&self, dep_name: &str) -> HashMap<String, HashSet<String>> {
267 let mut features = HashMap::new();
268
269 for (target, entry) in &self.cache {
270 let metadata = &entry.metadata;
271 if let Some(resolve) = &metadata.resolve {
272 for node in &resolve.nodes {
273 if let Some(pkg) = entry.package_by_id(&node.id)
275 && pkg.name == dep_name
276 {
277 let feat_set: HashSet<String> = node
279 .features
280 .iter()
281 .filter(|f| {
282 pkg.features.contains_key(f.as_str())
284 })
285 .map(|f| f.to_string())
286 .collect();
287
288 features.insert(target.clone(), feat_set);
289 break;
290 }
291 }
292 }
293 }
294
295 features
296 }
297
298 pub fn targets_with_dep(&self, dep_name: &str) -> Vec<String> {
300 let mut targets = Vec::new();
301
302 for (target, entry) in &self.cache {
303 let metadata = &entry.metadata;
304 if let Some(resolve) = &metadata.resolve {
305 for node in &resolve.nodes {
306 if let Some(pkg) = entry.package_by_id(&node.id)
307 && pkg.name == dep_name
308 {
309 targets.push(target.clone());
310 break;
311 }
312 }
313 }
314 }
315
316 targets.sort_unstable();
317 targets
318 }
319
320 pub fn find_fragmented_transitives(&self) -> Vec<FragmentedTransitive> {
323 let mut transitives = Vec::new();
324
325 let mut all_deps = HashSet::new();
327 for entry in self.cache.values() {
328 let metadata = &entry.metadata;
329 if let Some(resolve) = &metadata.resolve {
330 for node in &resolve.nodes {
331 if let Some(pkg) = entry.package_by_id(&node.id) {
332 all_deps.insert(pkg.name.clone());
333 }
334 }
335 }
336 }
337
338 for dep_name in all_deps {
339 if !self.is_transitive_only(&dep_name) {
340 continue; }
342
343 if self.is_path_dependency(&dep_name) {
345 continue;
346 }
347
348 let features = self.all_features(&dep_name);
349 let unique_sets: HashSet<_> = features
351 .values()
352 .map(|set| {
353 let mut vec: Vec<_> = set.iter().cloned().collect();
354 vec.sort_unstable();
355 vec
356 })
357 .collect();
358
359 if unique_sets.len() > 1 {
360 let common_features: HashSet<String> = features
368 .values()
369 .fold(None, |acc: Option<HashSet<String>>, set| match acc {
370 None => Some(set.clone()),
371 Some(existing) => Some(existing.intersection(set).cloned().collect()),
372 })
373 .unwrap_or_default();
374
375 let versions = self.all_versions(&dep_name);
377 let version = match versions.values().max() {
378 Some(v) => v.clone(),
379 None => continue, };
381
382 let mut unified_features: Vec<_> = common_features.into_iter().collect();
384 unified_features.sort_unstable();
385
386 transitives.push(FragmentedTransitive {
387 name: dep_name.to_string(),
388 version,
389 feature_sets: features,
390 unified_features,
391 });
392 }
393 }
394
395 transitives.sort_unstable_by(|a, b| a.name.cmp(&b.name));
397
398 transitives
399 }
400
401 fn compute_deps_msrv(&self) -> Option<(Version, Vec<String>, usize)> {
406 let mut max_version: Option<Version> = None;
407 let mut contributors: Vec<String> = Vec::new();
408 let mut deps_with_msrv = 0;
409 let mut seen_packages: HashSet<String> = HashSet::new();
410
411 for entry in self.cache.values() {
413 let metadata = &entry.metadata;
414 for pkg in &metadata.packages {
415 let pkg_key = format!("{}@{}", pkg.name, pkg.version);
417 if seen_packages.contains(&pkg_key) {
418 continue;
419 }
420 seen_packages.insert(pkg_key);
421
422 if let Some(ref rust_version) = pkg.rust_version {
424 deps_with_msrv += 1;
425
426 match &max_version {
427 None => {
428 max_version = Some(rust_version.clone());
429 contributors = vec![pkg.name.to_string()];
430 }
431 Some(current_max) => {
432 if rust_version > current_max {
433 max_version = Some(rust_version.clone());
434 contributors = vec![pkg.name.to_string()];
435 } else if rust_version == current_max {
436 contributors.push(pkg.name.to_string());
437 }
438 }
439 }
440 }
441 }
442 }
443
444 max_version.map(|v| (v, contributors, deps_with_msrv))
445 }
446
447 pub fn compute_msrv_with_config(
460 &self,
461 workspace_root: &Path,
462 msrv_source: crate::config::MsrvSource,
463 ) -> Option<ComputedMsrv> {
464 use crate::config::MsrvSource;
465
466 let deps_result = self.compute_deps_msrv();
468
469 let (workspace_msrv, used_package_fallback) = read_workspace_rust_version(workspace_root);
471
472 match msrv_source {
474 MsrvSource::Deps => {
475 deps_result.map(|(version, contributors, deps_with_msrv)| ComputedMsrv {
477 version: version.clone(),
478 contributors,
479 deps_with_msrv,
480 deps_msrv: Some(version),
481 workspace_msrv,
482 source_used: MsrvSourceUsed::Deps,
483 warning: None,
484 })
485 }
486
487 MsrvSource::Workspace => {
488 match (&workspace_msrv, &deps_result) {
490 (Some(ws_ver), Some((deps_ver, contributors, deps_with_msrv))) => {
491 let warning = if deps_ver > ws_ver {
492 Some(format!(
493 "workspace rust-version ({}.{}.{}) is lower than deps require ({}.{}.{}); \
494 deps {} need the higher version",
495 ws_ver.major,
496 ws_ver.minor,
497 ws_ver.patch,
498 deps_ver.major,
499 deps_ver.minor,
500 deps_ver.patch,
501 contributors.first().unwrap_or(&"unknown".to_string())
502 ))
503 } else if used_package_fallback {
504 Some(
505 "no [workspace.package].rust-version found; using [package].rust-version as baseline and writing it to [workspace.package].rust-version. \
506consider enabling MSRV inheritance (rust-version = { workspace = true }) to avoid drift."
507 .to_string(),
508 )
509 } else {
510 None
511 };
512 Some(ComputedMsrv {
513 version: ws_ver.clone(),
514 contributors: contributors.clone(),
515 deps_with_msrv: *deps_with_msrv,
516 deps_msrv: Some(deps_ver.clone()),
517 workspace_msrv: Some(ws_ver.clone()),
518 source_used: MsrvSourceUsed::Workspace,
519 warning,
520 })
521 }
522 (Some(ws_ver), None) => {
523 Some(ComputedMsrv {
525 version: ws_ver.clone(),
526 contributors: Vec::new(),
527 deps_with_msrv: 0,
528 deps_msrv: None,
529 workspace_msrv: Some(ws_ver.clone()),
530 source_used: MsrvSourceUsed::Workspace,
531 warning: if used_package_fallback {
532 Some(
533 "no [workspace.package].rust-version found; using [package].rust-version as baseline and writing it to [workspace.package].rust-version. \
534consider enabling MSRV inheritance (rust-version = { workspace = true }) to avoid drift."
535 .to_string(),
536 )
537 } else {
538 None
539 },
540 })
541 }
542 (None, Some((deps_ver, contributors, deps_with_msrv))) => {
543 Some(ComputedMsrv {
545 version: deps_ver.clone(),
546 contributors: contributors.clone(),
547 deps_with_msrv: *deps_with_msrv,
548 deps_msrv: Some(deps_ver.clone()),
549 workspace_msrv: None,
550 source_used: MsrvSourceUsed::Deps,
551 warning: Some("no workspace rust-version found, using deps MSRV".to_string()),
552 })
553 }
554 (None, None) => None,
555 }
556 }
557
558 MsrvSource::Max => {
559 match (&workspace_msrv, &deps_result) {
561 (Some(ws_ver), Some((deps_ver, contributors, deps_with_msrv))) => {
562 let (version, source_used) = if ws_ver >= deps_ver {
563 (ws_ver.clone(), MsrvSourceUsed::MaxWorkspace)
564 } else {
565 (deps_ver.clone(), MsrvSourceUsed::MaxDeps)
566 };
567 Some(ComputedMsrv {
568 version,
569 contributors: contributors.clone(),
570 deps_with_msrv: *deps_with_msrv,
571 deps_msrv: Some(deps_ver.clone()),
572 workspace_msrv: Some(ws_ver.clone()),
573 source_used,
574 warning: if used_package_fallback {
575 Some(
576 "no [workspace.package].rust-version found; using [package].rust-version as baseline. \
577consider enabling MSRV inheritance (rust-version = { workspace = true }) to avoid drift."
578 .to_string(),
579 )
580 } else {
581 None
582 },
583 })
584 }
585 (Some(ws_ver), None) => {
586 Some(ComputedMsrv {
588 version: ws_ver.clone(),
589 contributors: Vec::new(),
590 deps_with_msrv: 0,
591 deps_msrv: None,
592 workspace_msrv: Some(ws_ver.clone()),
593 source_used: MsrvSourceUsed::MaxWorkspace,
594 warning: if used_package_fallback {
595 Some(
596 "no [workspace.package].rust-version found; using [package].rust-version as baseline. \
597consider enabling MSRV inheritance (rust-version = { workspace = true }) to avoid drift."
598 .to_string(),
599 )
600 } else {
601 None
602 },
603 })
604 }
605 (None, Some((deps_ver, contributors, deps_with_msrv))) => {
606 Some(ComputedMsrv {
608 version: deps_ver.clone(),
609 contributors: contributors.clone(),
610 deps_with_msrv: *deps_with_msrv,
611 deps_msrv: Some(deps_ver.clone()),
612 workspace_msrv: None,
613 source_used: MsrvSourceUsed::MaxDeps,
614 warning: None,
615 })
616 }
617 (None, None) => None,
618 }
619 }
620 }
621 }
622}
623
624fn read_workspace_rust_version(workspace_root: &Path) -> (Option<Version>, bool) {
631 let cargo_toml_path = workspace_root.join("Cargo.toml");
632 let Ok(content) = std::fs::read_to_string(&cargo_toml_path) else {
633 return (None, false);
634 };
635 let Ok(doc) = content.parse::<toml_edit::DocumentMut>() else {
636 return (None, false);
637 };
638
639 let workspace_rust_version_str = doc
641 .get("workspace")
642 .and_then(|ws| ws.get("package"))
643 .and_then(|pkg| pkg.get("rust-version"))
644 .and_then(|v| v.as_str());
645
646 if let Some(s) = workspace_rust_version_str {
647 return (parse_rust_version(s), false);
648 }
649
650 let package_rust_version_str = doc
652 .get("package")
653 .and_then(|pkg| pkg.get("rust-version"))
654 .and_then(|v| v.as_str());
655
656 if let Some(s) = package_rust_version_str {
657 return (parse_rust_version(s), true);
658 }
659
660 (None, false)
661}
662
663fn parse_rust_version(s: &str) -> Option<Version> {
667 if let Ok(v) = Version::parse(s) {
669 return Some(v);
670 }
671
672 let parts: Vec<&str> = s.split('.').collect();
674 if parts.len() == 2
675 && let (Ok(major), Ok(minor)) = (parts[0].parse::<u64>(), parts[1].parse::<u64>())
676 {
677 return Some(Version::new(major, minor, 0));
678 }
679
680 None
681}
682
683#[derive(Debug, Clone)]
685pub struct FragmentedTransitive {
686 pub name: String,
688 pub version: Version,
690 pub feature_sets: HashMap<String, HashSet<String>>,
692 pub unified_features: Vec<String>,
694}
695
696impl FragmentedTransitive {
697 pub fn overhead_factor(&self) -> usize {
699 self.feature_sets.len()
700 }
701}
702
703#[derive(Debug, Clone)]
705pub struct ComputedMsrv {
706 pub version: Version,
708 pub contributors: Vec<String>,
710 pub deps_with_msrv: usize,
712 pub deps_msrv: Option<Version>,
714 pub workspace_msrv: Option<Version>,
716 pub source_used: MsrvSourceUsed,
718 pub warning: Option<String>,
720}
721
722#[derive(Debug, Clone, Copy, PartialEq, Eq)]
724pub enum MsrvSourceUsed {
725 Deps,
727 Workspace,
729 MaxWorkspace,
731 MaxDeps,
733}
734
735impl MultiTargetMetadata {
736 pub fn package_to_lib_name_map(&self) -> HashMap<String, String> {
750 use cargo_metadata::TargetKind;
751
752 let mut map = HashMap::new();
753
754 for entry in self.cache.values() {
755 let metadata = &entry.metadata;
756 for pkg in &metadata.packages {
757 let lib_name = pkg
759 .targets
760 .iter()
761 .find(|t| t.kind.contains(&TargetKind::Lib))
762 .map(|t| t.name.clone())
763 .unwrap_or_else(|| pkg.name.to_string());
764
765 let normalized_lib = lib_name.replace('-', "_");
767 map.insert(pkg.name.to_string(), normalized_lib);
768 }
769 }
770
771 map
772 }
773}
774
775#[cfg(test)]
776mod tests {
777 use super::*;
778
779 #[test]
780 fn test_parse_rust_version_full() {
781 let v = parse_rust_version("1.70.0").unwrap();
782 assert_eq!(v.major, 1);
783 assert_eq!(v.minor, 70);
784 assert_eq!(v.patch, 0);
785 }
786
787 #[test]
788 fn test_parse_rust_version_two_parts() {
789 let v = parse_rust_version("1.70").unwrap();
790 assert_eq!(v.major, 1);
791 assert_eq!(v.minor, 70);
792 assert_eq!(v.patch, 0);
793 }
794
795 #[test]
796 fn test_parse_rust_version_high_minor() {
797 let v = parse_rust_version("1.91").unwrap();
798 assert_eq!(v.major, 1);
799 assert_eq!(v.minor, 91);
800 assert_eq!(v.patch, 0);
801 }
802
803 #[test]
804 fn test_parse_rust_version_invalid() {
805 assert!(parse_rust_version("invalid").is_none());
806 assert!(parse_rust_version("").is_none());
807 assert!(parse_rust_version("1").is_none());
808 assert!(parse_rust_version("a.b.c").is_none());
809 }
810
811 #[test]
812 fn test_msrv_source_used_variants() {
813 assert_ne!(MsrvSourceUsed::Deps, MsrvSourceUsed::Workspace);
815 assert_ne!(MsrvSourceUsed::MaxWorkspace, MsrvSourceUsed::MaxDeps);
816 }
817
818 #[test]
823 fn test_targets_returns_sorted_output() {
824 let mut keys = vec!["z-target", "a-target", "m-target"];
827 keys.sort_unstable();
828 assert_eq!(keys, vec!["a-target", "m-target", "z-target"]);
829 }
830
831 #[test]
832 fn test_fragmented_transitive_unified_features_sorting_contract() {
833 let mut features = vec!["zebra".to_string(), "alpha".to_string(), "beta".to_string()];
839 features.sort_unstable(); let transitive = FragmentedTransitive {
842 name: "test-dep".to_string(),
843 version: Version::new(1, 0, 0),
844 feature_sets: HashMap::new(),
845 unified_features: features,
846 };
847
848 assert!(
850 is_sorted(&transitive.unified_features),
851 "unified_features should be sorted for deterministic output"
852 );
853 assert_eq!(
854 transitive.unified_features,
855 vec!["alpha", "beta", "zebra"],
856 "Features should be in alphabetical order"
857 );
858 }
859
860 #[test]
861 fn test_feature_set_comparison_is_deterministic() {
862 let mut set1: HashSet<String> = HashSet::new();
867 set1.insert("c".to_string());
868 set1.insert("a".to_string());
869 set1.insert("b".to_string());
870
871 let mut set2: HashSet<String> = HashSet::new();
872 set2.insert("a".to_string());
873 set2.insert("b".to_string());
874 set2.insert("c".to_string());
875
876 let mut vec1: Vec<_> = set1.iter().cloned().collect();
878 vec1.sort_unstable();
879 let mut vec2: Vec<_> = set2.iter().cloned().collect();
880 vec2.sort_unstable();
881
882 assert_eq!(vec1, vec2, "Sorted feature sets should be equal");
884 assert_eq!(vec1, vec!["a", "b", "c"]);
885 }
886
887 #[test]
888 fn test_find_fragmented_transitives_output_is_sorted() {
889 let mut transitives = [
894 FragmentedTransitive {
895 name: "zebra-crate".to_string(),
896 version: Version::new(1, 0, 0),
897 feature_sets: HashMap::new(),
898 unified_features: vec![],
899 },
900 FragmentedTransitive {
901 name: "alpha-crate".to_string(),
902 version: Version::new(1, 0, 0),
903 feature_sets: HashMap::new(),
904 unified_features: vec![],
905 },
906 FragmentedTransitive {
907 name: "middle-crate".to_string(),
908 version: Version::new(1, 0, 0),
909 feature_sets: HashMap::new(),
910 unified_features: vec![],
911 },
912 ];
913
914 transitives.sort_unstable_by(|a, b| a.name.cmp(&b.name));
916
917 assert_eq!(transitives[0].name, "alpha-crate");
918 assert_eq!(transitives[1].name, "middle-crate");
919 assert_eq!(transitives[2].name, "zebra-crate");
920 }
921
922 fn is_sorted(slice: &[String]) -> bool {
924 slice.windows(2).all(|w| w[0] <= w[1])
925 }
926}