use anyhow::{Context, Result};
use regex::Regex;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use super::flags::{FlagClass, FlagSpec, Matcher};
use super::{
ArtifactKind, ArtifactSet, CompileResult, Compiler, CompilerAdapter, CompilerId, KeyCtx,
RefuseReason, classify_by_filename,
};
pub const CC_ID: CompilerId = CompilerId::new("cc");
pub const ADAPTER: CompilerAdapter =
CompilerAdapter::new(CC_ID, "C-family compiler", CcCompiler::recognizes);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompileMode {
Compile,
Link,
Preprocess,
Assemble,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OptLevel {
O0,
O1,
O2,
O3,
Os,
Oz,
Og,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct DepInfoSpec {
pub emit: bool,
pub include_system: bool,
pub phony_targets: bool,
pub missing_generated: bool,
pub output: Option<PathBuf>,
pub target: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CcArgs {
pub program: String,
pub rest: Vec<String>,
pub sources: Vec<PathBuf>,
pub output: Option<PathBuf>,
pub mode: CompileMode,
pub includes: Vec<PathBuf>,
pub defines: Vec<(String, Option<String>)>,
pub optimization: Option<OptLevel>,
pub debug_level: Option<u8>,
pub std: Option<String>,
pub pic: bool,
pub depinfo: Option<DepInfoSpec>,
pub language_override: Option<String>,
}
const SOURCE_EXTENSIONS: &[&str] = &[
"c", "cc", "cpp", "cxx", "c++", "C", "m", "mm", "M", "i", "ii", "S", "s", "sx", ];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CcArgValueForm {
Flag,
Separated,
Concatenated { prefix: &'static str },
CanBeSeparated { prefix: &'static str },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CcArgAction {
SetMode(CompileMode),
SetOutput,
SetPic,
SetDebugLevel(u8),
SetOptimization(OptLevel),
SetStd,
DepIncludeSystem(bool),
DepPhonyTargets,
DepMissingGenerated,
DepOutput,
DepTarget,
LanguageOverride,
Include,
Define,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CcArgBucket {
Structural,
ModeledInKey,
ProbeKeyed,
Preprocessor,
#[allow(dead_code)]
RawKeyed,
#[allow(dead_code)]
ExtraHashFile,
Artifact,
NoObjectEffect,
TooHard,
}
#[derive(Debug, Clone, Copy)]
struct CcArgSpec {
matcher: Matcher,
value_form: CcArgValueForm,
action: CcArgAction,
bucket: CcArgBucket,
source: &'static str,
}
#[derive(Debug, Clone)]
struct ParsedCcArg {
spec: &'static CcArgSpec,
value: Option<String>,
consumed: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct CcArgAnalysis<'a> {
arg: &'a str,
class: Option<FlagClass>,
bucket: CcArgBucket,
normalized: Vec<String>,
refusal: Option<&'static str>,
source: Option<&'static str>,
}
static CC_ARG_SPECS: &[CcArgSpec] = &[
CcArgSpec {
matcher: Matcher::Exact("-c"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetMode(CompileMode::Compile),
bucket: CcArgBucket::Structural,
source: "compile mode marker",
},
CcArgSpec {
matcher: Matcher::Exact("-E"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetMode(CompileMode::Preprocess),
bucket: CcArgBucket::Structural,
source: "preprocess mode marker",
},
CcArgSpec {
matcher: Matcher::Exact("-S"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetMode(CompileMode::Assemble),
bucket: CcArgBucket::Structural,
source: "assembly mode marker",
},
CcArgSpec {
matcher: Matcher::Exact("-o"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::SetOutput,
bucket: CcArgBucket::Artifact,
source: "primary output path",
},
CcArgSpec {
matcher: Matcher::Exact("-fPIC"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetPic,
bucket: CcArgBucket::ModeledInKey,
source: "position-independent code",
},
CcArgSpec {
matcher: Matcher::Exact("-fpic"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetPic,
bucket: CcArgBucket::ModeledInKey,
source: "position-independent code",
},
CcArgSpec {
matcher: Matcher::Exact("-g"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(2),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
},
CcArgSpec {
matcher: Matcher::Exact("-g0"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(0),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
},
CcArgSpec {
matcher: Matcher::Exact("-g1"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(1),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
},
CcArgSpec {
matcher: Matcher::Exact("-g2"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(2),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
},
CcArgSpec {
matcher: Matcher::Exact("-g3"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(3),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
},
CcArgSpec {
matcher: Matcher::Exact("-O"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O1),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-O0"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O0),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-O1"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O1),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-O2"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O2),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-O3"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O3),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-Os"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::Os),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-Oz"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::Oz),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-Og"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::Og),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
},
CcArgSpec {
matcher: Matcher::Exact("-MD"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepIncludeSystem(true),
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar",
},
CcArgSpec {
matcher: Matcher::Exact("-MMD"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepIncludeSystem(false),
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar",
},
CcArgSpec {
matcher: Matcher::Exact("-MP"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepPhonyTargets,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar phony targets",
},
CcArgSpec {
matcher: Matcher::Exact("-MG"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepMissingGenerated,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar generated headers",
},
CcArgSpec {
matcher: Matcher::Exact("-MF"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::DepOutput,
bucket: CcArgBucket::Artifact,
source: "dependency output path",
},
CcArgSpec {
matcher: Matcher::Exact("-MT"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::DepTarget,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency target",
},
CcArgSpec {
matcher: Matcher::Exact("-MQ"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::DepTarget,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency target",
},
CcArgSpec {
matcher: Matcher::Prefix("-x"),
value_form: CcArgValueForm::CanBeSeparated { prefix: "-x" },
action: CcArgAction::LanguageOverride,
bucket: CcArgBucket::ProbeKeyed,
source: "language override",
},
CcArgSpec {
matcher: Matcher::Prefix("-I"),
value_form: CcArgValueForm::CanBeSeparated { prefix: "-I" },
action: CcArgAction::Include,
bucket: CcArgBucket::Preprocessor,
source: "include search path",
},
CcArgSpec {
matcher: Matcher::Prefix("-D"),
value_form: CcArgValueForm::CanBeSeparated { prefix: "-D" },
action: CcArgAction::Define,
bucket: CcArgBucket::Preprocessor,
source: "preprocessor define",
},
CcArgSpec {
matcher: Matcher::Prefix("-std="),
value_form: CcArgValueForm::Concatenated { prefix: "-std=" },
action: CcArgAction::SetStd,
bucket: CcArgBucket::ModeledInKey,
source: "language standard",
},
];
impl CcArgs {
pub fn parse(args: &[String]) -> Result<Self> {
let (program, rest) = args
.split_first()
.context("cc invocation missing argv[0]")?;
let mut parsed = CcArgs {
program: program.clone(),
rest: rest.to_vec(),
sources: Vec::new(),
output: None,
mode: CompileMode::Link, includes: Vec::new(),
defines: Vec::new(),
optimization: None,
debug_level: None,
std: None,
pic: false,
depinfo: None,
language_override: None,
};
let mut depinfo: Option<DepInfoSpec> = None;
let mut idx = 0;
while idx < rest.len() {
if let Some(arg) = parse_cc_arg_at(rest, idx) {
apply_cc_arg(&mut parsed, &mut depinfo, &arg);
idx += arg.consumed;
continue;
}
let arg = &rest[idx];
if !arg.starts_with('-') && looks_like_source(arg) {
parsed.sources.push(PathBuf::from(arg));
}
idx += 1;
}
parsed.depinfo = depinfo;
Ok(parsed)
}
pub fn refuse_reasons(&self, extra_allowlist_flags: &[String]) -> Vec<RefuseReason> {
let mut reasons = Vec::new();
match self.mode {
CompileMode::Compile => {}
CompileMode::Link => reasons.push(RefuseReason::Unsupported(
"cc: link mode (whole-program caching not yet supported)",
)),
CompileMode::Preprocess => reasons.push(RefuseReason::Unsupported(
"cc: preprocessor mode -E (not yet supported)",
)),
CompileMode::Assemble => reasons.push(RefuseReason::Unsupported(
"cc: assembly mode -S (not yet supported)",
)),
}
if let Some(output) = &self.output
&& output.as_os_str() == "-"
{
reasons.push(RefuseReason::Unsupported(
"cc: output to stdout (not yet supported)",
));
}
if !reasons.is_empty() {
return reasons;
}
if self.rest.iter().any(|a| a.starts_with('@')) {
reasons.push(RefuseReason::Unsupported(
"cc: response file @file (expansion not yet supported)",
));
}
let arch_count = self.rest.windows(2).filter(|w| w[0] == "-arch").count();
if arch_count > 1 {
reasons.push(RefuseReason::Unsupported(
"cc: multi-arch -arch X -arch Y (fat-binary caching not yet supported)",
));
}
for flag in &["--coverage", "-fprofile-arcs", "-ftest-coverage"] {
if self.rest.iter().any(|a| a == flag) {
reasons.push(RefuseReason::Unsupported(
"cc: coverage instrumentation (not yet supported)",
));
break;
}
}
if self.rest.iter().any(|a| a == "-gsplit-dwarf") {
reasons.push(RefuseReason::Unsupported(
"cc: -gsplit-dwarf (not yet supported)",
));
}
for flag in &["-include-pch", "-emit-pch"] {
if self.rest.iter().any(|a| a == flag) {
reasons.push(RefuseReason::Unsupported(
"cc: precompiled headers (not yet supported)",
));
break;
}
}
let mut iter = self.rest.iter().peekable();
while let Some(arg) = iter.next() {
if arg == "-include"
&& let Some(next) = iter.peek()
&& (next.ends_with(".pch") || next.ends_with(".gch"))
{
reasons.push(RefuseReason::Unsupported(
"cc: precompiled headers (not yet supported)",
));
break;
}
}
for flag in &["-fmodules", "-fcxx-modules"] {
if self.rest.iter().any(|a| a == flag) {
reasons.push(RefuseReason::Unsupported("cc: modules (not yet supported)"));
break;
}
}
let rejected = classify_and_trace_cc_flags(self, extra_allowlist_flags);
if !rejected.is_empty() {
let detail: &'static str = Box::leak(
format!("cc: unsupported flag(s): {}", rejected.join(" ")).into_boxed_str(),
);
tracing::debug!("{detail} — passthrough");
reasons.push(RefuseReason::Unsupported(detail));
}
if self.sources.len() > 1 {
reasons.push(RefuseReason::Unsupported(
"cc: multi-source compile (per-source split not yet supported)",
));
} else if self.sources.is_empty() {
reasons.push(RefuseReason::Unsupported(
"cc: no source file (not yet supported)",
));
}
reasons
}
pub fn object_output_path(&self) -> Option<PathBuf> {
if let Some(o) = &self.output {
return Some(o.clone());
}
let stem = self.sources.first()?.file_stem()?;
Some(PathBuf::from(format!("{}.o", stem.to_string_lossy())))
}
pub fn depinfo_output_path(&self) -> Option<PathBuf> {
let depinfo = self.depinfo.as_ref()?;
if !depinfo.emit {
return None;
}
if let Some(output) = &depinfo.output {
return Some(output.clone());
}
let mut object = self.object_output_path()?;
object.set_extension("d");
Some(object)
}
pub fn depinfo_anchor(&self) -> Option<PathBuf> {
self.depinfo_output_path()?;
let object = self.object_output_path()?;
Some(
object
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from(".")),
)
}
pub fn cache_target_arch(&self) -> String {
cc_target_arch(self)
}
pub fn config_args(&self) -> Vec<String> {
let mut out = Vec::new();
let mut iter = self.rest.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"-o" | "-MF" | "-MT" | "-MQ" => {
iter.next(); }
_ if self
.sources
.iter()
.any(|s| s.to_str() == Some(arg.as_str())) => {}
_ => out.push(arg.clone()),
}
}
out
}
}
fn parse_cc_arg_at(args: &[String], idx: usize) -> Option<ParsedCcArg> {
let arg = args.get(idx)?;
CC_ARG_SPECS
.iter()
.find_map(|spec| parse_cc_arg_with_spec(spec, args, idx, arg))
}
fn parse_cc_arg_with_spec(
spec: &'static CcArgSpec,
args: &[String],
idx: usize,
arg: &str,
) -> Option<ParsedCcArg> {
match spec.value_form {
CcArgValueForm::Flag => cc_arg_spec_matches(spec, arg).then_some(ParsedCcArg {
spec,
value: None,
consumed: 1,
}),
CcArgValueForm::Separated => cc_arg_spec_matches(spec, arg).then(|| ParsedCcArg {
spec,
value: args.get(idx + 1).cloned(),
consumed: if args.get(idx + 1).is_some() { 2 } else { 1 },
}),
CcArgValueForm::Concatenated { prefix } => {
arg.strip_prefix(prefix).map(|value| ParsedCcArg {
spec,
value: Some(value.to_string()),
consumed: 1,
})
}
CcArgValueForm::CanBeSeparated { prefix } => {
if arg == prefix {
Some(ParsedCcArg {
spec,
value: args.get(idx + 1).cloned(),
consumed: if args.get(idx + 1).is_some() { 2 } else { 1 },
})
} else {
arg.strip_prefix(prefix)
.filter(|value| !value.is_empty())
.map(|value| ParsedCcArg {
spec,
value: Some(value.to_string()),
consumed: 1,
})
}
}
}
}
fn cc_arg_spec_matches(spec: &CcArgSpec, arg: &str) -> bool {
match spec.matcher {
Matcher::Exact(s) => arg == s,
Matcher::Prefix(s) => arg.starts_with(s),
Matcher::Regex(pat) => Regex::new(&format!("^(?:{pat})$"))
.map(|re| re.is_match(arg))
.unwrap_or(false),
}
}
fn apply_cc_arg(parsed: &mut CcArgs, depinfo: &mut Option<DepInfoSpec>, arg: &ParsedCcArg) {
match arg.spec.action {
CcArgAction::SetMode(mode) => parsed.mode = mode,
CcArgAction::SetOutput => {
if let Some(value) = &arg.value {
parsed.output = Some(PathBuf::from(value));
}
}
CcArgAction::SetPic => parsed.pic = true,
CcArgAction::SetDebugLevel(level) => parsed.debug_level = Some(level),
CcArgAction::SetOptimization(level) => parsed.optimization = Some(level),
CcArgAction::SetStd => {
if let Some(value) = &arg.value {
parsed.std = Some(value.clone());
}
}
CcArgAction::DepIncludeSystem(include_system) => {
let d = depinfo.get_or_insert_with(DepInfoSpec::default);
d.emit = true;
d.include_system = include_system;
}
CcArgAction::DepPhonyTargets => {
let d = depinfo.get_or_insert_with(DepInfoSpec::default);
d.phony_targets = true;
}
CcArgAction::DepMissingGenerated => {
let d = depinfo.get_or_insert_with(DepInfoSpec::default);
d.missing_generated = true;
}
CcArgAction::DepOutput => {
if let Some(value) = &arg.value {
let d = depinfo.get_or_insert_with(DepInfoSpec::default);
d.output = Some(PathBuf::from(value));
}
}
CcArgAction::DepTarget => {
if let Some(value) = &arg.value {
let d = depinfo.get_or_insert_with(DepInfoSpec::default);
d.target = Some(value.clone());
}
}
CcArgAction::LanguageOverride => {
if let Some(value) = &arg.value {
parsed.language_override = Some(value.clone());
}
}
CcArgAction::Include => {
if let Some(value) = &arg.value {
parsed.includes.push(PathBuf::from(value));
}
}
CcArgAction::Define => {
if let Some(value) = &arg.value {
parsed.defines.push(parse_define(value));
}
}
}
}
const CC_ROOT_SENTINEL: &str = "<CC_ROOT>";
const CC_BUILD_SENTINEL: &str = "<CC_BUILD>";
const CC_SOURCE_SENTINEL: &str = "<CC_SOURCE>";
const CC_BASE_SENTINEL: &str = "<CC_BASE>";
#[derive(Debug, Clone, PartialEq, Eq)]
struct CcPrefixMap {
from: String,
to: &'static str,
}
fn cc_target_arch(parsed: &CcArgs) -> String {
parsed
.rest
.windows(2)
.find(|w| w[0] == "-arch")
.map(|w| w[1].clone())
.unwrap_or_else(|| std::env::consts::ARCH.to_string())
}
fn build_preprocess_args(parsed: &CcArgs) -> Vec<String> {
let mut out = vec!["-E".to_string(), "-P".to_string()];
let mut iter = parsed.rest.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"-c" | "-S" => {}
"-o" | "-MF" | "-MT" | "-MQ" => {
iter.next(); }
"-MMD" | "-MD" | "-MP" | "-MG" => {}
_ => out.push(arg.clone()),
}
}
out
}
fn preprocess_hash(parsed: &CcArgs, prefix_maps: &[CcPrefixMap]) -> Result<String> {
let pp_args = build_preprocess_args(parsed);
crate::opcounts::record_preprocessor_run();
let output = Command::new(&parsed.program)
.args(&pp_args)
.env("SOURCE_DATE_EPOCH", "0")
.output()
.with_context(|| format!("running preprocessor `{}`", parsed.program))?;
if !output.status.success() {
anyhow::bail!("preprocessor exited {} for cache key", output.status);
}
let stdout = apply_cc_prefix_maps_to_bytes(output.stdout, prefix_maps);
Ok(blake3::hash(&stdout).to_hex().to_string())
}
fn looks_like_source(arg: &str) -> bool {
Path::new(arg)
.extension()
.and_then(|e| e.to_str())
.map(|e| SOURCE_EXTENSIONS.contains(&e))
.unwrap_or(false)
}
fn parse_define(s: &str) -> (String, Option<String>) {
match s.split_once('=') {
Some((name, value)) => (name.to_string(), Some(value.to_string())),
None => (s.to_string(), None),
}
}
pub static CC_FLAGS: &[FlagSpec] = &[
FlagSpec {
matcher: Matcher::Regex(r"-O[0-3sz]?|-Og"),
class: FlagClass::ModeledInKey,
source: "PR #94 — opt level. Regex captures family; -Ofast/+others fall through to refuse.",
},
FlagSpec {
matcher: Matcher::Regex(r"-g[0-3]?"),
class: FlagClass::ModeledInKey,
source: "PR #94 — debug level. Regex captures `-g`/`-g0..3`; -gdwarf-* etc. refuse.",
},
FlagSpec {
matcher: Matcher::Exact("-fPIC"),
class: FlagClass::ModeledInKey,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-fpic"),
class: FlagClass::ModeledInKey,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Prefix("-std="),
class: FlagClass::ModeledInKey,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-arch"),
class: FlagClass::ModeledInKey,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-c"),
class: FlagClass::ParserHandled,
source: "PR #94 — compile mode marker parsed into CompileMode.",
},
FlagSpec {
matcher: Matcher::Exact("-E"),
class: FlagClass::ParserHandled,
source: "Flag audit — preprocessor mode marker parsed into CompileMode.",
},
FlagSpec {
matcher: Matcher::Exact("-S"),
class: FlagClass::ParserHandled,
source: "Flag audit — assembly mode marker parsed into CompileMode.",
},
FlagSpec {
matcher: Matcher::Prefix("-mmacosx-version-min="),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — Darwin deployment target.",
},
FlagSpec {
matcher: Matcher::Prefix("-fstrict-flex-arrays="),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — strict-flex-arrays codegen knob.",
},
FlagSpec {
matcher: Matcher::Prefix("-ffp-contract="),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — fp-contract codegen knob.",
},
FlagSpec {
matcher: Matcher::Exact("-pthread"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — pthread feature switch (also visible via _REENTRANT in preprocessor).",
},
FlagSpec {
matcher: Matcher::Exact("-fstack-protector-strong"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — stack-protector codegen mode.",
},
FlagSpec {
matcher: Matcher::Exact("-fstack-clash-protection"),
class: FlagClass::CapturedByProbe,
source: "Issue #245 — stack-clash-protection codegen hardening (Firefox).",
},
FlagSpec {
matcher: Matcher::Exact("-fno-math-errno"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — math-errno codegen knob.",
},
FlagSpec {
matcher: Matcher::Exact("-fno-strict-aliasing"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — alias-analysis codegen knob.",
},
FlagSpec {
matcher: Matcher::Exact("-fno-omit-frame-pointer"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — frame-pointer codegen knob.",
},
FlagSpec {
matcher: Matcher::Exact("-funwind-tables"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — unwind-tables codegen knob.",
},
FlagSpec {
matcher: Matcher::Exact("-gdwarf-4"),
class: FlagClass::CapturedByProbe,
source: "Issue #117 — DWARF v4 emission (Firefox baseline).",
},
FlagSpec {
matcher: Matcher::Exact("-gsimple-template-names"),
class: FlagClass::CapturedByProbe,
source: "Issue #117 — clang template-name compression in debug info.",
},
FlagSpec {
matcher: Matcher::Exact("-mllvm=-dwarf-linkage-names=Abstract"),
class: FlagClass::CapturedByProbe,
source: "Issue #117 — LLVM debug-info abstraction (Firefox baseline). Listed by exact value rather than `-mllvm=*` wildcard so unmodeled LLVM flags still refuse.",
},
FlagSpec {
matcher: Matcher::Prefix("-ffile-prefix-map="),
class: FlagClass::CapturedByProbe,
source: "Build-system path remapping (e.g. Firefox --enable-path-remapping). Resolved-token hash captures it; per-checkout `from` normalized via cc prefix maps.",
},
FlagSpec {
matcher: Matcher::Prefix("-fdebug-prefix-map="),
class: FlagClass::CapturedByProbe,
source: "Build-system debug-info path remapping. Resolved-token hash captures it; per-checkout `from` normalized via cc prefix maps.",
},
FlagSpec {
matcher: Matcher::Prefix("-fmacro-prefix-map="),
class: FlagClass::CapturedByProbe,
source: "Build-system __FILE__ path remapping. Resolved-token hash captures it; per-checkout `from` normalized via cc prefix maps.",
},
FlagSpec {
matcher: Matcher::Prefix("-stdlib="),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ standard-library selector (libc++ / libstdc++).",
},
FlagSpec {
matcher: Matcher::Exact("-fno-exceptions"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ exception mode (off).",
},
FlagSpec {
matcher: Matcher::Exact("-fexceptions"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ exception mode (on).",
},
FlagSpec {
matcher: Matcher::Exact("-fno-rtti"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ RTTI mode (off).",
},
FlagSpec {
matcher: Matcher::Exact("-frtti"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ RTTI mode (on).",
},
FlagSpec {
matcher: Matcher::Exact("-fno-sized-deallocation"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ sized-deallocation (disabled).",
},
FlagSpec {
matcher: Matcher::Exact("-fno-aligned-new"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ aligned new/delete (disabled).",
},
FlagSpec {
matcher: Matcher::Exact("-fvisibility=hidden"),
class: FlagClass::CapturedByProbe,
source: "Firefox bench evidence (post-#146) — symbol visibility default = hidden.",
},
FlagSpec {
matcher: Matcher::Exact("-fvisibility-inlines-hidden"),
class: FlagClass::CapturedByProbe,
source: "Firefox bench evidence (post-#146) — inline-function visibility default = hidden.",
},
FlagSpec {
matcher: Matcher::Prefix("--target="),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — cross-compilation target triple (sticky form).",
},
FlagSpec {
matcher: Matcher::Exact("-target"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — cross-compilation target triple (separate-arg form).",
},
FlagSpec {
matcher: Matcher::Prefix("-march="),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — architecture selection. `Prefix` is safe because the probe resolves the value into target-cpu/target-feature tokens.",
},
FlagSpec {
matcher: Matcher::Exact("-msimd128"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — WASM SIMD128 enable.",
},
FlagSpec {
matcher: Matcher::Exact("-ffunction-sections"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — function-per-section object layout.",
},
FlagSpec {
matcher: Matcher::Exact("-fdata-sections"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — data-per-section object layout.",
},
FlagSpec {
matcher: Matcher::Exact("-Wa,--noexecstack"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — assembler: non-executable stack section flag. Listed by exact value rather than `-Wa,*` wildcard so unmodeled assembler flags still refuse.",
},
FlagSpec {
matcher: Matcher::Exact("-x"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — language override (separate-arg form).",
},
FlagSpec {
matcher: Matcher::Regex(r"-x(?:c|c\+\+|objective-c|objective-c\+\+)"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 / flag audit — sticky language override forms.",
},
FlagSpec {
matcher: Matcher::Exact("-fobjc-exceptions"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — Objective-C exception model.",
},
FlagSpec {
matcher: Matcher::Exact("-fobjc-arc"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — Objective-C ARC mode.",
},
FlagSpec {
matcher: Matcher::Prefix("-D"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Prefix("-U"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Prefix("-I"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Prefix("--sysroot"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-include"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-imacros"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-isystem"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-iquote"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-idirafter"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-isysroot"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-nostdinc"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-nostdinc++"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-undef"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Regex(r"-W[^,]*"),
class: FlagClass::NoObjectEffect,
source: "PR #94 — warnings. Regex excludes `-Wl,*`/`-Wa,*`/`-Wp,*` passthrough forms.",
},
FlagSpec {
matcher: Matcher::Exact("-w"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Prefix("-pedantic"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Prefix("-fdiagnostics-"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-fcolor-diagnostics"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-fno-color-diagnostics"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Regex(r"-MM?D|-M[FTQPG]"),
class: FlagClass::NoObjectEffect,
source: "PR #94 — dep-info sidecar flags.",
},
FlagSpec {
matcher: Matcher::Exact("-o"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-P"),
class: FlagClass::NoObjectEffect,
source: "Flag audit — preprocessor line-marker suppression has no compile-mode object effect.",
},
FlagSpec {
matcher: Matcher::Exact("-pipe"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("-v"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("--verbose"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
},
FlagSpec {
matcher: Matcher::Exact("--start-no-unused-arguments"),
class: FlagClass::NoObjectEffect,
source: "Issue #117 — clang unused-argument warning region (open).",
},
FlagSpec {
matcher: Matcher::Exact("--end-no-unused-arguments"),
class: FlagClass::NoObjectEffect,
source: "Issue #117 — clang unused-argument warning region (close).",
},
];
#[derive(Debug, Default)]
struct FlagClassificationSummary {
modeled_in_key: usize,
captured_by_probe: usize,
preprocessor_captured: usize,
no_object_effect: usize,
parser_handled: usize,
user_allowed: usize,
unmodeled: usize,
}
impl FlagClassificationSummary {
fn record(&mut self, class: Option<FlagClass>) {
match class {
Some(FlagClass::ModeledInKey) => self.modeled_in_key += 1,
Some(FlagClass::CapturedByProbe) => self.captured_by_probe += 1,
Some(FlagClass::PreprocessorCaptured) => self.preprocessor_captured += 1,
Some(FlagClass::NoObjectEffect) => self.no_object_effect += 1,
Some(FlagClass::ParserHandled) => self.parser_handled += 1,
None => self.unmodeled += 1,
}
}
}
fn classify_and_trace_cc_flags<'a>(
parsed: &'a CcArgs,
extra_allowlist_flags: &[String],
) -> Vec<&'a str> {
let subject = parsed
.sources
.first()
.map(|source| source.display().to_string())
.unwrap_or_else(|| parsed.program.clone());
let mut summary = FlagClassificationSummary::default();
let mut rejected = Vec::new();
for arg in &parsed.rest {
let analysis = analyze_cc_arg(arg);
summary.record(analysis.class);
match analysis.class {
Some(class) => tracing::trace!(
"[cc:{subject}] flag {arg} -> {class:?} [{:?}]",
analysis.bucket
),
None if extra_allowlist_flags.iter().any(|f| f == arg) => {
summary.user_allowed += 1;
tracing::trace!(
"[cc:{subject}] flag {arg} -> user-allowed (config) [verbatim-keyed]"
);
}
None => {
tracing::trace!(
"[cc:{subject}] flag {arg} -> unmodeled [{:?}]",
analysis.bucket
);
rejected.push(arg.as_str());
}
}
}
if !parsed.rest.is_empty() {
tracing::debug!(
"[cc:{subject}] flag classify: {} modeled / {} probe / {} preprocessor / {} no-effect / {} parser-handled / {} user-allowed / {} unmodeled",
summary.modeled_in_key,
summary.captured_by_probe,
summary.preprocessor_captured,
summary.no_object_effect,
summary.parser_handled,
summary.user_allowed,
summary.unmodeled
);
}
rejected
}
fn cc_extra_flags_for_key<'a>(
parsed: &'a CcArgs,
extra_allowlist_flags: &[String],
) -> Vec<&'a str> {
if extra_allowlist_flags.is_empty() {
return Vec::new();
}
let mut matched: Vec<&str> = parsed
.rest
.iter()
.map(String::as_str)
.filter(|arg| {
classify_cc_flag(arg).is_none() && extra_allowlist_flags.iter().any(|f| f == arg)
})
.collect();
matched.sort_unstable();
matched.dedup();
matched
}
fn analyze_cc_arg(arg: &str) -> CcArgAnalysis<'_> {
let class = classify_cc_flag(arg);
let spec = cc_arg_spec_for_token(arg);
CcArgAnalysis {
arg,
class,
bucket: cc_arg_bucket(class, spec),
normalized: normalize_cc_arg(arg),
refusal: class.is_none().then_some("cc: unsupported flag"),
source: spec.map(|spec| spec.source),
}
}
fn cc_arg_bucket(class: Option<FlagClass>, spec: Option<&'static CcArgSpec>) -> CcArgBucket {
if class.is_none() {
return CcArgBucket::TooHard;
}
if let Some(spec) = spec {
return spec.bucket;
}
match class {
Some(FlagClass::ModeledInKey) => CcArgBucket::ModeledInKey,
Some(FlagClass::ParserHandled) => CcArgBucket::Structural,
Some(FlagClass::CapturedByProbe) => CcArgBucket::ProbeKeyed,
Some(FlagClass::PreprocessorCaptured) => CcArgBucket::Preprocessor,
Some(FlagClass::NoObjectEffect) => CcArgBucket::NoObjectEffect,
None => CcArgBucket::TooHard,
}
}
fn normalize_cc_arg(arg: &str) -> Vec<String> {
let Some(spec) = cc_arg_spec_for_token(arg) else {
return vec![arg.to_string()];
};
match spec.value_form {
CcArgValueForm::Flag | CcArgValueForm::Separated => vec![arg.to_string()],
CcArgValueForm::Concatenated { prefix } => arg
.strip_prefix(prefix)
.map(|value| vec![prefix.to_string(), value.to_string()])
.unwrap_or_else(|| vec![arg.to_string()]),
CcArgValueForm::CanBeSeparated { prefix } => {
if arg == prefix {
vec![prefix.to_string()]
} else {
arg.strip_prefix(prefix)
.filter(|value| !value.is_empty())
.map(|value| vec![prefix.to_string(), value.to_string()])
.unwrap_or_else(|| vec![arg.to_string()])
}
}
}
}
fn cc_arg_spec_for_token(arg: &str) -> Option<&'static CcArgSpec> {
CC_ARG_SPECS.iter().find(|spec| match spec.value_form {
CcArgValueForm::Flag | CcArgValueForm::Separated => cc_arg_spec_matches(spec, arg),
CcArgValueForm::Concatenated { prefix } => arg.starts_with(prefix),
CcArgValueForm::CanBeSeparated { prefix } => {
arg == prefix
|| arg
.strip_prefix(prefix)
.is_some_and(|value| !value.is_empty())
}
})
}
fn classify_cc_flag(arg: &str) -> Option<FlagClass> {
static CACHE: OnceLock<HashMap<&'static str, Regex>> = OnceLock::new();
crate::compiler::flags::classify_against(
arg,
CC_FLAGS,
CACHE.get_or_init(|| crate::compiler::flags::build_regex_cache(CC_FLAGS)),
)
}
fn cc_flags_need_resolved_invocation(parsed: &CcArgs) -> bool {
parsed
.rest
.iter()
.any(|arg| analyze_cc_arg(arg).bucket == CcArgBucket::ProbeKeyed)
}
fn cc_prefix_maps(parsed: &CcArgs) -> Vec<CcPrefixMap> {
if !cc_path_normalize_enabled() {
return Vec::new();
}
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(_) => return Vec::new(),
};
let base = std::env::var_os("KACHE_BASE_DIR").filter(|v| !v.is_empty());
cc_prefix_maps_cfg(parsed, &cwd, base.as_deref().map(Path::new))
}
fn cc_prefix_maps_cfg(parsed: &CcArgs, cwd: &Path, base_dir: Option<&Path>) -> Vec<CcPrefixMap> {
let mut maps = cc_prefix_maps_for(parsed, cwd);
if let Some(base) = base_dir {
let base_abs = absolutize_path(cwd, base);
for root in [base_abs.clone(), canonicalize_or_self(&base_abs)] {
let from = root.to_string_lossy().to_string();
if !from.is_empty() && !maps.iter().any(|m| m.from == from) {
maps.push(CcPrefixMap {
from,
to: CC_BASE_SENTINEL,
});
}
}
maps.sort_by_key(|m| std::cmp::Reverse(m.from.len()));
}
maps
}
fn cc_path_normalize_enabled() -> bool {
parse_cc_normalize_toggle(std::env::var("KACHE_CC_PATH_NORMALIZE").ok().as_deref())
}
fn parse_cc_normalize_toggle(value: Option<&str>) -> bool {
match value {
Some(v) => !matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
),
None => true,
}
}
fn cc_prefix_maps_for(parsed: &CcArgs, cwd: &Path) -> Vec<CcPrefixMap> {
let cwd_abs = absolutize_path(cwd, cwd);
let Some(source) = parsed.sources.first() else {
return prefix_maps_from_roots([(cwd_abs, CC_BUILD_SENTINEL)]);
};
let source_abs = absolutize_path(cwd, source);
let source_parent = source_abs
.parent()
.map(Path::to_path_buf)
.unwrap_or_else(|| source_abs.clone());
let mut roots: Vec<(PathBuf, &'static str)> = Vec::new();
if let Some(common) = common_ancestor(&cwd_abs, &source_parent)
&& stable_cc_common_root(&common, &cwd_abs, &source_parent)
{
roots.push((common, CC_ROOT_SENTINEL));
} else {
roots.push((cwd_abs.clone(), CC_BUILD_SENTINEL));
roots.push((source_parent.clone(), CC_SOURCE_SENTINEL));
}
let cwd_canon = canonicalize_or_self(&cwd_abs);
let source_canon = canonicalize_or_self(&source_abs);
let source_canon_parent = source_canon
.parent()
.map(Path::to_path_buf)
.unwrap_or(source_canon);
if let Some(common) = common_ancestor(&cwd_canon, &source_canon_parent)
&& stable_cc_common_root(&common, &cwd_canon, &source_canon_parent)
{
roots.push((common, CC_ROOT_SENTINEL));
}
for include in &parsed.includes {
let include_abs = absolutize_path(cwd, include);
for (a, b) in [
(&cwd_abs, include_abs.clone()),
(&cwd_canon, canonicalize_or_self(&include_abs)),
] {
if let Some(common) = common_ancestor(a, &b)
&& stable_cc_common_root(&common, a, &b)
{
roots.push((common, CC_ROOT_SENTINEL));
}
}
}
prefix_maps_from_roots(roots)
}
fn prefix_maps_from_roots<I>(roots: I) -> Vec<CcPrefixMap>
where
I: IntoIterator<Item = (PathBuf, &'static str)>,
{
let mut maps = Vec::new();
for (root, to) in roots {
let from = root.to_string_lossy().to_string();
if from.is_empty() || maps.iter().any(|m: &CcPrefixMap| m.from == from) {
continue;
}
maps.push(CcPrefixMap { from, to });
}
maps.sort_by_key(|m| std::cmp::Reverse(m.from.len()));
maps
}
fn absolutize_path(base: &Path, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else {
base.join(path)
}
}
fn canonicalize_or_self(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn common_ancestor(a: &Path, b: &Path) -> Option<PathBuf> {
let mut out = PathBuf::new();
for (left, right) in a.components().zip(b.components()) {
if left != right {
break;
}
out.push(left.as_os_str());
}
(!out.as_os_str().is_empty()).then_some(out)
}
fn useful_cc_prefix(path: &Path) -> bool {
path.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.count()
>= 3
}
fn stable_cc_common_root(common: &Path, cwd: &Path, source_parent: &Path) -> bool {
if common == cwd || common == source_parent {
return true;
}
if common_is_temp_dir(common) {
return false;
}
useful_cc_prefix(common) || common_is_below_temp_dir(common)
}
fn common_is_below_temp_dir(common: &Path) -> bool {
let temp_dir = canonicalize_or_self(&std::env::temp_dir());
let common = canonicalize_or_self(common);
common != temp_dir && common.starts_with(temp_dir)
}
fn common_is_temp_dir(common: &Path) -> bool {
canonicalize_or_self(common) == canonicalize_or_self(&std::env::temp_dir())
}
fn apply_cc_prefix_maps_to_bytes(mut bytes: Vec<u8>, prefix_maps: &[CcPrefixMap]) -> Vec<u8> {
for map in prefix_maps {
let from = map.from.as_bytes();
if from.is_empty() {
continue;
}
bytes = replace_bytes(&bytes, from, map.to.as_bytes());
}
bytes
}
fn replace_bytes(input: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
if from.is_empty() || input.len() < from.len() {
return input.to_vec();
}
let mut out = Vec::with_capacity(input.len());
let mut i = 0;
while i < input.len() {
if input[i..].starts_with(from) {
out.extend_from_slice(to);
i += from.len();
} else {
out.push(input[i]);
i += 1;
}
}
out
}
fn file_prefix_map_args(prefix_maps: &[CcPrefixMap]) -> Vec<String> {
prefix_maps
.iter()
.rev()
.map(|m| format!("-ffile-prefix-map={}={}", m.from, m.to))
.collect()
}
fn cc_trace_name(parsed: &CcArgs) -> String {
parsed
.sources
.first()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "cc".to_string())
}
#[derive(Default)]
pub struct CcCompiler {
extra_allowlist_flags: Vec<String>,
}
impl CcCompiler {
pub fn new() -> Self {
Self::default()
}
pub fn with_extra_allowlist_flags(extra_allowlist_flags: Vec<String>) -> Self {
Self {
extra_allowlist_flags,
}
}
pub fn recognizes(args: &[String]) -> bool {
let Some(arg0) = args.first() else {
return false;
};
let Some(name) = super::command_basename(arg0) else {
return false;
};
let name = super::strip_windows_exe_suffix(name);
if matches!(name, "cc" | "c++" | "gcc" | "g++" | "clang" | "clang++") {
return true;
}
let stem = name.split('-').next().unwrap_or("");
matches!(stem, "cc" | "c++" | "gcc" | "g++" | "clang" | "clang++")
&& name.len() > stem.len()
&& name.as_bytes()[stem.len()] == b'-'
}
pub fn recognizes_family_probe(args: &[String]) -> bool {
args.len() >= 2 && args[0] == "-E"
}
}
fn is_cc_family_env_key(key: &str) -> bool {
let base = key
.strip_prefix("TARGET_")
.or_else(|| key.strip_prefix("HOST_"))
.unwrap_or(key);
base == "CC" || base == "CXX" || base.starts_with("CC_") || base.starts_with("CXX_")
}
fn is_cxx_env_key(key: &str) -> bool {
let base = key
.strip_prefix("TARGET_")
.or_else(|| key.strip_prefix("HOST_"))
.unwrap_or(key);
base == "CXX" || base.starts_with("CXX_")
}
fn probe_token_is_self(token: &str, self_stem: &str) -> bool {
super::command_basename(token)
.map(super::strip_windows_exe_suffix)
.is_some_and(|name| name.eq_ignore_ascii_case(self_stem))
}
pub(crate) fn resolve_probe_compiler<I>(
self_stem: &str,
target: Option<&str>,
env_vars: I,
) -> Option<String>
where
I: IntoIterator<Item = (String, String)>,
{
let mut wrapped: HashMap<String, String> = HashMap::new();
for (key, value) in env_vars {
if !is_cc_family_env_key(&key) {
continue;
}
let mut tokens = value.split_whitespace();
let Some(first) = tokens.next() else { continue };
if !probe_token_is_self(first, self_stem) {
continue;
}
let Some(real) = tokens.next() else { continue };
if probe_token_is_self(real, self_stem) {
continue;
}
wrapped.entry(key).or_insert_with(|| real.to_string());
}
if wrapped.is_empty() {
return None;
}
for name in ["CC", "CXX"] {
if let Some(t) = target {
if let Some(c) = wrapped.get(&format!("{name}_{t}")) {
return Some(c.clone());
}
let underscored = t.replace('-', "_");
if underscored != t
&& let Some(c) = wrapped.get(&format!("{name}_{underscored}"))
{
return Some(c.clone());
}
if let Some(c) = wrapped.get(&format!("TARGET_{name}")) {
return Some(c.clone());
}
}
if let Some(c) = wrapped.get(name) {
return Some(c.clone());
}
if let Some(c) = wrapped.get(&format!("HOST_{name}")) {
return Some(c.clone());
}
}
let mut keys: Vec<&String> = wrapped.keys().collect();
keys.sort_by(|a, b| {
is_cxx_env_key(a)
.cmp(&is_cxx_env_key(b))
.then_with(|| a.cmp(b))
});
keys.first().map(|k| wrapped[*k].clone())
}
impl Compiler for CcCompiler {
type Parsed = CcArgs;
fn id(&self) -> CompilerId {
CC_ID
}
fn parse(&self, args: &[String]) -> Result<CcArgs> {
CcArgs::parse(args)
}
fn refuse_reasons(&self, parsed: &CcArgs) -> Vec<RefuseReason> {
parsed.refuse_reasons(&self.extra_allowlist_flags)
}
fn cache_key(&self, parsed: &CcArgs, ctx: &KeyCtx<'_, '_>) -> Result<String> {
let mut hasher = blake3::Hasher::new();
let trace_name = cc_trace_name(parsed);
let prefix_maps = cc_prefix_maps(parsed);
hasher.update(b"cc_key_version:");
hasher.update(crate::cache_key::CACHE_KEY_VERSION.to_string().as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] cc_key_version={}",
trace_name,
crate::cache_key::CACHE_KEY_VERSION
);
let mut prefix_sentinels: Vec<&str> = Vec::new();
for map in &prefix_maps {
if !prefix_sentinels.contains(&map.to) {
prefix_sentinels.push(map.to);
}
}
prefix_sentinels.sort_unstable();
hasher.update(b"prefix_maps:");
for sentinel in prefix_sentinels {
hasher.update(sentinel.as_bytes());
hasher.update(b"\x1f");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] cc_prefix_map={}",
trace_name,
sentinel
);
}
hasher.update(b"\n");
let program_name = Path::new(&parsed.program)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(parsed.program.as_str());
hasher.update(b"compiler:");
hasher.update(program_name.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] compiler={}",
trace_name,
program_name
);
let config_args = parsed.config_args();
let resolved = crate::probe::probe(
ctx.cache_dir,
&crate::probe::CcProber,
&crate::probe::ProbeRequest {
compiler: &parsed.program,
args: &parsed.rest,
key_args: &config_args,
},
)?;
if resolved.resolved_tokens.is_none() && cc_flags_need_resolved_invocation(parsed) {
anyhow::bail!("cc: resolved invocation unavailable for probe-captured flags");
}
hasher.update(b"compiler_version:");
hasher.update(resolved.version_line.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] compiler_version={}",
trace_name,
resolved.version_line
);
if let Some(tokens) = &resolved.resolved_tokens {
hasher.update(b"resolved:");
for tok in tokens {
let mapped = apply_cc_prefix_maps_to_bytes(tok.clone().into_bytes(), &prefix_maps);
hasher.update(&mapped);
hasher.update(b"\x1f");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] resolved_token={}",
trace_name,
String::from_utf8_lossy(&mapped)
);
}
hasher.update(b"\n");
}
let arch = cc_target_arch(parsed);
hasher.update(b"arch:");
hasher.update(arch.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] arch={}",
trace_name,
arch
);
if let Some(opt) = parsed.optimization {
hasher.update(b"opt:");
hasher.update(format!("{opt:?}").as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] opt={opt:?}",
trace_name
);
}
if let Some(dbg) = parsed.debug_level {
hasher.update(b"debug:");
hasher.update(&[dbg]);
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] debug={dbg}",
trace_name
);
}
if let Some(std) = &parsed.std {
hasher.update(b"std:");
hasher.update(std.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] std={}",
trace_name,
std
);
}
hasher.update(b"pic:");
hasher.update(&[parsed.pic as u8]);
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] pic={}",
trace_name,
parsed.pic
);
let matched = cc_extra_flags_for_key(parsed, &self.extra_allowlist_flags);
if !matched.is_empty() {
hasher.update(b"cc_extra_flags:");
for flag in matched {
hasher.update(flag.as_bytes());
hasher.update(b"\x1f");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] cc_extra_flag={}",
trace_name,
flag
);
}
hasher.update(b"\n");
}
hasher.update(b"depinfo:");
if let Some(depinfo) = parsed.depinfo.as_ref().filter(|d| d.emit) {
hasher.update(b"1\n");
hasher.update(b"depinfo_include_system:");
hasher.update(&[depinfo.include_system as u8]);
hasher.update(b"\n");
hasher.update(b"depinfo_phony_targets:");
hasher.update(&[depinfo.phony_targets as u8]);
hasher.update(b"\n");
hasher.update(b"depinfo_missing_generated:");
hasher.update(&[depinfo.missing_generated as u8]);
hasher.update(b"\n");
hasher.update(b"depinfo_target:");
if let Some(target) = &depinfo.target {
hasher.update(target.as_bytes());
} else if let Some(object) = parsed.object_output_path()
&& let Some(name) = object.file_name()
{
hasher.update(name.to_string_lossy().as_bytes());
}
hasher.update(b"\n");
} else {
hasher.update(b"0\n");
}
let pp_hash = preprocess_hash(parsed, &prefix_maps)?;
hasher.update(b"preprocessed:");
hasher.update(pp_hash.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed={}",
trace_name,
pp_hash
);
let key = hasher.finalize().to_hex().to_string();
debug_assert_eq!(
parsed.sources.len(),
1,
"cc cache_key expects a single-source compile (refuse_reasons gates the rest)"
);
let key = crate::extra_inputs::apply_extra_inputs(
key,
parsed.sources.first().map(|p| p.as_path()),
&trace_name,
true,
ctx.file_hasher,
);
let key = crate::cache_key::apply_key_salt(key, ctx.key_salt);
tracing::trace!(
target: "kache::cache_key",
"[key:{}] final={}",
trace_name,
&key[..16]
);
Ok(key)
}
fn execute(&self, parsed: &CcArgs) -> Result<CompileResult> {
crate::opcounts::record_compiler_run();
let mut command = Command::new(&parsed.program);
command.args(&parsed.rest);
let prefix_maps = cc_prefix_maps(parsed);
for flag in file_prefix_map_args(&prefix_maps) {
command.arg(flag);
}
let output = command
.output()
.with_context(|| format!("executing {}", parsed.program))?;
let exit_code = output.status.code().unwrap_or(1);
let artifacts = if exit_code == 0 && parsed.mode == CompileMode::Compile {
match parsed.object_output_path() {
Some(obj) if obj.exists() => {
let name = obj
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let mut outputs = vec![(obj, name)];
if let Some(depinfo) = parsed.depinfo_output_path()
&& depinfo.exists()
{
let name = depinfo
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
outputs.push((depinfo, name));
}
ArtifactSet::from_output_files(outputs, classify_by_filename)
}
_ => ArtifactSet::empty(),
}
} else {
ArtifactSet::empty()
};
Ok(CompileResult {
exit_code,
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
artifacts,
})
}
fn classify_output(&self, _parsed: &CcArgs, name: &str) -> ArtifactKind {
classify_by_filename(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|a| a.to_string()).collect()
}
#[test]
fn recognizes_canonical_command_names() {
for name in [
"cc",
"c++",
"gcc",
"g++",
"clang",
"clang++",
"/usr/bin/cc",
"/usr/bin/gcc",
"/usr/local/bin/clang++",
] {
assert!(
CcCompiler::recognizes(&s(&[name])),
"should recognize {name}"
);
}
}
#[test]
fn recognizes_windows_exe_command_paths() {
for name in [
"clang.exe",
"clang++.exe",
"gcc.exe",
"g++.exe",
"C:/Users/dev/.mozbuild/clang/bin/clang.exe",
r"C:\Users\dev\.mozbuild\clang\bin\clang.exe",
"C:/Users/dev/.mozbuild/clang/bin/clang++.EXE",
] {
assert!(
CcCompiler::recognizes(&s(&[name])),
"should recognize Windows compiler path {name}"
);
}
}
#[test]
fn adapter_descriptor_uses_cc_recognizer() {
assert_eq!(ADAPTER.id(), CC_ID);
assert!(ADAPTER.recognizes(&s(&["cc"])));
assert!(!ADAPTER.recognizes(&s(&["rustc"])));
}
#[test]
fn recognizes_versioned_variants() {
for name in [
"gcc-13",
"clang-15",
"g++-12",
"clang++-17",
"gcc-13.exe",
"clang++-17.exe",
] {
assert!(
CcCompiler::recognizes(&s(&[name])),
"should recognize versioned {name}"
);
}
}
#[test]
fn recognizes_family_probe_matches_dash_e_with_file_arg() {
assert!(CcCompiler::recognizes_family_probe(&s(&[
"-E",
"/tmp/probe.c"
])));
assert!(CcCompiler::recognizes_family_probe(&s(&[
"-E",
"/tmp/detect_compiler_family.c"
])));
}
#[test]
fn recognizes_family_probe_rejects_dash_e_alone() {
assert!(!CcCompiler::recognizes_family_probe(&s(&["-E"])));
}
#[test]
fn recognizes_family_probe_rejects_non_probe_shapes() {
for argv in [
vec![],
s(&["-c", "foo.c"]),
s(&["--version"]),
s(&["-dumpmachine"]),
s(&["report"]),
s(&["foo.c"]),
] {
assert!(
!CcCompiler::recognizes_family_probe(&argv),
"should NOT recognize {argv:?} as cc-probe"
);
}
}
fn env(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn probe_compiler_recovers_real_compiler_from_target_cc_var() {
let vars = env(&[(
"CC_aarch64_pc_windows_msvc",
"C:/Users/sasch/.cargo/bin/kache.exe C:/Users/sasch/.mozbuild/clang/bin/clang-cl.exe",
)]);
assert_eq!(
resolve_probe_compiler("kache", None, vars),
Some("C:/Users/sasch/.mozbuild/clang/bin/clang-cl.exe".to_string())
);
}
#[test]
fn probe_compiler_recovers_from_plain_cc() {
assert_eq!(
resolve_probe_compiler("kache", None, env(&[("CC", "kache cc")])),
Some("cc".to_string())
);
}
#[test]
fn probe_compiler_recovers_from_cxx_when_no_cc() {
assert_eq!(
resolve_probe_compiler("kache", None, env(&[("CXX", "kache clang++")])),
Some("clang++".to_string())
);
}
#[test]
fn probe_compiler_prefers_cc_over_cxx() {
let vars = env(&[("CXX", "kache clang++"), ("CC", "kache clang")]);
assert_eq!(
resolve_probe_compiler("kache", None, vars),
Some("clang".to_string())
);
}
#[test]
fn probe_compiler_matches_self_stem_case_insensitively() {
let vars = env(&[("CC", r"C:\bin\KACHE.EXE clang-cl.exe")]);
assert_eq!(
resolve_probe_compiler("kache", None, vars),
Some("clang-cl.exe".to_string())
);
}
#[test]
fn probe_compiler_none_when_cc_is_not_kache_wrapped() {
assert_eq!(
resolve_probe_compiler("kache", None, env(&[("CC", "clang -fPIC")])),
None
);
}
#[test]
fn probe_compiler_none_when_only_self_present() {
assert_eq!(
resolve_probe_compiler("kache", None, env(&[("CC", "kache")])),
None
);
assert_eq!(
resolve_probe_compiler("kache", None, env(&[("CC", "kache kache")])),
None
);
}
#[test]
fn probe_compiler_ignores_non_compiler_env_vars() {
let vars = env(&[
("CFLAGS", "kache -O2"),
("CXXFLAGS", "kache -O2"),
("CCACHE_DIR", "kache whatever"),
("RUSTC_WRAPPER", "kache"),
]);
assert_eq!(resolve_probe_compiler("kache", None, vars), None);
}
#[test]
fn probe_compiler_prefers_target_specific_cc_var() {
let vars = env(&[
("HOST_CC", "kache gcc"),
("CC_aarch64_pc_windows_msvc", "kache clang-cl.exe"),
]);
assert_eq!(
resolve_probe_compiler("kache", Some("aarch64-pc-windows-msvc"), vars),
Some("clang-cl.exe".to_string())
);
}
#[test]
fn probe_compiler_matches_dashed_target_cc_var() {
let vars = env(&[("CC_aarch64-pc-windows-msvc", "kache clang-cl.exe")]);
assert_eq!(
resolve_probe_compiler("kache", Some("aarch64-pc-windows-msvc"), vars),
Some("clang-cl.exe".to_string())
);
}
#[test]
fn probe_compiler_target_specific_beats_bare_cc() {
let vars = env(&[
("CC", "kache gcc"),
("CC_x86_64_unknown_linux_gnu", "kache clang"),
]);
assert_eq!(
resolve_probe_compiler("kache", Some("x86_64-unknown-linux-gnu"), vars),
Some("clang".to_string())
);
}
#[test]
fn probe_compiler_deterministic_when_target_unknown() {
let a = env(&[("CC_zzz", "kache zzz-cc"), ("CC_aaa", "kache aaa-cc")]);
let b = env(&[("CC_aaa", "kache aaa-cc"), ("CC_zzz", "kache zzz-cc")]);
assert_eq!(
resolve_probe_compiler("kache", None, a),
Some("aaa-cc".to_string())
);
assert_eq!(
resolve_probe_compiler("kache", None, b),
Some("aaa-cc".to_string())
);
}
#[test]
fn recognizes_rejects_non_c_compilers() {
for name in [
"rustc",
"ld",
"ar",
"make",
"cmake",
"ccache",
"--crate-name",
] {
assert!(
!CcCompiler::recognizes(&s(&[name])),
"should NOT recognize {name}"
);
}
assert!(!CcCompiler::recognizes(&[]));
}
#[test]
fn parse_splits_program_from_rest() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"])).unwrap();
assert_eq!(parsed.program, "cc");
assert_eq!(parsed.rest, vec!["-c", "foo.c", "-o", "foo.o"]);
}
#[test]
fn parse_default_mode_is_link() {
let parsed = CcArgs::parse(&s(&["cc", "foo.c", "-o", "foo"])).unwrap();
assert_eq!(parsed.mode, CompileMode::Link);
}
#[test]
fn parse_dash_c_sets_compile_mode() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"])).unwrap();
assert_eq!(parsed.mode, CompileMode::Compile);
}
#[test]
fn parse_dash_e_sets_preprocess_mode() {
let parsed = CcArgs::parse(&s(&["cc", "-E", "foo.c"])).unwrap();
assert_eq!(parsed.mode, CompileMode::Preprocess);
}
#[test]
fn parse_dash_s_sets_assemble_mode() {
let parsed = CcArgs::parse(&s(&["cc", "-S", "foo.c"])).unwrap();
assert_eq!(parsed.mode, CompileMode::Assemble);
}
#[test]
fn parse_dash_o_sets_output() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "build/foo.o"])).unwrap();
assert_eq!(parsed.output, Some(PathBuf::from("build/foo.o")));
}
#[test]
fn parse_no_output_means_compiler_default() {
let parsed = CcArgs::parse(&s(&["cc", "foo.c"])).unwrap();
assert_eq!(parsed.output, None);
}
#[test]
fn parse_collects_source_files_by_extension() {
let parsed =
CcArgs::parse(&s(&["cc", "main.c", "util.c", "-o", "foo", "lib.cpp"])).unwrap();
assert_eq!(
parsed.sources,
vec![
PathBuf::from("main.c"),
PathBuf::from("util.c"),
PathBuf::from("lib.cpp"),
]
);
}
#[test]
fn parse_recognizes_objc_and_assembly_extensions() {
for src in &[
"foo.m", "foo.mm", "foo.M", "foo.i", "foo.ii", "foo.s", "foo.S", "foo.sx", ] {
let parsed = CcArgs::parse(&s(&["cc", "-c", src])).unwrap();
assert_eq!(
parsed.sources,
vec![PathBuf::from(src)],
"expected {src} to be recognized as a source"
);
}
}
#[test]
fn parse_ignores_non_source_positional_args() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-lpthread"])).unwrap();
assert_eq!(parsed.sources, vec![PathBuf::from("foo.c")]);
assert!(parsed.rest.contains(&"-lpthread".to_string()));
}
#[test]
fn parse_includes_separate_arg_form() {
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"foo.c",
"-I",
"include",
"-I",
"/usr/local/include",
]))
.unwrap();
assert_eq!(
parsed.includes,
vec![
PathBuf::from("include"),
PathBuf::from("/usr/local/include"),
]
);
}
#[test]
fn parse_includes_sticky_form() {
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"foo.c",
"-Iinclude",
"-I/usr/local/include",
]))
.unwrap();
assert_eq!(
parsed.includes,
vec![
PathBuf::from("include"),
PathBuf::from("/usr/local/include"),
]
);
}
#[test]
fn parse_defines_with_and_without_values() {
let parsed = CcArgs::parse(&s(&[
"cc", "-c", "foo.c", "-DFOO", "-DBAR=42", "-D", "BAZ=qux",
]))
.unwrap();
assert_eq!(
parsed.defines,
vec![
("FOO".to_string(), None),
("BAR".to_string(), Some("42".to_string())),
("BAZ".to_string(), Some("qux".to_string())),
]
);
}
#[test]
fn parse_optimization_levels() {
for (flag, expected) in [
("-O0", OptLevel::O0),
("-O1", OptLevel::O1),
("-O", OptLevel::O1), ("-O2", OptLevel::O2),
("-O3", OptLevel::O3),
("-Os", OptLevel::Os),
("-Oz", OptLevel::Oz),
("-Og", OptLevel::Og),
] {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", flag])).unwrap();
assert_eq!(parsed.optimization, Some(expected), "for {flag}");
}
}
#[test]
fn parse_debug_levels() {
for (flag, expected) in [
("-g", 2u8), ("-g0", 0),
("-g1", 1),
("-g2", 2),
("-g3", 3),
] {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", flag])).unwrap();
assert_eq!(parsed.debug_level, Some(expected), "for {flag}");
}
}
#[test]
fn parse_std_strips_prefix() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-std=c++17"])).unwrap();
assert_eq!(parsed.std, Some("c++17".to_string()));
}
#[test]
fn parse_pic_flags() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-fPIC"])).unwrap();
assert!(parsed.pic);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-fpic"])).unwrap();
assert!(parsed.pic);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c"])).unwrap();
assert!(!parsed.pic);
}
#[test]
fn parse_depinfo_mmd_excludes_system_headers() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MMD"])).unwrap();
let d = parsed.depinfo.expect("dep-info should be set");
assert!(d.emit);
assert!(!d.include_system);
assert_eq!(d.output, None);
assert_eq!(d.target, None);
}
#[test]
fn parse_depinfo_md_includes_system_headers() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MD"])).unwrap();
let d = parsed.depinfo.expect("dep-info should be set");
assert!(d.emit);
assert!(d.include_system);
}
#[test]
fn parse_depinfo_mf_sets_output_path() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MMD", "-MF", "build/foo.d"])).unwrap();
let d = parsed.depinfo.expect("dep-info should be set");
assert_eq!(d.output, Some(PathBuf::from("build/foo.d")));
}
#[test]
fn parse_depinfo_mt_sets_target_name() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MMD", "-MT", "build/foo.o"])).unwrap();
let d = parsed.depinfo.expect("dep-info should be set");
assert_eq!(d.target, Some("build/foo.o".to_string()));
}
#[test]
fn parse_depinfo_mp_and_mg_shape_flags() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MMD", "-MP", "-MG"])).unwrap();
let d = parsed.depinfo.expect("dep-info should be set");
assert!(d.phony_targets);
assert!(d.missing_generated);
}
#[test]
fn parse_no_depinfo_flags_means_no_depinfo_struct() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"])).unwrap();
assert!(parsed.depinfo.is_none());
}
#[test]
fn depinfo_path_modifiers_alone_do_not_emit_depinfo() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MF", "deps/foo.d"])).unwrap();
assert!(parsed.depinfo.is_some());
assert_eq!(parsed.depinfo_output_path(), None);
assert_eq!(parsed.depinfo_anchor(), None);
}
#[test]
fn parse_language_override() {
let parsed = CcArgs::parse(&s(&["cc", "-x", "c++", "-c", "src"])).unwrap();
assert_eq!(parsed.language_override, Some("c++".to_string()));
}
#[test]
fn parse_language_override_sticky_form() {
for (flag, expected) in [
("-xc", "c"),
("-xc++", "c++"),
("-xobjective-c", "objective-c"),
("-xobjective-c++", "objective-c++"),
] {
let parsed = CcArgs::parse(&s(&["cc", flag, "-c", "foo.c"])).unwrap();
assert_eq!(
parsed.language_override,
Some(expected.to_string()),
"for {flag}"
);
}
}
#[test]
fn parse_table_driven_value_forms() {
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"foo.c",
"-I",
"include",
"-Ivendor",
"-D",
"FOO=1",
"-DBAR",
"-std=c++20",
"-xobjective-c++",
"-o",
"foo.o",
]))
.unwrap();
assert_eq!(
parsed.includes,
vec![PathBuf::from("include"), PathBuf::from("vendor")]
);
assert_eq!(
parsed.defines,
vec![
("FOO".to_string(), Some("1".to_string())),
("BAR".to_string(), None),
]
);
assert_eq!(parsed.std, Some("c++20".to_string()));
assert_eq!(parsed.language_override, Some("objective-c++".to_string()));
assert_eq!(parsed.output, Some(PathBuf::from("foo.o")));
}
#[test]
fn cc_flags_table_regexes_compile() {
crate::compiler::flags::assert_table_regexes_compile(CC_FLAGS);
}
fn refuse_descriptions(args: &[&str]) -> Vec<&'static str> {
refuse_descriptions_with_flags(args, &[])
}
fn refuse_descriptions_with_flags(args: &[&str], extra: &[String]) -> Vec<&'static str> {
let parsed = CcArgs::parse(&s(args)).unwrap();
parsed
.refuse_reasons(extra)
.iter()
.map(|r| r.description())
.collect()
}
#[test]
fn refuses_response_files() {
let descs = refuse_descriptions(&["cc", "-c", "@flags.rsp"]);
assert!(
descs.iter().any(|d| d.contains("response file")),
"expected response-file refuse, got: {descs:?}"
);
}
#[test]
fn refuses_multi_arch() {
let single = refuse_descriptions(&["cc", "-c", "foo.c", "-arch", "arm64"]);
assert!(!single.iter().any(|d| d.contains("multi-arch")));
let multi =
refuse_descriptions(&["cc", "-c", "foo.c", "-arch", "arm64", "-arch", "x86_64"]);
assert!(
multi.iter().any(|d| d.contains("multi-arch")),
"expected multi-arch refuse, got: {multi:?}"
);
}
#[test]
fn refuses_coverage_instrumentation() {
for flag in &["--coverage", "-fprofile-arcs", "-ftest-coverage"] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", flag]);
assert!(
descs.iter().any(|d| d.contains("coverage")),
"expected coverage refuse for {flag}, got: {descs:?}"
);
}
}
#[test]
fn refuses_split_dwarf() {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-gsplit-dwarf"]);
assert!(
descs.iter().any(|d| d.contains("gsplit-dwarf")),
"expected gsplit-dwarf refuse, got: {descs:?}"
);
}
#[test]
fn refuses_precompiled_headers() {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-include", "stdafx.pch"]);
assert!(
descs.iter().any(|d| d.contains("precompiled")),
"expected PCH refuse, got: {descs:?}"
);
let descs = refuse_descriptions(&["cc", "-c", "foo.h", "-emit-pch"]);
assert!(
descs.iter().any(|d| d.contains("precompiled")),
"expected PCH refuse for -emit-pch, got: {descs:?}"
);
}
#[test]
fn refuses_modules() {
for flag in &["-fmodules", "-fcxx-modules"] {
let descs = refuse_descriptions(&["cc", "-c", "foo.cpp", flag]);
assert!(
descs.iter().any(|d| d.contains("modules")),
"expected modules refuse for {flag}, got: {descs:?}"
);
}
}
#[test]
fn refuses_output_to_stdout() {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "-"]);
assert!(
descs.iter().any(|d| d.contains("stdout")),
"expected stdout-output refuse, got: {descs:?}"
);
}
#[test]
fn refuses_flags_unclassified_in_cc_flags_table() {
for flag in &[
"-ffast-math",
"-fsanitize=address",
"-funroll-loops",
"-fno-pic",
"-mtune=skylake",
"-mavx2",
"-Ofast",
"-gdwarf-5",
"-ggdb",
"-gline-tables-only",
"-pg",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"expected classifier refuse for {flag}, got: {descs:?}"
);
}
}
#[test]
fn cc_flags_table_classifies_known_cache_safe_flags() {
for flag in &[
"-O2",
"-O0",
"-Og",
"-g",
"-g2",
"-std=c11",
"-fPIC",
"-fpic", "-DFOO=1",
"-Iinclude",
"-isystem",
"-include",
"-nostdinc",
"-undef", "-Wall",
"-Wextra",
"-Werror",
"-Wno-unused",
"-w",
"-pedantic", "-pipe",
"-P",
"-MMD",
"-MF",
"-fdiagnostics-color", ] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is cache-safe and must NOT trip the classifier, got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_gecko_darwin_baseline_flags() {
for flag in &[
"-mmacosx-version-min=10.15",
"-mmacosx-version-min=11.0",
"-pthread",
"-fstack-protector-strong",
"-fstrict-flex-arrays=1",
"-fstrict-flex-arrays=3",
"-fno-math-errno",
"-fno-strict-aliasing",
"-ffp-contract=off",
"-ffp-contract=on",
"-fno-omit-frame-pointer",
"-funwind-tables",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} should be classified (Gecko/Darwin baseline), got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_stack_clash_protection() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-fstack-clash-protection",
]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"-fstack-clash-protection should be classified (issue #245), got: {descs:?}"
);
}
fn flags(list: &[&str]) -> Vec<String> {
list.iter().map(|s| s.to_string()).collect()
}
#[test]
fn user_allowed_flag_stops_refusing() {
let args = &["cc", "-c", "foo.c", "-o", "foo.o", "-fsome-exotic-flag"];
let refused = refuse_descriptions(args);
assert!(
refused.iter().any(|d| d.contains("unsupported flag")),
"unconfigured exotic flag should refuse, got: {refused:?}"
);
let allowed = refuse_descriptions_with_flags(args, &flags(&["-fsome-exotic-flag"]));
assert!(
!allowed.iter().any(|d| d.contains("unsupported flag")),
"allow-listed flag should not refuse, got: {allowed:?}"
);
}
#[test]
fn user_allowed_flag_cannot_override_structural_refusal() {
let args = &["cc", "-c", "foo.c", "-o", "foo.o", "--coverage"];
let descs = refuse_descriptions_with_flags(args, &flags(&["--coverage"]));
assert!(
descs.iter().any(|d| d.contains("coverage")),
"coverage must still refuse even when allow-listed, got: {descs:?}"
);
}
#[test]
fn cc_extra_flags_for_key_selects_present_unmodeled_sorted() {
let extra = flags(&["-fbravo", "-falpha", "-fPIC"]);
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-fbravo",
"-falpha",
"-falpha",
"-fPIC",
"-fcharlie",
]))
.unwrap();
assert_eq!(
cc_extra_flags_for_key(&parsed, &extra),
vec!["-falpha", "-fbravo"]
);
let absent = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"])).unwrap();
assert!(cc_extra_flags_for_key(&absent, &extra).is_empty());
assert!(cc_extra_flags_for_key(&parsed, &[]).is_empty());
}
#[test]
fn classifier_accepts_realistic_firefox_compile() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-O2",
"-g",
"-std=gnu11",
"-mmacosx-version-min=10.15",
"-pthread",
"-fno-strict-aliasing",
"-fno-math-errno",
"-funwind-tables",
"-fstack-protector-strong",
"-fno-omit-frame-pointer",
"-ffp-contract=off",
"-fstrict-flex-arrays=1",
"-Wall",
"-Wno-unused-parameter",
"-DMOZILLA_INTERNAL_API=1",
"-I/some/include",
]);
assert!(
descs.is_empty(),
"realistic Firefox compile should be fully cacheable, got: {descs:?}"
);
}
#[test]
fn classifier_does_not_overreach_gecko_darwin_family() {
for flag in &[
"-fmath-errno",
"-fstrict-aliasing",
"-fomit-frame-pointer",
"-fno-unwind-tables",
"-fstack-protector",
"-fstack-protector-all",
"-mmacosx-min-version=10.15",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is NOT on the #114 list and must still refuse, got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_firefox_debug_info_and_wrapper_flags() {
for flag in &[
"-gdwarf-4",
"-gsimple-template-names",
"-mllvm=-dwarf-linkage-names=Abstract",
"--start-no-unused-arguments",
"--end-no-unused-arguments",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} should be classified (#117 baseline), got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_unused_arguments_wrapper_pair() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-O2",
"--start-no-unused-arguments",
"-Wno-unused-command-line-argument",
"--end-no-unused-arguments",
]);
assert!(
descs.is_empty(),
"wrapped pair should be fully cacheable, got: {descs:?}"
);
}
#[test]
fn classifier_does_not_overreach_117_additions() {
for flag in &[
"-gdwarf-3",
"-gdwarf-5",
"-gdwarf",
"-gline-tables-only",
"-mllvm=-some-other-flag",
"-mllvm=-inline-threshold=1000",
"--start-no-unused",
"--no-unused-arguments",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is NOT on the #117 list and must still refuse, got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_cpp_abi_rtti_exception_flags() {
for flag in &[
"-stdlib=libc++",
"-stdlib=libstdc++",
"-fno-exceptions",
"-fexceptions",
"-fno-rtti",
"-frtti",
"-fno-sized-deallocation",
"-fno-aligned-new",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.cpp", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} should be classified (#116 baseline), got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_path_prefix_map_flags() {
for flag in &[
"-ffile-prefix-map=/build/clone-a/=/topsrcdir/",
"-fdebug-prefix-map=/build/clone-a/obj=/topobjdir/",
"-fmacro-prefix-map=/build/clone-a/=/topsrcdir/",
"-fdebug-prefix-map=/Applications/Xcode.app/.../SDK=/sysroot/",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.cpp", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} should be classified (path-remap), got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_realistic_firefox_cpp_compile() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.cpp",
"-o",
"foo.o",
"-O2",
"-g",
"-std=gnu++17",
"-stdlib=libc++",
"-fno-exceptions",
"-fno-rtti",
"-fno-sized-deallocation",
"-fno-aligned-new",
"-mmacosx-version-min=10.15",
"-fno-strict-aliasing",
"-fstack-protector-strong",
"-Wall",
"-DMOZILLA_INTERNAL_API=1",
]);
assert!(
descs.is_empty(),
"realistic Firefox C++ compile should be fully cacheable, got: {descs:?}"
);
}
#[test]
fn classifier_does_not_overreach_116_additions() {
for flag in &[
"-fsanitize=undefined",
"-faligned-new",
"-fsized-deallocation",
"-fstdlib=libc++",
"-fno-rt",
"-fno-rttis",
"-fexception",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.cpp", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is NOT on the #116 list and must still refuse, got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_visibility_flags() {
for flag in &["-fvisibility=hidden", "-fvisibility-inlines-hidden"] {
let descs = refuse_descriptions(&["cc", "-c", "foo.cpp", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} should classify (visibility cluster), got: {descs:?}"
);
}
}
#[test]
fn classifier_does_not_overreach_visibility_additions() {
for flag in &[
"-fvisibility=default",
"-fvisibility=protected",
"-fvisibility=internal",
"-fvisibility",
"-fvisible=hidden",
"-fno-visibility-inlines-hidden",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.cpp", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is NOT on the visibility list and must still refuse, got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_target_arch_objc_flags() {
for flag in &[
"--target=arm64-apple-macosx",
"--target=wasm32-wasi",
"--target=aarch64-linux-gnu",
"-target",
"-march=native",
"-march=armv8-a",
"-march=armv8.2-a+dotprod",
"-march=armv8.2-a+i8mm",
"-msimd128",
"-ffunction-sections",
"-fdata-sections",
"-Wa,--noexecstack",
"-x",
"-xc",
"-xc++",
"-xobjective-c",
"-xobjective-c++",
"-fobjc-exceptions",
"-fobjc-arc",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} should be classified (#115 baseline), got: {descs:?}"
);
}
}
#[test]
fn classifier_accepts_realistic_firefox_wasm_compile() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-O2",
"-g",
"-std=gnu11",
"--target=wasm32-wasi",
"-msimd128",
"-ffunction-sections",
"-fdata-sections",
"-fno-strict-aliasing",
"-Wa,--noexecstack",
"-Wall",
"-DMOZILLA_BUILD=1",
]);
assert!(
descs.is_empty(),
"realistic Firefox WASM compile should be fully cacheable, got: {descs:?}"
);
}
#[test]
fn classifier_accepts_realistic_firefox_objc_compile() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.mm",
"-o",
"foo.o",
"-O2",
"-g",
"-xobjective-c++",
"-fobjc-arc",
"-fobjc-exceptions",
"-fno-exceptions",
"-fno-rtti",
"-stdlib=libc++",
"-mmacosx-version-min=11.0",
"-march=armv8-a",
]);
assert!(
descs.is_empty(),
"realistic Firefox ObjC++ compile should be fully cacheable, got: {descs:?}"
);
}
#[test]
fn classifier_does_not_overreach_115_additions() {
for flag in &[
"-Wa,-mfp",
"-Wa,--something-else",
"-xassembler-with-cpp",
"-xnone",
"-fno-objc-arc",
"-fobjc-weak",
"-fno-function-sections",
"-fno-data-sections",
"-msse4.2",
"-mavx512f",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is NOT on the #115 list and must still refuse, got: {descs:?}"
);
}
}
#[test]
fn refuse_reason_names_the_rejected_flags() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-ffast-math",
"-fsanitize=address",
]);
let detail = descs
.iter()
.find(|d| d.contains("unsupported flag"))
.expect("expected an unsupported-flag refuse reason");
assert!(
detail.contains("-ffast-math"),
"reason should name the flag: {detail}"
);
assert!(
detail.contains("-fsanitize=address"),
"reason should name every rejected flag: {detail}"
);
}
#[test]
fn classifier_accepts_parser_handled_and_preprocessor_only_flags() {
for (flag, expected) in [
("-c", FlagClass::ParserHandled),
("-E", FlagClass::ParserHandled),
("-S", FlagClass::ParserHandled),
("-P", FlagClass::NoObjectEffect),
("-xc", FlagClass::CapturedByProbe),
("-xc++", FlagClass::CapturedByProbe),
("-xobjective-c", FlagClass::CapturedByProbe),
] {
assert_eq!(
classify_cc_flag(flag),
Some(expected),
"{flag} should have the expected class"
);
}
}
#[test]
fn arg_analysis_exposes_bucket_and_normalized_value_form() {
let language = analyze_cc_arg("-xc++");
assert_eq!(language.class, Some(FlagClass::CapturedByProbe));
assert_eq!(language.bucket, CcArgBucket::ProbeKeyed);
assert_eq!(
language.normalized,
vec!["-x".to_string(), "c++".to_string()]
);
assert_eq!(language.refusal, None);
let include = analyze_cc_arg("-Ivendor");
assert_eq!(include.class, Some(FlagClass::PreprocessorCaptured));
assert_eq!(include.bucket, CcArgBucket::Preprocessor);
assert_eq!(
include.normalized,
vec!["-I".to_string(), "vendor".to_string()]
);
let unknown = analyze_cc_arg("-funknown");
assert_eq!(unknown.class, None);
assert_eq!(unknown.bucket, CcArgBucket::TooHard);
assert_eq!(unknown.refusal, Some("cc: unsupported flag"));
}
#[test]
fn unsupported_flag_reason_excludes_classified_mixed_flags() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-P",
"-xc",
"-Ofast",
"-funknown",
]);
let detail = descs
.iter()
.find(|d| d.contains("unsupported flag"))
.expect("expected unsupported flags for the truly unmodeled args");
assert!(
detail.contains("-Ofast"),
"reason should name -Ofast: {detail}"
);
assert!(
detail.contains("-funknown"),
"reason should name -funknown: {detail}"
);
assert!(
!detail.contains("-P"),
"reason should not include -P: {detail}"
);
assert!(
!detail.contains("-xc"),
"reason should not include -xc: {detail}"
);
}
#[test]
fn probe_captured_flags_require_resolved_invocation() {
let needs_probe =
CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", "-fno-rtti"])).unwrap();
assert!(cc_flags_need_resolved_invocation(&needs_probe));
let modeled_only =
CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", "-O2", "-P"])).unwrap();
assert!(!cc_flags_need_resolved_invocation(&modeled_only));
}
#[test]
fn cache_key_refuses_probe_captured_flags_without_resolved_invocation() {
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&s(&["true", "-c", "foo.c", "-o", "foo.o", "-fno-rtti"]))
.unwrap();
let cache = tempfile::tempdir().unwrap();
let file_hasher = crate::cache_key::FileHasher::new();
let path_normalizer = crate::path_normalizer::PathNormalizer::empty();
let ctx = KeyCtx {
file_hasher: &file_hasher,
path_normalizer: &path_normalizer,
cache_dir: cache.path(),
key_salt: None,
};
let err = compiler.cache_key(&parsed, &ctx).unwrap_err().to_string();
assert!(
err.contains("resolved invocation unavailable"),
"expected resolved-invocation refusal, got: {err}"
);
}
#[test]
fn preprocess_mode_refusal_does_not_report_classified_flags_as_unsupported() {
let descs = refuse_descriptions(&["cc", "-E", "-xc", "-P", "foo.c"]);
assert!(
descs.iter().any(|d| d.contains("preprocessor mode")),
"expected preprocessor-mode refuse, got: {descs:?}"
);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"classified preprocess args should not be reported unsupported: {descs:?}"
);
}
#[test]
fn refuses_preprocess_and_assemble_modes() {
let preprocess = refuse_descriptions(&["cc", "-E", "foo.c"]);
assert!(
preprocess.iter().any(|d| d.contains("preprocessor")),
"expected preprocessor-mode refuse, got: {preprocess:?}"
);
let assemble = refuse_descriptions(&["cc", "-S", "foo.c"]);
assert!(
assemble.iter().any(|d| d.contains("assembly")),
"expected assembly-mode refuse, got: {assemble:?}"
);
}
#[test]
fn non_compile_refusal_does_not_carry_unsupported_flag_noise() {
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&s(&["cc", "-xc", "-P", "-E", "foo.c"]))
.unwrap();
let reasons = compiler.refuse_reasons(&parsed);
let descs: Vec<_> = reasons.iter().map(|r| r.description()).collect();
assert!(
descs.iter().any(|d| d.contains("preprocessor mode")),
"preprocessor mode must be reported, got: {descs:?}"
);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"preprocessor-mode refusal must not carry 'unsupported flag' noise, got: {descs:?}"
);
assert!(
descs.iter().any(|d| d.contains("not yet supported")),
"preprocessor mode message must read as deferral ('not yet supported'), got: {descs:?}"
);
let parsed = compiler
.parse(&s(&["cc", "foo.o", "-fuse-ld=lld", "-o", "out"]))
.unwrap();
let reasons = compiler.refuse_reasons(&parsed);
let descs: Vec<_> = reasons.iter().map(|r| r.description()).collect();
assert!(
descs.iter().any(|d| d.contains("link mode")),
"link mode must be reported, got: {descs:?}"
);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"link-mode refusal must not carry 'unsupported flag' noise, got: {descs:?}"
);
assert!(
reasons
.iter()
.any(|r| matches!(r, RefuseReason::Unsupported(d) if d.contains("link mode"))),
"link mode must classify as Unsupported (roadmap), got: {reasons:?}"
);
}
#[test]
fn compile_mode_unmodeled_flag_still_reports_unsupported_flag() {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", "-Ofast"]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"compile-mode unmodeled flag must still report 'unsupported flag', got: {descs:?}"
);
}
#[test]
fn refuses_nothing_for_clean_compile_invocation() {
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"src/foo.c",
"-o",
"build/foo.o",
"-O2",
"-g",
"-fPIC",
"-Iinclude",
]))
.unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"clean compile invocation should have no parser-level refuse reasons; got: {:?}",
parsed.refuse_reasons(&[])
);
}
#[test]
fn refuse_reasons_empty_for_cacheable_single_source_compile() {
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"]))
.unwrap();
assert!(
compiler.refuse_reasons(&parsed).is_empty(),
"single-source -c compile must be cacheable, got: {:?}",
compiler
.refuse_reasons(&parsed)
.iter()
.map(|r| r.description())
.collect::<Vec<_>>()
);
}
#[test]
fn refuse_reasons_refuses_link_mode() {
let compiler = CcCompiler::new();
let parsed = compiler.parse(&s(&["cc", "foo.c", "-o", "foo"])).unwrap();
let descs: Vec<_> = compiler
.refuse_reasons(&parsed)
.iter()
.map(|r| r.description())
.collect();
assert!(
descs.iter().any(|d| d.contains("link mode")),
"link invocation must be refused, got: {descs:?}"
);
}
#[test]
fn refuse_reasons_refuses_multi_source_compile() {
let compiler = CcCompiler::new();
let parsed = compiler.parse(&s(&["cc", "-c", "a.c", "b.c"])).unwrap();
let reasons = compiler.refuse_reasons(&parsed);
let descs: Vec<_> = reasons.iter().map(|r| r.description()).collect();
assert!(
descs.iter().any(|d| d.contains("multi-source")),
"multi-source compile must be refused, got: {descs:?}"
);
assert!(
descs.iter().any(|d| d.contains("not yet supported")),
"multi-source message must read as deferral, got: {descs:?}"
);
}
#[test]
fn object_output_path_uses_explicit_dash_o() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "src/foo.c", "-o", "build/foo.o"])).unwrap();
assert_eq!(
parsed.object_output_path(),
Some(PathBuf::from("build/foo.o"))
);
}
#[test]
fn object_output_path_defaults_to_source_stem_dot_o() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "src/foo.c"])).unwrap();
assert_eq!(parsed.object_output_path(), Some(PathBuf::from("foo.o")));
}
#[test]
fn depinfo_output_path_uses_mf_or_object_stem() {
let explicit =
CcArgs::parse(&s(&["cc", "-c", "foo.c", "-MMD", "-MF", "deps/foo.d"])).unwrap();
assert_eq!(
explicit.depinfo_output_path(),
Some(PathBuf::from("deps/foo.d"))
);
let derived = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "obj/foo.o", "-MMD"])).unwrap();
assert_eq!(
derived.depinfo_output_path(),
Some(PathBuf::from("obj/foo.d"))
);
assert_eq!(derived.depinfo_anchor(), Some(PathBuf::from("obj")));
}
#[test]
fn build_preprocess_args_forces_dash_e_dash_p_and_strips_mode() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", "-O2", "-Iinc"])).unwrap();
let pp = build_preprocess_args(&parsed);
assert_eq!(&pp[0], "-E");
assert_eq!(&pp[1], "-P");
assert!(!pp.iter().any(|a| a == "-c"));
assert!(!pp.iter().any(|a| a == "-o"));
assert!(!pp.iter().any(|a| a == "foo.o"));
assert!(pp.iter().any(|a| a == "-O2"));
assert!(pp.iter().any(|a| a == "-Iinc"));
assert!(pp.iter().any(|a| a == "foo.c"));
}
#[test]
fn build_preprocess_args_strips_dep_info_flags() {
let parsed = CcArgs::parse(&s(&[
"cc", "-c", "foo.c", "-MMD", "-MF", "foo.d", "-MT", "foo.o",
]))
.unwrap();
let pp = build_preprocess_args(&parsed);
for stripped in &["-MMD", "-MF", "foo.d", "-MT", "foo.o"] {
assert!(
!pp.iter().any(|a| a == stripped),
"{stripped} should be stripped from preprocess args, got {pp:?}"
);
}
}
#[test]
fn execute_returns_error_when_compiler_binary_missing() {
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&["this-binary-does-not-exist-pls-fail-1234567890".to_string()])
.unwrap();
let result = compiler.execute(&parsed);
assert!(
result.is_err(),
"execute() must return Err when the compiler binary can't be spawned"
);
}
#[test]
fn cc_prefix_maps_derive_common_source_and_build_root() {
let root = tempfile::TempDir::new().unwrap();
let src_dir = root.path().join("dom/canvas");
let obj_dir = root.path().join("obj-kache-bench/dom/canvas");
std::fs::create_dir_all(&src_dir).unwrap();
std::fs::create_dir_all(&obj_dir).unwrap();
let source = src_dir.join("Unified_cpp_dom_canvas3.cpp");
std::fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-o",
"Unified_cpp_dom_canvas3.o",
]))
.unwrap();
let maps = cc_prefix_maps_for(&parsed, &obj_dir);
let canonical_root = root
.path()
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string();
assert!(
maps.iter()
.any(|m| m.from == canonical_root && m.to == CC_ROOT_SENTINEL),
"expected common root map in {maps:?}"
);
let flags = file_prefix_map_args(&maps);
assert!(
flags
.iter()
.any(|f| f == &format!("-ffile-prefix-map={canonical_root}={CC_ROOT_SENTINEL}")),
"execute should inject the common-root prefix map, got {flags:?}"
);
}
#[test]
fn cc_prefix_maps_fall_back_to_distinct_roots_without_common_project_root() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "/opt/kache-src/foo.c", "-o", "foo.o"])).unwrap();
let maps = cc_prefix_maps_for(&parsed, Path::new("/tmp/kache-build"));
assert!(
maps.iter().any(|m| m.to == CC_BUILD_SENTINEL),
"missing build root map: {maps:?}"
);
assert!(
maps.iter().any(|m| m.to == CC_SOURCE_SENTINEL),
"missing source root map: {maps:?}"
);
}
#[test]
fn cc_prefix_maps_keep_shallow_in_tree_relocated_builds_stable() {
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"/tmp/kache-relocated/src/foo.c",
"-o",
"build/foo.o",
]))
.unwrap();
let maps = cc_prefix_maps_for(&parsed, Path::new("/tmp/kache-relocated"));
assert!(
maps.iter()
.any(|m| m.from == "/tmp/kache-relocated" && m.to == CC_ROOT_SENTINEL),
"in-tree shallow relocations should use the same root sentinel, got {maps:?}"
);
}
#[test]
fn cc_prefix_maps_accept_generated_tempdir_common_root() {
let root = tempfile::TempDir::new().unwrap();
let root = root.path();
assert!(
stable_cc_common_root(root, &root.join("obj"), &root.join("src")),
"generated temp project roots should be stable common roots"
);
assert!(
!stable_cc_common_root(&std::env::temp_dir(), &root.join("obj"), &root.join("src")),
"the temp directory itself is too broad to use as a common root"
);
}
#[test]
fn cc_prefix_maps_normalize_preprocessor_bytes() {
let maps = vec![CcPrefixMap {
from: "/Users/me/work/clone-a".to_string(),
to: CC_ROOT_SENTINEL,
}];
let input = br#"assert_fail("/Users/me/work/clone-a/obj/dist/include/fmt/format.h")"#;
let normalized = apply_cc_prefix_maps_to_bytes(input.to_vec(), &maps);
assert_eq!(
std::str::from_utf8(&normalized).unwrap(),
r#"assert_fail("<CC_ROOT>/obj/dist/include/fmt/format.h")"#
);
}
#[test]
fn resolved_tokens_normalize_identically_across_build_paths() {
let tok = |clone: &str| {
format!(r#"FIREFOX_ICO="/Users/me/work/{clone}/browser/branding/firefox.ico""#)
.into_bytes()
};
let maps_for = |clone: &str| {
vec![CcPrefixMap {
from: format!("/Users/me/work/{clone}"),
to: CC_ROOT_SENTINEL,
}]
};
let a = apply_cc_prefix_maps_to_bytes(tok("clone-a"), &maps_for("clone-a"));
let b = apply_cc_prefix_maps_to_bytes(tok("clone-b"), &maps_for("clone-b"));
assert_eq!(
a, b,
"the same resolved token at different build paths must normalize identically"
);
assert_eq!(
std::str::from_utf8(&a).unwrap(),
r#"FIREFOX_ICO="<CC_ROOT>/browser/branding/firefox.ico""#
);
}
#[test]
fn cc_prefix_maps_broaden_to_repo_root_via_includes_for_objdir_tus() {
let root = tempfile::TempDir::new().unwrap();
let obj_dir = root.path().join("obj-kache-bench/xpcom/components");
let inc_dir = root.path().join("xpcom/components");
std::fs::create_dir_all(&obj_dir).unwrap();
std::fs::create_dir_all(&inc_dir).unwrap();
let source = obj_dir.join("StaticComponents.cpp");
std::fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
inc_dir.to_str().unwrap(), "-I",
"/usr/include", "-o",
"StaticComponents.o",
]))
.unwrap();
let maps = cc_prefix_maps_for(&parsed, &obj_dir);
let canonical_root = root
.path()
.canonicalize()
.unwrap()
.to_string_lossy()
.to_string();
assert!(
maps.iter()
.any(|m| m.from == canonical_root && m.to == CC_ROOT_SENTINEL),
"include-folding must derive the repo root for objdir TUs, got {maps:?}"
);
assert!(
!maps.iter().any(|m| m.from == "/"),
"out-of-tree includes must not add a `/` root, got {maps:?}"
);
}
#[test]
fn cc_prefix_maps_cfg_maps_explicit_base_dir_to_base_sentinel() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "/work/checkout/src/foo.c", "-o", "foo.o"])).unwrap();
let cwd = Path::new("/work/checkout");
let maps = cc_prefix_maps_cfg(&parsed, cwd, Some(Path::new("/work")));
assert!(
maps.iter()
.any(|m| m.from == "/work" && m.to == CC_BASE_SENTINEL),
"explicit KACHE_BASE_DIR must map to the base sentinel, got {maps:?}"
);
}
#[test]
fn parse_cc_normalize_toggle_defaults_on_opts_out_explicitly() {
for on in [
None,
Some("1"),
Some("yes"),
Some("on"),
Some(""),
Some("garbage"),
] {
assert!(parse_cc_normalize_toggle(on), "{on:?} should keep it on");
}
for off in [
Some("0"),
Some("false"),
Some("off"),
Some("no"),
Some(" OFF "),
] {
assert!(!parse_cc_normalize_toggle(off), "{off:?} should disable it");
}
}
#[cfg(unix)]
#[test]
fn execute_propagates_non_zero_exit_when_compiler_runs_and_fails() {
let compiler = CcCompiler::new();
let parsed = compiler.parse(&["false".to_string()]).unwrap();
let result = compiler
.execute(&parsed)
.expect("a failed-but-spawned compiler is Ok(non-zero), not Err");
assert_ne!(
result.exit_code, 0,
"non-zero exit must reach the caller via CompileResult.exit_code"
);
}
#[test]
fn classify_output_delegates_to_shared_classifier() {
let compiler = CcCompiler::new();
let parsed = compiler.parse(&s(&["cc"])).unwrap();
assert_eq!(
compiler.classify_output(&parsed, "foo.o"),
ArtifactKind::Object
);
assert_eq!(
compiler.classify_output(&parsed, "libfoo.dylib"),
ArtifactKind::DynamicLibrary
);
assert_eq!(
compiler.classify_output(&parsed, "foo.d"),
ArtifactKind::DepInfo
);
assert_eq!(
compiler.classify_output(&parsed, "foo.o.pp"),
ArtifactKind::DepInfo
);
}
}