use std::collections::{BTreeSet, HashMap};
use std::hash::BuildHasher;
use cabin_core::{
ArchiverKind, CStandard, CxxStandard, ResolvedLanguageStandards, ResolvedToolchain,
SourceLanguage, ToolchainDetectionReport, classify_source, effective_c, effective_cxx,
validate_ar_for_backend, validate_c_standards, validate_cc_for_backend,
validate_cxx_for_backend, validate_cxx_standards,
};
use cabin_workspace::PackageGraph;
use crate::error::BuildError;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct RequestedStandards {
pub c: BTreeSet<CStandard>,
pub cxx: BTreeSet<CxxStandard>,
}
impl RequestedStandards {
#[must_use]
pub fn has_c_sources(&self) -> bool {
!self.c.is_empty()
}
}
#[must_use]
pub fn requested_standards_of(graph: &crate::BuildGraph) -> RequestedStandards {
use cabin_core::LanguageStandard;
use cabin_driver::BuildAction;
let mut out = RequestedStandards::default();
for action in &graph.actions {
if let BuildAction::Compile(compile) = action {
match compile.standard {
LanguageStandard::C(standard) => {
out.c.insert(standard);
}
LanguageStandard::Cxx(standard) => {
out.cxx.insert(standard);
}
}
}
}
out
}
#[must_use]
pub fn collect_requested_standards<S: BuildHasher>(
graph: &PackageGraph,
selected: &BTreeSet<usize>,
standards: &HashMap<usize, ResolvedLanguageStandards, S>,
dev_for: &BTreeSet<String>,
) -> RequestedStandards {
let mut out = RequestedStandards::default();
for &idx in selected {
let Some(pkg) = graph.packages.get(idx) else {
continue;
};
let resolved = standards.get(&idx).copied().unwrap_or_default();
let include_dev = dev_for.contains(pkg.package.name.as_str());
for target in &pkg.package.targets {
if target.kind.is_dev_only() && !include_dev {
continue;
}
for source in &target.sources {
match classify_source(source) {
Some(SourceLanguage::C) => {
out.c.insert(effective_c(&resolved, target).standard);
}
Some(SourceLanguage::Cxx) => {
out.cxx.insert(effective_cxx(&resolved, target).standard);
}
None => {}
}
}
}
}
out
}
pub fn validate_toolchain_for_backend(
toolchain: &ResolvedToolchain,
report: &ToolchainDetectionReport,
) -> Result<(), BuildError> {
let cxx_spec = toolchain.cxx.spec.display();
validate_cxx_for_backend(&cxx_spec, &report.cxx.identity, &report.cxx.capabilities)?;
let ar_spec = toolchain.ar.spec.display();
validate_ar_for_backend(&ar_spec, &report.ar.identity, &report.ar.capabilities)?;
let cxx_is_msvc = report.cxx.identity.kind.speaks_msvc_dialect();
let ar_is_msvc = report.ar.identity.kind == ArchiverKind::Lib;
if cxx_is_msvc != ar_is_msvc {
return Err(BuildError::MixedToolchainDialects {
detail: format!(
"C++ compiler `{cxx_spec}` is {}, but archiver `{ar_spec}` is {}",
dialect_label(cxx_is_msvc),
dialect_label(ar_is_msvc),
),
});
}
Ok(())
}
pub fn validate_toolchain_standards(
toolchain: &ResolvedToolchain,
report: &ToolchainDetectionReport,
requested: &RequestedStandards,
) -> Result<(), BuildError> {
let cxx_spec = toolchain.cxx.spec.display();
validate_cxx_standards(&cxx_spec, &report.cxx.identity, &requested.cxx)?;
if requested.has_c_sources()
&& let (Some(cc_tool), Some(cc_detection)) = (toolchain.cc.as_ref(), report.cc.as_ref())
{
let cc_spec = cc_tool.spec.display();
validate_cc_for_backend(&cc_spec, &cc_detection.identity, &cc_detection.capabilities)?;
let cxx_is_msvc = report.cxx.identity.kind.speaks_msvc_dialect();
let cc_is_msvc = cc_detection.identity.kind.speaks_msvc_dialect();
if cc_is_msvc != cxx_is_msvc {
return Err(BuildError::MixedToolchainDialects {
detail: format!(
"C++ compiler `{cxx_spec}` is {}, but C compiler `{cc_spec}` is {}",
dialect_label(cxx_is_msvc),
dialect_label(cc_is_msvc),
),
});
}
validate_c_standards(&cc_spec, &cc_detection.identity, &requested.c)?;
}
Ok(())
}
pub fn validate_planned_standards(graph: &crate::BuildGraph) -> Result<(), BuildError> {
match graph.standard_violations.first() {
Some(crate::StandardViolation::MsvcSpelling {
target,
language,
standard,
..
}) => Err(BuildError::StandardUnsupportedOnMsvcDialect {
target: target.clone(),
language,
standard,
}),
Some(crate::StandardViolation::FlagConflict { conflict, .. }) => {
Err(BuildError::StandardFlagConflict(Box::new(conflict.clone())))
}
Some(crate::StandardViolation::InterfaceIncompatibility {
consumer,
dependency,
language,
consumer_standard,
required,
requirement_source,
..
}) => Err(BuildError::IncompatibleLanguageStandard {
consumer: consumer.clone(),
dependency: dependency.clone(),
language,
consumer_standard,
required,
requirement_source,
}),
None => Ok(()),
}
}
fn dialect_label(is_msvc: bool) -> &'static str {
if is_msvc { "MSVC" } else { "GCC/Clang-style" }
}
#[must_use]
pub fn msvc_external_includes_supported(
report: &ToolchainDetectionReport,
has_c_sources: bool,
) -> bool {
let cxx_ok = report.cxx.capabilities.external_include_dirs.supported;
let cc_ok = !has_c_sources
|| report
.cc
.as_ref()
.is_none_or(|cc| cc.capabilities.external_include_dirs.supported);
cxx_ok && cc_ok
}
#[cfg(test)]
mod tests {
use super::*;
use cabin_core::{
ArchiverIdentity, ArchiverKind, CompilerIdentity, CompilerKind, CompilerVersion,
ResolvedTool, ResolvedToolchain, StandardDeclaration, ToolDetection, ToolKind, ToolSource,
ToolSpec, derive_ar_capabilities, derive_cxx_capabilities,
};
use camino::Utf8PathBuf;
fn requested(c: &[CStandard], cxx: &[CxxStandard]) -> RequestedStandards {
RequestedStandards {
c: c.iter().copied().collect(),
cxx: cxx.iter().copied().collect(),
}
}
fn requested_defaults(with_c: bool) -> RequestedStandards {
if with_c {
requested(&[CStandard::C11], &[CxxStandard::Cxx17])
} else {
requested(&[], &[CxxStandard::Cxx17])
}
}
fn make_toolchain(cxx_spec: &str, ar_spec: &str) -> ResolvedToolchain {
ResolvedToolchain {
cxx: ResolvedTool {
kind: ToolKind::CxxCompiler,
path: Utf8PathBuf::from("/bin").join(cxx_spec),
spec: ToolSpec::Name(cxx_spec.into()),
source: ToolSource::Default,
},
ar: ResolvedTool {
kind: ToolKind::Archiver,
path: Utf8PathBuf::from("/bin").join(ar_spec),
spec: ToolSpec::Name(ar_spec.into()),
source: ToolSource::Default,
},
cc: None,
}
}
fn report_for(cxx: CompilerIdentity, ar: ArchiverIdentity) -> ToolchainDetectionReport {
let cxx_caps = derive_cxx_capabilities(&cxx);
let ar_caps = derive_ar_capabilities(&ar);
ToolchainDetectionReport {
cxx: ToolDetection {
path: Utf8PathBuf::from("/bin/cxx"),
identity: cxx,
capabilities: cxx_caps,
},
cc: None,
ar: ToolDetection {
path: Utf8PathBuf::from("/bin/ar"),
identity: ar,
capabilities: ar_caps,
},
}
}
#[test]
fn accepts_clang_with_gnu_ar() {
let toolchain = make_toolchain("clang++", "ar");
let report = report_for(
CompilerIdentity {
kind: CompilerKind::Clang,
version: CompilerVersion::parse("17.0.6"),
target: None,
raw_version_line: "clang version 17.0.6".into(),
},
ArchiverIdentity {
kind: ArchiverKind::Ar,
version: CompilerVersion::parse("2.40"),
raw_version_line: "GNU ar".into(),
},
);
validate_toolchain_for_backend(&toolchain, &report).unwrap();
}
#[test]
fn accepts_full_msvc_toolchain() {
let toolchain = make_toolchain("cl.exe", "lib.exe");
let report = report_for(
CompilerIdentity {
kind: CompilerKind::Msvc,
version: None,
target: None,
raw_version_line: "Microsoft Optimizing Compiler".into(),
},
ArchiverIdentity {
kind: ArchiverKind::Lib,
version: None,
raw_version_line: "Microsoft Library Manager".into(),
},
);
validate_toolchain_for_backend(&toolchain, &report).unwrap();
}
#[test]
fn msvc_external_includes_follow_the_cl_version() {
let lib = || ArchiverIdentity {
kind: ArchiverKind::Lib,
version: None,
raw_version_line: "Microsoft Library Manager".into(),
};
let cl = |version: &str| CompilerIdentity {
kind: CompilerKind::Msvc,
version: CompilerVersion::parse(version),
target: None,
raw_version_line: format!("Microsoft Optimizing Compiler {version}"),
};
let modern = report_for(cl("19.29.30133"), lib());
assert!(msvc_external_includes_supported(&modern, false));
let old = report_for(cl("19.20.27508"), lib());
assert!(!msvc_external_includes_supported(&old, false));
}
#[test]
fn msvc_external_includes_consider_cc_only_with_c_sources() {
let lib = ArchiverIdentity {
kind: ArchiverKind::Lib,
version: None,
raw_version_line: "Microsoft Library Manager".into(),
};
let modern_cl = CompilerIdentity {
kind: CompilerKind::Msvc,
version: CompilerVersion::parse("19.39.33523"),
target: None,
raw_version_line: "Microsoft Optimizing Compiler 19.39".into(),
};
let old_cl = CompilerIdentity {
kind: CompilerKind::Msvc,
version: CompilerVersion::parse("19.20.27508"),
target: None,
raw_version_line: "Microsoft Optimizing Compiler 19.20".into(),
};
let mut report = report_for(modern_cl, lib);
let old_caps = derive_cxx_capabilities(&old_cl);
report.cc = Some(ToolDetection {
path: Utf8PathBuf::from("/bin/cc"),
identity: old_cl,
capabilities: old_caps,
});
assert!(msvc_external_includes_supported(&report, false));
assert!(!msvc_external_includes_supported(&report, true));
}
#[test]
fn accepts_clang_cl_with_lib_and_rejects_it_with_gnu_ar() {
let clang_cl = CompilerIdentity {
kind: CompilerKind::ClangCl,
version: CompilerVersion::parse("17.0.6"),
target: None,
raw_version_line: "clang version 17.0.6".into(),
};
let lib = ArchiverIdentity {
kind: ArchiverKind::Lib,
version: None,
raw_version_line: "Microsoft Library Manager".into(),
};
let report = report_for(clang_cl.clone(), lib);
validate_toolchain_for_backend(&make_toolchain("clang-cl", "lib.exe"), &report).unwrap();
let gnu_ar = ArchiverIdentity {
kind: ArchiverKind::Ar,
version: CompilerVersion::parse("2.40"),
raw_version_line: "GNU ar".into(),
};
let mixed = report_for(clang_cl, gnu_ar);
let err =
validate_toolchain_for_backend(&make_toolchain("clang-cl", "ar"), &mixed).unwrap_err();
assert!(
matches!(err, BuildError::MixedToolchainDialects { .. }),
"clang-cl + GNU ar should be rejected as mixed-dialect, got: {err}"
);
}
#[test]
fn rejects_mixed_dialect_toolchain() {
let toolchain = make_toolchain("clang++", "lib.exe");
let report = report_for(
CompilerIdentity {
kind: CompilerKind::Clang,
version: CompilerVersion::parse("17.0.6"),
target: None,
raw_version_line: "clang version 17.0.6".into(),
},
ArchiverIdentity {
kind: ArchiverKind::Lib,
version: None,
raw_version_line: "Microsoft Library Manager".into(),
},
);
let err = validate_toolchain_for_backend(&toolchain, &report).unwrap_err();
assert!(
matches!(err, BuildError::MixedToolchainDialects { .. }),
"expected mixed-dialect rejection, got: {err}"
);
}
#[test]
fn defers_cc_dialect_check_until_c_compiles_are_planned() {
let mut toolchain = make_toolchain("clang++", "ar");
toolchain.cc = Some(ResolvedTool {
kind: ToolKind::CCompiler,
path: Utf8PathBuf::from("/bin/cl.exe"),
spec: ToolSpec::Name("cl".into()),
source: ToolSource::Default,
});
let cc_identity = CompilerIdentity {
kind: CompilerKind::Msvc,
version: CompilerVersion::parse("19.39.0"),
target: None,
raw_version_line: "Microsoft Optimizing Compiler".into(),
};
let mut report = report_for(
CompilerIdentity {
kind: CompilerKind::Clang,
version: CompilerVersion::parse("17.0.6"),
target: None,
raw_version_line: "clang version 17.0.6".into(),
},
ArchiverIdentity {
kind: ArchiverKind::Ar,
version: CompilerVersion::parse("2.40"),
raw_version_line: "GNU ar".into(),
},
);
report.cc = Some(ToolDetection {
path: Utf8PathBuf::from("/bin/cl.exe"),
capabilities: derive_cxx_capabilities(&cc_identity),
identity: cc_identity,
});
validate_toolchain_for_backend(&toolchain, &report).unwrap();
validate_toolchain_standards(&toolchain, &report, &requested_defaults(false)).unwrap();
let err = validate_toolchain_standards(&toolchain, &report, &requested_defaults(true))
.unwrap_err();
assert!(
matches!(err, BuildError::MixedToolchainDialects { .. }),
"expected mixed-dialect rejection once C compiles are planned, got: {err}"
);
}
#[test]
fn rejects_unknown_compiler_clearly() {
let toolchain = make_toolchain("custom-cxx", "ar");
let report = report_for(
CompilerIdentity::unknown("???"),
ArchiverIdentity {
kind: ArchiverKind::Ar,
version: None,
raw_version_line: "GNU ar".into(),
},
);
let err = validate_toolchain_for_backend(&toolchain, &report).unwrap_err();
let message = err.to_string();
assert!(
message.contains("could not be identified"),
"expected unknown-compiler error, got: {message}"
);
}
#[test]
fn c_source_detection_is_scoped_to_the_selected_closure() {
use cabin_core::{Package, PackageName, Target, TargetKind, TargetName};
use cabin_workspace::{PackageKind, WorkspacePackage};
fn pkg(name: &str, source: &str) -> WorkspacePackage {
let target = Target {
name: TargetName::new("lib").unwrap(),
kind: TargetKind::Library,
sources: vec![Utf8PathBuf::from(source)],
include_dirs: Vec::new(),
defines: Vec::new(),
deps: Vec::new(),
language: Default::default(),
};
let package = Package::new(
PackageName::new(name).unwrap(),
semver::Version::parse("0.1.0").unwrap(),
vec![target],
Vec::new(),
)
.unwrap();
let dir = std::path::PathBuf::from("/tmp").join(name);
WorkspacePackage {
package,
manifest_path: dir.join("cabin.toml"),
manifest_dir: dir,
deps: Vec::new(),
kind: PackageKind::Local,
is_port: false,
}
}
let graph = PackageGraph {
root_manifest_path: std::path::PathBuf::from("/tmp/cabin.toml"),
root_dir: std::path::PathBuf::from("/tmp"),
is_workspace_root: true,
root_package: None,
root_settings: Default::default(),
primary_packages: vec![0, 1],
default_members: vec![0, 1],
excluded_members: Vec::new(),
packages: vec![pkg("with_c", "a.c"), pkg("cpp_only", "b.cc")],
};
let standards = HashMap::new();
let no_dev = BTreeSet::new();
let cpp_only =
collect_requested_standards(&graph, &BTreeSet::from([1usize]), &standards, &no_dev);
assert!(!cpp_only.has_c_sources());
assert_eq!(
cpp_only.cxx,
BTreeSet::from([cabin_core::DEFAULT_CXX_STANDARD])
);
let with_c =
collect_requested_standards(&graph, &BTreeSet::from([0usize]), &standards, &no_dev);
assert!(with_c.has_c_sources());
assert_eq!(with_c.c, BTreeSet::from([cabin_core::DEFAULT_C_STANDARD]));
let both = collect_requested_standards(
&graph,
&BTreeSet::from([0usize, 1usize]),
&standards,
&no_dev,
);
assert!(both.has_c_sources());
}
#[test]
fn requested_standards_skip_dev_only_targets_unless_activated() {
use cabin_core::{
CxxStandard, LanguageStandardSettings, Package, PackageName, Target, TargetKind,
TargetName,
};
use cabin_workspace::{PackageKind, WorkspacePackage};
let test_target = Target {
name: TargetName::new("t").unwrap(),
kind: TargetKind::Test,
sources: vec![Utf8PathBuf::from("t.cc")],
include_dirs: Vec::new(),
defines: Vec::new(),
deps: Vec::new(),
language: LanguageStandardSettings {
cxx_standard: Some(StandardDeclaration::Declared(CxxStandard::Cxx20)),
..Default::default()
},
};
let package = Package::new(
PackageName::new("demo").unwrap(),
semver::Version::parse("0.1.0").unwrap(),
vec![test_target],
Vec::new(),
)
.unwrap();
let dir = std::path::PathBuf::from("/tmp/demo");
let graph = PackageGraph {
root_manifest_path: dir.join("cabin.toml"),
root_dir: dir.clone(),
is_workspace_root: false,
root_package: Some(0),
root_settings: Default::default(),
primary_packages: vec![0],
default_members: vec![0],
excluded_members: Vec::new(),
packages: vec![WorkspacePackage {
package,
manifest_path: dir.join("cabin.toml"),
manifest_dir: dir,
deps: Vec::new(),
kind: PackageKind::Local,
is_port: false,
}],
};
let standards = HashMap::new();
let plain = collect_requested_standards(
&graph,
&BTreeSet::from([0usize]),
&standards,
&BTreeSet::new(),
);
assert!(plain.cxx.is_empty());
let dev = collect_requested_standards(
&graph,
&BTreeSet::from([0usize]),
&standards,
&BTreeSet::from(["demo".to_owned()]),
);
assert_eq!(dev.cxx, BTreeSet::from([CxxStandard::Cxx20]));
}
#[test]
fn standards_validation_rejects_unsupported_requested_standard() {
use cabin_core::CxxStandard;
let toolchain = make_toolchain("cl.exe", "lib.exe");
let report = report_for(
CompilerIdentity {
kind: CompilerKind::Msvc,
version: CompilerVersion::parse("19.39.33523"),
target: None,
raw_version_line: "Microsoft Optimizing Compiler 19.39".into(),
},
ArchiverIdentity {
kind: ArchiverKind::Lib,
version: None,
raw_version_line: "Microsoft Library Manager".into(),
},
);
validate_toolchain_for_backend(&toolchain, &report).unwrap();
validate_toolchain_standards(&toolchain, &report, &requested_defaults(false)).unwrap();
let err = validate_toolchain_standards(
&toolchain,
&report,
&requested(&[], &[CxxStandard::Cxx23]),
)
.unwrap_err();
assert!(
err.to_string().contains("c++23"),
"expected the offending standard to be named, got: {err}"
);
}
}