use std::collections::BTreeSet;
use std::ffi::{OsStr, OsString};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::args::{ExternDep, RustcArgs};
use crate::compiler::Compiler;
use crate::compiler::rustc::RustcCompiler;
use crate::config::Config;
const ROOT_NAME: &str = "out-dirs";
const LAYOUT: &str = "v1";
const SIDECAR_EXTENSION: &str = "kache-alias";
const MAX_BUILD_OUTPUT_BYTES: u64 = 64 * 1024;
const ALLOWED_DIRECTIVES: &[&str] = &[
"rerun-if-changed",
"rerun-if-env-changed",
"rustc-cfg",
"rustc-check-cfg",
"rustc-env",
"warning",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Tier {
ProcMacro,
Lib,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConsumerKind {
ProcMacro,
Other,
}
impl ConsumerKind {
fn marker(self) -> &'static str {
match self {
Self::ProcMacro => "pm",
Self::Other => "other",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LibVerdict {
Unseen,
MacrosOnly,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AliasState {
Absent,
Usable,
Unusable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SkipReason {
Test,
CrateType,
Refused,
NotRegistry,
OutDirShape,
OutDirNotEmpty,
BuildOutput,
EnvConflict,
Forced,
UnsafeRoot,
Denied,
AliasUnusable,
LibEvidence(LibVerdict),
Sidecar,
}
impl SkipReason {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Test => "test harness",
Self::CrateType => "crate type",
Self::Refused => "not a cacheable compile",
Self::NotRegistry => "not a registry package",
Self::OutDirShape => "OUT_DIR is not a Cargo build-script OUT_DIR",
Self::OutDirNotEmpty => "OUT_DIR is not empty",
Self::BuildOutput => "build script output exports or links",
Self::EnvConflict => "OUT_DIR appears in another value",
Self::Forced => "path_only_env_vars forces the var",
Self::UnsafeRoot => "alias root is not private",
Self::Denied => "denied",
Self::AliasUnusable => "alias directory is not an empty read-only directory",
Self::LibEvidence(LibVerdict::Unseen) => "lib evidence unseen",
Self::LibEvidence(LibVerdict::MacrosOnly) => "lib evidence macros only",
Self::LibEvidence(LibVerdict::Other) => "lib evidence other",
Self::Sidecar => "alias marker could not be written",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct UnitFacts {
pub crate_types: Vec<String>,
pub is_test: bool,
pub refused: bool,
pub registry: bool,
pub unit_dir: Option<String>,
pub out_dir_empty: bool,
pub directives_ok: bool,
pub env_ok: bool,
pub forced: bool,
pub root_safe: bool,
pub denied: bool,
}
pub(crate) fn host_allows(unix: bool, euid: u32) -> bool {
unix && euid != 0
}
pub(crate) fn unit_tier(crate_types: &[String]) -> Option<Tier> {
if crate_types == ["proc-macro"] {
return Some(Tier::ProcMacro);
}
let lib = !crate_types.is_empty()
&& crate_types
.iter()
.all(|crate_type| crate_type == "lib" || crate_type == "rlib");
lib.then_some(Tier::Lib)
}
pub(crate) fn decide(
facts: &UnitFacts,
verdict: LibVerdict,
alias: AliasState,
) -> Result<Tier, SkipReason> {
if facts.is_test {
return Err(SkipReason::Test);
}
let tier = unit_tier(&facts.crate_types).ok_or(SkipReason::CrateType)?;
let checks = [
(!facts.refused, SkipReason::Refused),
(facts.registry, SkipReason::NotRegistry),
(facts.unit_dir.is_some(), SkipReason::OutDirShape),
(facts.out_dir_empty, SkipReason::OutDirNotEmpty),
(facts.directives_ok, SkipReason::BuildOutput),
(facts.env_ok, SkipReason::EnvConflict),
(!facts.forced, SkipReason::Forced),
(facts.root_safe, SkipReason::UnsafeRoot),
(!facts.denied, SkipReason::Denied),
(alias != AliasState::Unusable, SkipReason::AliasUnusable),
];
if let Some((_, reason)) = checks.iter().find(|(passed, _)| !passed) {
return Err(*reason);
}
if tier == Tier::Lib && verdict != LibVerdict::MacrosOnly {
return Err(SkipReason::LibEvidence(verdict));
}
Ok(tier)
}
pub(crate) fn is_candidate(decision: Result<Tier, SkipReason>) -> bool {
matches!(decision, Ok(Tier::Lib) | Err(SkipReason::LibEvidence(_)))
}
pub(crate) fn unit_dir_name(out_dir: &Path, pkg: &str) -> Option<String> {
if !out_dir.is_absolute() {
return None;
}
crate::cargo_layout::out_dir_unit_name(out_dir, pkg)
}
pub(crate) fn directives_allow_alias(output: &str) -> bool {
output.lines().all(|line| {
let Some(rest) = line.strip_prefix("cargo:") else {
return true;
};
let directive = rest.strip_prefix(':').unwrap_or(rest);
let name = directive
.split_once('=')
.map_or(directive, |(name, _)| name);
ALLOWED_DIRECTIVES.contains(&name)
})
}
pub(crate) fn alias_dir(root: &Path, unit_dir: &str) -> PathBuf {
root.join(LAYOUT).join("d").join(unit_dir).join("out")
}
fn rewritten(value: &OsStr, real: &[&Path], alias: &Path) -> Option<OsString> {
let relative = real
.iter()
.find_map(|spelling| Path::new(value).strip_prefix(spelling).ok())?;
let target = if relative.as_os_str().is_empty() {
alias.to_path_buf()
} else {
alias.join(relative)
};
Some(target.into_os_string())
}
fn mentions(bytes: &[u8], real: &[&Path]) -> bool {
real.iter().any(|spelling| {
let needle = spelling.as_os_str().as_encoded_bytes();
bytes.windows(needle.len()).any(|window| window == needle)
})
}
pub(crate) fn env_rewrites(
env: &[(OsString, OsString)],
argv: &[String],
real: &[&Path],
alias: &Path,
) -> Option<Vec<(OsString, OsString)>> {
let mut rewrites = Vec::new();
for (name, value) in env {
match rewritten(value, real, alias) {
Some(target) => rewrites.push((name.clone(), target)),
None if mentions(value.as_encoded_bytes(), real) => return None,
None => {}
}
}
if argv.iter().any(|arg| mentions(arg.as_bytes(), real)) {
return None;
}
Some(rewrites)
}
pub(crate) fn forced_by_user(
entries: &[String],
crate_name: &str,
rewrites: &[(OsString, OsString)],
) -> bool {
entries.iter().any(|entry| {
entry.split_once(':').is_some_and(|(krate, var)| {
krate == crate_name
&& (var == "OUT_DIR" || rewrites.iter().any(|(name, _)| name == var))
})
})
}
pub(crate) fn root_location_ok(root: &Path, checkout_dirs: &[PathBuf]) -> bool {
!checkout_dirs.iter().any(|dir| root.starts_with(dir))
}
pub(crate) fn consumer_kind(crate_types: &[String], is_test: bool) -> ConsumerKind {
let proc_macro = !is_test
&& crate_types
.iter()
.any(|crate_type| crate_type == "proc-macro");
if proc_macro {
ConsumerKind::ProcMacro
} else {
ConsumerKind::Other
}
}
pub(crate) fn lib_verdict(pm: bool, other: bool) -> LibVerdict {
if other {
LibVerdict::Other
} else if pm {
LibVerdict::MacrosOnly
} else {
LibVerdict::Unseen
}
}
pub(crate) fn is_permission_failure(stderr: &str) -> bool {
stderr.contains("Permission denied") || stderr.contains("os error 13")
}
pub(crate) fn store_allowed(alias_clean: bool, lists_root_file: bool) -> bool {
alias_clean && !lists_root_file
}
fn alias_unit_of<'a>(path: &'a Path, root: &Path) -> Option<&'a str> {
let relative = path.strip_prefix(root.join(LAYOUT).join("d")).ok()?;
relative.components().next()?.as_os_str().to_str()
}
fn unit_package(unit: &str) -> Option<&str> {
crate::cargo_layout::legacy_unit_package(unit)
}
fn alias_units_in(bytes: &[u8], root: &Path) -> BTreeSet<String> {
let mut prefix = root
.join(LAYOUT)
.join("d")
.into_os_string()
.into_encoded_bytes();
prefix.push(b'/');
let mut units = BTreeSet::new();
let mut rest = bytes;
while let Some(at) = rest
.windows(prefix.len())
.position(|window| window == prefix)
{
rest = &rest[at + prefix.len()..];
let len = rest
.iter()
.position(|byte| !(byte.is_ascii_alphanumeric() || *byte == b'-' || *byte == b'_'))
.unwrap_or(rest.len());
let unit = std::str::from_utf8(&rest[..len]).unwrap_or_default();
if unit_package(unit).is_some() {
units.insert(unit.to_string());
}
}
units
}
fn loaded_macros(externs: &[ExternDep]) -> impl Iterator<Item = &Path> {
externs
.iter()
.filter_map(|dep| dep.path.as_deref())
.filter(|path| path.extension() == Some(OsStr::new(std::env::consts::DLL_EXTENSION)))
}
fn loaded_aliases(externs: &[ExternDep], root: &Path) -> BTreeSet<String> {
loaded_macros(externs)
.filter_map(|path| std::fs::read(path).ok())
.flat_map(|bytes| alias_units_in(&bytes, root))
.collect()
}
pub(crate) fn permission_hint(units: &BTreeSet<String>) -> String {
let packages: BTreeSet<&str> = units.iter().filter_map(|unit| unit_package(unit)).collect();
let clean = if packages.is_empty() {
"-p <macro crate>".to_string()
} else {
packages
.iter()
.map(|package| format!("-p {package}"))
.collect::<Vec<_>>()
.join(" ")
};
format!(
"[kache] shared OUT_DIRs are read-only; to let a macro write debug output, run \
`cargo clean {clean}` and rebuild with KACHE_OUT_DIR_ALIAS=0"
)
}
fn failure_hint(
stderr: &str,
externs: &[ExternDep],
root: &Path,
own: Option<&str>,
) -> Option<String> {
if !(root.exists() && is_permission_failure(stderr)) {
return None;
}
let mut known = alias_units_in(stderr.as_bytes(), root);
known.extend(own.map(str::to_string));
for unit in &known {
let _ = deny(root, unit);
}
known.extend(loaded_aliases(externs, root));
Some(permission_hint(&known))
}
fn sidecar_path(args: &RustcArgs) -> Option<PathBuf> {
let stem = args.output_stem()?;
Some(
args.out_dir
.as_ref()?
.join(format!("lib{stem}.{SIDECAR_EXTENSION}")),
)
}
fn extern_files(path: &Path) -> Option<[PathBuf; 3]> {
let stem = path.file_stem()?.to_str()?;
let dir = path.parent()?;
Some(["rlib", "rmeta", SIDECAR_EXTENSION].map(|ext| dir.join(format!("{stem}.{ext}"))))
}
fn libs_dir(root: &Path) -> PathBuf {
root.join(LAYOUT).join("libs")
}
fn deny_path(root: &Path, unit_dir: &str) -> PathBuf {
root.join(LAYOUT).join("deny").join(unit_dir)
}
pub(crate) fn effective_uid() -> u32 {
#[cfg(unix)]
{
unsafe { libc::geteuid() }
}
#[cfg(not(unix))]
{
0
}
}
fn dir_builder(mode: u32, recursive: bool) -> std::fs::DirBuilder {
let mut builder = std::fs::DirBuilder::new();
builder.recursive(recursive);
#[cfg(unix)]
std::os::unix::fs::DirBuilderExt::mode(&mut builder, mode);
#[cfg(not(unix))]
let _ = mode;
builder
}
pub(crate) fn create_private_dir_all(path: &Path) -> std::io::Result<()> {
dir_builder(0o700, true).create(path)
}
fn is_private_dir(path: &Path, euid: u32) -> bool {
let Ok(meta) = std::fs::symlink_metadata(path) else {
return false;
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
meta.is_dir() && meta.uid() == euid && meta.mode() & 0o022 == 0
}
#[cfg(not(unix))]
{
let _ = (meta, euid);
false
}
}
fn alias_root(cache_dir: &Path) -> Option<PathBuf> {
std::fs::create_dir_all(cache_dir).ok()?;
Some(std::fs::canonicalize(cache_dir).ok()?.join(ROOT_NAME))
}
fn checkout_dirs(args: &RustcArgs) -> Vec<PathBuf> {
let workspace = args
.workspace_root()
.filter(|dir| dir.join("Cargo.toml").is_file());
args.path_normalization_root()
.map(Path::to_path_buf)
.into_iter()
.chain(args.target_dir())
.chain(workspace)
.map(|dir| std::fs::canonicalize(&dir).unwrap_or(dir))
.collect()
}
fn root_is_safe(root: &Path, args: &RustcArgs, euid: u32) -> bool {
let _ = create_private_dir_all(root);
is_private_dir(root, euid) && root_location_ok(root, &checkout_dirs(args))
}
fn dir_has_no_entries(dir: &Path) -> bool {
std::fs::read_dir(dir).is_ok_and(|mut entries| entries.next().is_none())
}
fn build_output_allows_alias(path: &Path) -> bool {
let small = std::fs::metadata(path).is_ok_and(|meta| meta.len() <= MAX_BUILD_OUTPUT_BYTES);
small
&& std::fs::read(path)
.is_ok_and(|bytes| directives_allow_alias(&String::from_utf8_lossy(&bytes)))
}
pub(crate) fn alias_state(dir: &Path, euid: u32) -> AliasState {
let meta = match std::fs::symlink_metadata(dir) {
Ok(meta) => meta,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return AliasState::Absent,
Err(_) => return AliasState::Unusable,
};
#[cfg(unix)]
let usable = {
use std::os::unix::fs::MetadataExt;
meta.is_dir() && meta.uid() == euid && meta.mode() & 0o222 == 0 && dir_has_no_entries(dir)
};
#[cfg(not(unix))]
let usable = {
let _ = (meta, euid);
false
};
if usable {
AliasState::Usable
} else {
AliasState::Unusable
}
}
pub(crate) fn ensure_alias_dir(dir: &Path, euid: u32) -> bool {
let Some(parent) = dir.parent() else {
return false;
};
if create_private_dir_all(parent).is_err() {
return false;
}
dir_builder(0o555, false).create(dir).is_ok() || alias_state(dir, euid) == AliasState::Usable
}
fn listed_candidates(root: &Path) -> BTreeSet<String> {
std::fs::read_dir(libs_dir(root))
.into_iter()
.flatten()
.filter_map(|entry| entry.ok()?.file_name().into_string().ok())
.collect()
}
fn listed_extern<'a>(dep: &'a ExternDep, listed: &BTreeSet<String>) -> Option<(&'a Path, String)> {
let path = dep.path.as_deref()?;
let id = crate::args::unit_id_from_artifact_path(path)?;
listed.contains(&id).then_some((path, id))
}
pub(crate) fn record_consumers(
root: &Path,
listed: &BTreeSet<String>,
externs: &[ExternDep],
kind: ConsumerKind,
) {
for (_, id) in externs.iter().filter_map(|dep| listed_extern(dep, listed)) {
let _ = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(libs_dir(root).join(id).join(kind.marker()));
}
}
pub(crate) fn trip(
listed: &BTreeSet<String>,
externs: &[ExternDep],
kind: ConsumerKind,
) -> Vec<String> {
if kind == ConsumerKind::ProcMacro {
return Vec::new();
}
let mut tripped = Vec::new();
for dep in externs {
let Some([rlib, rmeta, sidecar]) = listed_extern(dep, listed)
.and_then(|(path, _)| extern_files(path))
.filter(|[_, _, sidecar]| sidecar.exists())
else {
continue;
};
for file in [rlib, rmeta, sidecar] {
let _ = std::fs::remove_file(file);
}
tripped.push(dep.name.clone());
}
tripped
}
pub(crate) fn consumer_checks(
root: &Path,
externs: &[ExternDep],
kind: ConsumerKind,
stderr: &mut dyn Write,
) -> Option<i32> {
let listed = listed_candidates(root);
record_consumers(root, &listed, externs, kind);
let tripped = trip(&listed, externs, kind);
if tripped.is_empty() {
return None;
}
let _ = writeln!(
stderr,
"[kache] {} was built with a shared read-only OUT_DIR that only proc macros may use; \
removed it so Cargo rebuilds it. Run the build again.",
tripped.join(", ")
);
Some(1)
}
fn checks_before_decision(args: &RustcArgs, root: &Path, stderr: &mut dyn Write) -> Option<i32> {
let kind = consumer_kind(&args.crate_types, args.is_test);
if let Some(code) = consumer_checks(root, &args.externs, kind, stderr) {
return Some(code);
}
if let Some(path) = sidecar_path(args) {
let _ = write_sidecar(&path, false);
}
None
}
pub(crate) fn register_candidate(root: &Path, unit_id: &str) -> std::io::Result<()> {
create_private_dir_all(&libs_dir(root).join(unit_id))
}
fn settle_candidate(root: &Path, unit_id: &str, bakes_out_dir: bool) -> std::io::Result<()> {
if bakes_out_dir {
return register_candidate(root, unit_id);
}
match std::fs::remove_dir_all(libs_dir(root).join(unit_id)) {
Err(error) if error.kind() != std::io::ErrorKind::NotFound => Err(error),
_ => Ok(()),
}
}
pub(crate) fn deny(root: &Path, unit_dir: &str) -> std::io::Result<()> {
let path = deny_path(root, unit_dir);
create_private_dir_all(path.parent().unwrap_or(root))?;
std::fs::write(path, b"")
}
pub(crate) fn write_sidecar(path: &Path, aliased: bool) -> std::io::Result<()> {
if aliased {
return std::fs::write(path, b"");
}
match std::fs::remove_file(path) {
Err(error) if error.kind() != std::io::ErrorKind::NotFound => Err(error),
_ => Ok(()),
}
}
fn read_verdict(root: &Path, unit_id: &str) -> LibVerdict {
let dir = libs_dir(root).join(unit_id);
lib_verdict(
dir.join(ConsumerKind::ProcMacro.marker()).exists(),
dir.join(ConsumerKind::Other.marker()).exists(),
)
}
#[derive(Debug)]
pub(crate) struct Detected {
pub facts: UnitFacts,
pub verdict: LibVerdict,
pub alias: AliasState,
pub rewrites: Vec<(OsString, OsString)>,
}
impl UnitFacts {
pub(crate) fn gather(
args: &RustcArgs,
env: &[(OsString, OsString)],
refused: bool,
path_only_env_vars: &[String],
root: &Path,
euid: u32,
) -> Detected {
let var = |name: &str| {
env.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_os_str())
};
let registry = var("CARGO_MANIFEST_DIR")
.is_some_and(|dir| crate::cache_key::is_registry_package(Path::new(dir)));
let out_dir = var("OUT_DIR").map(Path::new);
let unit_dir = out_dir
.zip(var("CARGO_PKG_NAME").and_then(OsStr::to_str))
.and_then(|(dir, pkg)| unit_dir_name(dir, pkg));
let checked = unit_dir.as_deref().zip(out_dir).filter(|_| registry);
let canonical = checked.and_then(|(_, dir)| std::fs::canonicalize(dir).ok());
let rewrites = checked.and_then(|(name, dir)| {
let spellings: Vec<&Path> = std::iter::once(dir).chain(canonical.as_deref()).collect();
env_rewrites(env, &args.all_args, &spellings, &alias_dir(root, name))
});
let facts = UnitFacts {
crate_types: args.crate_types.clone(),
is_test: args.is_test,
refused,
registry,
out_dir_empty: checked.is_some_and(|(_, dir)| dir_has_no_entries(dir)),
directives_ok: checked
.and_then(|(_, dir)| crate::cargo_layout::build_script_stdout(dir))
.is_some_and(|stdout| build_output_allows_alias(&stdout)),
env_ok: rewrites.is_some(),
forced: rewrites.as_deref().is_some_and(|rewrites| {
forced_by_user(
path_only_env_vars,
args.crate_name.as_deref().unwrap_or(""),
rewrites,
)
}),
root_safe: checked.is_some_and(|_| root_is_safe(root, args, euid)),
denied: checked.is_some_and(|(name, _)| deny_path(root, name).exists()),
unit_dir: unit_dir.clone(),
};
Detected {
verdict: checked
.and(args.unit_id())
.map_or(LibVerdict::Unseen, |id| read_verdict(root, &id)),
alias: checked.map_or(AliasState::Absent, |(name, _)| {
alias_state(&alias_dir(root, name), euid)
}),
rewrites: rewrites.unwrap_or_default(),
facts,
}
}
}
#[derive(Debug)]
struct Plan {
root: PathBuf,
alias: Option<(PathBuf, String)>,
candidate: Option<String>,
}
static PLAN: OnceLock<Plan> = OnceLock::new();
pub(crate) fn apply(config: &Config, wrapper_args: &[String]) -> Option<i32> {
let _trace = crate::phase_trace::phase("out_dir_alias");
let euid = effective_uid();
if !host_allows(cfg!(unix), euid) {
return None;
}
let compiler = RustcCompiler::new().with_base_dirs(config.base_dirs.clone());
let args = compiler.parse(wrapper_args).ok()?;
let root = alias_root(&config.cache_dir)?;
if let Some(code) = checks_before_decision(&args, &root, &mut std::io::stderr()) {
return Some(code);
}
if !config.out_dir_alias {
let _ = PLAN.set(Plan {
root,
alias: None,
candidate: None,
});
return None;
}
let env: Vec<(OsString, OsString)> = std::env::vars_os().collect();
let refused = !compiler.refuse_reasons(&args).is_empty();
let detected = UnitFacts::gather(
&args,
&env,
refused,
&config.path_only_env_vars,
&root,
euid,
);
let decision = decide(&detected.facts, detected.verdict, detected.alias);
let unit_dir = detected.facts.unit_dir.clone().unwrap_or_default();
let sidecar = sidecar_path(&args);
let aliased = decision.and_then(|tier| {
let dir = alias_dir(&root, &unit_dir);
if !ensure_alias_dir(&dir, euid) {
return Err(SkipReason::AliasUnusable);
}
let marked = sidecar
.as_deref()
.map_or(Ok(()), |path| write_sidecar(path, tier == Tier::Lib));
marked.map(|()| dir).map_err(|_| SkipReason::Sidecar)
});
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
match &aliased {
Ok(dir) => {
tracing::trace!(
target: "kache::cache_key",
"[key:{crate_name}] out_dir_alias: aliased to {}",
dir.display()
);
for (name, value) in &detected.rewrites {
unsafe { std::env::set_var(name, value) };
}
}
Err(reason) => {
tracing::trace!(
target: "kache::cache_key",
"[key:{crate_name}] out_dir_alias: skip: {}",
reason.as_str()
);
}
}
let _ = PLAN.set(Plan {
candidate: args.unit_id().filter(|_| is_candidate(decision)),
alias: aliased.ok().map(|dir| (dir, unit_dir)),
root,
});
None
}
pub(crate) fn active_alias() -> Option<&'static Path> {
PLAN.get()?.alias.as_ref().map(|(dir, _)| dir.as_path())
}
pub(crate) fn register_before_compile() {
let Some(plan) = PLAN.get() else {
return;
};
if let Some(unit_id) = plan.candidate.as_deref() {
let _ = register_candidate(&plan.root, unit_id);
}
}
pub(crate) fn register_after_key(bakes_out_dir: bool) {
let Some(plan) = PLAN.get() else {
return;
};
if let Some(unit_id) = plan.candidate.as_deref() {
let _ = settle_candidate(&plan.root, unit_id, bakes_out_dir);
}
}
pub(crate) fn store_gate(args: &RustcArgs) -> bool {
let Some(plan) = PLAN.get() else {
return true;
};
let dirty = plan
.alias
.as_ref()
.filter(|(dir, _)| alias_state(dir, effective_uid()) != AliasState::Usable)
.map(|(_, name)| name.as_str());
let sources = args
.dep_info_path()
.zip(args.source_file.as_deref())
.and_then(|(path, source)| crate::cache_key::dep_info_from_emitted(&path, source).ok())
.map(|dep_info| dep_info.source_files)
.unwrap_or_default();
let under_root: Vec<&PathBuf> = sources
.iter()
.filter(|source| source.starts_with(&plan.root))
.collect();
if store_allowed(dirty.is_none(), !under_root.is_empty()) {
return true;
}
let listed = under_root
.iter()
.filter_map(|source| alias_unit_of(source, &plan.root));
for unit_dir in dirty.into_iter().chain(listed) {
tracing::warn!(
"not caching: shared OUT_DIR {unit_dir} changed; it will not be shared again"
);
let _ = deny(&plan.root, unit_dir);
}
false
}
pub(crate) fn after_failed_compile(stderr: &str, externs: &[ExternDep]) {
let Some(plan) = PLAN.get() else {
return;
};
let own = plan.alias.as_ref().map(|(_, unit_dir)| unit_dir.as_str());
if let Some(hint) = failure_hint(stderr, externs, &plan.root, own) {
eprintln!("{hint}");
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
fn strings(values: &[&str]) -> Vec<String> {
values.iter().map(|value| value.to_string()).collect()
}
fn passing_facts(crate_types: &[&str]) -> UnitFacts {
UnitFacts {
crate_types: strings(crate_types),
is_test: false,
refused: false,
registry: true,
unit_dir: Some("dmac-0123456789abcdef".into()),
out_dir_empty: true,
directives_ok: true,
env_ok: true,
forced: false,
root_safe: true,
denied: false,
}
}
fn os(pairs: &[(&str, &str)]) -> Vec<(OsString, OsString)> {
pairs
.iter()
.map(|(name, value)| (OsString::from(name), OsString::from(value)))
.collect()
}
fn dep(name: &str, path: Option<&Path>) -> ExternDep {
ExternDep {
name: name.into(),
path: path.map(Path::to_path_buf),
}
}
#[test]
fn host_allows_only_a_unix_non_root_host() {
assert!(host_allows(true, 501));
assert!(!host_allows(false, 501));
assert!(!host_allows(true, 0));
}
#[test]
fn unit_tier_follows_the_crate_types() {
assert_eq!(unit_tier(&strings(&["proc-macro"])), Some(Tier::ProcMacro));
assert_eq!(unit_tier(&strings(&["lib"])), Some(Tier::Lib));
assert_eq!(unit_tier(&strings(&["rlib"])), Some(Tier::Lib));
assert_eq!(unit_tier(&strings(&["lib", "rlib"])), Some(Tier::Lib));
for crate_types in [
&[][..],
&["bin"],
&["dylib"],
&["cdylib"],
&["staticlib"],
&["lib", "cdylib"],
&["proc-macro", "lib"],
] {
assert_eq!(unit_tier(&strings(crate_types)), None, "{crate_types:?}");
}
}
#[test]
fn decide_aliases_a_proc_macro_that_passes_every_rule() {
for verdict in [
LibVerdict::Unseen,
LibVerdict::MacrosOnly,
LibVerdict::Other,
] {
for alias in [AliasState::Absent, AliasState::Usable] {
assert_eq!(
decide(&passing_facts(&["proc-macro"]), verdict, alias),
Ok(Tier::ProcMacro)
);
}
}
}
#[test]
fn decide_aliases_a_lib_only_on_macro_only_evidence() {
let facts = passing_facts(&["lib"]);
assert_eq!(
decide(&facts, LibVerdict::MacrosOnly, AliasState::Absent),
Ok(Tier::Lib)
);
assert_eq!(
decide(&facts, LibVerdict::MacrosOnly, AliasState::Usable),
Ok(Tier::Lib)
);
for verdict in [LibVerdict::Unseen, LibVerdict::Other] {
assert_eq!(
decide(&facts, verdict, AliasState::Absent),
Err(SkipReason::LibEvidence(verdict))
);
}
}
#[test]
fn decide_fails_each_rule_alone() {
type Flip = fn(&mut UnitFacts);
let flips: [(Flip, SkipReason); 12] = [
(|f| f.is_test = true, SkipReason::Test),
(|f| f.crate_types = strings(&["bin"]), SkipReason::CrateType),
(|f| f.refused = true, SkipReason::Refused),
(|f| f.registry = false, SkipReason::NotRegistry),
(|f| f.unit_dir = None, SkipReason::OutDirShape),
(|f| f.out_dir_empty = false, SkipReason::OutDirNotEmpty),
(|f| f.directives_ok = false, SkipReason::BuildOutput),
(|f| f.env_ok = false, SkipReason::EnvConflict),
(|f| f.forced = true, SkipReason::Forced),
(|f| f.root_safe = false, SkipReason::UnsafeRoot),
(|f| f.denied = true, SkipReason::Denied),
(
|f| f.crate_types = strings(&["dylib"]),
SkipReason::CrateType,
),
];
for tier in [&["proc-macro"][..], &["lib"]] {
for (flip, reason) in flips {
let mut facts = passing_facts(tier);
flip(&mut facts);
assert_eq!(
decide(&facts, LibVerdict::MacrosOnly, AliasState::Absent),
Err(reason),
"{tier:?}"
);
}
assert_eq!(
decide(
&passing_facts(tier),
LibVerdict::MacrosOnly,
AliasState::Unusable
),
Err(SkipReason::AliasUnusable)
);
}
}
#[test]
fn candidates_are_libs_past_every_rule_but_evidence() {
assert!(is_candidate(Ok(Tier::Lib)));
assert!(is_candidate(Err(SkipReason::LibEvidence(
LibVerdict::Unseen
))));
assert!(is_candidate(Err(SkipReason::LibEvidence(
LibVerdict::Other
))));
assert!(!is_candidate(Ok(Tier::ProcMacro)));
assert!(!is_candidate(Err(SkipReason::Denied)));
}
#[test]
fn skip_reasons_have_distinct_labels() {
let reasons = [
SkipReason::Test,
SkipReason::CrateType,
SkipReason::Refused,
SkipReason::NotRegistry,
SkipReason::OutDirShape,
SkipReason::OutDirNotEmpty,
SkipReason::BuildOutput,
SkipReason::EnvConflict,
SkipReason::Forced,
SkipReason::UnsafeRoot,
SkipReason::Denied,
SkipReason::AliasUnusable,
SkipReason::LibEvidence(LibVerdict::Unseen),
SkipReason::LibEvidence(LibVerdict::MacrosOnly),
SkipReason::LibEvidence(LibVerdict::Other),
SkipReason::Sidecar,
];
let labels: BTreeSet<&str> = reasons.iter().map(|reason| reason.as_str()).collect();
assert_eq!(labels.len(), reasons.len());
assert!(
labels
.iter()
.all(|label| !label.is_empty() && *label != "xyzzy")
);
assert_eq!(
SkipReason::LibEvidence(LibVerdict::Other).as_str(),
"lib evidence other"
);
}
#[test]
fn unit_dir_name_needs_cargos_build_layout_for_the_package() {
let ok = Path::new("/t/target/debug/build/dmac-0123456789abcdef/out");
assert_eq!(
unit_dir_name(ok, "dmac").as_deref(),
Some("dmac-0123456789abcdef")
);
assert_eq!(
unit_dir_name(
Path::new("/t/target/debug/build/dmac/0123456789abcdef/out"),
"dmac"
)
.as_deref(),
Some("dmac-0123456789abcdef"),
"Cargo 1.100's per-unit layout"
);
assert_eq!(
unit_dir_name(
Path::new("t/target/debug/build/dmac/0123456789abcdef/out"),
"dmac"
),
None,
"relative"
);
let dashed = Path::new("/t/target/debug/build/my-mac-0123456789ABCDEF/out");
assert_eq!(
unit_dir_name(dashed, "my-mac").as_deref(),
Some("my-mac-0123456789ABCDEF")
);
for (path, pkg) in [
("t/target/debug/build/dmac-0123456789abcdef/out", "dmac"),
("/t/target/debug/build/dmac-0123456789abcdef/gen", "dmac"),
("/t/target/debug/deps/dmac-0123456789abcdef/out", "dmac"),
("/t/target/debug/build/dmac-0123456789abcdef/out", "other"),
("/t/target/debug/build/dmacx0123456789abcdef/out", "dmac"),
("/t/target/debug/build/dmac-0123456789abcde/out", "dmac"),
("/t/target/debug/build/dmac-0123456789abcdef0/out", "dmac"),
("/t/target/debug/build/dmac-0123456789abcdeg/out", "dmac"),
("/out", "dmac"),
] {
assert_eq!(unit_dir_name(Path::new(path), pkg), None, "{path} {pkg}");
}
}
#[test]
fn directives_allow_only_non_exporting_non_linking_lines() {
for prefix in ["cargo:", "cargo::"] {
for directive in ALLOWED_DIRECTIVES {
let line = format!("{prefix}{directive}=value");
assert!(directives_allow_alias(&line), "{line}");
}
for directive in [
"metadata=KEY=V",
"KEY=V",
"rustc-link-lib=z",
"rustc-link-search=native=/x",
"rustc-link-arg=-v",
"rustc-flags=-l z",
"rustc-cdylib-link-arg=-v",
"error=boom",
"rustc-envx=A=B",
] {
let line = format!("{prefix}{directive}");
assert!(!directives_allow_alias(&line), "{line}");
}
}
assert!(directives_allow_alias(""));
assert!(directives_allow_alias("plain output\n cargo:KEY=V\n"));
assert!(directives_allow_alias(
"cargo:rustc-env=A=B\ncargo::rerun-if-changed=build.rs\n"
));
assert!(!directives_allow_alias(
"cargo:rustc-env=A=B\ncargo:KEY=V\ncargo:warning=w\n"
));
}
#[test]
fn alias_dir_is_per_unit_under_the_layout() {
assert_eq!(
alias_dir(Path::new("/c/out-dirs"), "dmac-0123456789abcdef"),
Path::new("/c/out-dirs/v1/d/dmac-0123456789abcdef/out")
);
}
#[test]
fn env_rewrites_point_values_at_or_under_out_dir_at_the_alias() {
let real = Path::new("/t/out");
let alias = Path::new("/c/d/out");
let rewrites = env_rewrites(
&os(&[
("OUT_DIR", "/t/out"),
("DEBUG_OUTPUT_DIR", "/t/out/"),
("GEN", "/t/out/sub/gen.rs"),
("HOME", "/home/u"),
]),
&strings(&["--crate-name", "dmac", "-L", "/t/deps"]),
&[real],
alias,
)
.expect("no conflict");
assert_eq!(
rewrites,
os(&[
("OUT_DIR", "/c/d/out"),
("DEBUG_OUTPUT_DIR", "/c/d/out"),
("GEN", "/c/d/out/sub/gen.rs"),
])
);
}
#[test]
fn env_rewrites_match_any_spelling_of_out_dir() {
let rewrites = env_rewrites(
&os(&[("OUT_DIR", "/t/out"), ("CANON", "/private/t/out/x")]),
&[],
&[Path::new("/t/out"), Path::new("/private/t/out")],
Path::new("/c/out"),
)
.unwrap();
assert_eq!(
rewrites,
os(&[("OUT_DIR", "/c/out"), ("CANON", "/c/out/x")])
);
}
#[test]
fn env_rewrites_refuse_any_other_mention_of_out_dir() {
let real = [Path::new("/t/out")];
let alias = Path::new("/c/out");
for value in ["/t/out2", "-I/t/out", "/x:/t/out/y"] {
assert_eq!(
env_rewrites(
&os(&[("OUT_DIR", "/t/out"), ("V", value)]),
&[],
&real,
alias
),
None,
"{value}"
);
}
assert_eq!(
env_rewrites(
&os(&[("OUT_DIR", "/t/out")]),
&strings(&["-L", "native=/t/out"]),
&real,
alias
),
None
);
assert_eq!(
env_rewrites(
&os(&[("OUT_DIR", "/t/out")]),
&strings(&["/t/o"]),
&real,
alias
),
Some(os(&[("OUT_DIR", "/c/out")]))
);
}
#[test]
fn env_rewrites_handle_non_utf8_values() {
use std::os::unix::ffi::OsStringExt;
let real = [Path::new("/t/out")];
let under = vec![(
OsString::from("RAW"),
OsString::from_vec(b"/t/out/\xff".to_vec()),
)];
assert_eq!(
env_rewrites(&under, &[], &real, Path::new("/c/out")),
Some(vec![(
OsString::from("RAW"),
OsString::from_vec(b"/c/out/\xff".to_vec())
)])
);
let mention = vec![(
OsString::from("RAW"),
OsString::from_vec(b"\xff/t/out".to_vec()),
)];
assert_eq!(
env_rewrites(&mention, &[], &real, Path::new("/c/out")),
None
);
}
#[test]
fn forced_by_user_matches_this_crates_out_dir_or_a_rewritten_var() {
let rewrites = os(&[("OUT_DIR", "/c/out"), ("DEBUG_OUTPUT_DIR", "/c/out")]);
let forced = |entries: &[&str]| forced_by_user(&strings(entries), "dmac", &rewrites);
assert!(forced(&["dmac:OUT_DIR"]));
assert!(forced(&["dmac:DEBUG_OUTPUT_DIR"]));
assert!(forced(&["other:OUT_DIR", "dmac:DEBUG_OUTPUT_DIR"]));
assert!(!forced(&["other:OUT_DIR"]));
assert!(!forced(&["dmac:HOME"]));
assert!(!forced(&["OUT_DIR", "DEBUG_OUTPUT_DIR"]));
assert!(!forced(&[]));
assert!(forced_by_user(&strings(&["dmac:OUT_DIR"]), "dmac", &[]));
}
#[test]
fn root_location_must_be_outside_the_checkout() {
let root = Path::new("/c/out-dirs");
assert!(root_location_ok(root, &[]));
assert!(root_location_ok(
root,
&[PathBuf::from("/w"), PathBuf::from("/w/target")]
));
assert!(!root_location_ok(root, &[PathBuf::from("/c")]));
assert!(!root_location_ok(
root,
&[PathBuf::from("/w"), PathBuf::from("/c/out-dirs")]
));
assert!(root_location_ok(root, &[PathBuf::from("/c/out")]));
}
#[test]
fn consumer_kind_is_proc_macro_only_for_a_proc_macro_itself() {
assert_eq!(
consumer_kind(&strings(&["proc-macro"]), false),
ConsumerKind::ProcMacro
);
assert_eq!(
consumer_kind(&strings(&["proc-macro"]), true),
ConsumerKind::Other
);
assert_eq!(consumer_kind(&[], true), ConsumerKind::Other);
assert_eq!(consumer_kind(&[], false), ConsumerKind::Other);
assert_eq!(
consumer_kind(&strings(&["lib"]), false),
ConsumerKind::Other
);
assert_eq!(
consumer_kind(&strings(&["bin"]), false),
ConsumerKind::Other
);
assert_eq!(ConsumerKind::ProcMacro.marker(), "pm");
assert_eq!(ConsumerKind::Other.marker(), "other");
}
#[test]
fn lib_verdict_covers_every_evidence_combination() {
assert_eq!(lib_verdict(false, false), LibVerdict::Unseen);
assert_eq!(lib_verdict(true, false), LibVerdict::MacrosOnly);
assert_eq!(lib_verdict(false, true), LibVerdict::Other);
assert_eq!(lib_verdict(true, true), LibVerdict::Other);
}
#[test]
fn permission_failures_are_recognized() {
assert!(is_permission_failure(
"error: Permission denied (os error 13)"
));
assert!(is_permission_failure("Permission denied"));
assert!(is_permission_failure("failed: os error 13"));
assert!(!is_permission_failure("error[E0425]: cannot find value"));
assert!(!is_permission_failure("os error 2"));
}
#[test]
fn store_allowed_needs_a_clean_alias_and_no_root_file() {
assert!(store_allowed(true, false));
assert!(!store_allowed(false, false));
assert!(!store_allowed(true, true));
assert!(!store_allowed(false, true));
}
#[test]
fn alias_unit_of_names_the_alias_a_path_is_in() {
let root = Path::new("/c/out-dirs");
assert_eq!(
alias_unit_of(Path::new("/c/out-dirs/v1/d/dmac-0123/out/x.rs"), root),
Some("dmac-0123")
);
assert_eq!(
alias_unit_of(Path::new("/c/out-dirs/v1/libs/x"), root),
None
);
assert_eq!(alias_unit_of(Path::new("/c/out-dirs/v1/d"), root), None);
}
#[test]
fn unit_package_needs_a_name_and_cargos_hash() {
assert_eq!(unit_package("sp-api-0123456789abcdef"), Some("sp-api"));
assert_eq!(unit_package("dmac-0123456789ABCDEF"), Some("dmac"));
for unit in [
"",
"dmac",
"-0123456789abcdef",
"dmac-0123456789abcde",
"dmac-0123456789abcdef0",
"dmac-0123456789abcdeg",
] {
assert_eq!(unit_package(unit), None, "{unit}");
}
}
#[test]
fn alias_units_in_finds_every_alias_spelled_in_bytes() {
let root = Path::new("/c/out-dirs");
let mut bytes = b"\x7fELF\0/c/out-dirs/v1/d/sp-api-0123456789abcdef/out/x.rs\xff".to_vec();
bytes.extend(b"/c/out-dirs/v1/d/my_mac-fedcba9876543210\"");
bytes.extend(b"/c/out-dirs/v1/d/short-0123/out");
bytes.extend(b"/c/out-dirs/v1/libs/lib-0123456789abcdef/pm");
bytes.extend(b"/d/out-dirs/v1/d/elsewhere-0123456789abcdef/out");
bytes.extend(b"/c/out-dirs/v1/d/end-0123456789abcdef");
assert_eq!(
alias_units_in(&bytes, root).into_iter().collect::<Vec<_>>(),
[
"end-0123456789abcdef",
"my_mac-fedcba9876543210",
"sp-api-0123456789abcdef"
]
);
assert!(alias_units_in(b"/c/out-dirs/v1/d/", root).is_empty());
assert!(alias_units_in(b"", root).is_empty());
}
#[test]
fn permission_hint_names_the_packages_to_clean() {
let units = |names: &[&str]| -> BTreeSet<String> {
names.iter().map(|name| name.to_string()).collect()
};
let one = permission_hint(&units(&["sp-api-0123456789abcdef"]));
assert!(one.contains("`cargo clean -p sp-api` and"), "{one}");
assert!(one.contains("KACHE_OUT_DIR_ALIAS=0"), "{one}");
let two = permission_hint(&units(&[
"wasm-macro-0123456789abcdef",
"expander-0123456789abcdef",
"expander-fedcba9876543210",
]));
assert!(
two.contains("`cargo clean -p expander -p wasm-macro` and"),
"{two}"
);
let generic = permission_hint(&units(&[]));
assert!(
generic.contains("`cargo clean -p <macro crate>` and"),
"{generic}"
);
}
#[test]
fn extern_files_sit_next_to_the_extern() {
assert_eq!(
extern_files(Path::new("/t/deps/libhelper-0123.rmeta")),
Some([
PathBuf::from("/t/deps/libhelper-0123.rlib"),
PathBuf::from("/t/deps/libhelper-0123.rmeta"),
PathBuf::from("/t/deps/libhelper-0123.kache-alias"),
])
);
}
#[test]
fn sidecar_path_uses_the_unit_output_stem() {
let args = RustcCompiler::new()
.parse(&strings(&[
"rustc",
"--crate-name",
"helper",
"--crate-type",
"lib",
"src/lib.rs",
"--out-dir",
"/t/deps",
"-C",
"extra-filename=-0123456789abcdef",
]))
.unwrap();
assert_eq!(
sidecar_path(&args),
Some(PathBuf::from(
"/t/deps/libhelper-0123456789abcdef.kache-alias"
))
);
}
mod fs {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn mode(path: &Path) -> u32 {
std::fs::symlink_metadata(path)
.unwrap()
.permissions()
.mode()
& 0o777
}
#[test]
fn ensure_alias_dir_creates_it_read_only() {
let tmp = tempfile::tempdir().unwrap();
let euid = effective_uid();
let dir = alias_dir(tmp.path(), "dmac-0123456789abcdef");
assert!(ensure_alias_dir(&dir, euid));
assert_eq!(mode(&dir) & 0o222, 0);
assert_eq!(mode(dir.parent().unwrap()) & 0o077, 0);
assert_eq!(alias_state(&dir, euid), AliasState::Usable);
assert!(ensure_alias_dir(&dir, euid));
if euid != 0 {
let error = std::fs::write(dir.join("x"), b"x").unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::PermissionDenied);
}
}
#[test]
fn ensure_alias_dir_refuses_what_it_did_not_make() {
let tmp = tempfile::tempdir().unwrap();
let euid = effective_uid();
let foreign = alias_dir(tmp.path(), "a-0123456789abcdef");
assert!(ensure_alias_dir(&foreign, euid));
assert!(!ensure_alias_dir(&foreign, euid + 1));
assert_eq!(alias_state(&foreign, euid + 1), AliasState::Unusable);
let target = tmp.path().join("elsewhere");
std::fs::create_dir(&target).unwrap();
std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o555)).unwrap();
let link = alias_dir(tmp.path(), "b-0123456789abcdef");
std::fs::create_dir_all(link.parent().unwrap()).unwrap();
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(!ensure_alias_dir(&link, euid));
let full = alias_dir(tmp.path(), "c-0123456789abcdef");
std::fs::create_dir_all(&full).unwrap();
std::fs::write(full.join("x"), b"x").unwrap();
std::fs::set_permissions(&full, std::fs::Permissions::from_mode(0o555)).unwrap();
assert!(!ensure_alias_dir(&full, euid));
std::fs::set_permissions(&full, std::fs::Permissions::from_mode(0o755)).unwrap();
let writable = alias_dir(tmp.path(), "d-0123456789abcdef");
std::fs::create_dir_all(&writable).unwrap();
std::fs::set_permissions(&writable, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(!ensure_alias_dir(&writable, euid));
assert_eq!(
alias_state(&tmp.path().join("missing"), euid),
AliasState::Absent
);
let file = tmp.path().join("file");
std::fs::write(&file, b"").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o444)).unwrap();
assert_eq!(alias_state(&file, euid), AliasState::Unusable);
assert_eq!(alias_state(&file.join("out"), euid), AliasState::Unusable);
assert!(!ensure_alias_dir(&file.join("d/out"), euid));
assert!(!ensure_alias_dir(Path::new("/"), euid));
}
#[test]
fn private_dirs_are_owned_and_closed_to_others() {
let tmp = tempfile::tempdir().unwrap();
let euid = effective_uid();
let dir = tmp.path().join("root");
create_private_dir_all(&dir).unwrap();
assert_eq!(mode(&dir), 0o700);
assert!(is_private_dir(&dir, euid));
assert!(!is_private_dir(&dir, euid + 1));
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o720)).unwrap();
assert!(!is_private_dir(&dir, euid));
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o702)).unwrap();
assert!(!is_private_dir(&dir, euid));
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o755)).unwrap();
assert!(is_private_dir(&dir, euid));
let link = tmp.path().join("link");
std::os::unix::fs::symlink(&dir, &link).unwrap();
assert!(!is_private_dir(&link, euid));
assert!(!is_private_dir(&tmp.path().join("missing"), euid));
}
fn aliased_lib(tmp: &Path) -> (PathBuf, PathBuf, [PathBuf; 3]) {
let root = tmp.join("out-dirs");
register_candidate(&root, "0123456789abcdef").unwrap();
let deps = tmp.join("target/debug/deps");
std::fs::create_dir_all(&deps).unwrap();
let rlib = deps.join("libhelper-0123456789abcdef.rlib");
let files = extern_files(&rlib).unwrap();
for file in &files {
std::fs::write(file, b"x").unwrap();
}
(root, rlib, files)
}
#[test]
fn tripwire_removes_an_aliased_lib_an_other_consumer_links() {
let tmp = tempfile::tempdir().unwrap();
let (root, rlib, files) = aliased_lib(tmp.path());
let mut stderr = Vec::new();
let exit = consumer_checks(
&root,
&[dep("helper", Some(&rlib))],
ConsumerKind::Other,
&mut stderr,
);
assert_eq!(exit, Some(1));
for file in &files {
assert!(!file.exists(), "{} survived", file.display());
}
assert!(libs_dir(&root).join("0123456789abcdef/other").exists());
let message = String::from_utf8(stderr).unwrap();
assert_eq!(message.lines().count(), 1, "{message}");
assert!(message.contains("helper"), "{message}");
assert!(message.contains("Run the build again"), "{message}");
}
#[test]
fn a_proc_macro_consumer_only_records_evidence() {
let tmp = tempfile::tempdir().unwrap();
let (root, rlib, files) = aliased_lib(tmp.path());
let mut stderr = Vec::new();
let exit = consumer_checks(
&root,
&[dep("helper", Some(&rlib)), dep("proc_macro", None)],
ConsumerKind::ProcMacro,
&mut stderr,
);
assert_eq!(exit, None);
assert!(files.iter().all(|file| file.exists()));
assert!(stderr.is_empty());
assert_eq!(
read_verdict(&root, "0123456789abcdef"),
LibVerdict::MacrosOnly
);
}
#[test]
fn an_unaliased_lib_does_not_trip_but_is_recorded() {
let tmp = tempfile::tempdir().unwrap();
let (root, rlib, [_, _, sidecar]) = aliased_lib(tmp.path());
std::fs::remove_file(&sidecar).unwrap();
let unlisted = tmp
.path()
.join("target/debug/deps/libother-fedcba9876543210.rlib");
std::fs::write(unlisted.with_extension("kache-alias"), b"").unwrap();
let mut stderr = Vec::new();
let exit = consumer_checks(
&root,
&[dep("helper", Some(&rlib)), dep("other", Some(&unlisted))],
ConsumerKind::Other,
&mut stderr,
);
assert_eq!(exit, None);
assert!(rlib.exists());
assert_eq!(read_verdict(&root, "0123456789abcdef"), LibVerdict::Other);
assert!(!libs_dir(&root).join("fedcba9876543210").exists());
}
fn parse(args: &[&str]) -> RustcArgs {
RustcCompiler::new().parse(&strings(args)).unwrap()
}
#[test]
fn a_proc_macro_test_harness_is_a_native_consumer() {
let tmp = tempfile::tempdir().unwrap();
let (root, rlib, files) = aliased_lib(tmp.path());
std::fs::remove_file(&files[2]).unwrap();
let harness = parse(&[
"rustc",
"--crate-name",
"pm2",
"src/lib.rs",
"--test",
"--extern",
"proc_macro",
"--extern",
&format!("helper={}", rlib.display()),
]);
let mut stderr = Vec::new();
assert_eq!(checks_before_decision(&harness, &root, &mut stderr), None);
assert_eq!(read_verdict(&root, "0123456789abcdef"), LibVerdict::Other);
let (root, _, files) = aliased_lib(&tmp.path().join("again"));
let harness = parse(&[
"rustc",
"--crate-name",
"pm2",
"src/lib.rs",
"--test",
"--extern",
"proc_macro",
"--extern",
&format!("helper={}", files[0].display()),
]);
assert_eq!(
checks_before_decision(&harness, &root, &mut stderr),
Some(1)
);
assert!(files.iter().all(|file| !file.exists()));
}
#[test]
fn the_marker_an_aliased_build_left_goes_before_the_decision() {
let tmp = tempfile::tempdir().unwrap();
let (root, rlib, [_, _, sidecar]) = aliased_lib(tmp.path());
let deps = rlib.parent().unwrap();
let lib = parse(&[
"rustc",
"--crate-name",
"helper",
"--crate-type",
"lib",
"src/lib.rs",
"--out-dir",
deps.to_str().unwrap(),
"-C",
"extra-filename=-0123456789abcdef",
]);
let mut stderr = Vec::new();
assert_eq!(checks_before_decision(&lib, &root, &mut stderr), None);
assert!(!sidecar.exists());
assert!(rlib.exists());
assert!(stderr.is_empty());
assert_eq!(checks_before_decision(&lib, &root, &mut stderr), None);
}
#[test]
fn a_candidate_stays_registered_only_while_its_key_bakes_out_dir() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("out-dirs");
settle_candidate(&root, "aaaa", true).unwrap();
assert!(listed_candidates(&root).contains("aaaa"));
std::fs::write(libs_dir(&root).join("aaaa/pm"), b"").unwrap();
settle_candidate(&root, "aaaa", true).unwrap();
assert_eq!(read_verdict(&root, "aaaa"), LibVerdict::MacrosOnly);
settle_candidate(&root, "aaaa", false).unwrap();
assert!(listed_candidates(&root).is_empty());
assert_eq!(read_verdict(&root, "aaaa"), LibVerdict::Unseen);
settle_candidate(&root, "aaaa", false).unwrap();
std::fs::write(libs_dir(&root).join("file"), b"").unwrap();
assert!(settle_candidate(&root, "file/x", false).is_err());
}
#[test]
fn a_path_less_write_failure_names_the_aliases_a_loaded_macro_bakes() {
use std::os::unix::ffi::OsStrExt;
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("out-dirs");
create_private_dir_all(&root).unwrap();
let deps = tmp.path().join("deps");
std::fs::create_dir_all(&deps).unwrap();
let spelled = |unit: &str| alias_dir(&root, unit).as_os_str().as_bytes().to_vec();
let dylib = deps.join(format!(
"libwasm_macro-0123.{}",
std::env::consts::DLL_EXTENSION
));
let mut bytes = b"\x7fELF\0".to_vec();
bytes.extend(spelled("wasm-macro-0123456789abcdef"));
bytes.extend(b"\0\x01");
bytes.extend(spelled("expander-fedcba9876543210"));
bytes.extend(b"/gen.rs\0");
std::fs::write(&dylib, bytes).unwrap();
let rlib = deps.join("libother-0123.rlib");
std::fs::write(&rlib, spelled("other-0123456789abcdef")).unwrap();
let stderr = "error: proc macro panicked\n = help: message: called \
`Result::unwrap()` on an `Err` value: Os { code: 13, kind: \
PermissionDenied, message: \"Permission denied\" }";
let externs = [
dep("wasm_macro", Some(&dylib)),
dep("other", Some(&rlib)),
dep("missing", Some(&deps.join("libgone-0123.dylib"))),
dep("proc_macro", None),
];
let hint = failure_hint(stderr, &externs, &root, None).unwrap();
assert!(
hint.contains("`cargo clean -p expander -p wasm-macro` and"),
"{hint}"
);
assert!(!root.join(LAYOUT).join("deny").exists());
let with_path = format!(
"{stderr}\n{}",
alias_dir(&root, "sp-api-0123456789abcdef").display()
);
let hint = failure_hint(&with_path, &externs, &root, None).unwrap();
assert!(
hint.contains("`cargo clean -p expander -p sp-api -p wasm-macro` and"),
"{hint}"
);
let denied: Vec<_> = std::fs::read_dir(root.join(LAYOUT).join("deny"))
.unwrap()
.map(|entry| entry.unwrap().file_name())
.collect();
assert_eq!(denied, ["sp-api-0123456789abcdef"]);
assert_eq!(
failure_hint("error: mismatched types", &externs, &root, None),
None
);
assert_eq!(
failure_hint(stderr, &externs, &tmp.path().join("gone"), None),
None
);
}
#[test]
fn a_write_failure_denies_its_own_alias_and_the_one_its_stderr_names() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("out-dirs");
create_private_dir_all(&root).unwrap();
let named = alias_dir(&root, "sp-api-0123456789abcdef").join("x.rs");
let stderr = format!(
"error: failed to write {}: Permission denied (os error 13)",
named.display()
);
let hint = failure_hint(&stderr, &[], &root, Some("dmac-fedcba9876543210")).unwrap();
assert!(
hint.contains("`cargo clean -p dmac -p sp-api` and"),
"{hint}"
);
assert!(deny_path(&root, "sp-api-0123456789abcdef").is_file());
assert!(deny_path(&root, "dmac-fedcba9876543210").is_file());
let other = tempfile::tempdir().unwrap();
let empty = other.path().join("out-dirs");
create_private_dir_all(&empty).unwrap();
let generic = failure_hint("os error 13", &[], &empty, None).unwrap();
assert!(generic.contains("-p <macro crate>`"), "{generic}");
assert!(!empty.join(LAYOUT).join("deny").exists());
}
#[test]
fn candidates_deny_markers_and_sidecars_round_trip() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("out-dirs");
assert!(listed_candidates(&root).is_empty());
register_candidate(&root, "aaaa").unwrap();
register_candidate(&root, "bbbb").unwrap();
assert_eq!(
listed_candidates(&root).into_iter().collect::<Vec<_>>(),
["aaaa", "bbbb"]
);
assert_eq!(read_verdict(&root, "aaaa"), LibVerdict::Unseen);
deny(&root, "dmac-0123").unwrap();
assert!(deny_path(&root, "dmac-0123").is_file());
let sidecar = tmp.path().join("libx-0123.kache-alias");
write_sidecar(&sidecar, true).unwrap();
assert!(sidecar.is_file());
write_sidecar(&sidecar, false).unwrap();
assert!(!sidecar.exists());
write_sidecar(&sidecar, false).unwrap();
assert!(write_sidecar(&tmp.path().join("no/such/dir/x"), true).is_err());
let blocker = tmp.path().join("dir.kache-alias");
std::fs::create_dir(&blocker).unwrap();
assert!(write_sidecar(&blocker, false).is_err());
}
#[test]
fn build_output_and_out_dir_checks_read_the_build_dir() {
let tmp = tempfile::tempdir().unwrap();
let build = tmp.path().join("dmac-0123456789abcdef");
let out = build.join("out");
let output = build.join("output");
std::fs::create_dir_all(&out).unwrap();
assert!(dir_has_no_entries(&out));
assert!(!dir_has_no_entries(&tmp.path().join("missing")));
assert!(!build_output_allows_alias(&output));
std::fs::write(&output, "cargo:rustc-env=A=B\n").unwrap();
assert!(build_output_allows_alias(&output));
std::fs::write(&output, "cargo:KEY=V\n").unwrap();
assert!(!build_output_allows_alias(&output));
let limit: usize = 64 << 10;
std::fs::write(&output, "x".repeat(limit)).unwrap();
assert!(build_output_allows_alias(&output));
std::fs::write(&output, "x".repeat(limit + 1)).unwrap();
assert!(!build_output_allows_alias(&output));
std::fs::write(out.join("gen.rs"), b"").unwrap();
assert!(!dir_has_no_entries(&out));
}
struct Layout {
_tmp: tempfile::TempDir,
root: PathBuf,
out_dir: PathBuf,
env: Vec<(OsString, OsString)>,
args: RustcArgs,
}
fn layout(crate_type: &str) -> Layout {
layout_in(crate_type, false)
}
fn layout_in(crate_type: &str, per_unit: bool) -> Layout {
let tmp = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(tmp.path()).unwrap();
let manifest = base.join("registry/src/index/dmac");
let target = base.join("w/target");
let (build, stdout, rustc_out) = if per_unit {
let build = target.join("debug/build/dmac/0123456789abcdef");
let stdout = build.join("run/stdout");
let rustc_out = target.join("debug/build/dmac/fedcba9876543210/out");
(build, stdout, rustc_out)
} else {
let build = target.join("debug/build/dmac-0123456789abcdef");
let stdout = build.join("output");
(build, stdout, target.join("debug/deps"))
};
let out_dir = build.join("out");
std::fs::create_dir_all(&manifest).unwrap();
std::fs::create_dir_all(&out_dir).unwrap();
std::fs::create_dir_all(stdout.parent().unwrap()).unwrap();
std::fs::create_dir_all(&rustc_out).unwrap();
std::fs::write(&stdout, "cargo:rustc-env=DEBUG_OUTPUT_DIR=x\n").unwrap();
let env = vec![
(
OsString::from("CARGO_MANIFEST_DIR"),
manifest.clone().into(),
),
(OsString::from("CARGO_PKG_NAME"), OsString::from("dmac")),
(OsString::from("OUT_DIR"), out_dir.clone().into()),
(OsString::from("DEBUG_OUTPUT_DIR"), out_dir.clone().into()),
];
let args = RustcCompiler::new()
.parse(&strings(&[
"rustc",
"--crate-name",
"dmac",
"--crate-type",
crate_type,
manifest.join("src/lib.rs").to_str().unwrap(),
"--out-dir",
rustc_out.to_str().unwrap(),
"-C",
"extra-filename=-fedcba9876543210",
]))
.unwrap();
Layout {
root: base.join("cache/out-dirs"),
_tmp: tmp,
out_dir,
env,
args,
}
}
fn gather(layout: &Layout, forced: &[&str]) -> Detected {
UnitFacts::gather(
&layout.args,
&layout.env,
false,
&strings(forced),
&layout.root,
effective_uid(),
)
}
#[test]
fn gather_reads_a_registry_unit_with_an_empty_out_dir() {
let layout = layout("proc-macro");
let detected = gather(&layout, &[]);
assert_eq!(
detected.facts,
UnitFacts {
crate_types: strings(&["proc-macro"]),
unit_dir: Some("dmac-0123456789abcdef".into()),
..passing_facts(&[])
}
);
let alias = alias_dir(&layout.root, "dmac-0123456789abcdef");
assert_eq!(
detected.rewrites,
vec![
(OsString::from("OUT_DIR"), alias.clone().into_os_string()),
(OsString::from("DEBUG_OUTPUT_DIR"), alias.into_os_string()),
]
);
assert_eq!(detected.alias, AliasState::Absent);
assert_eq!(detected.verdict, LibVerdict::Unseen);
assert!(is_private_dir(&layout.root, effective_uid()));
}
#[test]
fn gather_reads_the_same_unit_in_cargos_per_unit_layout() {
let layout = layout_in("proc-macro", true);
let detected = gather(&layout, &[]);
assert_eq!(
detected.facts,
UnitFacts {
crate_types: strings(&["proc-macro"]),
..passing_facts(&[])
},
"the alias is named as in the legacy layout, and the directives \
come from run/stdout"
);
std::fs::write(
layout.out_dir.parent().unwrap().join("run/stdout"),
"cargo:KEY=V\n",
)
.unwrap();
assert!(!gather(&layout, &[]).facts.directives_ok);
}
#[test]
fn gather_reads_evidence_denials_and_the_alias_state() {
let layout = layout("lib");
register_candidate(&layout.root, "fedcba9876543210").unwrap();
std::fs::write(libs_dir(&layout.root).join("fedcba9876543210/pm"), b"").unwrap();
let alias = alias_dir(&layout.root, "dmac-0123456789abcdef");
assert!(ensure_alias_dir(&alias, effective_uid()));
let detected = gather(&layout, &[]);
assert_eq!(detected.verdict, LibVerdict::MacrosOnly);
assert_eq!(detected.alias, AliasState::Usable);
assert!(!detected.facts.denied);
assert_eq!(
decide(&detected.facts, detected.verdict, detected.alias),
Ok(Tier::Lib)
);
deny(&layout.root, "dmac-0123456789abcdef").unwrap();
assert!(gather(&layout, &[]).facts.denied);
assert!(gather(&layout, &["dmac:DEBUG_OUTPUT_DIR"]).facts.forced);
}
#[test]
fn gather_needs_a_private_root_outside_the_checkout() {
let mut layout = layout("proc-macro");
assert!(gather(&layout, &[]).facts.root_safe);
std::fs::set_permissions(&layout.root, std::fs::Permissions::from_mode(0o777)).unwrap();
assert!(!gather(&layout, &[]).facts.root_safe);
let target = layout.args.target_dir().unwrap();
layout.root = target.join("out-dirs");
assert!(!gather(&layout, &[]).facts.root_safe);
assert!(is_private_dir(&layout.root, effective_uid()));
let workspace = target.parent().unwrap().to_path_buf();
layout.root = workspace.join(".kache/out-dirs");
assert!(gather(&layout, &[]).facts.root_safe);
std::fs::write(workspace.join("Cargo.toml"), "[workspace]\n").unwrap();
assert!(!gather(&layout, &[]).facts.root_safe);
assert!(checkout_dirs(&layout.args).contains(&workspace));
}
#[test]
fn gather_rules_the_unit_out_on_what_it_finds() {
let mut layout = layout("proc-macro");
std::fs::write(layout.out_dir.join("gen.rs"), b"").unwrap();
let full = gather(&layout, &[]);
assert!(!full.facts.out_dir_empty);
assert!(full.facts.directives_ok);
std::fs::remove_file(layout.out_dir.join("gen.rs")).unwrap();
std::fs::write(
layout.out_dir.parent().unwrap().join("output"),
"cargo:KEY=V\n",
)
.unwrap();
assert!(!gather(&layout, &[]).facts.directives_ok);
layout.env.push((
OsString::from("CFLAGS"),
format!("-I{}", layout.out_dir.display()).into(),
));
let conflict = gather(&layout, &[]);
assert!(!conflict.facts.env_ok);
assert!(conflict.rewrites.is_empty());
}
#[test]
fn gather_reads_nothing_past_the_shape_outside_the_registry() {
let mut layout = layout("proc-macro");
layout.env[0].1 = OsString::from("/w/crates/dmac");
let local = gather(&layout, &[]);
assert!(!local.facts.registry);
assert!(local.facts.unit_dir.is_some());
assert!(!local.facts.out_dir_empty);
assert!(!local.facts.root_safe);
assert!(!layout.root.exists());
}
}
}