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, Write};
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,
}
pub(crate) const CC_STDOUT_STORE_NAME: &str = "stdout.i";
pub(crate) fn cc_expansion_is_stdout(parsed: &CcArgs) -> bool {
parsed.mode == CompileMode::Preprocess && parsed.output.is_none()
}
pub(crate) fn cc_is_internal_key_probe() -> bool {
std::env::var_os("KACHE_CC_KEY_PROBE").is_some()
}
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,
pub(crate) depinfo_roots: Vec<(PathBuf, String, u8)>,
}
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,
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,
depinfo_roots: Vec::new(),
};
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 => {}
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 probe_args = self.probe_args();
let mut iter = probe_args.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
}
fn probe_args(&self) -> Vec<String> {
let mut out = Vec::new();
let mut rest = self.rest.as_slice();
while let Some((arg, tail)) = rest.split_first() {
if arg == "-Xclang"
&& let Some((inner, remaining)) = tail.split_first()
{
if !cc_terminal_diagnostic_flag(inner) {
out.extend([arg.clone(), inner.clone()]);
}
rest = remaining;
continue;
}
let consumed =
parse_cc_arg_at(rest, 0, self.family.dialect()).map_or(1, |parsed| parsed.consumed);
let (args, remaining) = rest.split_at(consumed);
if !cc_terminal_diagnostic_flag(arg) {
out.extend_from_slice(args);
}
rest = remaining;
}
out
}
}
fn cc_terminal_diagnostic_flag(arg: &str) -> bool {
matches!(
arg,
"-fcolor-diagnostics" | "-fno-color-diagnostics" | "-fansi-escape-codes"
) || arg.starts_with("-fdiagnostics-")
}
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";
pub(crate) const CC_OBJECT_STORE_NAME: &str = "__kache_cc_object.o";
const CC_BASE_SENTINEL: &str = "/kache/base-dir";
const CC_SDKROOT_SENTINEL: &str = "/kache/sdkroot";
const CC_OUT_DIR_SENTINEL: &str = "/kache/cc-out-dir";
const CC_TARGET_SENTINEL: &str = "/kache/cc-target";
const CC_DEP_OUT_DIR_SENTINEL: &str = "/kache/cc-dep-out";
#[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);
command.env("KACHE_CC_KEY_PROBE", "1");
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().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,
&cc_prefix_maps_key(prefix_maps),
&|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|cxx-exceptions|data-sections|",
"fast-math|finite-math-only|freestanding|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. cxx-exceptions / freestanding from Firefox Windows nightly.",
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("-faligned-new"),
class: FlagClass::CapturedByProbe,
source: "RocksDB C++17 aligned new/delete (enabled); resolved cc1 tokens distinguish this from the default and -fno-aligned-new.",
dialect: Some(Dialect::Gnu),
},
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::Regex(r"-mfpmath=(?:sse|387|both)"),
class: FlagClass::RawKeyed,
source: "Issue #826 — x87/SSE fp math; enumerated so unknown values still refuse.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-mtls-dialect=(?:gnu2?|trad|desc)"),
class: FlagClass::RawKeyed,
source: "Issue #826 — TLS dialect; closed value set, keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(
r"-mbranch-protection=(?:none|standard|pac-ret(?:\+leaf)?(?:\+b-key)?(?:\+bti)?|bti|gcs)",
),
class: FlagClass::RawKeyed,
source: "Issue #826 — aarch64 branch protection; enumerated closed set.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Exact("-mgeneral-regs-only"),
class: FlagClass::RawKeyed,
source: "Issue #826 — restrict to general registers; valueless, keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Regex(r"-mstack-protector-guard=(?:tls|global|sysreg)"),
class: FlagClass::RawKeyed,
source: "Issue #826 — stack-protector guard location; enumerated so host-relative values refuse.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mstack-protector-guard-offset="),
class: FlagClass::RawKeyed,
source: "Issue #826 — stack-protector guard offset; numeric, keyed verbatim.",
dialect: None,
},
FlagSpec {
matcher: Matcher::Prefix("-mstack-protector-guard-reg="),
class: FlagClass::RawKeyed,
source: "Issue #826 — stack-protector guard register name; keyed verbatim.",
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: "clang-cl omit frame pointer (MSVC spelling of -fomit-frame-pointer). Keyed via -### resolved tokens. -Oy- was modeled in #285; Firefox Windows uses the enable polarity.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Oy"),
class: FlagClass::CapturedByProbe,
source: "clang-cl omit frame pointer (slash spelling). 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("-Zl"),
class: FlagClass::CapturedByProbe,
source: "clang-cl omit default library name from the object (.drectve). Firefox Windows nightly. Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/Zl"),
class: FlagClass::CapturedByProbe,
source: "clang-cl omit default library name from the object (.drectve). Keyed via -### resolved tokens.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-fp:"),
class: FlagClass::CapturedByProbe,
source: "clang-cl floating-point model (-fp:fast/precise/strict). Firefox Windows nightly. Prefix safe: -### reflects the model into cc1.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/fp:"),
class: FlagClass::CapturedByProbe,
source: "clang-cl floating-point model (/fp:fast/precise/strict). Prefix safe: -### reflects the model into cc1.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("/clang:"),
class: FlagClass::CapturedByProbe,
source: "clang-cl /clang:<flag> forwards a clang driver flag. Firefox Windows uses /clang:-fno-finite-math-only. Prefix safe: -### captures the forwarded token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Prefix("-clang:"),
class: FlagClass::CapturedByProbe,
source: "clang-cl -clang:<flag> dash spelling of /clang:. Prefix safe: -### captures the forwarded token.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/WX"),
class: FlagClass::RawKeyed,
source: "clang-cl warnings-as-errors (/WX). Outcome gate, same contract as -Werror.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("-WX"),
class: FlagClass::RawKeyed,
source: "clang-cl warnings-as-errors (-WX). Outcome gate, same contract as -Werror.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Regex(r"/W(all|[0-4])"),
class: FlagClass::NoObjectEffect,
source: "clang-cl warning level (/W0-/W4, /Wall). Diagnostics only. Dash -Wall is already covered by the dialect-agnostic -W* row.",
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). MSVC makes __FILE__ absolute; clang accepts the flag and ignores it.",
dialect: Some(Dialect::Cl),
},
FlagSpec {
matcher: Matcher::Exact("/FC"),
class: FlagClass::PreprocessorCaptured,
source: "#285 Layer 4 — clang-cl full-path __FILE__ (/FC). MSVC makes __FILE__ absolute; clang accepts the flag and ignores it.",
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<crate::compiler::flags::RegexCache> = 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)
}
thread_local! {
static PREFIX_MAPS_MEMO: std::cell::RefCell<Option<PrefixMapsMemo>> =
const { std::cell::RefCell::new(None) };
}
struct PrefixMapsMemo {
identity: Vec<String>,
base_dirs: Vec<String>,
maps: Vec<CcPrefixMap>,
}
fn cc_prefix_maps(parsed: &CcArgs, configured_base_dirs: &[String]) -> Vec<CcPrefixMap> {
let env = |name: &str| {
std::env::var_os(name)
.map(|v| v.to_string_lossy().into_owned())
.unwrap_or_default()
};
let mut identity = Vec::with_capacity(parsed.rest.len() + 6);
identity.push(parsed.program.clone());
identity.extend(parsed.rest.iter().cloned());
identity.push(
std::env::current_dir()
.map(|d| d.to_string_lossy().into_owned())
.unwrap_or_default(),
);
for name in [
"KACHE_CC_PATH_NORMALIZE",
"KACHE_BASE_DIR",
"SDKROOT",
"OUT_DIR",
] {
identity.push(env(name));
}
let memoized = PREFIX_MAPS_MEMO.with(|memo| {
memo.borrow().as_ref().and_then(|last| {
(last.identity == identity && last.base_dirs == configured_base_dirs)
.then(|| last.maps.clone())
})
});
if let Some(maps) = memoized {
return maps;
}
let maps = cc_prefix_maps_uncached(parsed, configured_base_dirs);
PREFIX_MAPS_MEMO.with(|memo| {
*memo.borrow_mut() = Some(PrefixMapsMemo {
identity,
base_dirs: configured_base_dirs.to_vec(),
maps: maps.clone(),
});
});
maps
}
fn cc_prefix_maps_uncached(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());
let mut maps = cc_prefix_maps_cfg(
parsed,
&cwd,
base.as_deref().map(Path::new),
sdkroot.as_deref().map(Path::new),
configured_base_dirs,
);
if !maps.is_empty()
&& let Some(out_dir) = std::env::var_os("OUT_DIR").filter(|v| !v.is_empty())
{
push_cargo_out_dir_maps(&mut maps, &cwd, Path::new(&out_dir));
let mut include_dirs = cc_user_include_dirs(parsed, &cwd);
include_dirs.extend(
cc_flag_dir_values(&parsed.rest, "-isystem")
.into_iter()
.map(|dir| absolutize_path(&cwd, Path::new(dir))),
);
push_cargo_dep_out_dir_maps(&mut maps, &cwd, Path::new(&out_dir), &include_dirs);
maps.sort_by_key(|m| std::cmp::Reverse(m.from.len()));
}
maps
}
fn push_cargo_dep_out_dir_maps(
maps: &mut Vec<CcPrefixMap>,
cwd: &Path,
out_dir: &Path,
include_dirs: &[PathBuf],
) {
let own = absolutize_path(cwd, out_dir);
let Some(target) = crate::build_script::target_dir(&own) else {
return;
};
let mut units: std::collections::BTreeMap<String, Vec<PathBuf>> =
std::collections::BTreeMap::new();
for dir in include_dirs {
let Some((unit_out, name)) = crate::cargo_layout::unit_out_dir_under(&target, dir) else {
continue;
};
if unit_out == own {
continue;
}
let dirs = units.entry(name).or_default();
if !dirs.contains(&unit_out) {
dirs.push(unit_out);
}
}
for (name, dirs) in units {
let [unit_out] = dirs.as_slice() else {
continue;
};
let to = format!("{CC_DEP_OUT_DIR_SENTINEL}/{name}");
for root in [canonicalize_or_self(unit_out), unit_out.clone()] {
let from = root.to_string_lossy().to_string();
if !from.is_empty() && !maps.iter().any(|m| m.from == from) {
maps.push(CcPrefixMap {
from,
to: to.clone(),
});
}
}
}
}
fn push_cargo_out_dir_maps(maps: &mut Vec<CcPrefixMap>, cwd: &Path, out_dir: &Path) {
let out_abs = absolutize_path(cwd, out_dir);
let mut roots: Vec<(PathBuf, &'static str)> = Vec::new();
if let Some(target) = crate::build_script::target_dir(&out_abs) {
roots.push((canonicalize_or_self(&target), CC_TARGET_SENTINEL));
roots.push((target, CC_TARGET_SENTINEL));
}
roots.push((canonicalize_or_self(&out_abs), CC_OUT_DIR_SENTINEL));
roots.push((out_abs, CC_OUT_DIR_SENTINEL));
for (root, to) in roots {
let from = root.to_string_lossy().to_string();
if !from.is_empty() && !maps.iter().any(|m| m.from == from) {
maps.push(CcPrefixMap {
from,
to: to.to_string(),
});
}
}
}
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(),
)
}
pub(super) 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 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);
tracing::trace!(
target: "kache::cc_memo_key",
"memo-field {}={}",
String::from_utf8_lossy(label),
&blake3::hash(value).to_hex()[..12]
);
}
const CC_MEMO_VOLATILE_ENV: &[&str] = &[
"_",
"OLDPWD",
"PWD",
"SHLVL",
"CARGO_MAKEFLAGS",
"MAKEFLAGS",
"MFLAGS",
"MAKELEVEL",
"NUM_JOBS",
];
const CC_MEMO_KEYED_ENV: &[&str] = &[
"CPATH",
"C_INCLUDE_PATH",
"CPLUS_INCLUDE_PATH",
"OBJC_INCLUDE_PATH",
"OBJCPLUS_INCLUDE_PATH",
"GCC_EXEC_PREFIX",
"COMPILER_PATH",
"LIBRARY_PATH",
"LANG",
"LC_ALL",
"LC_CTYPE",
"LC_MESSAGES",
"DEPENDENCIES_OUTPUT",
"SUNPRO_DEPENDENCIES",
"SOURCE_DATE_EPOCH",
"CCC_OVERRIDE_OPTIONS",
"CLANG_CONFIG_FILE_SYSTEM_DIR",
"CLANG_CONFIG_FILE_USER_DIR",
"CLANG_NO_DEFAULT_CONFIG",
"SDKROOT",
"MACOSX_DEPLOYMENT_TARGET",
"IPHONEOS_DEPLOYMENT_TARGET",
"TVOS_DEPLOYMENT_TARGET",
"WATCHOS_DEPLOYMENT_TARGET",
"XROS_DEPLOYMENT_TARGET",
"DRIVERKIT_DEPLOYMENT_TARGET",
"INCLUDE",
"EXTERNAL_INCLUDE",
"CL",
"_CL_",
"VCINSTALLDIR",
"VCToolsInstallDir",
"VCToolsVersion",
"WindowsSdkDir",
"WindowsSDKVersion",
"UCRTVersion",
"Platform",
];
fn cc_memo_env_pattern_is_keyed(name: &str) -> bool {
let upper = name.to_ascii_uppercase();
[
"INCLUDE",
"SYSROOT",
"SDK",
"DEPLOYMENT_TARGET",
"CLANG",
"GCC",
"CCACHE",
]
.iter()
.any(|needle| upper.contains(needle))
|| upper.ends_with("FLAGS")
}
fn cc_memo_env_is_keyed(name: &OsStr) -> bool {
let Some(name) = name.to_str() else {
return false;
};
if name.starts_with("KACHE_") || CC_MEMO_VOLATILE_ENV.contains(&name) {
return false;
}
if name.starts_with("DEP_") {
return false;
}
CC_MEMO_KEYED_ENV.contains(&name) || cc_memo_env_pattern_is_keyed(name)
}
const CC_ARTIFACT_SCHEMA: &[u8] = b"cc_artifact_schema:4\n";
const CC_PREPROCESS_MEMO_SCHEMA: &[u8] = b"cc-preprocess-memo-v6";
fn cc_preprocess_memo_key(
parsed: &CcArgs,
prefix_maps: &[CcPrefixMap],
compiler_version: &str,
) -> Option<String> {
cc_preprocess_memo_key_as(
CC_PREPROCESS_MEMO_SCHEMA,
parsed,
prefix_maps,
compiler_version,
)
}
fn cc_preprocess_memo_key_as(
schema: &[u8],
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", schema);
fold_cc_memo_field(
&mut hasher,
b"compiler-program",
crate::cache_key::env_os_key_bytes(OsStr::new(&parsed.program)).as_slice(),
);
fold_cc_memo_field(
&mut hasher,
b"compiler-path",
crate::cache_key::env_os_key_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",
crate::cache_key::env_os_key_bytes(&epoch).as_slice(),
);
let mut memo_parsed = parsed.clone();
memo_parsed.rest = parsed.probe_args();
for arg in build_preprocess_args(&memo_parsed) {
let mapped = apply_cc_prefix_maps_to_bytes(arg.into_bytes(), prefix_maps);
fold_cc_memo_field(&mut hasher, b"arg", &mapped);
}
for source in &parsed.sources {
let content = crate::cache_key::hash_file(&absolutize_path(&cwd, source))
.unwrap_or_else(|_| "unreadable".to_string());
fold_cc_memo_field(&mut hasher, b"source-content", content.as_bytes());
}
let mut targets: Vec<&str> = prefix_maps.iter().map(|map| map.to.as_str()).collect();
targets.sort_unstable();
targets.dedup();
for target in targets {
fold_cc_memo_field(&mut hasher, b"prefix-to", target.as_bytes());
}
let mut environment: Vec<(Vec<u8>, Vec<u8>)> = std::env::vars_os()
.filter(|(name, _)| cc_memo_env_is_keyed(name))
.map(|(name, value)| {
let mapped = match value.to_str() {
Some(text) => {
let mapped =
apply_cc_prefix_maps_to_bytes(text.as_bytes().to_vec(), prefix_maps);
crate::cache_key::env_os_key_bytes(OsStr::new(
String::from_utf8_lossy(&mapped).as_ref(),
))
}
None => crate::cache_key::env_os_key_bytes(&value),
};
(crate::cache_key::env_os_key_bytes(&name), mapped)
})
.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_prefix_maps_key(prefix_maps: &[CcPrefixMap]) -> String {
let mut pairs: Vec<(&str, &str)> = prefix_maps
.iter()
.filter(|map| !map.from.is_empty())
.map(|map| (map.from.as_str(), map.to.as_str()))
.collect();
pairs.sort_unstable();
pairs.dedup();
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache.cc.maps.v1\0");
for (from, to) in pairs {
hasher.update(from.as_bytes());
hasher.update(b"\0");
hasher.update(to.as_bytes());
hasher.update(b"\n");
}
hasher.finalize().to_hex().to_string()
}
fn cc_mapped_content_hash(
path: &Path,
prefix_maps: &[CcPrefixMap],
) -> Option<crate::cache_key::CcContentHashes> {
let bytes = std::fs::read(path).ok()?;
let raw = blake3::hash(&bytes).to_hex().to_string();
let mapped = apply_cc_prefix_maps_to_bytes(bytes, prefix_maps);
Some(crate::cache_key::CcContentHashes {
raw,
mapped: 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 leading: Vec<u8> = maps.iter().filter_map(|m| m.from.bytes().next()).collect();
leading.sort_unstable();
leading.dedup();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i..].iter().position(|byte| leading.contains(byte)) {
Some(0) => {}
Some(offset) => {
let next = i + offset;
out.extend_from_slice(&bytes[i..next]);
i = next;
}
None => {
out.extend_from_slice(&bytes[i..]);
break;
}
}
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())
}
#[cfg(test)]
fn digest_cc_include_shadowing(parsed: &CcArgs, read_inputs: &[PathBuf]) -> Result<String> {
digest_cc_shadowing_names(parsed, &cc_shadowing_names(parsed, read_inputs))
}
fn cc_shadowing_names(
parsed: &CcArgs,
read_inputs: &[PathBuf],
) -> std::collections::BTreeSet<PathBuf> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let dirs = cc_user_include_dirs(parsed, &cwd);
let mut names: std::collections::BTreeSet<PathBuf> = std::collections::BTreeSet::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));
}
names.extend(candidates);
}
names
}
fn digest_cc_shadowing_names(
parsed: &CcArgs,
names: &std::collections::BTreeSet<PathBuf>,
) -> Result<String> {
digest_cc_shadowing_names_stamped(parsed, names).map(|(digest, _)| digest)
}
fn digest_cc_shadowing_names_stamped(
parsed: &CcArgs,
names: &std::collections::BTreeSet<PathBuf>,
) -> Result<(String, Option<CcListingStamps>)> {
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let dirs = cc_user_include_dirs(parsed, &cwd);
let mut hasher = blake3::Hasher::new();
let mut listings = CcDirectoryListings::default();
for name in names {
hasher.update(name.as_os_str().as_encoded_bytes());
hasher.update(b"\x1f");
match cc_first_include_dir_providing_cached(&dirs, name, &mut listings)? {
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(),
listings.stamps(&dirs),
))
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CcListingStamp {
Absent,
Present(crate::cache_key::FileFingerprint),
}
type CcListingStamps = Vec<(PathBuf, CcListingStamp)>;
const CC_LISTING_SETTLED_NS: i64 = 2_000_000_000;
fn cc_listing_settled(mtime_ns: i64, read_at: i64) -> bool {
mtime_ns < read_at - CC_LISTING_SETTLED_NS
}
impl CcListingStamp {
fn take(directory: &Path) -> Option<Self> {
let read_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_nanos();
let read_at = i64::try_from(read_at).ok()?;
match crate::cache_key::FileFingerprint::from_path(directory) {
Ok(fingerprint) if cc_listing_settled(fingerprint.mtime_ns, read_at) => {
Some(Self::Present(fingerprint))
}
Ok(_) => None,
Err(_) => match std::fs::metadata(directory) {
Err(error) if error.kind() == ErrorKind::NotFound => Some(Self::Absent),
_ => None,
},
}
}
fn holds(&self, directory: &Path) -> bool {
match self {
Self::Absent => {
std::fs::metadata(directory).is_err_and(|error| error.kind() == ErrorKind::NotFound)
}
Self::Present(fingerprint) => crate::cache_key::FileFingerprint::from_path(directory)
.is_ok_and(|now| now == *fingerprint),
}
}
}
#[cfg(test)]
fn cc_first_include_dir_providing(dirs: &[PathBuf], name: &Path) -> Result<Option<usize>> {
cc_first_include_dir_providing_cached(dirs, name, &mut CcDirectoryListings::default())
}
#[derive(Default)]
struct CcDirectoryListings {
listings: foldhash::HashMap<PathBuf, CcStampedListing>,
by_position: Vec<Option<CcStampedListing>>,
}
struct CcDirectoryListing {
names: foldhash::HashSet<std::ffi::OsString>,
folded: Option<foldhash::HashSet<String>>,
}
struct CcStampedListing {
listing: Option<CcDirectoryListing>,
stamp: Option<CcListingStamp>,
}
impl CcStampedListing {
fn read(directory: &Path) -> Result<Self> {
let stamp = CcListingStamp::take(directory);
Ok(Self {
listing: CcDirectoryListing::read(directory)?,
stamp,
})
}
}
impl CcDirectoryListing {
fn read(directory: &Path) -> Result<Option<Self>> {
match std::fs::read_dir(directory) {
Ok(entries) => {
let mut names = foldhash::HashSet::default();
for entry in entries {
names.insert(entry?.file_name());
}
let folded = (!Self::case_sensitive(directory, &names)).then(|| {
names
.iter()
.map(|name| name.to_string_lossy().to_lowercase())
.collect()
});
Ok(Some(Self { names, folded }))
}
Err(error)
if error.kind() == ErrorKind::NotFound
|| error.kind() == ErrorKind::NotADirectory =>
{
Ok(None)
}
Err(error) => anyhow::bail!(
"cc include directory {} is unreadable ({error})",
directory.display()
),
}
}
fn case_sensitive(directory: &Path, names: &foldhash::HashSet<std::ffi::OsString>) -> bool {
let Some(swapped) = names.iter().find_map(|name| {
let name = name.to_str()?;
name.bytes().any(|b| b.is_ascii_alphabetic()).then(|| {
name.chars()
.map(|c| {
if c.is_ascii_lowercase() {
c.to_ascii_uppercase()
} else {
c.to_ascii_lowercase()
}
})
.collect::<String>()
})
}) else {
return false;
};
!names.contains(OsStr::new(&swapped))
&& std::fs::symlink_metadata(directory.join(&swapped))
.is_err_and(|error| error.kind() == ErrorKind::NotFound)
}
fn provides(&self, directory: &Path, file_name: &OsStr) -> Result<bool> {
if self.names.contains(file_name) {
return Ok(true);
}
let Some(folded) = &self.folded else {
return Ok(false);
};
if folded.contains(&file_name.to_string_lossy().to_lowercase()) {
return match std::fs::symlink_metadata(directory.join(file_name)) {
Ok(_) => Ok(true),
Err(error)
if error.kind() == ErrorKind::NotFound
|| error.kind() == ErrorKind::NotADirectory =>
{
Ok(false)
}
Err(error) => anyhow::bail!(
"cc include candidate {} is unreadable ({error})",
directory.join(file_name).display()
),
};
}
Ok(false)
}
}
impl CcDirectoryListings {
fn contains(&mut self, directory: &Path, file_name: &OsStr) -> Result<bool> {
let listing = match self.listings.entry(directory.to_path_buf()) {
std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(CcStampedListing::read(directory)?)
}
};
match &listing.listing {
Some(listing) => listing.provides(directory, file_name),
None => Ok(false),
}
}
fn contains_at(
&mut self,
position: usize,
directory: &Path,
file_name: &OsStr,
) -> Result<bool> {
if self.by_position.len() <= position {
self.by_position.resize_with(position + 1, || None);
}
let slot = &mut self.by_position[position];
if slot.is_none() {
*slot = Some(CcStampedListing::read(directory)?);
}
match slot.as_ref().and_then(|stamped| stamped.listing.as_ref()) {
Some(listing) => listing.provides(directory, file_name),
None => Ok(false),
}
}
fn stamps(&self, dirs: &[PathBuf]) -> Option<CcListingStamps> {
let positioned = self
.by_position
.iter()
.zip(dirs)
.filter_map(|(slot, dir)| slot.as_ref().map(|stamped| (dir, stamped)));
positioned
.chain(self.listings.iter())
.map(|(dir, stamped)| Some((dir.clone(), stamped.stamp.clone()?)))
.collect()
}
}
fn cc_first_include_dir_providing_cached(
dirs: &[PathBuf],
name: &Path,
listings: &mut CcDirectoryListings,
) -> Result<Option<usize>> {
let Some(file_name) = name.file_name() else {
return Ok(None);
};
let parent = name
.parent()
.filter(|parent| !parent.as_os_str().is_empty());
for (index, dir) in dirs.iter().enumerate() {
let provided = match parent {
Some(parent) => listings.contains(&dir.join(parent), file_name)?,
None => listings.contains_at(index, dir, file_name)?,
};
if provided {
return Ok(Some(index));
}
}
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>,
captured: bool,
}
pub(crate) enum CcKeyDiscovery {
Expansion,
Deferrable,
Captured(CcCapturedInputs),
}
pub(crate) enum CcKeyOutcome {
Key(String),
Deferred(CcDeferredKey),
}
pub(crate) struct CcDeferredKey {
pub(crate) memo_key: String,
}
pub(crate) struct CcCapturedInputs {
fingerprints: Vec<crate::cache_key::CcPreprocessMemoInput>,
path_bound: bool,
inputs_changed: bool,
}
impl CcCapturedInputs {
pub(crate) fn inputs_changed(&self) -> bool {
self.inputs_changed
}
}
const CC_MEMO_PATH_BOUND_PREFIX: &str = "pb:";
fn cc_memo_identity_portable(parsed: &CcArgs, cwd: &Path, prefix_maps: &[CcPrefixMap]) -> bool {
let portable = |path: &Path| {
let absolute = absolutize_path(cwd, path);
let raw = absolute.to_string_lossy().into_owned();
let mapped = cc_mapped_path(&absolute, prefix_maps);
(mapped != raw
&& !mapped.starts_with(CC_BUILD_SENTINEL)
&& !mapped.starts_with(CC_SOURCE_SENTINEL))
|| cc_system_path(&raw)
};
portable(cwd)
&& parsed.sources.iter().all(|source| portable(source))
&& parsed.includes.iter().all(|include| portable(include))
}
fn cc_system_path(path: &str) -> bool {
["/usr/", "/opt/", "/Library/", "/Applications/", "/nix/"]
.iter()
.any(|root| path.starts_with(root))
}
fn cc_direct_inputs_digest(fingerprints: &[crate::cache_key::CcPreprocessMemoInput]) -> String {
let mut contents: Vec<&str> = fingerprints
.iter()
.map(|input| {
if cc_system_path(input.local_path()) {
input.content.as_str()
} else {
input.mapped.as_str()
}
})
.collect();
contents.sort_unstable();
contents.dedup();
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache.cc.direct-inputs.v2\0");
hasher.update(&(contents.len() as u64).to_le_bytes());
for content in contents {
hasher.update(content.as_bytes());
hasher.update(b"\n");
}
hasher.finalize().to_hex().to_string()
}
fn cc_memo_hash(digest: &str, path_bound: bool) -> String {
if path_bound {
format!("{CC_MEMO_PATH_BOUND_PREFIX}{digest}")
} else {
digest.to_string()
}
}
fn cc_memo_hash_parts(hash: &str) -> (&str, bool) {
match hash.strip_prefix(CC_MEMO_PATH_BOUND_PREFIX) {
Some(digest) => (digest, true),
None => (hash, false),
}
}
const CC_DIRECT_CAPTURE_TARGET: &str = "__kache_direct_capture";
pub(crate) fn cc_direct_key_eligible(parsed: &CcArgs) -> bool {
cc_direct_key_shape_ok(parsed)
&& cc_driver_captures_dependencies(&parsed.program, &crate::config::probe_memo_dir())
}
fn cc_direct_key_shape_ok(parsed: &CcArgs) -> bool {
parsed.family.dialect() == Dialect::Gnu
&& parsed.mode == CompileMode::Compile
&& parsed.sources.len() == 1
&& parsed
.depinfo
.as_ref()
.is_none_or(|depinfo| !depinfo.emit || depinfo.include_system)
}
fn cc_driver_captures_dependencies(program: &str, memo_dir: &Path) -> bool {
let Some(digest) = cc_direct_probe_material(program) else {
return false;
};
let path = crate::probe_memo::memo_path(memo_dir, "cc-direct-capture", "txt", &digest);
if let Some(body) = crate::probe_memo::read_verified(&path, &digest) {
return body.trim() == "ok";
}
let supported = {
let _trace = crate::phase_trace::phase("cc_direct_probe");
cc_direct_probe(program)
};
tracing::debug!("cc: probed {program} for compile-first capture: {supported}");
crate::probe_memo::write_verified(&path, &digest, if supported { "ok" } else { "no" });
supported
}
fn cc_direct_probe_material(program: &str) -> Option<String> {
let resolved = super::resolve_program_on_path(program)?;
let canonical = std::fs::canonicalize(&resolved).ok()?;
let metadata = std::fs::metadata(&canonical).ok()?;
let mut material = crate::probe_memo::Material::new("cc-direct-capture-v1");
material.push(program.as_bytes());
material.push(canonical.as_os_str().as_encoded_bytes());
material.push(&metadata.len().to_le_bytes());
material.push(&crate::cache_key::metadata_mtime_ns(&metadata).to_le_bytes());
Some(material.digest())
}
fn cc_direct_probe(program: &str) -> bool {
let Ok(dir) = tempfile::Builder::new()
.prefix("kache-cc-direct-probe-")
.tempdir()
else {
return false;
};
let root = dir.path();
if fs::write(
root.join("probe.c"),
"int kache_direct_probe(void) { return 0; }\n",
)
.is_err()
{
return false;
}
let Some(prefix_map) = root
.to_str()
.map(|root| format!("-ffile-prefix-map={root}=/kache/probe"))
else {
return false;
};
let output = Command::new(program)
.args([
"-c",
"probe.c",
"-o",
"probe.o",
"-MD",
"-MF",
"probe.d",
"-MT",
CC_DIRECT_CAPTURE_TARGET,
prefix_map.as_str(),
])
.current_dir(root)
.env("KACHE_CC_KEY_PROBE", "1")
.output();
let Ok(output) = output else {
return false;
};
let depfile = fs::read_to_string(root.join("probe.d")).unwrap_or_default();
let ok =
output.status.success() && root.join("probe.o").is_file() && depfile.contains("probe.c");
tracing::debug!(
"cc: {program} compile-first capture probe: exit {} object {} depfile {}: {}",
output.status,
root.join("probe.o").is_file(),
depfile.contains("probe.c"),
String::from_utf8_lossy(&output.stderr).trim()
);
ok
}
#[cfg(test)]
fn cc_inputs_hide_assembler_input(paths: &[PathBuf]) -> Option<&'static str> {
paths.iter().find_map(|path| {
let bytes = fs::read(path).ok()?;
cc_raw_assembler_hidden_input(&bytes)
})
}
fn cc_raw_assembler_hidden_input(text: &[u8]) -> Option<&'static str> {
if let Some(found) = cc_assembler_text_hidden_input(text) {
return Some(found);
}
let literals = cc_string_literals(text);
if literals.named_escape {
return Some(r"\N{...}");
}
cc_assembler_text_hidden_input(&literals.text)
}
#[derive(Default)]
pub struct CcCompiler {
extra_allowlist_flags: Vec<String>,
cache_cc_links: bool,
base_dirs: Vec<String>,
pending_preprocess_memo: RefCell<Option<PendingCcPreprocessMemo>>,
pending_include_dir_digest: RefCell<Option<PendingIncludeDirDigest>>,
key_path_bound: Cell<bool>,
}
struct PendingIncludeDirDigest {
digest: String,
names: Option<std::collections::BTreeSet<PathBuf>>,
stamps: Option<CcListingStamps>,
}
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,
cache_cc_links: false,
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_cache_cc_links(mut self, cache_cc_links: bool) -> Self {
self.cache_cc_links = cache_cc_links;
self
}
fn cc_link_probe_is_windows_aware(family: ToolFamily) -> bool {
family.dialect() != Dialect::Cl
}
fn cache_key_for_link(&self, parsed: &CcArgs, ctx: &KeyCtx<'_, '_>) -> Result<String> {
let mut hasher = blake3::Hasher::new();
hasher.update(b"cc_link_key_version:2\n");
hasher.update(CC_ARTIFACT_SCHEMA);
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");
let config_args = parsed.config_args();
let resolved = crate::probe::probe(
ctx.cache_dir,
&crate::probe::CcProber,
&crate::probe::ProbeRequest {
compiler: &parsed.program,
args: &parsed.rest,
key_args: &config_args,
per_tu_paths: &[],
windows_aware: Self::cc_link_probe_is_windows_aware(parsed.family),
},
)?;
hasher.update(b"compiler_version:");
hasher.update(resolved.version_line.as_bytes());
hasher.update(b"\n");
let expanded = expand_cc_response_files(&parsed.rest)?;
hasher.update(b"argv:");
for arg in &expanded {
hasher.update(ctx.path_normalizer.normalize(arg).as_bytes());
hasher.update(b"\x1f");
}
hasher.update(b"\n");
let mut inputs = parsed.sources.clone();
inputs.extend(cc_link_file_inputs(&expanded));
inputs.sort();
inputs.dedup();
hasher.update(b"inputs:\n");
for input in &inputs {
let digest = ctx
.file_hasher
.hash(input)
.with_context(|| format!("hashing link input {}", input.display()))?;
hasher.update(digest.as_bytes());
hasher.update(b" ");
hasher.update(
ctx.path_normalizer
.normalize(input.to_string_lossy())
.as_bytes(),
);
hasher.update(b"\n");
}
let key = hasher.finalize().to_hex().to_string();
let key = crate::cache_key::apply_key_env_vars(key, ctx.key_env_vars, "cc-link");
Ok(crate::cache_key::apply_key_salt(
key,
ctx.key_salt,
"cc-link",
))
}
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;
};
let _trace = crate::phase_trace::phase("memo_commit");
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 captured_preprocess_memo(&self) -> Option<crate::daemon_publish::CcMemoHandoff> {
let pending = self.pending_preprocess_memo.borrow();
let pending = pending.as_ref()?;
if !pending.captured
|| !pending
.fingerprints
.iter()
.all(crate::cache_key::CcPreprocessMemoInput::stamp_settled)
{
return None;
}
Some(crate::daemon_publish::CcMemoHandoff {
memo_key: pending.memo_key.clone(),
preprocessed_hash: pending.preprocessed_hash.clone(),
inputs: pending.fingerprints.clone(),
})
}
pub(crate) fn discard_preprocess_memo(&self) {
self.pending_preprocess_memo.borrow_mut().take();
}
pub(crate) fn include_dir_names_still_match(&self, parsed: &CcArgs) -> bool {
let pending = self.pending_include_dir_digest.borrow();
let Some(pending) = pending.as_ref() else {
return false;
};
if let Some(stamps) = &pending.stamps
&& stamps.iter().all(|(dir, stamp)| stamp.holds(dir))
{
return true;
}
let now = match &pending.names {
Some(names) => digest_cc_shadowing_names(parsed, names),
None => digest_cc_include_dir_names(parsed),
};
now.is_ok_and(|now| now == pending.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> {
let mut parsed = CcArgs::parse(args)?;
parsed.depinfo_roots = crate::path_normalizer::PathNormalizer::from_env(None)
.with_base_dirs(&self.base_dirs)
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect();
Ok(parsed)
}
fn refuse_reasons(&self, parsed: &CcArgs) -> Vec<RefuseReason> {
let mut reasons = parsed.refuse_reasons(&self.extra_allowlist_flags);
if self.cache_cc_links && parsed.mode == CompileMode::Link {
reasons.retain(|reason| match reason {
RefuseReason::Unsupported(detail) => !detail.contains("cc link mode"),
_ => true,
});
if parsed.output.is_none() {
reasons.push(RefuseReason::Unsupported(
"cc link mode without -o — not cacheable",
));
}
}
reasons
}
fn cache_key(&self, parsed: &CcArgs, ctx: &KeyCtx<'_, '_>) -> Result<String> {
match self.cache_key_with(parsed, ctx, CcKeyDiscovery::Expansion)? {
CcKeyOutcome::Key(key) => Ok(key),
CcKeyOutcome::Deferred(_) => {
anyhow::bail!("cc key deferred without a compile-first caller")
}
}
}
fn execute(&self, parsed: &CcArgs) -> Result<CompileResult> {
self.execute_with_extra_args(parsed, Vec::new(), false)
.map(|(result, _)| result)
}
fn classify_output(&self, _parsed: &CcArgs, name: &str) -> ArtifactKind {
classify_by_filename(name)
}
}
impl CcCompiler {
pub(crate) fn cache_key_with(
&self,
parsed: &CcArgs,
ctx: &KeyCtx<'_, '_>,
discovery: CcKeyDiscovery,
) -> Result<CcKeyOutcome> {
self.cache_key_under(CC_ARTIFACT_SCHEMA, parsed, ctx, discovery)
}
fn cache_key_under(
&self,
artifact_schema: &[u8],
parsed: &CcArgs,
ctx: &KeyCtx<'_, '_>,
discovery: CcKeyDiscovery,
) -> Result<CcKeyOutcome> {
if parsed.mode == CompileMode::Link {
return self.cache_key_for_link(parsed, ctx).map(CcKeyOutcome::Key);
}
self.pending_preprocess_memo.borrow_mut().take();
self.key_path_bound.set(false);
let _trace = crate::phase_trace::phase("key");
let mut hasher = blake3::Hasher::new();
let trace_name = cc_trace_name(parsed);
let prefix_maps = {
let _trace = crate::phase_trace::phase("cc_prefix_maps");
cc_prefix_maps(parsed, &self.base_dirs)
};
for map in &prefix_maps {
tracing::trace!(
target: "kache::cache_key",
"[key:{}] prefix_map {} => {}",
trace_name,
map.from,
map.to
);
}
hasher.update(b"cc_key_version:");
hasher.update(crate::cache_key::CACHE_KEY_VERSION.to_string().as_bytes());
hasher.update(b"\n");
hasher.update(artifact_schema);
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 probe_args = parsed.probe_args();
let per_tu_paths = cc_resolved_per_tu_paths(parsed);
let trace_probe = crate::phase_trace::phase("cc_probe");
let resolved = crate::probe::probe(
ctx.cache_dir,
&crate::probe::CcProber,
&crate::probe::ProbeRequest {
compiler: &parsed.program,
args: &probe_args,
key_args: &config_args,
per_tu_paths: &per_tu_paths,
windows_aware: parsed.family.dialect() != Dialect::Cl,
},
)?;
drop(trace_probe);
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");
}
hasher.update(b"source_paths:");
for source in &parsed.sources {
let mapped = cc_mapped_path(source, &prefix_maps);
hasher.update(mapped.as_bytes());
hasher.update(b"\x1f");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] source_path={}",
trace_name,
mapped
);
}
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 trace_memo_key = crate::phase_trace::phase("cc_memo_key");
let memo_key = ctx
.file_hasher
.supports_cc_preprocess_memo()
.then(|| cc_preprocess_memo_key(parsed, &prefix_maps, &resolved.version_line))
.flatten();
drop(trace_memo_key);
let (pp_hash, read_inputs) = if let CcKeyDiscovery::Captured(captured) = discovery {
let digest = cc_direct_inputs_digest(&captured.fingerprints);
self.key_path_bound.set(captured.path_bound);
let read_inputs = captured
.fingerprints
.iter()
.map(|input| PathBuf::from(input.local_path()))
.collect::<Vec<_>>();
if let Some(memo_key) = memo_key {
self.pending_preprocess_memo
.borrow_mut()
.replace(PendingCcPreprocessMemo {
memo_key,
preprocessed_hash: cc_memo_hash(&digest, captured.path_bound),
fingerprints: captured.fingerprints,
prefix_maps: prefix_maps.clone(),
captured: true,
});
}
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed_memo=captured",
trace_name
);
(digest, Some(read_inputs))
} else if let Some((memo_hash, satisfied)) = memo_key.as_ref().and_then(|key| {
let _trace = crate::phase_trace::phase("cc_memo_lookup");
ctx.file_hasher.cc_preprocess_memo_lookup(
key,
|name| cc_unmapped_path_candidates(name, &prefix_maps),
&|path| cc_mapped_content_hash(path, &prefix_maps),
)
}) {
let (memo_hash, path_bound) = cc_memo_hash_parts(&memo_hash);
self.key_path_bound.set(path_bound);
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed_memo=hit path_bound={path_bound}",
trace_name
);
(memo_hash.to_string(), Some(satisfied))
} else if let (CcKeyDiscovery::Deferrable, Some(memo_key)) = (&discovery, &memo_key)
&& cc_direct_key_eligible(parsed)
&& std::env::current_dir()
.is_ok_and(|cwd| cc_memo_identity_portable(parsed, &cwd, &prefix_maps))
&& !ctx.file_hasher.cc_preprocess_memo_recorded(memo_key)
{
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed_memo=none deferred",
trace_name
);
return Ok(CcKeyOutcome::Deferred(CcDeferredKey {
memo_key: memo_key.clone(),
}));
} else {
let _trace = crate::phase_trace::phase("cc_preprocess");
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<_>>()
});
let hash = match &preprocessed.fingerprints {
Some(fingerprints) => cc_direct_inputs_digest(fingerprints),
None => preprocessed.hash.clone(),
};
if let (Some(memo_key), Some(fingerprints)) = (memo_key, preprocessed.fingerprints) {
self.pending_preprocess_memo
.borrow_mut()
.replace(PendingCcPreprocessMemo {
memo_key,
preprocessed_hash: cc_memo_hash(&hash, preprocessed.path_bound),
fingerprints,
prefix_maps: prefix_maps.clone(),
captured: false,
});
}
tracing::trace!(
target: "kache::cache_key",
"[key:{}] preprocessed_memo=miss",
trace_name
);
(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
);
if self.key_path_bound.get() {
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();
hasher.update(b"path_bound_roots:");
for root in &roots {
hasher.update(root.as_bytes());
hasher.update(b"\0");
}
hasher.update(b"\n");
tracing::trace!(
target: "kache::cache_key",
"[key:{}] path_bound_roots={}",
trace_name,
roots.len()
);
}
let trace_shadowing = crate::phase_trace::phase("cc_shadowing");
let names = read_inputs
.as_deref()
.map(|inputs| cc_shadowing_names(parsed, inputs));
let (include_dir_digest, stamps, include_dir_mode) = match &names {
Some(names) => {
let (digest, stamps) = digest_cc_shadowing_names_stamped(parsed, names)?;
(digest, stamps, "resolved")
}
None => (digest_cc_include_dir_names(parsed)?, None, "walked"),
};
drop(trace_shadowing);
self.pending_include_dir_digest
.borrow_mut()
.replace(PendingIncludeDirDigest {
digest: include_dir_digest.clone(),
names,
stamps,
});
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(CcKeyOutcome::Key(key))
}
pub(crate) fn execute_capturing_inputs(
&self,
parsed: &CcArgs,
file_hasher: &crate::cache_key::FileHasher<'_>,
) -> Result<(CompileResult, Option<CcCapturedInputs>)> {
let caller_depfile = parsed.depinfo_output_path();
let private = if caller_depfile.is_none() {
match tempfile::Builder::new()
.prefix("kache-cc-capture-")
.tempdir()
{
Ok(dir) => Some(dir),
Err(error) => {
tracing::debug!("cc capture tempdir unavailable: {error}");
None
}
}
} else {
None
};
let private_path = private
.as_ref()
.map(|dir| dir.path().join("inputs.d"))
.filter(|path| path.to_str().is_some());
let extra = match &private_path {
Some(path) => vec![
"-MD".to_string(),
"-MF".to_string(),
path.to_str().expect("checked UTF-8").to_string(),
"-MT".to_string(),
CC_DIRECT_CAPTURE_TARGET.to_string(),
],
None => Vec::new(),
};
let (result, path_bound) = self.execute_with_extra_args(parsed, extra, true)?;
if result.exit_code != 0 {
return Ok((result, None));
}
let depfile = match (caller_depfile, private_path) {
(Some(path), _) => path,
(None, Some(path)) => path,
(None, None) => return Ok((result, None)),
};
let inputs = {
let _trace = crate::phase_trace::phase("cc_capture");
self.captured_inputs(parsed, &depfile, file_hasher)
};
let inputs_changed = file_hasher.too_new();
Ok((
result,
inputs.map(|fingerprints| CcCapturedInputs {
fingerprints,
path_bound,
inputs_changed,
}),
))
}
fn captured_inputs(
&self,
parsed: &CcArgs,
depfile: &Path,
file_hasher: &crate::cache_key::FileHasher<'_>,
) -> Option<Vec<crate::cache_key::CcPreprocessMemoInput>> {
let raw = match fs::read_to_string(depfile) {
Ok(raw) => raw,
Err(error) => {
tracing::debug!("cc capture depfile unreadable: {error}");
return None;
}
};
let cwd = std::env::current_dir().ok()?;
let mut paths = match parse_preprocess_dependencies(&raw, &cwd) {
Ok(paths) => paths,
Err(error) => {
tracing::debug!("cc capture depfile unusable: {error:#}");
return None;
}
};
for source in &parsed.sources {
paths.push(if source.is_absolute() {
source.clone()
} else {
cwd.join(source)
});
}
let prefix_maps = cc_prefix_maps(parsed, &self.base_dirs);
let mapped: Vec<(String, PathBuf)> = paths
.into_iter()
.map(|path| (cc_mapped_path(&path, &prefix_maps), path))
.collect();
let inputs = file_hasher.cc_preprocess_fingerprints(
&mapped,
&cc_prefix_maps_key(&prefix_maps),
&|path| cc_mapped_content_hash(path, &prefix_maps),
)?;
if let Some(found) = file_hasher.cc_inputs_hide_assembler_input(&inputs, &|path| {
let bytes = fs::read(path).ok()?;
Some((
blake3::hash(&bytes).to_hex().to_string(),
cc_raw_assembler_hidden_input(&bytes),
))
}) {
let why = if found == crate::cache_key::CC_INPUT_CHANGED_WHILE_SCANNED {
found
} else {
format!("the assembler may read a file the key cannot see (`{found}`)")
};
tracing::debug!(
"cc: {} not keyed from its read set: {why}",
cc_trace_name(parsed)
);
return None;
}
Some(inputs)
}
fn execute_with_extra_args(
&self,
parsed: &CcArgs,
extra: Vec<String>,
bind_on_embedded_root: bool,
) -> Result<(CompileResult, bool)> {
let mut path_bound = false;
crate::opcounts::record_compiler_run();
let _trace = crate::phase_trace::phase("cc_compile");
let mut command = Command::new(&parsed.program);
let prefix_maps = cc_prefix_maps(parsed, &self.base_dirs);
let mut appended = file_prefix_map_args(&prefix_maps);
appended.extend(extra);
let args = compose_cc_args(&parsed.rest, appended);
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::Link && self.cache_cc_links)
|| (parsed.mode == CompileMode::Preprocess && parsed.output.is_some());
let mut keepalive = Vec::new();
let mut artifacts = if exit_code == 0 && discovers_outputs {
discover_cc_output_artifacts(parsed)
} else {
ArtifactSet::empty()
};
if exit_code == 0 && cc_expansion_is_stdout(parsed) {
match stage_cc_stdout_artifact(&output.stdout) {
Ok((artifact, temp)) => {
artifacts = ArtifactSet::new(vec![artifact]);
keepalive.push(temp);
}
Err(error) => {
tracing::warn!("cc: staging -E stdout failed: {error:#}; not caching it");
}
}
} else {
let embeds = parsed
.object_output_path()
.map(|path| cc_object_embeds_mapped_root(&path, &prefix_maps));
if bind_on_embedded_root && !artifacts.is_empty() && matches!(embeds, Some(Ok(true))) {
path_bound = true;
} else {
let unsafe_to_store =
cc_unsafe_to_store(artifacts.is_empty(), self.key_path_bound.get(), || embeds);
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,
signaled: output.status.code().is_none(),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
pending_stderr: None,
artifacts,
keepalive,
},
path_bound,
))
}
}
fn stage_cc_stdout_artifact(bytes: &[u8]) -> Result<(Artifact, tempfile::TempPath)> {
let mut staged = tempfile::Builder::new()
.prefix("kache-cc-stdout-")
.suffix(".i")
.tempfile()
.context("cc: creating -E stdout staging file")?;
staged
.write_all(bytes)
.context("cc: writing -E stdout staging file")?;
staged
.flush()
.context("cc: flushing -E stdout staging file")?;
let temp = staged.into_temp_path();
Ok((
Artifact {
path: temp.to_path_buf(),
store_name: CC_STDOUT_STORE_NAME.to_string(),
kind: ArtifactKind::Other("stdout"),
required: true,
},
temp,
))
}
fn expand_cc_response_files(args: &[String]) -> Result<Vec<String>> {
let mut expanded = Vec::new();
for arg in args {
let Some(path) = arg.strip_prefix('@') else {
expanded.push(arg.clone());
continue;
};
let body =
fs::read_to_string(path).with_context(|| format!("reading cc response file {path}"))?;
expanded.extend(body.split_whitespace().map(str::to_string));
}
Ok(expanded)
}
fn cc_link_file_inputs(args: &[String]) -> Vec<PathBuf> {
let mut inputs = Vec::new();
let mut skip_next = false;
for arg in args {
if skip_next {
skip_next = false;
continue;
}
if arg == "-o" || arg == "-I" || arg == "-L" || arg == "-include" {
skip_next = true;
continue;
}
if arg.starts_with('-') {
continue;
}
let path = PathBuf::from(arg);
let ext = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
if matches!(
ext,
"o" | "obj" | "a" | "lib" | "so" | "dylib" | "dll" | "res" | "lo"
) {
inputs.push(path);
}
}
inputs
}
fn discover_cc_link_sidecars(output: &Path) -> Vec<Artifact> {
let Some(stem) = output.file_stem().map(|name| name.to_os_string()) else {
return Vec::new();
};
let parent = output.parent().unwrap_or_else(|| Path::new("."));
let mut sidecars = Vec::new();
for (suffix, kind) in [
(".map", ArtifactKind::Other("map")),
(".pdb", ArtifactKind::DebugSidecar),
(".lib", ArtifactKind::Library),
(".exp", ArtifactKind::Other("exp")),
] {
let mut name = stem.clone();
name.push(suffix);
let path = parent.join(name);
if std::fs::symlink_metadata(&path).is_ok_and(|meta| meta.file_type().is_file()) {
let store_name = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
sidecars.push(Artifact {
path,
kind,
store_name,
required: false,
});
}
}
let dsym = parent.join(format!(
"{}.dSYM",
output
.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default()
));
if dsym.is_dir() {
let file_name = output
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default();
sidecars.push(Artifact {
path: dsym,
kind: ArtifactKind::DebugBundle,
store_name: format!("{file_name}.dsym.tar"),
required: false,
});
}
sidecars
}
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 (kind, store_name) = match parsed.mode {
CompileMode::Compile => (ArtifactKind::Object, CC_OBJECT_STORE_NAME.to_string()),
_ => (classify_by_filename(&object_name), object_name),
};
let mut outputs = vec![Artifact {
path: object.clone(),
kind,
store_name,
required: true,
}];
if parsed.mode == CompileMode::Link {
outputs.extend(discover_cc_link_sidecars(&object));
}
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;