1use std::collections::{BTreeSet, HashMap};
20use std::hash::BuildHasher;
21
22use cabin_core::{
23 ArchiverKind, CStandard, CxxStandard, ResolvedLanguageStandards, ResolvedToolchain,
24 SourceLanguage, ToolchainDetectionReport, classify_source, effective_c, effective_cxx,
25 validate_ar_for_backend, validate_c_standards, validate_cc_for_backend,
26 validate_cxx_for_backend, validate_cxx_standards,
27};
28use cabin_workspace::PackageGraph;
29
30use crate::error::BuildError;
31
32#[derive(Debug, Default, Clone, PartialEq, Eq)]
45pub struct RequestedStandards {
46 pub c: BTreeSet<CStandard>,
47 pub cxx: BTreeSet<CxxStandard>,
48}
49
50impl RequestedStandards {
51 #[must_use]
54 pub fn has_c_sources(&self) -> bool {
55 !self.c.is_empty()
56 }
57}
58
59#[must_use]
66pub fn requested_standards_of(graph: &crate::BuildGraph) -> RequestedStandards {
67 use cabin_core::LanguageStandard;
68 use cabin_driver::BuildAction;
69 let mut out = RequestedStandards::default();
70 for action in &graph.actions {
71 if let BuildAction::Compile(compile) = action {
72 match compile.standard {
73 LanguageStandard::C(standard) => {
74 out.c.insert(standard);
75 }
76 LanguageStandard::Cxx(standard) => {
77 out.cxx.insert(standard);
78 }
79 }
80 }
81 }
82 out
83}
84
85#[must_use]
94pub fn collect_requested_standards<S: BuildHasher>(
95 graph: &PackageGraph,
96 selected: &BTreeSet<usize>,
97 standards: &HashMap<usize, ResolvedLanguageStandards, S>,
98 dev_for: &BTreeSet<String>,
99) -> RequestedStandards {
100 let mut out = RequestedStandards::default();
101 for &idx in selected {
102 let Some(pkg) = graph.packages.get(idx) else {
103 continue;
104 };
105 let resolved = standards.get(&idx).copied().unwrap_or_default();
106 let include_dev = dev_for.contains(pkg.package.name.as_str());
107 for target in &pkg.package.targets {
108 if target.kind.is_dev_only() && !include_dev {
109 continue;
110 }
111 for source in &target.sources {
114 match classify_source(source) {
115 Some(SourceLanguage::C) => {
116 if let Some(resolved) = effective_c(&resolved, target) {
117 out.c.insert(resolved.standard);
118 }
119 }
120 Some(SourceLanguage::Cxx) => {
121 if let Some(resolved) = effective_cxx(&resolved, target) {
122 out.cxx.insert(resolved.standard);
123 }
124 }
125 None => {}
126 }
127 }
128 }
129 }
130 out
131}
132
133pub fn validate_toolchain_for_backend(
165 toolchain: &ResolvedToolchain,
166 report: &ToolchainDetectionReport,
167) -> Result<(), BuildError> {
168 let cxx_spec = toolchain.cxx.spec.display();
169 validate_cxx_for_backend(&cxx_spec, &report.cxx.identity, &report.cxx.capabilities)?;
170 let ar_spec = toolchain.ar.spec.display();
171 validate_ar_for_backend(&ar_spec, &report.ar.identity, &report.ar.capabilities)?;
172
173 let cxx_is_msvc = report.cxx.identity.kind.speaks_msvc_dialect();
177 let ar_is_msvc = report.ar.identity.kind == ArchiverKind::Lib;
178 if cxx_is_msvc != ar_is_msvc {
179 return Err(BuildError::MixedToolchainDialects {
180 detail: format!(
181 "C++ compiler `{cxx_spec}` is {}, but archiver `{ar_spec}` is {}",
182 dialect_label(cxx_is_msvc),
183 dialect_label(ar_is_msvc),
184 ),
185 });
186 }
187 Ok(())
188}
189
190pub fn validate_toolchain_standards(
212 toolchain: &ResolvedToolchain,
213 report: &ToolchainDetectionReport,
214 requested: &RequestedStandards,
215) -> Result<(), BuildError> {
216 let cxx_spec = toolchain.cxx.spec.display();
217 validate_cxx_standards(&cxx_spec, &report.cxx.identity, &requested.cxx)?;
218 if requested.has_c_sources()
224 && let (Some(cc_tool), Some(cc_detection)) = (toolchain.cc.as_ref(), report.cc.as_ref())
225 {
226 let cc_spec = cc_tool.spec.display();
227 validate_cc_for_backend(&cc_spec, &cc_detection.identity, &cc_detection.capabilities)?;
228 let cxx_is_msvc = report.cxx.identity.kind.speaks_msvc_dialect();
229 let cc_is_msvc = cc_detection.identity.kind.speaks_msvc_dialect();
230 if cc_is_msvc != cxx_is_msvc {
231 return Err(BuildError::MixedToolchainDialects {
232 detail: format!(
233 "C++ compiler `{cxx_spec}` is {}, but C compiler `{cc_spec}` is {}",
234 dialect_label(cxx_is_msvc),
235 dialect_label(cc_is_msvc),
236 ),
237 });
238 }
239 validate_c_standards(&cc_spec, &cc_detection.identity, &requested.c)?;
240 }
241 Ok(())
242}
243
244pub fn validate_planned_standards(graph: &crate::BuildGraph) -> Result<(), BuildError> {
258 match graph.standard_violations.first() {
259 Some(crate::StandardViolation::MsvcSpelling {
260 target,
261 language,
262 standard,
263 ..
264 }) => Err(BuildError::StandardUnsupportedOnMsvcDialect {
265 target: target.clone(),
266 language,
267 standard,
268 }),
269 Some(crate::StandardViolation::MsvcGnuExtensions { target, .. }) => {
270 Err(BuildError::GnuExtensionsUnsupportedOnMsvcDialect {
271 target: target.clone(),
272 })
273 }
274 Some(crate::StandardViolation::FlagConflict { conflict, .. }) => {
275 Err(BuildError::StandardFlagConflict(Box::new(conflict.clone())))
276 }
277 Some(crate::StandardViolation::InterfaceIncompatibility {
278 consumer,
279 dependency,
280 language,
281 consumer_standard,
282 required,
283 requirement_source,
284 ..
285 }) => Err(BuildError::IncompatibleLanguageStandard {
286 consumer: consumer.clone(),
287 dependency: dependency.clone(),
288 language,
289 consumer_standard,
290 required,
291 requirement_source,
292 }),
293 None => Ok(()),
294 }
295}
296
297fn dialect_label(is_msvc: bool) -> &'static str {
299 if is_msvc { "MSVC" } else { "GCC/Clang-style" }
300}
301
302#[must_use]
314pub fn msvc_external_includes_supported(
315 report: &ToolchainDetectionReport,
316 has_c_sources: bool,
317) -> bool {
318 let cxx_ok = report.cxx.capabilities.external_include_dirs.supported;
319 let cc_ok = !has_c_sources
320 || report
321 .cc
322 .as_ref()
323 .is_none_or(|cc| cc.capabilities.external_include_dirs.supported);
324 cxx_ok && cc_ok
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330 use cabin_core::{
331 ArchiverIdentity, ArchiverKind, CompilerIdentity, CompilerKind, CompilerVersion,
332 ResolvedTool, ResolvedToolchain, StandardDeclaration, ToolDetection, ToolKind, ToolSource,
333 ToolSpec, derive_ar_capabilities, derive_cxx_capabilities,
334 };
335 use camino::Utf8PathBuf;
336
337 fn requested(c: &[CStandard], cxx: &[CxxStandard]) -> RequestedStandards {
338 RequestedStandards {
339 c: c.iter().copied().collect(),
340 cxx: cxx.iter().copied().collect(),
341 }
342 }
343
344 fn requested_defaults(with_c: bool) -> RequestedStandards {
346 if with_c {
347 requested(&[CStandard::C11], &[CxxStandard::Cxx17])
348 } else {
349 requested(&[], &[CxxStandard::Cxx17])
350 }
351 }
352
353 fn make_toolchain(cxx_spec: &str, ar_spec: &str) -> ResolvedToolchain {
354 ResolvedToolchain {
355 cxx: ResolvedTool {
356 kind: ToolKind::CxxCompiler,
357 path: Utf8PathBuf::from("/bin").join(cxx_spec),
358 spec: ToolSpec::Name(cxx_spec.into()),
359 source: ToolSource::Default,
360 },
361 ar: ResolvedTool {
362 kind: ToolKind::Archiver,
363 path: Utf8PathBuf::from("/bin").join(ar_spec),
364 spec: ToolSpec::Name(ar_spec.into()),
365 source: ToolSource::Default,
366 },
367 cc: None,
368 }
369 }
370
371 fn report_for(cxx: CompilerIdentity, ar: ArchiverIdentity) -> ToolchainDetectionReport {
372 let cxx_caps = derive_cxx_capabilities(&cxx);
373 let ar_caps = derive_ar_capabilities(&ar);
374 ToolchainDetectionReport {
375 cxx: ToolDetection {
376 path: Utf8PathBuf::from("/bin/cxx"),
377 identity: cxx,
378 capabilities: cxx_caps,
379 },
380 cc: None,
381 ar: ToolDetection {
382 path: Utf8PathBuf::from("/bin/ar"),
383 identity: ar,
384 capabilities: ar_caps,
385 },
386 }
387 }
388
389 #[test]
390 fn accepts_clang_with_gnu_ar() {
391 let toolchain = make_toolchain("clang++", "ar");
392 let report = report_for(
393 CompilerIdentity {
394 kind: CompilerKind::Clang,
395 version: CompilerVersion::parse("17.0.6"),
396 target: None,
397 raw_version_line: "clang version 17.0.6".into(),
398 },
399 ArchiverIdentity {
400 kind: ArchiverKind::Ar,
401 version: CompilerVersion::parse("2.40"),
402 raw_version_line: "GNU ar".into(),
403 },
404 );
405 validate_toolchain_for_backend(&toolchain, &report).unwrap();
406 }
407
408 #[test]
409 fn accepts_full_msvc_toolchain() {
410 let toolchain = make_toolchain("cl.exe", "lib.exe");
413 let report = report_for(
414 CompilerIdentity {
415 kind: CompilerKind::Msvc,
416 version: None,
417 target: None,
418 raw_version_line: "Microsoft Optimizing Compiler".into(),
419 },
420 ArchiverIdentity {
421 kind: ArchiverKind::Lib,
422 version: None,
423 raw_version_line: "Microsoft Library Manager".into(),
424 },
425 );
426 validate_toolchain_for_backend(&toolchain, &report).unwrap();
427 }
428
429 #[test]
430 fn msvc_external_includes_follow_the_cl_version() {
431 let lib = || ArchiverIdentity {
432 kind: ArchiverKind::Lib,
433 version: None,
434 raw_version_line: "Microsoft Library Manager".into(),
435 };
436 let cl = |version: &str| CompilerIdentity {
437 kind: CompilerKind::Msvc,
438 version: CompilerVersion::parse(version),
439 target: None,
440 raw_version_line: format!("Microsoft Optimizing Compiler {version}"),
441 };
442 let modern = report_for(cl("19.29.30133"), lib());
443 assert!(msvc_external_includes_supported(&modern, false));
444 let old = report_for(cl("19.20.27508"), lib());
445 assert!(!msvc_external_includes_supported(&old, false));
446 }
447
448 #[test]
449 fn msvc_external_includes_consider_cc_only_with_c_sources() {
450 let lib = ArchiverIdentity {
453 kind: ArchiverKind::Lib,
454 version: None,
455 raw_version_line: "Microsoft Library Manager".into(),
456 };
457 let modern_cl = CompilerIdentity {
458 kind: CompilerKind::Msvc,
459 version: CompilerVersion::parse("19.39.33523"),
460 target: None,
461 raw_version_line: "Microsoft Optimizing Compiler 19.39".into(),
462 };
463 let old_cl = CompilerIdentity {
464 kind: CompilerKind::Msvc,
465 version: CompilerVersion::parse("19.20.27508"),
466 target: None,
467 raw_version_line: "Microsoft Optimizing Compiler 19.20".into(),
468 };
469 let mut report = report_for(modern_cl, lib);
470 let old_caps = derive_cxx_capabilities(&old_cl);
471 report.cc = Some(ToolDetection {
472 path: Utf8PathBuf::from("/bin/cc"),
473 identity: old_cl,
474 capabilities: old_caps,
475 });
476 assert!(msvc_external_includes_supported(&report, false));
477 assert!(!msvc_external_includes_supported(&report, true));
478 }
479
480 #[test]
481 fn accepts_clang_cl_with_lib_and_rejects_it_with_gnu_ar() {
482 let clang_cl = CompilerIdentity {
485 kind: CompilerKind::ClangCl,
486 version: CompilerVersion::parse("17.0.6"),
487 target: None,
488 raw_version_line: "clang version 17.0.6".into(),
489 };
490 let lib = ArchiverIdentity {
491 kind: ArchiverKind::Lib,
492 version: None,
493 raw_version_line: "Microsoft Library Manager".into(),
494 };
495 let report = report_for(clang_cl.clone(), lib);
496 validate_toolchain_for_backend(&make_toolchain("clang-cl", "lib.exe"), &report).unwrap();
497
498 let gnu_ar = ArchiverIdentity {
499 kind: ArchiverKind::Ar,
500 version: CompilerVersion::parse("2.40"),
501 raw_version_line: "GNU ar".into(),
502 };
503 let mixed = report_for(clang_cl, gnu_ar);
504 let err =
505 validate_toolchain_for_backend(&make_toolchain("clang-cl", "ar"), &mixed).unwrap_err();
506 assert!(
507 matches!(err, BuildError::MixedToolchainDialects { .. }),
508 "clang-cl + GNU ar should be rejected as mixed-dialect, got: {err}"
509 );
510 }
511
512 #[test]
513 fn rejects_mixed_dialect_toolchain() {
514 let toolchain = make_toolchain("clang++", "lib.exe");
517 let report = report_for(
518 CompilerIdentity {
519 kind: CompilerKind::Clang,
520 version: CompilerVersion::parse("17.0.6"),
521 target: None,
522 raw_version_line: "clang version 17.0.6".into(),
523 },
524 ArchiverIdentity {
525 kind: ArchiverKind::Lib,
526 version: None,
527 raw_version_line: "Microsoft Library Manager".into(),
528 },
529 );
530 let err = validate_toolchain_for_backend(&toolchain, &report).unwrap_err();
531 assert!(
532 matches!(err, BuildError::MixedToolchainDialects { .. }),
533 "expected mixed-dialect rejection, got: {err}"
534 );
535 }
536
537 #[test]
538 fn defers_cc_dialect_check_until_c_compiles_are_planned() {
539 let mut toolchain = make_toolchain("clang++", "ar");
545 toolchain.cc = Some(ResolvedTool {
546 kind: ToolKind::CCompiler,
547 path: Utf8PathBuf::from("/bin/cl.exe"),
548 spec: ToolSpec::Name("cl".into()),
549 source: ToolSource::Default,
550 });
551 let cc_identity = CompilerIdentity {
552 kind: CompilerKind::Msvc,
553 version: CompilerVersion::parse("19.39.0"),
554 target: None,
555 raw_version_line: "Microsoft Optimizing Compiler".into(),
556 };
557 let mut report = report_for(
558 CompilerIdentity {
559 kind: CompilerKind::Clang,
560 version: CompilerVersion::parse("17.0.6"),
561 target: None,
562 raw_version_line: "clang version 17.0.6".into(),
563 },
564 ArchiverIdentity {
565 kind: ArchiverKind::Ar,
566 version: CompilerVersion::parse("2.40"),
567 raw_version_line: "GNU ar".into(),
568 },
569 );
570 report.cc = Some(ToolDetection {
571 path: Utf8PathBuf::from("/bin/cl.exe"),
572 capabilities: derive_cxx_capabilities(&cc_identity),
573 identity: cc_identity,
574 });
575
576 validate_toolchain_for_backend(&toolchain, &report).unwrap();
578 validate_toolchain_standards(&toolchain, &report, &requested_defaults(false)).unwrap();
581 let err = validate_toolchain_standards(&toolchain, &report, &requested_defaults(true))
583 .unwrap_err();
584 assert!(
585 matches!(err, BuildError::MixedToolchainDialects { .. }),
586 "expected mixed-dialect rejection once C compiles are planned, got: {err}"
587 );
588 }
589
590 #[test]
591 fn rejects_unknown_compiler_clearly() {
592 let toolchain = make_toolchain("custom-cxx", "ar");
593 let report = report_for(
594 CompilerIdentity::unknown("???"),
595 ArchiverIdentity {
596 kind: ArchiverKind::Ar,
597 version: None,
598 raw_version_line: "GNU ar".into(),
599 },
600 );
601 let err = validate_toolchain_for_backend(&toolchain, &report).unwrap_err();
602 let message = err.to_string();
603 assert!(
604 message.contains("could not be identified"),
605 "expected unknown-compiler error, got: {message}"
606 );
607 }
608
609 #[test]
610 fn c_source_detection_is_scoped_to_the_selected_closure() {
611 use cabin_core::{Package, PackageName, Target, TargetKind, TargetName};
612 use cabin_workspace::{PackageKind, WorkspacePackage};
613
614 fn pkg(name: &str, source: &str) -> WorkspacePackage {
615 let target = Target {
616 name: TargetName::new("lib").unwrap(),
617 kind: TargetKind::Library,
618 sources: vec![Utf8PathBuf::from(source)],
619 include_dirs: Vec::new(),
620 defines: Vec::new(),
621 deps: Vec::new(),
622 required_features: Vec::new(),
623 language: cabin_core::LanguageStandardSettings {
624 c_standard: Some(StandardDeclaration::Declared(CStandard::C11)),
625 cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx17)),
626 ..Default::default()
627 },
628 };
629 let package = Package::new(
630 PackageName::new(name).unwrap(),
631 semver::Version::parse("0.1.0").unwrap(),
632 vec![target],
633 Vec::new(),
634 )
635 .unwrap();
636 let dir = std::path::PathBuf::from("/tmp").join(name);
637 WorkspacePackage {
638 package,
639 manifest_path: dir.join("cabin.toml"),
640 manifest_dir: dir,
641 deps: Vec::new(),
642 kind: PackageKind::Local,
643 is_port: false,
644 }
645 }
646
647 let graph = PackageGraph {
649 root_manifest_path: std::path::PathBuf::from("/tmp/cabin.toml"),
650 root_dir: std::path::PathBuf::from("/tmp"),
651 is_workspace_root: true,
652 root_package: None,
653 root_settings: Default::default(),
654 primary_packages: vec![0, 1],
655 default_members: vec![0, 1],
656 excluded_members: Vec::new(),
657 packages: vec![pkg("with_c", "a.c"), pkg("cpp_only", "b.cc")],
658 };
659
660 let standards = HashMap::new();
663 let no_dev = BTreeSet::new();
664 let cpp_only =
665 collect_requested_standards(&graph, &BTreeSet::from([1usize]), &standards, &no_dev);
666 assert!(!cpp_only.has_c_sources());
667 assert_eq!(cpp_only.cxx, BTreeSet::from([CxxStandard::Cxx17]));
668 let with_c =
670 collect_requested_standards(&graph, &BTreeSet::from([0usize]), &standards, &no_dev);
671 assert!(with_c.has_c_sources());
672 assert_eq!(with_c.c, BTreeSet::from([CStandard::C11]));
673 let both = collect_requested_standards(
674 &graph,
675 &BTreeSet::from([0usize, 1usize]),
676 &standards,
677 &no_dev,
678 );
679 assert!(both.has_c_sources());
680 }
681
682 #[test]
683 fn requested_standards_skip_dev_only_targets_unless_activated() {
684 use cabin_core::{
685 CxxStandard, LanguageStandardSettings, Package, PackageName, Target, TargetKind,
686 TargetName,
687 };
688 use cabin_workspace::{PackageKind, WorkspacePackage};
689
690 let test_target = Target {
691 name: TargetName::new("t").unwrap(),
692 kind: TargetKind::Test,
693 sources: vec![Utf8PathBuf::from("t.cc")],
694 include_dirs: Vec::new(),
695 defines: Vec::new(),
696 deps: Vec::new(),
697 required_features: Vec::new(),
698 language: LanguageStandardSettings {
699 cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
700 ..Default::default()
701 },
702 };
703 let package = Package::new(
704 PackageName::new("demo").unwrap(),
705 semver::Version::parse("0.1.0").unwrap(),
706 vec![test_target],
707 Vec::new(),
708 )
709 .unwrap();
710 let dir = std::path::PathBuf::from("/tmp/demo");
711 let graph = PackageGraph {
712 root_manifest_path: dir.join("cabin.toml"),
713 root_dir: dir.clone(),
714 is_workspace_root: false,
715 root_package: Some(0),
716 root_settings: Default::default(),
717 primary_packages: vec![0],
718 default_members: vec![0],
719 excluded_members: Vec::new(),
720 packages: vec![WorkspacePackage {
721 package,
722 manifest_path: dir.join("cabin.toml"),
723 manifest_dir: dir,
724 deps: Vec::new(),
725 kind: PackageKind::Local,
726 is_port: false,
727 }],
728 };
729 let standards = HashMap::new();
730 let plain = collect_requested_standards(
733 &graph,
734 &BTreeSet::from([0usize]),
735 &standards,
736 &BTreeSet::new(),
737 );
738 assert!(plain.cxx.is_empty());
739 let dev = collect_requested_standards(
741 &graph,
742 &BTreeSet::from([0usize]),
743 &standards,
744 &BTreeSet::from(["demo".to_owned()]),
745 );
746 assert_eq!(dev.cxx, BTreeSet::from([CxxStandard::Cxx20]));
747 }
748
749 #[test]
750 fn standards_validation_rejects_unsupported_requested_standard() {
751 use cabin_core::CxxStandard;
752 let toolchain = make_toolchain("cl.exe", "lib.exe");
753 let report = report_for(
754 CompilerIdentity {
755 kind: CompilerKind::Msvc,
756 version: CompilerVersion::parse("19.39.33523"),
757 target: None,
758 raw_version_line: "Microsoft Optimizing Compiler 19.39".into(),
759 },
760 ArchiverIdentity {
761 kind: ArchiverKind::Lib,
762 version: None,
763 raw_version_line: "Microsoft Library Manager".into(),
764 },
765 );
766 validate_toolchain_for_backend(&toolchain, &report).unwrap();
768 validate_toolchain_standards(&toolchain, &report, &requested_defaults(false)).unwrap();
769 let err = validate_toolchain_standards(
772 &toolchain,
773 &report,
774 &requested(&[], &[CxxStandard::Cxx23]),
775 )
776 .unwrap_err();
777 assert!(
778 err.to_string().contains("c++23"),
779 "expected the offending standard to be named, got: {err}"
780 );
781 }
782}