use anyhow::{Context, Result};
use regex::Regex;
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::ffi::OsStr;
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use super::flags::{Dialect, FlagClass, FlagSpec, Matcher};
use super::{
Artifact, ArtifactKind, ArtifactSet, CompileResult, Compiler, CompilerAdapter, CompilerId,
KeyCtx, RefuseReason, classify_by_filename,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolFamily {
Gnu,
Clang,
ClangCl,
}
impl ToolFamily {
pub fn dialect(self) -> Dialect {
match self {
ToolFamily::Gnu | ToolFamily::Clang => Dialect::Gnu,
ToolFamily::ClangCl => Dialect::Cl,
}
}
pub fn detect(program: &str, rest: &[String]) -> ToolFamily {
let name = super::command_basename(program)
.map(super::strip_windows_exe_suffix)
.unwrap_or(program)
.to_ascii_lowercase();
if rest.iter().any(|a| a == "--driver-mode=cl") {
return ToolFamily::ClangCl;
}
if name == "zigcc" || name.starts_with("zigcc-") {
return ToolFamily::Clang;
}
named_tool_family(&name).unwrap_or(ToolFamily::Gnu)
}
}
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>,
pub family: ToolFamily,
}
const SOURCE_EXTENSIONS: &[&str] = &[
"c", "cc", "cpp", "cxx", "c++", "C", "m", "mm", "M", "i", "ii", "S", "s", "sx", ];
const LANGUAGE_OVERRIDE_ALLOWLIST: &[&str] = &[
"c",
"c++",
"objective-c",
"objective-c++",
"assembler",
"assembler-with-cpp",
"cpp-output",
"c++-cpp-output",
"objective-c-cpp-output",
"objective-c++-cpp-output",
];
#[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,
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,
dialect: Option<Dialect>,
}
#[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",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("/c"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetMode(CompileMode::Compile),
bucket: CcArgBucket::Structural,
source: "compile mode marker (cl)",
dialect: Some(Dialect::Cl),
},
CcArgSpec {
matcher: Matcher::Exact("-E"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetMode(CompileMode::Preprocess),
bucket: CcArgBucket::Structural,
source: "preprocess mode marker",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-S"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetMode(CompileMode::Assemble),
bucket: CcArgBucket::Structural,
source: "assembly mode marker",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-o"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::SetOutput,
bucket: CcArgBucket::Artifact,
source: "primary output path",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-fPIC"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetPic,
bucket: CcArgBucket::ModeledInKey,
source: "position-independent code",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-fpic"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetPic,
bucket: CcArgBucket::ModeledInKey,
source: "position-independent code",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-g"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(2),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-g0"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(0),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-g1"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(1),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-g2"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(2),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-g3"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetDebugLevel(3),
bucket: CcArgBucket::ModeledInKey,
source: "debug-info level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-O"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O1),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-O0"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O0),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-O1"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O1),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-O2"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O2),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-O3"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::O3),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-Os"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::Os),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-Oz"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::Oz),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-Og"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::SetOptimization(OptLevel::Og),
bucket: CcArgBucket::ModeledInKey,
source: "optimization level",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Exact("-MD"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepIncludeSystem(true),
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Exact("-MMD"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepIncludeSystem(false),
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Exact("-MP"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepPhonyTargets,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar phony targets",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Exact("-MG"),
value_form: CcArgValueForm::Flag,
action: CcArgAction::DepMissingGenerated,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency sidecar generated headers",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Exact("-MF"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::DepOutput,
bucket: CcArgBucket::Artifact,
source: "dependency output path",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Exact("-MT"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::DepTarget,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency target",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Exact("-MQ"),
value_form: CcArgValueForm::Separated,
action: CcArgAction::DepTarget,
bucket: CcArgBucket::NoObjectEffect,
source: "dependency target",
dialect: Some(Dialect::Gnu),
},
CcArgSpec {
matcher: Matcher::Prefix("-x"),
value_form: CcArgValueForm::CanBeSeparated { prefix: "-x" },
action: CcArgAction::LanguageOverride,
bucket: CcArgBucket::ProbeKeyed,
source: "language override",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Prefix("-I"),
value_form: CcArgValueForm::CanBeSeparated { prefix: "-I" },
action: CcArgAction::Include,
bucket: CcArgBucket::Preprocessor,
source: "include search path",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Prefix("-D"),
value_form: CcArgValueForm::CanBeSeparated { prefix: "-D" },
action: CcArgAction::Define,
bucket: CcArgBucket::Preprocessor,
source: "preprocessor define",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Prefix("-std="),
value_form: CcArgValueForm::Concatenated { prefix: "-std=" },
action: CcArgAction::SetStd,
bucket: CcArgBucket::ModeledInKey,
source: "language standard",
dialect: None,
},
CcArgSpec {
matcher: Matcher::Prefix("-Fo"),
value_form: CcArgValueForm::Concatenated { prefix: "-Fo" },
action: CcArgAction::SetOutput,
bucket: CcArgBucket::Artifact,
source: "clang-cl object output (#285)",
dialect: Some(Dialect::Cl),
},
CcArgSpec {
matcher: Matcher::Prefix("/Fo"),
value_form: CcArgValueForm::Concatenated { prefix: "/Fo" },
action: CcArgAction::SetOutput,
bucket: CcArgBucket::Artifact,
source: "clang-cl object output (#285)",
dialect: Some(Dialect::Cl),
},
CcArgSpec {
matcher: Matcher::Prefix("-std:"),
value_form: CcArgValueForm::Concatenated { prefix: "-std:" },
action: CcArgAction::SetStd,
bucket: CcArgBucket::ModeledInKey,
source: "clang-cl language standard (#285)",
dialect: Some(Dialect::Cl),
},
CcArgSpec {
matcher: Matcher::Prefix("/std:"),
value_form: CcArgValueForm::Concatenated { prefix: "/std:" },
action: CcArgAction::SetStd,
bucket: CcArgBucket::ModeledInKey,
source: "clang-cl language standard (#285)",
dialect: Some(Dialect::Cl),
},
];
impl CcArgs {
pub fn parse(args: &[String]) -> Result<Self> {
let (program, rest) = args
.split_first()
.context("cc invocation missing argv[0]")?;
let family = ToolFamily::detect(program, rest);
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,
family,
};
let mut depinfo: Option<DepInfoSpec> = None;
let mut idx = 0;
while idx < rest.len() {
if let Some(arg) = parse_cc_arg_at(rest, idx, family.dialect()) {
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",
)),
CompileMode::Preprocess if self.output.is_none() => reasons.push(
RefuseReason::Unsupported("cc preprocessor mode -E to stdout — not yet"),
),
CompileMode::Preprocess => {}
CompileMode::Assemble => {
reasons.push(RefuseReason::Unsupported("cc assembly mode -S — not yet"))
}
}
if let Some(output) = &self.output
&& output.as_os_str() == "-"
{
reasons.push(RefuseReason::Unsupported("cc output to stdout — not yet"));
}
if !reasons.is_empty() {
return reasons;
}
if self.requires_compiler_output_semantics() {
reasons.push(RefuseReason::Unsupported(
"existing output path requires compiler write semantics — caching not yet supported",
));
}
if let Some(language) = &self.language_override
&& !LANGUAGE_OVERRIDE_ALLOWLIST.contains(&language.as_str())
{
reasons.push(RefuseReason::Unsupported(
"cc language override -x outside the single-pass C family — not yet",
));
}
let cuda_input = self
.rest
.iter()
.any(|arg| arg.ends_with(".cu") || arg.ends_with(".cuh"));
if cuda_input {
reasons.push(RefuseReason::Unsupported(
"cc CUDA source input (.cu/.cuh) — not yet",
));
}
if self.rest.iter().any(|a| a.starts_with('@')) {
reasons.push(RefuseReason::Unsupported(
"cc response file @file (expansion) — not yet",
));
}
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",
));
}
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",
));
break;
}
}
if self.rest.iter().any(|a| a == "-gsplit-dwarf") {
reasons.push(RefuseReason::Unsupported("cc -gsplit-dwarf — not yet"));
}
for flag in &["-include-pch", "-emit-pch"] {
if self.rest.iter().any(|a| a == flag) {
reasons.push(RefuseReason::Unsupported(
"cc precompiled headers — not yet",
));
break;
}
}
let is_pch = |p: &str| p.ends_with(".pch") || p.ends_with(".gch");
let mut iter = self.rest.iter().peekable();
while let Some(arg) = iter.next() {
let pch = match arg.strip_prefix("--include=") {
Some(value) => is_pch(value),
None => {
(arg == "-include" || arg == "--include")
&& iter.peek().is_some_and(|next| is_pch(next))
}
};
if pch {
reasons.push(RefuseReason::Unsupported(
"cc precompiled headers — not yet",
));
break;
}
}
for flag in &["-fmodules", "-fcxx-modules"] {
if self.rest.iter().any(|a| a == flag) {
reasons.push(RefuseReason::Unsupported("cc modules — not yet"));
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): {} — not yet", 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",
));
} else if self.sources.is_empty() && !cuda_input {
reasons.push(RefuseReason::Unsupported("cc no source file — not yet"));
}
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()?;
let ext = match self.family.dialect() {
Dialect::Cl => "obj",
Dialect::Gnu => "o",
};
Some(PathBuf::from(format!("{}.{ext}", 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(crate) fn compiler_output_paths(&self) -> Vec<PathBuf> {
if self.mode != CompileMode::Compile {
return Vec::new();
}
let objects = if let Some(output) = &self.output {
vec![output.clone()]
} else {
let ext = match self.family.dialect() {
Dialect::Cl => "obj",
Dialect::Gnu => "o",
};
self.sources
.iter()
.filter_map(|source| {
source
.file_stem()
.map(|stem| PathBuf::from(format!("{}.{ext}", stem.to_string_lossy())))
})
.collect()
};
let mut paths = objects.clone();
if let Some(depinfo) = &self.depinfo
&& depinfo.emit
{
if let Some(output) = &depinfo.output {
paths.push(output.clone());
} else {
paths.extend(objects.into_iter().map(|mut object| {
object.set_extension("d");
object
}));
}
}
paths
}
pub(crate) fn requires_compiler_output_semantics(&self) -> bool {
self.compiler_output_paths()
.into_iter()
.any(|path| output_path_requires_compiler_semantics(&path))
}
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 embeds_codeview_debug(&self) -> bool {
cl_debug_present(self)
}
pub fn config_args(&self) -> Vec<String> {
let mut out = Vec::new();
let mut iter = self.rest.iter();
let drops_value: &[&str] = match self.family.dialect() {
Dialect::Gnu => &["-o", "-MF", "-MT", "-MQ"],
Dialect::Cl => &["-o"],
};
while let Some(arg) = iter.next() {
if drops_value.contains(&arg.as_str()) {
iter.next(); } else if self.family.dialect() == Dialect::Cl
&& (arg.starts_with("-Fo") || arg.starts_with("/Fo"))
{
} else if self
.sources
.iter()
.any(|s| s.to_str() == Some(arg.as_str()))
{
} else {
out.push(arg.clone());
}
}
out
}
}
pub(crate) fn output_path_requires_compiler_semantics(path: &Path) -> bool {
match std::fs::symlink_metadata(path) {
Ok(meta) => !meta.file_type().is_file() || !regular_output_is_replaceable(path, &meta),
Err(err) => err.kind() != std::io::ErrorKind::NotFound,
}
}
fn regular_output_is_replaceable(path: &Path, meta: &std::fs::Metadata) -> bool {
regular_output_is_independent(path, meta) && regular_output_is_owner_writable(meta)
}
fn regular_output_is_owner_writable(meta: &std::fs::Metadata) -> bool {
if meta.permissions().readonly() {
return false;
}
#[cfg(unix)]
{
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let current_uid = unsafe { libc::geteuid() };
meta.uid() == current_uid && meta.permissions().mode() & 0o200 != 0
}
#[cfg(not(unix))]
{
true
}
}
#[cfg(unix)]
fn regular_output_is_independent(_path: &Path, meta: &std::fs::Metadata) -> bool {
use std::os::unix::fs::MetadataExt;
meta.nlink() == 1
}
#[cfg(windows)]
fn regular_output_is_independent_windows(path: &Path, _meta: &std::fs::Metadata) -> bool {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,
};
let Ok(file) = std::fs::File::open(path) else {
return false;
};
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let ok = unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut info) };
ok != 0 && info.nNumberOfLinks == 1
}
#[cfg(windows)]
use self::regular_output_is_independent_windows as regular_output_is_independent;
#[cfg(not(any(unix, windows)))]
fn regular_output_is_independent_unsupported(_path: &Path, _meta: &std::fs::Metadata) -> bool {
true
}
#[cfg(not(any(unix, windows)))]
use self::regular_output_is_independent_unsupported as regular_output_is_independent;
fn parse_cc_arg_at(args: &[String], idx: usize, dialect: Dialect) -> Option<ParsedCcArg> {
let arg = args.get(idx)?;
CC_ARG_SPECS
.iter()
.find_map(|spec| parse_cc_arg_with_spec(spec, args, idx, arg, dialect))
}
fn parse_cc_arg_with_spec(
spec: &'static CcArgSpec,
args: &[String],
idx: usize,
arg: &str,
dialect: Dialect,
) -> Option<ParsedCcArg> {
if let Some(d) = spec.dialect
&& d != dialect
{
return None;
}
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 = "/kache/cc-root";
#[cfg(target_os = "linux")]
const CC_BUILD_SENTINEL: &str = "/proc/self/cwd";
#[cfg(not(target_os = "linux"))]
const CC_BUILD_SENTINEL: &str = "/kache/cc-build";
const CC_SOURCE_SENTINEL: &str = "/kache/cc-source";
pub(crate) const CC_DEPINFO_STORE_NAME: &str = "__kache_cc_depinfo.d";
const CC_BASE_SENTINEL: &str = "/kache/base-dir";
const CC_SDKROOT_SENTINEL: &str = "/kache/sdkroot";
#[derive(Debug, Clone, PartialEq, Eq)]
struct CcPrefixMap {
from: String,
to: String,
}
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> {
match parsed.family.dialect() {
Dialect::Gnu => {
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
}
Dialect::Cl => {
let mut out = vec!["/EP".to_string()];
let mut iter = parsed.rest.iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"-c" | "-S" => {}
"-o" => {
iter.next();
}
_ if arg.starts_with("-Fo") || arg.starts_with("/Fo") => {}
_ => out.push(arg.clone()),
}
}
out
}
}
}
const CC_PREPROCESS_MEMO_TARGET: &str = "__kache_preprocess_memo";
fn add_preprocess_dep_capture(
parsed: &CcArgs,
pp_args: Vec<String>,
dep_path: &Path,
) -> Vec<String> {
let path = dep_path
.to_str()
.expect("dependency capture is enabled only for UTF-8 temp paths")
.to_string();
let extra = match parsed.family.dialect() {
Dialect::Gnu => vec![
"-MD".to_string(),
"-MF".to_string(),
path,
"-MT".to_string(),
CC_PREPROCESS_MEMO_TARGET.to_string(),
],
Dialect::Cl => vec![
"-Xclang".to_string(),
"-dependency-file".to_string(),
"-Xclang".to_string(),
path,
"-Xclang".to_string(),
"-MT".to_string(),
"-Xclang".to_string(),
CC_PREPROCESS_MEMO_TARGET.to_string(),
"-Xclang".to_string(),
"-sys-header-deps".to_string(),
],
};
compose_cc_args(&pp_args, extra)
}
fn parse_preprocess_dependencies(raw: &str, cwd: &Path) -> Result<Vec<PathBuf>> {
let bytes = raw.as_bytes();
let mut logical = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'\\' && bytes.get(index + 1) == Some(&b'\n') {
logical.push(b' ');
index = index.saturating_add(2);
} else if bytes[index] == b'\\'
&& bytes.get(index + 1) == Some(&b'\r')
&& bytes.get(index + 2) == Some(&b'\n')
{
logical.push(b' ');
index = index.saturating_add(3);
} else {
logical.push(bytes[index]);
index = index.saturating_add(1);
}
}
let logical = std::str::from_utf8(&logical).context("cc dependency file is not UTF-8")?;
let (rule, separator) = logical
.lines()
.find_map(|line| {
line.find(": ")
.or_else(|| line.find(":\t"))
.map(|separator| (line, separator))
})
.context("cc dependency file has no Make rule")?;
let dependencies = &rule[separator + 1..];
let mut paths = Vec::new();
let mut word = String::new();
let mut chars = dependencies.chars().peekable();
while let Some(character) = chars.next() {
match character {
'\\' => match chars.peek().copied() {
Some(next) if matches!(next, ' ' | '\t' | '#' | '\\' | ':') => {
word.push(next);
chars.next();
}
Some(_) => word.push('\\'),
None => anyhow::bail!("cc dependency rule ends in an escape"),
},
'$' if chars.peek() == Some(&'$') => {
word.push('$');
chars.next();
}
'#' => break,
whitespace if whitespace.is_whitespace() => {
if !word.is_empty() {
paths.push(PathBuf::from(std::mem::take(&mut word)));
}
}
other => word.push(other),
}
}
if !word.is_empty() {
paths.push(PathBuf::from(word));
}
if paths.is_empty() {
anyhow::bail!("cc dependency rule contains no inputs");
}
for path in &mut paths {
let text = path.to_string_lossy();
let bytes = text.as_bytes();
let windows_absolute = bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\');
if path.is_relative() && !windows_absolute && !text.starts_with("\\\\") {
*path = cwd.join(&*path);
}
}
paths.sort();
paths.dedup();
Ok(paths)
}
#[derive(Debug)]
struct PreprocessHash {
hash: String,
fingerprints: Option<Vec<crate::cache_key::CcPreprocessMemoInput>>,
path_bound: bool,
}
fn key_preprocess_args(parsed: &CcArgs, prefix_maps: &[CcPrefixMap]) -> Vec<String> {
compose_cc_args(
&build_preprocess_args(parsed),
file_prefix_map_args(prefix_maps),
)
}
#[derive(Debug, PartialEq, Eq)]
struct CcExpansionHash {
hash: String,
path_bound: bool,
}
fn hash_cc_expansion(raw: Vec<u8>, prefix_maps: &[CcPrefixMap]) -> CcExpansionHash {
let mapped = apply_cc_prefix_maps_to_bytes(raw.clone(), prefix_maps);
if mapped == raw {
return CcExpansionHash {
hash: blake3::hash(&mapped).to_hex().to_string(),
path_bound: false,
};
}
let mut roots: Vec<&str> = prefix_maps
.iter()
.map(|map| map.from.as_str())
.filter(|from| !from.is_empty())
.collect();
roots.sort_unstable();
roots.dedup();
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache.cc.path-bound-expansion.v1\0");
for root in roots {
hasher.update(root.as_bytes());
hasher.update(b"\0");
}
hasher.update(b"\n");
hasher.update(&raw);
CcExpansionHash {
hash: hasher.finalize().to_hex().to_string(),
path_bound: true,
}
}
const CC_ASSEMBLER_FILE_DIRECTIVES: [&str; 2] = [".incbin", ".include"];
const CC_ASSEMBLER_MACRO_DIRECTIVES: [&str; 4] = [".macro", ".irp", ".irpc", ".rept"];
fn cc_assembler_hidden_input(expansion: &[u8]) -> Option<&'static str> {
if let Some(found) = cc_assembler_text_hidden_input(expansion) {
return Some(found);
}
let literals = cc_string_literals(expansion);
if literals.computed_asm {
return Some("asm((...))");
}
if literals.named_escape {
return Some(r"\N{...}");
}
cc_assembler_text_hidden_input(&literals.text)
}
fn cc_assembler_text_hidden_input(text: &[u8]) -> Option<&'static str> {
let file = CC_ASSEMBLER_FILE_DIRECTIVES.into_iter().find(|directive| {
cc_directive_operands(text, directive.as_bytes()).any(|rest| {
rest.iter()
.find(|byte| !matches!(byte, b' ' | b'\t'))
.is_some_and(|byte| matches!(byte, b'"' | b'\\'))
})
});
file.or_else(|| {
CC_ASSEMBLER_MACRO_DIRECTIVES.into_iter().find(|directive| {
cc_directive_operands(text, directive.as_bytes()).any(|rest| {
rest.first()
.is_some_and(|byte| matches!(byte, b' ' | b'\t'))
})
})
})
.or_else(|| {
cc_directive_operands(text, b".altmacro")
.next()
.map(|_| ".altmacro")
})
.or_else(|| {
text.windows(3)
.any(|window| window == br"\()")
.then_some(r"\()")
})
}
#[derive(Debug, Default, PartialEq, Eq)]
struct CcStringLiterals {
text: Vec<u8>,
computed_asm: bool,
named_escape: bool,
}
const CC_RAW_STRING_PREFIXES: [&[u8]; 5] = [b"R", b"u8R", b"uR", b"UR", b"LR"];
const CC_LITERAL_PREFIXES: [&[u8]; 4] = [b"u8", b"u", b"U", b"L"];
fn cc_string_literals(src: &[u8]) -> CcStringLiterals {
let mut literals = CcStringLiterals::default();
let mut joining = false;
let mut i = 0;
while let Some(&byte) = src.get(i) {
let next = if byte.is_ascii_whitespace() {
i + 1
} else if byte == b'/' && matches!(src.get(i + 1), Some(b'*' | b'/')) {
cc_skip_comment(src, i)
} else {
cc_read_token(src, i, &mut literals, &mut joining)
};
debug_assert!(next > i, "string literal reader must advance");
i = next;
}
literals
}
fn cc_read_token(
src: &[u8],
i: usize,
literals: &mut CcStringLiterals,
joining: &mut bool,
) -> usize {
let word_end = cc_word_end(src, i);
let word = &src[i..word_end];
let quote = src.get(word_end).copied();
let plain = word.is_empty() || CC_LITERAL_PREFIXES.contains(&word);
let raw_paren = (quote == Some(b'"') && CC_RAW_STRING_PREFIXES.contains(&word))
.then(|| cc_raw_delimiter(src, word_end))
.flatten();
if raw_paren.is_some() || (quote == Some(b'"') && plain) {
if !*joining {
literals.text.push(b'\n');
}
*joining = true;
return match raw_paren {
Some(paren) => cc_read_raw_string(src, word_end, paren, &mut literals.text),
None => cc_read_string(src, word_end, literals),
};
}
*joining = false;
if quote == Some(b'\'') && plain {
return cc_skip_char_literal(src, word_end);
}
if word.is_empty() {
return i + 1;
}
if matches!(word, b"asm" | b"__asm" | b"__asm__") && !cc_asm_text_is_literal(src, word_end) {
literals.computed_asm = true;
}
word_end
}
fn cc_word_end(src: &[u8], start: usize) -> usize {
let is_word = |byte: &u8| byte.is_ascii_alphanumeric() || *byte == b'_';
if !src.get(start).is_some_and(is_word) {
return start;
}
let number = src[start].is_ascii_digit();
let mut i = start;
while let Some(byte) = src.get(i) {
let separator = number && *byte == b'\'' && src.get(i + 1).is_some_and(is_word);
if !is_word(byte) && !separator {
break;
}
let next = i + 1;
debug_assert!(next > i, "word reader must advance");
i = next;
}
i
}
fn cc_read_string(src: &[u8], open: usize, literals: &mut CcStringLiterals) -> usize {
let mut i = open + 1;
while let Some(&byte) = src.get(i) {
let next = match byte {
b'"' | b'\n' => return i + 1,
b'\\' => cc_decode_escape(src, i + 1, literals),
_ => {
literals.text.push(byte);
i + 1
}
};
debug_assert!(next > i, "string reader must advance");
i = next;
}
i
}
fn cc_decode_escape(src: &[u8], at: usize, literals: &mut CcStringLiterals) -> usize {
let out = &mut literals.text;
let digits = |from: usize, max: usize, radix: u32| {
let len = src[from..]
.iter()
.take(max)
.take_while(|byte| char::from(**byte).is_digit(radix))
.count();
let value = src[from..from + len].iter().fold(0u32, |value, byte| {
value
.wrapping_mul(radix)
.wrapping_add(char::from(*byte).to_digit(radix).unwrap_or(0))
});
(value, from + len)
};
let push_code_point = |out: &mut Vec<u8>, value: u32| {
let mut utf8 = [0; 4];
let decoded = char::from_u32(value).unwrap_or(' ');
out.extend_from_slice(decoded.encode_utf8(&mut utf8).as_bytes());
};
let Some(&kind) = src.get(at) else {
return at;
};
if matches!(kind, b'x' | b'o' | b'u') && src.get(at + 1) == Some(&b'{') {
let (value, end) = digits(at + 2, usize::MAX, if kind == b'o' { 8 } else { 16 });
if kind == b'u' {
push_code_point(out, value);
} else {
out.push(value as u8);
}
return end + usize::from(src.get(end) == Some(&b'}'));
}
match kind {
b'x' => {
let (value, end) = digits(at + 1, usize::MAX, 16);
out.push(value as u8);
end
}
b'0'..=b'7' => {
let (value, end) = digits(at, 3, 8);
out.push(value as u8);
end
}
b'u' | b'U' => {
let (value, end) = digits(at + 1, if kind == b'u' { 4 } else { 8 }, 16);
push_code_point(out, value);
end
}
b'n' => {
out.push(b'\n');
at + 1
}
b't' => {
out.push(b'\t');
at + 1
}
b'r' | b'f' | b'v' | b'a' | b'b' => {
out.push(b' ');
at + 1
}
b'N' if src.get(at + 1) == Some(&b'{') => {
literals.named_escape = true;
let close = src[at..]
.iter()
.position(|byte| matches!(byte, b'}' | b'"' | b'\n'));
close.map_or(src.len(), |offset| {
at + offset + usize::from(src[at + offset] == b'}')
})
}
other => {
out.push(other);
at + 1
}
}
}
fn cc_raw_delimiter(src: &[u8], open: usize) -> Option<usize> {
let body = src.get(open + 1..)?;
let paren = body.iter().take(17).position(|byte| *byte == b'(')?;
body[..paren]
.iter()
.all(|byte| !byte.is_ascii_whitespace() && !matches!(byte, b'\\' | b')' | b'"'))
.then_some(paren)
}
fn cc_read_raw_string(src: &[u8], open: usize, paren: usize, out: &mut Vec<u8>) -> usize {
let body = &src[open + 1..];
let mut close = vec![b')'];
close.extend_from_slice(&body[..paren]);
close.push(b'"');
let text = &body[paren + 1..];
let len = text
.windows(close.len())
.position(|window| window == close.as_slice())
.unwrap_or(text.len());
out.extend_from_slice(&text[..len]);
open + 1 + paren + 1 + len + close.len()
}
fn cc_skip_char_literal(src: &[u8], open: usize) -> usize {
let mut i = open + 1;
while let Some(&byte) = src.get(i) {
let next = match byte {
b'\\' => i + 2,
b'\'' | b'\n' => return i + 1,
_ => i + 1,
};
debug_assert!(next > i, "character literal reader must advance");
i = next;
}
i
}
fn cc_skip_comment(src: &[u8], start: usize) -> usize {
let close: &[u8] = if src.get(start + 1) == Some(&b'*') {
b"*/"
} else {
b"\n"
};
let rest = start + 2;
src[rest.min(src.len())..]
.windows(close.len())
.position(|window| window == close)
.map_or(src.len(), |offset| rest + offset + close.len())
}
fn cc_asm_text_is_literal(src: &[u8], after: usize) -> bool {
let skip_blanks = |mut i: usize| loop {
let next = if src.get(i).is_some_and(u8::is_ascii_whitespace) {
i + 1
} else if src.get(i) == Some(&b'/') && matches!(src.get(i + 1), Some(b'*' | b'/')) {
cc_skip_comment(src, i)
} else {
return i;
};
debug_assert!(next > i, "asm reader must advance");
i = next;
};
let mut i = skip_blanks(after);
loop {
let end = cc_word_end(src, i);
match &src[i..end] {
b"volatile" | b"__volatile__" | b"__volatile" | b"inline" | b"__inline__" | b"goto" => {
i = skip_blanks(end)
}
b"" if src.get(i) == Some(&b'(') => break,
_ => return true,
}
}
let start = skip_blanks(i + 1);
let end = cc_word_end(src, start);
let word = &src[start..end];
src.get(end) == Some(&b'"')
&& (word.is_empty()
|| CC_LITERAL_PREFIXES.contains(&word)
|| CC_RAW_STRING_PREFIXES.contains(&word))
}
fn cc_directive_operands<'a>(
expansion: &'a [u8],
name: &'a [u8],
) -> impl Iterator<Item = &'a [u8]> + 'a {
expansion
.iter()
.enumerate()
.filter(|(_, byte)| **byte == b'.')
.filter_map(move |(start, _)| {
let candidate = expansion.get(start..start + name.len())?;
if !candidate.eq_ignore_ascii_case(name) {
return None;
}
let rest = &expansion[start + name.len()..];
let continues_word = rest
.first()
.is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_');
(!continues_word).then_some(rest)
})
}
fn bytes_embed_mapped_root(bytes: &[u8], prefix_maps: &[CcPrefixMap]) -> bool {
prefix_maps
.iter()
.map(|map| map.from.as_bytes())
.filter(|from| !from.is_empty())
.any(|from| {
bytes
.iter()
.enumerate()
.filter(|(_, byte)| **byte == from[0])
.any(|(start, _)| bytes[start..].starts_with(from))
})
}
fn cc_object_embeds_mapped_root(path: &Path, prefix_maps: &[CcPrefixMap]) -> std::io::Result<bool> {
std::fs::read(path).map(|bytes| bytes_embed_mapped_root(&bytes, prefix_maps))
}
fn cc_unsafe_to_store(
no_artifacts: bool,
key_path_bound: bool,
object_embeds_root: impl FnOnce() -> Option<std::io::Result<bool>>,
) -> Option<String> {
if no_artifacts || key_path_bound {
return None;
}
match object_embeds_root() {
None => Some("has no object to check for checkout roots".to_string()),
Some(Ok(false)) => None,
Some(Ok(true)) => {
Some("embeds a checkout root the prefix maps did not rewrite".to_string())
}
Some(Err(error)) => Some(format!(
"could not be read to check for checkout roots: {error}"
)),
}
}
#[derive(Debug)]
pub(crate) struct CcHiddenInput {
pub(crate) construct: &'static str,
}
impl std::fmt::Display for CcHiddenInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"cc: the assembler may read a file the cache key cannot see (`{}`)",
self.construct
)
}
}
impl std::error::Error for CcHiddenInput {}
fn preprocess_hash(
parsed: &CcArgs,
prefix_maps: &[CcPrefixMap],
file_hasher: &crate::cache_key::FileHasher<'_>,
capture_dependencies: bool,
) -> Result<PreprocessHash> {
let pp_args = key_preprocess_args(parsed, prefix_maps);
let dep_temp = if capture_dependencies {
match tempfile::Builder::new()
.prefix("kache-cc-preprocess-")
.tempdir()
{
Ok(temp) => Some(temp),
Err(error) => {
tracing::debug!("cc preprocess dependency tempdir unavailable: {error}");
None
}
}
} else {
None
};
let dep_path = dep_temp
.as_ref()
.map(|dir| dir.path().join("inputs.d"))
.filter(|path| path.to_str().is_some());
let pp_args = dep_path.as_deref().map_or(pp_args.clone(), |path| {
add_preprocess_dep_capture(parsed, pp_args.clone(), path)
});
let run = |args: &[String]| {
crate::opcounts::record_preprocessor_run();
let mut command = Command::new(&parsed.program);
command.args(args);
if let Some(epoch) = effective_source_date_epoch() {
command.env("SOURCE_DATE_EPOCH", epoch);
}
command
.output()
.with_context(|| format!("running preprocessor `{}`", parsed.program))
};
let mut output = run(&pp_args)?;
if !output.status.success() && dep_path.is_some() {
tracing::debug!("cc preprocess dependency capture failed; retrying without memo capture");
output = run(&key_preprocess_args(parsed, prefix_maps))?;
}
if !output.status.success() {
anyhow::bail!(
"cc -E key probe exited {}",
output
.status
.code()
.map_or_else(|| "by signal".to_string(), |c| c.to_string())
);
}
if output.stdout.is_empty() {
anyhow::bail!("cc -E key probe produced no output");
}
if let Some(construct) = cc_assembler_hidden_input(&output.stdout) {
return Err(CcHiddenInput { construct }.into());
}
let CcExpansionHash { hash, path_bound } = hash_cc_expansion(output.stdout, prefix_maps);
let fingerprints = dep_path
.as_deref()
.filter(|_| !path_bound)
.and_then(|path| {
let dependencies = std::fs::read_to_string(path)
.context("reading cc preprocess dependency file")
.and_then(|raw| {
let cwd = std::env::current_dir().context("reading cc compiler directory")?;
parse_preprocess_dependencies(&raw, &cwd)
});
match dependencies {
Ok(paths) => {
let mapped: Vec<(String, PathBuf)> = paths
.into_iter()
.map(|path| (cc_mapped_path(&path, prefix_maps), path))
.collect();
file_hasher.cc_preprocess_fingerprints(&mapped, &|path| {
cc_mapped_content_hash(path, prefix_maps)
})
}
Err(error) => {
tracing::debug!("cc preprocess dependency capture unavailable: {error:#}");
None
}
}
});
Ok(PreprocessHash {
hash,
fingerprints,
path_bound,
})
}
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.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-g[0-3]?"),
class: FlagClass::ModeledInKey,
source: "PR #94 — debug level. Regex captures `-g`/`-g0..3`; -gdwarf-* etc. refuse.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fPIC"),
class: FlagClass::ModeledInKey,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fpic"),
class: FlagClass::ModeledInKey,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-std="),
class: FlagClass::ModeledInKey,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-arch"),
class: FlagClass::ModeledInKey,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-c"),
class: FlagClass::ParserHandled,
source: "PR #94 — compile mode marker parsed into CompileMode.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("--"),
class: FlagClass::NoObjectEffect,
source: "Firefox Windows 0.20 nightly — end-of-options marker.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("/c"),
class: FlagClass::ParserHandled,
source: "Issue #312 — MSVC /c compile-mode marker, cl dialect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-E"),
class: FlagClass::ParserHandled,
source: "Flag audit — preprocessor mode marker parsed into CompileMode.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-S"),
class: FlagClass::ParserHandled,
source: "Flag audit — assembly mode marker parsed into CompileMode.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("--driver-mode="),
class: FlagClass::ParserHandled,
source: "Issue #411 — driver-mode selector; consumed by ToolFamily::detect, effects keyed via preprocessor + -### probe.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mmacosx-version-min="),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — Darwin deployment target.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-fstrict-flex-arrays="),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — strict-flex-arrays codegen knob.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-ffp-contract="),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — fp-contract codegen knob.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-ftrivial-auto-var-init=pattern"),
class: FlagClass::CapturedByProbe,
source: "Issue #849 — Firefox automatic-variable pattern initialization; Apple clang forwards the value to -cc1 verbatim (verified -###).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(concat!(
r"-f(?:no-)?(?:",
"associative-math|asynchronous-unwind-tables|data-sections|fast-math|",
"finite-math-only|function-sections|math-errno|merge-all-constants|",
"omit-frame-pointer|reciprocal-math|rounding-math|semantic-interposition|",
"signaling-nans|signed-zeros|strict-aliasing|trapping-math|",
"unsafe-math-optimizations|unroll-loops|unwind-tables|wrapv",
")",
)),
class: FlagClass::CapturedByProbe,
source: "#114/#245/#418/#422/#426/#580/#856 + 0.20 nightly — codegen knobs, both polarities, resolved into -cc1 tokens. One sorted stem per knob covers -f<stem> AND -fno-<stem> (prevents the missed-polarity passthrough class). unroll-loops / asynchronous-unwind-tables from lance/Firefox passthroughs.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mrecip="),
class: FlagClass::CapturedByProbe,
source: "Firefox nightly bench — reciprocal-estimate codegen selector; value forwarded to -cc1 (verified clang -###).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-mno-omit-leaf-frame-pointer"),
class: FlagClass::CapturedByProbe,
source: "Issue #839 — cc-rs forced-frame-pointer flag (aws-lc-sys debug builds); clang -### resolves to -mframe-pointer=all.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-pthread"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 — pthread feature switch (also visible via _REENTRANT in preprocessor).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-f(?:no-)?stack-protector(?:-strong|-all)?"),
class: FlagClass::CapturedByProbe,
source: "Issue #114 + Firefox 0.20 nightly — stack-protector family, both polarities.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fstack-clash-protection"),
class: FlagClass::CapturedByProbe,
source: "Issue #245 — stack-clash-protection codegen hardening (Firefox).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("--param"),
class: FlagClass::CapturedByProbe,
source: "Issue #580 — aws-lc-sys jitterentropy `--param ssp-buffer-size=4`; gcc forwards the pair to cc1, clang drops it.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("--param="),
class: FlagClass::CapturedByProbe,
source: "Issue #580 — joined spelling of --param; value rides through to cc1.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fsanitize-undefined-strip-path-components=-1"),
class: FlagClass::CapturedByProbe,
source: "Issue #840 — aws-lc-sys 0.44 jitterentropy UBSan path-stripping; Apple clang forwards the value to -cc1 verbatim (verified -###).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-ffile-reproducible"),
class: FlagClass::CapturedByProbe,
source: "Issue #411 — clang reproducible embedded paths; keyed via -### resolved tokens.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-file-reproducible"),
class: FlagClass::CapturedByProbe,
source: "Issue #411 — clang reproducible embedded paths (negation); keyed via -### resolved tokens.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-gdwarf-4"),
class: FlagClass::CapturedByProbe,
source: "Issue #117 — DWARF v4 emission (Firefox baseline).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-gdwarf-2"),
class: FlagClass::CapturedByProbe,
source: "Issue #838 — DWARF v2 emission; cc-rs default on Apple targets.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-gfull"),
class: FlagClass::CapturedByProbe,
source: "Issue #857 — Darwin full debug info; ring 0.17 dead-strip contract. Apple clang -### resolves it to standalone DWARF; keyed via those tokens.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-gsimple-template-names"),
class: FlagClass::CapturedByProbe,
source: "Issue #117 — clang template-name compression in debug info.",
dialect: None,
},
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.",
dialect: None,
},
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.",
dialect: None,
},
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.",
dialect: None,
},
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.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-Wa,--debug-prefix-map=[^,=]+=[^,]*"),
class: FlagClass::RawKeyed,
source: "Issue #644 — GNU assembler debug path remapping; raw-keyed because the cc1 probe omits the separate assembler subprocess.",
dialect: Some(Dialect::Gnu),
},
FlagSpec {
matcher: Matcher::Prefix("-stdlib="),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ standard-library selector (libc++ / libstdc++).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-exceptions"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ exception mode (off).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fexceptions"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ exception mode (on).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-rtti"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ RTTI mode (off).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-frtti"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ RTTI mode (on).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-sized-deallocation"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ sized-deallocation (disabled).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-aligned-new"),
class: FlagClass::CapturedByProbe,
source: "Issue #116 — C++ aligned new/delete (disabled).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fvisibility=hidden"),
class: FlagClass::CapturedByProbe,
source: "Firefox bench evidence (post-#146) — symbol visibility default = hidden.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fvisibility-inlines-hidden"),
class: FlagClass::CapturedByProbe,
source: "Firefox bench evidence (post-#146) — inline-function visibility default = hidden.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("--target="),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — cross-compilation target triple (sticky form).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-target"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — cross-compilation target triple (separate-arg form).",
dialect: None,
},
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.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mabi="),
class: FlagClass::RawKeyed,
source: "Issue #823 — target ABI selection; closed value set, keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mfloat-abi="),
class: FlagClass::RawKeyed,
source: "Issue #823 — arm float ABI; closed value set (soft/softfp/hard), keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mcmodel="),
class: FlagClass::RawKeyed,
source: "Issue #823 — code model; closed value set, keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(
r"-mfpu=(?:none|vfp|vfpv2|vfpv3(?:-fp16|-d16(?:-fp16)?|xd(?:-fp16)?)?|vfpv4(?:-d16)?|fpv4-sp-d16|fpv5-(?:sp-)?d16|fp-armv8(?:-fullfp16)?|neon(?:-fp16|-vfpv3|-vfpv4|-fp-armv8)?|crypto-neon-fp-armv8)",
),
class: FlagClass::RawKeyed,
source: "Issue #823 — concrete arm FPU selection; enumerated so `-mfpu=auto` (resolved inside cc1, not text-deterministic) still refuses.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-m(?:(?:no-)?thumb|arm)"),
class: FlagClass::RawKeyed,
source: "Issue #823 — arm/thumb instruction-set state, keyed verbatim in argv order.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-msimd128"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — WASM SIMD128 enable.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(
r"^-m(?:no-)?(?:32|64|mmx|sse|sse2|sse3|ssse3|sse4|sse4\.1|sse4\.2|sse4a|avx|avx2|avxvnni|avx512[a-z0-9]+|fma|fma4|f16c|bmi|bmi2|abm|popcnt|lzcnt|aes|vaes|pclmul|vpclmulqdq|gfni|sha|movbe|rdrnd|rdseed|adx|fsgsbase|xsave|xsaveopt|xsavec|xsaves|prfchw|clflushopt|clwb|cldemote|fxsr)$",
),
class: FlagClass::CapturedByProbe,
source: "Issue #375 (extended, Firefox nightly bench) — x86 width + SIMD/ISA codec feature flags; resolved into target-cpu/target-feature tokens.",
dialect: None,
},
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.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-x"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — language override (separate-arg form).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-x(?:c|c\+\+|objective-c|objective-c\+\+)"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 / flag audit — sticky language override forms.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fobjc-exceptions"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — Objective-C exception model.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fobjc-arc"),
class: FlagClass::CapturedByProbe,
source: "Issue #115 — Objective-C ARC mode.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-D"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-U"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-I"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("--sysroot"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-include"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("--include="),
class: FlagClass::PreprocessorCaptured,
source: "Issue #580 — aws-lc-sys boringssl_prefix_symbols forced include; `=` form of -include.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("--include"),
class: FlagClass::PreprocessorCaptured,
source: "Issue #580 — separated form of --include=<file>.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-imacros"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-isystem"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-iquote"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-idirafter"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-isysroot"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-nostdinc"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-nostdinc++"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-undef"),
class: FlagClass::PreprocessorCaptured,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-W(no-)?error.*"),
class: FlagClass::RawKeyed,
source: "review #2 — outcome gate: -Werror/-Wno-error change success vs failure; keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-pedantic-errors"),
class: FlagClass::RawKeyed,
source: "review #2 — outcome gate: -pedantic-errors changes success vs failure.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-W[^,]*"),
class: FlagClass::NoObjectEffect,
source: "PR #94 — warnings. Regex excludes `-Wl,*`/`-Wa,*`/`-Wp,*` passthrough forms.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-w"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-pedantic"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-fdiagnostics-"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fcolor-diagnostics"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-color-diagnostics"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fansi-escape-codes"),
class: FlagClass::NoObjectEffect,
source: "Issue #424/#438 — Firefox/Windows bare diagnostics flag; ANSI color escapes only, no object effect.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-MM?D|-M[FTQPG]"),
class: FlagClass::NoObjectEffect,
source: "PR #94 — gcc dep-info flags. Gnu-dialect ONLY: in cl mode -MD/-MT/-MTd/-MDd are CRT selection (codegen), -MP is multi-process; they must not classify as inert dep-info (issue #285).",
dialect: Some(Dialect::Gnu),
},
FlagSpec {
matcher: Matcher::Exact("-o"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-P"),
class: FlagClass::NoObjectEffect,
source: "Flag audit — preprocessor line-marker suppression has no compile-mode object effect.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-pipe"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-fno-lto"),
class: FlagClass::NoObjectEffect,
source: "Firefox nightly bench — explicit non-LTO `-c` compile; identical -cc1 tokens vs absent (verified clang -###). -flto/-flto=* stay refused.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-v"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("--verbose"),
class: FlagClass::NoObjectEffect,
source: "PR #94",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("--start-no-unused-arguments"),
class: FlagClass::NoObjectEffect,
source: "Issue #117 — clang unused-argument warning region (open).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("--end-no-unused-arguments"),
class: FlagClass::NoObjectEffect,
source: "Issue #117 — clang unused-argument warning region (close).",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-Fo"),
class: FlagClass::NoObjectEffect,
source: "Issue #285 — clang-cl object output path, no object-content effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/Fo"),
class: FlagClass::NoObjectEffect,
source: "Issue #285 — clang-cl object output path, no object-content effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-Zc:inline"),
class: FlagClass::NoObjectEffect,
source: "Issue #285 — clang-cl ignores -Zc:inline (no cc1 token, no object effect).",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Zc:inline"),
class: FlagClass::NoObjectEffect,
source: "Issue #285 — clang-cl ignores /Zc:inline (no cc1 token, no object effect).",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-std:"),
class: FlagClass::ModeledInKey,
source: "Issue #285 — clang-cl language standard (-std:c++NN); modeled in key via CcArgs.std.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/std:"),
class: FlagClass::ModeledInKey,
source: "Issue #285 — clang-cl language standard (/std:c++NN); modeled in key via CcArgs.std.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-FI"),
class: FlagClass::PreprocessorCaptured,
source: "Issue #285 — clang-cl forced include; content captured by /EP preprocessor hash.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/FI"),
class: FlagClass::PreprocessorCaptured,
source: "Issue #285 — clang-cl forced include; content captured by /EP preprocessor hash.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-guard:"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl Control Flow Guard. Prefix wildcard safe: -### resolves each variant to a distinct -cc1 token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/guard:"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl Control Flow Guard (/guard: spelling). Prefix wildcard safe: -### resolves each variant to a distinct -cc1 token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-fms-compatibility-version="),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — MSVC compatibility version. Prefix wildcard safe: -### reflects the exact version into a -cc1 token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-Gy"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl function-level linking (COMDAT). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Gy"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl function-level linking (COMDAT). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-Gw"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl global data optimization (COMDAT). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Gw"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl global data optimization (COMDAT). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-Oy-"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl frame-pointer omission disabled. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Oy-"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl frame-pointer omission disabled. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-MD"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded DLL (dynamic). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-MDd"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded DLL debug. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-MT"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded static. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-MTd"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded static debug. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/MD"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded DLL (dynamic). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/MDd"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded DLL debug. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/MT"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded static. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/MTd"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl CRT: multithreaded static debug. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Regex(r"[-/]O[12dx]"),
class: FlagClass::CapturedByProbe,
source: "Issue #285 — clang-cl optimization levels. Bare -O1/-O2 are caught earlier as ModeledInKey; this row covers /Onn and -Od/-Ox. -### resolves each to a distinct cc1 level. -Ofast/-Os/-Oz not matched → still refuse.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-TP"),
class: FlagClass::CapturedByProbe,
source: "Issue #411 — clang-cl force-C++ source mode (-TP). -### resolves it into the -cc1 -x c++ token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/TP"),
class: FlagClass::CapturedByProbe,
source: "Issue #411 — clang-cl force-C++ source mode (/TP). -### resolves it into the -cc1 -x c++ token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-TC"),
class: FlagClass::CapturedByProbe,
source: "Issue #411 — clang-cl force-C source mode (-TC). -### resolves it into the -cc1 -x c token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/TC"),
class: FlagClass::CapturedByProbe,
source: "Issue #411 — clang-cl force-C source mode (/TC). -### resolves it into the -cc1 -x c token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-EH"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl exception model (-EHsc, -EHs-c-, …). Prefix safe: -### resolves each variant into distinct -fexceptions/-fcxx-exceptions tokens (or their negations).",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/EH"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl exception model (/EH* spelling). Prefix safe: -### resolves each variant into distinct -fexceptions/-fcxx-exceptions tokens (or their negations).",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-GR"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl RTTI enabled (-GR). -### reflects -frtti.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-GR-"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl RTTI disabled (-GR-). -### reflects -fno-rtti.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/GR"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl RTTI enabled (/GR). -### reflects -frtti.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/GR-"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl RTTI disabled (/GR-). -### reflects -fno-rtti.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-GS"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl stack-buffer-security-check enabled (-GS). -### reflects -stack-protector.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-GS-"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl stack-buffer-security-check disabled (-GS-). -### removes -stack-protector.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/GS"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl stack-buffer-security-check enabled (/GS). -### reflects -stack-protector.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/GS-"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl stack-buffer-security-check disabled (/GS-). -### removes -stack-protector.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-Brepro"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl reproducible build (-Brepro). -### removes -mincremental-linker-compatible.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Brepro"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl reproducible build (/Brepro). -### removes -mincremental-linker-compatible.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-utf-8"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl UTF-8 source/execution charset (-utf-8). clang-cl is UTF-8 by default; the flag produces no cc1 token but is inert — CapturedByProbe is safe (keyed if token present, inert if not; probe always resolves for clang-cl).",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/utf-8"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl UTF-8 source/execution charset (/utf-8). Same rationale as -utf-8.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-Zc:"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl conformance flags (-Zc:wchar_t, -Zc:forScope, …). Prefix safe: -### captures the exact value (or flag is inert); placed AFTER the -Zc:inline Exact row so that spelling resolves NoObjectEffect first.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/Zc:"),
class: FlagClass::CapturedByProbe,
source: "#285 Layer 4 — clang-cl conformance flags (/Zc:* spelling). Prefix safe: -### captures the exact value (or flag is inert); placed AFTER the /Zc:inline Exact row so that spelling resolves NoObjectEffect first.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-FC"),
class: FlagClass::PreprocessorCaptured,
source: "#285 Layer 4 — clang-cl full-path __FILE__ (-FC). Makes __FILE__ expand to the absolute source path; that expansion is captured by the /EP preprocessor hash.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/FC"),
class: FlagClass::PreprocessorCaptured,
source: "#285 Layer 4 — clang-cl full-path __FILE__ (/FC). Makes __FILE__ expand to the absolute source path; that expansion is captured by the /EP preprocessor hash.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-nologo"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl suppress banner (-nologo). Pure build-output mechanic; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/nologo"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl suppress banner (/nologo). Pure build-output mechanic; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-wd"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl disable warning (-wdNNNN). Diagnostics only; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/wd"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl disable warning (/wdNNNN). Diagnostics only; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-FS"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl force synchronous PDB writes (-FS). Build mechanic; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/FS"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl force synchronous PDB writes (/FS). Build mechanic; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-Gm-"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl minimal rebuild disabled (-Gm-, deprecated). No object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Gm-"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl minimal rebuild disabled (/Gm-, deprecated). No object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-external:"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl external-header warning level (-external:*). Diagnostics only; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/external:"),
class: FlagClass::NoObjectEffect,
source: "#285 Layer 4 — clang-cl external-header warning level (/external:*). Diagnostics only; no object effect.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Regex(r"[-/]Z[7iId]"),
class: FlagClass::CapturedByProbe,
source: "Issue #312 — clang-cl CodeView debug-info flags; variant captured via cc -### resolved tokens; embedded paths folded by cl_debug_path_inputs.",
dialect: Some(Dialect::Cl),
},
];
#[derive(Debug, Default)]
struct FlagClassificationSummary {
modeled_in_key: usize,
raw_keyed: 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::RawKeyed) => self.raw_keyed += 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();
let dialect = parsed.family.dialect();
let mut idx = 0;
while idx < parsed.rest.len() {
let arg = &parsed.rest[idx];
if arg == "-Xclang" {
summary.record(Some(FlagClass::NoObjectEffect));
let Some(inner) = parsed.rest.get(idx + 1) else {
tracing::trace!("[cc:{subject}] flag -Xclang (no operand) -> NoObjectEffect");
idx += 1;
continue;
};
let class = classify_xclang_forwarded(inner, dialect);
summary.record(class);
match class {
Some(class) => tracing::trace!(
"[cc:{subject}] flag -Xclang {inner} -> {class:?} [forwarded cc1]"
),
None if extra_allowlist_flags.iter().any(|f| f == inner) => {
summary.user_allowed += 1;
tracing::trace!("[cc:{subject}] flag -Xclang {inner} -> user-allowed (config)");
}
None => {
tracing::trace!(
"[cc:{subject}] flag -Xclang {inner} -> unmodeled [forwarded cc1]"
);
rejected.push(inner.as_str());
}
}
idx += 2;
continue;
}
let analysis = analyze_cc_arg(arg, dialect);
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());
}
}
idx += 1;
}
if !parsed.rest.is_empty() {
tracing::debug!(
"[cc:{subject}] flag classify: {} modeled / {} raw-keyed / {} probe / {} preprocessor / {} no-effect / {} parser-handled / {} user-allowed / {} unmodeled",
summary.modeled_in_key,
summary.raw_keyed,
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 dialect = parsed.family.dialect();
let mut matched: Vec<&str> = parsed
.rest
.iter()
.map(String::as_str)
.filter(|arg| {
classify_cc_flag(arg, dialect).is_none()
&& extra_allowlist_flags.iter().any(|f| f == arg)
})
.collect();
matched.sort_unstable();
matched.dedup();
matched
}
const WA_DEBUG_PREFIX_MAP_PREFIX: &str = "-Wa,--debug-prefix-map=";
fn cc_raw_flags_for_key(parsed: &CcArgs, prefix_maps: &[CcPrefixMap]) -> Vec<Vec<u8>> {
let dialect = parsed.family.dialect();
parsed
.rest
.iter()
.filter(|arg| classify_cc_flag(arg, dialect) == Some(FlagClass::RawKeyed))
.map(|arg| normalize_raw_keyed_cc_flag(arg, prefix_maps))
.collect()
}
fn normalize_raw_keyed_cc_flag(arg: &str, prefix_maps: &[CcPrefixMap]) -> Vec<u8> {
let Some(mapping) = arg.strip_prefix(WA_DEBUG_PREFIX_MAP_PREFIX) else {
return arg.as_bytes().to_vec();
};
let Some((from, to)) = mapping.split_once('=') else {
return arg.as_bytes().to_vec();
};
let from = apply_cc_prefix_maps_to_bytes(from.as_bytes().to_vec(), prefix_maps);
let mut normalized = Vec::with_capacity(arg.len());
normalized.extend_from_slice(WA_DEBUG_PREFIX_MAP_PREFIX.as_bytes());
normalized.extend_from_slice(&from);
normalized.push(b'=');
normalized.extend_from_slice(to.as_bytes());
normalized
}
fn analyze_cc_arg(arg: &str, dialect: Dialect) -> CcArgAnalysis<'_> {
let class = classify_cc_flag(arg, dialect);
let spec = cc_arg_spec_for_token(arg, dialect);
CcArgAnalysis {
arg,
class,
bucket: cc_arg_bucket(class, spec),
normalized: normalize_cc_arg(arg, dialect),
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::RawKeyed) => CcArgBucket::RawKeyed,
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, dialect: Dialect) -> Vec<String> {
let Some(spec) = cc_arg_spec_for_token(arg, dialect) 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, dialect: Dialect) -> Option<&'static CcArgSpec> {
CC_ARG_SPECS.iter().find(|spec| {
if spec.dialect.is_some_and(|d| d != dialect) {
return false;
}
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, dialect: Dialect) -> 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)),
dialect,
)
}
const XCLANG_INERT_CC1_FLAGS: &[&str] = &[
"-dependency-file", "-MT", "-MQ", "-MP", "-MG", "-MV", "-sys-header-deps", "-module-file-deps", "-dependency-dot", "-fansi-escape-codes", ];
fn classify_xclang_forwarded(inner: &str, dialect: Dialect) -> Option<FlagClass> {
if !inner.starts_with('-') {
return Some(FlagClass::NoObjectEffect);
}
if XCLANG_INERT_CC1_FLAGS.contains(&inner) {
return Some(FlagClass::NoObjectEffect);
}
if classify_cc_flag(inner, dialect) == Some(FlagClass::CapturedByProbe) {
return Some(FlagClass::CapturedByProbe);
}
None
}
fn cc_flags_need_resolved_invocation(parsed: &CcArgs) -> bool {
let dialect = parsed.family.dialect();
if parsed
.rest
.iter()
.any(|arg| analyze_cc_arg(arg, dialect).bucket == CcArgBucket::ProbeKeyed)
{
return true;
}
parsed.rest.windows(2).any(|w| {
w[0] == "-Xclang"
&& classify_xclang_forwarded(&w[1], dialect) == Some(FlagClass::CapturedByProbe)
})
}
const CL_DEBUG_FLAGS: &[&str] = &["/Z7", "/Zi", "/ZI", "/Zd", "-Z7", "-Zi", "-ZI", "-Zd"];
fn cl_debug_present(parsed: &CcArgs) -> bool {
parsed.family.dialect() == Dialect::Cl
&& (parsed.debug_level.is_some_and(|d| d > 0)
|| parsed
.rest
.iter()
.any(|a| CL_DEBUG_FLAGS.contains(&a.as_str())))
}
fn cc_resolved_per_tu_paths(parsed: &CcArgs) -> Vec<String> {
let mut set = HashSet::new();
let mut add = |p: &Path| {
set.insert(p.to_string_lossy().into_owned());
if let Some(name) = p.file_name() {
set.insert(name.to_string_lossy().into_owned());
}
};
for src in &parsed.sources {
add(src);
}
if let Some(o) = &parsed.output {
add(o);
}
if let Some(o) = parsed.object_output_path() {
add(&o);
}
if let Some(d) = parsed.depinfo_output_path() {
add(&d);
}
if let Some(t) = parsed.depinfo.as_ref().and_then(|d| d.target.clone()) {
set.insert(t);
}
set.remove("");
set.into_iter().collect()
}
fn cl_debug_path_inputs(parsed: &CcArgs) -> Option<Vec<String>> {
if !cl_debug_present(parsed) {
return None;
}
let mut out = Vec::new();
for src in &parsed.sources {
out.push(format!("src={}", src.to_string_lossy()));
}
if let Some(o) = &parsed.output {
out.push(format!("out={}", o.to_string_lossy()));
}
let dir = parsed
.rest
.iter()
.find_map(|a| {
[
"-fdebug-compilation-dir=",
"-ffile-compilation-dir=",
"/fdebug-compilation-dir=",
"/ffile-compilation-dir=",
]
.iter()
.find_map(|p| a.strip_prefix(p))
.map(str::to_string)
})
.or_else(|| {
std::env::current_dir()
.ok()
.map(|p| p.to_string_lossy().into_owned())
});
if let Some(d) = dir {
out.push(format!("dir={d}"));
}
Some(out)
}
fn cc_prefix_maps(parsed: &CcArgs, configured_base_dirs: &[String]) -> 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());
let sdkroot = std::env::var_os("SDKROOT").filter(|v| !v.is_empty());
cc_prefix_maps_cfg(
parsed,
&cwd,
base.as_deref().map(Path::new),
sdkroot.as_deref().map(Path::new),
configured_base_dirs,
)
}
fn cc_sdk_root(parsed: &CcArgs, sdkroot_env: Option<&Path>) -> Option<PathBuf> {
let mut iter = parsed.rest.iter();
while let Some(arg) = iter.next() {
if arg == "-isysroot"
&& let Some(path) = iter.next()
&& !path.is_empty()
{
return Some(PathBuf::from(path));
}
}
sdkroot_env.map(Path::to_path_buf)
}
fn cc_prefix_maps_cfg(
parsed: &CcArgs,
cwd: &Path,
base_dir: Option<&Path>,
sdk_root: Option<&Path>,
configured_base_dirs: &[String],
) -> Vec<CcPrefixMap> {
if parsed.family.dialect() == Dialect::Cl {
return Vec::new();
}
let mut maps: Vec<CcPrefixMap> = Vec::new();
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.to_string(),
});
}
}
}
for (from, to) in crate::path_normalizer::configured_base_dir_prefix_maps(configured_base_dirs)
{
if !from.is_empty() && !maps.iter().any(|m| m.from == from) {
maps.push(CcPrefixMap { from, to });
}
}
for map in cc_prefix_maps_for(parsed, cwd) {
if !maps.iter().any(|existing| existing.from == map.from) {
maps.push(map);
}
}
if let Some(sdk) = cc_sdk_root(parsed, sdk_root) {
let sdk_abs = absolutize_path(cwd, &sdk);
for root in [sdk_abs.clone(), canonicalize_or_self(&sdk_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_SDKROOT_SENTINEL.to_string(),
});
}
}
}
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 effective_source_date_epoch() -> Option<std::ffi::OsString> {
resolve_source_date_epoch(
std::env::var_os("SOURCE_DATE_EPOCH"),
source_date_epoch_passthrough(),
)
}
fn resolve_source_date_epoch(
build_value: Option<std::ffi::OsString>,
passthrough: bool,
) -> Option<std::ffi::OsString> {
match build_value {
Some(v) => Some(v),
None if passthrough => None,
None => Some(std::ffi::OsString::from("0")),
}
}
fn source_date_epoch_passthrough() -> bool {
std::env::var("KACHE_CC_SOURCE_DATE_EPOCH")
.ok()
.map(|v| {
let v = v.trim().to_ascii_lowercase();
v == "passthrough" || v == "wallclock" || v == "off"
})
.unwrap_or(false)
}
fn cc_memo_os_bytes(value: &OsStr) -> Vec<u8> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
value.as_bytes().to_vec()
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
value.encode_wide().flat_map(u16::to_le_bytes).collect()
}
#[cfg(not(any(unix, windows)))]
{
value.to_string_lossy().into_owned().into_bytes()
}
}
fn fold_cc_memo_field(hasher: &mut blake3::Hasher, label: &[u8], value: &[u8]) {
hasher.update(&(label.len() as u64).to_le_bytes());
hasher.update(label);
hasher.update(&(value.len() as u64).to_le_bytes());
hasher.update(value);
}
const CC_MEMO_VOLATILE_ENV: &[&str] = &[
"_",
"OLDPWD",
"PWD",
"SHLVL",
"CARGO_MAKEFLAGS",
"MAKEFLAGS",
"MFLAGS",
"MAKELEVEL",
"NUM_JOBS",
];
fn cc_memo_env_is_volatile(name: &OsStr) -> bool {
let Some(name) = name.to_str() else {
return false;
};
name.starts_with("KACHE_") || CC_MEMO_VOLATILE_ENV.contains(&name)
}
fn cc_preprocess_memo_key(
parsed: &CcArgs,
prefix_maps: &[CcPrefixMap],
compiler_version: &str,
) -> Option<String> {
let epoch = effective_source_date_epoch()?;
let cwd = std::env::current_dir().ok()?;
let compiler_path = super::resolve_program_on_path(&parsed.program)?;
let compiler_metadata = std::fs::metadata(&compiler_path).ok()?;
let mut hasher = blake3::Hasher::new();
fold_cc_memo_field(&mut hasher, b"schema", b"cc-preprocess-memo-v5");
fold_cc_memo_field(
&mut hasher,
b"compiler-program",
cc_memo_os_bytes(OsStr::new(&parsed.program)).as_slice(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-path",
cc_memo_os_bytes(compiler_path.as_os_str()).as_slice(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-size",
&compiler_metadata.len().to_le_bytes(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-mtime",
&crate::cache_key::metadata_mtime_ns(&compiler_metadata).to_le_bytes(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-ctime",
&crate::cache_key::metadata_ctime_ns(&compiler_metadata).to_le_bytes(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-inode",
&crate::cache_key::metadata_inode(&compiler_metadata).to_le_bytes(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-version",
compiler_version.as_bytes(),
);
fold_cc_memo_field(
&mut hasher,
b"cwd",
cc_mapped_path(&cwd, prefix_maps).as_bytes(),
);
fold_cc_memo_field(
&mut hasher,
b"source-date-epoch",
cc_memo_os_bytes(&epoch).as_slice(),
);
for arg in build_preprocess_args(parsed) {
let mapped = apply_cc_prefix_maps_to_bytes(arg.into_bytes(), prefix_maps);
fold_cc_memo_field(&mut hasher, b"arg", &mapped);
}
for map in prefix_maps {
fold_cc_memo_field(&mut hasher, b"prefix-to", map.to.as_bytes());
}
let mut environment: Vec<(Vec<u8>, Vec<u8>)> = std::env::vars_os()
.filter(|(name, _)| !cc_memo_env_is_volatile(name))
.map(|(name, value)| (cc_memo_os_bytes(&name), cc_memo_os_bytes(&value)))
.collect();
environment.sort();
for (name, value) in environment {
fold_cc_memo_field(&mut hasher, b"env-name", &name);
fold_cc_memo_field(&mut hasher, b"env-value", &value);
}
Some(hasher.finalize().to_hex().to_string())
}
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();
push_cwd_source_roots(&mut roots, &cwd_abs, &source_parent);
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 push_cwd_source_roots(
roots: &mut Vec<(PathBuf, &'static str)>,
cwd_abs: &Path,
source_parent: &Path,
) {
let common = common_ancestor(cwd_abs, source_parent);
let nested = matches!(&common, Some(c) if c == cwd_abs || c == source_parent);
if nested {
if let Some(common) = common {
roots.push((common, CC_ROOT_SENTINEL));
}
} else {
roots.push((cwd_abs.to_path_buf(), CC_BUILD_SENTINEL));
match common {
Some(common) if has_normal_component(&common) => {
roots.push((common, CC_ROOT_SENTINEL));
}
_ => {
roots.push((source_parent.to_path_buf(), CC_SOURCE_SENTINEL));
}
}
}
}
fn has_normal_component(path: &Path) -> bool {
path.components()
.any(|c| matches!(c, std::path::Component::Normal(_)))
}
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: to.to_string(),
});
}
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 cc_mapped_path(path: &Path, prefix_maps: &[CcPrefixMap]) -> String {
let text = path.to_string_lossy().into_owned().into_bytes();
String::from_utf8_lossy(&apply_cc_prefix_maps_to_bytes(text, prefix_maps)).into_owned()
}
fn cc_mapped_content_hash(path: &Path, prefix_maps: &[CcPrefixMap]) -> Option<String> {
let bytes = std::fs::read(path).ok()?;
let mapped = apply_cc_prefix_maps_to_bytes(bytes, prefix_maps);
Some(blake3::hash(&mapped).to_hex().to_string())
}
fn cc_unmapped_path_candidates(mapped: &str, prefix_maps: &[CcPrefixMap]) -> Vec<PathBuf> {
let mut maps: Vec<&CcPrefixMap> = prefix_maps
.iter()
.filter(|map| !map.from.is_empty() && !map.to.is_empty())
.collect();
maps.sort_by_key(|map| std::cmp::Reverse(map.to.len()));
let mut candidates: Vec<PathBuf> = Vec::new();
for map in maps {
if let Some(rest) = mapped.strip_prefix(map.to.as_str()) {
let candidate = PathBuf::from(format!("{}{rest}", map.from));
if !candidates.contains(&candidate) {
candidates.push(candidate);
}
}
}
if candidates.is_empty() && Path::new(mapped).is_absolute() {
candidates.push(PathBuf::from(mapped));
}
candidates
}
fn apply_cc_prefix_maps_to_bytes(bytes: Vec<u8>, prefix_maps: &[CcPrefixMap]) -> Vec<u8> {
let mut maps: Vec<&CcPrefixMap> = prefix_maps.iter().filter(|m| !m.from.is_empty()).collect();
maps.sort_by_key(|m| std::cmp::Reverse(m.from.len()));
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
let matched = maps.iter().find(|m| {
let from = m.from.as_bytes();
bytes[i..].starts_with(from)
&& (!is_configured_cc_prefix_map(m)
|| configured_cc_prefix_starts_path_token(&bytes, i, from))
});
if let Some(m) = matched {
out.extend_from_slice(m.to.as_bytes());
i += m.from.len();
} else {
out.push(bytes[i]);
i += 1;
}
}
out
}
fn is_configured_cc_prefix_map(map: &CcPrefixMap) -> bool {
map.to
.strip_prefix("/kache/base-dir-")
.is_some_and(|index| !index.is_empty() && index.bytes().all(|byte| byte.is_ascii_digit()))
}
fn configured_cc_prefix_starts_path_token(input: &[u8], start: usize, prefix: &[u8]) -> bool {
if !cc_windows_absolute_prefix(prefix) && cc_follows_windows_drive_prefix(input, start) {
return false;
}
let before = &input[..start];
start == 0
|| before.ends_with(b"-I")
|| before.ends_with(b"-L")
|| before.ends_with(b"-F")
|| before.ends_with(b"-B")
|| before.last().is_some_and(|byte| {
byte.is_ascii_whitespace()
|| matches!(
byte,
b'=' | b':' | b';' | b',' | b'"' | b'\'' | b'(' | b'[' | b'{' | b'@'
)
})
}
fn cc_windows_absolute_prefix(prefix: &[u8]) -> bool {
(prefix.len() >= 3
&& prefix[0].is_ascii_alphabetic()
&& prefix[1] == b':'
&& matches!(prefix[2], b'/' | b'\\'))
|| prefix.starts_with(b"//")
|| prefix.starts_with(b"\\\\")
}
fn cc_follows_windows_drive_prefix(input: &[u8], start: usize) -> bool {
if start < 2 || input[start - 1] != b':' || !input[start - 2].is_ascii_alphabetic() {
return false;
}
let lead = &input[..start - 2];
lead.is_empty()
|| lead.ends_with(b"-I")
|| lead.ends_with(b"-L")
|| lead.ends_with(b"-F")
|| lead.ends_with(b"-B")
|| lead.last().is_some_and(|byte| {
byte.is_ascii_whitespace()
|| matches!(
byte,
b'=' | b':' | b';' | b',' | b'"' | b'\'' | b'(' | b'[' | b'{' | b'@'
)
})
}
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 compose_cc_args(rest: &[String], appended: Vec<String>) -> Vec<String> {
if appended.is_empty() {
return rest.to_vec();
}
match rest.iter().position(|a| a == "--") {
Some(sep) => {
let mut out = Vec::with_capacity(rest.len() + appended.len());
out.extend_from_slice(&rest[..sep]);
out.extend(appended);
out.extend_from_slice(&rest[sep..]);
out
}
None => {
let mut out = rest.to_vec();
out.extend(appended);
out
}
}
}
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())
}
fn digest_cc_include_shadowing(parsed: &CcArgs, read_inputs: &[PathBuf]) -> Result<String> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let dirs = cc_user_include_dirs(parsed, &cwd);
let mut names: Vec<PathBuf> = Vec::new();
for input in read_inputs {
let absolute = absolutize_path(&cwd, input);
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(relative) = dirs
.iter()
.filter_map(|dir| absolute.strip_prefix(dir).ok())
.min_by_key(|relative| relative.components().count())
{
candidates.push(relative.to_path_buf());
}
if let Some(file_name) = absolute.file_name() {
candidates.push(PathBuf::from(file_name));
}
for candidate in candidates {
if !names.contains(&candidate) {
names.push(candidate);
}
}
}
names.sort();
let mut hasher = blake3::Hasher::new();
for name in &names {
hasher.update(name.as_os_str().as_encoded_bytes());
hasher.update(b"\x1f");
match cc_first_include_dir_providing(&dirs, name)? {
Some(index) => {
hasher.update(b"@");
hasher.update(index.to_string().as_bytes());
}
None => {
hasher.update(b"-");
}
}
hasher.update(b"\n");
}
Ok(hasher.finalize().to_hex().to_string())
}
fn cc_first_include_dir_providing(dirs: &[PathBuf], name: &Path) -> Result<Option<usize>> {
for (index, dir) in dirs.iter().enumerate() {
let candidate = dir.join(name);
match std::fs::symlink_metadata(&candidate) {
Ok(_) => return Ok(Some(index)),
Err(error) if error.kind() == ErrorKind::NotFound => {}
Err(error) => anyhow::bail!(
"cc include candidate {} is unreadable ({error})",
candidate.display()
),
}
}
Ok(None)
}
const CC_INCLUDE_DIR_NAME_CAP: usize = 8192;
const CC_INCLUDE_DIR_NAME_EXTENSIONS: &[&str] = &[
"h", "hh", "hpp", "hxx", "h++", "cuh", "c", "cc", "cpp", "cxx", "c++", "m", "mm", "i", "ii",
"inl", "inc", "def", "pch", "gch",
];
fn digest_cc_include_dir_names(parsed: &CcArgs) -> Result<String> {
digest_cc_include_dir_names_capped(parsed, CC_INCLUDE_DIR_NAME_CAP)
}
fn digest_cc_include_dir_names_capped(parsed: &CcArgs, cap: usize) -> Result<String> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let exempt = cc_system_include_dirs(parsed, &cwd);
let mut hasher = blake3::Hasher::new();
hasher.update(b"include_dir_names.v1\n");
let mut seen = 0usize;
for dir in cc_user_include_dirs(parsed, &cwd) {
if exempt.iter().any(|root| dir.starts_with(root)) {
continue;
}
hasher.update(b"dir\n");
collect_include_dir_names(&dir, Path::new(""), &mut hasher, &mut seen, cap)?;
hasher.update(b"enddir\n");
}
Ok(hasher.finalize().to_hex().to_string())
}
fn cc_user_include_dirs(parsed: &CcArgs, cwd: &Path) -> Vec<PathBuf> {
let mut dirs = Vec::new();
let mut seen = HashSet::new();
let mut push = |path: PathBuf| {
let abs = absolutize_path(cwd, &path);
if seen.insert(abs.clone()) {
dirs.push(abs);
}
};
if let Some(source) = parsed.sources.first() {
let parent = source.parent().filter(|p| !p.as_os_str().is_empty());
push(parent.map_or_else(|| cwd.to_path_buf(), Path::to_path_buf));
}
for value in cc_flag_dir_values(&parsed.rest, "-iquote") {
push(PathBuf::from(value));
}
for include in &parsed.includes {
push(include.clone());
}
dirs
}
fn cc_system_include_dirs(parsed: &CcArgs, cwd: &Path) -> Vec<PathBuf> {
let mut dirs = Vec::new();
let mut seen = HashSet::new();
let mut push = |path: PathBuf| {
let abs = absolutize_path(cwd, &path);
if seen.insert(abs.clone()) {
dirs.push(abs);
}
};
for value in cc_flag_dir_values(&parsed.rest, "-isystem") {
push(PathBuf::from(value));
}
for value in cc_flag_dir_values(&parsed.rest, "-isysroot") {
push(PathBuf::from(value));
push(PathBuf::from(value).join("usr/include"));
}
if let Ok(sdk) = std::env::var("SDKROOT") {
let path = PathBuf::from(sdk);
if path.is_dir() {
push(path.clone());
push(path.join("usr/include"));
}
}
dirs
}
fn cc_flag_dir_values<'a>(rest: &'a [String], flag: &'a str) -> Vec<&'a str> {
let mut values = Vec::new();
let mut args = rest.iter();
while let Some(arg) = args.next() {
let Some(suffix) = arg.strip_prefix(flag) else {
continue;
};
if suffix.is_empty() {
if let Some(value) = args.next() {
values.push(value.as_str());
}
} else if let Some(value) = suffix.strip_prefix('=')
&& !value.is_empty()
{
values.push(value);
}
}
values
}
fn collect_include_dir_names(
dir: &Path,
rel: &Path,
hasher: &mut blake3::Hasher,
seen: &mut usize,
cap: usize,
) -> Result<()> {
let entries = match fs::read_dir(dir) {
Ok(entries) => entries,
Err(error) if error.kind() == ErrorKind::NotFound => return Ok(()),
Err(error) => {
anyhow::bail!("cc include dir {} is unreadable ({error})", dir.display())
}
};
let mut files = Vec::new();
let mut subdirs = Vec::new();
for entry in entries {
let entry = entry.with_context(|| format!("reading {}", dir.display()))?;
let file_type = entry
.file_type()
.with_context(|| format!("stat {}", entry.path().display()))?;
let name = entry.file_name();
if file_type.is_symlink() {
if cc_include_name_counts(&name) {
files.push(name);
}
continue;
}
if file_type.is_dir() {
subdirs.push(name);
continue;
}
if cc_include_name_counts(&name) {
files.push(name);
}
}
files.sort();
subdirs.sort();
for name in files {
*seen += 1;
if *seen > cap {
anyhow::bail!("cc include dir name walk exceeded {cap} entries");
}
let path = rel.join(&name);
hasher.update(path.as_os_str().as_encoded_bytes());
hasher.update(b"\n");
}
for name in subdirs {
*seen += 1;
if *seen > cap {
anyhow::bail!("cc include dir name walk exceeded {cap} entries");
}
collect_include_dir_names(&dir.join(&name), &rel.join(&name), hasher, seen, cap)?;
}
Ok(())
}
fn cc_include_name_counts(name: &OsStr) -> bool {
let Some(ext) = Path::new(name).extension() else {
return true;
};
let ext = ext.to_string_lossy();
CC_INCLUDE_DIR_NAME_EXTENSIONS
.iter()
.any(|keep| ext.eq_ignore_ascii_case(keep))
}
struct PendingCcPreprocessMemo {
memo_key: String,
preprocessed_hash: String,
fingerprints: Vec<crate::cache_key::CcPreprocessMemoInput>,
prefix_maps: Vec<CcPrefixMap>,
}
#[derive(Default)]
pub struct CcCompiler {
extra_allowlist_flags: Vec<String>,
base_dirs: Vec<String>,
pending_preprocess_memo: RefCell<Option<PendingCcPreprocessMemo>>,
pending_include_dir_digest: RefCell<Option<(String, Option<Vec<PathBuf>>)>>,
key_path_bound: Cell<bool>,
}
const C_FAMILY_DRIVERS: [(&str, ToolFamily); 7] = [
("clang-cl", ToolFamily::ClangCl),
("clang++", ToolFamily::Clang),
("clang", ToolFamily::Clang),
("gcc", ToolFamily::Gnu),
("g++", ToolFamily::Gnu),
("c++", ToolFamily::Gnu),
("cc", ToolFamily::Gnu),
];
fn is_compiler_version_suffix(suffix: &str) -> bool {
!suffix.is_empty()
&& suffix.split('.').all(|component| {
!component.is_empty() && component.bytes().all(|byte| byte.is_ascii_digit())
})
}
fn strip_compiler_qualifiers(mut name: &str) -> (&str, bool) {
let mut removed_version = false;
let mut removed_mingw_flavor = false;
while let Some((head, suffix)) = name.rsplit_once('-') {
if !removed_version && is_compiler_version_suffix(suffix) {
removed_version = true;
name = head;
} else if !removed_mingw_flavor && matches!(suffix, "posix" | "win32") {
removed_mingw_flavor = true;
name = head;
} else {
break;
}
}
(name, removed_mingw_flavor)
}
fn named_tool_family(name: &str) -> Option<ToolFamily> {
let (base, removed_mingw_flavor) = strip_compiler_qualifiers(name);
C_FAMILY_DRIVERS.iter().find_map(|(driver, family)| {
let exact = base == *driver;
let target_prefixed = base.strip_suffix(driver).is_some_and(|prefix| {
prefix.strip_suffix('-').is_some_and(|target| {
!target.is_empty() && target.bytes().any(|byte| byte.is_ascii_alphanumeric())
})
});
if !exact && !target_prefixed {
return None;
}
if removed_mingw_flavor && *family != ToolFamily::Gnu {
return None;
}
Some(*family)
})
}
fn is_unresolvable_bare_program(program: &str) -> bool {
if program.contains('/') {
return false;
}
if program.contains('\\') {
return false;
}
super::resolve_program_on_path(program).is_none()
}
impl CcCompiler {
#[cfg(test)]
pub fn new() -> Self {
Self::default()
}
pub fn with_extra_allowlist_flags(extra_allowlist_flags: Vec<String>) -> Self {
Self {
extra_allowlist_flags,
base_dirs: Vec::new(),
pending_preprocess_memo: RefCell::new(None),
pending_include_dir_digest: RefCell::new(None),
key_path_bound: Cell::new(false),
}
}
pub fn with_base_dirs(mut self, base_dirs: Vec<String>) -> Self {
self.base_dirs = base_dirs;
self.base_dirs.sort();
self.base_dirs.dedup();
self
}
pub(crate) fn commit_preprocess_memo(&self, file_hasher: &crate::cache_key::FileHasher<'_>) {
let Some(pending) = self.pending_preprocess_memo.borrow_mut().take() else {
return;
};
file_hasher.cc_preprocess_memo_record_if_unchanged(
&pending.memo_key,
&pending.preprocessed_hash,
&pending.fingerprints,
&|path| cc_mapped_content_hash(path, &pending.prefix_maps),
);
}
pub(crate) fn include_dir_names_still_match(&self, parsed: &CcArgs) -> bool {
let pending = self.pending_include_dir_digest.borrow();
let Some((digest, read_inputs)) = pending.as_ref() else {
return false;
};
let now = match read_inputs {
Some(inputs) => digest_cc_include_shadowing(parsed, inputs),
None => digest_cc_include_dir_names(parsed),
};
now.is_ok_and(|now| now == *digest)
}
pub fn recognizes(args: &[String]) -> bool {
if super::is_workspace_wrapper_chain(args) {
return false;
}
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).to_ascii_lowercase();
if named_tool_family(&name).is_some() {
return true;
}
if name == "zigcc" || name.starts_with("zigcc-") {
return true;
}
if super::is_kache_subcommand_or_flag(&name) {
return false;
}
if is_unresolvable_bare_program(arg0) {
return false;
}
if super::is_version_or_info_query(&args[1..]) {
return false;
}
crate::probe::probe_compiler_family(arg0).is_some()
}
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> {
self.pending_preprocess_memo.borrow_mut().take();
self.key_path_bound.set(false);
let mut hasher = blake3::Hasher::new();
let trace_name = cc_trace_name(parsed);
let prefix_maps = cc_prefix_maps(parsed, &self.base_dirs);
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
);
if !self.base_dirs.is_empty() {
hasher.update(b"configured_base_dirs.v1:");
hasher.update(self.base_dirs.len().to_string().as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] configured_base_dirs={}",
trace_name,
self.base_dirs.len()
);
}
let mut prefix_sentinels: Vec<&str> = Vec::new();
for map in &prefix_maps {
if !prefix_sentinels.contains(&map.to.as_str()) {
prefix_sentinels.push(map.to.as_str());
}
}
prefix_sentinels.sort_unstable();
hasher.update(b"expansion_roots:literal-bound.v1\n");
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 per_tu_paths = cc_resolved_per_tu_paths(parsed);
let resolved = crate::probe::probe(
ctx.cache_dir,
&crate::probe::CcProber,
&crate::probe::ProbeRequest {
compiler: &parsed.program,
args: &parsed.rest,
key_args: &config_args,
per_tu_paths: &per_tu_paths,
windows_aware: parsed.family.dialect() != Dialect::Cl,
},
)?;
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(paths) = cl_debug_path_inputs(parsed) {
hasher.update(b"cl_debug_paths:");
for p in &paths {
hasher.update(p.as_bytes());
hasher.update(b"\x1f");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] cl_debug_path={}",
trace_name,
p
);
}
hasher.update(b"\n");
}
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 raw_flags = cc_raw_flags_for_key(parsed, &prefix_maps);
if !raw_flags.is_empty() {
hasher.update(b"cc_raw_flags:");
for flag in raw_flags {
hasher.update(&(flag.len() as u64).to_le_bytes());
hasher.update(&flag);
tracing::trace!(
target: "kache::cache_key",
"[key:{}] cc_raw_flag={}",
trace_name,
String::from_utf8_lossy(&flag)
);
}
hasher.update(b"\n");
}
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");
let depinfo_target: std::borrow::Cow<str> = if let Some(target) = &depinfo.target {
std::borrow::Cow::Borrowed(target.as_str())
} else if let Some(object) = parsed.object_output_path()
&& let Some(name) = object.file_name()
{
std::borrow::Cow::Owned(name.to_string_lossy().into_owned())
} else {
std::borrow::Cow::Borrowed("")
};
hasher.update(b"depinfo_target:");
hasher.update(depinfo_target.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] depinfo=1 include_system={} phony_targets={} missing_generated={} target={}",
trace_name,
depinfo.include_system,
depinfo.phony_targets,
depinfo.missing_generated,
depinfo_target
);
} else {
hasher.update(b"0\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] depinfo=0",
trace_name
);
}
let memo_key = ctx
.file_hasher
.supports_cc_preprocess_memo()
.then(|| cc_preprocess_memo_key(parsed, &prefix_maps, &resolved.version_line))
.flatten();
let (pp_hash, read_inputs) = if let Some((memo_hash, satisfied)) =
memo_key.as_ref().and_then(|key| {
ctx.file_hasher.cc_preprocess_memo_lookup(
key,
|name| cc_unmapped_path_candidates(name, &prefix_maps),
&|path| cc_mapped_content_hash(path, &prefix_maps),
)
}) {
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed_memo=hit",
trace_name
);
(memo_hash, Some(satisfied))
} else {
let preprocessed =
preprocess_hash(parsed, &prefix_maps, ctx.file_hasher, memo_key.is_some())?;
self.key_path_bound.set(preprocessed.path_bound);
let read_inputs = preprocessed.fingerprints.as_ref().map(|inputs| {
inputs
.iter()
.map(|input| PathBuf::from(input.local_path()))
.collect::<Vec<_>>()
});
if let (Some(memo_key), Some(fingerprints)) = (memo_key, preprocessed.fingerprints) {
self.pending_preprocess_memo
.borrow_mut()
.replace(PendingCcPreprocessMemo {
memo_key,
preprocessed_hash: preprocessed.hash.clone(),
fingerprints,
prefix_maps: prefix_maps.clone(),
});
}
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed_memo=miss",
trace_name
);
(preprocessed.hash, read_inputs)
};
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 (include_dir_digest, include_dir_mode) = match &read_inputs {
Some(inputs) => (digest_cc_include_shadowing(parsed, inputs)?, "resolved"),
None => (digest_cc_include_dir_names(parsed)?, "walked"),
};
self.pending_include_dir_digest
.borrow_mut()
.replace((include_dir_digest.clone(), read_inputs));
hasher.update(b"include_dir_names:");
hasher.update(include_dir_digest.as_bytes());
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] include_dir_names={} mode={}",
trace_name,
include_dir_digest,
include_dir_mode
);
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_env_vars(key, ctx.key_env_vars, &trace_name);
let key = crate::cache_key::apply_key_salt(key, ctx.key_salt, &trace_name);
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);
let prefix_maps = cc_prefix_maps(parsed, &self.base_dirs);
let args = compose_cc_args(&parsed.rest, file_prefix_map_args(&prefix_maps));
command.args(&args);
if let Some(epoch) = effective_source_date_epoch() {
command.env("SOURCE_DATE_EPOCH", epoch);
}
let output = command
.output()
.with_context(|| format!("executing {}", parsed.program))?;
let exit_code = output.status.code().unwrap_or(1);
let discovers_outputs = matches!(parsed.mode, CompileMode::Compile)
|| (parsed.mode == CompileMode::Preprocess && parsed.output.is_some());
let artifacts = if exit_code == 0 && discovers_outputs {
discover_cc_output_artifacts(parsed)
} else {
ArtifactSet::empty()
};
let unsafe_to_store =
cc_unsafe_to_store(artifacts.is_empty(), self.key_path_bound.get(), || {
parsed
.object_output_path()
.map(|path| cc_object_embeds_mapped_root(&path, &prefix_maps))
});
let artifacts = match unsafe_to_store {
None => artifacts,
Some(reason) => {
tracing::warn!("cc: {} {reason}; not caching it", cc_trace_name(parsed));
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)
}
}
fn discover_cc_output_artifacts(parsed: &CcArgs) -> ArtifactSet {
fn is_plain_file(path: &std::path::Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|meta| {
meta.file_type().is_file() && regular_output_is_independent(path, &meta)
})
}
let Some(object) = parsed
.object_output_path()
.filter(|path| is_plain_file(path))
else {
return ArtifactSet::empty();
};
let object_name = object
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
let mut outputs = vec![Artifact {
path: object,
kind: classify_by_filename(&object_name),
store_name: object_name,
required: true,
}];
if let Some(depinfo) = parsed
.depinfo_output_path()
.filter(|path| is_plain_file(path))
{
outputs.push(Artifact {
path: depinfo,
store_name: CC_DEPINFO_STORE_NAME.to_string(),
kind: ArtifactKind::DepInfo,
required: true,
});
}
ArtifactSet::new(outputs)
}
#[cfg(test)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|a| a.to_string()).collect()
}
#[test]
fn cc_flags_dep_info_is_gnu_only() {
use crate::compiler::flags::{Dialect, FlagClass};
for flag in ["-MD", "-MMD", "-MT", "-MF", "-MQ", "-MP", "-MG"] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::NoObjectEffect),
"{flag} should be inert dep-info under Gnu"
);
}
for flag in ["-MD", "-MT"] {
assert_eq!(
classify_cc_flag(flag, Dialect::Cl),
Some(FlagClass::CapturedByProbe),
"{flag} should be CapturedByProbe under Cl (CRT selection)"
);
}
for flag in ["-MMD", "-MF", "-MQ", "-MP", "-MG"] {
assert_eq!(
classify_cc_flag(flag, Dialect::Cl),
None,
"{flag} must refuse under Cl (no cl-specific row)"
);
}
assert_eq!(
classify_cc_flag("-DFOO", Dialect::Cl),
Some(FlagClass::PreprocessorCaptured)
);
}
#[test]
fn clang_cl_flag_classification() {
use crate::compiler::flags::{Dialect, FlagClass};
let cl = Dialect::Cl;
for f in [
"-guard:cf,nochecks",
"-Gy",
"-Gw",
"-Oy-",
"-fms-compatibility-version=19.50",
"-MD",
"-MT",
"/MD",
"/O2",
] {
assert_eq!(
classify_cc_flag(f, cl),
Some(FlagClass::CapturedByProbe),
"{f}"
);
}
for f in ["-Fofoo.obj", "/Fofoo.obj", "-Zc:inline"] {
assert_eq!(
classify_cc_flag(f, cl),
Some(FlagClass::NoObjectEffect),
"{f}"
);
}
assert_eq!(
classify_cc_flag("-std:c++20", cl),
Some(FlagClass::ModeledInKey)
);
assert_eq!(
classify_cc_flag("-FIfoo.h", cl),
Some(FlagClass::PreprocessorCaptured)
);
assert_eq!(
classify_cc_flag("-MD", Dialect::Gnu),
Some(FlagClass::NoObjectEffect)
);
}
#[test]
fn parse_records_tool_family() {
let gnu = CcArgs::parse(&s(&["gcc", "-c", "a.c"])).unwrap();
assert_eq!(gnu.family, ToolFamily::Gnu);
let cl = CcArgs::parse(&s(&["clang-cl.exe", "-c", "a.c"])).unwrap();
assert_eq!(cl.family, ToolFamily::ClangCl);
}
#[test]
fn tool_family_detects_clang_cl_and_dialects() {
use crate::compiler::flags::Dialect;
let f = |prog: &str, rest: &[&str]| ToolFamily::detect(prog, &s(rest));
assert_eq!(f("clang-cl", &[]), ToolFamily::ClangCl);
assert_eq!(f("clang-cl.exe", &[]), ToolFamily::ClangCl);
assert_eq!(f(r"C:\VS\bin\clang-cl.EXE", &[]), ToolFamily::ClangCl);
assert_eq!(f("clang", &["--driver-mode=cl"]), ToolFamily::ClangCl);
assert_eq!(f("clang", &[]), ToolFamily::Clang);
assert_eq!(f("clang++-17", &[]), ToolFamily::Clang);
assert_eq!(f("clang-15", &[]), ToolFamily::Clang);
assert_eq!(f("aarch64-linux-gnu-clang", &[]), ToolFamily::Clang);
assert_eq!(
f("armv7a-linux-androideabi21-clang++-18", &[]),
ToolFamily::Clang
);
assert_eq!(f("clang-cl-17", &[]), ToolFamily::ClangCl);
assert_eq!(f("x86_64-w64-mingw32-clang-cl", &[]), ToolFamily::ClangCl);
assert_eq!(f("gcc", &[]), ToolFamily::Gnu);
assert_eq!(f("arm-linux-gnueabihf-gcc", &[]), ToolFamily::Gnu);
assert_eq!(f("/usr/bin/cc", &[]), ToolFamily::Gnu);
assert_eq!(f("g++", &[]), ToolFamily::Gnu);
assert_eq!(ToolFamily::Gnu.dialect(), Dialect::Gnu);
assert_eq!(ToolFamily::Clang.dialect(), Dialect::Gnu);
assert_eq!(ToolFamily::ClangCl.dialect(), Dialect::Cl);
}
#[test]
fn clang_cl_output_and_std_parse() {
use crate::compiler::flags::Dialect;
let p = CcArgs::parse(&s(&[
"clang-cl",
"-c",
"-Fobuild\\foo.obj",
"-std:c++20",
"foo.c",
]))
.unwrap();
assert_eq!(p.family.dialect(), Dialect::Cl);
assert_eq!(p.output.as_ref().unwrap().to_str(), Some("build\\foo.obj"));
assert_eq!(
p.object_output_path().unwrap().to_str(),
Some("build\\foo.obj")
);
assert_eq!(p.std.as_deref(), Some("c++20"));
let q =
CcArgs::parse(&s(&["clang-cl", "-c", "/Fofoo.obj", "/std:c++17", "foo.c"])).unwrap();
assert_eq!(q.output.as_ref().unwrap().to_str(), Some("foo.obj"));
assert_eq!(q.std.as_deref(), Some("c++17"));
}
#[test]
fn parser_skips_gnu_only_rows_under_cl() {
let gnu = CcArgs::parse(&s(&["gcc", "-c", "-MT", "tgt.c", "a.c"])).unwrap();
assert_eq!(gnu.sources.len(), 1, "-MT should consume tgt.c under gnu");
assert_eq!(gnu.sources[0].to_str(), Some("a.c"));
let cl = CcArgs::parse(&s(&["clang-cl", "-c", "-MT", "a.c"])).unwrap();
assert_eq!(cl.sources.len(), 1, "-MT must not consume a.c under cl");
assert_eq!(cl.sources[0].to_str(), Some("a.c"));
}
#[test]
fn config_args_keeps_crt_flags_under_cl_strips_dep_under_gnu() {
let gnu = CcArgs::parse(&s(&["gcc", "-c", "-MT", "tgt", "-DFOO", "a.c"])).unwrap();
assert!(!gnu.config_args().iter().any(|a| a == "-MT" || a == "tgt"));
assert!(gnu.config_args().iter().any(|a| a == "-DFOO"));
let cl = CcArgs::parse(&s(&["clang-cl", "-c", "-MT", "-DFOO", "a.c"])).unwrap();
assert!(cl.config_args().iter().any(|a| a == "-MT"));
let cl_md = CcArgs::parse(&s(&["clang-cl", "-c", "-MD", "-DFOO", "a.c"])).unwrap();
assert!(cl_md.config_args().iter().any(|a| a == "-MD"));
}
#[test]
fn config_args_keeps_separated_param_value_issue_580() {
let four =
CcArgs::parse(&s(&["gcc", "-c", "--param", "ssp-buffer-size=4", "a.c"])).unwrap();
let cfg = four.config_args();
assert!(
cfg.iter().any(|a| a == "--param"),
"--param must stay in the probe-memo key: {cfg:?}"
);
assert!(
cfg.iter().any(|a| a == "ssp-buffer-size=4"),
"--param's value must stay in the probe-memo key: {cfg:?}"
);
let thirty_two =
CcArgs::parse(&s(&["gcc", "-c", "--param", "ssp-buffer-size=32", "a.c"])).unwrap();
assert_ne!(
cfg,
thirty_two.config_args(),
"differing --param values must not share a probe-memo key"
);
let inc = CcArgs::parse(&s(&["gcc", "-c", "--include", "pfx.h", "a.c"])).unwrap();
let inc_cfg = inc.config_args();
assert!(
inc_cfg.iter().any(|a| a == "--include") && inc_cfg.iter().any(|a| a == "pfx.h"),
"--include and its header must stay in the probe-memo key: {inc_cfg:?}"
);
assert_eq!(
inc.sources.len(),
1,
"the forced-include header must not be parsed as a second source: {:?}",
inc.sources
);
}
#[test]
fn config_args_strips_clang_cl_output() {
let p = CcArgs::parse(&s(&["clang-cl", "-c", "-Fofoo.obj", "-guard:cf", "foo.c"])).unwrap();
let cfg = p.config_args();
assert!(
!cfg.iter().any(|a| a.starts_with("-Fo")),
"-Fo must be stripped from probe-memo key: {cfg:?}"
);
assert!(
cfg.iter().any(|a| a == "-guard:cf"),
"codegen flag must stay: {cfg:?}"
);
}
#[test]
fn clang_cl_firefox_style_invocation_is_cacheable() {
let p = CcArgs::parse(&s(&[
"clang-cl",
"-c",
"foo.c",
"-Fofoo.obj",
"-fms-compatibility-version=19.50",
"-guard:cf,nochecks",
"-Gy",
"-Gw",
"-Oy-",
"-Zc:inline",
"-MD",
]))
.unwrap();
let refuse = p.refuse_reasons(&[]);
assert!(
refuse.is_empty(),
"should be cacheable, refused: {:?}",
refuse.iter().map(|r| r.description()).collect::<Vec<_>>()
);
let dbg = CcArgs::parse(&s(&["clang-cl", "-c", "foo.c", "-Fofoo.obj", "-Z7"])).unwrap();
assert!(
dbg.refuse_reasons(&[]).is_empty(),
"-Z7 must be cacheable after #312, got: {:?}",
dbg.refuse_reasons(&[])
.iter()
.map(|r| r.description())
.collect::<Vec<_>>()
);
assert!(
cl_debug_path_inputs(&dbg).is_some(),
"-Z7 must activate the cl_debug_path_inputs key fold"
);
}
#[test]
fn recognizes_canonical_command_names() {
for name in [
"cc",
"c++",
"gcc",
"g++",
"clang",
"clang++",
"clang-cl",
"zigcc",
"/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",
"clang-cl.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_target_prefixed_cross_compilers() {
for name in [
"arm-linux-gnueabihf-gcc",
"aarch64-linux-gnu-g++-13",
"x86_64-w64-mingw32-clang",
"riscv64-unknown-elf-clang++-18.1",
"x86_64-w64-mingw32-clang-cl",
"x86_64-w64-mingw32-gcc-posix",
"x86_64-w64-mingw32-gcc-13-posix",
"x86_64-w64-mingw32-g++-win32",
"x86_64-w64-mingw32-c++-posix",
"x86_64-w64-mingw32-cc-win32",
"/opt/cross/bin/arm-none-eabi-gcc",
r"C:\toolchains\bin\AARCH64-W64-MINGW32-GCC.EXE",
] {
assert!(
CcCompiler::recognizes(&s(&[name])),
"should recognize target-prefixed compiler {name}"
);
}
}
#[test]
fn rejects_companion_tools_and_malformed_versions() {
for name in [
"gcc-ar",
"gcc-nm",
"gcc-ranlib",
"arm-linux-gnueabihf-gcc-ar",
"clang-format",
"clang-tidy",
"clangd",
"ccache",
"gcc-13..1",
"clang-posix",
] {
assert!(
!CcCompiler::recognizes(&s(&[name])),
"should NOT recognize companion tool {name}"
);
}
}
#[test]
fn recognizes_unknown_wrapper_via_probe() {
if cfg!(target_os = "macos") {
return; }
let _lock = crate::config::config_path_lock();
let temp = tempfile::TempDir::new().unwrap();
let compilers = ["cc", "gcc", "clang"];
let source_compiler = compilers.iter().find_map(|&c| {
let path = crate::compiler::resolve_program_on_path(c)?;
if crate::probe::probe_compiler_family(path.to_str()?).is_some() {
Some(path)
} else {
None
}
});
let Some(source_path) = source_compiler else {
return; };
let custom_name = if cfg!(windows) {
"my custom & compiler.cmd"
} else {
"my-custom-compiler"
};
let dest_path = temp.path().join(custom_name);
#[cfg(unix)]
{
std::os::unix::fs::symlink(&source_path, &dest_path).unwrap();
}
#[cfg(windows)]
{
std::fs::write(
&dest_path,
format!("@echo off\r\n\"{}\" %*", source_path.display()),
)
.unwrap();
}
let dest_str = dest_path.to_str().unwrap().to_string();
assert!(CcCompiler::recognizes(std::slice::from_ref(&dest_str)));
let previous_path = std::env::var_os("PATH");
let mut path_entries = vec![temp.path().to_path_buf()];
if let Some(previous) = previous_path.as_deref() {
path_entries.extend(std::env::split_paths(previous));
}
let joined_path = std::env::join_paths(path_entries).unwrap();
unsafe {
std::env::set_var("PATH", joined_path);
}
let recognized_by_bare_name = CcCompiler::recognizes(&s(&[custom_name]));
unsafe {
match previous_path {
Some(previous) => std::env::set_var("PATH", previous),
None => std::env::remove_var("PATH"),
}
}
assert!(recognized_by_bare_name);
let recognized_during_dispatch = {
let prev = std::env::var_os("KACHE_ACTIVE");
unsafe {
std::env::set_var("KACHE_ACTIVE", "1");
}
struct Guard(Option<std::ffi::OsString>);
impl Drop for Guard {
fn drop(&mut self) {
unsafe {
match self.0.as_ref() {
Some(val) => std::env::set_var("KACHE_ACTIVE", val),
None => std::env::remove_var("KACHE_ACTIVE"),
}
}
}
}
let _guard = Guard(prev);
CcCompiler::recognizes(std::slice::from_ref(&dest_str))
};
assert!(
recognized_during_dispatch,
"unknown compiler wrapper must be recognized during wrapper dispatch when KACHE_ACTIVE is set"
);
}
#[test]
fn recognizes_does_not_probe_kache_subcommands() {
assert!(!CcCompiler::recognizes(&s(&["list"])));
assert!(!CcCompiler::recognizes(&s(&["gc"])));
assert!(!CcCompiler::recognizes(&s(&["monitor"])));
assert!(!CcCompiler::recognizes(&s(&["config"])));
}
#[test]
fn recognizes_checks_path_separators_and_path_resolution() {
assert!(!CcCompiler::recognizes(&s(&[
"kache_nonexistent_cc_binary_12345"
])));
let nonexistent_path = if cfg!(windows) {
r"C:\nonexistent\path\to\mycc"
} else {
"/nonexistent/path/to/mycc"
};
assert!(!CcCompiler::recognizes(&s(&[nonexistent_path])));
}
#[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_slash_c_sets_compile_mode_for_cl_only() {
let cl = CcArgs::parse(&s(&["clang-cl", "/c", "a.c", "-Foa.obj"])).unwrap();
assert_eq!(cl.mode, CompileMode::Compile, "cl `/c` must set Compile");
let cl_dash = CcArgs::parse(&s(&["clang-cl", "-c", "a.c"])).unwrap();
assert_eq!(cl_dash.mode, CompileMode::Compile);
let gnu = CcArgs::parse(&s(&["gcc", "/c", "a.c"])).unwrap();
assert_ne!(gnu.mode, CompileMode::Compile, "gnu `/c` is not a flag");
}
#[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_cuda_language_override_in_both_forms() {
for args in [
vec!["cc", "-x", "cuda", "-c", "foo.cpp"],
vec!["cc", "-xcuda", "-c", "foo.cpp"],
] {
let descs = refuse_descriptions(&args);
assert!(
descs.iter().any(|d| d.contains("language override")),
"CUDA language override must refuse, got: {descs:?}"
);
}
}
#[test]
fn accepts_all_allowlisted_language_overrides() {
for language in LANGUAGE_OVERRIDE_ALLOWLIST {
let descs = refuse_descriptions(&["cc", "-x", language, "-c", "foo.cpp"]);
assert!(
descs.is_empty(),
"allowlisted language {language} should remain cacheable, got: {descs:?}"
);
}
}
#[test]
fn refuses_cuda_source_with_dedicated_reason() {
let descs = refuse_descriptions(&["cc", "-c", "foo.cu"]);
assert!(
descs.iter().any(|d| d.contains("CUDA source input")),
"CUDA source must get its dedicated refusal, got: {descs:?}"
);
assert!(
!descs.iter().any(|d| d.contains("no source file")),
"CUDA source must not be misreported as missing, got: {descs:?}"
);
}
#[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 cl_slash_flag_refuses_but_gnu_treats_it_positional() {
let cl = refuse_descriptions(&["clang-cl", "-c", "/unknown", "a.c"]);
assert!(
cl.iter().any(|d| d.contains("unsupported flag")),
"clang-cl /unknown should refuse as an unsupported flag, got: {cl:?}"
);
let gnu = refuse_descriptions(&["gcc", "-c", "/unknown", "a.c"]);
assert!(
!gnu.iter().any(|d| d.contains("unsupported flag")),
"gcc /unknown is an inert positional, not an unsupported flag, got: {gnu:?}"
);
let cl_o2 = refuse_descriptions(&["clang-cl", "-c", "/O2", "a.c"]);
assert!(
!cl_o2.iter().any(|d| d.contains("unsupported flag")),
"clang-cl /O2 is now CapturedByProbe (Layer 2) and must not refuse, got: {cl_o2:?}"
);
}
#[test]
fn clang_cl_debug_is_now_cacheable_and_path_keyed() {
for flag in ["-g2", "/Z7", "-Z7", "/Zi", "/ZI", "/Zd"] {
let p = CcArgs::parse(&s(&["clang-cl", "-c", "a.c", "-Foa.obj", flag])).unwrap();
let descs = p
.refuse_reasons(&[])
.iter()
.map(|r| r.description())
.collect::<Vec<_>>();
assert!(
descs.is_empty(),
"{flag} must be cacheable now, got: {descs:?}"
);
assert!(
cl_debug_path_inputs(&p).is_some(),
"{flag}: cl_debug_path_inputs must recognise a debug compile"
);
assert!(
p.embeds_codeview_debug(),
"{flag}: clang-cl debug objects stay machine-local"
);
}
let gnu = CcArgs::parse(&s(&["gcc", "-c", "a.c", "-g2"])).unwrap();
assert!(
!gnu.embeds_codeview_debug(),
"GCC debug objects may publish to a remote"
);
assert_eq!(
cl_debug_path_inputs(&gnu),
None,
"gnu debug must not fold cl paths"
);
}
#[test]
fn clang_cl_debug_compiles_are_no_longer_refused() {
for f in ["/Z7", "/Zi", "/ZI", "-Z7"] {
let p = CcArgs::parse(&s(&["clang-cl", "-c", "a.c", "-Foa.obj", f])).unwrap();
let reasons = p.refuse_reasons(&[]);
assert!(
reasons.is_empty(),
"{f}: clang-cl debug must be cacheable now, got: {reasons:?}"
);
}
let g = CcArgs::parse(&s(&["clang-cl", "-c", "a.c", "-Foa.obj", "-g"])).unwrap();
assert!(
g.refuse_reasons(&[]).is_empty(),
"-g clang-cl must be cacheable"
);
}
#[test]
fn clang_cl_slash_c_compile_is_not_refused() {
let p = CcArgs::parse(&s(&["clang-cl", "/c", "a.c", "-Foa.obj"])).unwrap();
assert_eq!(p.mode, CompileMode::Compile);
assert!(
p.refuse_reasons(&[]).is_empty(),
"clang-cl /c must not be refused, got: {:?}",
p.refuse_reasons(&[])
);
let d = CcArgs::parse(&s(&["clang-cl", "/c", "a.c", "-Foa.obj", "/Z7"])).unwrap();
assert!(
d.refuse_reasons(&[]).is_empty(),
"clang-cl /c /Z7 must not be refused"
);
}
#[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:?}"
);
for args in [
vec!["cc", "-c", "foo.c", "--include=stdafx.pch"],
vec!["cc", "-c", "foo.c", "--include", "stdafx.gch"],
] {
let descs = refuse_descriptions(&args);
assert!(
descs.iter().any(|d| d.contains("precompiled")),
"expected PCH refuse for {args:?}, got: {descs:?}"
);
}
}
#[test]
fn caches_aws_lc_sys_prefix_symbols_and_jitterentropy_flags_issue_580() {
let prefix_header = "/cargo/registry/src/index.crates.io-1/aws-lc-sys-0.43.0/\
generated-include/openssl/boringssl_prefix_symbols.h";
let joined_include = format!("--include={prefix_header}");
for args in [
vec!["cc", "-c", "bcm.c", "-o", "bcm.o", &joined_include],
vec![
"cc",
"-c",
"bcm.c",
"-o",
"bcm.o",
"--include",
prefix_header,
],
vec![
"cc",
"-c",
"jitterentropy-base.c",
"-o",
"je.o",
"-fwrapv",
"--param",
"ssp-buffer-size=4",
"-O0",
],
vec![
"cc",
"-c",
"jitterentropy-base.c",
"-o",
"je.o",
"-fno-wrapv",
"--param=ssp-buffer-size=4",
"-O0",
],
] {
let descs = refuse_descriptions(&args);
assert!(
descs.is_empty(),
"aws-lc-sys invocation must cache, got: {descs:?} for {args:?}"
);
}
}
#[test]
fn caches_aws_lc_sys_ubsan_strip_path_components_issue_840() {
let flag = "-fsanitize-undefined-strip-path-components=-1";
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::CapturedByProbe),
"{flag} must key through the resolved invocation"
);
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"jitterentropy-base.c",
"-o",
"je.o",
"-fwrapv",
"--param",
"ssp-buffer-size=4",
flag,
"-O0",
]))
.unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"aws-lc-sys 0.44 jitterentropy invocation must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(cc_flags_need_resolved_invocation(&parsed));
}
#[test]
fn ubsan_strip_path_components_other_spellings_still_refuse_issue_840() {
for flag in [
"-fsanitize-undefined-strip-path-components",
"-fsanitize-undefined-strip-path-components=0",
"-fsanitize-undefined-strip-path-components=2",
"-fsanitize-undefined-strip-path-components=-2",
] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
None,
"{flag} is not modeled and must keep refusing"
);
}
}
#[test]
fn caches_trivial_auto_var_init_pattern_issue_849() {
let flag = "-ftrivial-auto-var-init=pattern";
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::CapturedByProbe),
"{flag} must key through the resolved invocation"
);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", flag, "-O0"])).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"Firefox compile with {flag} must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(cc_flags_need_resolved_invocation(&parsed));
}
#[test]
fn trivial_auto_var_init_other_spellings_still_refuse_issue_849() {
for flag in [
"-ftrivial-auto-var-init",
"-ftrivial-auto-var-init=zero",
"-ftrivial-auto-var-init=uninitialized",
"-ftrivial-auto-var-init=patterns",
] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
None,
"{flag} is not modeled and must keep refusing"
);
}
}
#[test]
fn long_include_row_does_not_swallow_neighbouring_options_issue_580() {
for flag in [
"--include-directory=/tmp/inc",
"--include-directory-after=/tmp/inc",
"--include-with-prefix=/tmp/inc",
"--include-barrier",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", flag]);
assert!(
descs.iter().any(|d| d.contains(flag)),
"{flag} must still refuse as unmodeled, 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 &[
"-fsanitize=address",
"-fno-pic",
"-mtune=skylake",
"-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", "-ffast-math",
"-ftrapping-math",
"-fno-trapping-math",
"-funsafe-math-optimizations",
"-freciprocal-math",
"-fno-signed-zeros",
"-ffinite-math-only",
"-fno-finite-math-only",
"-frounding-math",
"-fsignaling-nans",
"-fno-fast-math",
"-fomit-frame-pointer",
"-mavx",
"-mbmi2",
"-mf16c",
"-mssse3",
"-mfma",
"-mavx512f",
"-mavxvnni",
"-mno-sse3",
"-fmerge-all-constants",
"-fno-merge-all-constants",
"-funroll-loops",
"-fno-unroll-loops",
"-fno-stack-protector",
"-fstack-protector",
"-fstack-protector-strong",
"-fno-asynchronous-unwind-tables",
"-fasynchronous-unwind-tables",
"--",
] {
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 bare_fansi_escape_codes_is_inert_issue_424() {
assert_eq!(
classify_cc_flag("-fansi-escape-codes", Dialect::Gnu),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_cc_flag("-fansi-escape-codes", Dialect::Cl),
Some(FlagClass::NoObjectEffect)
);
}
#[test]
fn firefox_windows_remaining_flags_are_cacheable_issue_424() {
let descs = refuse_descriptions(&[
"clang-cl",
"-c",
"-TP",
"-ffp-contract=off",
"-fansi-escape-codes",
"-FoBasePrincipal.obj",
"caps/BasePrincipal.cpp",
]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"issue #424 flags must all classify; got: {descs:?}"
);
}
#[test]
fn codegen_knob_stems_classify_in_both_polarities() {
for stem in &[
"omit-frame-pointer",
"trapping-math",
"semantic-interposition",
"math-errno",
"merge-all-constants",
"strict-aliasing",
"function-sections",
"data-sections",
"unwind-tables",
"asynchronous-unwind-tables",
"unroll-loops",
"fast-math",
"finite-math-only",
] {
for flag in [format!("-f{stem}"), format!("-fno-{stem}")] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", &flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"codegen knob {flag} must classify in both polarities, got: {descs:?}"
);
}
}
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", "-fomit-not-a-knob"]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"an unknown -f flag must still refuse, got: {descs:?}"
);
}
#[test]
fn firefox_omit_frame_pointer_no_longer_refuses() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-O2",
"-fomit-frame-pointer",
"-mavx",
"-mbmi2",
"-mf16c",
]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"the Firefox omit-fp + SIMD combo must no longer refuse, got: {descs:?}"
);
}
#[test]
fn cc_rs_no_omit_leaf_frame_pointer_no_longer_refuses_issue_839() {
let argv = s(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-O0",
"-fno-omit-frame-pointer",
"-mno-omit-leaf-frame-pointer",
]);
let parsed = CcArgs::parse(&argv).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"the cc-rs forced-frame-pointer invocation must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(
cc_flags_need_resolved_invocation(&parsed),
"-mno-omit-leaf-frame-pointer must force the resolved invocation"
);
assert_eq!(
classify_cc_flag("-momit-leaf-frame-pointer", Dialect::Gnu),
None,
"-momit-leaf-frame-pointer is not modeled and must keep refusing"
);
}
#[test]
fn zstd_sys_merge_all_constants_caches_issue_856() {
for flag in ["-fmerge-all-constants", "-fno-merge-all-constants"] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::CapturedByProbe),
"{flag} must key through the resolved invocation"
);
let parsed =
CcArgs::parse(&s(&["cc", "-c", "zstd.c", "-o", "zstd.o", flag, "-O0"])).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"zstd-sys compile with {flag} must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(
cc_flags_need_resolved_invocation(&parsed),
"{flag} must force the resolved invocation"
);
}
assert_eq!(
classify_cc_flag("-fmerge-constants", Dialect::Gnu),
None,
"-fmerge-constants is a different knob and must keep refusing"
);
}
#[test]
fn llvm_bench_trapping_math_combo_no_longer_refuses() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-O2",
"-fno-semantic-interposition",
"-ftrapping-math",
]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"the LLVM -ftrapping-math combo must no longer refuse, 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 cc_compiler_constructor_keeps_extra_allowlist_flags() {
let expected = flags(&["-fsome-exotic-flag"]);
let compiler = CcCompiler::with_extra_allowlist_flags(expected.clone());
assert_eq!(compiler.extra_allowlist_flags, expected);
}
#[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() {
let descs = refuse_descriptions(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-mmacosx-min-version=10.15",
]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"-mmacosx-min-version=10.15 is NOT on the #114 list and must still refuse, got: {descs:?}"
);
for flag in &[
"-fstack-protector",
"-fstack-protector-all",
"-fno-stack-protector",
] {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", flag]);
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} is the stack-protector family and must classify, 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_gdwarf2_issue_838() {
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", "-gdwarf-2"]);
assert!(
descs.is_empty(),
"-gdwarf-2 should be classified (#838), got: {descs:?}"
);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", "-gdwarf-2"])).unwrap();
assert!(
cc_flags_need_resolved_invocation(&parsed),
"-gdwarf-2 is probe-keyed and must force the resolved invocation"
);
}
#[test]
fn classifier_accepts_gfull_issue_857() {
assert_eq!(
classify_cc_flag("-gfull", Dialect::Gnu),
Some(FlagClass::CapturedByProbe),
"-gfull must key through the resolved invocation"
);
let descs = refuse_descriptions(&["cc", "-c", "foo.c", "-o", "foo.o", "-gfull"]);
assert!(
descs.is_empty(),
"-gfull should be classified (#857), got: {descs:?}"
);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", "-gfull"])).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"ring Darwin compile with -gfull must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(
cc_flags_need_resolved_invocation(&parsed),
"-gfull is probe-keyed and must force the resolved invocation"
);
}
#[test]
fn gfull_neighbours_still_refuse_issue_857() {
for flag in ["-gused", "-gfuller", "-gfull-dwarf"] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
None,
"{flag} is not modeled and must keep refusing"
);
}
}
#[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 flag_classification_summary_records_raw_keyed_issue_644() {
let mut summary = FlagClassificationSummary::default();
summary.record(Some(FlagClass::RawKeyed));
assert_eq!(summary.raw_keyed, 1);
}
#[test]
fn outcome_gates_are_raw_keyed() {
for flag in &[
"-Werror",
"-Werror=unused-variable",
"-Wno-error",
"-Wno-error=unused-variable",
"-pedantic-errors",
"-Werror-implicit-function-declaration",
"-Wno-error-implicit-function-declaration",
] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::RawKeyed),
"{flag} is an outcome gate and must be keyed directly"
);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", flag])).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"{flag} should be cacheable: {:?}",
parsed.refuse_reasons(&[])
);
}
let parse = |args: &[&str]| cc_raw_flags_for_key(&CcArgs::parse(&s(args)).unwrap(), &[]);
let plain = parse(&["cc", "-c", "foo.c", "-o", "foo.o"]);
let err = parse(&["cc", "-c", "foo.c", "-o", "foo.o", "-Werror"]);
let no_err = parse(&["cc", "-c", "foo.c", "-o", "foo.o", "-Wno-error=foo"]);
let both = parse(&[
"cc",
"-c",
"foo.c",
"-o",
"foo.o",
"-Werror",
"-Wno-error=foo",
]);
assert_ne!(plain, err, "-Werror must change the keyed flags");
assert_ne!(err, both, "-Wno-error must distinguish from bare -Werror");
assert_ne!(no_err, both);
}
#[test]
fn mabi_is_raw_keyed_and_cacheable_issue_823() {
for flag in &[
"-mabi=lp64d", "-mabi=lp64", "-mabi=ilp32",
"-mabi=sysv", "-mabi=ms",
"-mabi=aapcs-linux", "-mfloat-abi=hard",
"-mfloat-abi=softfp",
"-mfloat-abi=soft",
"-mfpu=vfpv3-d16", "-mfpu=neon",
"-mfpu=vfp",
"-mfpu=crypto-neon-fp-armv8",
"-mthumb",
"-marm",
"-mcmodel=medany", "-mcmodel=large",
] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::RawKeyed),
"{flag} selects the ABI and must be folded into the key verbatim"
);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", flag])).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"{flag} should be cacheable: {:?}",
parsed.refuse_reasons(&[])
);
}
let raw = |abi: &str| {
cc_raw_flags_for_key(
&CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", abi])).unwrap(),
&[],
)
};
let plain = cc_raw_flags_for_key(
&CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"])).unwrap(),
&[],
);
assert_ne!(raw("-mabi=lp64d"), raw("-mabi=lp64"));
assert_ne!(raw("-mabi=sysv"), raw("-mabi=ms"));
assert_ne!(plain, raw("-mabi=lp64d"), "an ABI gate must move the key");
assert_ne!(raw("-mfloat-abi=hard"), raw("-mfloat-abi=soft"));
assert_ne!(raw("-mfpu=neon"), raw("-mfpu=vfpv3-d16"));
assert_ne!(raw("-mthumb"), raw("-marm"));
assert_ne!(raw("-mcmodel=medany"), raw("-mcmodel=medlow"));
let raw2 = |a: &str, b: &str| {
cc_raw_flags_for_key(
&CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o", a, b])).unwrap(),
&[],
)
};
assert_ne!(
raw2("-mthumb", "-marm"),
raw2("-marm", "-mthumb"),
"conflicting ISA-state flags are last-one-wins; order must key"
);
assert_ne!(raw("-mthumb"), raw("-mno-thumb"));
}
#[test]
fn ring_cross_compile_invocation_is_cacheable_issue_823() {
let argv = s(&[
"cc",
"-O0",
"-ffunction-sections",
"-fdata-sections",
"-fPIC",
"-g",
"-gdwarf-4",
"-fno-omit-frame-pointer",
"-march=rv64gc",
"-mabi=lp64d",
"-I",
"/cargo/registry/ring-0.17.14/include",
"-I",
"/cargo/registry/ring-0.17.14/pregenerated",
"-Wall",
"-Wextra",
"-fvisibility=hidden",
"-std=c1x",
"-Wbad-function-cast",
"-Wcast-align",
"-Wcast-qual",
"-Wconversion",
"-Wmissing-field-initializers",
"-Wmissing-include-dirs",
"-Wnested-externs",
"-Wredundant-decls",
"-Wshadow",
"-Wsign-compare",
"-Wsign-conversion",
"-Wstrict-prototypes",
"-Wundef",
"-Wuninitialized",
"-g3",
"-DNDEBUG",
"-o",
"/build/out/25ac62e5b3c53843-curve25519.o",
"-c",
"/cargo/registry/ring-0.17.14/crypto/curve25519/curve25519.c",
]);
let parsed = CcArgs::parse(&argv).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"the #823 invocation must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(cc_flags_need_resolved_invocation(&parsed));
}
#[test]
fn cc_rs_armv7_injected_flags_are_cacheable_issue_823() {
let argv = s(&[
"arm-linux-gnueabihf-gcc",
"-O2",
"-ffunction-sections",
"-fdata-sections",
"-fPIC",
"-march=armv7-a",
"-mthumb",
"-mfpu=vfpv3-d16",
"-mfloat-abi=hard",
"-o",
"/build/out/foo.o",
"-c",
"foo.c",
]);
let parsed = CcArgs::parse(&argv).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"the cc-rs armv7 invocation must cache: {:?}",
parsed.refuse_reasons(&[])
);
assert!(cc_flags_need_resolved_invocation(&parsed));
}
#[test]
fn host_relative_m_knobs_still_refuse_issue_823() {
for flag in &[
"-mtune=native",
"-mtune=skylake",
"-mcpu=native",
"-mfpu=auto",
] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
None,
"{flag} is not modeled and must keep refusing"
);
}
}
#[test]
fn wa_debug_prefix_map_is_raw_keyed_issue_644() {
for flag in &[
"-Wa,--debug-prefix-map=/home/runner/.cargo/registry/src/index.crates.io-hash/aws-lc-sys-0.43.0=",
"-Wa,--debug-prefix-map=/build/aws-lc-sys-0.44.1=/vendor/aws-lc",
] {
assert_eq!(
classify_cc_flag(flag, Dialect::Gnu),
Some(FlagClass::RawKeyed),
"{flag} should be keyed directly"
);
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.S", "-o", "foo.o", flag])).unwrap();
assert!(
parsed.refuse_reasons(&[]).is_empty(),
"{flag} should be cacheable: {:?}",
parsed.refuse_reasons(&[])
);
assert!(
!cc_flags_need_resolved_invocation(&parsed),
"raw-keyed assembler flags must not depend on a cc1 probe"
);
}
}
#[test]
fn wa_debug_prefix_map_normalizes_only_from_issue_644() {
let maps_a = vec![CcPrefixMap {
from: "/work/clone-a".to_string(),
to: CC_ROOT_SENTINEL.to_string(),
}];
let maps_b = vec![CcPrefixMap {
from: "/work/clone-b".to_string(),
to: CC_ROOT_SENTINEL.to_string(),
}];
let parse =
|flag: &str| CcArgs::parse(&s(&["cc", "-c", "foo.S", "-o", "foo.o", flag])).unwrap();
let a = cc_raw_flags_for_key(
&parse("-Wa,--debug-prefix-map=/work/clone-a/vendor/aws-lc="),
&maps_a,
);
let b = cc_raw_flags_for_key(
&parse("-Wa,--debug-prefix-map=/work/clone-b/vendor/aws-lc="),
&maps_b,
);
assert_eq!(a, b, "relocated OLD paths should normalize identically");
assert_eq!(
String::from_utf8(a[0].clone()).unwrap(),
format!("-Wa,--debug-prefix-map={CC_ROOT_SENTINEL}/vendor/aws-lc=")
);
let target_a = cc_raw_flags_for_key(
&parse("-Wa,--debug-prefix-map=/work/clone-a/vendor/aws-lc=/mapped-a"),
&maps_a,
);
let target_b = cc_raw_flags_for_key(
&parse("-Wa,--debug-prefix-map=/work/clone-a/vendor/aws-lc=/mapped-b"),
&maps_a,
);
assert_ne!(
target_a, target_b,
"NEW is object material and must stay keyed"
);
assert!(
String::from_utf8(target_a[0].clone())
.unwrap()
.ends_with("=/mapped-a"),
"NEW must remain verbatim"
);
let ordered = CcArgs::parse(&s(&[
"cc",
"-c",
"foo.S",
"-o",
"foo.o",
"-Wa,--debug-prefix-map=/work/clone-a/vendor/aws-lc=/first",
"-Wa,--debug-prefix-map=/work/clone-a/vendor/aws-lc=/second",
]))
.unwrap();
assert_eq!(
cc_raw_flags_for_key(&ordered, &maps_a),
vec![
format!("-Wa,--debug-prefix-map={CC_ROOT_SENTINEL}/vendor/aws-lc=/first")
.into_bytes(),
format!("-Wa,--debug-prefix-map={CC_ROOT_SENTINEL}/vendor/aws-lc=/second")
.into_bytes(),
],
"raw-keyed flags must preserve argv order"
);
}
#[test]
fn wa_debug_prefix_map_does_not_open_other_assembler_flags_issue_644() {
for flag in &[
"-Wa,--debug-prefix-map",
"-Wa,--debug-prefix-map=/from-only",
"-Wa,--debug-prefix-map-extra=/from=/to",
"-Wa,--debug-prefix-map=/from=/to,--fatal-warnings",
"-Wa,--something-else",
] {
assert_eq!(classify_cc_flag(flag, Dialect::Gnu), None, "{flag}");
let descs = refuse_descriptions(&["cc", "-c", "foo.S", "-o", "foo.o", flag]);
assert!(
descs.iter().any(|d| d.contains("unsupported flag")),
"{flag} must remain unsupported, got: {descs:?}"
);
}
}
#[cfg(unix)]
#[test]
fn wa_debug_prefix_map_changes_cache_key_issue_644() {
use std::fs;
let temp = tempfile::tempdir().unwrap();
let fake_cc =
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mock_cc_constant_output.sh");
let source = temp.path().join("unit.S");
fs::write(&source, "/* fake compiler ignores this */\n").unwrap();
let output = temp.path().join("unit.o");
let compiler = CcCompiler::new();
let parse = |target: &str| {
compiler
.parse(&[
fake_cc.to_string_lossy().into_owned(),
"-c".to_string(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
output.to_string_lossy().into_owned(),
format!("-Wa,--debug-prefix-map=/source={target}"),
])
.unwrap()
};
let cache = temp.path().join("cache");
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,
key_salt: None,
key_env_vars: &[],
extra_inputs_digest: None,
};
let key_a = compiler.cache_key(&parse("/mapped-a"), &ctx).unwrap();
let key_b = compiler.cache_key(&parse("/mapped-b"), &ctx).unwrap();
assert_ne!(key_a, key_b, "different NEW values must not collide");
}
#[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",
"-m64",
"-m32",
"-msse2",
"-msse4.1",
"-msse4.2",
"-mavx2",
"-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",
"-mtune=skylake",
"-mfpmath=sse",
"-mfpu=auto",
] {
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",
"-mtune=skylake",
"-fsanitize=address",
]);
let detail = descs
.iter()
.find(|d| d.contains("unsupported flag"))
.expect("expected an unsupported-flag refuse reason");
assert!(
detail.contains("-mtune=skylake"),
"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, Dialect::Gnu),
Some(expected),
"{flag} should have the expected class"
);
}
}
#[test]
fn cc_arg_spec_for_token_filters_by_dialect() {
assert!(cc_arg_spec_for_token("-MT", Dialect::Gnu).is_some());
assert!(cc_arg_spec_for_token("-MT", Dialect::Cl).is_none());
assert!(cc_arg_spec_for_token("-o", Dialect::Gnu).is_some());
assert!(cc_arg_spec_for_token("-o", Dialect::Cl).is_some());
}
#[test]
fn arg_analysis_exposes_bucket_and_normalized_value_form() {
let language = analyze_cc_arg("-xc++", Dialect::Gnu);
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", Dialect::Gnu);
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", Dialect::Gnu);
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 force_lang_and_file_reproducible_classify_as_probe_captured_issue_411() {
assert_eq!(
classify_cc_flag("-TP", Dialect::Cl),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(
classify_cc_flag("/TP", Dialect::Cl),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(
classify_cc_flag("-TC", Dialect::Cl),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(
classify_cc_flag("-ffile-reproducible", Dialect::Cl),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(
classify_cc_flag("-ffile-reproducible", Dialect::Gnu),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(classify_cc_flag("-TP", Dialect::Gnu), None);
assert_eq!(classify_cc_flag("-TC", Dialect::Gnu), None);
}
#[test]
fn xclang_forwarded_classifier_issue_411() {
let cl = Dialect::Cl;
assert_eq!(
classify_xclang_forwarded("-MP", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("-dependency-file", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("-fansi-escape-codes", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("-MT", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("-MQ", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("-sys-header-deps", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("dom/ipc/Unified_cpp_dom_ipc5.cpp.pp", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_xclang_forwarded("-ffp-contract=off", cl),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(
classify_xclang_forwarded("-ffast-math", cl),
Some(FlagClass::CapturedByProbe)
);
assert_eq!(classify_xclang_forwarded("-mllvm", cl), None);
assert_eq!(classify_xclang_forwarded("-fnot-a-real-flag", cl), None);
}
#[test]
fn firefox_windows_clang_cl_compile_is_cacheable_issue_411() {
let parsed = CcArgs::parse(&s(&[
"clang-cl",
"-c",
"-TP",
"-ffile-reproducible",
"-Xclang",
"-MP",
"-Xclang",
"-dependency-file",
"-Xclang",
"dom/ipc/Unified_cpp_dom_ipc5.cpp.pp",
"-Xclang",
"-MT",
"-Xclang",
"Unified_cpp_dom_ipc5.obj",
"-Xclang",
"-fansi-escape-codes",
"-FoUnified_cpp_dom_ipc5.obj",
"dom/ipc/Unified_cpp_dom_ipc5.cpp",
]))
.unwrap();
assert_eq!(parsed.sources.len(), 1, "exactly one source TU");
let descs: Vec<&str> = parsed
.refuse_reasons(&[])
.iter()
.map(|r| r.description())
.collect();
assert!(
!descs.iter().any(|d| d.contains("unsupported flag")),
"issue #411 flags must all classify; got: {descs:?}"
);
assert!(
cc_flags_need_resolved_invocation(&parsed),
"probe-captured flags must force the resolved invocation"
);
}
#[test]
fn xclang_forwarded_modeled_knob_caches_unmodeled_refuses_issue_428() {
let ok = refuse_descriptions(&[
"clang-cl",
"-c",
"-Xclang",
"-ffp-contract=off",
"-Foa.obj",
"a.cpp",
]);
assert!(
!ok.iter().any(|d| d.contains("unsupported flag")),
"-Xclang -ffp-contract=off must classify (cache), got: {ok:?}"
);
let bad = refuse_descriptions(&[
"clang-cl",
"-c",
"-Xclang",
"-fnot-a-real-codegen-flag",
"-Foa.obj",
"a.cpp",
]);
let detail = bad
.iter()
.find(|d| d.contains("unsupported flag"))
.expect("an UNMODELED -Xclang flag must still refuse");
assert!(
detail.contains("-fnot-a-real-codegen-flag"),
"reason should name the unmodeled forwarded flag: {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));
}
#[cfg(unix)]
#[test]
fn cache_key_refuses_probe_captured_flags_without_resolved_invocation() {
let compiler = CcCompiler::new();
for flag in ["-fno-rtti", "-gdwarf-2", "-gfull"] {
let parsed = compiler
.parse(&s(&["true", "-c", "foo.c", "-o", "foo.o", flag]))
.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,
key_env_vars: &[],
extra_inputs_digest: None,
};
let err = compiler.cache_key(&parsed, &ctx).unwrap_err().to_string();
assert!(
err.contains("resolved invocation unavailable"),
"expected resolved-invocation refusal for {flag}, 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")),
"preprocessor mode message must read as deferral ('— not yet'), 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")),
"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 object_output_path_defaults_to_obj_for_clang_cl() {
let cl = CcArgs::parse(&s(&["clang-cl", "-c", "foo.c"])).unwrap();
assert_eq!(cl.object_output_path().unwrap().to_str(), Some("foo.obj"));
let gnu = CcArgs::parse(&s(&["gcc", "-c", "foo.c"])).unwrap();
assert_eq!(gnu.object_output_path().unwrap().to_str(), Some("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 key_probe_runs_with_the_compile_prefix_maps() {
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", "foo.o"])).unwrap();
let maps = vec![
CcPrefixMap {
from: "/w/src".to_string(),
to: "<CC_SOURCE>".to_string(),
},
CcPrefixMap {
from: "/w".to_string(),
to: CC_ROOT_SENTINEL.to_string(),
},
];
let pp = key_preprocess_args(&parsed, &maps);
let mut expected = build_preprocess_args(&parsed);
expected.extend(file_prefix_map_args(&maps));
assert_eq!(pp, expected);
assert_eq!(
key_preprocess_args(&parsed, &[]),
build_preprocess_args(&parsed)
);
let separated = CcArgs::parse(&s(&["clang", "-c", "-o", "foo.o", "--", "foo.c"])).unwrap();
let pp = key_preprocess_args(&separated, &maps);
let separator = pp.iter().position(|a| a == "--").expect("`--` is kept");
let first_map = pp
.iter()
.position(|a| a.starts_with("-ffile-prefix-map="))
.expect("maps are passed");
assert!(first_map < separator, "{pp:?}");
}
#[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 build_preprocess_args_uses_ep_for_clang_cl() {
use crate::compiler::flags::Dialect;
let cl = CcArgs::parse(&s(&["clang-cl", "-c", "a.c", "-DFOO"])).unwrap();
assert_eq!(cl.family.dialect(), Dialect::Cl);
let args = build_preprocess_args(&cl);
assert_eq!(args.first().map(String::as_str), Some("/EP"));
assert!(!args.iter().any(|a| a == "-E" || a == "-P"));
assert!(args.iter().any(|a| a == "-DFOO"));
assert!(args.iter().any(|a| a == "a.c"));
assert!(!args.iter().any(|a| a == "-c"));
let gnu = CcArgs::parse(&s(&["gcc", "-c", "a.c"])).unwrap();
let g = build_preprocess_args(&gnu);
assert_eq!(&g[..2], &["-E".to_string(), "-P".to_string()]);
}
#[test]
fn preprocess_dep_capture_is_complete_for_both_dialects() {
let dep = Path::new("memo inputs.d");
let gnu = CcArgs::parse(&s(&["gcc", "-c", "a.c"])).unwrap();
let gnu_args = add_preprocess_dep_capture(&gnu, build_preprocess_args(&gnu), dep);
assert!(gnu_args.windows(2).any(|args| args == ["-MD", "-MF"]));
assert!(gnu_args.iter().any(|arg| arg == "memo inputs.d"));
let cl = CcArgs::parse(&s(&["clang-cl", "-c", "a.c"])).unwrap();
let cl_args = add_preprocess_dep_capture(&cl, build_preprocess_args(&cl), dep);
assert!(
cl_args
.windows(2)
.any(|args| args == ["-Xclang", "-dependency-file"])
);
assert!(
cl_args
.windows(2)
.any(|args| args == ["-Xclang", "-sys-header-deps"]),
"clang-cl memo dependency capture must include system headers"
);
}
#[test]
fn preprocess_dependency_parser_handles_make_escapes_and_continuations() {
let cwd = Path::new("work/project");
let raw = concat!(
"__kache_preprocess_memo: src/main.c ab/header.h include/a\\ b.h \\\r\n",
" include/hash\\#tag.h \\\n",
" include/cash$$value.h include/single$value.h C:\\sdk\\header.h\n",
);
let actual = parse_preprocess_dependencies(raw, cwd).unwrap();
let mut expected = vec![
PathBuf::from("C:\\sdk\\header.h"),
cwd.join("ab/header.h"),
cwd.join("include/a b.h"),
cwd.join("include/cash$value.h"),
cwd.join("include/hash#tag.h"),
cwd.join("include/single$value.h"),
cwd.join("src/main.c"),
];
expected.sort();
assert_eq!(actual, expected);
}
#[test]
fn cc_memo_os_bytes_preserves_distinct_values() {
assert_ne!(
cc_memo_os_bytes(OsStr::new("compiler-a")),
cc_memo_os_bytes(OsStr::new("compiler-b"))
);
}
#[test]
fn fold_cc_memo_field_changes_and_separates_hashes() {
let mut first = blake3::Hasher::new();
fold_cc_memo_field(&mut first, b"arg", b"one");
let mut second = blake3::Hasher::new();
fold_cc_memo_field(&mut second, b"arg", b"two");
assert_ne!(first.finalize(), blake3::Hasher::new().finalize());
assert_ne!(first.finalize(), second.finalize());
}
#[test]
fn cc_preprocess_memo_key_is_blake3_digest() {
let compiler = std::env::current_exe()
.unwrap()
.to_string_lossy()
.into_owned();
let parsed =
CcArgs::parse(&[compiler, "-c".to_string(), "memo-source.c".to_string()]).unwrap();
let key = cc_preprocess_memo_key(&parsed, &[], "test compiler version").unwrap();
assert_eq!(key.len(), 64);
assert!(
key.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
);
}
#[test]
fn cc_mapped_content_hash_digests_the_mapped_bytes() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a.h");
let b = dir.path().join("b.h");
std::fs::write(&a, "#define P \"/work/one/out\"\n").unwrap();
std::fs::write(&b, "#define P \"/work/two/out\"\n").unwrap();
let maps = vec![
CcPrefixMap {
from: "/work/one".to_string(),
to: "/kache/root".to_string(),
},
CcPrefixMap {
from: "/work/two".to_string(),
to: "/kache/root".to_string(),
},
];
let hash_a = cc_mapped_content_hash(&a, &maps).unwrap();
let hash_b = cc_mapped_content_hash(&b, &maps).unwrap();
assert_eq!(hash_a.len(), 64, "a blake3 digest, not a placeholder");
assert_eq!(
hash_a, hash_b,
"contents the maps make equal must hash equal"
);
assert_ne!(
hash_a,
crate::cache_key::hash_file(&a).unwrap(),
"mapping must actually change what is hashed"
);
assert_ne!(
hash_a,
cc_mapped_content_hash(&b, &[]).unwrap(),
"without the maps the two contents differ"
);
assert_eq!(
cc_mapped_content_hash(&dir.path().join("absent.h"), &maps),
None,
"an unreadable input cannot be reused"
);
}
#[test]
fn cc_memo_paths_map_out_and_back_across_checkouts() {
let maps = vec![
CcPrefixMap {
from: "/work/clone-a".to_string(),
to: "/kache/root".to_string(),
},
CcPrefixMap {
from: "/work/clone-a/build".to_string(),
to: "/kache/build".to_string(),
},
];
assert_eq!(
cc_mapped_path(Path::new("/work/clone-a/src/a.h"), &maps),
"/kache/root/src/a.h"
);
assert_eq!(
cc_mapped_path(Path::new("/work/clone-a/build/gen.h"), &maps),
"/kache/build/gen.h"
);
let other = vec![
CcPrefixMap {
from: "/work/clone-b".to_string(),
to: "/kache/root".to_string(),
},
CcPrefixMap {
from: "/work/clone-b/build".to_string(),
to: "/kache/build".to_string(),
},
];
assert_eq!(
cc_unmapped_path_candidates("/kache/root/src/a.h", &other),
vec![PathBuf::from("/work/clone-b/src/a.h")],
"a name recorded in one checkout resolves into the other"
);
assert_eq!(
cc_unmapped_path_candidates("/kache/build/gen.h", &other),
vec![PathBuf::from("/work/clone-b/build/gen.h")]
);
let shared = vec![
CcPrefixMap {
from: "/work/one".to_string(),
to: "/kache/root".to_string(),
},
CcPrefixMap {
from: "/elsewhere/two".to_string(),
to: "/kache/root".to_string(),
},
];
assert_eq!(
cc_unmapped_path_candidates("/kache/root/h.h", &shared),
vec![
PathBuf::from("/work/one/h.h"),
PathBuf::from("/elsewhere/two/h.h")
]
);
let system_header = if cfg!(windows) {
r"C:\Program Files\sdk\stdio.h"
} else {
"/usr/include/stdio.h"
};
assert!(Path::new(system_header).is_absolute());
assert_eq!(
cc_unmapped_path_candidates(system_header, &other),
vec![PathBuf::from(system_header)]
);
assert!(cc_unmapped_path_candidates("relative/h.h", &other).is_empty());
let half_empty = vec![
CcPrefixMap {
from: "/work/one".to_string(),
to: String::new(),
},
CcPrefixMap {
from: String::new(),
to: "/kache/root".to_string(),
},
];
assert!(
!cc_unmapped_path_candidates("/kache/root/h.h", &half_empty)
.iter()
.any(|candidate| candidate.starts_with("/work/one")),
"an empty target must not graft its source onto every name"
);
assert!(
cc_unmapped_path_candidates("relative/h.h", &half_empty).is_empty(),
"an empty source must not strip a name down to a relative path"
);
}
#[test]
fn cc_preprocess_memo_key_ignores_only_volatile_environment() {
let _lock = crate::test_support::process_state_test_lock();
let compiler = std::env::current_exe()
.unwrap()
.to_string_lossy()
.into_owned();
let parsed =
CcArgs::parse(&[compiler, "-c".to_string(), "memo-source.c".to_string()]).unwrap();
let key = || cc_preprocess_memo_key(&parsed, &[], "test compiler version").unwrap();
let baseline = key();
for (name, value) in [
("_", "/usr/bin/whatever"),
("OLDPWD", "/somewhere/else"),
("SHLVL", "9"),
("CARGO_MAKEFLAGS", "-j --jobserver-fds=7,9"),
("NUM_JOBS", "13"),
("KACHE_CACHE_DIR", "/tmp/some-other-cache"),
] {
unsafe { std::env::set_var(name, value) };
assert_eq!(
key(),
baseline,
"{name} cannot change an expansion and must not change the memo key"
);
unsafe { std::env::remove_var(name) };
}
unsafe { std::env::set_var("CPATH", "/opt/extra/include") };
let with_cpath = key();
unsafe { std::env::remove_var("CPATH") };
assert_ne!(
with_cpath, baseline,
"an include-path variable changes which headers are found and must be keyed"
);
}
#[test]
fn cc_prefix_maps_empty_for_clang_cl() {
let cwd = std::path::Path::new("/work/proj");
let cl = CcArgs::parse(&s(&["clang-cl", "-c", "/work/proj/a.c"])).unwrap();
assert!(cc_prefix_maps_cfg(&cl, cwd, None, None, &[]).is_empty());
let gnu = CcArgs::parse(&s(&["gcc", "-c", "/work/proj/a.c"])).unwrap();
assert!(!cc_prefix_maps_cfg(&gnu, cwd, None, None, &[]).is_empty());
}
#[test]
fn clang_cl_invocation_injects_no_flags_issue_299() {
let cwd = std::path::Path::new("/work/proj");
let cl = CcArgs::parse(&s(&[
"clang-cl",
"-Werror",
"-ffile-reproducible",
"-c",
"/work/proj/a.c",
"-Foa.obj",
]))
.unwrap();
let maps = cc_prefix_maps_cfg(&cl, cwd, None, None, &[]);
assert!(
maps.is_empty(),
"clang-cl must get no prefix maps (#295/#299)"
);
let composed = compose_cc_args(&cl.rest, file_prefix_map_args(&maps));
assert_eq!(
composed, cl.rest,
"kache must inject nothing into a clang-cl argv, or it poisons \
`-Werror` compiles/probes (#299); got {composed:?}"
);
}
#[test]
fn compose_cc_args_splices_appended_flags_before_double_dash() {
let rest = s(&["-c", "-Fofoo.o", "--", "windows.c"]);
let appended = s(&["-ffile-prefix-map=/a=<CC_ROOT>"]);
let out = compose_cc_args(&rest, appended);
assert_eq!(
out,
s(&[
"-c",
"-Fofoo.o",
"-ffile-prefix-map=/a=<CC_ROOT>",
"--",
"windows.c"
]),
"appended flags must land before `--`, not after"
);
}
#[test]
fn compose_cc_args_appends_at_end_without_double_dash() {
let rest = s(&["-c", "foo.c"]);
let appended = s(&["-ffile-prefix-map=/a=<CC_ROOT>"]);
let out = compose_cc_args(&rest, appended);
assert_eq!(out, s(&["-c", "foo.c", "-ffile-prefix-map=/a=<CC_ROOT>"]));
}
#[test]
fn compose_cc_args_is_identity_when_nothing_appended() {
let rest = s(&["-c", "-Fofoo.o", "--", "windows.c"]);
assert_eq!(compose_cc_args(&rest, Vec::new()), rest);
}
#[test]
fn compose_cc_args_splices_before_the_first_double_dash() {
let rest = s(&["-c", "--", "a.c", "--", "b.c"]);
let out = compose_cc_args(&rest, s(&["-ffile-prefix-map=/a=<CC_ROOT>"]));
assert_eq!(
out,
s(&[
"-c",
"-ffile-prefix-map=/a=<CC_ROOT>",
"--",
"a.c",
"--",
"b.c"
])
);
}
#[test]
fn compose_cc_args_handles_double_dash_as_first_token() {
let rest = s(&["--", "a.c"]);
let out = compose_cc_args(&rest, s(&["-ffile-prefix-map=/a=<CC_ROOT>"]));
assert_eq!(out, s(&["-ffile-prefix-map=/a=<CC_ROOT>", "--", "a.c"]));
}
#[cfg(unix)]
#[test]
fn preprocess_hash_bails_on_empty_stdout() {
let parsed = CcArgs::parse(&s(&["true", "-c", "a.c"])).unwrap();
let err =
preprocess_hash(&parsed, &[], &crate::cache_key::FileHasher::new(), false).unwrap_err();
assert!(err.to_string().contains("no output"), "got: {err}");
}
#[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"
);
}
#[cfg(unix)]
#[test]
fn real_probe_separates_file_macro_from_literal_roots() {
let _lock = crate::test_support::process_state_test_lock();
let probe = |with_literal: bool| {
let tree = tempfile::TempDir::new().unwrap();
let root = tree.path().canonicalize().unwrap();
let src = root.join("src");
std::fs::create_dir_all(&src).unwrap();
let source = src.join("x.c");
std::fs::write(
&source,
"const char *f(void) { return __FILE__; }\n\
#ifdef DATA\nconst char *d(void) { return DATA; }\n#endif\n",
)
.unwrap();
let mut args = vec![
"cc".to_string(),
"-c".to_string(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
root.join("x.o").to_string_lossy().into_owned(),
];
if with_literal {
args.push(format!("-DDATA=\"{}/data\"", root.display()));
}
let parsed = CcArgs::parse(&args).unwrap();
let maps = cc_prefix_maps_for(&parsed, &root);
let hasher = crate::cache_key::FileHasher::persistent(&root.join("idx.sqlite"));
let hashed = preprocess_hash(&parsed, &maps, &hasher, true).unwrap();
(tree, hashed)
};
let (_a, portable_a) = probe(false);
let (_b, portable_b) = probe(false);
assert!(!portable_a.path_bound && !portable_b.path_bound);
assert_eq!(
portable_a.hash, portable_b.hash,
"__FILE__ must stay portable"
);
assert!(
portable_a
.fingerprints
.as_ref()
.is_some_and(|inputs| !inputs.is_empty()),
"dependency capture still works with the maps on the probe"
);
let (_c, literal_a) = probe(true);
let (_d, literal_b) = probe(true);
assert!(literal_a.path_bound && literal_b.path_bound);
assert_ne!(literal_a.hash, literal_b.hash);
assert!(literal_a.fingerprints.is_none(), "never memoized");
}
#[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| Path::new(&m.from) == Path::new("/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_map_targets_are_absolute_distinct_and_carry_no_sentinel() {
let all = [
CC_ROOT_SENTINEL,
CC_BUILD_SENTINEL,
CC_SOURCE_SENTINEL,
CC_BASE_SENTINEL,
CC_SDKROOT_SENTINEL,
];
for c in all {
assert!(c.starts_with('/'), "cc target must be absolute: {c}");
assert!(
!c.contains('<') && !c.contains('>'),
"cc target must not be an angle-bracket sentinel: {c}"
);
}
let uniq: std::collections::HashSet<_> = all.iter().collect();
assert_eq!(uniq.len(), all.len(), "cc targets must be distinct");
#[cfg(target_os = "linux")]
assert_eq!(CC_BUILD_SENTINEL, "/proc/self/cwd");
#[cfg(not(target_os = "linux"))]
assert_eq!(CC_BUILD_SENTINEL, "/kache/cc-build");
}
#[test]
fn apply_cc_prefix_maps_does_not_chain_through_targets() {
let maps = vec![
CcPrefixMap {
from: "/work/build".to_string(),
to: "/proc/self/cwd".to_string(),
},
CcPrefixMap {
from: "/proc/self".to_string(),
to: "/kache/base-dir".to_string(),
},
];
let out = apply_cc_prefix_maps_to_bytes(b"X=/work/build/foo.c".to_vec(), &maps);
assert_eq!(
String::from_utf8_lossy(&out),
"X=/proc/self/cwd/foo.c",
"the /proc/self map must not rewrite the /proc/self/cwd just emitted"
);
}
fn clone_root_maps(clone: &str) -> Vec<CcPrefixMap> {
vec![CcPrefixMap {
from: format!("/Users/me/work/{clone}"),
to: CC_ROOT_SENTINEL.to_string(),
}]
}
#[test]
fn expansion_with_a_literal_root_binds_the_key_to_the_checkout() {
let expansion = |clone: &str| {
format!(r#"const char *d(void) {{ return "/Users/me/work/{clone}/data"; }}"#)
.into_bytes()
};
let a = hash_cc_expansion(expansion("clone-a"), &clone_root_maps("clone-a"));
let b = hash_cc_expansion(expansion("clone-b"), &clone_root_maps("clone-b"));
assert!(a.path_bound && b.path_bound);
assert_ne!(a.hash, b.hash, "another checkout must not share the key");
assert_eq!(
a,
hash_cc_expansion(expansion("clone-a"), &clone_root_maps("clone-a")),
"the same checkout keeps its key"
);
}
#[test]
fn bound_expansion_folds_every_root_not_only_the_one_it_names() {
let maps = |clone: &str| {
let mut maps = clone_root_maps(clone);
maps.push(CcPrefixMap {
from: "/opt/shared".to_string(),
to: "/kache/base-dir-0".to_string(),
});
maps
};
let expansion = br#"const char *s = "/opt/shared/data";"#.to_vec();
let a = hash_cc_expansion(expansion.clone(), &maps("clone-a"));
let b = hash_cc_expansion(expansion, &maps("clone-b"));
assert!(a.path_bound && b.path_bound);
assert_ne!(a.hash, b.hash);
}
#[test]
fn expansion_without_raw_roots_stays_portable() {
let expansion = format!(r#"const char *f = "{CC_ROOT_SENTINEL}/src/x.c";"#).into_bytes();
let a = hash_cc_expansion(expansion.clone(), &clone_root_maps("clone-a"));
let b = hash_cc_expansion(expansion.clone(), &clone_root_maps("clone-b"));
assert!(!a.path_bound);
assert_eq!(a, b);
assert_eq!(a.hash, blake3::hash(&expansion).to_hex().to_string());
}
#[test]
fn store_gate_keeps_raw_roots_out_of_portable_keys() {
let never = || -> Option<std::io::Result<bool>> { panic!("scanned without a need") };
assert_eq!(cc_unsafe_to_store(true, false, never), None);
assert_eq!(cc_unsafe_to_store(false, true, never), None);
let no_object = cc_unsafe_to_store(false, false, || None).unwrap();
assert!(no_object.contains("no object"), "{no_object}");
assert_eq!(cc_unsafe_to_store(false, false, || Some(Ok(false))), None);
let embeds = cc_unsafe_to_store(false, false, || Some(Ok(true))).unwrap();
assert!(embeds.contains("embeds a checkout root"), "{embeds}");
let unreadable =
cc_unsafe_to_store(false, false, || Some(Err(std::io::Error::other("gone")))).unwrap();
assert!(
unreadable.contains("could not be read") && unreadable.contains("gone"),
"{unreadable}"
);
}
#[test]
fn object_root_check_reads_the_object() {
let dir = tempfile::tempdir().unwrap();
let maps = clone_root_maps("clone-a");
let dirty = dir.path().join("dirty.o");
std::fs::write(&dirty, b"\x7fELF\0/Users/me/work/clone-a/data\0").unwrap();
let clean = dir.path().join("clean.o");
std::fs::write(&clean, format!("\x7fELF\0{CC_ROOT_SENTINEL}/data\0")).unwrap();
assert!(cc_object_embeds_mapped_root(&dirty, &maps).unwrap());
assert!(!cc_object_embeds_mapped_root(&clean, &maps).unwrap());
assert!(cc_object_embeds_mapped_root(&dir.path().join("missing.o"), &maps).is_err());
}
#[test]
fn assembler_file_reads_and_macros_are_refused() {
let found = |text: &str| cc_assembler_hidden_input(text.as_bytes());
assert_eq!(found(".incbin \"payload.bin\"\n"), Some(".incbin"));
assert_eq!(found("\t.INCBIN\t\"payload.bin\""), Some(".incbin"));
assert_eq!(
found(r#"__asm__(".incbin \"payload.bin\"\n");"#),
Some(".incbin")
);
assert_eq!(found(".macro blob f\n.incbin \\f\n.endm"), Some(".incbin"));
assert_eq!(found(".include \"macros.s\""), Some(".include"));
assert_eq!(found(".include\"macros.s\""), Some(".include"));
assert_eq!(found("opts.include(\"x\");"), None);
assert_eq!(found("cfg.include = \"x\";"), None);
assert_eq!(found(".includes \"x\""), None);
assert_eq!(found(".incbin_data \"x\""), None);
assert_eq!(found("int x = 1;"), None);
assert_eq!(found("end.incbin"), None, "no operand");
assert_eq!(
found(".macro emit op, file\n.\\op \"\\file\"\n.endm\nemit incbin, p.bin"),
Some(".macro")
);
assert_eq!(
found(".irp op, incbin\n.\\op \"p.bin\"\n.endr"),
Some(".irp")
);
assert_eq!(found("\t.IRPC c, ab\n.endr"), Some(".irpc"));
assert_eq!(found(r#"__asm__(".macro emit\n.endm\n");"#), Some(".macro"));
assert_eq!(found(".macros x"), None);
assert_eq!(found("cfg.macro(x);"), None);
assert_eq!(found(".endm"), None);
assert_eq!(
found(".rept 1\n.inc\\()bin \"p.bin\"\n.endr"),
Some(".rept")
);
assert_eq!(found(".inc\\()bin \"p.bin\""), Some(r"\()"));
assert_eq!(found(".altmacro\n"), Some(".altmacro"));
assert_eq!(found(".altmacro_x"), None);
assert_eq!(
found(r#"__asm__(".inc" "bin \"p.bin\"");"#),
Some(".incbin")
);
assert_eq!(
found(r#"__asm__(".inc\x62in \"p.bin\"");"#),
Some(".incbin")
);
assert_eq!(found("void f() { asm((text())); }"), Some("asm((...))"));
}
#[cfg(unix)]
#[test]
fn real_probe_refuses_a_directive_behind_a_delimited_escape() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::TempDir::new().unwrap();
let source = dir.path().join("delimited.c");
std::fs::write(
&source,
"__asm__(\"\\x{2e}incbin \\\"payload.bin\\\"\\n\");\n",
)
.unwrap();
let parsed =
CcArgs::parse(&s(&["cc", "-c", source.to_str().unwrap(), "-o", "out.o"])).unwrap();
let refused = preprocess_hash(&parsed, &[], &crate::cache_key::FileHasher::new(), false)
.unwrap_err()
.to_string();
assert!(refused.contains(".incbin"), "{refused}");
}
#[test]
fn string_literals_are_read_as_the_compiler_reads_them() {
let text = |src: &str| String::from_utf8(cc_string_literals(src.as_bytes()).text).unwrap();
assert_eq!(text(r#"x(".inc" "bin \"p\"");"#), "\n.incbin \"p\"");
assert_eq!(text(r#"".inc\x62in""#), "\n.incbin");
assert_eq!(text(r#"".inc\142in""#), "\n.incbin");
assert_eq!(text(r#"".incbin""#), "\n.incbin");
assert_eq!(text(r#"".inc\U00000062in""#), "\n.incbin");
assert_eq!(text(r#""a\tb\nc\rd\\e\"""#), "\na\tb\nc d\\e\"");
assert_eq!(text(r#"u8".in" L"cbin""#), "\n.incbin");
assert_eq!(text(r#"R"x(.inc\x62in ")x""#), "\n.inc\\x62in \"");
assert_eq!(text("R\"abc"), "\nabc");
assert_eq!(text(r#"R"0123456789abcdef(x)0123456789abcdef""#), "\nx");
assert_eq!(
text(r#"R"0123456789abcdefg(x)0123456789abcdefg""#),
"\n0123456789abcdefg(x)0123456789abcdefg"
);
assert_eq!(text(r#"R"a b(x)a b""#), "\na b(x)a b");
assert_eq!(text(r#"R"a\b(x)a\b""#), "\na (x)a ");
assert_eq!(text(r#"R"a)(x)a)""#), "\na)(x)a)");
let named = cc_string_literals(br#"__asm__(".inc\N{LATIN SMALL LETTER B}in \"p\"");"#);
assert!(named.named_escape);
assert_eq!(String::from_utf8(named.text).unwrap(), "\n.incin \"p\"");
assert!(!cc_string_literals(br#"s = "\\N{x}";"#).named_escape);
assert!(!cc_string_literals(br#"s = "\N";"#).named_escape);
assert_eq!(
cc_assembler_hidden_input(br#"__asm__(".inc\N{LATIN SMALL LETTER B}in \"p\"");"#),
Some(r"\N{...}")
);
assert_eq!(text("\"cut\nx = \"ok\""), "\ncut\nok");
assert_eq!(text(r#"f(".inc"); g("bin");"#), "\n.inc\nbin");
assert_eq!(text(r#"char q = '"'; int n = 1'000; s = "ok";"#), "\nok");
assert_eq!(text(r#"c = '\''; w = L'a'; s = "ok";"#), "\nok");
assert_eq!(text(r#"".inc\x{62}in""#), "\n.incbin");
assert_eq!(text(r#"".inc\o{142}in""#), "\n.incbin");
assert_eq!(text(r#"".inc\u{62}in""#), "\n.incbin");
assert_eq!(text(r#"".inc\x{62in""#), "\n.incbin", "no closing brace");
assert_eq!(text(r#""\o""#), "\no");
assert_eq!(
cc_assembler_hidden_input(br#"__asm__("\x{2e}incbin \"p\"");"#),
Some(".incbin")
);
assert_eq!(text("\"a\" /* \" */ \"b\""), "\nab");
assert_eq!(text("// \"x\n\"ok\""), "\nok");
assert_eq!(text("x = 1 / 2; s = \"ok\";"), "\nok");
assert_eq!(text("/* \"x"), "");
assert_eq!(text(r#"/*/ "x" */ s = "ok";"#), "\nok");
assert_eq!(text(r#"s = R"ab(xyz)ab" "k";"#), "\nxyzk");
assert_eq!(text(r#"R x(y); s = "ok";"#), "\nok");
assert_eq!(text(r#"x = 1'"'; s = "ok";"#), "\nok");
assert_eq!(text(r#"'\'' "ok""#), "\nok");
assert_eq!(cc_string_literals(br#""\u{e9}""#).text, b"\n\xc3\xa9");
assert_eq!(cc_string_literals(br#""\x{e9}""#).text, b"\n\xe9");
assert_eq!(
cc_assembler_hidden_input(b"/* \" */ asm(\".inc\" \"bin \\\"p\\\"\");"),
Some(".incbin")
);
}
#[test]
fn asm_with_a_computed_string_is_refused() {
let computed = |src: &str| cc_string_literals(src.as_bytes()).computed_asm;
assert!(computed("void f() { asm((s())); }"));
assert!(computed("__asm__ __volatile__ ( text );"));
assert!(!computed(r#"asm volatile goto ("jmp %l0" :::: out);"#));
assert!(!computed(r#"extern int f(void) __asm("_" "f");"#));
assert!(!computed(r#"__asm__(R"(nop)");"#));
assert!(!computed(r#"__asm(u8"nop");"#));
assert!(!computed("int asm = 1; myasm(x);"));
assert!(computed("asm(/* c */ x);"));
assert!(!computed(r#"asm(/* c */ "nop");"#));
assert!(!computed(
r#"asm(/* why */ "nop"); asm volatile // why
("nop");"#
));
}
#[cfg(unix)]
#[test]
fn real_probe_refuses_sources_that_incbin_a_file() {
let _lock = crate::test_support::process_state_test_lock();
let dir = tempfile::TempDir::new().unwrap();
let write = |name: &str, text: &str| {
let path = dir.path().join(name);
std::fs::write(&path, text).unwrap();
path.to_string_lossy().into_owned()
};
std::fs::write(dir.path().join("payload.bin"), "one").unwrap();
let probe = |source: String| {
let parsed = CcArgs::parse(&s(&["cc", "-c", source.as_str(), "-o", "out.o"])).unwrap();
preprocess_hash(&parsed, &[], &crate::cache_key::FileHasher::new(), false)
};
let asm = write(
"blob.S",
".globl payload\npayload:\n.incbin \"payload.bin\"\n",
);
let refused = probe(asm).unwrap_err().to_string();
assert!(refused.contains(".incbin"), "{refused}");
let inline = write("inline.c", "__asm__(\".incbin \\\"payload.bin\\\"\\n\");\n");
let refused = probe(inline).unwrap_err().to_string();
assert!(refused.contains(".incbin"), "{refused}");
let generated = write(
"macro.S",
".macro emit op, file\n.\\op \"\\file\"\n.endm\nemit incbin, payload.bin\n",
);
let refused = probe(generated).unwrap_err().to_string();
assert!(refused.contains(".macro"), "{refused}");
let repeated = write("rept.S", ".rept 1\n.inc\\()bin \"payload.bin\"\n.endr\n");
let refused = probe(repeated).unwrap_err().to_string();
assert!(refused.contains(".rept"), "{refused}");
let split = write(
"split.c",
"__asm__(\".inc\" \"bin \\\"payload.bin\\\"\\n\");\n",
);
let refused = probe(split).unwrap_err().to_string();
assert!(refused.contains(".incbin"), "{refused}");
let escaped = write(
"escaped.c",
"__asm__(\".inc\\x62in \\\"payload.bin\\\"\\n\");\n",
);
let refused = probe(escaped).unwrap_err().to_string();
assert!(refused.contains(".incbin"), "{refused}");
let plain = write("plain.S", ".globl answer\nanswer:\n.byte 42\n");
assert!(probe(plain).is_ok());
}
#[test]
fn object_scan_finds_raw_roots_the_token_mapper_skips() {
let maps = clone_root_maps("clone-a");
assert!(bytes_embed_mapped_root(
b"\0\0/Users/me/work/clone-a/data\0",
&maps
));
let other = format!("\0{CC_ROOT_SENTINEL}/data\0/Users/me/work/clone-b\0");
assert!(!bytes_embed_mapped_root(other.as_bytes(), &maps));
assert!(
!bytes_embed_mapped_root(b"/Users/me/work/clone-", &maps),
"a prefix of the root is not the root"
);
let configured = vec![CcPrefixMap {
from: "/base".to_string(),
to: "/kache/base-dir-0".to_string(),
}];
let object = b"x\0/base/data\0".to_vec();
assert_eq!(
apply_cc_prefix_maps_to_bytes(object.clone(), &configured),
object
);
assert!(bytes_embed_mapped_root(&object, &configured));
let empty = vec![CcPrefixMap {
from: String::new(),
to: CC_ROOT_SENTINEL.to_string(),
}];
assert!(!bytes_embed_mapped_root(b"anything", &empty));
}
#[test]
fn cc_prefix_maps_normalize_preprocessor_bytes() {
let maps = vec![CcPrefixMap {
from: "/Users/me/work/clone-a".to_string(),
to: CC_ROOT_SENTINEL.to_string(),
}];
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(),
format!(r#"assert_fail("{CC_ROOT_SENTINEL}/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.to_string(),
}]
};
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(),
format!(r#"FIREFOX_ICO="{CC_ROOT_SENTINEL}/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")), None, &[]);
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 cc_configured_base_dirs_are_distinct_order_independent_and_longest_first() {
let dir = tempfile::TempDir::new().unwrap();
let parent = dir.path().join("container");
let child = parent.join("work");
std::fs::create_dir_all(&child).unwrap();
let source = child.join("src/foo.c");
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(&source, "int x;\n").unwrap();
let parsed =
CcArgs::parse(&s(&["cc", "-c", source.to_str().unwrap(), "-o", "foo.o"])).unwrap();
let parent_cfg = parent.to_string_lossy().into_owned();
let child_cfg = child.to_string_lossy().into_owned();
let forward = cc_prefix_maps_cfg(
&parsed,
&child,
None,
None,
&[parent_cfg.clone(), child_cfg.clone()],
);
let reverse = cc_prefix_maps_cfg(&parsed, &child, None, None, &[child_cfg, parent_cfg]);
let input = format!("{}/include/generated.h", child.display()).into_bytes();
let normalized_forward = apply_cc_prefix_maps_to_bytes(input.clone(), &forward);
let normalized_reverse = apply_cc_prefix_maps_to_bytes(input, &reverse);
assert_eq!(normalized_forward, normalized_reverse);
assert_eq!(
String::from_utf8(normalized_forward).unwrap(),
format!(
"{}/include/generated.h",
crate::path_normalizer::configured_base_dir_target(1)
)
);
assert!(
forward
.iter()
.any(|map| map.to == crate::path_normalizer::configured_base_dir_target(0))
);
assert!(
forward
.iter()
.any(|map| map.to == crate::path_normalizer::configured_base_dir_target(1))
);
}
#[test]
fn cc_configured_base_dir_matches_compiler_raw_prefix_at_path_tokens() {
let maps = vec![CcPrefixMap {
from: "/work".to_string(),
to: crate::path_normalizer::configured_base_dir_target(0),
}];
let input = b"/work/src /workspace/src /opt/work/src -I/work/include".to_vec();
assert_eq!(
String::from_utf8(apply_cc_prefix_maps_to_bytes(input, &maps)).unwrap(),
"/kache/base-dir-0/src /kache/base-dir-0space/src /opt/work/src -I/kache/base-dir-0/include"
);
}
#[test]
fn cc_configured_windows_root_has_portable_variants() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "C:/Build/Root/src/foo.c", "-o", "foo.o"])).unwrap();
let maps = cc_prefix_maps_cfg(
&parsed,
Path::new("C:/Build/Root"),
None,
None,
&["C:/Build/Root".to_string()],
);
let target = crate::path_normalizer::configured_base_dir_target(0);
for variant in [
"C:/Build/Root",
r"C:\Build\Root",
"c:/Build/Root",
r"c:\Build\Root",
] {
assert!(
maps.iter()
.any(|map| map.from == variant && map.to == target),
"missing configured Windows variant {variant:?}: {maps:?}"
);
}
}
#[test]
fn cc_configured_posix_root_does_not_match_windows_drive_path() {
let maps = crate::path_normalizer::configured_base_dir_prefix_maps(&["/snap".to_string()])
.into_iter()
.map(|(from, to)| CcPrefixMap { from, to })
.collect::<Vec<_>>();
assert!(maps.iter().all(|map| map.from != r"\snap"));
assert_eq!(
apply_cc_prefix_maps_to_bytes(b"C:/snap/pkg /snap/pkg".to_vec(), &maps),
b"C:/snap/pkg /kache/base-dir-0/pkg"
);
}
#[test]
fn cc_prefix_maps_sentinel_set_is_location_independent_for_out_of_tree() {
let sentinels = |cwd: &str, src: &str| -> Vec<String> {
let parsed = CcArgs::parse(&s(&["cc", "-c", src, "-o", "foo.o"])).unwrap();
let mut set: Vec<String> = cc_prefix_maps_cfg(&parsed, Path::new(cwd), None, None, &[])
.iter()
.map(|m| m.to.clone())
.collect();
set.sort_unstable();
set.dedup();
set
};
let deep = sentinels("/home/user/proj/build", "/home/user/proj/src/foo.c");
let shallow = sentinels("/tmp/build", "/tmp/src/foo.c");
assert_eq!(
deep, shallow,
"out-of-tree prefix-map sentinel set must not depend on absolute location"
);
assert!(
deep.iter().any(|value| value == CC_ROOT_SENTINEL)
&& deep.iter().any(|value| value == CC_BUILD_SENTINEL),
"out-of-tree build should fold the build and shared-root sentinels, got {deep:?}"
);
assert!(
!deep.iter().any(|value| value == CC_SOURCE_SENTINEL),
"a usable shared root makes <CC_SOURCE> redundant, got {deep:?}"
);
let rooted = sentinels("/build", "/src/foo.c");
assert!(
!rooted.iter().any(|value| value == CC_ROOT_SENTINEL)
&& rooted.iter().any(|value| value == CC_SOURCE_SENTINEL),
"a bare root must fall back to <CC_SOURCE>, not <CC_ROOT>, got {rooted:?}"
);
}
#[test]
fn cc_prefix_maps_preserve_source_parent_dir_for_out_of_tree() {
let src = "/home/user/proj/src/security/sandbox/chromium/base/location.cc";
let parsed = CcArgs::parse(&s(&["cc", "-c", src, "-o", "location.o"])).unwrap();
let maps = cc_prefix_maps_for(&parsed, Path::new("/home/user/proj/obj/security"));
let got = String::from_utf8(apply_cc_prefix_maps_to_bytes(
src.as_bytes().to_vec(),
&maps,
))
.unwrap();
assert!(
got.ends_with("base/location.cc"),
"source __FILE__ must keep the base/ parent dir, got {got:?} from {maps:?}"
);
assert!(
!got.contains(CC_SOURCE_SENTINEL),
"source path must not collapse to a flat <CC_SOURCE>, got {got:?}"
);
}
#[test]
fn cc_prefix_maps_cfg_maps_explicit_isysroot_to_sdkroot_sentinel() {
let sdk = "/Applications/Xcode_15.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.2.sdk";
let parsed = CcArgs::parse(&s(&[
"cc",
"-isysroot",
sdk,
"-c",
"/work/checkout/src/foo.c",
"-o",
"foo.o",
]))
.unwrap();
let maps = cc_prefix_maps_cfg(&parsed, Path::new("/work/checkout"), None, None, &[]);
assert!(
maps.iter()
.any(|m| m.from == sdk && m.to == CC_SDKROOT_SENTINEL),
"explicit -isysroot must map to <SDKROOT>, got {maps:?}"
);
}
#[test]
fn cc_prefix_maps_cfg_maps_sdkroot_env_to_sentinel() {
let sdk = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk";
let parsed =
CcArgs::parse(&s(&["cc", "-c", "/work/checkout/src/foo.c", "-o", "foo.o"])).unwrap();
let maps = cc_prefix_maps_cfg(
&parsed,
Path::new("/work/checkout"),
None,
Some(Path::new(sdk)),
&[],
);
assert!(
maps.iter()
.any(|m| m.from == sdk && m.to == CC_SDKROOT_SENTINEL),
"SDKROOT env must map to <SDKROOT>, got {maps:?}"
);
}
#[test]
fn cc_prefix_maps_cfg_isysroot_wins_over_sdkroot_env() {
let arg_sdk = "/Applications/Xcode_15.2.app/Contents/Developer/.../MacOSX14.2.sdk";
let env_sdk = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk";
let parsed = CcArgs::parse(&s(&[
"cc",
"-isysroot",
arg_sdk,
"-c",
"/work/checkout/src/foo.c",
"-o",
"foo.o",
]))
.unwrap();
let maps = cc_prefix_maps_cfg(
&parsed,
Path::new("/work/checkout"),
None,
Some(Path::new(env_sdk)),
&[],
);
assert!(
maps.iter().any(|m| m.from == arg_sdk),
"explicit -isysroot must be the SDK that is mapped, got {maps:?}"
);
assert!(
!maps.iter().any(|m| m.from == env_sdk),
"SDKROOT env must be ignored when -isysroot is explicit, got {maps:?}"
);
}
#[test]
fn cc_prefix_maps_cfg_no_sdk_adds_no_sdkroot_map() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "/work/checkout/src/foo.c", "-o", "foo.o"])).unwrap();
let maps = cc_prefix_maps_cfg(&parsed, Path::new("/work/checkout"), None, None, &[]);
assert!(
!maps.iter().any(|m| m.to == CC_SDKROOT_SENTINEL),
"no SDK source means no <SDKROOT> map, got {maps:?}"
);
}
#[test]
fn sdkroot_map_normalizes_resolved_tokens_identically_across_installs() {
let sdk_a = "/Applications/Xcode_15.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX14.2.sdk";
let sdk_b = "/Library/Developer/CommandLineTools/SDKs/MacOSX14.2.sdk";
let cwd = Path::new("/work/checkout");
let parsed_a = CcArgs::parse(&s(&[
"cc",
"-isysroot",
sdk_a,
"-c",
"/work/checkout/src/foo.c",
"-o",
"foo.o",
]))
.unwrap();
let parsed_b = CcArgs::parse(&s(&[
"cc",
"-isysroot",
sdk_b,
"-c",
"/work/checkout/src/foo.c",
"-o",
"foo.o",
]))
.unwrap();
let maps_a = cc_prefix_maps_cfg(&parsed_a, cwd, None, None, &[]);
let maps_b = cc_prefix_maps_cfg(&parsed_b, cwd, None, None, &[]);
let token_a = format!("-internal-isystem{sdk_a}/usr/include").into_bytes();
let token_b = format!("-internal-isystem{sdk_b}/usr/include").into_bytes();
let norm_a = apply_cc_prefix_maps_to_bytes(token_a, &maps_a);
let norm_b = apply_cc_prefix_maps_to_bytes(token_b, &maps_b);
assert_eq!(
norm_a, norm_b,
"same SDK contents at different install paths must normalize to the same key bytes"
);
assert_eq!(
String::from_utf8_lossy(&norm_a),
format!("-internal-isystem{CC_SDKROOT_SENTINEL}/usr/include")
);
}
#[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"
);
}
#[cfg(unix)]
fn execute_retrying_etxtbsy(compiler: &CcCompiler, parsed: &CcArgs) -> Result<CompileResult> {
let mut last = compiler.execute(parsed);
for _ in 0..10 {
let is_etxtbsy = last.as_ref().err().is_some_and(|e| {
e.root_cause()
.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.raw_os_error() == Some(26))
});
if !is_etxtbsy {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
last = compiler.execute(parsed);
}
last
}
#[cfg(unix)]
#[test]
fn preprocess_to_a_file_is_cacheable_and_to_stdout_is_not() {
let to_file = CcArgs::parse(&s(&["cc", "-E", "unit.c", "-o", "unit.i"])).unwrap();
assert_eq!(to_file.mode, CompileMode::Preprocess);
assert!(
to_file.refuse_reasons(&[]).is_empty(),
"a named output is the artifact: {:?}",
to_file.refuse_reasons(&[])
);
assert_eq!(
to_file.object_output_path(),
Some(PathBuf::from("unit.i")),
"the named output is what restore has to write"
);
let to_stdout = CcArgs::parse(&s(&["cc", "-E", "unit.c"])).unwrap();
let reasons = to_stdout.refuse_reasons(&[]);
assert!(
reasons.iter().any(|reason| matches!(
reason,
RefuseReason::Unsupported(message) if message.contains("to stdout")
)),
"stdout has no file to store: {reasons:?}"
);
}
#[cfg(unix)]
#[test]
fn successful_non_compile_execute_never_discovers_cache_artifacts() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("linker.sh");
let source = dir.path().join("foo.c");
let output = dir.path().join("foo.o");
std::fs::write(&source, "int main(void) { return 0; }\n").unwrap();
std::fs::write(&script, "#!/bin/sh\nprintf object > \"$3\"\n").unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&[
script.to_string_lossy().into_owned(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
output.to_string_lossy().into_owned(),
])
.unwrap();
assert_eq!(parsed.mode, CompileMode::Link);
let result =
execute_retrying_etxtbsy(&compiler, &parsed).expect("stand-in linker should run");
assert_eq!(result.exit_code, 0);
assert!(output.exists(), "stand-in linker should create its output");
assert!(
result.artifacts.is_empty(),
"a successful link must remain passthrough-only even when its output resembles an object"
);
}
#[cfg(unix)]
#[test]
fn failed_compile_execute_never_discovers_leftover_artifacts() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let script = dir.path().join("compiler.sh");
let source = dir.path().join("foo.c");
let object = dir.path().join("foo.o");
std::fs::write(&source, "int answer(void) { return 42; }\n").unwrap();
std::fs::write(&script, "#!/bin/sh\nprintf object > \"$4\"\nexit 1\n").unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&[
script.to_string_lossy().into_owned(),
source.to_string_lossy().into_owned(),
"-c".to_string(),
"-o".to_string(),
object.to_string_lossy().into_owned(),
])
.unwrap();
assert_eq!(parsed.mode, CompileMode::Compile);
let result = execute_retrying_etxtbsy(&compiler, &parsed)
.expect("failed-but-spawned compiler should return a result");
assert_ne!(result.exit_code, 0);
assert!(
object.exists(),
"stand-in compiler should leave an object behind before failing"
);
assert!(
result.artifacts.is_empty(),
"failed compiles must never publish artifacts even when the compiler left outputs"
);
}
#[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
);
}
#[test]
fn output_discovery_keeps_arbitrary_mf_paths_semantically_depinfo() {
let dir = tempfile::tempdir().unwrap();
let source = dir.path().join("foo.c");
let object = dir.path().join("foo.o");
std::fs::write(&source, "int answer(void) { return 42; }\n").unwrap();
std::fs::write(&object, b"object").unwrap();
for dep_name in ["foo.d.tmp", "extensionless", "foo.unrelated"] {
let depinfo = dir.path().join(dep_name);
std::fs::write(&depinfo, b"foo.o: foo.c\n").unwrap();
let parsed = CcCompiler::new()
.parse(&[
"cc".to_string(),
"-c".to_string(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
object.to_string_lossy().into_owned(),
"-MMD".to_string(),
"-MF".to_string(),
depinfo.to_string_lossy().into_owned(),
])
.unwrap();
let artifacts = discover_cc_output_artifacts(&parsed);
assert_eq!(artifacts.outputs().len(), 2);
assert_eq!(artifacts.outputs()[0].kind, ArtifactKind::Object);
assert_eq!(artifacts.outputs()[1].path, depinfo);
assert_eq!(artifacts.outputs()[1].kind, ArtifactKind::DepInfo);
assert_eq!(artifacts.outputs()[1].store_name, CC_DEPINFO_STORE_NAME);
assert_eq!(
classify_by_filename(&artifacts.outputs()[1].store_name),
ArtifactKind::DepInfo,
"the semantic store name must survive metadata-only classification"
);
}
}
#[test]
fn compiler_output_paths_covers_every_multi_source_default_output() {
let parsed =
CcArgs::parse(&s(&["cc", "-c", "src/alpha.c", "other/beta.c", "-MMD"])).unwrap();
assert_eq!(
parsed.compiler_output_paths(),
vec![
PathBuf::from("alpha.o"),
PathBuf::from("beta.o"),
PathBuf::from("alpha.d"),
PathBuf::from("beta.d"),
]
);
}
#[test]
fn compiler_output_paths_ignores_object_shape_outside_compile_mode() {
let parsed = CcArgs::parse(&s(&["cc", "-E", "foo.c", "-o", "foo.o"])).unwrap();
assert!(parsed.compiler_output_paths().is_empty());
assert!(!parsed.requires_compiler_output_semantics());
}
#[test]
fn cc_output_safety_allows_existing_writable_private_regular_file() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("plain.o");
std::fs::write(&output, b"ordinary compiler output").unwrap();
let output_str = output.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", &output_str])).unwrap();
assert!(!parsed.requires_compiler_output_semantics());
assert!(!parsed.refuse_reasons(&[]).iter().any(|reason| {
reason
.description()
.contains("requires compiler write semantics")
}));
assert_eq!(
discover_cc_output_artifacts(&parsed).outputs().len(),
1,
"post-compile discovery may ingest an independent regular file"
);
}
#[test]
fn regular_output_writability_distinguishes_readonly_metadata() {
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("permissions.o");
std::fs::write(&output, b"ordinary compiler output").unwrap();
let writable = std::fs::metadata(&output).unwrap();
let original_permissions = writable.permissions();
assert!(regular_output_is_owner_writable(&writable));
let mut readonly_permissions = original_permissions.clone();
readonly_permissions.set_readonly(true);
std::fs::set_permissions(&output, readonly_permissions).unwrap();
let readonly = std::fs::metadata(&output).unwrap();
assert!(!regular_output_is_owner_writable(&readonly));
std::fs::set_permissions(&output, original_permissions).unwrap();
}
#[cfg(unix)]
#[test]
fn cc_output_safety_refuses_readonly_regular_file() {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("readonly.o");
std::fs::write(&output, b"user-owned").unwrap();
std::fs::set_permissions(&output, std::fs::Permissions::from_mode(0o444)).unwrap();
let before = std::fs::metadata(&output).unwrap();
let output_str = output.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", &output_str])).unwrap();
assert!(parsed.requires_compiler_output_semantics());
assert!(parsed.refuse_reasons(&[]).iter().any(|reason| {
reason
.description()
.contains("requires compiler write semantics")
}));
assert_eq!(discover_cc_output_artifacts(&parsed).outputs().len(), 1);
let after = std::fs::metadata(&output).unwrap();
assert_eq!((after.dev(), after.ino()), (before.dev(), before.ino()));
assert_eq!(std::fs::read(&output).unwrap(), b"user-owned");
}
#[cfg(unix)]
#[test]
fn cc_output_safety_refuses_regular_file_without_owner_write() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("group-writable.o");
std::fs::write(&output, b"user-owned").unwrap();
std::fs::set_permissions(&output, std::fs::Permissions::from_mode(0o460)).unwrap();
let output_str = output.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", &output_str])).unwrap();
assert!(parsed.requires_compiler_output_semantics());
assert_eq!(discover_cc_output_artifacts(&parsed).outputs().len(), 1);
assert_eq!(std::fs::read(&output).unwrap(), b"user-owned");
}
#[cfg(unix)]
#[test]
fn cc_output_safety_checks_explicit_depinfo_path() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let object = dir.path().join("foo.o");
let depinfo = dir.path().join("custom.tmp");
std::fs::write(&depinfo, b"user-owned depinfo").unwrap();
std::fs::set_permissions(&depinfo, std::fs::Permissions::from_mode(0o444)).unwrap();
let object_str = object.to_string_lossy().into_owned();
let depinfo_str = depinfo.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
"foo.c",
"-MMD",
"-MF",
&depinfo_str,
"-o",
&object_str,
]))
.unwrap();
assert!(parsed.requires_compiler_output_semantics());
assert!(parsed.refuse_reasons(&[]).iter().any(|reason| {
reason
.description()
.contains("requires compiler write semantics")
}));
assert_eq!(std::fs::read(&depinfo).unwrap(), b"user-owned depinfo");
}
#[cfg(unix)]
#[test]
fn cc_output_safety_refuses_symlinked_object() {
use std::fs;
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("real.o");
let output = dir.path().join("link.o");
fs::write(&target, b"original").unwrap();
symlink(&target, &output).unwrap();
let output_str = output.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", &output_str])).unwrap();
assert!(
parsed.requires_compiler_output_semantics(),
"an existing output symlink must route through the real compiler"
);
assert!(
fs::symlink_metadata(&output)
.unwrap()
.file_type()
.is_symlink(),
"classification must leave the -o symlink in place"
);
assert_eq!(fs::read(&target).unwrap(), b"original");
}
#[cfg(unix)]
#[test]
fn cc_output_safety_refuses_all_hardlinks() {
use std::fs;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("real.o");
let output = dir.path().join("link.o");
fs::write(&target, b"original").unwrap();
fs::hard_link(&target, &output).unwrap();
let output_str = output.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", &output_str])).unwrap();
assert!(parsed.requires_compiler_output_semantics());
assert!(parsed.refuse_reasons(&[]).iter().any(|reason| {
reason
.description()
.contains("requires compiler write semantics")
}));
let target_meta = fs::metadata(&target).unwrap();
let output_meta = fs::metadata(&output).unwrap();
assert_eq!(target_meta.dev(), output_meta.dev());
assert_eq!(target_meta.ino(), output_meta.ino());
assert_eq!(target_meta.nlink(), 2);
assert!(
discover_cc_output_artifacts(&parsed).is_empty(),
"writable hardlinked outputs must not enter the blob store"
);
fs::set_permissions(&output, fs::Permissions::from_mode(0o444)).unwrap();
assert!(
parsed.requires_compiler_output_semantics(),
"read-only hardlinks are user-owned unless proven otherwise"
);
assert!(discover_cc_output_artifacts(&parsed).is_empty());
}
#[cfg(windows)]
#[test]
fn cc_output_safety_refuses_windows_hardlinks() {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("real.obj");
let output = dir.path().join("link.obj");
std::fs::write(&target, b"original").unwrap();
std::fs::hard_link(&target, &output).unwrap();
let output_str = output.to_string_lossy().into_owned();
let output_arg = format!("/Fo{output_str}");
let parsed = CcArgs::parse(&s(&["clang-cl.exe", "/c", "foo.c", &output_arg])).unwrap();
assert_eq!(
parsed.object_output_path().as_deref(),
Some(output.as_path())
);
assert!(parsed.requires_compiler_output_semantics());
assert!(discover_cc_output_artifacts(&parsed).is_empty());
let mut perms = std::fs::metadata(&output).unwrap().permissions();
perms.set_readonly(true);
std::fs::set_permissions(&output, perms).unwrap();
assert!(parsed.requires_compiler_output_semantics());
let mut perms = std::fs::metadata(&output).unwrap().permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
std::fs::set_permissions(&output, perms).unwrap();
}
#[cfg(unix)]
#[test]
fn cc_output_safety_preserves_and_refuses_non_regular_file() {
use std::fs;
use std::os::unix::fs::FileTypeExt;
use std::os::unix::net::UnixListener;
let dir = tempfile::tempdir().unwrap();
let output = dir.path().join("compiler-output.sock");
let _listener = UnixListener::bind(&output).unwrap();
let output_str = output.to_string_lossy().into_owned();
let parsed = CcArgs::parse(&s(&["cc", "-c", "foo.c", "-o", &output_str])).unwrap();
assert!(parsed.requires_compiler_output_semantics());
assert!(parsed.refuse_reasons(&[]).iter().any(|reason| {
reason
.description()
.contains("requires compiler write semantics")
}));
assert!(
fs::symlink_metadata(&output)
.unwrap()
.file_type()
.is_socket(),
"classification must not unlink a non-regular compiler output"
);
assert!(
discover_cc_output_artifacts(&parsed).is_empty(),
"non-regular compiler outputs must not enter the blob store"
);
}
#[test]
fn resolve_source_date_epoch_defaults_to_zero() {
assert_eq!(
resolve_source_date_epoch(None, false).as_deref(),
Some(std::ffi::OsStr::new("0"))
);
}
#[test]
fn resolve_source_date_epoch_honors_build_value_verbatim() {
use std::ffi::OsString;
assert_eq!(
resolve_source_date_epoch(Some(OsString::from("1700000000")), false),
Some(OsString::from("1700000000"))
);
assert_eq!(
resolve_source_date_epoch(Some(OsString::from(" 1700000000 ")), true),
Some(OsString::from(" 1700000000 ")),
"a build value is passed through untrimmed and wins over passthrough"
);
assert_eq!(
resolve_source_date_epoch(Some(OsString::from("")), false),
Some(OsString::from(""))
);
}
#[test]
fn resolve_source_date_epoch_passthrough_disables_default_pin() {
assert_eq!(resolve_source_date_epoch(None, true), None);
}
#[cfg(unix)]
#[test]
fn execute_pins_source_date_epoch_on_real_compile() {
use std::fs;
let dir = tempfile::tempdir().unwrap();
let obj = dir.path().join("stamp.o");
let src = dir.path().join("stamp.c");
fs::write(&src, b"int x;\n").unwrap();
let obj_str = obj.to_string_lossy().into_owned();
let src_str = src.to_string_lossy().into_owned();
let script = format!("printf %s \"${{SOURCE_DATE_EPOCH-UNSET}}\" > '{obj_str}'");
let compiler = CcCompiler::new();
let parsed = compiler
.parse(&[
"sh".to_string(),
"-c".to_string(),
script,
src_str,
"-c".to_string(),
"-o".to_string(),
obj_str.clone(),
])
.unwrap();
let result = compiler.execute(&parsed).expect("execute must not Err");
assert_eq!(result.exit_code, 0);
let baked = fs::read_to_string(&obj).unwrap();
let expected = effective_source_date_epoch()
.map(|v| v.to_string_lossy().into_owned())
.unwrap_or_default();
assert_eq!(
baked, expected,
"real compile must inherit kache's pinned SOURCE_DATE_EPOCH"
);
assert_ne!(
baked, "UNSET",
"SOURCE_DATE_EPOCH must be set on the compile"
);
}
#[test]
fn clang_cl_layer4_flag_classification() {
use crate::compiler::flags::{Dialect, FlagClass};
let cl = Dialect::Cl;
for f in [
"-EHsc",
"-EHs-c-",
"/EHsc",
"-GR-",
"/GR-",
"-GS-",
"/GS",
"-Brepro",
"-utf-8",
"-Zc:wchar_t",
"-Zc:forScope-",
] {
assert_eq!(
classify_cc_flag(f, cl),
Some(FlagClass::CapturedByProbe),
"{f}"
);
}
assert_eq!(
classify_cc_flag("-Zc:inline", cl),
Some(FlagClass::NoObjectEffect)
);
assert_eq!(
classify_cc_flag("-FC", cl),
Some(FlagClass::PreprocessorCaptured)
);
for f in [
"-nologo",
"-wd4800",
"/wd4244",
"-FS",
"-Gm-",
"-external:W0",
] {
assert_eq!(
classify_cc_flag(f, cl),
Some(FlagClass::NoObjectEffect),
"{f}"
);
}
assert_eq!(classify_cc_flag("-bigobj", cl), None);
assert_eq!(classify_cc_flag("-showIncludes", cl), None);
}
#[test]
fn clang_cl_full_firefox_invocation_is_cacheable() {
let p = CcArgs::parse(&s(&[
"clang-cl",
"-c",
"foo.c",
"-Fofoo.obj",
"-std:c++20",
"-fms-compatibility-version=19.50",
"-guard:cf,nochecks",
"-Gy",
"-Gw",
"-Oy-",
"-Zc:inline",
"-Zc:wchar_t",
"-MD",
"-EHs-c-",
"-GR-",
"-GS-",
"-nologo",
"-wd4800",
"-utf-8",
"-FS",
"-external:W0",
"-Brepro",
"-FC",
]))
.unwrap();
let refuse = p.refuse_reasons(&[]);
assert!(
refuse.is_empty(),
"should cache, refused: {:?}",
refuse.iter().map(|r| r.description()).collect::<Vec<_>>()
);
let big = CcArgs::parse(&s(&["clang-cl", "-c", "foo.c", "-Fofoo.obj", "-bigobj"])).unwrap();
assert!(!big.refuse_reasons(&[]).is_empty());
}
#[test]
fn clang_cl_debug_flags_require_the_resolved_probe() {
for f in ["/Z7", "/Zi", "/ZI", "/Zd", "-Z7"] {
let p = CcArgs::parse(&s(&["clang-cl", "-c", "a.c", "-Foa.obj", f])).unwrap();
assert!(
cc_flags_need_resolved_invocation(&p),
"{f}: clang-cl debug must require the -### probe (CapturedByProbe)"
);
}
let nodebug = CcArgs::parse(&s(&["clang-cl", "-c", "a.c", "-Foa.obj"])).unwrap();
assert!(
!cc_flags_need_resolved_invocation(&nodebug),
"plain clang-cl compile without debug flags must not require the probe"
);
}
#[test]
fn cc_resolved_per_tu_paths_includes_full_path_and_basename() {
let p = CcArgs::parse(&s(&["cc", "-c", "src/u00.c", "-o", "build/u00.o", "-O2"])).unwrap();
let set: std::collections::HashSet<String> =
cc_resolved_per_tu_paths(&p).into_iter().collect();
assert!(set.contains("src/u00.c"), "full source path: {set:?}");
assert!(set.contains("u00.c"), "source basename: {set:?}");
assert!(set.contains("build/u00.o"), "full output path: {set:?}");
assert!(set.contains("u00.o"), "output basename: {set:?}");
assert!(!set.contains(""), "must never blank an empty token");
}
#[test]
fn cl_debug_path_inputs_folds_source_output_and_dir() {
let comp = |args: &[&str]| cl_debug_path_inputs(&CcArgs::parse(&s(args)).unwrap());
let foo = comp(&["clang-cl", "-c", "foo.c", "-Fofoo.obj", "/Z7"]);
let bar = comp(&["clang-cl", "-c", "bar.c", "-Fobar.obj", "/Z7"]);
assert!(foo.is_some() && bar.is_some());
assert_ne!(
foo, bar,
"different source/output must change the component (H1/D3)"
);
let a1 = comp(&["clang-cl", "-c", "C:\\d1\\a.c", "-Foa.obj", "/Z7"]);
let a2 = comp(&["clang-cl", "-c", "C:\\d2\\a.c", "-Foa.obj", "/Z7"]);
assert_ne!(
a1, a2,
"absolute source path must change the component (H2)"
);
let p = comp(&["clang-cl", "-c", "a.c", "-Fopp.obj", "/Z7"]);
let q = comp(&["clang-cl", "-c", "a.c", "-Foqq.obj", "/Z7"]);
assert_ne!(p, q, "different -Fo must change the component (D3)");
let explicit = comp(&[
"clang-cl",
"-c",
"a.c",
"-Foa.obj",
"/Z7",
"-fdebug-compilation-dir=C:\\proj\\x",
])
.unwrap();
assert!(
explicit.iter().any(|e| e.contains("C:\\proj\\x")),
"explicit compilation-dir must appear in the component"
);
for f in ["/Zi", "/ZI", "-Zi"] {
assert!(
comp(&["clang-cl", "-c", "a.c", "-Foa.obj", f]).is_some(),
"{f} must fold"
);
}
assert_eq!(comp(&["clang-cl", "-c", "a.c", "-Foa.obj"]), None);
assert_eq!(comp(&["gcc", "-c", "a.c", "-g"]), None);
}
#[cfg(unix)]
fn include_dir_test_compiler(dir: &Path) -> (CcCompiler, PathBuf, PathBuf) {
use std::fs;
let fake_cc =
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/mock_cc_static.sh");
let source = dir.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
(CcCompiler::new(), fake_cc, source)
}
#[cfg(unix)]
fn include_dir_key_ctx(dir: &Path) -> (crate::cache_key::FileHasher<'static>, PathBuf) {
(crate::cache_key::FileHasher::new(), dir.join("cache"))
}
#[test]
fn include_shadowing_notices_a_header_appearing_ahead_of_the_one_read() {
let temp = tempfile::tempdir().unwrap();
let first = temp.path().join("first");
let second = temp.path().join("second");
fs::create_dir(&first).unwrap();
fs::create_dir(&second).unwrap();
let read = second.join("header.h");
fs::write(&read, "#define A 1\n").unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "#include \"header.h\"\nint x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
first.to_str().unwrap(),
"-I",
second.to_str().unwrap(),
]))
.unwrap();
let inputs = vec![source.clone(), read.clone()];
let before = digest_cc_include_shadowing(&parsed, &inputs).unwrap();
fs::write(first.join("header.h"), "#define A 2\n").unwrap();
let after = digest_cc_include_shadowing(&parsed, &inputs).unwrap();
assert_ne!(
before, after,
"a header ahead of the one that was read must change the key"
);
fs::remove_file(first.join("header.h")).unwrap();
assert_eq!(
digest_cc_include_shadowing(&parsed, &inputs).unwrap(),
before
);
}
#[test]
fn include_shadowing_ignores_a_name_that_was_never_read() {
let temp = tempfile::tempdir().unwrap();
let include = temp.path().join("inc");
fs::create_dir(&include).unwrap();
let read = include.join("header.h");
fs::write(&read, "int h;\n").unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "#include \"header.h\"\nint x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
let inputs = vec![source.clone(), read.clone()];
let before = digest_cc_include_shadowing(&parsed, &inputs).unwrap();
for name in ["unrelated.h", "other.hpp", "header.o", "notes.txt"] {
fs::write(include.join(name), "x").unwrap();
}
assert_eq!(
digest_cc_include_shadowing(&parsed, &inputs).unwrap(),
before,
"names no unit read cannot shadow and must not churn the key"
);
let big = temp.path().join("big");
fs::create_dir(&big).unwrap();
for i in 0..12_000 {
fs::write(big.join(format!("h{i}.h")), "x").unwrap();
}
let wide = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
"-I",
big.to_str().unwrap(),
]))
.unwrap();
assert!(
digest_cc_include_shadowing(&wide, &inputs).is_ok(),
"a large include tree must resolve rather than refuse"
);
assert!(
digest_cc_include_dir_names(&wide).is_err(),
"the walk this replaces would have refused the same tree"
);
}
#[test]
fn include_shadowing_covers_headers_read_from_outside_the_user_dirs() {
let temp = tempfile::tempdir().unwrap();
let user = temp.path().join("user");
let elsewhere = temp.path().join("elsewhere");
fs::create_dir(&user).unwrap();
fs::create_dir(&elsewhere).unwrap();
let read = elsewhere.join("stdio.h");
fs::write(&read, "int puts(const char*);\n").unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
user.to_str().unwrap(),
]))
.unwrap();
let inputs = vec![source.clone(), read.clone()];
let before = digest_cc_include_shadowing(&parsed, &inputs).unwrap();
fs::write(user.join("stdio.h"), "int puts(const char*);\n").unwrap();
assert_ne!(
digest_cc_include_shadowing(&parsed, &inputs).unwrap(),
before,
"a user dir gaining the name of a system header must change the key"
);
}
#[test]
fn include_dir_digest_changes_when_an_earlier_dir_gains_a_header() {
let temp = tempfile::tempdir().unwrap();
let first = temp.path().join("first");
let second = temp.path().join("second");
fs::create_dir(&first).unwrap();
fs::create_dir(&second).unwrap();
fs::write(second.join("header.h"), "#define A 1\n").unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "#include \"header.h\"\nint x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
first.to_str().unwrap(),
"-I",
second.to_str().unwrap(),
]))
.unwrap();
let before = digest_cc_include_dir_names(&parsed).unwrap();
fs::write(first.join("header.h"), "#define A 2\n").unwrap();
let after = digest_cc_include_dir_names(&parsed).unwrap();
assert_ne!(
before, after,
"a header appearing in an earlier -I dir must change the name digest"
);
}
#[test]
fn include_dir_digest_ignores_object_and_dep_files() {
let temp = tempfile::tempdir().unwrap();
let include = temp.path().join("inc");
fs::create_dir(&include).unwrap();
fs::write(include.join("header.h"), "int h;\n").unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
let before = digest_cc_include_dir_names(&parsed).unwrap();
fs::write(include.join("header.o"), "obj").unwrap();
fs::write(include.join("header.obj"), "obj").unwrap();
fs::write(include.join("header.d"), "deps").unwrap();
fs::write(include.join("header.pp"), "deps").unwrap();
fs::write(include.join("libfoo.a"), "ar").unwrap();
let after = digest_cc_include_dir_names(&parsed).unwrap();
assert_eq!(
before, after,
"sibling compile products must not churn the key"
);
}
#[test]
fn include_dir_digest_overflow_is_fail_closed() {
let temp = tempfile::tempdir().unwrap();
let include = temp.path().join("inc");
fs::create_dir(&include).unwrap();
for i in 0..3 {
fs::write(include.join(format!("h{i}.h")), "int h;\n").unwrap();
}
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
let err = digest_cc_include_dir_names_capped(&parsed, 2).unwrap_err();
assert!(
err.to_string().contains("exceeded 2"),
"overflow must fail closed, got {err}"
);
}
#[test]
fn include_dir_digest_tracks_iquote_dirs() {
let temp = tempfile::tempdir().unwrap();
let quote = temp.path().join("quote");
fs::create_dir("e).unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-iquote",
quote.to_str().unwrap(),
]))
.unwrap();
let before = digest_cc_include_dir_names(&parsed).unwrap();
fs::write(quote.join("local.h"), "int q;\n").unwrap();
let after = digest_cc_include_dir_names(&parsed).unwrap();
assert_ne!(
before, after,
"-iquote dirs must participate in the name digest"
);
}
#[test]
fn cc_flag_dir_values_reads_separated_and_equals_forms() {
let rest = [
"-iquote".to_string(),
"/q".to_string(),
"-isystem=/sys".to_string(),
"-isysroot".to_string(),
"/sdk".to_string(),
"-isystem=".to_string(),
];
assert_eq!(cc_flag_dir_values(&rest, "-iquote"), ["/q"]);
assert_eq!(cc_flag_dir_values(&rest, "-isystem"), ["/sys"]);
assert_eq!(cc_flag_dir_values(&rest, "-isysroot"), ["/sdk"]);
assert!(cc_flag_dir_values(&rest, "-idirafter").is_empty());
}
#[test]
fn include_dir_digest_isystem_on_source_dir_is_exempt() {
let temp = tempfile::tempdir().unwrap();
let srcdir = temp.path().join("src");
fs::create_dir(&srcdir).unwrap();
let source = srcdir.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-isystem",
srcdir.to_str().unwrap(),
]))
.unwrap();
let before = digest_cc_include_dir_names(&parsed).unwrap();
fs::write(srcdir.join("next_to_source.h"), "int n;\n").unwrap();
let after = digest_cc_include_dir_names(&parsed).unwrap();
assert_eq!(
before, after,
"-isystem on the source directory must exempt names next to the source"
);
}
#[test]
fn include_dir_digest_missing_dir_is_empty_not_an_error() {
let temp = tempfile::tempdir().unwrap();
let missing = temp.path().join("no-such-inc");
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
missing.to_str().unwrap(),
]))
.unwrap();
digest_cc_include_dir_names(&parsed)
.expect("ENOENT include dir must hash as empty, not fail closed");
}
#[test]
fn include_dir_digest_accepts_a_walk_that_hits_the_cap_exactly() {
let temp = tempfile::tempdir().unwrap();
let include = temp.path().join("inc");
fs::create_dir(&include).unwrap();
for i in 0..2 {
fs::write(include.join(format!("h{i}.h")), "int h;\n").unwrap();
}
let source = include.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
digest_cc_include_dir_names_capped(&parsed, 3)
.expect("a walk of exactly `cap` names must succeed");
}
#[test]
fn include_dir_digest_counts_nested_directories_toward_the_cap() {
let temp = tempfile::tempdir().unwrap();
let srcdir = temp.path().join("src");
let include = temp.path().join("inc");
let nested = include.join("nested");
fs::create_dir_all(&srcdir).unwrap();
fs::create_dir_all(&nested).unwrap();
fs::write(nested.join("h.h"), "int h;\n").unwrap();
let source = srcdir.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
let err = digest_cc_include_dir_names_capped(&parsed, 2).unwrap_err();
assert!(
err.to_string().contains("exceeded 2"),
"the nested directory itself must count, got {err}"
);
}
#[test]
fn include_dir_digest_counts_an_empty_subdir_at_exact_cap() {
let temp = tempfile::tempdir().unwrap();
let include = temp.path().join("inc");
fs::create_dir_all(include.join("empty")).unwrap();
fs::write(include.join("h.h"), "int h;\n").unwrap();
let source = include.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
digest_cc_include_dir_names_capped(&parsed, 3)
.expect("file + empty subdir at exact cap must succeed");
}
#[test]
fn include_dir_digest_ignores_empty_sdkroot() {
let _lock = crate::test_support::process_state_test_lock();
let temp = tempfile::tempdir().unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&["cc", "-c", source.to_str().unwrap()])).unwrap();
unsafe { std::env::remove_var("SDKROOT") };
let unset = digest_cc_include_dir_names(&parsed).unwrap();
unsafe { std::env::set_var("SDKROOT", "") };
let empty = digest_cc_include_dir_names(&parsed).unwrap();
unsafe { std::env::remove_var("SDKROOT") };
assert_eq!(
unset, empty,
"empty SDKROOT must not become an include root"
);
}
#[test]
fn include_dir_digest_exempts_an_existing_sdkroot_directory() {
let _lock = crate::test_support::process_state_test_lock();
let temp = tempfile::tempdir().unwrap();
let sdk = temp.path().join("sdk");
let srcdir = temp.path().join("src");
fs::create_dir(&sdk).unwrap();
fs::create_dir(&srcdir).unwrap();
let source = srcdir.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
unsafe { std::env::set_var("SDKROOT", &sdk) };
let parsed = CcArgs::parse(&s(&["cc", "-c", source.to_str().unwrap()])).unwrap();
let before = digest_cc_include_dir_names(&parsed).unwrap();
fs::write(sdk.join("sdk.h"), "int s;\n").unwrap();
let after = digest_cc_include_dir_names(&parsed).unwrap();
unsafe { std::env::remove_var("SDKROOT") };
assert_eq!(
before, after,
"headers under SDKROOT must not change the user include-dir digest"
);
}
#[test]
fn include_dir_names_still_match_is_false_without_a_snapshot() {
let temp = tempfile::tempdir().unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&["cc", "-c", source.to_str().unwrap()])).unwrap();
assert!(
!CcCompiler::new().include_dir_names_still_match(&parsed),
"no key snapshot means the names must not be treated as matching"
);
}
#[test]
fn include_dir_digest_skips_isystem_roots() {
let temp = tempfile::tempdir().unwrap();
let system = temp.path().join("sys");
let user = temp.path().join("inc");
let srcdir = temp.path().join("src");
fs::create_dir(&system).unwrap();
fs::create_dir(&user).unwrap();
fs::create_dir(&srcdir).unwrap();
fs::write(user.join("user.h"), "int u;\n").unwrap();
let source = srcdir.join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
user.to_str().unwrap(),
"-isystem",
system.to_str().unwrap(),
]))
.unwrap();
let before = digest_cc_include_dir_names(&parsed).unwrap();
fs::write(system.join("shadow.h"), "int s;\n").unwrap();
let after = digest_cc_include_dir_names(&parsed).unwrap();
assert_eq!(before, after, "-isystem roots must stay exempt");
}
#[cfg(unix)]
#[test]
fn include_dir_resolution_separates_absent_from_unreadable() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
let first = temp.path().join("first");
let second = temp.path().join("second");
fs::create_dir(&first).unwrap();
fs::create_dir(&second).unwrap();
fs::write(second.join("shadow.h"), "int s;\n").unwrap();
let dirs = vec![first.clone(), second.clone()];
let name = Path::new("shadow.h");
assert_eq!(
cc_first_include_dir_providing(&dirs, name).unwrap(),
Some(1),
"an empty first dir is genuinely absent, so the search goes on"
);
assert_eq!(
cc_first_include_dir_providing(&dirs, Path::new("nowhere.h")).unwrap(),
None,
"no dir providing the name is an answer in itself"
);
fs::set_permissions(&first, fs::Permissions::from_mode(0o000)).unwrap();
let blocked = cc_first_include_dir_providing(&dirs, name);
fs::set_permissions(&first, fs::Permissions::from_mode(0o755)).unwrap();
let err = blocked.expect_err("an unreadable include dir must fail closed");
assert!(err.to_string().contains("unreadable"), "got {err}");
}
#[cfg(unix)]
#[test]
fn include_dir_digest_unreadable_dir_is_fail_closed() {
use std::os::unix::fs::PermissionsExt;
let temp = tempfile::tempdir().unwrap();
let include = temp.path().join("inc");
fs::create_dir(&include).unwrap();
fs::set_permissions(&include, fs::Permissions::from_mode(0o000)).unwrap();
let source = temp.path().join("unit.c");
fs::write(&source, "int x;\n").unwrap();
let parsed = CcArgs::parse(&s(&[
"cc",
"-c",
source.to_str().unwrap(),
"-I",
include.to_str().unwrap(),
]))
.unwrap();
let err = digest_cc_include_dir_names(&parsed);
fs::set_permissions(&include, fs::Permissions::from_mode(0o755)).unwrap();
let err = err.expect_err("unreadable -I dir must fail closed");
assert!(err.to_string().contains("unreadable"), "got {err}");
}
#[cfg(unix)]
#[test]
fn shadowing_header_changes_cc_cache_key() {
let temp = tempfile::tempdir().unwrap();
let (compiler, fake_cc, source) = include_dir_test_compiler(temp.path());
let first = temp.path().join("first");
let second = temp.path().join("second");
fs::create_dir(&first).unwrap();
fs::create_dir(&second).unwrap();
fs::write(second.join("header.h"), "#define A 1\n").unwrap();
let output = temp.path().join("unit.o");
let parse = || {
compiler
.parse(&[
fake_cc.to_string_lossy().into_owned(),
"-c".to_string(),
source.to_string_lossy().into_owned(),
"-o".to_string(),
output.to_string_lossy().into_owned(),
"-I".to_string(),
first.to_string_lossy().into_owned(),
"-I".to_string(),
second.to_string_lossy().into_owned(),
])
.unwrap()
};
let (file_hasher, cache) = include_dir_key_ctx(temp.path());
let path_normalizer = crate::path_normalizer::PathNormalizer::empty();
let ctx = KeyCtx {
file_hasher: &file_hasher,
path_normalizer: &path_normalizer,
cache_dir: &cache,
key_salt: None,
key_env_vars: &[],
extra_inputs_digest: None,
};
let before = compiler.cache_key(&parse(), &ctx).unwrap();
fs::write(first.join("header.h"), "#define A 2\n").unwrap();
let after = compiler.cache_key(&parse(), &ctx).unwrap();
assert_ne!(
before, after,
"a shadowing header must change the cc key even when preprocess output is unchanged"
);
assert!(compiler.include_dir_names_still_match(&parse()));
}
}