#![deny(missing_docs)]
use mbx_cache_core::{
CacheDigest, FileDigestCache, PathMapping, PathNormalizationError, canonical_json,
normalize_mapped_path,
};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsString;
use std::path::{Component, Path, PathBuf};
use thiserror::Error;
mod depfile;
pub use depfile::{CcDepfile, CcDiscoveredInputs, INCLUDE_MANIFEST_PREFIX, manifest_snapshot};
pub const ACTION_SCHEMA_VERSION: u8 = 1;
pub const ADAPTER_VERSION: u8 = 1;
pub const MAX_PREDICTED_INPUTS: usize = 16 * 1024;
pub const MAX_INPUT_BYTES: u64 = 2 * 1024 * 1024 * 1024;
pub const MAX_MANIFEST_ENTRIES: usize = 16 * 1024;
pub const KEYED_ENVIRONMENT: &[&str] = &[
"IPHONEOS_DEPLOYMENT_TARGET",
"LANG",
"LC_ALL",
"LC_MESSAGES",
"MACOSX_DEPLOYMENT_TARGET",
"SDKROOT",
"SOURCE_DATE_EPOCH",
"TVOS_DEPLOYMENT_TARGET",
"WATCHOS_DEPLOYMENT_TARGET",
"XROS_DEPLOYMENT_TARGET",
];
pub const BYPASS_ENVIRONMENT: &[&str] = &[
"CPATH",
"COMPILER_PATH",
"CPLUS_INCLUDE_PATH",
"C_INCLUDE_PATH",
"DEPENDENCIES_OUTPUT",
"GCC_EXEC_PREFIX",
"OBJC_INCLUDE_PATH",
"SUNPRO_DEPENDENCIES",
];
pub const SYSTEM_ROOTS: &[&str] = &[
"/Applications/Xcode.app",
"/Library/Developer",
"/nix/store",
"/usr/include",
"/usr/lib",
"/usr/local/include",
];
const SUPPORTED_F_FLAGS: &[&str] = &[
"PIC",
"PIE",
"asynchronous-unwind-tables",
"color-diagnostics",
"data-sections",
"diagnostics-color",
"exceptions",
"function-sections",
"merge-all-constants",
"no-asynchronous-unwind-tables",
"no-builtin",
"no-common",
"no-exceptions",
"no-omit-frame-pointer",
"no-plt",
"no-rtti",
"no-strict-aliasing",
"omit-frame-pointer",
"pic",
"pie",
"rtti",
"short-enums",
"signed-char",
"stack-protector",
"stack-protector-all",
"stack-protector-strong",
"strict-aliasing",
"unsigned-char",
"visibility",
"visibility-inlines-hidden",
"wrapv",
];
const SUPPORTED_M_FLAGS: &[&str] = &[
"32",
"64",
"arch",
"arm",
"avx",
"avx2",
"cpu",
"float-abi",
"fma",
"fpu",
"iphoneos-version-min",
"macosx-version-min",
"no-omit-leaf-frame-pointer",
"omit-leaf-frame-pointer",
"sse",
"sse2",
"sse3",
"sse4.1",
"sse4.2",
"thumb",
"tune",
];
const SUPPORTED_O_FLAGS: &[&str] = &[
"-O", "-O0", "-O1", "-O2", "-O3", "-Ofast", "-Og", "-Os", "-Oz",
];
const SUPPORTED_G_FLAGS: &[&str] = &[
"-g",
"-g0",
"-g1",
"-g2",
"-g3",
"-gdwarf-2",
"-gdwarf-3",
"-gdwarf-4",
"-gdwarf-5",
];
const SUPPORTED_BARE_FLAGS: &[&str] = &[
"-ansi",
"-nostdinc",
"-nostdinc++",
"-pedantic",
"-pedantic-errors",
"-pipe",
"-pthread",
"-w",
];
const SEPARATE_PATH_FLAGS: &[&str] = &[
"-idirafter",
"-imacros",
"-include",
"-iquote",
"-isysroot",
"-isystem",
];
const TOOL_PASSTHROUGH_FLAGS: &[&str] = &["-Xassembler", "-Xclang", "-Xlinker", "-Xpreprocessor"];
const COMPILER_QUERY_FLAGS: &[&str] = &[
"--help",
"--version",
"-###",
"-?",
"-dumpmachine",
"-dumpversion",
"-v",
];
const PREFIX_MAP_FLAGS: &[&str] = &[
"-fdebug-prefix-map",
"-ffile-prefix-map",
"-fmacro-prefix-map",
];
impl CcBypassReason {
pub fn kind(&self) -> &'static str {
self.into()
}
pub fn remediation(&self) -> Option<&'static str> {
match self {
Self::UnsupportedEnvironment(_) => Some(
"Unset the reported environment variable for this build so the compiler invocation describes all of its inputs.",
),
Self::LocalCpuTarget(_) => Some(
"Replace the reported local-CPU option with an explicit architecture or CPU name.",
),
Self::EmbeddedTimestampMacro(_) => Some(
"Remove the reported timestamp macro, or keep this compilation uncached if its changing value is intentional.",
),
Self::SearchPathModifiedDuringCompilation(_) => Some(
"Generate headers before compilation instead of changing an include directory while the compiler is running.",
),
Self::UnknownFlag(_) | Self::ToolPassthrough(_) => Some(
"Remove the reported compiler option, or upgrade mbx if the option should be modeled.",
),
Self::UnmappedAbsolutePath(_) => Some(
"Move the input under a mapped project or system root, or keep this compilation uncached.",
),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error, strum::IntoStaticStr)]
#[strum(serialize_all = "kebab-case")]
#[non_exhaustive]
pub enum CcBypassReason {
#[error("compiler argument {index} is not valid UTF-8")]
NonUtf8Argument {
index: usize,
},
#[error("compiler response file is not modeled by the cache adapter: {0}")]
ResponseFile(String),
#[error("compiler flag is not modeled by the cache adapter: {0}")]
UnknownFlag(String),
#[error("compiler flag {0} is missing its value")]
MissingValue(String),
#[error("compiler invocation queries the driver instead of compiling")]
CompilerQuery,
#[error("compiler invocation does not compile with -c")]
NotACompile,
#[error("compiler invocation emits a non-object output: {0}")]
NonObjectOutput(String),
#[error("compiler invocation reads its source from standard input")]
StandardInput,
#[error("compiler invocation names no source file")]
MissingInput,
#[error("compiler invocation names more than one source file")]
MultipleInputs,
#[error("compiler invocation names no output file")]
MissingOutput,
#[error("compiler input language is not modeled by the cache adapter: {0}")]
UnsupportedLanguage(String),
#[error("compiler invocation requests its own dependency output: {0}")]
CallerDependencyFlags(String),
#[error("precompiled headers are not modeled by the cache adapter: {0}")]
PrecompiledHeader(String),
#[error("coverage instrumentation is not modeled by the cache adapter: {0}")]
CoverageInstrumentation(String),
#[error("split debug output is not modeled by the cache adapter: {0}")]
SplitDebugOutput(String),
#[error("preserved temporaries are not modeled by the cache adapter: {0}")]
SaveTemps(String),
#[error("compiler flag forwards options to another tool: {0}")]
ToolPassthrough(String),
#[error("compiler plugins are not modeled by the cache adapter: {0}")]
Plugin(String),
#[error("include search directory changed during the compilation: {0}")]
SearchPathModifiedDuringCompilation(PathBuf),
#[error("compilation output records a path its key normalized away: {0}")]
UnportableOutput(PathBuf),
#[error("compiler flag tunes for the local CPU: {0}")]
LocalCpuTarget(String),
#[error("compiler driver is not modeled by the cache adapter: {0}")]
UnsupportedCompilerDriver(String),
#[error("could not establish compiler identity: {0}")]
CompilerIdentityUnavailable(String),
#[error("environment variable {0} changes the compilation in an unmodeled way")]
UnsupportedEnvironment(String),
#[error("no real compiler was pinned for the cc shim")]
RealCompilerUnpinned,
#[error("input expands a timestamp macro: {0}")]
EmbeddedTimestampMacro(PathBuf),
#[error("could not model the compiler depfile: {0}")]
MalformedDepfile(String),
#[error("could not read the compiler depfile {path}: {message}")]
DepfileRead {
path: PathBuf,
message: String,
},
#[error("compilation reads more inputs than the cache adapter models")]
TooManyInputs,
#[error("path is outside every modeled root: {0}")]
UnmappedAbsolutePath(PathBuf),
#[error("path is not valid UTF-8: {0}")]
NonUtf8Path(PathBuf),
#[error("compiler working directory is not absolute: {0}")]
RelativeWorkingDirectory(PathBuf),
#[error("path mapping root is not absolute: {0}")]
RelativePathMapping(PathBuf),
#[error("invalid path mapping placeholder: {0}")]
InvalidPathPlaceholder(String),
#[error("required input is missing from the discovered inputs: {0}")]
MissingRequiredInput(String),
#[error("invalid digest for input: {0}")]
InvalidInputDigest(String),
#[error("conflicting digests for input: {0}")]
ConflictingInput(String),
#[error("could not read input {path}: {message}")]
InputRead {
path: PathBuf,
message: String,
},
#[error("input changed during the compilation: {0}")]
InputChanged(PathBuf),
#[error("input was modified during the compilation: {0}")]
InputModifiedDuringCompilation(PathBuf),
#[error("discovered inputs use a different working directory")]
DiscoveryWorkingDirectory,
#[error("action prediction is not modeled by this adapter version")]
UnsupportedPrediction,
#[error("invalid predicted input: {0}")]
InvalidPredictedInput(String),
#[error("could not serialize the action descriptor: {0}")]
Serialization(String),
}
impl From<PathNormalizationError> for CcBypassReason {
fn from(reason: PathNormalizationError) -> Self {
match reason {
PathNormalizationError::UnmappedAbsolutePath(path) => Self::UnmappedAbsolutePath(path),
PathNormalizationError::NonUtf8Path(path) => Self::NonUtf8Path(path),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CcLanguage {
C,
Cxx,
}
impl CcLanguage {
pub fn shim_stem(self) -> &'static str {
match self {
Self::C => "mbx-cc",
Self::Cxx => "mbx-cxx",
}
}
pub fn default_driver(self) -> &'static str {
if cfg!(windows) {
return "cl.exe";
}
match self {
Self::C => "cc",
Self::Cxx => "c++",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CcCompilerFamily {
Gcc,
Clang,
AppleClang,
#[cfg(windows)]
Msvc,
}
impl CcCompilerFamily {
pub fn as_str(self) -> &'static str {
match self {
Self::Gcc => "gcc",
Self::Clang => "clang",
Self::AppleClang => "apple-clang",
#[cfg(windows)]
Self::Msvc => "msvc",
}
}
pub fn uses_external_assembler(self) -> bool {
matches!(self, Self::Gcc)
}
pub fn is_msvc(self) -> bool {
#[cfg(windows)]
{
matches!(self, Self::Msvc)
}
#[cfg(not(windows))]
{
false
}
}
pub fn classify(probe: &str) -> Result<Self, CcBypassReason> {
#[cfg(windows)]
if probe.contains("Microsoft (R) C/C++ Optimizing Compiler") {
return Ok(Self::Msvc);
}
if probe.contains("Apple clang version") {
Ok(Self::AppleClang)
} else if probe.contains("clang version") {
Ok(Self::Clang)
} else if probe.contains("gcc version") {
Ok(Self::Gcc)
} else {
Err(CcBypassReason::UnsupportedCompilerDriver(
probe.lines().next().unwrap_or_default().into(),
))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcCompilerIdentity {
pub family: CcCompilerFamily,
pub version_text: String,
pub target: String,
pub assembler: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcActionInput {
pub path: PathBuf,
pub digest: CacheDigest,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcActionContext {
pub compiler: CcCompilerIdentity,
pub working_dir: PathBuf,
pub path_mappings: Vec<PathMapping>,
pub environment: BTreeMap<String, Option<String>>,
pub inputs: Vec<CcActionInput>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcAction {
pub digest: CacheDigest,
pub bytes: Vec<u8>,
}
#[derive(Debug, Serialize)]
struct CcCompilerDescriptor {
assembler: String,
family: String,
target: String,
version_text: String,
}
#[derive(Debug, Serialize)]
struct CcInputDescriptor {
digest: CacheDigest,
path: String,
}
#[derive(Debug, Serialize)]
struct CcActionDescriptor {
version: u8,
kind: &'static str,
adapter_version: u8,
compiler: CcCompilerDescriptor,
arguments: Vec<String>,
environment: BTreeMap<String, Option<String>>,
inputs: Vec<CcInputDescriptor>,
}
#[derive(Debug, Serialize)]
struct CcInvocationDescriptor {
version: u8,
kind: &'static str,
adapter_version: u8,
compiler: CcCompilerDescriptor,
arguments: Vec<String>,
required_inputs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CcInputPrediction {
pub version: u8,
pub inputs: Vec<String>,
pub environment: Vec<String>,
#[serde(default, skip_serializing_if = "is_zero")]
pub compiler_duration_ns: u64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub source_name: String,
}
fn is_zero(value: &u64) -> bool {
*value == 0
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum Argument {
Plain(String),
Path { flag: String, path: PathBuf },
PrefixMap {
flag: String,
from: PathBuf,
to: String,
},
Source(PathBuf),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CcInvocation {
arguments: Vec<Argument>,
source: PathBuf,
output: PathBuf,
include_dirs: Vec<PathBuf>,
required_inputs: Vec<PathBuf>,
language: CcLanguage,
sysroot: Option<PathBuf>,
}
impl CcInvocation {
pub fn parse(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
Parser::new(arguments).parse()
}
pub fn parse_for(
arguments: &[OsString],
family: CcCompilerFamily,
) -> Result<Self, CcBypassReason> {
if family.is_msvc() {
MsvcParser::new(arguments).parse()
} else {
Self::parse(arguments)
}
}
pub fn parse_msvc(arguments: &[OsString]) -> Result<Self, CcBypassReason> {
MsvcParser::new(arguments).parse()
}
pub fn source(&self) -> &Path {
&self.source
}
pub fn output(&self) -> &Path {
&self.output
}
pub fn include_dirs(&self) -> &[PathBuf] {
&self.include_dirs
}
pub fn required_inputs(&self) -> &[PathBuf] {
&self.required_inputs
}
pub fn language(&self) -> CcLanguage {
self.language
}
pub fn sysroot(&self) -> Option<&Path> {
self.sysroot.as_deref()
}
pub fn source_name(&self) -> String {
self.source
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_default()
}
pub fn dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
vec!["-MD".into(), "-MF".into(), depfile.into()]
}
pub fn dependency_arguments_for(
&self,
depfile: &Path,
family: CcCompilerFamily,
) -> Vec<OsString> {
if family.is_msvc() {
vec!["/sourceDependencies".into(), depfile.into()]
} else {
self.dependency_arguments(depfile)
}
}
pub fn msvc_dependency_arguments(&self, depfile: &Path) -> Vec<OsString> {
vec!["/sourceDependencies".into(), depfile.into()]
}
pub fn invocation_digest(
&self,
context: &CcActionContext,
) -> Result<CacheDigest, CcBypassReason> {
let builder = ActionBuilder::new(self, context.clone());
let descriptor = builder.invocation_descriptor()?;
let bytes = canonical_json(&descriptor)
.map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
Ok(CacheDigest::blake3(&bytes))
}
pub fn action(&self, context: CcActionContext) -> Result<CcAction, CcBypassReason> {
ActionBuilder::new(self, context).build()
}
pub fn prediction(
&self,
context: &CcActionContext,
compiler_duration_ns: u64,
) -> Result<CcInputPrediction, CcBypassReason> {
let builder = ActionBuilder::new(self, context.clone());
let mut inputs = context
.inputs
.iter()
.map(|input| builder.normalize_input_path(&input.path))
.collect::<Result<Vec<_>, _>>()?;
inputs.sort();
inputs.dedup();
Ok(CcInputPrediction {
version: 1,
inputs,
environment: context.environment.keys().cloned().collect(),
compiler_duration_ns,
source_name: self.source_name(),
})
}
}
impl CcInputPrediction {
pub fn discover(
&self,
working_dir: &Path,
path_mappings: &[PathMapping],
digests: &dyn FileDigestCache,
) -> Result<CcDiscoveredInputs, CcBypassReason> {
if self.version != 1 {
return Err(CcBypassReason::UnsupportedPrediction);
}
if self.inputs.len() > MAX_PREDICTED_INPUTS || self.environment.len() > 4 * 1024 {
return Err(CcBypassReason::UnsupportedPrediction);
}
let mappings = PathMapping::ordered(path_mappings);
let mut files = BTreeSet::new();
let mut directories = BTreeSet::new();
for entry in &self.inputs {
match entry.strip_prefix(INCLUDE_MANIFEST_PREFIX) {
Some(directory) => {
directories.insert(denormalize_path(directory, &mappings)?);
}
None => {
files.insert(denormalize_path(entry, &mappings)?);
}
}
}
CcDiscoveredInputs::collect(working_dir, files, directories, digests)
}
}
fn denormalize_path(value: &str, mappings: &[PathMapping]) -> Result<PathBuf, CcBypassReason> {
for mapping in mappings {
let prefix = format!("${{{}}}", mapping.placeholder);
let suffix = if value == prefix {
""
} else if let Some(suffix) = value.strip_prefix(&format!("{prefix}/")) {
suffix
} else {
continue;
};
if !mapping.root.is_absolute() || !safe_suffix(suffix) {
return Err(CcBypassReason::InvalidPredictedInput(value.into()));
}
let mut path = normalize_components(&mapping.root);
path.extend(suffix.split('/').filter(|component| !component.is_empty()));
return Ok(path);
}
let path = PathBuf::from(value);
if path.is_absolute() && is_system_path(&path) && normalize_components(&path) == path {
return Ok(path);
}
Err(CcBypassReason::InvalidPredictedInput(value.into()))
}
fn safe_suffix(suffix: &str) -> bool {
suffix.is_empty()
|| !suffix.split('/').any(|component| {
component.is_empty() || matches!(component, "." | "..") || component.contains('\\')
})
}
pub fn is_system_path(path: &Path) -> bool {
SYSTEM_ROOTS
.iter()
.any(|root| path.starts_with(Path::new(root)))
}
fn normalize_components(path: &Path) -> PathBuf {
let mut normalized = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
normalized.pop();
}
component => normalized.push(component.as_os_str()),
}
}
normalized
}
pub fn environment_inputs<F>(
lookup: F,
sysroot: Option<&Path>,
) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
where
F: Fn(&str) -> Option<String>,
{
environment_inputs_for(lookup, sysroot, CcCompilerFamily::Clang)
}
pub fn environment_inputs_for<F>(
lookup: F,
sysroot: Option<&Path>,
family: CcCompilerFamily,
) -> Result<BTreeMap<String, Option<String>>, CcBypassReason>
where
F: Fn(&str) -> Option<String>,
{
for name in BYPASS_ENVIRONMENT {
if lookup(name).is_some() {
return Err(CcBypassReason::UnsupportedEnvironment((*name).into()));
}
}
let mut environment = BTreeMap::new();
for name in KEYED_ENVIRONMENT {
if *name == "SDKROOT" && sysroot.is_some() {
continue;
}
environment.insert((*name).to_string(), lookup(name));
}
if family.is_msvc() {
for name in [
"INCLUDE",
"VCToolsVersion",
"WindowsSDKVersion",
"UCRTVersion",
] {
environment.insert(name.into(), lookup(name));
}
for name in ["CL", "_CL_"] {
if lookup(name).is_some() {
return Err(CcBypassReason::UnsupportedEnvironment(name.into()));
}
}
}
Ok(environment)
}
struct ActionBuilder<'a> {
invocation: &'a CcInvocation,
context: CcActionContext,
mappings: Vec<PathMapping>,
}
impl<'a> ActionBuilder<'a> {
fn new(invocation: &'a CcInvocation, mut context: CcActionContext) -> Self {
context.path_mappings = PathMapping::ordered(&context.path_mappings);
let mappings = context.path_mappings.clone();
Self {
invocation,
context,
mappings,
}
}
fn build(self) -> Result<CcAction, CcBypassReason> {
self.validate_mappings()?;
let invocation = self.invocation_descriptor()?;
let mut inputs = BTreeMap::<String, CacheDigest>::new();
for input in &self.context.inputs {
input.digest.validate().map_err(|_| {
CcBypassReason::InvalidInputDigest(input.path.display().to_string())
})?;
let path = self.normalize_input_path(&input.path)?;
if inputs
.insert(path.clone(), input.digest.clone())
.is_some_and(|existing| existing != input.digest)
{
return Err(CcBypassReason::ConflictingInput(path));
}
}
let required = self
.invocation
.required_inputs
.iter()
.map(|path| self.normalize_path(path))
.collect::<Result<BTreeSet<_>, _>>()?;
if let Some(missing) = required.iter().find(|path| !inputs.contains_key(*path)) {
return Err(CcBypassReason::MissingRequiredInput(missing.clone()));
}
let inputs = inputs
.into_iter()
.map(|(path, digest)| CcInputDescriptor { path, digest })
.collect();
let descriptor = CcActionDescriptor {
version: ACTION_SCHEMA_VERSION,
kind: "cc",
adapter_version: ADAPTER_VERSION,
compiler: invocation.compiler,
arguments: invocation.arguments,
environment: self.context.environment.clone(),
inputs,
};
let bytes = canonical_json(&descriptor)
.map_err(|error| CcBypassReason::Serialization(error.to_string()))?;
let digest = CacheDigest::blake3(&bytes);
Ok(CcAction { digest, bytes })
}
fn invocation_descriptor(&self) -> Result<CcInvocationDescriptor, CcBypassReason> {
self.validate_mappings()?;
let arguments = self
.invocation
.arguments
.iter()
.map(|argument| self.normalize_argument(argument))
.collect::<Result<Vec<_>, _>>()?;
let required_inputs = self
.invocation
.required_inputs
.iter()
.map(|path| self.normalize_path(path))
.collect::<Result<BTreeSet<_>, _>>()?
.into_iter()
.collect();
Ok(CcInvocationDescriptor {
version: ACTION_SCHEMA_VERSION,
kind: "cc",
adapter_version: ADAPTER_VERSION,
compiler: self.compiler_descriptor(),
arguments,
required_inputs,
})
}
fn compiler_descriptor(&self) -> CcCompilerDescriptor {
CcCompilerDescriptor {
assembler: self.context.compiler.assembler.clone(),
family: self.context.compiler.family.as_str().into(),
target: self.context.compiler.target.clone(),
version_text: self.context.compiler.version_text.clone(),
}
}
fn validate_mappings(&self) -> Result<(), CcBypassReason> {
if !self.context.working_dir.is_absolute() {
return Err(CcBypassReason::RelativeWorkingDirectory(
self.context.working_dir.clone(),
));
}
let mut roots = BTreeSet::new();
let mut placeholders = BTreeSet::new();
for mapping in &self.mappings {
if !mapping.root.is_absolute() {
return Err(CcBypassReason::RelativePathMapping(mapping.root.clone()));
}
if mapping.placeholder.is_empty()
|| !mapping
.placeholder
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
|| !roots.insert(normalize_components(&mapping.root))
|| !placeholders.insert(&mapping.placeholder)
{
return Err(CcBypassReason::InvalidPathPlaceholder(
mapping.placeholder.clone(),
));
}
}
Ok(())
}
fn normalize_argument(&self, argument: &Argument) -> Result<String, CcBypassReason> {
match argument {
Argument::Plain(value) => Ok(value.clone()),
Argument::Path { flag, path } => Ok(format!("{flag}={}", self.normalize_path(path)?)),
Argument::PrefixMap { flag, from, to } => {
Ok(format!("{flag}={}={to}", self.normalize_path(from)?))
}
Argument::Source(path) => Ok(self.normalize_path(path)?),
}
}
fn normalize_path(&self, path: &Path) -> Result<String, CcBypassReason> {
match normalize_mapped_path(path, &self.context.working_dir, &self.mappings) {
Ok(normalized) => Ok(normalized),
Err(reason) => {
let absolute = absolute_path(path, &self.context.working_dir);
if is_system_path(&absolute) {
return absolute
.to_str()
.map(ToOwned::to_owned)
.ok_or_else(|| CcBypassReason::NonUtf8Path(absolute.clone()));
}
Err(reason.into())
}
}
}
fn normalize_input_path(&self, path: &Path) -> Result<String, CcBypassReason> {
match path.to_str().and_then(|path| {
path.strip_prefix(INCLUDE_MANIFEST_PREFIX)
.map(ToOwned::to_owned)
}) {
Some(directory) => Ok(format!(
"{INCLUDE_MANIFEST_PREFIX}{}",
self.normalize_path(Path::new(&directory))?
)),
None => self.normalize_path(path),
}
}
}
fn absolute_path(path: &Path, working_dir: &Path) -> PathBuf {
if path.is_absolute() {
normalize_components(path)
} else {
normalize_components(&working_dir.join(path))
}
}
struct Parser<'a> {
arguments: &'a [OsString],
index: usize,
parsed: Vec<Argument>,
source: Option<PathBuf>,
output: Option<PathBuf>,
include_dirs: Vec<PathBuf>,
required_inputs: Vec<PathBuf>,
sysroot: Option<PathBuf>,
explicit_language: Option<CcLanguage>,
compiling: bool,
}
impl<'a> Parser<'a> {
fn new(arguments: &'a [OsString]) -> Self {
Self {
arguments,
index: 0,
parsed: Vec::new(),
source: None,
output: None,
include_dirs: Vec::new(),
required_inputs: Vec::new(),
sysroot: None,
explicit_language: None,
compiling: false,
}
}
fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
while self.index < self.arguments.len() {
let value = self.current()?.to_string();
self.index += 1;
if value == "-" {
return Err(CcBypassReason::StandardInput);
}
if let Some(argfile) = value.strip_prefix('@') {
return Err(CcBypassReason::ResponseFile(argfile.into()));
}
if value.starts_with('-') {
self.parse_flag(&value)?;
} else {
self.parse_input(&value)?;
}
}
if !self.compiling {
return Err(CcBypassReason::NotACompile);
}
let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
let language = self.language(&source)?;
self.required_inputs.push(source.clone());
Ok(CcInvocation {
arguments: self.parsed,
source,
output,
include_dirs: self.include_dirs,
required_inputs: self.required_inputs,
language,
sysroot: self.sysroot,
})
}
fn language(&self, source: &Path) -> Result<CcLanguage, CcBypassReason> {
if let Some(language) = self.explicit_language {
return Ok(language);
}
let extension = source
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default();
match extension {
"c" => Ok(CcLanguage::C),
"cc" | "cpp" | "cxx" | "c++" => Ok(CcLanguage::Cxx),
_ => Err(CcBypassReason::UnsupportedLanguage(
source.display().to_string(),
)),
}
}
fn current(&self) -> Result<&str, CcBypassReason> {
self.arguments[self.index]
.to_str()
.ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
}
fn take_value(&mut self, flag: &str, inline: Option<&str>) -> Result<String, CcBypassReason> {
if let Some(value) = inline
&& !value.is_empty()
{
return Ok(value.into());
}
if self.index >= self.arguments.len() {
return Err(CcBypassReason::MissingValue(flag.into()));
}
let value = self.current()?.to_string();
self.index += 1;
Ok(value)
}
fn parse_input(&mut self, value: &str) -> Result<(), CcBypassReason> {
if self.source.is_some() {
return Err(CcBypassReason::MultipleInputs);
}
let path = PathBuf::from(value);
if self.explicit_language.is_none() {
let extension = path
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default();
if !matches!(extension, "c" | "cc" | "cpp" | "cxx" | "c++") {
return Err(CcBypassReason::UnsupportedLanguage(value.into()));
}
}
self.source = Some(path.clone());
self.parsed.push(Argument::Source(path));
Ok(())
}
fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
if COMPILER_QUERY_FLAGS.contains(&value) || value.starts_with("-print") {
return Err(CcBypassReason::CompilerQuery);
}
if matches!(value, "-E" | "-S") {
return Err(CcBypassReason::NonObjectOutput(value.into()));
}
if value.starts_with("-M") {
return Err(CcBypassReason::CallerDependencyFlags(value.into()));
}
if value.starts_with("-save-temps") {
return Err(CcBypassReason::SaveTemps(value.into()));
}
if value == "--coverage" {
return Err(CcBypassReason::CoverageInstrumentation(value.into()));
}
if TOOL_PASSTHROUGH_FLAGS.contains(&value)
|| value.starts_with("-Wp,")
|| value.starts_with("-Wa,")
|| value.starts_with("-Wl,")
{
return Err(CcBypassReason::ToolPassthrough(value.into()));
}
if value.starts_with("-include-pch") || value == "-emit-pch" {
return Err(CcBypassReason::PrecompiledHeader(value.into()));
}
if value == "-c" {
self.compiling = true;
self.parsed.push(Argument::Plain(value.into()));
return Ok(());
}
if SUPPORTED_BARE_FLAGS.contains(&value)
|| SUPPORTED_O_FLAGS.contains(&value)
|| SUPPORTED_G_FLAGS.contains(&value)
|| value.starts_with("-std=")
{
self.parsed.push(Argument::Plain(value.into()));
return Ok(());
}
if let Some(rest) = value.strip_prefix("-o") {
let path = self.take_value("-o", Some(rest))?;
self.output = Some(PathBuf::from(&path));
self.parsed.push(Argument::Path {
flag: "-o".into(),
path: PathBuf::from(path),
});
return Ok(());
}
if let Some(rest) = value.strip_prefix("-I") {
let path = PathBuf::from(self.take_value("-I", Some(rest))?);
self.include_dirs.push(path.clone());
self.parsed.push(Argument::Path {
flag: "-I".into(),
path,
});
return Ok(());
}
if SEPARATE_PATH_FLAGS.contains(&value) {
let path = PathBuf::from(self.take_value(value, None)?);
match value {
"-isystem" | "-iquote" | "-idirafter" => self.include_dirs.push(path.clone()),
"-isysroot" => self.sysroot = Some(path.clone()),
_ => {}
}
self.parsed.push(Argument::Path {
flag: value.into(),
path,
});
return Ok(());
}
if let Some(rest) = value.strip_prefix("--include=") {
let path = PathBuf::from(rest);
self.parsed.push(Argument::Path {
flag: "-include".into(),
path,
});
return Ok(());
}
if let Some((flag, rest)) = PREFIX_MAP_FLAGS.iter().find_map(|flag| {
value
.strip_prefix(&format!("{flag}="))
.map(|rest| (*flag, rest))
}) {
let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
self.parsed.push(Argument::PrefixMap {
flag: flag.into(),
from: PathBuf::from(from),
to: to.into(),
});
return Ok(());
}
if value == "--param" {
let parameter = self.take_value("--param", None)?;
self.parsed
.push(Argument::Plain(format!("--param={parameter}")));
return Ok(());
}
if let Some(parameter) = value.strip_prefix("--param=") {
self.parsed
.push(Argument::Plain(format!("--param={parameter}")));
return Ok(());
}
if let Some(rest) = value.strip_prefix("--sysroot=") {
let path = PathBuf::from(rest);
self.sysroot = Some(path.clone());
self.parsed.push(Argument::Path {
flag: "--sysroot".into(),
path,
});
return Ok(());
}
if let Some(rest) = value
.strip_prefix("-D")
.or_else(|| value.strip_prefix("-U"))
{
let flag = &value[..2];
let definition = self.take_value(flag, Some(rest))?;
self.parsed
.push(Argument::Plain(format!("{flag}{definition}")));
return Ok(());
}
if let Some(rest) = value.strip_prefix("-x") {
let language = self.take_value("-x", Some(rest))?;
self.explicit_language = Some(match language.as_str() {
"c" => CcLanguage::C,
"c++" => CcLanguage::Cxx,
other => return Err(CcBypassReason::UnsupportedLanguage(other.into())),
});
self.parsed.push(Argument::Plain(format!("-x{language}")));
return Ok(());
}
if let Some(target) = value.strip_prefix("--target=") {
self.parsed
.push(Argument::Plain(format!("--target={target}")));
return Ok(());
}
if value == "-target" {
let target = self.take_value("-target", None)?;
self.parsed
.push(Argument::Plain(format!("--target={target}")));
return Ok(());
}
if value == "-arch" {
let arch = self.take_value("-arch", None)?;
self.parsed.push(Argument::Plain(format!("-arch={arch}")));
return Ok(());
}
if let Some(option) = value.strip_prefix("-f") {
return self.parse_f_flag(value, option);
}
if let Some(option) = value.strip_prefix("-m") {
return self.parse_m_flag(value, option);
}
if value.starts_with("-g") {
return Err(if value.starts_with("-gsplit-dwarf") {
CcBypassReason::SplitDebugOutput(value.into())
} else {
CcBypassReason::UnknownFlag(value.into())
});
}
if value.starts_with("-W") {
self.parsed.push(Argument::Plain(value.into()));
return Ok(());
}
Err(CcBypassReason::UnknownFlag(value.into()))
}
fn parse_f_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
if option.starts_with("plugin") || option.starts_with("pass-plugin") {
return Err(CcBypassReason::Plugin(value.into()));
}
if option.starts_with("profile-") || option == "test-coverage" {
return Err(CcBypassReason::CoverageInstrumentation(value.into()));
}
let name = option.split_once('=').map_or(option, |(name, _)| name);
if SUPPORTED_F_FLAGS.binary_search(&name).is_err() {
return Err(CcBypassReason::UnknownFlag(value.into()));
}
self.parsed.push(Argument::Plain(value.into()));
Ok(())
}
fn parse_m_flag(&mut self, value: &str, option: &str) -> Result<(), CcBypassReason> {
if option == "llvm" {
return Err(CcBypassReason::ToolPassthrough(value.into()));
}
if let Some((name, selection)) = option.split_once('=')
&& matches!(name, "arch" | "cpu" | "tune")
&& matches!(selection, "native" | "host")
{
return Err(CcBypassReason::LocalCpuTarget(value.into()));
}
let name = option.split_once('=').map_or(option, |(name, _)| name);
if SUPPORTED_M_FLAGS.binary_search(&name).is_err() {
return Err(CcBypassReason::UnknownFlag(value.into()));
}
self.parsed.push(Argument::Plain(value.into()));
Ok(())
}
}
struct MsvcParser<'a> {
arguments: &'a [OsString],
index: usize,
parsed: Vec<Argument>,
source: Option<PathBuf>,
output: Option<PathBuf>,
include_dirs: Vec<PathBuf>,
required_inputs: Vec<PathBuf>,
explicit_language: Option<CcLanguage>,
compiling: bool,
}
impl<'a> MsvcParser<'a> {
fn new(arguments: &'a [OsString]) -> Self {
Self {
arguments,
index: 0,
parsed: Vec::new(),
source: None,
output: None,
include_dirs: Vec::new(),
required_inputs: Vec::new(),
explicit_language: None,
compiling: false,
}
}
fn parse(mut self) -> Result<CcInvocation, CcBypassReason> {
while self.index < self.arguments.len() {
let value = self.current()?.to_owned();
self.index += 1;
if let Some(file) = value.strip_prefix('@') {
return Err(CcBypassReason::ResponseFile(file.into()));
}
if value.starts_with('/') || value.starts_with('-') {
self.parse_flag(&value)?;
} else {
self.add_source(&value)?;
}
}
if !self.compiling {
return Err(CcBypassReason::NotACompile);
}
let source = self.source.clone().ok_or(CcBypassReason::MissingInput)?;
let output = self.output.clone().ok_or(CcBypassReason::MissingOutput)?;
let language = self.explicit_language.unwrap_or_else(|| {
if source
.extension()
.and_then(|value| value.to_str())
.is_some_and(|value| value.eq_ignore_ascii_case("c"))
{
CcLanguage::C
} else {
CcLanguage::Cxx
}
});
self.required_inputs.push(source.clone());
Ok(CcInvocation {
arguments: self.parsed,
source,
output,
include_dirs: self.include_dirs,
required_inputs: self.required_inputs,
language,
sysroot: None,
})
}
fn current(&self) -> Result<&str, CcBypassReason> {
self.arguments[self.index]
.to_str()
.ok_or(CcBypassReason::NonUtf8Argument { index: self.index })
}
fn value(&mut self, flag: &str, attached: &str) -> Result<String, CcBypassReason> {
if !attached.is_empty() {
return Ok(attached.into());
}
if self.index == self.arguments.len() {
return Err(CcBypassReason::MissingValue(flag.into()));
}
let value = self.current()?.to_owned();
self.index += 1;
Ok(value)
}
fn add_source(&mut self, value: &str) -> Result<(), CcBypassReason> {
if self.source.is_some() {
return Err(CcBypassReason::MultipleInputs);
}
let path = PathBuf::from(value);
if self.explicit_language.is_none()
&& !path
.extension()
.and_then(|value| value.to_str())
.is_some_and(|value| {
matches!(
value.to_ascii_lowercase().as_str(),
"c" | "cc" | "cpp" | "cxx"
)
})
{
return Err(CcBypassReason::UnsupportedLanguage(value.into()));
}
self.source = Some(path.clone());
self.parsed.push(Argument::Source(path));
Ok(())
}
fn parse_flag(&mut self, value: &str) -> Result<(), CcBypassReason> {
let option = value.trim_start_matches(['/', '-']);
let lower = option.to_ascii_lowercase();
if matches!(lower.as_str(), "?" | "help") {
return Err(CcBypassReason::CompilerQuery);
}
if lower == "showincludes" || lower.starts_with("sourcedependencies") {
return Err(CcBypassReason::CallerDependencyFlags(value.into()));
}
if matches!(lower.as_str(), "e" | "ep" | "p") {
return Err(CcBypassReason::NonObjectOutput(value.into()));
}
if (lower.starts_with("fa") && !lower.starts_with("favor:"))
|| lower.starts_with("fd")
|| lower.starts_with("zi")
{
return Err(CcBypassReason::SplitDebugOutput(value.into()));
}
if lower.starts_with("yc")
|| lower.starts_with("yu")
|| (lower.starts_with("fp") && !lower.starts_with("fp:"))
{
return Err(CcBypassReason::PrecompiledHeader(value.into()));
}
if lower == "link" || lower.starts_with("bt+") || lower.starts_with("analyze") {
return Err(CcBypassReason::ToolPassthrough(value.into()));
}
if matches!(lower.as_str(), "ld" | "ldd") {
return Err(CcBypassReason::NotACompile);
}
if lower == "c" {
self.compiling = true;
self.parsed.push(Argument::Plain("/c".into()));
return Ok(());
}
for (prefix, canonical) in [("Fo", "/Fo"), ("I", "/I"), ("FI", "/FI")] {
if let Some(attached) = option.strip_prefix(prefix) {
let path = PathBuf::from(self.value(canonical, attached)?);
if prefix == "Fo" {
self.output = Some(path.clone());
} else if prefix == "I" {
self.include_dirs.push(path.clone());
}
self.parsed.push(Argument::Path {
flag: canonical.into(),
path,
});
return Ok(());
}
}
if lower.starts_with("external:i") {
let path = PathBuf::from(self.value("/external:I", &option[10..])?);
self.include_dirs.push(path.clone());
self.parsed.push(Argument::Path {
flag: "/external:I".into(),
path,
});
return Ok(());
}
if option.starts_with("Tc") || option.starts_with("Tp") {
let c = option.starts_with("Tc");
let path = self.value(if c { "/Tc" } else { "/Tp" }, &option[2..])?;
self.explicit_language = Some(if c { CcLanguage::C } else { CcLanguage::Cxx });
return self.add_source(&path);
}
if lower.starts_with("pathmap:") {
let rest = &option[8..];
let (from, to) = rest.split_once('=').unwrap_or((rest, ""));
self.parsed.push(Argument::PrefixMap {
flag: "/pathmap".into(),
from: PathBuf::from(from),
to: to.into(),
});
return Ok(());
}
if matches!(option, "D" | "U") {
let definition = self.value(value, "")?;
self.parsed
.push(Argument::Plain(format!("/{option}{definition}")));
return Ok(());
}
let definition = option.starts_with('D') || option.starts_with('U');
let warning = matches!(lower.as_str(), "wall" | "wx" | "wx-")
|| lower
.strip_prefix('w')
.is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
|| ["wd", "we", "wo"].iter().any(|prefix| {
lower
.strip_prefix(prefix)
.is_some_and(|value| value.bytes().all(|byte| byte.is_ascii_digit()))
});
let admitted = definition
|| warning
|| lower.starts_with("std:")
|| lower.starts_with("arch:")
|| lower.starts_with("favor:")
|| lower.starts_with("volatile:")
|| lower.starts_with("fp:")
|| lower.starts_with("eh")
|| lower.starts_with('o')
|| lower.starts_with("ob")
|| lower.starts_with("oi")
|| lower.starts_with("ot")
|| lower.starts_with("oy")
|| lower.starts_with("gs")
|| lower.starts_with("gr")
|| lower.starts_with("gy")
|| lower.starts_with("gw")
|| lower.starts_with("gl")
|| lower.starts_with("zc:")
|| lower.starts_with("diagnostics:")
|| matches!(
lower.as_str(),
"nologo"
| "brepro"
| "bigobj"
| "utf-8"
| "permissive-"
| "z7"
| "md"
| "mdd"
| "mt"
| "mtd"
);
if admitted {
self.parsed.push(Argument::Plain(value.into()));
return Ok(());
}
Err(CcBypassReason::UnknownFlag(value.into()))
}
}
#[cfg(test)]
#[path = "cc_cache_tests.rs"]
mod tests;