use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::link::LinkStrategy;
pub mod cc;
pub mod flags;
pub mod platform;
pub mod rustc;
pub use platform::Platform;
pub use crate::compile::CompileResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompilerId(&'static str);
impl CompilerId {
pub const fn new(id: &'static str) -> Self {
Self(id)
}
pub const fn as_str(self) -> &'static str {
self.0
}
}
impl std::fmt::Display for CompilerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CompilerAdapter {
id: CompilerId,
display_name: &'static str,
recognizes: fn(&[String]) -> bool,
}
impl CompilerAdapter {
pub const fn new(
id: CompilerId,
display_name: &'static str,
recognizes: fn(&[String]) -> bool,
) -> Self {
Self {
id,
display_name,
recognizes,
}
}
pub const fn id(self) -> CompilerId {
self.id
}
pub const fn display_name(self) -> &'static str {
self.display_name
}
pub fn recognizes(self, args: &[String]) -> bool {
(self.recognizes)(args)
}
}
#[derive(Debug, Clone)]
pub enum RefuseReason {
NotPrimary,
Unsupported(&'static str),
}
impl RefuseReason {
pub fn description(&self) -> &'static str {
match self {
RefuseReason::NotPrimary => "not a primary compilation",
RefuseReason::Unsupported(detail) => detail,
}
}
}
pub struct KeyCtx<'a, 'db> {
pub file_hasher: &'a crate::cache_key::FileHasher<'db>,
pub path_normalizer: &'a crate::path_normalizer::PathNormalizer,
pub cache_dir: &'a Path,
pub key_salt: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactKind {
Library,
DynamicLibrary,
Metadata,
Object,
DepInfo,
Executable,
DebugSidecar,
Other(&'static str),
}
impl ArtifactKind {
pub fn link_strategy(self) -> LinkStrategy {
match self {
ArtifactKind::Executable | ArtifactKind::DynamicLibrary => LinkStrategy::Copy,
_ => LinkStrategy::Hardlink,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Artifact {
pub path: PathBuf,
pub store_name: String,
pub kind: ArtifactKind,
pub required: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ArtifactSet {
outputs: Vec<Artifact>,
}
impl ArtifactSet {
pub fn new(outputs: Vec<Artifact>) -> Self {
Self { outputs }
}
pub fn empty() -> Self {
Self::default()
}
pub fn from_output_files(
output_files: Vec<(PathBuf, String)>,
classify: impl Fn(&str) -> ArtifactKind,
) -> Self {
Self::new(
output_files
.into_iter()
.map(|(path, store_name)| {
let kind = classify(&store_name);
Artifact {
path,
store_name,
kind,
required: true,
}
})
.collect(),
)
}
pub fn is_empty(&self) -> bool {
self.outputs.is_empty()
}
pub fn outputs(&self) -> &[Artifact] {
&self.outputs
}
pub fn store_files(&self) -> Vec<(PathBuf, String)> {
self.outputs
.iter()
.map(|artifact| (artifact.path.clone(), artifact.store_name.clone()))
.collect()
}
pub fn total_size(&self) -> u64 {
self.outputs
.iter()
.map(|artifact| {
std::fs::metadata(&artifact.path)
.map(|m| m.len())
.unwrap_or(0)
})
.sum()
}
}
pub fn classify_by_filename(name: &str) -> ArtifactKind {
let ext = std::path::Path::new(name)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"rlib" => ArtifactKind::Library,
"rmeta" => ArtifactKind::Metadata,
"d" | "pp" => ArtifactKind::DepInfo,
"o" | "obj" => ArtifactKind::Object,
"dylib" | "so" | "dll" => ArtifactKind::DynamicLibrary,
"dwo" | "pdb" | "dSYM" => ArtifactKind::DebugSidecar,
"exe" => ArtifactKind::Executable,
"" => ArtifactKind::Other("extensionless"),
_ => ArtifactKind::Other("unknown-ext"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigningPurpose {
OsLoading,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PostRestoreAction {
ExpandDepInfoPaths,
Sign(SigningPurpose),
}
pub fn plan_post_restore(kind: ArtifactKind) -> Vec<PostRestoreAction> {
let mut plan = Vec::new();
if matches!(kind, ArtifactKind::DepInfo) {
plan.push(PostRestoreAction::ExpandDepInfoPaths);
}
if matches!(
kind,
ArtifactKind::Executable | ArtifactKind::DynamicLibrary
) {
plan.push(PostRestoreAction::Sign(SigningPurpose::OsLoading));
}
plan
}
impl PostRestoreAction {
pub fn is_content_transform(self) -> bool {
match self {
PostRestoreAction::ExpandDepInfoPaths => true,
PostRestoreAction::Sign(_) => false,
}
}
pub fn transform(self, content: Vec<u8>, anchor: &std::path::Path) -> Vec<u8> {
match self {
PostRestoreAction::ExpandDepInfoPaths => {
match String::from_utf8(content) {
Ok(text) => crate::link::rewrite_depinfo_content(
&text,
anchor,
crate::link::DepInfoMode::Expand,
)
.into_bytes(),
Err(e) => e.into_bytes(),
}
}
PostRestoreAction::Sign(_) => content,
}
}
pub fn apply(&self, path: &std::path::Path, platform: &dyn Platform) -> Result<()> {
match self {
PostRestoreAction::Sign(SigningPurpose::OsLoading) => {
platform.ensure_binary_loadable(path)
}
PostRestoreAction::ExpandDepInfoPaths => {
debug_assert!(
false,
"ExpandDepInfoPaths is a content transform; route it through transform()"
);
Ok(())
}
}
}
}
pub trait Compiler {
type Parsed;
fn id(&self) -> CompilerId;
fn parse(&self, args: &[String]) -> Result<Self::Parsed>;
fn refuse_reasons(&self, parsed: &Self::Parsed) -> Vec<RefuseReason>;
fn cache_key(&self, parsed: &Self::Parsed, ctx: &KeyCtx<'_, '_>) -> Result<String>;
fn execute(&self, parsed: &Self::Parsed) -> Result<CompileResult>;
fn classify_output(&self, parsed: &Self::Parsed, name: &str) -> ArtifactKind;
}
pub const COMPILER_ADAPTERS: &[CompilerAdapter] = &[rustc::ADAPTER, cc::ADAPTER];
pub fn detect_compiler(args: &[String]) -> Option<&'static CompilerAdapter> {
COMPILER_ADAPTERS
.iter()
.find(|adapter| adapter.recognizes(args))
}
#[cfg(test)]
mod tests {
use super::*;
fn s(args: &[&str]) -> Vec<String> {
args.iter().map(|a| a.to_string()).collect()
}
#[test]
fn detect_compiler_returns_none_for_empty_argv() {
assert!(detect_compiler(&[]).is_none());
}
#[test]
fn detect_compiler_recognizes_rustc_paths() {
assert_eq!(
detect_compiler(&s(&["rustc"])).map(|adapter| adapter.id()),
Some(rustc::RUSTC_ID)
);
assert_eq!(
detect_compiler(&s(&["/usr/bin/rustc", "src/lib.rs"])).map(|adapter| adapter.id()),
Some(rustc::RUSTC_ID)
);
assert_eq!(
detect_compiler(&s(&["clippy-driver"])).map(|adapter| adapter.id()),
Some(rustc::RUSTC_ID)
);
}
#[test]
fn detect_compiler_recognizes_cc_paths() {
assert_eq!(
detect_compiler(&s(&["cc"])).map(|adapter| adapter.id()),
Some(cc::CC_ID)
);
assert_eq!(
detect_compiler(&s(&["gcc"])).map(|adapter| adapter.id()),
Some(cc::CC_ID)
);
assert_eq!(
detect_compiler(&s(&["clang++"])).map(|adapter| adapter.id()),
Some(cc::CC_ID)
);
assert_eq!(
detect_compiler(&s(&["/usr/bin/cc", "-c", "foo.c"])).map(|adapter| adapter.id()),
Some(cc::CC_ID)
);
}
#[test]
fn detect_compiler_returns_none_for_cc_probe_shape() {
assert!(detect_compiler(&s(&["-E", "/tmp/probe.c"])).is_none());
assert!(detect_compiler(&s(&["-E", "/tmp/detect_compiler_family.c"])).is_none());
}
#[test]
fn detect_compiler_returns_none_for_unrelated_argv() {
assert!(detect_compiler(&s(&["cargo", "build"])).is_none());
assert!(detect_compiler(&s(&["make"])).is_none());
assert!(detect_compiler(&s(&["ld"])).is_none());
assert!(detect_compiler(&s(&["--crate-name"])).is_none());
}
#[test]
fn plan_post_restore_dep_info_expands_paths() {
assert_eq!(
plan_post_restore(ArtifactKind::DepInfo),
vec![PostRestoreAction::ExpandDepInfoPaths]
);
}
#[test]
fn plan_post_restore_executable_signs_for_os_loading() {
assert_eq!(
plan_post_restore(ArtifactKind::Executable),
vec![PostRestoreAction::Sign(SigningPurpose::OsLoading)]
);
}
#[test]
fn plan_post_restore_dynamic_library_signs_for_os_loading() {
assert_eq!(
plan_post_restore(ArtifactKind::DynamicLibrary),
vec![PostRestoreAction::Sign(SigningPurpose::OsLoading)]
);
}
#[test]
fn plan_post_restore_object_is_empty() {
assert!(plan_post_restore(ArtifactKind::Object).is_empty());
}
#[test]
fn plan_post_restore_passive_kinds_are_empty() {
for kind in [
ArtifactKind::Library,
ArtifactKind::Metadata,
ArtifactKind::DebugSidecar,
ArtifactKind::Other("test"),
] {
assert!(
plan_post_restore(kind).is_empty(),
"{kind:?} should have no post-restore actions"
);
}
}
#[test]
fn expand_dep_info_paths_is_a_content_transform() {
assert!(PostRestoreAction::ExpandDepInfoPaths.is_content_transform());
assert!(!PostRestoreAction::Sign(SigningPurpose::OsLoading).is_content_transform());
}
#[test]
fn transform_expand_dep_info_paths_roots_relative_paths_at_anchor() {
let blob = b"__kache_root__/target/debug/foo: __kache_root__/src/lib.rs".to_vec();
let anchor = std::path::Path::new("/restored/worktree");
let out = PostRestoreAction::ExpandDepInfoPaths.transform(blob, anchor);
let content = String::from_utf8(out).unwrap();
assert!(
content.contains("/restored/worktree/target/debug/foo"),
"expected anchor-rooted target path, got: {content}"
);
assert!(
content.contains("/restored/worktree/src/lib.rs"),
"expected anchor-rooted source path, got: {content}"
);
assert!(
!content.contains("__kache_root__/"),
"no kache dep-info markers should remain, got: {content}"
);
}
#[test]
fn transform_expand_dep_info_paths_preserves_parent_relative_deps() {
let blob =
b"foo.o: ../../src/foo.cc ../include/foo.h __kache_root__/generated/header.h".to_vec();
let anchor = std::path::Path::new("/restored/worktree/obj");
let out = PostRestoreAction::ExpandDepInfoPaths.transform(blob, anchor);
let content = String::from_utf8(out).unwrap();
assert!(
content.contains("../../src/foo.cc"),
"compiler-emitted parent-relative source paths must survive: {content}"
);
assert!(
content.contains("../include/foo.h"),
"compiler-emitted parent-relative header paths must survive: {content}"
);
assert!(
content.contains("/restored/worktree/obj/generated/header.h"),
"kache sentinel paths should still expand: {content}"
);
}
#[test]
fn transform_expand_dep_info_paths_passes_through_non_utf8() {
let blob = vec![0xff, 0xfe, 0x00, 0x42];
let out = PostRestoreAction::ExpandDepInfoPaths
.transform(blob.clone(), std::path::Path::new("/anchor"));
assert_eq!(out, blob);
}
#[test]
fn apply_sign_os_loading_routes_through_platform() {
use crate::compiler::platform::tests::CountingPlatform;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("not-actually-a-binary");
std::fs::write(&path, b"definitely not Mach-O").unwrap();
let platform = CountingPlatform::new();
PostRestoreAction::Sign(SigningPurpose::OsLoading)
.apply(&path, &platform)
.expect("apply must not error even when the platform impl is a no-op");
assert_eq!(
platform.ensure_calls(),
1,
"Sign(OsLoading) must dispatch to platform.ensure_binary_loadable exactly once"
);
}
#[test]
fn rustc_classify_to_plan_chain_for_typical_lib_build() {
use crate::compiler::rustc::RustcCompiler;
let compiler = RustcCompiler::new();
let lib_args = compiler
.parse(&[
"rustc".into(),
"src/lib.rs".into(),
"--crate-name".into(),
"foo".into(),
"--crate-type".into(),
"lib".into(),
])
.unwrap();
let cases: &[(&str, Vec<PostRestoreAction>)] = &[
("libfoo-abc.rlib", vec![]),
("libfoo-abc.rmeta", vec![]),
("foo-abc.d", vec![PostRestoreAction::ExpandDepInfoPaths]),
("foo-abc.rcgu.o", vec![]),
("foo-abc.dwo", vec![]),
];
for (name, expected) in cases {
let kind = compiler.classify_output(&lib_args, name);
assert_eq!(
&plan_post_restore(kind),
expected,
"for {name}: kind = {kind:?}"
);
}
}
#[test]
fn classify_by_filename_recognizes_known_extensions() {
assert_eq!(
classify_by_filename("libfoo-abc.rlib"),
ArtifactKind::Library
);
assert_eq!(
classify_by_filename("libfoo-abc.rmeta"),
ArtifactKind::Metadata
);
assert_eq!(classify_by_filename("foo-abc.d"), ArtifactKind::DepInfo);
assert_eq!(
classify_by_filename("host_pathsub.o.pp"),
ArtifactKind::DepInfo
);
assert_eq!(classify_by_filename("foo.o"), ArtifactKind::Object);
assert_eq!(
classify_by_filename("foo-abc.123.rcgu.o"),
ArtifactKind::Object
);
assert_eq!(classify_by_filename("foo.obj"), ArtifactKind::Object);
assert_eq!(
classify_by_filename("libfoo.dylib"),
ArtifactKind::DynamicLibrary
);
assert_eq!(
classify_by_filename("libfoo.so"),
ArtifactKind::DynamicLibrary
);
assert_eq!(
classify_by_filename("foo.dll"),
ArtifactKind::DynamicLibrary
);
assert_eq!(
classify_by_filename("foo-abc.dwo"),
ArtifactKind::DebugSidecar
);
assert_eq!(classify_by_filename("foo.pdb"), ArtifactKind::DebugSidecar);
assert_eq!(classify_by_filename("foo.exe"), ArtifactKind::Executable);
}
#[test]
fn classify_by_filename_distinguishes_extensionless_from_unknown() {
match classify_by_filename("my_bin-abc123") {
ArtifactKind::Other("extensionless") => {}
other => panic!("expected Other(extensionless), got {other:?}"),
}
match classify_by_filename("foo.lock") {
ArtifactKind::Other("unknown-ext") => {}
other => panic!("expected Other(unknown-ext), got {other:?}"),
}
}
#[test]
fn rustc_classify_to_plan_chain_for_typical_bin_build() {
use crate::compiler::rustc::RustcCompiler;
let compiler = RustcCompiler::new();
let bin_args = compiler
.parse(&[
"rustc".into(),
"src/main.rs".into(),
"--crate-name".into(),
"foo".into(),
"--crate-type".into(),
"bin".into(),
])
.unwrap();
let cases: &[(&str, Vec<PostRestoreAction>)] = &[
(
"foo-abc",
vec![PostRestoreAction::Sign(SigningPurpose::OsLoading)],
),
("foo-abc.d", vec![PostRestoreAction::ExpandDepInfoPaths]),
("foo-abc.rcgu.o", vec![]),
("foo-abc.dwo", vec![]),
];
for (name, expected) in cases {
let kind = compiler.classify_output(&bin_args, name);
assert_eq!(
&plan_post_restore(kind),
expected,
"for {name}: kind = {kind:?}"
);
}
}
}