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 => "query / probe (--print, -vV)",
RefuseReason::Unsupported(detail) => detail,
}
}
pub fn category(&self) -> &'static str {
match self {
RefuseReason::NotPrimary => "not-a-compile",
RefuseReason::Unsupported(_) => "unsupported",
}
}
}
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>,
pub key_env_vars: &'a [String],
pub extra_inputs_digest: Option<&'a str>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactKind {
Library,
DynamicLibrary,
Metadata,
Object,
DepInfo,
Executable,
WasmModule,
DebugSidecar,
DebugBundle,
Other(&'static str),
}
impl ArtifactKind {
pub fn link_strategy(self) -> LinkStrategy {
match self {
ArtifactKind::Executable | ArtifactKind::DynamicLibrary | ArtifactKind::WasmModule => {
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 push(&mut self, artifact: Artifact) {
self.outputs.push(artifact);
}
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 {
if name.ends_with(".dsym.tar") {
return ArtifactKind::DebugBundle;
}
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,
"wasm" => ArtifactKind::WasmModule,
"dwo" | "pdb" | "dSYM" => ArtifactKind::DebugSidecar,
"exe" => ArtifactKind::Executable,
"" => ArtifactKind::Other("extensionless"),
_ => ArtifactKind::Other("unknown-ext"),
}
}
pub const GATED_EMIT_KINDS: [&str; 8] = [
"link", "metadata", "obj", "dep-info", "asm", "llvm-ir", "llvm-bc", "mir",
];
pub fn emit_kind_for_filename(name: &str) -> Option<&'static str> {
let ext = std::path::Path::new(name)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
match ext {
"rlib" | "so" | "dylib" | "dll" | "exe" | "a" | "lib" | "wasm" => Some("link"),
"rmeta" => Some("metadata"),
"o" | "obj" => Some("obj"),
"d" | "pp" => Some("dep-info"),
"s" | "asm" => Some("asm"),
"ll" => Some("llvm-ir"),
"bc" => Some("llvm-bc"),
"mir" => Some("mir"),
"" => Some("link"),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SigningPurpose {
OsLoading,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PostRestoreAction {
ExpandDepInfoPaths,
Sign(SigningPurpose),
UnpackDebugBundle,
}
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));
}
if matches!(kind, ArtifactKind::DebugBundle) {
plan.push(PostRestoreAction::UnpackDebugBundle);
}
plan
}
impl PostRestoreAction {
pub fn is_content_transform(self) -> bool {
match self {
PostRestoreAction::ExpandDepInfoPaths => true,
PostRestoreAction::Sign(_) => false,
PostRestoreAction::UnpackDebugBundle => 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,
PostRestoreAction::UnpackDebugBundle => 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::UnpackDebugBundle => unpack_debug_bundle(path),
PostRestoreAction::ExpandDepInfoPaths => {
debug_assert!(
false,
"ExpandDepInfoPaths is a content transform; route it through transform()"
);
Ok(())
}
}
}
}
const MAX_DEBUG_BUNDLE_BYTES: u64 = 2_147_483_648;
fn unpack_debug_bundle(tar_path: &std::path::Path) -> Result<()> {
unpack_debug_bundle_with_cap(tar_path, MAX_DEBUG_BUNDLE_BYTES)
}
fn unpack_debug_bundle_with_cap(tar_path: &std::path::Path, max_bytes: u64) -> Result<()> {
use anyhow::Context as _;
let file_name = tar_path
.file_name()
.and_then(|n| n.to_str())
.with_context(|| {
format!(
"debug bundle has no usable file name: {}",
tar_path.display()
)
})?;
let stem = file_name
.strip_suffix(".dsym.tar")
.with_context(|| format!("debug bundle artifact is not a `.dsym.tar`: {}", file_name))?;
let parent = tar_path
.parent()
.with_context(|| format!("debug bundle has no parent dir: {}", tar_path.display()))?;
let bundle_dir = parent.join(format!("{stem}.dSYM"));
let tmp_dir = tempfile::Builder::new()
.prefix(".kache-dsym-")
.tempdir_in(parent)
.context("creating temp dir for debug bundle unpack")?;
let file = std::fs::File::open(tar_path)
.with_context(|| format!("opening debug bundle {}", tar_path.display()))?;
let mut archive = tar::Archive::new(file);
let mut total_bytes = 0u64;
for entry in archive.entries().context("reading debug bundle tar")? {
let mut entry = entry.context("reading debug bundle tar entry")?;
total_bytes = total_bytes.saturating_add(entry.size());
if total_bytes > max_bytes {
anyhow::bail!(
"debug bundle exceeds the {max_bytes}-byte extraction cap \
(corrupt or hostile archive)"
);
}
let path = entry
.path()
.context("debug bundle entry path")?
.to_path_buf();
if matches!(
path.components().next(),
Some(std::path::Component::RootDir | std::path::Component::Prefix(_))
) {
anyhow::bail!("debug bundle entry has absolute path: {}", path.display());
}
if path
.components()
.any(|c| c == std::path::Component::ParentDir)
{
anyhow::bail!("debug bundle entry has path traversal: {}", path.display());
}
let entry_type = entry.header().entry_type();
if entry_type.is_symlink() || entry_type.is_hard_link() {
anyhow::bail!(
"debug bundle entry is a link (rejected): {}",
path.display()
);
}
let dest = tmp_dir.path().join(&path);
if entry_type.is_dir() {
std::fs::create_dir_all(&dest)
.with_context(|| format!("creating {}", dest.display()))?;
continue;
}
if let Some(dir) = dest.parent() {
std::fs::create_dir_all(dir).with_context(|| format!("creating {}", dir.display()))?;
}
entry
.unpack(&dest)
.with_context(|| format!("unpacking debug bundle entry {}", path.display()))?;
}
if bundle_dir.symlink_metadata().is_ok() {
if bundle_dir.is_dir() {
std::fs::remove_dir_all(&bundle_dir)
.with_context(|| format!("removing stale bundle {}", bundle_dir.display()))?;
} else {
std::fs::remove_file(&bundle_dir)
.with_context(|| format!("removing stale bundle {}", bundle_dir.display()))?;
}
}
let tmp_path = tmp_dir.keep();
std::fs::rename(&tmp_path, &bundle_dir).with_context(|| {
format!(
"publishing debug bundle {} -> {}",
tmp_path.display(),
bundle_dir.display()
)
})?;
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(unix)]
fn is_executable(path: &std::path::Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(not(unix))]
fn is_executable(path: &std::path::Path) -> bool {
path.is_file()
}
pub(crate) fn is_kache_subcommand_or_flag(s: &str) -> bool {
if s.starts_with('-') {
return true;
}
use clap::CommandFactory;
let mut cmd = crate::Cli::command();
cmd.build();
cmd.find_subcommand(s).is_some()
}
pub(crate) fn is_version_or_info_query(args: &[String]) -> bool {
!args.is_empty()
&& args.iter().all(|a| {
matches!(
a.as_str(),
"-vV"
| "-V"
| "--version"
| "-dumpversion"
| "-dumpfullversion"
| "-dumpmachine"
| "-print-search-dirs"
| "--print-search-dirs"
)
})
}
pub(crate) fn resolve_program_on_path(program: &str) -> Option<std::path::PathBuf> {
let path = std::env::var_os("PATH");
let pathext = std::env::var_os("PATHEXT");
resolve_program_on_path_with(program, path.as_deref(), pathext.as_deref())
}
fn resolve_program_on_path_with(
program: &str,
path: Option<&std::ffi::OsStr>,
pathext: Option<&std::ffi::OsStr>,
) -> Option<std::path::PathBuf> {
if program.contains('/') || program.contains('\\') {
return Some(std::path::PathBuf::from(program));
}
let dirs: Vec<std::path::PathBuf> = std::env::split_paths(path?).collect();
let extensions: Vec<String> = if cfg!(windows) {
if let Some(pathext) = pathext {
std::env::split_paths(pathext)
.filter_map(|p| p.to_str().map(|s| s.to_string()))
.collect()
} else {
vec![
".exe".to_string(),
".bat".to_string(),
".cmd".to_string(),
".com".to_string(),
]
}
} else {
vec!["".to_string()]
};
for dir in dirs {
let p = dir.join(program);
if is_executable(&p) {
return Some(p);
}
for ext in &extensions {
if ext.is_empty() {
continue;
}
let mut suffixed = p.clone().into_os_string();
suffixed.push(ext);
let suffixed_path = std::path::PathBuf::from(suffixed);
if is_executable(&suffixed_path) {
return Some(suffixed_path);
}
}
}
None
}
fn is_program_on_path(program: &str) -> bool {
resolve_program_on_path(program).is_some()
}
pub(crate) fn is_passthrough_compiler_invocation(args: &[String]) -> bool {
let rustc = std::env::var_os("RUSTC");
is_passthrough_compiler_invocation_with(args, rustc.as_deref())
}
pub(crate) fn is_passthrough_compiler_invocation_with(
args: &[String],
configured_rustc: Option<&std::ffi::OsStr>,
) -> bool {
let Some(program) = args.first() else {
return false;
};
if is_kache_subcommand_or_flag(program) {
return false;
}
let is_configured_rustc =
configured_rustc.is_some_and(|rustc| rustc == std::ffi::OsStr::new(program.as_str()));
let is_nvcc = command_basename(program)
.map(strip_windows_exe_suffix)
.is_some_and(|name| name.eq_ignore_ascii_case("nvcc"));
is_configured_rustc || is_nvcc
}
pub fn is_workspace_wrapper_chain(args: &[String]) -> bool {
let workspace_wrapper = std::env::var_os("RUSTC_WORKSPACE_WRAPPER");
is_workspace_wrapper_chain_with(args, workspace_wrapper.as_deref(), is_program_on_path)
}
fn is_workspace_wrapper_chain_with(
args: &[String],
workspace_wrapper: Option<&std::ffi::OsStr>,
program_on_path: impl FnOnce(&str) -> bool,
) -> bool {
if args.len() < 2 || !rustc::RustcCompiler::recognizes(&args[1..]) {
return false;
}
if args[0].contains('/') || args[0].contains('\\') {
return true;
}
if workspace_wrapper.is_some_and(|wrapper| wrapper == std::ffi::OsStr::new(&args[0])) {
return true;
}
!is_kache_subcommand_or_flag(&args[0]) && program_on_path(&args[0])
}
pub(crate) fn command_basename(arg0: &str) -> Option<&str> {
arg0.rsplit(['/', '\\'])
.next()
.filter(|name| !name.is_empty())
}
pub(crate) fn strip_windows_exe_suffix(name: &str) -> &str {
let bytes = name.as_bytes();
if bytes.len() >= 4 && bytes[bytes.len() - 4..].eq_ignore_ascii_case(b".exe") {
&name[..bytes.len() - 4]
} else {
name
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_or_info_query_needs_every_arg_to_be_a_query_flag() {
let q = |a: &[&str]| {
is_version_or_info_query(&a.iter().map(|s| s.to_string()).collect::<Vec<_>>())
};
assert!(q(&["-vV"]));
assert!(q(&["--version"]));
assert!(q(&["-dumpmachine"]));
assert!(!q(&["-c", "hello.c", "-o", "-V"]));
assert!(!q(&["-MF", "--version"]));
assert!(!q(&["-vV", "hello.c"]));
assert!(!q(&[]));
}
#[test]
fn test_is_kache_subcommand_or_flag() {
assert!(is_kache_subcommand_or_flag("help"));
assert!(is_kache_subcommand_or_flag("-h"));
assert!(is_kache_subcommand_or_flag("--help"));
assert!(is_kache_subcommand_or_flag("-V"));
assert!(is_kache_subcommand_or_flag("--version"));
assert!(is_kache_subcommand_or_flag("gc"));
assert!(is_kache_subcommand_or_flag("list"));
assert!(!is_kache_subcommand_or_flag("not-a-subcommand"));
}
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)
);
assert_eq!(
detect_compiler(&s(&[
r"G:\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\bin\clippy-driver.exe",
"rustc",
"-vV",
]))
.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)
);
assert_eq!(
detect_compiler(&s(&[
"/opt/cross/bin/arm-linux-gnueabihf-gcc",
"-c",
"foo.c",
]))
.map(|adapter| adapter.id()),
Some(cc::CC_ID)
);
assert!(detect_compiler(&s(&["arm-linux-gnueabihf-gcc-ar"])).is_none());
}
#[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 workspace_wrapper_chain_detects_unrecognized_drivers() {
assert!(is_workspace_wrapper_chain(&s(&[
"/Users/dev/.dylint_drivers/nightly/dylint-driver",
"rustc",
"--crate-name",
])));
assert!(is_workspace_wrapper_chain(&s(&[
r"C:\tools\custom-driver.exe",
"rustc",
])));
}
#[test]
fn workspace_wrapper_chain_detects_bare_name_via_env() {
let args = s(&["mydriver", "rustc"]);
assert!(is_workspace_wrapper_chain_with(
&args,
Some(std::ffi::OsStr::new("mydriver")),
|_| false,
));
assert!(!is_workspace_wrapper_chain_with(
&args,
Some(std::ffi::OsStr::new("other-driver")),
|_| false,
));
}
#[test]
fn workspace_wrapper_chain_detects_bare_name_via_path() {
use std::fs::File;
let temp_dir = tempfile::TempDir::new().unwrap();
let wrapper_name = "custom-wrapper-test-executable";
#[cfg(windows)]
{
let wrapper_path_exe = temp_dir.path().join(format!("{}.exe", wrapper_name));
File::create(&wrapper_path_exe).unwrap();
}
#[cfg(not(windows))]
{
let wrapper_path = temp_dir.path().join(wrapper_name);
File::create(&wrapper_path).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&wrapper_path).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&wrapper_path, perms).unwrap();
}
}
let test_path = std::env::join_paths([temp_dir.path()]).unwrap();
assert!(is_workspace_wrapper_chain_with(
&s(&[wrapper_name, "rustc"]),
None,
|program| {
resolve_program_on_path_with(program, Some(test_path.as_os_str()), None).is_some()
},
));
}
#[test]
fn workspace_wrapper_chain_rejects_non_paths() {
for subcommand in ["init", "gc", "doctor", "config", "report"] {
assert!(!is_workspace_wrapper_chain_with(
&s(&[subcommand, "rustc"]),
None,
|_| true,
));
}
assert!(!is_workspace_wrapper_chain_with(
&s(&["nonexistentwrappername12345", "rustc"]),
None,
|_| false,
));
assert!(!is_workspace_wrapper_chain(&s(&["/usr/bin/cc", "file.c"])));
assert!(!is_workspace_wrapper_chain(&s(&["cargo", "build"])));
assert!(!is_workspace_wrapper_chain(&s(&["/usr/bin/rustc"])));
}
#[test]
fn passthrough_compiler_accepts_configured_rustc_and_nvcc() {
assert!(is_passthrough_compiler_invocation_with(
&s(&["/home/user/.kani/kani-0.67.0/bin/kani-compiler", "-vV"]),
Some(std::ffi::OsStr::new(
"/home/user/.kani/kani-0.67.0/bin/kani-compiler",
)),
));
assert!(is_passthrough_compiler_invocation_with(
&s(&["custom-rustc-driver", "--crate-name", "demo"]),
Some(std::ffi::OsStr::new("custom-rustc-driver")),
));
assert!(is_passthrough_compiler_invocation_with(
&s(&[r"C:\CUDA\bin\nvcc.exe", "-c", "kernel.cu"]),
None,
));
assert!(is_passthrough_compiler_invocation_with(
&s(&["nvcc", "-c", "kernel.cu"]),
None,
));
}
#[test]
fn passthrough_compiler_rejects_unrelated_programs() {
assert!(!is_passthrough_compiler_invocation(&s(&["gc"])));
assert!(!is_passthrough_compiler_invocation_with(&[], None));
assert!(!is_passthrough_compiler_invocation_with(
&s(&["stat"]),
None,
));
assert!(!is_passthrough_compiler_invocation_with(
&s(&["/usr/bin/stat"]),
Some(std::ffi::OsStr::new("/usr/bin/other-driver")),
));
assert!(!is_passthrough_compiler_invocation_with(
&s(&["gc"]),
Some(std::ffi::OsStr::new("gc")),
));
}
#[test]
fn command_basename_splits_both_separators() {
assert_eq!(command_basename("rustc"), Some("rustc"));
assert_eq!(command_basename("/usr/bin/rustc"), Some("rustc"));
assert_eq!(
command_basename(r"G:\bin\clippy-driver.exe"),
Some("clippy-driver.exe")
);
assert_eq!(command_basename(r"C:\a/b\c.exe"), Some("c.exe"));
assert_eq!(command_basename("/usr/bin/"), None);
assert_eq!(command_basename(r"C:\bin\"), None);
assert_eq!(command_basename(""), None);
}
#[test]
fn strip_windows_exe_suffix_is_case_insensitive_and_optional() {
assert_eq!(strip_windows_exe_suffix("rustc.exe"), "rustc");
assert_eq!(
strip_windows_exe_suffix("clippy-driver.EXE"),
"clippy-driver"
);
assert_eq!(strip_windows_exe_suffix("rustc"), "rustc");
assert_eq!(strip_windows_exe_suffix("a.exe.b"), "a.exe.b");
assert_eq!(strip_windows_exe_suffix(".ex"), ".ex");
}
#[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::WasmModule,
ArtifactKind::Other("test"),
] {
assert!(
plan_post_restore(kind).is_empty(),
"{kind:?} should have no post-restore actions"
);
}
}
#[test]
fn plan_post_restore_debug_bundle_unpacks_exactly() {
assert_eq!(
plan_post_restore(ArtifactKind::DebugBundle),
vec![PostRestoreAction::UnpackDebugBundle]
);
}
#[test]
fn expand_dep_info_paths_is_a_content_transform() {
assert!(PostRestoreAction::ExpandDepInfoPaths.is_content_transform());
assert!(!PostRestoreAction::Sign(SigningPurpose::OsLoading).is_content_transform());
assert!(!PostRestoreAction::UnpackDebugBundle.is_content_transform());
let bytes = b"tar bytes".to_vec();
assert_eq!(
PostRestoreAction::UnpackDebugBundle
.transform(bytes.clone(), std::path::Path::new("/anchor")),
bytes
);
}
#[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"
);
}
fn synthetic_tar(entries: &[(&str, &[u8])]) -> Vec<u8> {
let mut builder = tar::Builder::new(Vec::new());
for (path, content) in entries {
let mut header = tar::Header::new_gnu();
header.set_size(content.len() as u64);
header.set_mode(0o644);
header.set_mtime(0);
header.set_entry_type(tar::EntryType::Regular);
builder
.append_data(&mut header, path, &content[..])
.unwrap();
}
builder.into_inner().unwrap()
}
#[test]
fn apply_unpack_debug_bundle_creates_sibling_dsym_dir() {
use crate::compiler::platform::tests::CountingPlatform;
let dir = tempfile::tempdir().unwrap();
let tar_path = dir.path().join("foo-abc123.dsym.tar");
std::fs::write(
&tar_path,
synthetic_tar(&[
("Contents/Info.plist", b"plist"),
("Contents/Resources/DWARF/foo-abc123", b"dwarf bytes"),
]),
)
.unwrap();
PostRestoreAction::UnpackDebugBundle
.apply(&tar_path, &CountingPlatform::new())
.unwrap();
let bundle = dir.path().join("foo-abc123.dSYM");
assert_eq!(
std::fs::read(bundle.join("Contents/Resources/DWARF/foo-abc123")).unwrap(),
b"dwarf bytes"
);
assert_eq!(
std::fs::read(bundle.join("Contents/Info.plist")).unwrap(),
b"plist"
);
assert!(tar_path.is_file(), "the restored tar must not be deleted");
}
#[test]
fn apply_unpack_debug_bundle_replaces_stale_bundle() {
use crate::compiler::platform::tests::CountingPlatform;
let dir = tempfile::tempdir().unwrap();
let bundle = dir.path().join("foo.dSYM");
std::fs::create_dir_all(bundle.join("Contents")).unwrap();
std::fs::write(bundle.join("Contents/stale"), b"old").unwrap();
let tar_path = dir.path().join("foo.dsym.tar");
std::fs::write(
&tar_path,
synthetic_tar(&[("Contents/Resources/DWARF/foo", b"new dwarf")]),
)
.unwrap();
PostRestoreAction::UnpackDebugBundle
.apply(&tar_path, &CountingPlatform::new())
.unwrap();
assert!(
!bundle.join("Contents/stale").exists(),
"a stale bundle must be replaced wholesale, not merged — lldb \
would otherwise trust leftover files from another build"
);
assert_eq!(
std::fs::read(bundle.join("Contents/Resources/DWARF/foo")).unwrap(),
b"new dwarf"
);
}
fn forged_tar_with_entry_name(name: &[u8]) -> Vec<u8> {
let mut header = tar::Header::new_gnu();
header.set_size(5);
header.set_mode(0o644);
header.set_entry_type(tar::EntryType::Regular);
let mut builder = tar::Builder::new(Vec::new());
builder
.append_data(&mut header, "placeholder", &b"pwned"[..])
.unwrap();
let mut bytes = builder.into_inner().unwrap();
assert!(name.len() < 100, "GNU tar name field is 100 bytes");
bytes[..name.len()].copy_from_slice(name);
bytes[name.len()..100].fill(0);
let mut patched = tar::Header::new_gnu();
patched.as_mut_bytes().copy_from_slice(&bytes[..512]);
patched.set_cksum();
bytes[..512].copy_from_slice(patched.as_bytes());
bytes
}
#[test]
fn apply_unpack_debug_bundle_rejects_path_traversal() {
use crate::compiler::platform::tests::CountingPlatform;
let dir = tempfile::tempdir().unwrap();
let outdir = dir.path().join("deps");
std::fs::create_dir_all(&outdir).unwrap();
let tar_path = outdir.join("evil.dsym.tar");
std::fs::write(&tar_path, forged_tar_with_entry_name(b"../escaped-file")).unwrap();
let err = PostRestoreAction::UnpackDebugBundle
.apply(&tar_path, &CountingPlatform::new())
.unwrap_err()
.to_string();
assert!(
err.contains("path traversal"),
"a `..` entry must be rejected, got: {err}"
);
assert!(
!dir.path().join("escaped-file").exists(),
"nothing may be written outside the temp extraction dir"
);
assert!(
!outdir.join("evil.dSYM").exists(),
"a rejected archive must not publish a bundle"
);
}
#[test]
fn apply_unpack_debug_bundle_rejects_absolute_entry() {
use crate::compiler::platform::tests::CountingPlatform;
let dir = tempfile::tempdir().unwrap();
let tar_path = dir.path().join("abs.dsym.tar");
std::fs::write(
&tar_path,
forged_tar_with_entry_name(b"/tmp/kache-absolute-escape"),
)
.unwrap();
let err = PostRestoreAction::UnpackDebugBundle
.apply(&tar_path, &CountingPlatform::new())
.unwrap_err()
.to_string();
assert!(
err.contains("absolute path"),
"an absolute entry must be rejected, got: {err}"
);
}
#[test]
fn apply_unpack_debug_bundle_rejects_non_dsym_tar_name() {
use crate::compiler::platform::tests::CountingPlatform;
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("foo.tar");
std::fs::write(&path, synthetic_tar(&[("Contents/x", b"y")])).unwrap();
let err = PostRestoreAction::UnpackDebugBundle
.apply(&path, &CountingPlatform::new())
.unwrap_err()
.to_string();
assert!(err.contains(".dsym.tar"), "got: {err}");
}
#[test]
fn apply_unpack_debug_bundle_rejects_each_link_kind_alone() {
use crate::compiler::platform::tests::CountingPlatform;
for entry_type in [tar::EntryType::Symlink, tar::EntryType::Link] {
let dir = tempfile::tempdir().unwrap();
let tar_path = dir.path().join("linky.dsym.tar");
let mut header = tar::Header::new_gnu();
header.set_size(0);
header.set_mode(0o644);
header.set_entry_type(entry_type);
let mut builder = tar::Builder::new(Vec::new());
builder
.append_link(&mut header, "Contents/evil", "/etc/passwd")
.unwrap();
std::fs::write(&tar_path, builder.into_inner().unwrap()).unwrap();
let err = PostRestoreAction::UnpackDebugBundle
.apply(&tar_path, &CountingPlatform::new())
.unwrap_err()
.to_string();
assert!(
err.contains("is a link"),
"{entry_type:?} alone must be rejected, got: {err}"
);
}
}
#[test]
fn unpack_debug_bundle_cap_boundary_is_exact() {
let payload = vec![b'x'; 100];
let dir = tempfile::tempdir().unwrap();
let tar_path = dir.path().join("capped.dsym.tar");
std::fs::write(
&tar_path,
synthetic_tar(&[("Contents/blob", payload.as_slice())]),
)
.unwrap();
let err = unpack_debug_bundle_with_cap(&tar_path, 99)
.unwrap_err()
.to_string();
assert!(err.contains("extraction cap"), "got: {err}");
assert!(!dir.path().join("capped.dSYM").exists());
unpack_debug_bundle_with_cap(&tar_path, 100)
.expect("a bundle exactly at the cap is within budget");
assert!(dir.path().join("capped.dSYM/Contents/blob").exists());
}
#[test]
fn artifact_set_push_appends_the_artifact() {
let mut set = ArtifactSet::empty();
set.push(Artifact {
path: std::path::PathBuf::from("/tmp/x.dsym.tar"),
store_name: "x.dsym.tar".to_string(),
kind: ArtifactKind::DebugBundle,
required: false,
});
assert_eq!(set.outputs().len(), 1);
assert_eq!(set.outputs()[0].store_name, "x.dsym.tar");
assert_eq!(set.outputs()[0].kind, ArtifactKind::DebugBundle);
}
#[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("rococo_runtime.wasm"),
ArtifactKind::WasmModule
);
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);
assert_eq!(
classify_by_filename("foo-abc123.dsym.tar"),
ArtifactKind::DebugBundle
);
assert_eq!(
classify_by_filename("foo.tar"),
ArtifactKind::Other("unknown-ext")
);
assert_eq!(
ArtifactKind::DebugBundle.link_strategy(),
LinkStrategy::Hardlink
);
}
#[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 emit_kind_for_filename_maps_outputs() {
let cases = [
("libfoo-abc.rlib", Some("link")),
("libfoo.so", Some("link")),
("libfoo.dylib", Some("link")),
("foo.dll", Some("link")),
("foo.exe", Some("link")),
("rococo_runtime.wasm", Some("link")),
("my_bin-abc123", Some("link")), ("libfoo-abc.rmeta", Some("metadata")),
("foo-abc.123.rcgu.o", Some("obj")),
("foo.obj", Some("obj")),
("foo-abc.d", Some("dep-info")),
("foo.s", Some("asm")),
("foo.ll", Some("llvm-ir")),
("foo.bc", Some("llvm-bc")),
("foo.mir", Some("mir")),
("foo.dwo", None),
("foo.pdb", None),
("foo.lock", None),
("foo-abc.dsym.tar", None),
];
for (name, expected) in cases {
assert_eq!(emit_kind_for_filename(name), expected, "for {name}");
if let Some(kind) = expected {
assert!(
GATED_EMIT_KINDS.contains(&kind),
"{kind} (from {name}) must be in GATED_EMIT_KINDS"
);
}
}
}
#[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![]),
(
"foo-abc.dsym.tar",
vec![PostRestoreAction::UnpackDebugBundle],
),
];
for (name, expected) in cases {
let kind = compiler.classify_output(&bin_args, name);
assert_eq!(
&plan_post_restore(kind),
expected,
"for {name}: kind = {kind:?}"
);
}
}
}