use crate::args::RustcArgs;
use crate::path_normalizer::{PathNormalizer, check_for_path_leak};
use anyhow::{Context, Result};
pub(crate) use kache_format::{is_valid_cache_key, is_valid_crate_name};
pub(crate) use kache_store::file_hash::*;
use std::borrow::Cow;
use std::cell::{Cell, OnceCell, RefCell};
use std::collections::{BTreeMap, HashMap};
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use crate::key_env::KeyEnv;
mod apple_install_name;
pub(crate) use kache_format::CACHE_KEY_VERSION;
fn normalize_rustflags(rustflags: &str) -> String {
rustflags.split_whitespace().collect::<Vec<_>>().join(" ")
}
const REMAP_FROM_SENTINEL: &str = "<REMAP_FROM>";
fn scrub_remap_from_prefixes<'a, I>(tokens: I) -> Vec<String>
where
I: IntoIterator<Item = &'a str>,
{
const EQ_FLAGS: [&str; 4] = [
"--remap-path-prefix=",
"-ffile-prefix-map=",
"-fdebug-prefix-map=",
"-fmacro-prefix-map=",
];
let mut out = Vec::new();
let mut iter = tokens.into_iter();
while let Some(tok) = iter.next() {
if let Some(flag) = EQ_FLAGS.iter().find(|f| tok.starts_with(**f)) {
out.push(format!("{flag}{}", scrub_remap_value(&tok[flag.len()..])));
} else if tok == "--remap-path-prefix" {
out.push(tok.to_string());
if let Some(value) = iter.next() {
out.push(scrub_remap_value(value));
}
} else {
out.push(tok.to_string());
}
}
out
}
fn scrub_remap_value(value: &str) -> String {
match value.rsplit_once('=') {
Some((_from, to)) => format!("{REMAP_FROM_SENTINEL}={to}"),
None => value.to_string(),
}
}
fn normalize_direct_remap_value(value: &str, path_normalizer: &PathNormalizer) -> String {
match value.rsplit_once('=') {
Some((from, to)) => format!("{}={to}", path_normalizer.normalize(from)),
None => value.to_string(),
}
}
pub(crate) fn apply_key_salt(base: String, salt: Option<&str>, label: &str) -> String {
match salt {
Some(salt) if !salt.is_empty() => {
let keyed = fold_labeled(base, "key_salt", salt);
tracing::trace!(
"[key:{label}] key_salt={salt:?} -> {}",
&keyed[..keyed.len().min(16)]
);
keyed
}
_ => base,
}
}
pub(crate) fn apply_key_env_vars(base: String, patterns: &[String], label: &str) -> String {
if patterns.is_empty() {
return base;
}
let (matched, matched_names) = matching_key_env_vars(patterns);
let keyed = fold_labeled(base, "key_env_vars", &key_env_digest(patterns, matched));
tracing::trace!(
"[key:{label}] key_env_vars patterns={patterns:?} matched={matched_names:?} -> {}",
&keyed[..keyed.len().min(16)]
);
keyed
}
pub(crate) fn key_env_guard(patterns: &[String]) -> Option<String> {
(!patterns.is_empty()).then(|| {
let (matched, _) = matching_key_env_vars(patterns);
key_env_digest(patterns, matched)
})
}
type RawEnvPair = (Vec<u8>, Vec<u8>);
fn matching_key_env_vars(patterns: &[String]) -> (Vec<RawEnvPair>, Vec<String>) {
let mut matched: Vec<RawEnvPair> = Vec::new();
let mut matched_names: Vec<String> = Vec::new();
for (name, value) in std::env::vars_os() {
let lossy = name.to_string_lossy();
if !key_env_var_matches(patterns, &lossy) {
continue;
}
matched_names.push(lossy.into_owned());
matched.push((env_name_key_bytes(&name), env_os_key_bytes(&value)));
}
(matched, matched_names)
}
fn key_env_digest(patterns: &[String], mut matched: Vec<RawEnvPair>) -> String {
let mut names_seen = std::collections::HashSet::new();
let has_duplicate_names = matched
.iter()
.any(|(name, _)| !names_seen.insert(name.clone()));
if !has_duplicate_names {
matched.sort();
}
let mut hasher = blake3::Hasher::new();
for pattern in patterns {
fold_field(&mut hasher, b"key_env_pattern:", pattern.as_bytes());
}
for (name, value) in &matched {
fold_field(&mut hasher, b"key_env_name:", name);
fold_field(&mut hasher, b"key_env_val:", value);
}
hasher.finalize().to_hex().to_string()
}
pub(crate) fn env_os_key_bytes(value: &std::ffi::OsStr) -> Vec<u8> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
value.as_bytes().to_vec()
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
value.encode_wide().flat_map(u16::to_le_bytes).collect()
}
#[cfg(not(any(unix, windows)))]
{
value.to_string_lossy().into_owned().into_bytes()
}
}
pub(crate) fn env_name_key_bytes(name: &std::ffi::OsStr) -> Vec<u8> {
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
match name.to_str() {
Some(name) => name
.to_uppercase()
.encode_utf16()
.flat_map(u16::to_le_bytes)
.collect(),
None => name.encode_wide().flat_map(u16::to_le_bytes).collect(),
}
}
#[cfg(not(windows))]
env_os_key_bytes(name)
}
fn env_text_key_bytes(text: &std::ffi::OsStr) -> Vec<u8> {
match text.to_str() {
Some(utf8) => utf8.as_bytes().to_vec(),
None => {
let mut bytes = vec![0xff];
bytes.extend(env_os_key_bytes(text));
bytes
}
}
}
fn cargo_cfg_pairs(
vars: impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
let mut pairs: Vec<(std::ffi::OsString, std::ffi::OsString)> = vars
.filter(|(name, _)| name.to_string_lossy().starts_with("CARGO_CFG_"))
.collect();
pairs.sort_by_cached_key(|(name, _)| {
(name.to_string_lossy().into_owned(), env_os_key_bytes(name))
});
pairs
}
fn key_env_var_matches(patterns: &[String], name: &str) -> bool {
let name = name.as_bytes();
patterns
.iter()
.any(|pattern| match pattern.strip_suffix('*') {
Some(prefix) => {
let prefix = prefix.as_bytes();
name.len() >= prefix.len() && name[..prefix.len()].eq_ignore_ascii_case(prefix)
}
None => name.eq_ignore_ascii_case(pattern.as_bytes()),
})
}
pub(crate) fn fold_labeled(base: String, label: &str, value: &str) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(label.as_bytes());
hasher.update(b":");
hasher.update(value.as_bytes());
hasher.update(b"\x1f");
hasher.update(base.as_bytes());
hasher.finalize().to_hex().to_string()
}
fn fold_field<H: KeyFold>(hasher: &mut H, label: &[u8], value: &[u8]) {
hasher.update(label);
hasher.update(&(value.len() as u64).to_le_bytes());
hasher.update(value);
}
trait KeyFold {
fn update(&mut self, bytes: &[u8]);
}
impl KeyFold for blake3::Hasher {
fn update(&mut self, bytes: &[u8]) {
blake3::Hasher::update(self, bytes);
}
}
impl KeyFold for GroupedHasher {
fn update(&mut self, bytes: &[u8]) {
GroupedHasher::update(self, bytes);
}
}
pub(crate) const KEY_FIELD_HEX: usize = 16;
struct GroupedHasher {
main: blake3::Hasher,
groups: std::collections::BTreeMap<&'static str, blake3::Hasher>,
current: &'static str,
}
impl GroupedHasher {
fn new(initial_group: &'static str) -> Self {
GroupedHasher {
main: blake3::Hasher::new(),
groups: std::collections::BTreeMap::new(),
current: initial_group,
}
}
fn set_group(&mut self, group: &'static str) {
self.current = group;
}
fn update(&mut self, bytes: &[u8]) {
self.main.update(bytes);
self.groups.entry(self.current).or_default().update(bytes);
}
fn finalize_with_fields(self) -> (blake3::Hash, std::collections::BTreeMap<String, String>) {
let fields = self
.groups
.into_iter()
.map(|(group, hasher)| {
(
group.to_string(),
hasher.finalize().to_hex()[..KEY_FIELD_HEX].to_string(),
)
})
.collect();
(self.main.finalize(), fields)
}
}
#[derive(Debug, Default)]
pub struct KeyOutputs {
pub fields: Option<BTreeMap<String, String>>,
pub env_deps: Option<BTreeMap<String, String>>,
pub externs: Option<BTreeMap<String, String>>,
pub extern_units: Option<BTreeMap<String, String>>,
pub unit_id: Option<String>,
pub native_archives: Option<KeyedNativeArchives>,
pub dep_info: Option<DepInfo>,
pub tree_guard: Option<TreeGuard>,
pub bakes_out_dir: bool,
pub used_prediction: bool,
}
#[derive(Debug, Clone)]
pub struct TreeGuard {
pub digest: String,
walks: Vec<GuardWalk>,
}
impl TreeGuard {
pub(crate) fn held(&self) -> bool {
let _trace = crate::phase_trace::phase("tree_recheck");
let held = !self.walks.is_empty() && self.walks.iter().all(GuardWalk::holds);
crate::phase_trace::decision("tree_recheck", if held { "held" } else { "moved" });
held
}
}
#[cfg(test)]
impl TreeGuard {
pub(crate) fn stamping(digest: &str, root: &Path) -> Self {
let root = TreeRoot::new(root.to_path_buf(), b"root", &[]).walked();
let stamp = stamp_root(&root, CRATE_TREE_MAX_ENTRIES)
.expect("a tree to stamp")
.digest;
Self {
digest: digest.to_string(),
walks: vec![GuardWalk {
root,
max_entries: CRATE_TREE_MAX_ENTRIES,
stamp,
}],
}
}
}
#[derive(Debug, Clone)]
struct GuardWalk {
root: WalkedRoot,
max_entries: usize,
stamp: String,
}
impl GuardWalk {
fn holds(&self) -> bool {
stamp_root(&self.root, self.max_entries).is_some_and(|stamp| stamp.digest == self.stamp)
}
}
#[derive(Debug, Clone)]
struct WalkedRoot {
path: PathBuf,
role: Vec<u8>,
excluded: Vec<PathBuf>,
rules: crate::tree_stamp::StampRules,
}
#[derive(Debug)]
enum StampRefusal {
TooLarge(usize),
TooLargeTogether,
Unwalkable,
}
struct StampedRoot {
stamp: crate::tree_stamp::TreeStamp,
entries: usize,
small_files: Vec<FileFingerprint>,
}
fn guard_file_memo_eligible(size: i64, inode: i64) -> bool {
#[cfg(unix)]
{
size < MIN_PERSISTED_HASH_BYTES && inode != 0
}
#[cfg(not(unix))]
{
let _ = (size, inode);
false
}
}
fn stamp_roots(
roots: &[WalkedRoot],
max_entries: usize,
collect_small_files: bool,
) -> std::result::Result<Vec<StampedRoot>, StampRefusal> {
let mut stamps = Vec::with_capacity(roots.len());
for (index, root) in roots.iter().enumerate() {
#[cfg(test)]
BEFORE_STAMP.with(|hook| {
if let Some(hook) = hook.borrow_mut().as_mut() {
hook(&root.path);
}
});
let mut stamper = crate::tree_stamp::Stamper::new();
let mut budget = max_entries;
stamper.label(&root.role);
let mut small_files = Vec::new();
let mut observe = |path: PathBuf, metadata: &std::fs::Metadata| {
let size = i64::try_from(metadata.len()).unwrap_or(i64::MAX);
let inode = metadata_inode(metadata);
if collect_small_files && metadata.is_file() && guard_file_memo_eligible(size, inode) {
let absolute = if path.is_absolute() {
path
} else {
absolute_path(&path)
};
let Ok(path) = absolute.into_os_string().into_string() else {
return;
};
small_files.push(FileFingerprint {
path,
size,
mtime_ns: metadata_mtime_ns(metadata),
ctime_ns: metadata_ctime_ns(metadata),
inode,
});
}
};
match stamper.walk_observing(
&root.path,
&root.excluded,
root.rules,
&mut budget,
&mut observe,
) {
crate::tree_stamp::WalkOutcome::Fits => {
stamps.push(StampedRoot {
stamp: stamper.finish(),
entries: max_entries - budget,
small_files,
});
}
crate::tree_stamp::WalkOutcome::TooLarge => return Err(StampRefusal::TooLarge(index)),
crate::tree_stamp::WalkOutcome::Unreadable => return Err(StampRefusal::Unwalkable),
}
}
if stamps.iter().map(|root| root.entries).sum::<usize>() > max_entries {
return Err(StampRefusal::TooLargeTogether);
}
Ok(stamps)
}
fn stamp_root(root: &WalkedRoot, max_entries: usize) -> Option<crate::tree_stamp::TreeStamp> {
let stamped = stamp_roots(std::slice::from_ref(root), max_entries, false)
.ok()?
.pop()?;
Some(stamped.stamp)
}
#[cfg(test)]
type BeforeStamp = Box<dyn FnMut(&Path)>;
#[cfg(test)]
thread_local! {
static BEFORE_STAMP: std::cell::RefCell<Option<BeforeStamp>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
fn set_before_stamp(hook: Option<BeforeStamp>) {
BEFORE_STAMP.with(|slot| *slot.borrow_mut() = hook);
}
fn tree_guard_of(
file_hasher: &FileHasher<'_>,
digest: impl FnOnce() -> Option<String>,
) -> Option<TreeGuard> {
file_hasher.take_tree_walks();
let digest = digest()?;
Some(TreeGuard {
digest,
walks: file_hasher.take_tree_walks(),
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct KeyedNativeArchives {
pub archives: Vec<PathBuf>,
pub bundled: Vec<BundledArchive>,
pub dirs: Vec<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BundledArchive {
pub path: PathBuf,
pub packed: bool,
}
pub const EXTERN_UNREADABLE: &str = "(sysroot)";
pub(crate) const CRATE_TREE_MAX_ENTRIES: usize = 20_000;
pub(crate) fn crate_tree_digest(file_hasher: &FileHasher<'_>) -> Option<String> {
let manifest_dir = PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR")?);
let out_dir = std::env::var_os("OUT_DIR").map(PathBuf::from);
crate_tree_digest_in(manifest_dir, out_dir, file_hasher)
}
fn crate_tree_digest_in(
manifest_dir: PathBuf,
out_dir: Option<PathBuf>,
file_hasher: &FileHasher<'_>,
) -> Option<String> {
if !is_registry_package(&manifest_dir) {
return None;
}
let mut roots = vec![TreeRoot::new(
manifest_dir,
b"manifest_dir",
MANIFEST_DIR_SKIPPED,
)];
if let Some(out_dir) = out_dir {
roots.push(TreeRoot::new(out_dir, b"out_dir", &[]));
}
tree_digest(roots, file_hasher, CRATE_TREE_MAX_ENTRIES)
}
const MANIFEST_DIR_SKIPPED: &[&str] = &["target", ".git"];
const OUT_DIR_TREE_MAX_ENTRIES: usize = 256;
pub(crate) fn out_dir_tree_digest(out_dir: &Path, file_hasher: &FileHasher<'_>) -> Option<String> {
tree_digest(
vec![TreeRoot::new(out_dir.to_path_buf(), b"out_dir", &[])],
file_hasher,
OUT_DIR_TREE_MAX_ENTRIES,
)
}
struct TreeRoot<'a> {
path: PathBuf,
role: &'a [u8],
skipped: &'a [&'a str],
skips_build_dirs: bool,
own: Vec<PathBuf>,
}
impl<'a> TreeRoot<'a> {
fn new(path: PathBuf, role: &'a [u8], skipped: &'a [&'a str]) -> Self {
Self {
path,
role,
skipped,
skips_build_dirs: false,
own: Vec::new(),
}
}
fn skipping_build_dirs(self) -> Self {
Self {
skips_build_dirs: true,
..self
}
}
fn excluded(&self) -> Vec<PathBuf> {
self.skipped
.iter()
.map(|name| self.path.join(name))
.chain(self.own.iter().cloned())
.collect()
}
fn stamp_rules(&self) -> crate::tree_stamp::StampRules {
crate::tree_stamp::StampRules {
link_text: true,
skip_build_dirs: self.skips_build_dirs,
root_metadata: true,
unreadable_entries: true,
top_files_only: false,
unsearchable_dirs: false,
}
}
fn walked(&self) -> WalkedRoot {
WalkedRoot {
path: self.path.clone(),
role: self.role.to_vec(),
excluded: self.excluded(),
rules: self.stamp_rules(),
}
}
}
fn tree_digest(
roots: Vec<TreeRoot<'_>>,
file_hasher: &FileHasher<'_>,
max_entries: usize,
) -> Option<String> {
tree_digest_memoised(
roots,
file_hasher,
max_entries,
&crate::config::probe_memo_dir(),
std::time::SystemTime::now(),
)
}
pub(crate) const OVERSIZED_TREE_TTL: std::time::Duration = std::time::Duration::from_secs(60 * 60);
fn oversized_tree_marker(memo_dir: &Path, root: &Path, max_entries: usize) -> PathBuf {
let mut hasher = blake3::Hasher::new();
hasher.update(root.as_os_str().as_encoded_bytes());
hasher.update(&max_entries.to_le_bytes());
memo_dir
.join("oversized-trees")
.join(&hasher.finalize().to_hex()[..32])
}
fn oversized_roots_marker(memo_dir: &Path, roots: &[WalkedRoot], max_entries: usize) -> PathBuf {
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache-oversized-roots-v1\n");
for root in roots {
fold_field(
&mut hasher,
b"root:",
root.path.as_os_str().as_encoded_bytes(),
);
}
hasher.update(&max_entries.to_le_bytes());
memo_dir
.join("oversized-trees")
.join(&hasher.finalize().to_hex()[..32])
}
pub(crate) fn marked_recently(marker: &Path, now: std::time::SystemTime) -> bool {
std::fs::metadata(marker)
.and_then(|metadata| metadata.modified())
.is_ok_and(|marked| {
now.duration_since(marked)
.map_or(true, |age| age < OVERSIZED_TREE_TTL)
})
}
fn write_marker(marker: &Path) {
if let Some(parent) = marker.parent() {
let _ = std::fs::create_dir_all(parent).and_then(|()| std::fs::write(marker, b""));
}
}
const TREE_DIGEST_VERSION: &[u8] = b"kache-crate-tree-v1\n";
const TREE_ROOTS_VERSION: &[u8] = b"kache-crate-tree-roots-v1\n";
fn tree_digest_memoised(
mut roots: Vec<TreeRoot<'_>>,
file_hasher: &FileHasher<'_>,
max_entries: usize,
memo_dir: &Path,
now: std::time::SystemTime,
) -> Option<String> {
for root in &mut roots {
root.own = own_dirs_within(&root.path, &file_hasher.own_dirs);
}
if roots.iter().any(|root| {
marked_recently(
&oversized_tree_marker(memo_dir, &root.path, max_entries),
now,
)
}) {
return None;
}
let walked: Vec<WalkedRoot> = roots.iter().map(TreeRoot::walked).collect();
let together = oversized_roots_marker(memo_dir, &walked, max_entries);
if marked_recently(&together, now) {
return None;
}
let stamps = match stamp_roots(&walked, max_entries, file_hasher.cache.is_some()) {
Ok(stamps) => stamps,
Err(StampRefusal::TooLarge(index)) => {
write_marker(&oversized_tree_marker(
memo_dir,
&walked[index].path,
max_entries,
));
return None;
}
Err(StampRefusal::TooLargeTogether) => {
write_marker(&together);
return None;
}
Err(StampRefusal::Unwalkable) => return None,
};
let total: usize = stamps.iter().map(|root| root.entries).sum();
let mut digests = Vec::with_capacity(roots.len());
let mut walks = Vec::with_capacity(roots.len());
for ((root, walked), stamped) in roots.iter().zip(walked).zip(stamps) {
let walk = GuardWalk {
root: walked,
max_entries,
stamp: stamped.stamp.digest.clone(),
};
let budget = max_entries - (total - stamped.entries);
digests.push(root_digest(
root,
&walk,
&stamped,
budget,
file_hasher,
memo_dir,
now,
)?);
walks.push(walk);
}
for walk in walks {
file_hasher.note_tree_walk(walk);
}
if let [digest] = digests.as_slice() {
return Some(digest.clone());
}
let mut hasher = blake3::Hasher::new();
hasher.update(TREE_ROOTS_VERSION);
for digest in &digests {
fold_field(&mut hasher, b"digest:", digest.as_bytes());
}
Some(hasher.finalize().to_hex().to_string())
}
fn root_digest(
root: &TreeRoot<'_>,
walk: &GuardWalk,
stamped: &StampedRoot,
mut budget: usize,
file_hasher: &FileHasher<'_>,
memo_dir: &Path,
now: std::time::SystemTime,
) -> Option<String> {
let stamp = &stamped.stamp;
let memo = tree_digest_memo(memo_dir, root);
if let Some(digest) = crate::tree_stamp::memoised_digest(&memo, stamp.memo_key())
.filter(|digest| digest.len() == 64 && digest.bytes().all(|b| b.is_ascii_hexdigit()))
{
crate::phase_trace::decision("tree_memo", "hit");
return Some(digest);
}
crate::phase_trace::decision("tree_memo", "miss");
let mut hashes = file_hasher.memoised_hashes(stamped.small_files.iter());
let memoised: HashMap<FileFingerprint, String> = stamped
.small_files
.iter()
.filter_map(|stamp| hashes.remove(&stamp.path).map(|hash| (stamp.clone(), hash)))
.collect();
let mut hasher = blake3::Hasher::new();
hasher.update(TREE_DIGEST_VERSION);
fold_field(&mut hasher, b"root:", root.role);
let read_every_file = crate_tree_fold(
&root.path,
&root.path,
&root.excluded(),
root.skips_build_dirs,
&GuardFileHashes {
hasher: file_hasher,
memoised,
},
&mut hasher,
&mut budget,
)?;
let digest = hasher.finalize().to_hex().to_string();
if read_every_file {
memoise_tree_digest(&memo, walk, stamp, &digest, now);
}
Some(digest)
}
fn memoise_tree_digest(
memo: &Path,
walk: &GuardWalk,
stamp: &crate::tree_stamp::TreeStamp,
digest: &str,
now: std::time::SystemTime,
) {
if stamp.settled_at(now) && walk.holds() {
crate::tree_stamp::record_digest(memo, stamp.memo_key(), digest);
}
}
fn tree_digest_memo(memo_dir: &Path, root: &TreeRoot<'_>) -> PathBuf {
let mut hasher = blake3::Hasher::new();
hasher.update(TREE_DIGEST_VERSION);
fold_field(
&mut hasher,
b"path:",
root.path.as_os_str().as_encoded_bytes(),
);
fold_field(&mut hasher, b"role:", root.role);
for name in root.skipped {
fold_field(&mut hasher, b"skipped:", name.as_bytes());
}
fold_field(
&mut hasher,
b"build_dirs:",
if root.skips_build_dirs { b"1" } else { b"0" },
);
memo_dir
.join("tree-digests")
.join(&hasher.finalize().to_hex()[..32])
}
fn own_dirs_within(root: &Path, own: &[PathBuf]) -> Vec<PathBuf> {
if own.is_empty() {
return Vec::new();
}
let resolved_root = std::fs::canonicalize(root).ok();
own.iter()
.filter_map(|dir| {
let rest = dir
.strip_prefix(root)
.ok()
.or_else(|| dir.strip_prefix(resolved_root.as_deref()?).ok())?;
(!rest.as_os_str().is_empty()).then(|| root.join(rest))
})
.collect()
}
pub(crate) fn is_registry_package(manifest_dir: &Path) -> bool {
let mut components = manifest_dir.components().rev();
let _package = components.next();
let _index = components.next();
let src = components.next();
let registry = components.next();
matches!(
(src, registry),
(Some(std::path::Component::Normal(src)), Some(std::path::Component::Normal(registry)))
if src == "src" && registry == "registry"
)
}
fn registry_src_root(manifest_dir: &Path) -> Option<&Path> {
if !is_registry_package(manifest_dir) {
return None;
}
manifest_dir.parent()?.parent()
}
struct GuardFileHashes<'a, 'db> {
hasher: &'a FileHasher<'db>,
memoised: HashMap<FileFingerprint, String>,
}
fn crate_tree_fold(
root: &Path,
directory: &Path,
excluded: &[PathBuf],
skips_build_dirs: bool,
files: &GuardFileHashes<'_, '_>,
hasher: &mut blake3::Hasher,
budget: &mut usize,
) -> Option<bool> {
let listed = std::fs::read_dir(directory)
.and_then(|entries| entries.collect::<std::io::Result<Vec<_>>>());
let mut entries = match listed {
Ok(entries) => entries,
Err(_) if directory != root => {
fold_field(hasher, b"unreadable:", b"");
return Some(true);
}
Err(_) => return None,
};
entries.sort_by_key(std::fs::DirEntry::file_name);
if skips_build_dirs && directory != root {
crate::tree_stamp::keep_only_build_tag(&mut entries);
}
let mut read_every_file = true;
for entry in entries {
let path = entry.path();
if excluded.contains(&path) {
continue;
}
*budget = budget.checked_sub(1)?;
let relative = path.strip_prefix(root).ok()?;
fold_field(hasher, b"path:", relative.as_os_str().as_encoded_bytes());
let observed_ns = wall_clock_ns();
let Ok(metadata) = std::fs::symlink_metadata(&path) else {
fold_field(hasher, b"unreadable:", b"");
continue;
};
if metadata.file_type().is_symlink() {
match std::fs::read_link(&path) {
Ok(target) => {
fold_field(hasher, b"symlink:", target.as_os_str().as_encoded_bytes());
}
Err(_) => fold_field(hasher, b"unreadable:", b""),
}
} else if metadata.is_dir() {
fold_field(hasher, b"dir:", b"");
read_every_file &= crate_tree_fold(
root,
&path,
excluded,
skips_build_dirs,
files,
hasher,
budget,
)?;
} else if metadata.is_file() {
match files.hasher.guard_file_hash_from_metadata(
&path,
&metadata,
observed_ns,
&files.memoised,
) {
Ok(hash) => fold_field(hasher, b"file:", hash.as_bytes()),
Err(_) => {
fold_field(hasher, b"unreadable:", b"");
read_every_file = false;
}
}
} else {
fold_field(hasher, b"other:", b"");
}
}
Some(read_every_file)
}
thread_local! {
static DEFER_DISCOVERY: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static COMPILE_WHILE_KEYING: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
static PROVIDED_DEP_INFO: std::cell::RefCell<Option<(DepInfo, Option<TreeGuard>)>> = const { std::cell::RefCell::new(None) };
}
#[derive(Debug)]
pub struct DeferredDiscovery {
pub miss_certain: bool,
}
impl std::fmt::Display for DeferredDiscovery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("closure discovery deferred until after the compile")
}
}
impl std::error::Error for DeferredDiscovery {}
pub fn set_defer_discovery(allowed: bool) {
DEFER_DISCOVERY.with(|cell| cell.set(allowed));
}
pub fn set_compile_while_keying(allowed: bool) {
COMPILE_WHILE_KEYING.with(|cell| cell.set(allowed));
}
pub fn provide_dep_info(dep_info: DepInfo, tree: Option<TreeGuard>) {
PROVIDED_DEP_INFO.with(|cell| *cell.borrow_mut() = Some((dep_info, tree)));
}
pub fn dep_info_provided() -> bool {
PROVIDED_DEP_INFO.with(|cell| cell.borrow().is_some())
}
pub fn provided_dep_info_sources() -> Option<std::collections::HashSet<String>> {
PROVIDED_DEP_INFO.with(|cell| {
cell.borrow().as_ref().map(|(dep_info, _)| {
dep_info
.source_files
.iter()
.map(|path| absolute_path(path).to_string_lossy().into_owned())
.collect()
})
})
}
pub fn dep_info_from_emitted(path: &Path, source_file: &Path) -> Result<DepInfo> {
let content = read_dep_info_file(path)?;
let mut source_files = parse_dep_info(&content);
if source_files.is_empty() {
source_files.push(source_file.to_path_buf());
}
let env_deps = parse_env_dep_info(&content);
Ok(DepInfo {
source_files,
env_deps,
})
}
pub(crate) fn source_path_identity(
file: &Path,
path_normalizer: &PathNormalizer,
) -> Result<Vec<u8>> {
if let Some(identity) = path_normalizer.source_path_identity(file) {
return Ok(identity);
}
let mut opaque = blake3::Hasher::new();
opaque.update(b"kache-source-path-v1\0");
opaque.update(&env_os_key_bytes(file.as_os_str()));
Ok(format!("<OPAQUE_PATH>/{}", opaque.finalize().to_hex()).into_bytes())
}
pub(crate) fn rustc_prediction_identity(args: &RustcArgs) -> Option<String> {
rustc_prediction_identity_in_env(args, std::env::vars_os().collect())
}
fn rustc_prediction_identity_in_env(
args: &RustcArgs,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
) -> Option<String> {
rustc_prediction_identity_with_args(args, vars, None)
}
fn rustc_prediction_identity_with_args(
args: &RustcArgs,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
closure_args: Option<Vec<String>>,
) -> Option<String> {
let source_file = args.source_file.as_ref()?;
let rustc_version = get_rustc_version(&args.rustc).ok()?;
Some(prediction_identity_in_env(
&PredictionIdentityParts {
rustc_version: &rustc_version,
inner_rustc: args.inner_rustc.as_deref(),
current_dir: std::env::current_dir().ok().as_deref(),
source_file,
closure_args: &closure_args
.unwrap_or_else(|| closure_shaping_args(source_file, &args.all_args)),
skip_path_remap: args.skip_path_remap(),
},
vars,
))
}
pub(crate) fn rustc_shared_prediction_identity(args: &RustcArgs) -> Option<String> {
rustc_shared_prediction_identity_in(args, std::env::vars_os().collect())
}
fn rustc_shared_prediction_identity_in(
args: &RustcArgs,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
) -> Option<String> {
let target = args.target_dir()?;
let manifest_dir = env_var_in(&vars, "CARGO_MANIFEST_DIR").map(Path::new);
if !target.is_absolute() || !shared_prediction_eligible(&args.externs, manifest_dir) {
return None;
}
let source = args.source_file.as_ref()?;
let closure_args =
shared_prediction_args(&closure_shaping_args(source, &args.all_args), &target);
let identity = rustc_prediction_identity_with_args(
args,
shared_prediction_vars(vars.into_iter(), &target),
Some(closure_args),
)?;
Some(format!("{SHARED_PREDICTION_PREFIX}{identity}"))
}
const SHARED_PREDICTION_PREFIX: &str = "shared-target-v2:";
const RELOCATABLE_PREDICTION_PREFIX: &str = "shared-out-dir-v1:";
fn shared_prediction_eligible(
externs: &[crate::args::ExternDep],
manifest_dir: Option<&Path>,
) -> bool {
!has_dylib_extern(externs) || manifest_dir.is_some_and(is_registry_package)
}
fn env_var_in<'a>(
vars: &'a [(std::ffi::OsString, std::ffi::OsString)],
name: &str,
) -> Option<&'a std::ffi::OsStr> {
vars.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_os_str())
}
fn relocatable_prediction_identity(
args: &RustcArgs,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
) -> Option<String> {
let manifest_dir = Path::new(env_var_in(&vars, "CARGO_MANIFEST_DIR")?);
if !is_registry_package(manifest_dir) || env_var_in(&vars, "OUT_DIR").is_none() {
return None;
}
let shared = rustc_shared_prediction_identity_in(args, vars)?;
let hash = shared.strip_prefix(SHARED_PREDICTION_PREFIX)?;
Some(format!("{RELOCATABLE_PREDICTION_PREFIX}{hash}"))
}
fn shared_prediction_vars(
vars: impl Iterator<Item = (std::ffi::OsString, std::ffi::OsString)>,
target: &Path,
) -> Vec<(std::ffi::OsString, std::ffi::OsString)> {
vars.map(|(name, value)| {
if name != "OUT_DIR" {
return (name, value);
}
let relative = target_relative_env_value(&value, target);
(name, relative.unwrap_or(value))
})
.collect()
}
fn target_relative_env_value(value: &std::ffi::OsStr, target: &Path) -> Option<std::ffi::OsString> {
let relative = Path::new(value).strip_prefix(target).ok()?;
if relative
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return None;
}
Some(std::ffi::OsString::from(format!(
"kache-target-relative:{}",
relative.to_str()?
)))
}
fn shared_prediction_args(args: &[String], target: &Path) -> Vec<String> {
let mut kind = None;
args.iter()
.map(|arg| {
let path_arg = match kind.take() {
Some(flag) => Some((flag, arg.as_str())),
None => {
if arg == "--extern" || arg == "-L" {
kind = Some(arg.as_str());
}
arg.strip_prefix("--extern=")
.map(|value| ("--extern=", value))
.or_else(|| {
arg.strip_prefix("-L")
.filter(|s| !s.is_empty())
.map(|value| ("-L", value))
})
}
};
let mapped = path_arg.and_then(|(flag, value)| {
let (name, path) = value.split_once('=').unwrap_or(("", value));
let relative = Path::new(path).strip_prefix(target).ok()?;
if relative
.components()
.any(|c| !matches!(c, std::path::Component::Normal(_)))
{
return None;
}
Some(("target", flag, name, relative.to_str()?))
});
serde_json::to_string(&mapped.unwrap_or(("literal", "", "", arg))).unwrap()
})
.collect()
}
pub(crate) fn shared_prediction_can_record(args: &RustcArgs, dep_info: &DepInfo) -> bool {
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from);
shared_prediction_can_record_in(args, dep_info, manifest_dir.as_deref())
}
fn shared_prediction_can_record_in(
args: &RustcArgs,
dep_info: &DepInfo,
manifest_dir: Option<&Path>,
) -> bool {
args.target_dir().is_some_and(|target| {
shared_sources_can_record(&dep_info.source_files, &target, manifest_dir)
})
}
fn shared_sources_can_record(
sources: &[PathBuf],
target: &Path,
manifest_dir: Option<&Path>,
) -> bool {
let registry_src = manifest_dir.and_then(registry_src_root);
sources.iter().all(|source| {
!source.starts_with(target)
&& registry_src.is_none_or(|root| under_registry_src(source, root))
})
}
fn under_registry_src(source: &Path, registry_src: &Path) -> bool {
suffix_within(source.as_os_str(), registry_src, 2).is_some_and(|suffix| !suffix.is_empty())
}
fn out_dir_suffix(value: &std::ffi::OsStr, root: &Path) -> Option<String> {
suffix_within(value, root, 0)
}
fn suffix_within(value: &std::ffi::OsStr, root: &Path, floor: usize) -> Option<String> {
let rest = value
.as_encoded_bytes()
.strip_prefix(root.as_os_str().as_encoded_bytes())?;
let suffix = std::str::from_utf8(rest).ok()?;
let Some(components) = suffix.strip_prefix(['/', '\\']) else {
return suffix.is_empty().then(String::new);
};
(stays_below(components.split(['/', '\\']), floor) && stays_below(components.split('/'), floor))
.then(|| suffix.to_string())
}
fn stays_below<'a>(components: impl IntoIterator<Item = &'a str>, floor: usize) -> bool {
let mut depth = 0usize;
components.into_iter().all(|component| match component {
"" => false,
"." => true,
".." if depth > floor => {
depth -= 1;
true
}
".." => false,
_ => {
depth += 1;
true
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum VerifyPredictions {
Off,
Sampled,
Always,
}
const VERIFY_PREDICTION_RATE: usize = 64;
fn parse_verify_predictions(value: Option<&str>) -> VerifyPredictions {
match value {
Some(v) if v.eq_ignore_ascii_case("sampled") => VerifyPredictions::Sampled,
Some(v)
if v.eq_ignore_ascii_case("always") || v == "1" || v.eq_ignore_ascii_case("true") =>
{
VerifyPredictions::Always
}
_ => VerifyPredictions::Off,
}
}
fn should_verify_this_prediction(mode: VerifyPredictions, identity: &str) -> bool {
match mode {
VerifyPredictions::Off => false,
VerifyPredictions::Always => true,
VerifyPredictions::Sampled => sampled_by_identity(identity, VERIFY_PREDICTION_RATE),
}
}
fn sampled_by_identity(identity: &str, rate: usize) -> bool {
if rate <= 1 {
return true;
}
let digest = blake3::hash(identity.as_bytes());
let bucket = u64::from_le_bytes(digest.as_bytes()[..8].try_into().unwrap_or([0; 8]));
bucket % (rate as u64) == 0
}
fn verifies_prediction(
mode: VerifyPredictions,
args: &RustcArgs,
current_dir: Option<&Path>,
env: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> bool {
should_verify_this_prediction(mode, &prediction_sample_identity(args, current_dir, env))
}
fn prediction_sample_identity(
args: &RustcArgs,
current_dir: Option<&Path>,
env: impl Fn(&str) -> Option<std::ffi::OsString>,
) -> String {
let manifest_dir = env("CARGO_MANIFEST_DIR").map(PathBuf::from);
let source = args.source_file.as_deref().map(|source| {
checkout_relative(source, &[manifest_dir.as_deref(), current_dir]).to_string_lossy()
});
let package = ["CARGO_PKG_NAME", "CARGO_PKG_VERSION"]
.map(|var| env(var).map(|value| value.to_string_lossy().into_owned()));
serde_json::to_string(&(
args.crate_name.as_deref(),
&args.crate_types,
args.is_test,
args.target.as_deref().map(target_name),
package,
&args.cfgs,
source,
))
.unwrap()
}
fn target_name(target: &str) -> &str {
target
.strip_suffix(".json")
.and_then(|spec| Path::new(spec).file_name()?.to_str())
.unwrap_or(target)
}
fn checkout_relative<'a>(path: &'a Path, roots: &[Option<&Path>]) -> &'a Path {
roots
.iter()
.flatten()
.find_map(|root| path.strip_prefix(root).ok())
.unwrap_or(path)
}
fn closures_agree(predicted: &DepInfo, discovered: &DepInfo) -> bool {
let mut a = predicted.source_files.clone();
let mut b = discovered.source_files.clone();
a.sort();
b.sort();
let mut ea = predicted.env_deps.clone();
let mut eb = discovered.env_deps.clone();
ea.sort();
eb.sort();
a == b && ea == eb
}
fn predicted_key_inputs(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
tree_guard: &mut Option<TreeGuard>,
) -> std::result::Result<DepInfo, Rejection> {
let _trace = crate::phase_trace::phase("prediction_validate");
*tree_guard = None;
if !file_hasher.uses_input_predictions() {
return Err(Rejection::Disabled);
}
let vars: Vec<_> = std::env::vars_os().collect();
let workspace = workspace_roots(args, &vars);
let manifest_dir = env_var_in(&vars, "CARGO_MANIFEST_DIR").map(Path::new);
let tree = if needs_tree_guard(&args.externs, manifest_dir) {
let _trace = crate::phase_trace::phase(if workspace.is_some() {
"workspace_tree"
} else {
"crate_tree"
});
let guard = tree_guard_of(file_hasher, || match &workspace {
Some(workspace) => workspace_tree_digest(workspace, file_hasher),
None => crate_tree_digest(file_hasher),
})
.ok_or(Rejection::NotEligible)?;
let digest = guard.digest.clone();
*tree_guard = Some(guard);
Some(digest)
} else {
None
};
let identity = rustc_prediction_identity(args).ok_or(Rejection::Disabled)?;
let shared = rustc_shared_prediction_identity(args);
let mut from_remote = None;
let Some(record) = file_hasher
.input_prediction(&identity)
.or_else(|| file_hasher.input_prediction(shared.as_deref()?))
.or_else(|| {
let shared = shared.as_deref()?;
let record = remote_plain_row(shared, ®istry_src_of(&vars)?)?;
from_remote = Some(shared);
Some(record)
})
else {
return match &workspace {
Some(workspace) => workspace_key_inputs(args, file_hasher, workspace, vars, tree),
None => relocated_key_inputs(args, file_hasher, tree, tree_guard),
};
};
if let Some(tree) = &tree {
match &record.tree {
Some(recorded) if recorded == tree => {}
Some(_) => return Err(Rejection::TreeChanged),
None => return Err(Rejection::NoRecord),
}
}
if let Some(workspace) = &workspace
&& !guard_covers(&record.sources, workspace)
{
return Err(Rejection::NoRecord);
}
let validated = validate_prediction(
&record,
|path| std::fs::metadata(path).ok(),
|path| path.exists(),
|var| std::env::var(var).ok(),
);
if let Some(shared) = from_remote {
keep_remote_plain_row(file_hasher, shared, args, &record, &validated);
}
validated
}
fn keep_remote_plain_row(
file_hasher: &FileHasher<'_>,
identity: &str,
args: &RustcArgs,
record: &InputPrediction,
validated: &std::result::Result<DepInfo, Rejection>,
) {
crate::phase_trace::decision(
"remote_prediction",
if validated.is_ok() { "used" } else { "refused" },
);
if let Ok(dep_info) = validated {
file_hasher.record_input_prediction(
identity,
args.crate_name.as_deref(),
dep_info,
record.tree.clone(),
);
}
}
fn relocated_key_inputs(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
tree: Option<String>,
tree_guard: &mut Option<TreeGuard>,
) -> std::result::Result<DepInfo, Rejection> {
let vars: Vec<_> = std::env::vars_os().collect();
let out_dir = env_var_in(&vars, "OUT_DIR")
.and_then(std::ffi::OsStr::to_str)
.map(str::to_string);
let registry = registry_src_of(&vars);
let identity = relocatable_prediction_identity(args, vars).ok_or(Rejection::NoRecord)?;
let out_dir = out_dir.ok_or(Rejection::NoRecord)?;
let guard = match tree {
Some(tree) => tree,
None => {
let _trace = crate::phase_trace::phase("out_dir_tree");
let guard = tree_guard_of(file_hasher, || {
out_dir_tree_digest(Path::new(&out_dir), file_hasher)
})
.ok_or(Rejection::NoRecord)?;
let digest = guard.digest.clone();
*tree_guard = Some(guard);
digest
}
};
let places = Places {
out_dir: Some(&out_dir),
workspace: None,
registry: registry.as_deref().and_then(Path::to_str),
};
portable_key_inputs(file_hasher, &identity, args, &guard, &places)
}
fn portable_key_inputs(
file_hasher: &FileHasher<'_>,
identity: &str,
args: &RustcArgs,
guard: &str,
places: &Places<'_>,
) -> std::result::Result<DepInfo, Rejection> {
let validate = |record: &PortablePrediction| {
validate_portable_prediction(
record,
guard,
places,
|path| std::fs::metadata(path).ok(),
|path| path.exists(),
|var| std::env::var(var).ok(),
)
};
if let Some(record) = file_hasher.portable_prediction(identity) {
return validate(&record);
}
let record = remote_portable_row(identity).ok_or(Rejection::NoRecord)?;
let validated = validate(&record);
crate::phase_trace::decision(
"remote_prediction",
if validated.is_ok() { "used" } else { "refused" },
);
if validated.is_ok() {
file_hasher.record_portable_prediction(identity, args.crate_name.as_deref(), &record);
}
validated
}
thread_local! {
static REMOTE_ROWS: std::cell::RefCell<Option<RemoteRows>> =
const { std::cell::RefCell::new(None) };
}
pub(crate) type RemoteRows = Box<dyn Fn(&str) -> Option<crate::prediction_share::SharedPrediction>>;
pub(crate) fn set_remote_rows(rows: Option<RemoteRows>) {
REMOTE_ROWS.with(|slot| *slot.borrow_mut() = rows);
}
fn remote_row(identity: &str) -> Option<crate::prediction_share::SharedPrediction> {
REMOTE_ROWS.with(|slot| slot.borrow().as_ref().and_then(|rows| rows(identity)))
}
fn remote_portable_row(identity: &str) -> Option<PortablePrediction> {
match remote_row(identity)? {
crate::prediction_share::SharedPrediction::Portable(row) => Some(row),
crate::prediction_share::SharedPrediction::Plain(_) => None,
}
}
fn remote_plain_row(identity: &str, registry: &Path) -> Option<InputPrediction> {
match remote_row(identity)? {
crate::prediction_share::SharedPrediction::Plain(row) => {
crate::prediction_share::plain_from_remote(&row, registry)
}
crate::prediction_share::SharedPrediction::Portable(_) => None,
}
}
pub(crate) fn registry_suffix(value: &str, registry: &Path) -> Option<String> {
suffix_within(std::ffi::OsStr::new(value), registry, 2).filter(|suffix| !suffix.is_empty())
}
pub(crate) fn is_shared_target_identity(identity: &str) -> bool {
identity.starts_with(SHARED_PREDICTION_PREFIX)
}
pub(crate) fn registry_src_of(
vars: &[(std::ffi::OsString, std::ffi::OsString)],
) -> Option<PathBuf> {
registry_src_root(Path::new(env_var_in(vars, "CARGO_MANIFEST_DIR")?)).map(Path::to_path_buf)
}
pub(crate) fn is_portable_identity(identity: &str) -> bool {
identity.starts_with(WORKSPACE_PREDICTION_PREFIX)
|| identity.starts_with(RELOCATABLE_PREDICTION_PREFIX)
}
fn workspace_key_inputs(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
workspace: &WorkspaceRoots,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
tree: Option<String>,
) -> std::result::Result<DepInfo, Rejection> {
let identity =
workspace_prediction_identity(args, vars, workspace).ok_or(Rejection::NoRecord)?;
let guard = tree.ok_or(Rejection::NoRecord)?;
let places = workspace.places().ok_or(Rejection::NoRecord)?;
let dep_info = portable_key_inputs(file_hasher, &identity, args, &guard, &places)?;
if !guard_covers(&dep_info.source_files, workspace) {
return Err(Rejection::NoRecord);
}
Ok(dep_info)
}
fn workspace_neutral_link_args(args: &[String], workspace: &WorkspaceRoots) -> Vec<Option<String>> {
let neutral = |path: &str| {
[&workspace.root, &workspace.canonical_root]
.iter()
.find_map(|root| suffix_within(std::ffi::OsStr::new(path), root, 0))
.map(|relative| format!("kache-workspace:{relative}"))
};
let mut previous: Option<&str> = None;
args.iter()
.map(|arg| {
let flag = previous.replace(arg.as_str());
match flag {
Some("-C" | "--codegen") => arg
.strip_prefix("linker=")
.and_then(neutral)
.map(|path| format!("linker={path}")),
_ => ["-Clinker=", "--codegen=linker="]
.iter()
.find_map(|prefix| {
let path = arg.strip_prefix(prefix)?;
Some(format!("{prefix}{}", neutral(path)?))
}),
}
})
.collect()
}
#[derive(Debug, Clone)]
pub(crate) struct WorkspaceRoots {
pub(crate) root: PathBuf,
pub(crate) cwd: String,
pub(crate) canonical_root: PathBuf,
pub(crate) target: PathBuf,
pub(crate) canonical_target: PathBuf,
pub(crate) out_dir: Option<PathBuf>,
pub(crate) vendored_package: Option<PathBuf>,
}
impl WorkspaceRoots {
fn places(&self) -> Option<Places<'_>> {
Some(Places {
out_dir: match &self.out_dir {
Some(out_dir) => Some(out_dir.to_str()?),
None => None,
},
workspace: Some(self.root.to_str()?),
registry: None,
})
}
}
fn workspace_roots(
args: &RustcArgs,
vars: &[(std::ffi::OsString, std::ffi::OsString)],
) -> Option<WorkspaceRoots> {
workspace_roots_in(args, vars, &std::env::current_dir().ok()?)
}
fn workspace_target_dir(args: &RustcArgs) -> Option<PathBuf> {
let build_script_profile = args
.out_dir
.as_deref()
.filter(|dir| crate::cargo_layout::per_unit_out_dir(dir).is_none())
.and_then(crate::cargo_layout::build_script_dir_profile);
match build_script_profile {
Some(profile) => profile.parent().map(Path::to_path_buf),
None => args.target_dir(),
}
}
fn workspace_roots_in(
args: &RustcArgs,
vars: &[(std::ffi::OsString, std::ffi::OsString)],
current_dir: &Path,
) -> Option<WorkspaceRoots> {
let manifest_dir = Path::new(env_var_in(vars, "CARGO_MANIFEST_DIR")?);
if is_registry_package(manifest_dir) {
return None;
}
let root = args.verified_workspace_root(current_dir).or_else(|| {
manifest_dir
.ancestors()
.find(|root| root.join("Cargo.toml").is_file() && same_dir(root, current_dir))
.map(Path::to_path_buf)
})?;
let target = workspace_target_dir(args)?;
suffix_within(manifest_dir.as_os_str(), &root, 0)?;
let canonical_root = std::fs::canonicalize(&root).ok()?;
let cwd = suffix_within(current_dir.as_os_str(), &root, 0).or_else(|| {
let canonical_cwd = std::fs::canonicalize(current_dir).ok()?;
suffix_within(canonical_cwd.as_os_str(), &canonical_root, 0)
})?;
Some(WorkspaceRoots {
cwd,
canonical_root,
canonical_target: std::fs::canonicalize(&target).ok()?,
out_dir: env_var_in(vars, "OUT_DIR").map(PathBuf::from),
vendored_package: vendored_source(args, manifest_dir, current_dir)
.then(|| manifest_dir.to_path_buf()),
root,
target,
})
}
const WORKSPACE_PREDICTION_PREFIX: &str = "shared-workspace-v1:";
fn workspace_prediction_identity(
args: &RustcArgs,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
workspace: &WorkspaceRoots,
) -> Option<String> {
let source = args.source_file.as_ref()?;
let root = &workspace.root;
let within = |path: &Path| suffix_within(path.as_os_str(), root, 0);
let source_spelling = if source.is_absolute() {
format!("kache-workspace:{}", within(source)?)
} else {
format!("kache-cwd:{}", source.to_str()?)
};
let current_dir = format!("kache-workspace:{}", workspace.cwd);
let raw_args = closure_shaping_args(source, &args.all_args);
let mut closure_args = shared_prediction_args(&raw_args, &workspace.target);
for (arg, neutral) in closure_args
.iter_mut()
.zip(workspace_neutral_link_args(&raw_args, workspace))
{
if let Some(neutral) = neutral
&& arg.starts_with("[\"literal\"")
{
*arg = serde_json::to_string(&("workspace-link", "", "", neutral.as_str())).ok()?;
}
}
*closure_args.first_mut()? =
serde_json::to_string(&("source", "", "", source_spelling.as_str())).ok()?;
let vars = shared_prediction_vars(vars.into_iter(), &workspace.target)
.into_iter()
.map(|(name, value)| {
if name != "CARGO_MANIFEST_DIR" {
return Some((name, value));
}
let relative = within(Path::new(&value))?;
Some((name, format!("kache-workspace:{relative}").into()))
})
.collect::<Option<Vec<_>>>()?;
let rustc_version = get_rustc_version(&args.rustc).ok()?;
let identity = prediction_identity_in_env(
&PredictionIdentityParts {
rustc_version: &rustc_version,
inner_rustc: args.inner_rustc.as_deref(),
current_dir: Some(Path::new(¤t_dir)),
source_file: Path::new(&source_spelling),
closure_args: &closure_args,
skip_path_remap: args.skip_path_remap(),
},
vars,
);
Some(format!("{WORKSPACE_PREDICTION_PREFIX}{identity}"))
}
fn workspace_tree_digest(
workspace: &WorkspaceRoots,
file_hasher: &FileHasher<'_>,
) -> Option<String> {
if let Some(package) = &workspace.vendored_package {
return vendored_package_digest(
package,
workspace.out_dir.as_deref(),
&workspace.root,
file_hasher,
);
}
workspace_tree_digest_within(workspace, file_hasher, CRATE_TREE_MAX_ENTRIES)
}
pub(crate) fn is_vendored_package(manifest_dir: &Path) -> bool {
manifest_dir.join(".cargo-checksum.json").is_file()
}
pub(crate) fn vendored_source(args: &RustcArgs, manifest_dir: &Path, current_dir: &Path) -> bool {
args.cargo_capped_lints()
&& args
.source_file
.as_deref()
.is_some_and(|source| source.is_absolute() && source.starts_with(manifest_dir))
&& same_dir(current_dir, manifest_dir)
&& is_vendored_package(manifest_dir)
}
fn same_dir(a: &Path, b: &Path) -> bool {
a == b
|| std::fs::canonicalize(a)
.ok()
.zip(std::fs::canonicalize(b).ok())
.is_some_and(|(a, b)| a == b)
}
fn vendored_package_digest(
package: &Path,
out_dir: Option<&Path>,
workspace_root: &Path,
file_hasher: &FileHasher<'_>,
) -> Option<String> {
let mut roots = vec![TreeRoot::new(
package.to_path_buf(),
b"vendored_package",
MANIFEST_DIR_SKIPPED,
)];
if let Some(out_dir) = out_dir {
roots.push(TreeRoot::new(out_dir.to_path_buf(), b"out_dir", &[]));
}
let package_digest = tree_digest(roots, file_hasher, CRATE_TREE_MAX_ENTRIES)?;
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache-vendored-guard-v1\n");
fold_field(&mut hasher, b"package:", package_digest.as_bytes());
let mut ancestor = package.parent();
while let Some(directory) = ancestor.filter(|directory| directory.starts_with(workspace_root)) {
fold_field(
&mut hasher,
b"ancestor:",
ancestor_top_digest(directory, file_hasher)?.as_bytes(),
);
ancestor = directory.parent();
}
Some(hasher.finalize().to_hex().to_string())
}
fn ancestor_top_digest(directory: &Path, file_hasher: &FileHasher<'_>) -> Option<String> {
let top = WalkedRoot {
path: directory.to_path_buf(),
role: b"ancestor".to_vec(),
excluded: vec![directory.join(".git")],
rules: crate::tree_stamp::StampRules {
link_text: true,
skip_build_dirs: false,
root_metadata: true,
unreadable_entries: true,
top_files_only: true,
unsearchable_dirs: false,
},
};
let stamp = stamp_root(&top, CRATE_TREE_MAX_ENTRIES)?;
let mut entries: Vec<_> = std::fs::read_dir(directory)
.ok()?
.collect::<std::io::Result<_>>()
.ok()?;
entries.sort_by_key(std::fs::DirEntry::file_name);
file_hasher.note_tree_walk(GuardWalk {
root: top,
max_entries: CRATE_TREE_MAX_ENTRIES,
stamp: stamp.digest,
});
let own = own_dirs_within(directory, &file_hasher.own_dirs);
let mut hasher = blake3::Hasher::new();
for entry in entries {
let name = entry.file_name();
if name == ".git" {
continue;
}
let path = entry.path();
if own.contains(&path) {
continue;
}
let unreadable = || "unreadable".to_string();
let content = match std::fs::symlink_metadata(&path) {
Err(_) => unreadable(),
Ok(metadata) if metadata.file_type().is_symlink() => match std::fs::read_link(&path) {
Ok(target) => {
let found = if path.is_file() {
file_hasher
.hash_unkeyed(&path)
.unwrap_or_else(|_| unreadable())
} else {
String::new()
};
format!("symlink:{}:{found}", target.display())
}
Err(_) => unreadable(),
},
Ok(metadata) if metadata.is_file() => match file_hasher.hash_unkeyed(&path) {
Ok(hash) => format!("file:{hash}"),
Err(_) => unreadable(),
},
Ok(metadata)
if metadata.is_dir()
&& name.as_encoded_bytes().starts_with(b".")
&& name != ".git" =>
{
let roots = vec![TreeRoot::new(path.clone(), b"dot_dir", &[])];
format!(
"dir:{}",
tree_digest(roots, file_hasher, CRATE_TREE_MAX_ENTRIES)?
)
}
Ok(_) => continue,
};
fold_field(&mut hasher, b"name:", name.as_encoded_bytes());
fold_field(&mut hasher, b"entry:", content.as_bytes());
}
Some(hasher.finalize().to_hex().to_string())
}
fn workspace_tree_digest_within(
workspace: &WorkspaceRoots,
file_hasher: &FileHasher<'_>,
max_entries: usize,
) -> Option<String> {
let target =
suffix_within(workspace.target.as_os_str(), &workspace.root, 0).unwrap_or_default();
let target = target.trim_start_matches(['/', '\\']);
let skipped = [target, ".git"];
let mut roots =
vec![TreeRoot::new(workspace.root.clone(), b"workspace", &skipped).skipping_build_dirs()];
if let Some(out_dir) = &workspace.out_dir {
roots.push(TreeRoot::new(out_dir.clone(), b"out_dir", &[]));
}
tree_digest(roots, file_hasher, max_entries)
}
fn prediction_discovery_identity(args: &RustcArgs, file_hasher: &FileHasher<'_>) -> Option<String> {
if !file_hasher.uses_input_predictions() || has_dylib_extern(&args.externs) {
return None;
}
rustc_shared_prediction_identity(args).or_else(|| rustc_prediction_identity(args))
}
fn discovery_flight_identity(args: &RustcArgs, file_hasher: &FileHasher<'_>) -> Option<String> {
prediction_discovery_identity(args, file_hasher)
.or_else(|| rustc_shared_prediction_identity(args))
.or_else(|| rustc_prediction_identity(args))
}
fn resolve_key_inputs(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
crate_name: &str,
env: &KeyEnv,
out: &mut KeyOutputs,
) -> Result<Option<DepInfo>> {
if let Some((provided, tree)) = PROVIDED_DEP_INFO.with(|cell| cell.borrow_mut().take()) {
out.tree_guard = tree;
crate::phase_trace::decision("prediction", "emitted");
tracing::trace!("[key:{}] inputs=emitted-dep-info", crate_name);
return Ok(Some(provided));
}
if args.source_file.is_some() {
let mut prediction = predicted_key_inputs(args, file_hasher, &mut out.tree_guard);
let mut owns_flight = false;
if prediction.is_err()
&& let Some(cache_dir) = &file_hasher.prediction_flight_dir
&& let Some(identity) = discovery_flight_identity(args, file_hasher)
{
let flight = crate::scheduler::join_discovery_flight(cache_dir, &identity);
owns_flight = flight.lock.is_some();
*file_hasher.discovery_flight.borrow_mut() = flight.lock;
if flight.waited {
prediction = predicted_key_inputs(args, file_hasher, &mut out.tree_guard);
}
}
match prediction {
Ok(dep_info) => {
crate::phase_trace::decision("prediction", "validated");
let mode =
parse_verify_predictions(env.var("KACHE_VERIFY_INPUT_PREDICTIONS").as_deref());
if verifies_prediction(mode, args, env.cwd(), |var| env.var_os(var)) {
crate::phase_trace::decision("prediction", "verify-sampled");
let discovered = dep_info_pre_pass(args)?;
if discovered
.as_ref()
.is_some_and(|discovered| closures_agree(&dep_info, discovered))
{
tracing::trace!("[key:{}] inputs=predicted(verified)", crate_name);
} else {
tracing::warn!(
"[key:{}] input prediction disagreed with the dep-info pass; \
using the pass. Please report this with the crate and its \
dependencies (kunobi-ninja/kache).",
crate_name
);
crate::opcounts::record_prediction_mismatch();
}
return Ok(discovered);
}
tracing::trace!("[key:{}] inputs=predicted", crate_name);
out.used_prediction = true;
return Ok(Some(dep_info));
}
Err(Rejection::NoRecord | Rejection::Disabled | Rejection::NotEligible)
if owns_flight
&& DEFER_DISCOVERY.with(std::cell::Cell::get)
&& file_hasher.store_lacks_unit(
crate_name,
args.get_codegen_opt("metadata").unwrap_or(""),
) =>
{
crate::phase_trace::decision("prediction", "deferred-new-crate");
tracing::trace!("[key:{}] inputs=deferred(new crate)", crate_name);
return Err(anyhow::Error::new(DeferredDiscovery { miss_certain: true }));
}
Err(reason)
if owns_flight
&& DEFER_DISCOVERY.with(std::cell::Cell::get)
&& COMPILE_WHILE_KEYING.with(std::cell::Cell::get) =>
{
crate::phase_trace::decision("prediction", reason.as_str());
tracing::trace!(
"[key:{}] inputs=compile-while-keying({})",
crate_name,
reason.as_str()
);
return Err(anyhow::Error::new(DeferredDiscovery {
miss_certain: false,
}));
}
Err(reason) => {
crate::phase_trace::decision("prediction", reason.as_str());
tracing::trace!("[key:{}] inputs=dep-info({})", crate_name, reason.as_str())
}
}
}
dep_info_pre_pass(args)
}
fn dep_info_pre_pass(args: &RustcArgs) -> Result<Option<DepInfo>> {
let _trace = crate::phase_trace::phase("dep-info");
args.source_file
.as_ref()
.map(|source| {
run_dep_info_pass(
&args.rustc,
args.inner_rustc.as_deref(),
source,
&args.all_args,
args.has_expanded_argfiles(),
)
.with_context(|| {
format!(
"dep-info pre-pass failed for {} — refusing to cache from an \
incomplete input set",
source.display()
)
})
})
.transpose()
}
pub fn compute_cache_key_with_outputs(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
path_normalizer: &PathNormalizer,
env: &KeyEnv,
) -> (Result<String>, KeyOutputs) {
let mut out = KeyOutputs {
unit_id: args.unit_id(),
..KeyOutputs::default()
};
let key = compute_key_into(args, file_hasher, path_normalizer, env, &mut out);
(key, out)
}
#[cfg(test)]
pub(crate) fn compute_cache_key(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
path_normalizer: &PathNormalizer,
env: &KeyEnv,
) -> Result<String> {
compute_cache_key_with_outputs(args, file_hasher, path_normalizer, env).0
}
fn compute_key_into(
args: &RustcArgs,
file_hasher: &FileHasher<'_>,
path_normalizer: &PathNormalizer,
env: &KeyEnv,
out: &mut KeyOutputs,
) -> Result<String> {
let _trace = crate::phase_trace::phase("key");
let mut hasher = GroupedHasher::new("compiler");
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let out_dir = env.var_os("OUT_DIR");
hasher.update(b"key_version:");
hasher.update(CACHE_KEY_VERSION.to_string().as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] key_version={}", crate_name, CACHE_KEY_VERSION);
let configured_base_dirs = path_normalizer.configured_base_dir_count();
if configured_base_dirs != 0 {
fold_field(
&mut hasher,
b"configured_base_dirs.v1:",
configured_base_dirs.to_string().as_bytes(),
);
tracing::trace!(
"[key:{}] configured_base_dirs={}",
crate_name,
configured_base_dirs
);
}
let rustc_version = get_rustc_version(&args.rustc)?;
hasher.update(b"rustc_version:");
hasher.update(rustc_version.as_bytes());
hasher.update(b"\n");
tracing::trace!(
"[key:{}] rustc_version={}",
crate_name,
rustc_version.lines().next().unwrap_or("?")
);
if args.is_clippy_chain() {
let identity = clippy_identity(&args.rustc, env)?;
fold_field(&mut hasher, b"clippy.v1:", identity.as_bytes());
tracing::trace!(
"[key:{}] clippy={}",
crate_name,
identity.lines().next().unwrap_or("?")
);
}
let target = args
.target
.as_deref()
.unwrap_or_else(|| host_target_triple());
check_for_path_leak(target, "target");
hasher.update(b"target:");
hasher.update(target.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] target={}", crate_name, target);
let target_path = Path::new(target);
if target_path.is_file() {
match hash_file(target_path) {
Ok(spec_hash) => {
hasher.update(b"target_spec:");
hasher.update(spec_hash.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] target_spec={}", crate_name, &spec_hash[..16]);
}
Err(e) => {
tracing::warn!(
"[key:{}] failed to hash target spec {}: {}",
crate_name,
target,
e
);
}
}
}
hasher.set_group("crate");
if let Some(name) = &args.crate_name {
hasher.update(b"crate_name:");
hasher.update(name.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] crate_name={}", crate_name, name);
}
for ct in &args.crate_types {
hasher.update(b"crate_type:");
hasher.update(ct.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] crate_type={}", crate_name, ct);
}
if let Some(edition) = &args.edition {
hasher.update(b"edition:");
hasher.update(edition.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] edition={}", crate_name, edition);
}
hasher.set_group("args");
let mut emit: Vec<&String> = args.emit.iter().collect();
emit.sort();
for kind in &emit {
hasher.update(b"emit:");
hasher.update(kind.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] emit:{}", crate_name, kind);
}
let mut codegen_opts: Vec<_> = args
.codegen_opts
.iter()
.filter(|(k, _)| {
k != "incremental" && k != "linker"
})
.collect();
codegen_opts.sort_by_key(|(k, _)| k.as_str());
for (key, value) in &codegen_opts {
fold_field(&mut hasher, b"codegen_key:", key.as_bytes());
if let Some(v) = value {
check_for_path_leak(v, &format!("codegen:{key}"));
fold_field(&mut hasher, b"codegen_val:", v.as_bytes());
tracing::trace!("[key:{}] codegen:{}={}", crate_name, key, v);
} else {
tracing::trace!("[key:{}] codegen:{}", crate_name, key);
}
}
for feat in &args.features {
hasher.update(b"feature:");
hasher.update(feat.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] feature:{}", crate_name, feat);
}
let mut cfgs: Vec<_> = args
.cfgs
.iter()
.filter(|c| !c.starts_with("feature="))
.collect();
cfgs.sort();
for cfg in &cfgs {
check_for_path_leak(cfg, "cfg");
fold_field(&mut hasher, b"cfg:", cfg.as_bytes());
tracing::trace!("[key:{}] cfg:{}", crate_name, cfg);
}
let dep_info = resolve_key_inputs(args, file_hasher, crate_name, env, out)?;
out.dep_info = dep_info.clone();
let mut externs: Vec<_> = args.externs.iter().filter(|e| e.path.is_some()).collect();
externs.sort_by_key(|e| &e.name);
let mut hash_paths = Vec::new();
if let Some(dep_info) = &dep_info {
hash_paths.extend(dep_info.source_files.iter().map(|p| p.as_path()));
}
hash_paths.extend(externs.iter().filter_map(|ext| ext.path.as_deref()));
file_hasher.prefetch(&hash_paths);
hasher.set_group("sources");
if let Some(dep_info) = &dep_info {
let mut hashed: Vec<(Vec<u8>, String)> = Vec::with_capacity(dep_info.source_files.len());
for file in &dep_info.source_files {
let file_hash = file_hasher
.hash(file)
.with_context(|| format!("hashing source identity {}", file.display()))?;
let normalized_path = source_path_identity(file, path_normalizer)?;
hashed.push((normalized_path, file_hash));
}
hashed.sort();
for (normalized_path, file_hash) in &hashed {
fold_field(&mut hasher, b"source_path:", normalized_path);
fold_field(&mut hasher, b"source_hash:", file_hash.as_bytes());
tracing::trace!(
"[key:{}] source:{}={}",
crate_name,
String::from_utf8_lossy(normalized_path),
&file_hash[..16]
);
}
hasher.set_group("env_deps");
let aliased_out_dir = crate::out_dir_alias::active_alias();
let env_dep_paths = EnvDepPaths::new(out_dir.as_deref(), &dep_info.source_files);
let mut bakes_out_dir = false;
let mut env_fields = BTreeMap::new();
for (var, val) in &dep_info.env_deps {
let value = EnvDepValue::new(val);
let normalized_env_dep = normalize_env_dep_value_with_hasher(
crate_name,
var,
&value,
&env_dep_paths,
file_hasher,
path_normalizer,
aliased_out_dir,
);
bakes_out_dir |= env_dep_bakes_out_dir(var, normalized_env_dep.decision, || {
env_dep_paths.value_is_under_out_dir(&value)
});
env_fields.insert(
var.clone(),
blake3::hash(normalized_env_dep.value.as_bytes()).to_hex()[..KEY_FIELD_HEX]
.to_string(),
);
fold_field(&mut hasher, b"env_dep_var:", var.as_bytes());
fold_field(
&mut hasher,
b"env_dep_val:",
normalized_env_dep.value.as_bytes(),
);
tracing::trace!(
"[key:{}] env_dep:{}={} ({})",
crate_name,
var,
normalized_env_dep.value,
normalized_env_dep.decision.as_str()
);
}
out.env_deps = Some(env_fields);
out.bakes_out_dir = bakes_out_dir;
}
hasher.set_group("externs");
let mut extern_digests = std::collections::BTreeMap::new();
let mut extern_units = std::collections::BTreeMap::new();
for ext in &externs {
if let Some(path) = &ext.path {
if let Some(unit) = crate::args::unit_id_from_artifact_path(path) {
extern_units.insert(ext.name.clone(), unit);
}
match file_hasher.hash(path) {
Ok(dep_hash) => {
hasher.update(b"extern:");
hasher.update(ext.name.as_bytes());
hasher.update(b"=");
hasher.update(dep_hash.as_bytes());
hasher.update(b"\n");
extern_digests.insert(
ext.name.clone(),
dep_hash
.get(..KEY_FIELD_HEX)
.unwrap_or(dep_hash.as_str())
.to_string(),
);
tracing::trace!(
"[key:{}] extern:{}={}",
crate_name,
ext.name,
&dep_hash[..16]
);
}
Err(_) => {
hasher.update(b"extern_unreadable:");
hasher.update(ext.name.as_bytes());
hasher.update(b"\n");
extern_digests.insert(ext.name.clone(), EXTERN_UNREADABLE.to_string());
tracing::trace!("[key:{}] extern_unreadable:{}", crate_name, ext.name);
}
}
}
}
out.externs = Some(extern_digests);
out.extern_units = Some(extern_units);
hasher.set_group("args");
if let Some(rustflags) = env.var("RUSTFLAGS") {
let scrubbed = scrub_remap_from_prefixes(rustflags.split_whitespace()).join(" ");
let normalized = normalize_rustflags(&path_normalizer.normalize(&scrubbed));
hasher.update(b"RUSTFLAGS:");
hasher.update(normalized.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] RUSTFLAGS={}", crate_name, normalized);
}
if let Some(flags) = env.var("CARGO_ENCODED_RUSTFLAGS") {
let scrubbed = scrub_remap_from_prefixes(flags.split('\x1f')).join("\x1f");
let normalized = path_normalizer.normalize(&scrubbed);
hasher.update(b"CARGO_ENCODED_RUSTFLAGS:");
hasher.update(normalized.as_bytes());
hasher.update(b"\n");
tracing::trace!(
"[key:{}] CARGO_ENCODED_RUSTFLAGS={}",
crate_name,
normalized
);
}
for value in &args.remap_path_prefixes {
let normalized = normalize_direct_remap_value(value, path_normalizer);
fold_field(
&mut hasher,
b"argv_remap_path_prefix.v1:",
normalized.as_bytes(),
);
tracing::trace!(
"[key:{}] argv --remap-path-prefix={}",
crate_name,
normalized
);
}
if let Some(bootstrap) = env.var("RUSTC_BOOTSTRAP")
&& !bootstrap.is_empty()
{
hasher.update(b"RUSTC_BOOTSTRAP:");
hasher.update(bootstrap.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] RUSTC_BOOTSTRAP={}", crate_name, bootstrap);
}
hasher.set_group("link");
if let Some(sysroot) = &args.sysroot {
let normalized = path_normalizer.normalize(sysroot.to_string_lossy());
hasher.update(b"sysroot:");
hasher.update(normalized.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] sysroot={}", crate_name, normalized);
}
const KNOWN_L_KINDS: [&str; 5] = ["dependency", "crate", "native", "framework", "all"];
let mut native_search_dirs: Vec<PathBuf> = Vec::new();
let mut framework_search_dirs: Vec<PathBuf> = Vec::new();
for spec in &args.link_search {
let (kind, path) = match spec.split_once('=') {
Some((k, p)) if KNOWN_L_KINDS.contains(&k) => (Some(k), p),
_ => (None, spec.as_str()),
};
if matches!(kind, Some("dependency") | Some("crate")) {
continue;
}
if matches!(kind, None | Some("native") | Some("all")) {
native_search_dirs.push(PathBuf::from(path));
}
if kind == Some("framework") {
framework_search_dirs.push(PathBuf::from(path));
}
let normalized = path_normalizer.normalize(path);
hasher.update(b"link_search:");
if let Some(k) = kind {
hasher.update(k.as_bytes());
hasher.update(b"=");
}
hasher.update(normalized.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] link_search:{}", crate_name, normalized);
}
if native_linker_side_files_are_unmodeled(args) {
anyhow::bail!("native linker order/map/response side files are not cacheable");
}
let native_windows_msvc = is_native_windows_msvc_link(
args,
&rustc_version,
cfg!(target_os = "windows"),
get_rustc_version,
)?;
let link_rustc_version = if args.invokes_linker() {
Some(rustc_version_for_native_link(
args,
&rustc_version,
get_rustc_version,
)?)
} else {
None
};
let rustc_host = link_rustc_version.as_deref().and_then(rustc_host_triple);
let target = link_target(args.target.as_deref(), rustc_host);
let unit_out_dir = out_dir.map(PathBuf::from);
let native_archives = fold_native_link_inputs(
&mut hasher,
args,
&NativeSearchDirs {
native: &native_search_dirs,
framework: &framework_search_dirs,
},
&NativeLinkContext {
native_windows_msvc,
target,
default_dirs: default_library_dirs(
target,
rustc_host,
env.var_os("LIBRARY_PATH").as_deref(),
),
out_dir: unit_out_dir.as_deref(),
build_tree: build_tree_roots(args),
system_dirs: system_library_dirs(),
path_normalizer,
},
file_hasher,
)?;
if needs_native_bundle_audit(args, &native_archives.dirs) {
hasher.set_group("native_bundle_audit");
fold_field(&mut hasher, b"native_bundle_audit.v2", b"");
tracing::trace!("[key:{}] native_bundle_audit", crate_name);
}
out.native_archives = Some(native_archives);
hasher.set_group("args");
let backend_dylib = args.codegen_backend_dylib();
for z in &args.unstable_flags {
if let Some(path) = z
.strip_prefix("codegen-backend=")
.filter(|path| Some(*path) == backend_dylib)
{
let content = file_hasher
.hash(Path::new(path))
.with_context(|| format!("hashing codegen backend {path}"))?;
fold_field(
&mut hasher,
b"codegen_backend_content.v1:",
content.as_bytes(),
);
tracing::trace!("[key:{}] codegen_backend_content:{}", crate_name, content);
continue;
}
hasher.update(b"unstable:");
hasher.update(z.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] unstable:{}", crate_name, z);
}
if let Some(backend) = args.codegen_backend_name() {
for (library, content) in
toolchain_codegen_backends(args, &rustc_version, backend, file_hasher)?
{
fold_field(
&mut hasher,
b"codegen_backend_library.v1:",
library.as_bytes(),
);
fold_field(
&mut hasher,
b"codegen_backend_content.v1:",
content.as_bytes(),
);
tracing::trace!(
"[key:{}] codegen_backend_library:{} {}",
crate_name,
library,
content
);
}
}
for jobs in &args.frontend_jobs {
fold_field(&mut hasher, b"frontend_jobs.v1:", jobs.as_bytes());
tracing::trace!("[key:{}] frontend_jobs:{}", crate_name, jobs);
}
if !args.residual_args.is_empty() {
let mut residual: Vec<String> = args
.residual_args
.iter()
.map(|tok| path_normalizer.normalize(tok))
.collect();
residual.sort();
for tok in &residual {
check_for_path_leak(tok, "residual_arg");
fold_field(&mut hasher, b"residual_args.v1:", tok.as_bytes());
tracing::trace!("[key:{}] residual_arg:{}", crate_name, tok);
}
let mut raw: Vec<&str> = args.residual_args.iter().map(String::as_str).collect();
raw.sort_unstable();
raw.dedup();
tracing::warn!(
"[key:{}] {} unmodeled rustc flag(s) folded into the cache key \
(kache does not model these; keyed defensively so they cannot cause \
a false hit, but model them for precise keying): {}",
crate_name,
raw.len(),
raw.join(" "),
);
}
if !args.outcome_lint_flags.is_empty() {
hasher.set_group("outcome_lints");
for tok in &args.outcome_lint_flags {
check_for_path_leak(tok, "outcome_lint");
fold_field(&mut hasher, b"outcome_lint.v1:", tok.as_bytes());
tracing::trace!("[key:{}] outcome_lint:{}", crate_name, tok);
}
}
hasher.set_group("env_cfg");
let cargo_cfgs = cargo_cfg_pairs(env.cargo_cfgs());
tracing::trace!("[key:{}] cargo_cfg_count={}", crate_name, cargo_cfgs.len());
for (key, value) in &cargo_cfgs {
let key_lossy = key.to_string_lossy();
check_for_path_leak(&value.to_string_lossy(), &format!("cargo_cfg:{key_lossy}"));
hasher.update(&env_text_key_bytes(key));
hasher.update(b"=");
hasher.update(&env_text_key_bytes(value));
hasher.update(b"\n");
}
hasher.set_group("link");
fold_generic_linker_identity(&mut hasher, args, native_windows_msvc, get_linker_identity);
fold_native_host_libc_signature(
&mut hasher,
args,
&rustc_version,
cfg!(target_os = "linux"),
probe_linux_libc_signature,
)?;
fold_native_link_runtime_identity(
&mut hasher,
args,
&rustc_version,
cfg!(target_os = "linux"),
cfg!(target_os = "macos"),
|driver| {
crate::native_link_key::probe_linux_crt_objects_memoized(
&crate::config::probe_memo_dir(),
driver,
|path| file_hasher.hash(path),
)
},
|sdkroot| match crate::native_link_key::sdk_identity_for(sdkroot)? {
Some(identity) => Ok(identity),
None => anyhow::bail!("the macOS SDK could not be identified"),
},
env,
)?;
fold_native_windows_msvc_identity(
&mut hasher,
args,
&rustc_version,
cfg!(target_os = "windows"),
|linker, architecture, host| {
let search_dirs = windows_native_link_search_dirs(args)?;
crate::native_link_key::probe_windows_msvc_identity_with_library_dirs(
linker,
architecture,
&rustc_tool_dirs(args, host)?,
&search_dirs.rustc,
&search_dirs.linker,
&args.link_libs,
|path| file_hasher.hash_static_lib(path),
)
.map(|identity| identity.encode())
},
)?;
hasher.set_group("remap");
let remap = if args.skip_path_remap() {
hasher.update(b"remap:none\n");
fold_unremapped_path_identity(&mut hasher, args, path_normalizer, env);
"none".to_string()
} else {
hasher.update(b"remap:multi-prefix\n");
let remap_args = path_normalizer.remap_args();
let mut targets: Vec<String> = remap_args
.iter()
.filter_map(|a| a.split('=').next_back().map(str::to_string))
.collect();
targets.sort();
targets.dedup();
format!("multi-prefix({})", targets.join(","))
};
tracing::trace!("[key:{}] remap={}", crate_name, remap);
let (hash, fields) = hasher.finalize_with_fields();
out.fields = Some(fields);
let key = hash.to_hex().to_string();
tracing::trace!("[key:{}] final={}", crate_name, &key[..16]);
complete_key(env, key)
}
fn complete_key(env: &KeyEnv, key: String) -> Result<String> {
anyhow::ensure!(
!env.read_undeclared(),
"key computation read an environment variable missing from KEY_ENV_VARS"
);
Ok(key)
}
fn fold_unremapped_path_identity<H: KeyFold>(
hasher: &mut H,
args: &RustcArgs,
path_normalizer: &PathNormalizer,
env: &KeyEnv,
) {
hasher.update(b"unremapped_path_identity:v1\n");
if let Some(cwd) = env.cwd() {
fold_field(
hasher,
b"unremapped:cwd:",
cwd.as_os_str().as_encoded_bytes(),
);
}
if let Some(source) = &args.source_file {
fold_field(
hasher,
b"unremapped:source:",
source.as_os_str().as_encoded_bytes(),
);
}
let mut prefixes: Vec<&str> = path_normalizer.raw_prefixes().collect();
if args.is_trybuild_unit() {
let reached = trybuild_paths_in_the_artifact(args, env);
prefixes.retain(|prefix| reached.iter().any(|path| path_is_under(path, prefix)));
}
prefixes.sort_unstable();
prefixes.dedup();
for prefix in prefixes {
fold_field(hasher, b"unremapped:prefix:", prefix.as_bytes());
}
}
fn trybuild_paths_in_the_artifact(args: &RustcArgs, env: &KeyEnv) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(cwd) = env.cwd() {
paths.push(cwd.to_path_buf());
}
if let Some(source) = &args.source_file {
paths.push(joined_under(env.cwd(), source));
}
if let Some(out_dir) = env.var_os("OUT_DIR").filter(|value| !value.is_empty()) {
paths.push(joined_under(env.cwd(), Path::new(&out_dir)));
}
paths
}
fn joined_under(cwd: Option<&Path>, path: &Path) -> PathBuf {
if path.is_absolute() {
path.to_path_buf()
} else if let Some(cwd) = cwd {
cwd.join(path)
} else {
path.to_path_buf()
}
}
fn path_is_under(path: &Path, prefix: &str) -> bool {
path.starts_with(prefix)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EnvDepNormalizationDecision {
Unchanged,
NormalizedPathOnly,
KeptAbsoluteNotPathOnly,
KeptAbsoluteManifestDir,
KeptAbsoluteNoIncludeProof,
KeptAbsoluteRuntimeUse,
KeptAbsoluteScanError,
ForcedPathOnly,
AliasedOutDir,
}
impl EnvDepNormalizationDecision {
fn keeps_literal_value(self) -> bool {
!matches!(self, Self::NormalizedPathOnly | Self::ForcedPathOnly)
}
fn as_str(self) -> &'static str {
match self {
Self::Unchanged => "unchanged",
Self::NormalizedPathOnly => "normalized path-only",
Self::KeptAbsoluteNotPathOnly => "kept absolute: not a path-only var",
Self::KeptAbsoluteManifestDir => "kept absolute: CARGO_MANIFEST_DIR is never path-only",
Self::KeptAbsoluteNoIncludeProof => "kept absolute: no include proof",
Self::KeptAbsoluteRuntimeUse => "kept absolute: value use in source",
Self::KeptAbsoluteScanError => "kept absolute: source scan failed",
Self::ForcedPathOnly => "forced path-only (user-asserted)",
Self::AliasedOutDir => "aliased OUT_DIR",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct NormalizedEnvDep {
value: String,
decision: EnvDepNormalizationDecision,
}
fn lexically_resolve_path(input: &str) -> String {
let is_sep = |c: char| c == '/' || c == '\\';
let sep = std::path::MAIN_SEPARATOR;
let chars: Vec<char> = input.chars().collect();
let n = chars.len();
let (anchor, start) = if n >= 2 && is_sep(chars[0]) && is_sep(chars[1]) {
let mut root = String::from(r"\\");
let mut i = 2;
let mut taken = 0;
while i < n && taken < 2 {
while i < n && is_sep(chars[i]) {
i += 1;
}
let comp_start = i;
while i < n && !is_sep(chars[i]) {
i += 1;
}
if comp_start == i {
break;
}
if taken == 1 {
root.push(sep);
}
root.extend(&chars[comp_start..i]);
taken += 1;
}
root.push(sep);
(root, i)
} else if n >= 2 && chars[1] == ':' && chars[0].is_ascii_alphabetic() {
let mut root: String = chars[..2].iter().collect();
let mut i = 2;
if i < n && is_sep(chars[i]) {
root.push(sep);
i += 1;
}
(root, i)
} else if n >= 1 && is_sep(chars[0]) {
(String::from(sep), 1) } else {
(String::new(), 0) };
let absolute = anchor.ends_with(sep);
let tail: String = chars[start..].iter().collect();
let mut stack: Vec<&str> = Vec::new();
for comp in tail.split(is_sep).filter(|c| !c.is_empty()) {
match comp {
"." => {}
".." => match stack.last() {
Some(&top) if top != ".." => {
stack.pop();
}
_ if absolute => {} _ => stack.push(".."),
},
other => stack.push(other),
}
}
let joined = stack.join(&sep.to_string());
match (anchor.is_empty(), joined.is_empty()) {
(true, true) => ".".to_string(),
(true, false) => joined,
(false, true) => anchor,
(false, false) => format!("{anchor}{joined}"),
}
}
fn resolve_native_static_lib(
spec: &str,
search_dirs: &[PathBuf],
file_hasher: &FileHasher<'_>,
links: bool,
usage: impl Fn(&Path) -> StaticLibUse,
) -> Result<Option<(PathBuf, String)>> {
let file_names = match static_lib_spec(spec) {
StaticLibSpec::NotStatic => return Ok(None),
StaticLibSpec::Archive { bundle: false, .. } if !links => return Ok(None),
StaticLibSpec::Archive { files, .. } => files,
StaticLibSpec::Unmodeled(spec) => {
anyhow::bail!("native static library spec {spec:?} is not cacheable")
}
};
let mut found: Option<PathBuf> = None;
let mut copies: Vec<(PathBuf, String)> = Vec::new();
for (index, dir) in search_dirs.iter().enumerate() {
for filename in &file_names {
let candidate = dir.join(filename);
if candidate.is_file() {
if copies.iter().any(|(path, _)| path == &candidate) {
continue;
}
if let Some(first) = &found
&& file_hasher.hash(first)? != file_hasher.hash(&candidate)?
{
anyhow::bail!("ambiguous native static library {spec:?}");
}
copies.push((candidate.clone(), format!("{index}:{filename}")));
found.get_or_insert(candidate);
}
}
}
let Some(path) = found else {
return Ok(None);
};
let mut hash = file_hasher.hash_static_lib_for(&path, usage(&path))?;
if copies.len() > 1 {
let places: Vec<&str> = copies.iter().map(|(_, place)| place.as_str()).collect();
hash = format!("{hash};copies={}", places.join(","));
}
Ok(Some((path, hash)))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaticLibUse {
Bundled,
Linked,
}
impl StaticLibUse {
fn memo_namespace(self) -> &'static str {
match self {
Self::Bundled => "static-ar-v8-bundled",
Self::Linked => "static-ar-v8",
}
}
}
fn hermetic_archive_under(archive: &Path, resolved: &Path, spelled_root: &Path) -> bool {
std::path::absolute(archive).is_ok_and(|spelled| spelled.starts_with(spelled_root))
&& !resolved.starts_with(spelled_root)
&& crate::build_script::in_sealed_out_dir(resolved)
}
fn oso_archive_use(
args: &RustcArgs,
archive: &Path,
spelled_root: &Path,
root: &Path,
) -> StaticLibUse {
let resolved = resolved_path(archive);
if hermetic_archive_under(archive, &resolved, spelled_root) {
linked_archive_use(args, archive, Some(spelled_root))
} else {
linked_archive_use(args, &resolved, Some(root))
}
}
fn linked_archive_use(args: &RustcArgs, archive: &Path, oso_root: Option<&Path>) -> StaticLibUse {
if !args.is_executable_output() || oso_root.is_some_and(|root| archive.starts_with(root)) {
StaticLibUse::Bundled
} else {
StaticLibUse::Linked
}
}
fn unresolved_static_lib_is_error(links: bool, native_windows_msvc: bool) -> bool {
links && !native_windows_msvc
}
fn unix_library_request(spec: &str) -> Option<(&str, bool)> {
let (kind, name) = spec.split_once('=').unwrap_or(("dylib", spec));
let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
if kind != "dylib" || name.is_empty() {
return None;
}
let verbatim = modifiers
.split(',')
.fold(false, |verbatim, modifier| match modifier {
"+verbatim" => true,
"-verbatim" => false,
_ => verbatim,
});
Some((name, verbatim))
}
fn resolve_unix_library(
name: &str,
verbatim: bool,
dirs: &[PathBuf],
prefer_static: bool,
shared_extensions: &[&str],
is_file: impl Fn(&Path) -> bool,
) -> Option<PathBuf> {
let (archive, shared) = if verbatim {
if !is_native_archive_name(name) {
return None;
}
(name.to_string(), Vec::new())
} else if prefer_static {
(format!("lib{name}.a"), Vec::new())
} else {
let shared = shared_extensions
.iter()
.map(|extension| format!("lib{name}.{extension}"))
.collect();
(format!("lib{name}.a"), shared)
};
for dir in dirs {
let has_archive = is_file(&dir.join(&archive));
let has_shared = shared.iter().any(|file| is_file(&dir.join(file)));
if has_archive || has_shared {
return (!has_shared).then(|| dir.join(&archive));
}
}
None
}
fn shared_library_extensions(target: &str) -> &'static [&'static str] {
if target.contains("-apple-") {
&["tbd", "dylib", "so"]
} else {
&["so"]
}
}
fn prefers_static_libraries(target_features: &[&str], target: &str) -> bool {
target_features
.iter()
.flat_map(|features| features.split(','))
.filter_map(|feature| match feature.trim() {
"+crt-static" => Some(true),
"-crt-static" => Some(false),
_ => None,
})
.next_back()
.unwrap_or_else(|| target.contains("musl"))
}
fn link_target<'a>(target: Option<&'a str>, rustc_host: Option<&'a str>) -> &'a str {
target.or(rustc_host).unwrap_or("unknown")
}
fn default_library_dirs(
target: &str,
rustc_host: Option<&str>,
library_path: Option<&std::ffi::OsStr>,
) -> Vec<PathBuf> {
if rustc_host != Some(target) {
return Vec::new();
}
let fixed: Vec<String> = if target.contains("-apple-") {
vec!["/usr/lib".into(), "/usr/local/lib".into()]
} else if target.contains("-linux-") {
let multiarch = multiarch_name(target);
vec![
format!("/usr/local/lib/{multiarch}"),
format!("/lib/{multiarch}"),
format!("/usr/lib/{multiarch}"),
"/usr/local/lib64".into(),
"/lib64".into(),
"/usr/lib64".into(),
"/usr/local/lib".into(),
"/lib".into(),
"/usr/lib".into(),
]
} else {
return Vec::new();
};
library_path
.map(std::env::split_paths)
.into_iter()
.flatten()
.filter(|dir| !dir.as_os_str().is_empty())
.chain(fixed.into_iter().map(PathBuf::from))
.collect()
}
fn multiarch_name(target: &str) -> String {
match target.split('-').collect::<Vec<_>>()[..] {
[arch, _vendor, os, env] => format!("{arch}-{os}-{env}"),
_ => target.to_string(),
}
}
fn is_native_archive_name(name: &str) -> bool {
crate::native_link_key::ends_with_ignore_ascii_case(name, ".a")
|| crate::native_link_key::ends_with_ignore_ascii_case(name, ".lib")
}
fn is_cargo_package_dir(normalized: &str) -> bool {
let Some(rest) = normalized.strip_prefix("<CARGO_HOME>") else {
return false;
};
let mut parts = rest.split(['/', '\\']).filter(|part| !part.is_empty());
matches!(
(parts.next(), parts.next()),
(Some("registry"), Some("src")) | (Some("git"), Some("checkouts"))
)
}
fn unique_dirs<'a>(dirs: impl IntoIterator<Item = &'a PathBuf>) -> Vec<PathBuf> {
let mut kept: Vec<PathBuf> = Vec::new();
for dir in dirs {
if !kept.contains(dir) {
kept.push(dir.clone());
}
}
kept
}
fn dirs_outside_packages<'a>(
dirs: &'a [PathBuf],
normalize: impl Fn(&str) -> String + 'a,
) -> impl Iterator<Item = &'a PathBuf> {
dirs.iter()
.filter(move |dir| !is_cargo_package_dir(&normalize(&dir.to_string_lossy())))
}
fn dirs_under(dirs: &[PathBuf], root: &Path) -> Vec<PathBuf> {
if dirs.is_empty() {
return Vec::new();
}
let root = resolved_path(root);
dirs.iter()
.filter(|dir| resolved_path(dir).starts_with(&root))
.cloned()
.collect()
}
fn is_under_any(path: &Path, roots: &[PathBuf]) -> bool {
roots.iter().any(|root| path.starts_with(root))
}
const SYSTEM_LIBRARY_DIRS: &[&str] = &[
"/lib",
"/lib32",
"/lib64",
"/libx32",
"/usr/lib",
"/usr/lib32",
"/usr/lib64",
"/usr/libx32",
"/usr/local/lib",
"/usr/local/lib64",
"/opt/homebrew",
"/usr/local/Cellar",
"/usr/local/opt",
"/home/linuxbrew/.linuxbrew",
"/opt/local",
"/nix/store",
"/gnu/store",
"/System",
"/Library",
"/Applications",
];
fn system_library_dirs() -> Vec<PathBuf> {
SYSTEM_LIBRARY_DIRS.iter().map(PathBuf::from).collect()
}
fn build_tree_roots(args: &RustcArgs) -> Vec<PathBuf> {
build_tree_roots_of(
args.out_dir.as_deref(),
args.target.as_deref(),
args.path_normalization_root(),
)
}
fn build_tree_roots_of(
out_dir: Option<&Path>,
target: Option<&str>,
workspace: Option<&Path>,
) -> Vec<PathBuf> {
out_dir
.and_then(crate::compiler::platform::cargo_profile_dir)
.map(|profile| cargo_target_dir(&profile, target))
.into_iter()
.chain(workspace.map(Path::to_path_buf))
.collect()
}
fn cargo_target_dir(profile: &Path, target: Option<&str>) -> PathBuf {
let Some(parent) = profile.parent() else {
return profile.to_path_buf();
};
let target_name = target.map(|target| match target.strip_suffix(".json") {
Some(spec) => Path::new(spec).file_name().unwrap_or_default(),
None => std::ffi::OsStr::new(target),
});
match (parent.parent(), target_name) {
(Some(grandparent), Some(name)) if parent.file_name() == Some(name) => {
grandparent.to_path_buf()
}
_ => parent.to_path_buf(),
}
}
pub(crate) fn native_dir_archives(dir: &Path) -> Result<Vec<PathBuf>> {
let mut archives = Vec::new();
for path in native_dir_entries(dir)? {
if path
.file_name()
.is_some_and(|name| is_native_archive_name(&name.to_string_lossy()))
&& path.is_file()
{
archives.push(path);
}
}
Ok(archives)
}
fn native_dir_static_frameworks(dir: &Path) -> Result<Vec<PathBuf>> {
let mut frameworks = Vec::new();
for path in native_dir_entries(dir)? {
let Some(name) = path
.file_name()
.and_then(|name| name.to_str())
.and_then(|name| name.strip_suffix(".framework"))
else {
continue;
};
let binary = path.join(name);
if binary.is_file() && crate::native_archive::has_archive_magic(&binary)? {
frameworks.push(binary);
}
}
Ok(frameworks)
}
fn framework_request(spec: &str) -> Option<&str> {
let (kind, name) = spec.split_once('=')?;
let kind = kind.split_once(':').map_or(kind, |(kind, _)| kind);
let name = name.split_once(':').map_or(name, |(_, rename)| rename);
(kind == "framework" && !name.is_empty()).then_some(name)
}
fn is_link_arg_spec(spec: &str) -> bool {
spec.split_once('=')
.is_some_and(|(kind, _)| kind.split_once(':').map_or(kind, |(kind, _)| kind) == "link-arg")
}
fn resolve_static_framework(name: &str, dirs: &[PathBuf]) -> Result<Option<PathBuf>> {
for dir in dirs {
let binary = dir.join(format!("{name}.framework")).join(name);
if binary.is_file() {
return Ok(crate::native_archive::has_archive_magic(&binary)?.then_some(binary));
}
}
Ok(None)
}
fn native_dir_entries(dir: &Path) -> Result<Vec<PathBuf>> {
let entries = match std::fs::read_dir(dir) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => {
return Err(error)
.with_context(|| format!("reading native search dir {}", dir.display()));
}
};
let mut paths = Vec::new();
for entry in entries {
paths.push(
entry
.with_context(|| format!("reading native search dir {}", dir.display()))?
.path(),
);
}
paths.sort();
Ok(paths)
}
fn fold_native_dir_archives<H: KeyFold>(
hasher: &mut H,
dirs: &[PathBuf],
list: impl Fn(&Path) -> Result<Vec<PathBuf>>,
hash: impl Fn(&Path) -> Result<String>,
) -> Result<Vec<PathBuf>> {
let mut hashed = Vec::new();
for (index, dir) in dirs.iter().enumerate() {
for archive in list(dir)? {
let digest = hash(&archive)?;
let name = archive
.strip_prefix(dir)
.unwrap_or(&archive)
.to_string_lossy();
fold_field(
hasher,
b"native_dir_archive.v1:",
format!("{index}/{name}={digest}").as_bytes(),
);
hashed.push(archive);
}
}
Ok(hashed)
}
fn resolved_path(path: &Path) -> PathBuf {
std::fs::canonicalize(path)
.or_else(|_| std::path::absolute(path))
.unwrap_or_else(|_| path.to_path_buf())
}
pub(crate) fn needs_native_bundle_audit(args: &RustcArgs, native_dirs: &[PathBuf]) -> bool {
args.emits_rlib() && !native_dirs.is_empty()
}
fn packs_bundled_libs(unstable_flags: &[String]) -> bool {
unstable_flags.iter().any(|flag| {
let name = flag.split_once('=').map_or(flag.as_str(), |(name, _)| name);
name.replace('_', "-") == "packed-bundled-libs"
})
}
fn rlib_bundle(spec: &StaticLibSpec<'_>, emits_rlib: bool, packs_all: bool) -> Option<bool> {
match spec {
StaticLibSpec::Archive {
bundle: true,
whole_archive,
..
} if emits_rlib => Some(*whole_archive || packs_all),
_ => None,
}
}
fn unix_link_arguments(args: &RustcArgs) -> Result<crate::native_link_key::LinkArgInputs> {
let mut inputs = crate::native_link_key::LinkArgInputs::default();
for (key, value) in &args.codegen_opts {
if let ("link-arg" | "link-args", Some(value)) = (key.as_str(), value.as_deref()) {
let parsed = crate::native_link_key::unix_link_arg_inputs(key, value)?;
inputs.files.extend(parsed.files);
inputs.dirs.extend(parsed.dirs);
inputs.libs.extend(parsed.libs);
}
}
Ok(inputs)
}
struct NativeSearchDirs<'a> {
native: &'a [PathBuf],
framework: &'a [PathBuf],
}
struct NativeLinkContext<'a> {
native_windows_msvc: bool,
target: &'a str,
default_dirs: Vec<PathBuf>,
out_dir: Option<&'a Path>,
build_tree: Vec<PathBuf>,
system_dirs: Vec<PathBuf>,
path_normalizer: &'a PathNormalizer,
}
impl NativeLinkContext<'_> {
fn apple_dynamic_dirs(&self, dirs: &[PathBuf]) -> Vec<PathBuf> {
let build_tree = std::cell::OnceCell::new();
unique_dirs(dirs)
.into_iter()
.filter(|dir| {
let resolved = resolved_path(dir);
!is_under_any(&resolved, &self.system_dirs)
|| is_under_any(
&resolved,
build_tree.get_or_init(|| {
self.build_tree
.iter()
.map(|root| resolved_path(root))
.collect::<Vec<_>>()
}),
)
})
.collect()
}
fn scanned_dirs(&self, dirs: &[PathBuf]) -> Vec<PathBuf> {
let unique = unique_dirs(dirs);
let build_tree = std::cell::OnceCell::new();
let resolved_build_tree = || {
build_tree.get_or_init(|| {
self.build_tree
.iter()
.map(|root| resolved_path(root))
.collect::<Vec<_>>()
})
};
dirs_outside_packages(&unique, |dir| self.path_normalizer.normalize(dir))
.filter(|dir| {
let dir = resolved_path(dir);
!is_under_any(&dir, &self.system_dirs) || is_under_any(&dir, resolved_build_tree())
})
.cloned()
.collect()
}
}
fn native_scan_dirs(
args: &RustcArgs,
dirs: &[PathBuf],
context: &NativeLinkContext<'_>,
) -> Vec<PathBuf> {
if args.links_native_closure() {
context.scanned_dirs(dirs)
} else if args.emits_rlib()
&& let Some(out_dir) = context.out_dir
{
dirs_under(&unique_dirs(dirs), out_dir)
} else {
Vec::new()
}
}
fn fold_native_link_inputs<H: KeyFold>(
hasher: &mut H,
args: &RustcArgs,
search: &NativeSearchDirs<'_>,
context: &NativeLinkContext<'_>,
file_hasher: &FileHasher<'_>,
) -> Result<KeyedNativeArchives> {
let crate_name = args.crate_name.as_deref().unwrap_or("unknown");
let links = args.invokes_linker();
let unix_link = links && !context.native_windows_msvc;
let link_arguments = if unix_link {
unix_link_arguments(args)?
} else {
crate::native_link_key::LinkArgInputs::default()
};
let spelled_oso_root = crate::compiler::rustc::oso_prefix_root_for_key(args);
let oso_root = spelled_oso_root.as_deref().map(resolved_path);
let archive_use = |path: &Path| match (spelled_oso_root.as_deref(), oso_root.as_deref()) {
(Some(spelled_root), Some(root)) => oso_archive_use(args, path, spelled_root, root),
_ => linked_archive_use(args, path, None),
};
let hash_archive = |path: &Path| {
let usage = archive_use(path);
let digest = file_hasher.hash_static_lib_for(path, usage)?;
tracing::trace!(
"[key:{}] native_archive:{}={} ({usage:?})",
crate_name,
path.display(),
&digest[..digest.len().min(24)]
);
Ok::<_, anyhow::Error>(digest)
};
let mut lib_dirs = search.native.to_vec();
lib_dirs.extend(link_arguments.dirs.iter().cloned());
let mut library_dirs = lib_dirs.clone();
library_dirs.extend(context.default_dirs.iter().cloned());
let target_features: Vec<&str> = args
.codegen_opts
.iter()
.filter(|(key, _)| key == "target-feature")
.filter_map(|(_, value)| value.as_deref())
.collect();
let prefer_static = prefers_static_libraries(&target_features, context.target);
let shared_extensions = shared_library_extensions(context.target);
let packs_all = packs_bundled_libs(&args.unstable_flags);
if unix_link && context.target.contains("-apple-") {
fold_field(hasher, b"apple_install_names.v1:", b"");
for (index, dir) in context.apple_dynamic_dirs(&library_dirs).iter().enumerate() {
for path in native_dir_entries(dir)? {
if path.is_file()
&& path
.extension()
.is_some_and(|ext| matches!(ext.to_str(), Some("tbd" | "dylib" | "so")))
{
fold_apple_install_name(hasher, &path, dir, index)?;
}
}
}
for (index, dir) in context
.apple_dynamic_dirs(search.framework)
.iter()
.enumerate()
{
for path in native_dir_entries(dir)? {
let Some(name) = path
.file_name()
.and_then(|name| name.to_str())
.and_then(|name| name.strip_suffix(".framework"))
else {
continue;
};
for binary in [path.join(format!("{name}.tbd")), path.join(name)] {
if binary.is_file() && !crate::native_archive::has_archive_magic(&binary)? {
fold_apple_install_name(hasher, &binary, dir, index)?;
}
}
}
}
let requests = args
.link_libs
.iter()
.filter_map(|spec| unix_library_request(spec))
.chain(
link_arguments
.libs
.iter()
.map(|(name, verbatim)| (name.as_str(), *verbatim)),
);
for (index, (name, verbatim)) in requests.enumerate() {
if !verbatim {
continue;
}
for dir in &library_dirs {
let path = dir.join(name);
if path.is_file() {
if !context
.apple_dynamic_dirs(std::slice::from_ref(dir))
.is_empty()
&& !crate::native_archive::has_archive_magic(&path)?
{
fold_apple_install_name(hasher, &path, dir, index)?;
}
break;
}
}
}
}
let mut archives = Vec::new();
let mut bundled = Vec::new();
let effective_libs = effective_native_libraries(&args.link_libs);
for (index, lib) in args.link_libs.iter().enumerate() {
hasher.update(b"link_lib:");
hasher.update(lib.as_bytes());
hasher.update(b"\n");
tracing::trace!("[key:{}] link_lib:{}", crate_name, lib);
let Some(&lib) = effective_libs.get(&index) else {
continue;
};
if links && is_link_arg_spec(lib) {
anyhow::bail!(
"native library spec {lib:?} hands the linker a raw argument the key does not \
model"
);
}
let dirs = if unix_link { &lib_dirs } else { search.native };
let spec = static_lib_spec(lib);
let mut resolved = resolve_native_static_lib(lib, dirs, file_hasher, links, archive_use)?;
let is_static = matches!(spec, StaticLibSpec::Archive { .. });
if resolved.is_none()
&& is_static
&& unresolved_static_lib_is_error(links, context.native_windows_msvc)
{
anyhow::bail!(
"native static library {lib:?} is in no -L directory; the linker could take \
a copy the key does not hash"
);
}
if let Some((path, _)) = &resolved
&& let Some(packed) = rlib_bundle(&spec, args.emits_rlib(), packs_all)
{
bundled.push(BundledArchive {
path: path.clone(),
packed,
});
}
if resolved.is_none()
&& unix_link
&& let Some((name, verbatim)) = unix_library_request(lib)
&& let Some(path) = resolve_unix_library(
name,
verbatim,
&library_dirs,
prefer_static,
shared_extensions,
Path::is_file,
)
{
let hash = hash_archive(&path)?;
resolved = Some((path, hash));
}
if links
&& let Some(name) = framework_request(lib)
&& let Some(path) = resolve_static_framework(name, search.framework)?
{
let hash = hash_archive(&path)?;
resolved = Some((path, hash));
}
if let Some((path, content_hash)) = resolved {
hasher.update(b"link_lib_content:");
hasher.update(content_hash.as_bytes());
hasher.update(b"\n");
tracing::trace!(
"[key:{}] link_lib_content:{}={} ({})",
crate_name,
lib,
&content_hash[..content_hash.len().min(16)],
path.display()
);
archives.push(path);
}
}
for (index, file) in link_arguments.files.iter().enumerate() {
if !file.is_file() {
anyhow::bail!("linker input {} is not a regular file", file.display());
}
let digest = if is_native_archive_name(&file.to_string_lossy()) {
hash_archive(file)?
} else {
file_hasher
.hash(file)
.with_context(|| format!("hashing linker input {}", file.display()))?
};
fold_field(
hasher,
b"link_arg_input.v1:",
format!("{index}={digest}").as_bytes(),
);
tracing::trace!("[key:{}] link_arg_input:{}", crate_name, file.display());
}
for (index, (name, verbatim)) in link_arguments.libs.iter().enumerate() {
if let Some(path) = resolve_unix_library(
name,
*verbatim,
&library_dirs,
true,
shared_extensions,
Path::is_file,
) {
let digest = hash_archive(&path)?;
fold_field(
hasher,
b"link_arg_library.v1:",
format!("{index}={digest}").as_bytes(),
);
tracing::trace!("[key:{}] link_arg_library:{}", crate_name, path.display());
archives.push(path);
}
}
let scan_dirs = native_scan_dirs(args, &lib_dirs, context);
archives.extend(fold_native_dir_archives(
hasher,
&scan_dirs,
native_dir_archives,
hash_archive,
)?);
if links {
archives.extend(fold_native_dir_archives(
hasher,
&context.scanned_dirs(search.framework),
native_dir_static_frameworks,
hash_archive,
)?);
}
let audit_dirs = if args.emits_rlib() {
context.scanned_dirs(search.native)
} else {
Vec::new()
};
Ok(KeyedNativeArchives {
archives,
bundled,
dirs: audit_dirs,
})
}
fn effective_native_libraries(libraries: &[String]) -> BTreeMap<usize, &str> {
let mut by_name: BTreeMap<&str, (usize, &str)> = BTreeMap::new();
for (index, library) in libraries.iter().enumerate() {
let (kind, name) = library.split_once('=').unwrap_or(("", library));
let spec = if kind.is_empty() {
by_name
.get(name)
.map_or(library.as_str(), |(_, spec)| *spec)
} else {
library.as_str()
};
by_name.insert(name, (index, spec));
}
by_name.into_values().collect()
}
fn fold_apple_install_name<H: KeyFold>(
hasher: &mut H,
path: &Path,
dir: &Path,
index: usize,
) -> Result<()> {
let name = apple_install_name::read(path)?;
let file = path.strip_prefix(dir).unwrap_or(path).to_string_lossy();
fold_field(
hasher,
b"apple_install_name.v1:",
format!("{index}/{file}={name}").as_bytes(),
);
Ok(())
}
fn native_linker_side_files_are_unmodeled(args: &RustcArgs) -> bool {
let apple_target = match args.target.as_deref() {
Some(target) => is_builtin_apple_target(target),
None => cfg!(target_vendor = "apple"),
};
args.codegen_opts.iter().any(|(key, value)| {
matches!(key.as_str(), "link-arg" | "link-args")
&& value
.as_deref()
.is_some_and(|value| linker_value_has_unmodeled_file(value, apple_target))
})
}
fn is_builtin_apple_target(target: &str) -> bool {
matches!(
target,
"aarch64-apple-darwin"
| "aarch64-apple-ios"
| "aarch64-apple-ios-macabi"
| "aarch64-apple-ios-sim"
| "aarch64-apple-tvos"
| "aarch64-apple-tvos-sim"
| "aarch64-apple-visionos"
| "aarch64-apple-visionos-sim"
| "aarch64-apple-watchos"
| "aarch64-apple-watchos-sim"
| "arm64_32-apple-watchos"
| "arm64e-apple-darwin"
| "arm64e-apple-ios"
| "arm64e-apple-tvos"
| "armv7k-apple-watchos"
| "armv7s-apple-ios"
| "i386-apple-ios"
| "i686-apple-darwin"
| "x86_64-apple-darwin"
| "x86_64-apple-ios"
| "x86_64-apple-ios-macabi"
| "x86_64-apple-tvos"
| "x86_64-apple-watchos-sim"
| "x86_64h-apple-darwin"
)
}
fn linker_value_has_unmodeled_file(value: &str, apple_target: bool) -> bool {
value.split([',', '=', ' ', '\t', '\n', '\r']).any(|token| {
matches!(
token,
"-map"
| "-Map"
| "--Map"
| "-order_file"
| "-sectorder"
| "--symbol-ordering-file"
| "--call-graph-ordering-file"
| "--section-ordering-file"
) || token.eq_ignore_ascii_case("/map")
|| ascii_prefix_eq_ignore_case(token, "/map:")
|| ascii_prefix_eq_ignore_case(token, "/mapinfo:")
|| ascii_prefix_eq_ignore_case(token, "/order:")
|| ascii_prefix_eq_ignore_case(token, "/call-graph-ordering-file:")
|| (token.starts_with('@')
&& !(apple_target
&& (token == "@loader_path"
|| token.starts_with("@loader_path/")
|| token == "@rpath"
|| token.starts_with("@rpath/")
|| token == "@executable_path"
|| token.starts_with("@executable_path/"))))
})
}
fn ascii_prefix_eq_ignore_case(value: &str, prefix: &str) -> bool {
value
.get(..prefix.len())
.is_some_and(|head| head.eq_ignore_ascii_case(prefix))
}
#[derive(Debug, PartialEq, Eq)]
enum StaticLibSpec<'a> {
NotStatic,
Archive {
files: Vec<String>,
bundle: bool,
whole_archive: bool,
},
Unmodeled(&'a str),
}
fn static_lib_spec(spec: &str) -> StaticLibSpec<'_> {
let (kind, name) = spec.split_once('=').unwrap_or(("", spec));
let (kind, modifiers) = kind.split_once(':').unwrap_or((kind, ""));
if kind != "framework" && name.contains(':') {
return StaticLibSpec::Unmodeled(spec);
}
if kind != "static" {
return StaticLibSpec::NotStatic;
}
if name.is_empty() {
return StaticLibSpec::Unmodeled(spec);
}
let mut verbatim = false;
let mut bundle = true;
let mut whole_archive = false;
for modifier in modifiers.split(',').filter(|m| !m.is_empty()) {
match modifier {
"+verbatim" => verbatim = true,
"-verbatim" => verbatim = false,
"+bundle" => bundle = true,
"-bundle" => bundle = false,
"+whole-archive" => whole_archive = true,
"-whole-archive" => whole_archive = false,
"+as-needed" | "-as-needed" => {}
_ => return StaticLibSpec::Unmodeled(spec),
}
}
let files = if verbatim {
vec![name.to_string()]
} else {
vec![format!("lib{name}.a"), format!("{name}.lib")]
};
StaticLibSpec::Archive {
files,
bundle,
whole_archive,
}
}
fn sentinelized_env_dep_value(
paths: &EnvDepPaths<'_>,
resolved: &EnvDepValue<'_>,
normalized: &str,
) -> String {
if let Some(rel) = paths.out_dir_suffix(resolved) {
let unit = out_dir_unit(paths.out_dir);
if rel.is_empty() {
format!("<OUT_DIR:{unit}>")
} else {
format!("<OUT_DIR:{unit}>/{}", rel.trim_start_matches('/'))
}
} else {
normalized.to_string()
}
}
fn normalize_env_dep_value_with_hasher(
crate_name: &str,
var: &str,
value: &EnvDepValue<'_>,
paths: &EnvDepPaths<'_>,
file_hasher: &FileHasher<'_>,
path_normalizer: &PathNormalizer,
aliased_out_dir: Option<&Path>,
) -> NormalizedEnvDep {
let val = value.raw;
if aliased_out_dir.is_some_and(|dir| value_at_or_under(val, dir)) {
return NormalizedEnvDep {
value: val.to_string(),
decision: EnvDepNormalizationDecision::AliasedOutDir,
};
}
let resolved = value
.canonical()
.and_then(crate::path_normalizer::canonical_form)
.unwrap_or_else(|| lexically_resolve_path(val));
let normalized = path_normalizer.normalize(&resolved);
if resolved == val && normalized == val {
return NormalizedEnvDep {
value: val.to_string(),
decision: EnvDepNormalizationDecision::Unchanged,
};
}
let forced = !is_manifest_dir_var(var)
&& path_normalizer.path_only_env_vars().iter().any(|entry| {
matches!(entry.split_once(':'), Some((krate, v)) if krate == crate_name && v == var)
});
let resolved_value = (resolved != val).then(|| EnvDepValue::new(&resolved));
let resolved_value = resolved_value.as_ref().unwrap_or(value);
if forced {
return NormalizedEnvDep {
value: sentinelized_env_dep_value(paths, resolved_value, &normalized),
decision: EnvDepNormalizationDecision::ForcedPathOnly,
};
}
let decision = env_dep_path_only_decision(
var,
resolved_value,
paths,
path_normalizer.path_only_env_vars(),
file_hasher,
);
if decision == EnvDepNormalizationDecision::NormalizedPathOnly {
return NormalizedEnvDep {
value: sentinelized_env_dep_value(paths, resolved_value, &normalized),
decision: EnvDepNormalizationDecision::NormalizedPathOnly,
};
}
NormalizedEnvDep {
value: val.to_string(),
decision,
}
}
#[cfg(test)]
fn normalize_env_dep_value(
crate_name: &str,
var: &str,
val: &str,
source_files: &[std::path::PathBuf],
path_normalizer: &PathNormalizer,
) -> NormalizedEnvDep {
normalize_env_dep_value_in(crate_name, var, val, source_files, path_normalizer, None)
}
#[cfg(test)]
fn normalize_env_dep_value_in(
crate_name: &str,
var: &str,
val: &str,
source_files: &[std::path::PathBuf],
path_normalizer: &PathNormalizer,
out_dir: Option<&OsStr>,
) -> NormalizedEnvDep {
normalize_env_dep_value_with_hasher(
crate_name,
var,
&EnvDepValue::new(val),
&EnvDepPaths::new(out_dir, source_files),
&FileHasher::new(),
path_normalizer,
None,
)
}
pub(crate) fn value_at_or_under(value: &str, dir: &Path) -> bool {
Path::new(value).starts_with(dir)
}
fn env_dep_bakes_out_dir(
var: &str,
decision: EnvDepNormalizationDecision,
under: impl FnOnce() -> bool,
) -> bool {
decision.keeps_literal_value() && (var == "OUT_DIR" || under())
}
fn env_dep_path_only_decision(
var: &str,
value: &EnvDepValue<'_>,
paths: &EnvDepPaths<'_>,
allowlist: &[String],
file_hasher: &FileHasher<'_>,
) -> EnvDepNormalizationDecision {
if is_manifest_dir_var(var) {
return EnvDepNormalizationDecision::KeptAbsoluteManifestDir;
}
if !(var == "OUT_DIR"
|| allowlist.iter().any(|v| v == var)
|| paths.value_is_under_out_dir(value))
{
return EnvDepNormalizationDecision::KeptAbsoluteNotPathOnly;
}
if !path_is_only_used_for_includes(value.probe(), paths.source_probes()) {
return EnvDepNormalizationDecision::KeptAbsoluteNoIncludeProof;
}
env_dep_source_decision(var, paths.source_files, file_hasher)
}
pub(crate) fn is_manifest_dir_var(var: &str) -> bool {
var.eq_ignore_ascii_case("CARGO_MANIFEST_DIR")
}
fn path_probe(path: &Path) -> PathBuf {
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
}
fn out_dir_probe(out_dir: Option<&OsStr>) -> Option<PathBuf> {
let probe = path_probe(Path::new(out_dir?));
probe.is_absolute().then_some(probe)
}
fn out_dir_unit(out_dir: Option<&OsStr>) -> String {
out_dir
.map(Path::new)
.and_then(Path::parent)
.and_then(Path::file_name)
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default()
}
struct EnvDepValue<'a> {
raw: &'a str,
canonical: OnceCell<Option<PathBuf>>,
}
impl<'a> EnvDepValue<'a> {
fn new(raw: &'a str) -> Self {
Self {
raw,
canonical: OnceCell::new(),
}
}
fn canonical(&self) -> Option<&Path> {
self.canonical
.get_or_init(|| std::fs::canonicalize(self.raw).ok())
.as_deref()
}
fn probe(&self) -> &Path {
self.canonical().unwrap_or(Path::new(self.raw))
}
}
struct EnvDepPaths<'a> {
out_dir: Option<&'a OsStr>,
out_probe: OnceCell<Option<PathBuf>>,
source_files: &'a [PathBuf],
source_probes: OnceCell<Vec<PathBuf>>,
}
impl<'a> EnvDepPaths<'a> {
fn new(out_dir: Option<&'a OsStr>, source_files: &'a [PathBuf]) -> Self {
Self {
out_dir,
out_probe: OnceCell::new(),
source_files,
source_probes: OnceCell::new(),
}
}
fn out_probe(&self) -> Option<&Path> {
self.out_probe
.get_or_init(|| out_dir_probe(self.out_dir))
.as_deref()
}
fn source_probes(&self) -> &[PathBuf] {
self.source_probes
.get_or_init(|| self.source_files.iter().map(|f| path_probe(f)).collect())
}
fn out_dir_suffix(&self, value: &EnvDepValue<'_>) -> Option<String> {
out_dir_relative_suffix(value.probe(), self.out_probe()?)
}
fn value_is_under_out_dir(&self, value: &EnvDepValue<'_>) -> bool {
self.out_dir_suffix(value).is_some()
}
}
fn out_dir_relative_suffix(value_probe: &Path, out_probe: &Path) -> Option<String> {
value_probe
.strip_prefix(out_probe)
.ok()
.map(|rel| rel.to_string_lossy().replace('\\', "/"))
}
fn path_is_only_used_for_includes(value_probe: &Path, source_probes: &[PathBuf]) -> bool {
source_probes.iter().any(|f| f.starts_with(value_probe))
}
fn env_dep_source_decision(
var: &str,
source_files: &[std::path::PathBuf],
file_hasher: &FileHasher<'_>,
) -> EnvDepNormalizationDecision {
let mut proven = false;
for file in source_files {
match file_hasher.env_dep_use(file, var) {
Ok(SourceEnvDepUse::Unused) => {}
Ok(SourceEnvDepUse::IncludeLocator) => {
proven |= file.extension().is_some_and(|ext| ext == "rs");
}
Ok(SourceEnvDepUse::RuntimeValue) => {
return EnvDepNormalizationDecision::KeptAbsoluteRuntimeUse;
}
Err(e) => {
tracing::debug!(
"keeping env dep {var} absolute: failed to inspect source {}: {}",
file.display(),
e
);
return EnvDepNormalizationDecision::KeptAbsoluteScanError;
}
}
}
if proven {
EnvDepNormalizationDecision::NormalizedPathOnly
} else {
EnvDepNormalizationDecision::KeptAbsoluteNoIncludeProof
}
}
const SOURCE_ENV_DEP_SCANNER_VERSION: u32 = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SourceEnvDepUse {
Unused,
IncludeLocator,
RuntimeValue,
}
impl SourceEnvDepUse {
fn memo_code(self) -> i64 {
match self {
Self::Unused => 0,
Self::IncludeLocator => 1,
Self::RuntimeValue => 2,
}
}
fn from_memo_code(code: i64) -> Option<Self> {
match code {
0 => Some(Self::Unused),
1 => Some(Self::IncludeLocator),
2 => Some(Self::RuntimeValue),
_ => None,
}
}
}
fn source_env_dep_use(source: &str, var: &str) -> SourceEnvDepUse {
let bytes = source.as_bytes();
let mut i = 0usize;
let mut groups: Vec<bool> = Vec::new();
let mut include_depth = 0usize;
let mut include_locator = false;
for _ in 0..bytes.len() {
let Some(&byte) = bytes.get(i) else { break };
let whitespace = rust_whitespace_len(bytes, i);
if whitespace > 0 {
i += whitespace;
continue;
}
match byte {
b'/' if comment_starts_at(bytes, i) => i = skip_comment(bytes, i),
b'"' => i = skip_quoted_string(bytes, i + 1),
b'\'' => i = skip_char_literal_or_lifetime(source, i),
b'b' | b'c' | b'r' if raw_string_starts_at(bytes, i).is_some() => {
i = skip_raw_string(bytes, i);
}
b'(' | b'[' | b'{' => {
groups.push(false);
i += 1;
}
b')' | b']' | b'}' => {
if groups.pop() == Some(true) {
include_depth -= 1;
}
i += 1;
}
b'0'..=b'9' => i = skip_ident_bytes(bytes, i),
b if is_ident_start(b) => {
let ident_start = i;
i = skip_ident_bytes(bytes, i);
let ident = &bytes[ident_start..i];
let Some(open) = parse_macro_open(bytes, i) else {
continue;
};
if matches!(ident, b"env" | b"option_env") {
match parse_env_macro_name(source, open + 1) {
Some(name) if name != var => {}
Some(_) if include_depth > 0 => include_locator = true,
None if include_depth > 0 => {}
_ => return SourceEnvDepUse::RuntimeValue,
}
}
let include = is_include_macro(ident);
groups.push(include);
include_depth += usize::from(include);
i = open + 1;
}
_ => i += 1,
}
}
if include_locator {
SourceEnvDepUse::IncludeLocator
} else {
SourceEnvDepUse::Unused
}
}
fn is_include_macro(name: &[u8]) -> bool {
matches!(name, b"include" | b"include_str" | b"include_bytes")
}
fn parse_env_macro_name(source: &str, after_open: usize) -> Option<&str> {
let bytes = source.as_bytes();
let start = skip_trivia(bytes, after_open);
if bytes.get(start) != Some(&b'"') {
return None;
}
let len = bytes[start + 1..]
.iter()
.position(|b| matches!(b, b'"' | b'\\'))?;
let end = start + 1 + len;
(bytes[end] == b'"').then(|| &source[start + 1..end])
}
fn parse_macro_open(bytes: &[u8], after_ident: usize) -> Option<usize> {
let bang = skip_trivia(bytes, after_ident);
if bytes.get(bang) != Some(&b'!') {
return None;
}
let open = skip_trivia(bytes, bang + 1);
matches!(bytes.get(open), Some(b'(' | b'[' | b'{')).then_some(open)
}
fn skip_trivia(bytes: &[u8], mut i: usize) -> usize {
for _ in 0..bytes.len() {
let whitespace = rust_whitespace_len(bytes, i);
if whitespace > 0 {
i += whitespace;
} else if comment_starts_at(bytes, i) {
i = skip_comment(bytes, i);
} else {
break;
}
}
i
}
fn comment_starts_at(bytes: &[u8], i: usize) -> bool {
bytes.get(i) == Some(&b'/') && matches!(bytes.get(i + 1), Some(b'/' | b'*'))
}
fn rust_whitespace_len(bytes: &[u8], i: usize) -> usize {
match bytes.get(i..).unwrap_or_default() {
[b'\t' | b'\n' | b'\x0B' | b'\x0C' | b'\r' | b' ', ..] => 1,
[0xC2, 0x85, ..] => 2,
[0xE2, 0x80, 0x8E | 0x8F | 0xA8 | 0xA9, ..] => 3,
[0xEF, 0xBB, 0xBF, ..] => 3,
_ => 0,
}
}
fn skip_comment(bytes: &[u8], mut i: usize) -> usize {
if bytes.get(i + 1) == Some(&b'/') {
for _ in 0..bytes.len() {
match bytes.get(i) {
Some(b'\n') | None => break,
Some(_) => i += 1,
}
}
return i;
}
let mut depth = 1usize;
i += 2;
for _ in 0..bytes.len() {
match (bytes.get(i), bytes.get(i + 1)) {
(Some(b'/'), Some(b'*')) => {
depth += 1;
i += 2;
}
(Some(b'*'), Some(b'/')) => {
depth -= 1;
i += 2;
if depth == 0 {
return i;
}
}
(Some(_), _) => i += 1,
(None, _) => break,
}
}
bytes.len()
}
fn skip_quoted_string(bytes: &[u8], mut i: usize) -> usize {
for _ in 0..bytes.len() {
match bytes.get(i) {
Some(b'\\') => i += 2,
Some(b'"') => return i + 1,
Some(_) => i += 1,
None => break,
}
}
bytes.len()
}
fn skip_char_literal_or_lifetime(source: &str, quote: usize) -> usize {
let bytes = source.as_bytes();
if bytes.get(quote + 1) == Some(&b'\\') {
return skip_char_literal(bytes, quote + 1);
}
let Some(ch) = source[quote + 1..].chars().next() else {
return bytes.len();
};
let close = quote + 1 + ch.len_utf8();
if bytes.get(close) == Some(&b'\'') {
close + 1
} else {
quote + 1
}
}
fn skip_char_literal(bytes: &[u8], mut i: usize) -> usize {
for _ in 0..bytes.len() {
match bytes.get(i) {
Some(b'\\') => i += 2,
Some(b'\'') => return i + 1,
Some(_) => i += 1,
None => break,
}
}
bytes.len()
}
fn raw_string_starts_at(bytes: &[u8], i: usize) -> Option<usize> {
let mut cursor = i;
if matches!(bytes.get(cursor), Some(b'b' | b'c')) {
cursor += 1;
}
if bytes.get(cursor) != Some(&b'r') {
return None;
}
cursor += 1;
for _ in 0..bytes.len() {
if bytes.get(cursor) != Some(&b'#') {
break;
}
cursor += 1;
}
if bytes.get(cursor) == Some(&b'"') {
Some(cursor)
} else {
None
}
}
fn skip_raw_string(bytes: &[u8], i: usize) -> usize {
let Some(open_quote) = raw_string_starts_at(bytes, i) else {
return i + 1;
};
let hashes = open_quote - i - usize::from(bytes[i] != b'r') - 1;
for cursor in open_quote + 1..bytes.len() {
if bytes[cursor] == b'"'
&& cursor + hashes < bytes.len()
&& bytes[cursor + 1..cursor + 1 + hashes]
.iter()
.all(|b| *b == b'#')
{
return cursor + hashes + 1;
}
}
bytes.len()
}
fn is_ident_start(byte: u8) -> bool {
byte == b'_' || byte.is_ascii_alphabetic() || !byte.is_ascii()
}
fn skip_ident_bytes(bytes: &[u8], mut i: usize) -> usize {
for _ in 0..bytes.len() {
let Some(&byte) = bytes.get(i) else { break };
if !(is_ident_start(byte) || byte.is_ascii_digit()) || rust_whitespace_len(bytes, i) > 0 {
break;
}
i += 1;
}
i
}
fn compute_static_lib_hash(path: &Path, usage: StaticLibUse) -> Result<String> {
let bytes = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
if bytes.starts_with(b"!<thin>\n") {
anyhow::bail!(
"thin static archive {} has external members that are not modeled",
path.display()
);
}
if let Some(identity) = crate::native_archive::portable_static_archive_identity(&bytes)
&& !(usage == StaticLibUse::Linked && identity.macho_dwarf_members)
{
return Ok(identity.digest);
}
let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let encoded_path = absolute.as_os_str().as_encoded_bytes();
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache.native-ar.path-bound-fallback.v1\0");
hasher.update(&(encoded_path.len() as u64).to_le_bytes());
hasher.update(encoded_path);
hasher.update(&(bytes.len() as u64).to_le_bytes());
hasher.update(&bytes);
Ok(format!("path-ar-v1:{}", hasher.finalize().to_hex()))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepInfo {
pub source_files: Vec<std::path::PathBuf>,
pub env_deps: Vec<(String, String)>,
}
pub(crate) const PREDICTION_SCHEMA: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct InputPrediction {
pub(crate) schema: u32,
pub(crate) sources: Vec<PathBuf>,
pub(crate) env_deps: Vec<(String, String)>,
#[serde(default)]
pub(crate) tree: Option<String>,
}
impl InputPrediction {
pub(crate) fn from_dep_info(dep_info: &DepInfo, tree: Option<String>) -> Self {
Self {
schema: PREDICTION_SCHEMA,
sources: dep_info.source_files.clone(),
env_deps: dep_info.env_deps.clone(),
tree,
}
}
}
pub(crate) const PORTABLE_PREDICTION_SCHEMA: u32 = 101;
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) struct PortablePrediction {
pub(crate) schema: u32,
pub(crate) sources: Vec<Portable>,
pub(crate) env_deps: Vec<(String, Portable)>,
pub(crate) tree: String,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub(crate) enum Portable {
Literal(String),
OutDir(String),
Workspace(String),
Registry(String),
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Places<'a> {
pub(crate) out_dir: Option<&'a str>,
pub(crate) workspace: Option<&'a str>,
pub(crate) registry: Option<&'a str>,
}
#[derive(Debug, Clone)]
pub(crate) struct PortableRoots {
pub(crate) out_dir: PathBuf,
pub(crate) target: PathBuf,
pub(crate) canonical_target: PathBuf,
pub(crate) registry_src: PathBuf,
}
pub(crate) fn portable_prediction(
dep_info: &DepInfo,
roots: &PortableRoots,
tree: Option<&str>,
) -> Option<PortablePrediction> {
let tree = tree?;
let sources = dep_info
.source_files
.iter()
.map(|source| portable_source(source, roots))
.collect::<Option<Vec<_>>>()?;
if !sources
.iter()
.any(|source| matches!(source, Portable::OutDir(_)))
{
return None;
}
let env_deps = dep_info
.env_deps
.iter()
.map(|(name, value)| Some((name.clone(), portable_env_value(value, roots)?)))
.collect::<Option<Vec<_>>>()?;
Some(PortablePrediction {
schema: PORTABLE_PREDICTION_SCHEMA,
sources,
env_deps,
tree: tree.to_string(),
})
}
fn portable_source(source: &Path, roots: &PortableRoots) -> Option<Portable> {
if let Some(suffix) = out_dir_suffix(source.as_os_str(), &roots.out_dir) {
return Some(Portable::OutDir(suffix));
}
if !under_registry_src(source, &roots.registry_src) {
return None;
}
Some(Portable::Literal(source.to_str()?.to_string()))
}
fn portable_env_value(value: &str, roots: &PortableRoots) -> Option<Portable> {
if let Some(suffix) = out_dir_suffix(std::ffi::OsStr::new(value), &roots.out_dir) {
return Some(Portable::OutDir(suffix));
}
let spells = |root: &Path| {
value
.as_bytes()
.starts_with(root.as_os_str().as_encoded_bytes())
};
if spells(&roots.target) || spells(&roots.canonical_target) {
return None;
}
Some(Portable::Literal(value.to_string()))
}
impl PortablePrediction {
pub(crate) fn resolve(&self, places: &Places<'_>) -> Option<InputPrediction> {
let place = |portable: &Portable| match portable {
Portable::Literal(value) => Some(value.clone()),
Portable::OutDir(suffix) => Some(format!("{}{suffix}", places.out_dir?)),
Portable::Workspace(suffix) => Some(format!("{}{suffix}", places.workspace?)),
Portable::Registry(suffix) => Some(format!("{}{suffix}", places.registry?)),
};
Some(InputPrediction {
schema: PREDICTION_SCHEMA,
sources: self
.sources
.iter()
.map(|source| place(source).map(PathBuf::from))
.collect::<Option<_>>()?,
env_deps: self
.env_deps
.iter()
.map(|(name, value)| Some((name.clone(), place(value)?)))
.collect::<Option<_>>()?,
tree: Some(self.tree.clone()),
})
}
}
fn validate_portable_prediction(
record: &PortablePrediction,
guard: &str,
places: &Places<'_>,
stat: impl Fn(&Path) -> Option<std::fs::Metadata>,
exists: impl Fn(&Path) -> bool,
env_value: impl Fn(&str) -> Option<String>,
) -> std::result::Result<DepInfo, Rejection> {
if record.tree != guard {
return Err(Rejection::TreeChanged);
}
let resolved = record.resolve(places).ok_or(Rejection::NoRecord)?;
validate_prediction(&resolved, stat, exists, env_value)
}
pub(crate) fn mentions_root(content: &[u8], roots: &[&Path]) -> bool {
roots.iter().any(|root| {
let raw = root.as_os_str().as_encoded_bytes();
let doubled: Vec<u8> = raw
.iter()
.flat_map(|&byte| std::iter::repeat_n(byte, if byte == b'\\' { 2 } else { 1 }))
.collect();
crate::build_script::find_bytes(content, raw).is_some()
|| crate::build_script::find_bytes(content, &doubled).is_some()
})
}
fn out_dir_spells_no_root(out_dir: &Path, roots: &[&Path]) -> bool {
fn walk(directory: &Path, roots: &[&Path], budget: &mut usize) -> Option<()> {
for entry in std::fs::read_dir(directory).ok()? {
let path = entry.ok()?.path();
*budget = budget.checked_sub(1)?;
let metadata = std::fs::symlink_metadata(&path).ok()?;
let content = if metadata.file_type().is_symlink() {
std::fs::read_link(&path)
.ok()?
.into_os_string()
.into_encoded_bytes()
} else if metadata.is_dir() {
walk(&path, roots, budget)?;
continue;
} else if metadata.is_file() {
std::fs::read(&path).ok()?
} else {
return None;
};
if mentions_root(&content, roots) {
return None;
}
}
Some(())
}
let mut budget = OUT_DIR_TREE_MAX_ENTRIES;
walk(out_dir, roots, &mut budget).is_some()
}
pub(crate) fn relocatable_record(
args: &RustcArgs,
dep_info: &DepInfo,
tree: Option<&str>,
) -> Option<(String, PortablePrediction)> {
relocatable_record_in(args, dep_info, tree, std::env::vars_os().collect())
}
fn relocatable_record_in(
args: &RustcArgs,
dep_info: &DepInfo,
tree: Option<&str>,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
) -> Option<(String, PortablePrediction)> {
let roots = portable_roots(args, &vars)?;
let identity = relocatable_prediction_identity(args, vars)?;
let record = portable_prediction(dep_info, &roots, tree)?;
out_dir_spells_no_root(&roots.out_dir, &[&roots.target, &roots.canonical_target])
.then_some((identity, record))
}
pub(crate) fn same_checkout_guard(
args: &RustcArgs,
dep_info: &DepInfo,
tree: Option<String>,
) -> Option<String> {
let workspace = workspace_roots(args, &std::env::vars_os().collect::<Vec<_>>());
same_tree_guard(
tree,
workspace.is_some(),
workspace.is_some_and(|workspace| guard_covers(&dep_info.source_files, &workspace)),
)
}
fn same_tree_guard(tree: Option<String>, workspace_unit: bool, covered: bool) -> Option<String> {
if workspace_unit && !covered {
None
} else {
tree
}
}
pub(crate) fn workspace_record(
args: &RustcArgs,
dep_info: &DepInfo,
tree: Option<&str>,
) -> Option<(String, PortablePrediction)> {
let vars: Vec<_> = std::env::vars_os().collect();
let workspace = workspace_roots(args, &vars)?;
let record = workspace_portable_prediction(dep_info, &workspace, tree)?;
if !guard_covers(&dep_info.source_files, &workspace) {
return None;
}
let identity = workspace_prediction_identity(args, vars, &workspace)?;
let named = [
workspace.root.as_path(),
&workspace.canonical_root,
&workspace.target,
&workspace.canonical_target,
];
if let Some(out_dir) = &workspace.out_dir
&& !out_dir_spells_no_root(out_dir, &named)
{
return None;
}
Some((identity, record))
}
fn guard_covers(sources: &[PathBuf], workspace: &WorkspaceRoots) -> bool {
let mut entered = HashMap::new();
sources.iter().all(|source| {
guard_place(source, workspace).is_some_and(|(root, below, skips_build_dirs)| {
walk_reaches(root, &below, skips_build_dirs, &mut entered)
})
})
}
fn guard_place<'a>(
source: &Path,
workspace: &'a WorkspaceRoots,
) -> Option<(&'a Path, String, bool)> {
if !source.has_root() {
workspace_relative_source(source, workspace)?;
let below = format!("{}/{}", workspace.cwd, source.to_str()?);
return Some((&workspace.root, below, true));
}
match workspace_portable_value(source.as_os_str(), workspace)? {
Portable::OutDir(below) => Some((workspace.out_dir.as_deref()?, below, false)),
Portable::Workspace(below) => Some((&workspace.root, below, true)),
Portable::Literal(_) | Portable::Registry(_) => None,
}
}
fn walk_reaches(
root: &Path,
below: &str,
skips_build_dirs: bool,
entered: &mut HashMap<(PathBuf, bool), bool>,
) -> bool {
use std::path::Component;
let components: Vec<Component<'_>> = Path::new(below)
.components()
.filter(|component| matches!(component, Component::Normal(_) | Component::ParentDir))
.collect();
let Some((_, directories)) = components.split_last() else {
return true;
};
let mut directory = root.to_path_buf();
let mut depth = 0usize;
for component in directories {
if *component == Component::ParentDir {
let Some(up) = depth.checked_sub(1) else {
return false;
};
depth = up;
directory.pop();
continue;
}
directory.push(component);
depth += 1;
let walked = *entered
.entry((directory.clone(), skips_build_dirs))
.or_insert_with(|| {
std::fs::symlink_metadata(&directory).is_ok_and(|metadata| metadata.is_dir())
&& !(skips_build_dirs && crate::tree_stamp::holds_cargo_build_tag(&directory))
});
if !walked {
return false;
}
}
true
}
fn workspace_portable_prediction(
dep_info: &DepInfo,
workspace: &WorkspaceRoots,
tree: Option<&str>,
) -> Option<PortablePrediction> {
let tree = tree?;
let sources = dep_info
.source_files
.iter()
.map(|source| {
if !source.has_root() {
return workspace_relative_source(source, workspace);
}
let relocated = workspace_portable_value(source.as_os_str(), workspace)?;
(!matches!(relocated, Portable::Literal(_))).then_some(relocated)
})
.collect::<Option<Vec<_>>>()?;
let env_deps = dep_info
.env_deps
.iter()
.map(|(name, value)| {
let value = workspace_portable_value(std::ffi::OsStr::new(value), workspace)?;
Some((name.clone(), value))
})
.collect::<Option<Vec<_>>>()?;
Some(PortablePrediction {
schema: PORTABLE_PREDICTION_SCHEMA,
sources,
env_deps,
tree: tree.to_string(),
})
}
fn workspace_relative_source(source: &Path, workspace: &WorkspaceRoots) -> Option<Portable> {
let spelled = source.to_str()?;
let from_root = format!("{}/{spelled}", workspace.cwd);
let components = from_root.strip_prefix(['/', '\\'])?;
(stays_below(components.split(['/', '\\']), 0) && stays_below(components.split('/'), 0))
.then(|| Portable::Literal(spelled.to_string()))
}
fn workspace_portable_value(
value: &std::ffi::OsStr,
workspace: &WorkspaceRoots,
) -> Option<Portable> {
if let Some(out_dir) = &workspace.out_dir
&& let Some(suffix) = out_dir_suffix(value, out_dir)
{
return Some(Portable::OutDir(suffix));
}
let bytes = value.as_encoded_bytes();
let spells = |root: &Path| bytes.starts_with(root.as_os_str().as_encoded_bytes());
if spells(&workspace.target) || spells(&workspace.canonical_target) {
return None;
}
if let Some(suffix) = suffix_within(value, &workspace.root, 0)
.or_else(|| suffix_within(value, &workspace.canonical_root, 0))
{
return Some(Portable::Workspace(suffix));
}
Some(Portable::Literal(value.to_str()?.to_string()))
}
fn portable_roots(
args: &RustcArgs,
vars: &[(std::ffi::OsString, std::ffi::OsString)],
) -> Option<PortableRoots> {
let out_dir = PathBuf::from(env_var_in(vars, "OUT_DIR")?);
let target = args.target_dir()?;
let registry_src = registry_src_root(Path::new(env_var_in(vars, "CARGO_MANIFEST_DIR")?))?;
Some(PortableRoots {
canonical_target: std::fs::canonicalize(&target).ok()?,
registry_src: registry_src.to_path_buf(),
out_dir,
target,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Rejection {
Disabled,
NotEligible,
NoRecord,
Missing,
NotRegular,
EnvChanged,
Sibling,
TreeChanged,
}
impl Rejection {
fn as_str(self) -> &'static str {
match self {
Rejection::Disabled => "disabled",
Rejection::NotEligible => "not-eligible",
Rejection::NoRecord => "no-record",
Rejection::Missing => "missing",
Rejection::NotRegular => "not-regular",
Rejection::EnvChanged => "env-changed",
Rejection::Sibling => "sibling",
Rejection::TreeChanged => "tree-changed",
}
}
}
fn needs_tree_guard(externs: &[crate::args::ExternDep], manifest_dir: Option<&Path>) -> bool {
has_dylib_extern(externs) || manifest_dir.is_some_and(|dir| !is_registry_package(dir))
}
fn has_dylib_extern(externs: &[crate::args::ExternDep]) -> bool {
externs.iter().any(|ext| {
ext.path.as_deref().is_some_and(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
matches!(
crate::compiler::classify_by_filename(name),
crate::compiler::ArtifactKind::DynamicLibrary
)
})
})
})
}
fn mod_sibling_candidate(file: &Path) -> Option<PathBuf> {
let stem = file.file_stem()?.to_str()?;
let parent = file.parent()?;
if file.extension().and_then(|e| e.to_str()) != Some("rs") {
return None;
}
match stem {
"mod" => Some(
parent
.parent()?
.join(parent.file_name()?)
.with_extension("rs"),
),
"lib" | "main" => None,
stem => Some(parent.join(stem).join("mod.rs")),
}
}
pub(crate) fn validate_prediction(
record: &InputPrediction,
stat: impl Fn(&Path) -> Option<std::fs::Metadata>,
exists: impl Fn(&Path) -> bool,
env_value: impl Fn(&str) -> Option<String>,
) -> std::result::Result<DepInfo, Rejection> {
for file in &record.sources {
let Some(metadata) = stat(file) else {
return Err(Rejection::Missing);
};
if !metadata.is_file() {
return Err(Rejection::NotRegular);
}
if mod_sibling_candidate(file).is_some_and(|sibling| exists(&sibling)) {
return Err(Rejection::Sibling);
}
}
for (var, recorded) in &record.env_deps {
let matches = match env_value(var) {
Some(value) => value == *recorded,
None => recorded.is_empty(),
};
if !matches {
return Err(Rejection::EnvChanged);
}
}
Ok(DepInfo {
source_files: record.sources.clone(),
env_deps: record.env_deps.clone(),
})
}
pub(crate) struct PredictionIdentityParts<'a> {
pub(crate) rustc_version: &'a str,
pub(crate) inner_rustc: Option<&'a Path>,
pub(crate) current_dir: Option<&'a Path>,
pub(crate) source_file: &'a Path,
pub(crate) closure_args: &'a [String],
pub(crate) skip_path_remap: bool,
}
const PREDICTION_ENV: &[&str] = &[
"RUSTFLAGS",
"CARGO_ENCODED_RUSTFLAGS",
"RUSTC_BOOTSTRAP",
"OUT_DIR",
"CARGO_MANIFEST_DIR",
];
fn prediction_identity_in_env(
parts: &PredictionIdentityParts<'_>,
vars: Vec<(std::ffi::OsString, std::ffi::OsString)>,
) -> String {
let mut hasher = blake3::Hasher::new();
hasher.update(b"kache-input-prediction-v1\n");
fold_field(
&mut hasher,
b"prediction_schema:",
PREDICTION_SCHEMA.to_string().as_bytes(),
);
fold_field(
&mut hasher,
b"key_version:",
CACHE_KEY_VERSION.to_string().as_bytes(),
);
fold_field(
&mut hasher,
b"rustc_version:",
parts.rustc_version.as_bytes(),
);
fold_field(
&mut hasher,
b"inner_rustc:",
&parts
.inner_rustc
.map(|path| env_os_key_bytes(path.as_os_str()))
.unwrap_or_default(),
);
fold_field(
&mut hasher,
b"current_dir:",
&parts
.current_dir
.map(|path| env_os_key_bytes(path.as_os_str()))
.unwrap_or_default(),
);
fold_field(
&mut hasher,
b"source_file:",
&env_os_key_bytes(parts.source_file.as_os_str()),
);
fold_field(
&mut hasher,
b"closure_args_len:",
parts.closure_args.len().to_string().as_bytes(),
);
for arg in parts.closure_args {
fold_field(&mut hasher, b"closure_arg:", arg.as_bytes());
}
let by_name: std::collections::BTreeMap<Vec<u8>, &std::ffi::OsString> = vars
.iter()
.map(|(name, value)| (env_text_key_bytes(name), value))
.collect();
for name in PREDICTION_ENV {
fold_field(&mut hasher, b"env_var:", name.as_bytes());
match by_name.get(name.as_bytes()) {
Some(value) => {
fold_field(&mut hasher, b"env_set:", b"1");
fold_field(&mut hasher, b"env_val:", &env_os_key_bytes(value));
}
None => fold_field(&mut hasher, b"env_set:", b"0"),
}
}
for (name, value) in cargo_cfg_pairs(vars.iter().cloned()) {
fold_field(&mut hasher, b"cargo_cfg_name:", &env_text_key_bytes(&name));
fold_field(&mut hasher, b"cargo_cfg_val:", &env_os_key_bytes(&value));
}
fold_field(
&mut hasher,
b"skip_path_remap:",
if parts.skip_path_remap { b"1" } else { b"0" },
);
hasher.finalize().to_hex().to_string()
}
pub struct FileHasher<'db> {
cache: Option<FileHashCache<'db>>,
daemon_socket: Option<PathBuf>,
use_input_predictions: bool,
prediction_flight_dir: Option<PathBuf>,
discovery_flight: RefCell<Option<crate::store::StoreLock>>,
prefetched: RefCell<HashMap<FileFingerprint, PrefetchedHash>>,
recent_hashes: RefCell<HashMap<PathBuf, RecentHash>>,
env_dep_uses: RefCell<HashMap<(String, String), SourceEnvDepUse>>,
stats: FileHashStatsCells,
too_new: TooNewGuard,
guard_inputs: RefCell<Vec<ObservedFingerprint>>,
pending_memo: RefCell<Vec<(ObservedFingerprint, String)>>,
tree_walks: RefCell<Vec<GuardWalk>>,
own_dirs: Vec<PathBuf>,
}
impl Drop for FileHasher<'_> {
fn drop(&mut self) {
self.flush_memo();
}
}
#[derive(Default)]
struct TooNewGuard {
invocation_start_ns: i64,
margin_ns: i64,
saw_too_new: Cell<bool>,
saw_write_since_start: Cell<bool>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct FileHashStats {
pub cache_hits: u64,
pub cache_misses: u64,
pub bytes_hashed: u64,
}
#[derive(Default)]
struct FileHashStatsCells {
cache_hits: Cell<u64>,
cache_misses: Cell<u64>,
bytes_hashed: Cell<u64>,
}
#[derive(Debug, Clone)]
struct PrefetchedHash {
hash: String,
cache_hit: bool,
bytes_hashed: u64,
}
#[derive(Clone)]
struct RecentHash {
hash: String,
fingerprint: Option<FileFingerprint>,
}
fn read_file_hash(path: &Path) -> Result<String> {
#[cfg(test)]
BEFORE_READ.with(|hook| {
if let Some(hook) = hook.borrow_mut().as_mut() {
hook(path);
}
});
hash_file(path)
}
#[cfg(test)]
pub(crate) type BeforeRead = Box<dyn FnMut(&Path)>;
#[cfg(test)]
thread_local! {
static BEFORE_READ: std::cell::RefCell<Option<BeforeRead>> =
const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
pub(crate) fn set_before_read(hook: Option<BeforeRead>) {
BEFORE_READ.with(|slot| *slot.borrow_mut() = hook);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CcContentHashes {
pub(crate) raw: String,
pub(crate) mapped: String,
}
pub(crate) const CC_INPUT_CHANGED_WHILE_SCANNED: &str = "an input changed while it was scanned";
impl FileHasher<'static> {
pub fn new() -> Self {
FileHasher {
cache: None,
daemon_socket: None,
use_input_predictions: false,
prediction_flight_dir: None,
discovery_flight: RefCell::new(None),
prefetched: RefCell::new(HashMap::new()),
recent_hashes: RefCell::new(HashMap::new()),
env_dep_uses: RefCell::new(HashMap::new()),
stats: FileHashStatsCells::default(),
too_new: TooNewGuard::default(),
guard_inputs: RefCell::new(Vec::new()),
pending_memo: RefCell::new(Vec::new()),
tree_walks: RefCell::new(Vec::new()),
own_dirs: Vec::new(),
}
}
#[cfg(test)]
pub fn persistent(index_db_path: &Path) -> Self {
match FileHashCache::open(index_db_path) {
Ok(cache) => FileHasher {
cache: Some(cache),
daemon_socket: None,
use_input_predictions: false,
prediction_flight_dir: None,
discovery_flight: RefCell::new(None),
prefetched: RefCell::new(HashMap::new()),
recent_hashes: RefCell::new(HashMap::new()),
env_dep_uses: RefCell::new(HashMap::new()),
stats: FileHashStatsCells::default(),
too_new: TooNewGuard::default(),
guard_inputs: RefCell::new(Vec::new()),
pending_memo: RefCell::new(Vec::new()),
tree_walks: RefCell::new(Vec::new()),
own_dirs: Vec::new(),
},
Err(e) => {
tracing::debug!(
"file hash cache disabled for {}: {e}",
index_db_path.display()
);
FileHasher::new()
}
}
}
}
impl<'db> FileHasher<'db> {
pub fn flush_memo(&self) {
let mut pending = std::mem::take(&mut *self.pending_memo.borrow_mut());
pending.retain(|(observed, _)| observed.settled());
if pending.is_empty() {
return;
}
let _trace = crate::phase_trace::phase("memo_flush");
let Some(cache) = &self.cache else {
return;
};
let db = cache.db();
let _ = db.busy_timeout(std::time::Duration::from_millis(100));
let written = (|| -> rusqlite::Result<()> {
db.execute_batch("BEGIN IMMEDIATE")?;
for (observed, hash) in &pending {
if let Err(error) = cache.put(&observed.fingerprint, hash) {
let _ = db.execute_batch("ROLLBACK");
return Err(error);
}
}
db.execute_batch("COMMIT")
})();
let _ = db.busy_timeout(std::time::Duration::from_millis(5000));
if let Err(error) = written {
tracing::debug!(rows = pending.len(), "file hash memo not written: {error}");
}
}
#[cfg(test)]
pub(crate) fn flush_memo_as_if_settled(&self) {
for (observed, _) in self.pending_memo.borrow_mut().iter_mut() {
observed.observed_ns = observed.observed_ns.saturating_add(HASH_SETTLE_NS);
}
self.flush_memo();
}
pub(crate) fn from_cache(cache: FileHashCache<'db>) -> Self {
FileHasher {
cache: Some(cache),
daemon_socket: None,
use_input_predictions: false,
prediction_flight_dir: None,
discovery_flight: RefCell::new(None),
prefetched: RefCell::new(HashMap::new()),
recent_hashes: RefCell::new(HashMap::new()),
env_dep_uses: RefCell::new(HashMap::new()),
stats: FileHashStatsCells::default(),
too_new: TooNewGuard::default(),
guard_inputs: RefCell::new(Vec::new()),
pending_memo: RefCell::new(Vec::new()),
tree_walks: RefCell::new(Vec::new()),
own_dirs: Vec::new(),
}
}
pub(crate) fn with_daemon(mut self, socket_path: PathBuf) -> Self {
self.daemon_socket = Some(socket_path);
self
}
pub(crate) fn with_input_predictions(mut self, enabled: bool) -> Self {
self.use_input_predictions = enabled;
self
}
pub(crate) fn with_prediction_flights(mut self, cache_dir: Option<PathBuf>) -> Self {
self.prediction_flight_dir = cache_dir;
self
}
pub(crate) fn with_own_dirs(mut self, paths: impl IntoIterator<Item = PathBuf>) -> Self {
self.own_dirs.extend(paths);
self
}
pub(crate) fn take_discovery_flight(&self) -> Option<crate::store::StoreLock> {
self.discovery_flight.borrow_mut().take()
}
fn note_tree_walk(&self, walk: GuardWalk) {
self.tree_walks.borrow_mut().push(walk);
}
fn take_tree_walks(&self) -> Vec<GuardWalk> {
std::mem::take(&mut *self.tree_walks.borrow_mut())
}
fn uses_input_predictions(&self) -> bool {
self.use_input_predictions && self.cache.is_some()
}
pub fn arm_too_new_guard(&mut self, invocation_start_ns: i64, margin_ns: i64) {
self.too_new.invocation_start_ns = invocation_start_ns;
self.too_new.margin_ns = margin_ns;
}
pub fn too_new(&self) -> bool {
self.too_new.saw_too_new.get()
}
fn guard_input(&self, observed: &ObservedFingerprint) {
if self.too_new.invocation_start_ns > 0 {
self.guard_inputs.borrow_mut().push(observed.clone());
}
}
pub fn take_guarded_inputs(&self) -> Vec<ObservedFingerprint> {
std::mem::take(&mut *self.guard_inputs.borrow_mut())
}
pub fn guarded_inputs_unchanged_since_hash(inputs: &[ObservedFingerprint]) -> bool {
if inputs.is_empty() {
return false;
}
inputs.iter().all(|input| {
let expected = &input.fingerprint;
expected.inode != 0
&& FileFingerprint::from_path(Path::new(&expected.path))
.is_ok_and(|current| current == *expected)
})
}
pub fn guarded_inputs_moved_since_hash(inputs: &[ObservedFingerprint]) -> bool {
inputs.iter().any(|input| {
let expected = &input.fingerprint;
FileFingerprint::from_path(Path::new(&expected.path)).map_or(true, |current| {
current.size != expected.size
|| current.mtime_ns != expected.mtime_ns
|| current.inode != expected.inode
})
})
}
pub(crate) fn inputs_changed_since_keyed(&self) -> bool {
if self.too_new.saw_write_since_start.get() {
return true;
}
let start_ns = self.too_new.invocation_start_ns;
let guarded = self.guard_inputs.borrow();
let recent = self.recent_hashes.borrow();
let distinct: std::collections::HashSet<&FileFingerprint> = guarded
.iter()
.map(|observed| &observed.fingerprint)
.collect();
distinct.into_iter().any(|keyed| {
let path = Path::new(&keyed.path);
let holds_hashed_bytes = || {
recent.get(path).is_some_and(|hashed| {
hashed.fingerprint.as_ref() == Some(keyed)
&& hash_file(path).is_ok_and(|bytes| bytes == hashed.hash)
})
};
!FileFingerprint::from_path(path).is_ok_and(|current| current == *keyed)
|| (!stamp_is_settled(keyed, start_ns) && !holds_hashed_bytes())
})
}
fn note_too_new(&self, fingerprint: &FileFingerprint) {
if self.too_new.invocation_start_ns > 0 {
let since = self
.too_new
.invocation_start_ns
.saturating_sub(self.too_new.margin_ns);
if stamp_written_since(fingerprint, since) {
self.too_new.saw_too_new.set(true);
let now_ns = wall_clock_ns();
if [fingerprint.mtime_ns, fingerprint.ctime_ns]
.into_iter()
.any(|stamp| {
stamp >= since.saturating_sub(stamp_window_ns(stamp)) && stamp <= now_ns
})
{
self.too_new.saw_write_since_start.set(true);
}
}
}
}
pub fn stats(&self) -> FileHashStats {
FileHashStats {
cache_hits: self.stats.cache_hits.get(),
cache_misses: self.stats.cache_misses.get(),
bytes_hashed: self.stats.bytes_hashed.get(),
}
}
pub(crate) fn supports_cc_preprocess_memo(&self) -> bool {
self.cache.is_some()
}
pub(crate) fn supports_input_predictions(&self) -> bool {
self.cache.is_some()
}
fn store_lacks_unit(&self, crate_name: &str, unit: &str) -> bool {
let _trace = crate::phase_trace::phase("crate_presence");
let Some(cache) = self.cache.as_ref() else {
return false;
};
match cache.has_entry_for_unit(crate_name, unit) {
Ok(present) => !present,
Err(error) => {
tracing::debug!("crate presence lookup failed: {error}");
false
}
}
}
pub(crate) fn record_input_prediction(
&self,
identity: &str,
crate_name: Option<&str>,
dep_info: &DepInfo,
tree: Option<String>,
) {
let Some(cache) = self.cache.as_ref() else {
return;
};
let record = InputPrediction::from_dep_info(dep_info, tree);
let json = match serde_json::to_string(&record) {
Ok(json) => json,
Err(error) => {
tracing::debug!("input prediction encode failed: {error}");
return;
}
};
if let Err(error) = cache.put_input_prediction(identity, record.schema, crate_name, &json) {
tracing::debug!("input prediction record failed: {error}");
}
}
pub(crate) fn input_prediction(&self, identity: &str) -> Option<InputPrediction> {
let _trace = crate::phase_trace::phase("prediction_read");
let cache = self.cache.as_ref()?;
let (schema, json) = match cache.get_input_prediction(identity) {
Ok(row) => row?,
Err(error) => {
tracing::debug!("input prediction lookup failed: {error}");
return None;
}
};
if schema != PREDICTION_SCHEMA {
return None;
}
match serde_json::from_str::<InputPrediction>(&json) {
Ok(record) if record.schema == PREDICTION_SCHEMA => Some(record),
Ok(_) => None,
Err(error) => {
tracing::debug!("input prediction decode failed: {error}");
None
}
}
}
pub(crate) fn record_portable_prediction(
&self,
identity: &str,
crate_name: Option<&str>,
record: &PortablePrediction,
) {
let Some(cache) = self.cache.as_ref() else {
return;
};
let json = match serde_json::to_string(record) {
Ok(json) => json,
Err(error) => {
tracing::debug!("portable prediction encode failed: {error}");
return;
}
};
if let Err(error) = cache.put_input_prediction(identity, record.schema, crate_name, &json) {
tracing::debug!("portable prediction record failed: {error}");
}
}
pub(crate) fn portable_prediction(&self, identity: &str) -> Option<PortablePrediction> {
let _trace = crate::phase_trace::phase("prediction_read");
let cache = self.cache.as_ref()?;
let (schema, json) = match cache.get_input_prediction(identity) {
Ok(row) => row?,
Err(error) => {
tracing::debug!("portable prediction lookup failed: {error}");
return None;
}
};
if schema != PORTABLE_PREDICTION_SCHEMA {
return None;
}
match serde_json::from_str::<PortablePrediction>(&json) {
Ok(record) if record.schema == PORTABLE_PREDICTION_SCHEMA => Some(record),
Ok(_) => None,
Err(error) => {
tracing::debug!("portable prediction decode failed: {error}");
None
}
}
}
pub(crate) fn cc_preprocess_memo_recorded(&self, memo_key: &str) -> bool {
self.cache
.as_ref()
.is_some_and(|cache| matches!(cache.get_cc_preprocess_memo(memo_key), Ok(Some(_))))
}
pub(crate) fn cc_preprocess_memo_lookup(
&self,
memo_key: &str,
resolve: impl Fn(&str) -> Vec<PathBuf>,
mapped_content: &impl Fn(&Path) -> Option<CcContentHashes>,
) -> Option<(String, Vec<PathBuf>)> {
let cache = self.cache.as_ref()?;
let record = match cache.get_cc_preprocess_memo(memo_key) {
Ok(record) => record?,
Err(error) => {
tracing::debug!("cc preprocess memo lookup failed: {error}");
return None;
}
};
let digest = record
.preprocessed_hash
.strip_prefix("pb:")
.unwrap_or(&record.preprocessed_hash);
if digest.len() != 64
|| !digest
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
tracing::debug!("cc preprocess memo hash is invalid");
return None;
}
let inputs = record.inputs;
if inputs.is_empty() {
return None;
}
let mut satisfied = Vec::with_capacity(inputs.len());
for expected in &inputs {
satisfied.push(self.memo_input_is_unchanged(expected, &resolve, mapped_content)?);
}
if record.needs_touch
&& let Err(error) = cache.touch_cc_preprocess_memo(memo_key)
{
tracing::debug!("cc preprocess memo touch failed: {error}");
}
Some((record.preprocessed_hash, satisfied))
}
fn memo_input_is_unchanged(
&self,
expected: &CcPreprocessMemoInput,
resolve: &impl Fn(&str) -> Vec<PathBuf>,
mapped_content: &impl Fn(&Path) -> Option<CcContentHashes>,
) -> Option<PathBuf> {
let candidates = resolve(&expected.name);
for path in &candidates {
let Ok(current) = ObservedFingerprint::from_path(path) else {
continue;
};
self.note_too_new(¤t.fingerprint);
if current.fingerprint == expected.fingerprint {
self.guard_input(¤t);
self.recent_hashes.borrow_mut().insert(
absolute_path(path),
RecentHash {
hash: expected.content.clone(),
fingerprint: Some(current.fingerprint),
},
);
return Some(path.clone());
}
if self
.hash(path)
.is_ok_and(|content| content == expected.content)
{
return Some(path.clone());
}
if !expected.mapped.is_empty()
&& mapped_content(path).is_some_and(|read| read.mapped == expected.mapped)
{
return Some(path.clone());
}
}
tracing::debug!(
"cc preprocess memo input {} matched none of {} candidate paths",
expected.name,
candidates.len()
);
None
}
pub(crate) fn cc_preprocess_fingerprints(
&self,
paths: &[(String, PathBuf)],
maps_key: &str,
mapped_content: &impl Fn(&Path) -> Option<CcContentHashes>,
) -> Option<Vec<CcPreprocessMemoInput>> {
if paths.is_empty() {
return None;
}
let _trace = crate::phase_trace::phase("cc_fingerprints");
let mut stamped: Vec<(&String, &PathBuf, ObservedFingerprint)> =
Vec::with_capacity(paths.len());
for (name, path) in paths {
let observed = match ObservedFingerprint::from_path(path) {
Ok(observed) => observed,
Err(error) => {
tracing::debug!(
"cc preprocess memo input {} could not be fingerprinted: {error}",
path.display()
);
return None;
}
};
self.note_too_new(&observed.fingerprint);
stamped.push((name, path, observed));
}
let memoised = self.memoised_hashes(stamped.iter().map(|(_, _, stamp)| &stamp.fingerprint));
let mut pending: Vec<(String, ObservedFingerprint, String, PathBuf)> =
Vec::with_capacity(stamped.len());
for (name, path, observed) in stamped {
let content = match self.header_hash(path, &observed, &memoised) {
Ok(content) => content,
Err(error) => {
tracing::debug!(
"cc preprocess memo input {} could not be hashed: {error}",
path.display()
);
return None;
}
};
pending.push((name.clone(), observed, content, path.clone()));
}
let memo = self.cache.as_ref().filter(|_| !maps_key.is_empty());
let known = match memo {
Some(cache) => {
let contents: Vec<&str> = pending.iter().map(|p| p.2.as_str()).collect();
cache
.get_cc_mapped_hashes(maps_key, &contents)
.unwrap_or_else(|error| {
tracing::debug!("cc mapped hash memo lookup failed: {error}");
Default::default()
})
}
None => Default::default(),
};
let mut known = known;
let mut learned: Vec<(String, String)> = Vec::new();
let mut inputs = Vec::with_capacity(pending.len());
for (name, observed, content, path) in pending {
let mapped = match known.get(&content) {
Some(mapped) => mapped.clone(),
None => {
let read = mapped_content(&path)?;
if read.raw != content {
tracing::debug!(
"cc preprocess memo input {} changed while it was fingerprinted",
path.display()
);
return None;
}
known.insert(content.clone(), read.mapped.clone());
learned.push((content.clone(), read.mapped.clone()));
read.mapped
}
};
inputs.push(CcPreprocessMemoInput {
name,
fingerprint: observed.fingerprint,
content,
mapped,
observed_ns: observed.observed_ns,
});
}
if let Some(cache) = memo
&& let Err(error) = cache.put_cc_mapped_hashes(maps_key, &learned)
{
tracing::debug!("cc mapped hash memo update failed: {error}");
}
inputs.sort_by(|a, b| a.name.cmp(&b.name));
inputs.dedup_by(|a, b| a.name == b.name);
Some(inputs)
}
pub(crate) fn cc_inputs_hide_assembler_input(
&self,
inputs: &[CcPreprocessMemoInput],
scan: &impl Fn(&Path) -> Option<(String, Option<&'static str>)>,
) -> Option<String> {
let _trace = crate::phase_trace::phase("cc_asm_scan");
let known = match &self.cache {
Some(cache) => {
let contents: Vec<&str> = inputs.iter().map(|i| i.content.as_str()).collect();
cache.get_cc_asm_scans(&contents).unwrap_or_else(|error| {
tracing::debug!("cc assembler scan memo lookup failed: {error}");
Default::default()
})
}
None => Default::default(),
};
let mut known = known;
let mut learned: Vec<(String, String)> = Vec::new();
let mut found: Option<String> = None;
for input in inputs {
let verdict = match known.get(&input.content) {
Some(construct) => construct.clone(),
None => {
let Some((raw, scanned)) = scan(Path::new(&input.fingerprint.path)) else {
continue;
};
if raw != input.content {
found.get_or_insert_with(|| CC_INPUT_CHANGED_WHILE_SCANNED.to_string());
continue;
}
let construct = scanned.unwrap_or("").to_string();
known.insert(input.content.clone(), construct.clone());
learned.push((input.content.clone(), construct.clone()));
construct
}
};
if !verdict.is_empty() && found.is_none() {
found = Some(verdict);
}
}
if let Some(cache) = &self.cache
&& let Err(error) = cache.put_cc_asm_scans(&learned)
{
tracing::debug!("cc assembler scan memo update failed: {error}");
}
found
}
pub(crate) fn cc_preprocess_memo_record_if_unchanged(
&self,
memo_key: &str,
preprocessed_hash: &str,
inputs: &[CcPreprocessMemoInput],
mapped_content: &impl Fn(&Path) -> Option<CcContentHashes>,
) {
let Some(cache) = &self.cache else {
return;
};
if inputs.is_empty() {
return;
}
for expected in inputs {
let recorded_path = PathBuf::from(&expected.fingerprint.path);
if self
.memo_input_is_unchanged(
expected,
&|_: &str| vec![recorded_path.clone()],
mapped_content,
)
.is_none()
{
return;
}
}
if let Err(error) = cache.put_cc_preprocess_memo_inputs(memo_key, preprocessed_hash, inputs)
{
tracing::debug!("cc preprocess memo update failed: {error}");
}
}
pub fn prefetch(&self, paths: &[&Path]) {
let _trace = crate::phase_trace::phase("input_hash_prefetch");
let Some(socket_path) = &self.daemon_socket else {
return;
};
let mut requests = Vec::new();
for path in paths {
let Ok(fingerprint) = FileFingerprint::from_path(path) else {
continue;
};
if fingerprint.size < MIN_PERSISTED_HASH_BYTES
|| self.prefetched.borrow().contains_key(&fingerprint)
{
continue;
}
requests.push(crate::daemon::HashFileRequest {
path: fingerprint.path,
size: fingerprint.size,
mtime_ns: fingerprint.mtime_ns,
ctime_ns: fingerprint.ctime_ns,
inode: fingerprint.inode,
});
}
if requests.is_empty() {
return;
}
match crate::daemon::send_hash_files_request(socket_path, requests) {
Ok(results) => {
let mut prefetched = self.prefetched.borrow_mut();
for result in results {
let Some(hash) = result.hash else {
continue;
};
prefetched.insert(
FileFingerprint {
path: result.path,
size: result.size,
mtime_ns: result.mtime_ns,
ctime_ns: result.ctime_ns,
inode: result.inode,
},
PrefetchedHash {
hash,
cache_hit: result.cache_hit,
bytes_hashed: result.bytes_hashed,
},
);
}
}
Err(e) => tracing::debug!("daemon file hash prefetch failed: {e}"),
}
}
pub fn hash(&self, path: &Path) -> Result<String> {
let _trace = crate::phase_trace::phase("input_hash");
let (hash, observed) = self.hash_inner(path, true)?;
if let Some(observed) = &observed {
self.guard_input(observed);
}
self.recent_hashes.borrow_mut().insert(
absolute_path(path),
RecentHash {
hash: hash.clone(),
fingerprint: observed.map(|observed| observed.fingerprint),
},
);
Ok(hash)
}
pub(crate) fn hash_unkeyed(&self, path: &Path) -> Result<String> {
let _trace = crate::phase_trace::phase("input_hash");
self.hash_inner(path, false).map(|(hash, _)| hash)
}
fn guard_file_hash_from_metadata(
&self,
path: &Path,
metadata: &std::fs::Metadata,
observed_ns: i64,
memoised: &HashMap<FileFingerprint, String>,
) -> Result<String> {
#[cfg(unix)]
{
if self.cache.is_none() || absolute_path(path).to_str().is_none() {
return self.hash_unkeyed(path);
}
let observed = ObservedFingerprint {
fingerprint: FileFingerprint {
path: absolute_path(path).to_string_lossy().into_owned(),
size: i64::try_from(metadata.len()).unwrap_or(i64::MAX),
mtime_ns: metadata_mtime_ns(metadata),
ctime_ns: metadata_ctime_ns(metadata),
inode: metadata_inode(metadata),
},
observed_ns,
};
self.guard_file_hash_observed(path, observed, memoised)
}
#[cfg(not(unix))]
{
let _ = (metadata, observed_ns, memoised);
self.hash_unkeyed(path)
}
}
#[cfg(test)]
fn guard_file_hash(
&self,
path: &Path,
memoised: &HashMap<FileFingerprint, String>,
) -> Result<String> {
let observed_ns = wall_clock_ns();
let metadata = std::fs::symlink_metadata(path)?;
self.guard_file_hash_from_metadata(path, &metadata, observed_ns, memoised)
}
#[cfg(any(unix, test))]
fn guard_file_hash_observed(
&self,
path: &Path,
observed: ObservedFingerprint,
memoised: &HashMap<FileFingerprint, String>,
) -> Result<String> {
if !guard_file_memo_eligible(observed.fingerprint.size, observed.fingerprint.inode) {
return self.hash_unkeyed(path);
}
let _trace = crate::phase_trace::phase("input_hash");
if let Some(hash) = memoised.get(&observed.fingerprint) {
self.record_hit();
return Ok(hash.clone());
}
let hash = read_file_hash(path)?;
self.record_miss(observed.fingerprint.size);
if FileFingerprint::from_path(path).is_ok_and(|after| after == observed.fingerprint) {
self.pending_memo
.borrow_mut()
.push((observed, hash.clone()));
}
Ok(hash)
}
fn hash_inner(
&self,
path: &Path,
keyed: bool,
) -> Result<(String, Option<ObservedFingerprint>)> {
let Some(cache) = &self.cache else {
let observed_ns = wall_clock_ns();
let observed = |fingerprint| ObservedFingerprint {
fingerprint,
observed_ns,
};
if !keyed || self.too_new.invocation_start_ns == 0 {
let hash = hash_file(path)?;
return Ok((hash, FileFingerprint::from_path(path).ok().map(observed)));
}
let Ok(before) = FileFingerprint::from_path(path).map(observed) else {
let hash = read_file_hash(path)?;
let after = FileFingerprint::from_path(path).ok();
if after.is_some() {
self.too_new.saw_too_new.set(true);
}
return Ok((hash, after.map(observed)));
};
self.note_too_new(&before.fingerprint);
let hash = read_file_hash(path)?;
let (observed, _) = self.restat_after_read(path, before, true);
return Ok((hash, Some(observed)));
};
let observed = match ObservedFingerprint::from_path(path) {
Ok(observed) => observed,
Err(e) => {
tracing::debug!(
"file hash cache metadata lookup failed for {}: {e}",
path.display()
);
return hash_file(path).map(|hash| (hash, None));
}
};
let fingerprint = &observed.fingerprint;
if keyed {
self.note_too_new(fingerprint);
}
if fingerprint.size < MIN_PERSISTED_HASH_BYTES {
let hash = read_file_hash(path)?;
self.record_miss(fingerprint.size);
let (observed, _) = self.restat_after_read(path, observed, keyed);
return Ok((hash, Some(observed)));
}
if let Some(prefetched) = self.prefetched.borrow().get(fingerprint) {
if prefetched.cache_hit {
self.record_hit();
} else {
self.record_miss_count();
self.record_miss_bytes(prefetched.bytes_hashed);
}
return Ok((prefetched.hash.clone(), Some(observed)));
}
match cache.get(fingerprint) {
Ok(Some(hash)) => {
self.record_hit();
return Ok((hash, Some(observed)));
}
Ok(None) => {}
Err(e) => {
tracing::debug!("file hash cache lookup failed for {}: {e}", path.display());
}
}
let hash = read_file_hash(path)?;
self.record_miss(fingerprint.size);
let (observed, unchanged) = self.restat_after_read(path, observed, keyed);
if unchanged {
self.pending_memo
.borrow_mut()
.push((observed.clone(), hash.clone()));
}
Ok((hash, Some(observed)))
}
fn restat_after_read(
&self,
path: &Path,
before: ObservedFingerprint,
keyed: bool,
) -> (ObservedFingerprint, bool) {
if self.too_new.invocation_start_ns == 0 {
return (before, true);
}
match FileFingerprint::from_path(path) {
Ok(after) if after == before.fingerprint => (before, true),
Ok(after) => {
if keyed {
self.too_new.saw_too_new.set(true);
self.guard_input(&before);
}
let observed_ns = before.observed_ns;
(
ObservedFingerprint {
fingerprint: after,
observed_ns,
},
false,
)
}
Err(_) => {
if keyed {
self.too_new.saw_too_new.set(true);
}
(before, false)
}
}
}
fn env_dep_use(&self, path: &Path, var: &str) -> Result<SourceEnvDepUse> {
let absolute = absolute_path(path);
let recent = self.recent_hashes.borrow().get(&absolute).cloned();
let recent = match recent {
Some(recent) => recent,
None => {
self.hash(path)?;
self.recent_hashes
.borrow()
.get(&absolute)
.cloned()
.expect("a successful hash records its fingerprint")
}
};
if let Some(expected) = recent.fingerprint {
let current = FileFingerprint::from_path(path)
.with_context(|| format!("revalidating {} before env-use scan", path.display()))?;
if current != expected {
anyhow::bail!(
"source {} changed between content hashing and env-use scan",
path.display()
);
}
return self.env_dep_use_for_hash(path, var, &recent.hash);
}
self.scan_env_dep_use(path, var, &recent.hash)
}
fn env_dep_use_for_hash(
&self,
path: &Path,
var: &str,
content_hash: &str,
) -> Result<SourceEnvDepUse> {
let key = (content_hash.to_string(), var.to_string());
if let Some(result) = self.env_dep_uses.borrow().get(&key) {
return Ok(*result);
}
if let Some(cache) = &self.cache {
match cache.get_source_env_dep_use(content_hash, var, SOURCE_ENV_DEP_SCANNER_VERSION) {
Ok(Some(code)) => {
if let Some(result) = SourceEnvDepUse::from_memo_code(code) {
self.env_dep_uses.borrow_mut().insert(key, result);
return Ok(result);
}
}
Ok(None) => {}
Err(error) => {
tracing::debug!("env-use cache lookup failed: {error}");
}
}
}
self.scan_env_dep_use(path, var, content_hash)
}
fn scan_env_dep_use(
&self,
path: &Path,
var: &str,
content_hash: &str,
) -> Result<SourceEnvDepUse> {
let key = (content_hash.to_string(), var.to_string());
let bytes = std::fs::read(path)
.with_context(|| format!("reading {} for env-use scan", path.display()))?;
let observed_hash = blake3::hash(&bytes).to_hex().to_string();
if observed_hash != content_hash {
anyhow::bail!(
"source {} changed between content hashing and env-use scan",
path.display()
);
}
let source = String::from_utf8_lossy(&bytes);
let result = source_env_dep_use(&source, var);
if let Some(cache) = &self.cache
&& let Err(error) = cache.put_source_env_dep_use(
content_hash,
var,
SOURCE_ENV_DEP_SCANNER_VERSION,
result.memo_code(),
)
{
tracing::debug!("env-use cache update failed: {error}");
}
self.env_dep_uses.borrow_mut().insert(key, result);
Ok(result)
}
pub fn hash_static_lib(&self, path: &Path) -> Result<String> {
self.hash_static_lib_for(path, StaticLibUse::Linked)
}
pub fn hash_static_lib_for(&self, path: &Path, usage: StaticLibUse) -> Result<String> {
let Some(cache) = &self.cache else {
if let Ok(fingerprint) = FileFingerprint::from_path(path) {
self.note_too_new(&fingerprint);
}
return compute_static_lib_hash(path, usage);
};
let ObservedFingerprint {
fingerprint,
observed_ns,
} = match ObservedFingerprint::from_path(path) {
Ok(observed) => observed,
Err(e) => {
tracing::debug!(
"static-lib hash metadata lookup failed for {}: {e}",
path.display()
);
return compute_static_lib_hash(path, usage);
}
};
self.note_too_new(&fingerprint);
let size = fingerprint.size;
if size < MIN_PERSISTED_HASH_BYTES {
let hash = compute_static_lib_hash(path, usage)?;
self.record_miss(size);
return Ok(hash);
}
let key = FileFingerprint {
path: format!("{}\0{}", usage.memo_namespace(), fingerprint.path),
size: fingerprint.size,
mtime_ns: fingerprint.mtime_ns,
ctime_ns: fingerprint.ctime_ns,
inode: fingerprint.inode,
};
match cache.get(&key) {
Ok(Some(hash)) => {
self.record_hit();
return Ok(hash);
}
Ok(None) => {}
Err(e) => tracing::debug!("static-lib hash cache lookup failed: {e}"),
}
let hash = compute_static_lib_hash(path, usage)?;
self.record_miss(size);
self.pending_memo.borrow_mut().push((
ObservedFingerprint {
fingerprint: key,
observed_ns,
},
hash.clone(),
));
Ok(hash)
}
fn memoised_hashes<'a>(
&self,
stamps: impl Iterator<Item = &'a FileFingerprint>,
) -> HashMap<String, String> {
let Some(cache) = &self.cache else {
return HashMap::new();
};
let stamps: Vec<&FileFingerprint> = stamps.collect();
cache.get_many(&stamps).unwrap_or_else(|error| {
tracing::debug!("file hash memo batch lookup failed: {error}");
HashMap::new()
})
}
fn header_hash(
&self,
path: &Path,
observed: &ObservedFingerprint,
memoised: &HashMap<String, String>,
) -> Result<String> {
if self.cache.is_none() {
return self.hash(path);
}
let fingerprint = &observed.fingerprint;
let prefetched = self.prefetched.borrow().get(fingerprint).map(|prefetched| {
(
prefetched.hash.clone(),
prefetched.cache_hit,
prefetched.bytes_hashed,
)
});
let (hash, reported) = if let Some((hash, cache_hit, bytes_hashed)) = prefetched {
if cache_hit {
self.record_hit();
} else {
self.record_miss_count();
self.record_miss_bytes(bytes_hashed);
}
(hash, observed.clone())
} else if let Some(hash) = memoised.get(&fingerprint.path) {
self.record_hit();
(hash.clone(), observed.clone())
} else {
let hash = read_file_hash(path)?;
self.record_miss(fingerprint.size);
let (reported, unchanged) = self.restat_after_read(path, observed.clone(), true);
if unchanged {
self.pending_memo
.borrow_mut()
.push((observed.clone(), hash.clone()));
}
(hash, reported)
};
self.guard_input(&reported);
self.recent_hashes.borrow_mut().insert(
absolute_path(path),
RecentHash {
hash: hash.clone(),
fingerprint: Some(reported.fingerprint),
},
);
Ok(hash)
}
fn record_hit(&self) {
self.stats.cache_hits.set(self.stats.cache_hits.get() + 1);
}
fn record_miss(&self, size: i64) {
self.record_miss_count();
if let Ok(size) = u64::try_from(size) {
self.record_miss_bytes(size);
}
}
fn record_miss_count(&self) {
self.stats
.cache_misses
.set(self.stats.cache_misses.get() + 1);
}
fn record_miss_bytes(&self, bytes: u64) {
self.stats
.bytes_hashed
.set(self.stats.bytes_hashed.get().saturating_add(bytes));
}
}
fn is_extra_filename_option(value: &str) -> bool {
value.starts_with("extra-filename=") || value.starts_with("extra_filename=")
}
fn dep_info_pass_args(source_file: &Path, rustc_args: &[String], dep_file: &Path) -> Vec<String> {
let mut dep_args = closure_shaping_args(source_file, rustc_args);
dep_args.push("--emit".to_string());
dep_args.push("dep-info".to_string());
dep_args.push("-o".to_string());
dep_args.push(dep_file.to_string_lossy().into_owned());
dep_args
}
fn closure_shaping_args(source_file: &Path, rustc_args: &[String]) -> Vec<String> {
let source_str = source_file.to_string_lossy();
let rustc_args = crate::compile::strip_incremental_flags(rustc_args);
let mut dep_args = vec![source_str.to_string()];
let mut remaining = rustc_args.iter().peekable();
while let Some(arg) = remaining.next() {
match arg.as_str() {
"--emit" | "--out-dir" | "-o" => {
remaining.next(); }
"-C" | "--codegen"
if remaining
.peek()
.is_some_and(|value| is_extra_filename_option(value)) =>
{
remaining.next();
}
_ if arg.starts_with("--emit=") || arg.starts_with("--out-dir=") => {}
_ if arg.starts_with("-o") => {}
_ if arg
.strip_prefix("-C")
.or_else(|| arg.strip_prefix("--codegen="))
.is_some_and(is_extra_filename_option) => {}
_ if arg.as_str() == source_str.as_ref() => {}
_ => dep_args.push((*arg).clone()),
}
}
dep_args
}
pub(crate) fn first_rustc_error_line(stderr: &str) -> Option<&str> {
let mut fallback = None;
let mut lines = stderr.lines();
while let Some(line) = lines.next() {
if line.trim().is_empty() {
continue;
}
if is_panic_header(line)
&& let Some(message) = lines.clone().find(|next| !next.trim().is_empty())
{
return Some(message);
}
if line.contains(r#""level":"error""#)
|| line.starts_with("error:")
|| line.starts_with("error[")
{
return Some(line);
}
fallback.get_or_insert(line);
}
fallback
}
fn is_panic_header(line: &str) -> bool {
line.starts_with("thread '") && line.contains(" panicked at ")
}
pub fn run_dep_info_pass(
rustc: &Path,
inner_rustc: Option<&Path>,
source_file: &Path,
rustc_args: &[String],
use_response_file: bool,
) -> Result<DepInfo> {
let temp_dir = tempfile::Builder::new()
.prefix("kache-depinfo")
.tempdir()
.context("creating temp dir for dep-info")?;
let dep_file = temp_dir.path().join("deps.d");
let mut cmd = std::process::Command::new(rustc);
if let Some(inner_rustc) = inner_rustc {
cmd.arg(inner_rustc);
}
let dep_args = dep_info_pass_args(source_file, rustc_args, &dep_file);
let response_file = if use_response_file {
let response = crate::compile::RustcResponseFile::new(
dep_args.iter().map(std::string::String::as_str),
)?;
cmd.arg(response.argument());
Some(response)
} else {
cmd.args(&dep_args);
None
};
tracing::trace!("dep-info pre-pass: {:?}", cmd);
let spawned = std::time::Instant::now();
let output = cmd
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output()
.context("running rustc --emit=dep-info")?;
crate::opcounts::record_dep_info_run(spawned.elapsed());
drop(response_file);
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"dep-info pre-pass failed (exit {}): {}",
output.status.code().unwrap_or(-1),
first_rustc_error_line(&stderr).unwrap_or("(no output)")
);
}
let dep_content = read_dep_info_file(&dep_file)?;
let mut source_files = parse_dep_info(&dep_content);
if source_files.is_empty() {
source_files.push(source_file.to_path_buf());
}
let env_deps = parse_env_dep_info(&dep_content);
tracing::trace!(
"dep-info found {} source files, {} env deps for {}",
source_files.len(),
env_deps.len(),
source_file.display()
);
Ok(DepInfo {
source_files,
env_deps,
})
}
fn read_dep_info_file(dep_file: &Path) -> Result<String> {
let bytes = std::fs::read(dep_file).context("reading dep-info output")?;
String::from_utf8(bytes).context("dep-info output is not valid UTF-8")
}
pub(crate) fn parse_dep_info(dep_info: &str) -> Vec<std::path::PathBuf> {
let line = match dep_info.lines().next() {
Some(l) => l,
None => return vec![],
};
let pos = match line.find(": ") {
Some(p) => p,
None => return vec![],
};
let mut deps = Vec::new();
let mut current = String::new();
let mut chars = line[pos + 2..].chars().peekable();
loop {
match chars.next() {
Some('\\') if chars.peek() == Some(&' ') => {
current.push(' ');
chars.next();
}
Some('\\') => current.push('\\'),
Some(' ') => {
if !current.is_empty() {
deps.push(std::path::PathBuf::from(¤t));
current.clear();
}
}
Some(c) => current.push(c),
None => {
if !current.is_empty() {
deps.push(std::path::PathBuf::from(¤t));
}
break;
}
}
}
deps.sort();
deps
}
fn parse_env_dep_info(dep_info: &str) -> Vec<(String, String)> {
let mut env_deps = Vec::new();
for line in dep_info.lines() {
if let Some(env_dep) = line.strip_prefix("# env-dep:") {
if let Some((var, val)) = env_dep.split_once('=') {
env_deps.push((var.to_string(), unescape_env_dep_value(val)));
} else {
env_deps.push((env_dep.to_string(), String::new()));
}
}
}
env_deps.sort_by(|(a, _), (b, _)| a.cmp(b));
env_deps
}
fn unescape_env_dep_value(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('\\') => out.push('\\'),
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
pub(crate) fn rustc_version_text(rustc: &Path) -> Option<String> {
get_rustc_version(rustc).ok()
}
fn get_rustc_version(rustc: &Path) -> Result<String> {
let _trace = crate::phase_trace::phase("compiler_identity");
if let Some(cached) = read_tool_version_cache(rustc, "rustc-ver") {
return Ok(cached);
}
let output = std::process::Command::new(rustc)
.arg("--version")
.arg("--verbose")
.output()
.context("running rustc --version --verbose")?;
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
write_tool_version_cache(rustc, "rustc-ver", &version);
Ok(version)
}
fn get_clippy_version(driver: &Path) -> Result<String> {
if let Some(cached) = read_tool_version_cache(driver, "clippy-ver") {
return Ok(cached);
}
let output = std::process::Command::new(driver)
.arg("--version")
.output()
.context("running clippy-driver --version")?;
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
anyhow::ensure!(
!version.is_empty(),
"clippy-driver --version printed nothing"
);
write_tool_version_cache(driver, "clippy-ver", &version);
Ok(version)
}
pub(crate) fn clippy_identity(driver: &Path, env: &KeyEnv) -> Result<String> {
clippy_identity_in(
driver,
|name| env.var_os(name),
env.cwd().map(Path::to_path_buf),
)
}
fn clippy_identity_in(
driver: &Path,
env: impl Fn(&str) -> Option<std::ffi::OsString>,
current_dir: Option<PathBuf>,
) -> Result<String> {
let mut identity = get_clippy_version(driver)?;
identity.push('\n');
if let Some(manifest_dir) = env("CARGO_MANIFEST_DIR") {
let manifest = PathBuf::from(manifest_dir).join("Cargo.toml");
match std::fs::read(&manifest) {
Ok(content) => {
identity.push_str(&format!("manifest:{}\n", blake3::hash(&content).to_hex()))
}
Err(_) => identity.push_str("manifest:none\n"),
}
}
let start = env("CLIPPY_CONF_DIR")
.or_else(|| env("CARGO_MANIFEST_DIR"))
.map(PathBuf::from)
.or(current_dir);
match start.and_then(|start| selected_clippy_config(&start)) {
Some(path) => {
let content = std::fs::read(&path)
.with_context(|| format!("reading Clippy configuration {}", path.display()))?;
identity.push_str(&format!(
"config:{}:{}\n",
path.file_name()
.map(|n| n.to_string_lossy())
.unwrap_or_default(),
blake3::hash(&content).to_hex()
));
}
None => identity.push_str("config:none\n"),
}
for name in ["CLIPPY_ARGS", "CLIPPY_DISABLE_DOCS_LINKS"] {
match env(name) {
Some(value) => identity.push_str(&format!("{name}={}\n", value.to_string_lossy())),
None => identity.push_str(&format!("{name} unset\n")),
}
}
Ok(identity)
}
fn selected_clippy_config(start: &Path) -> Option<PathBuf> {
let mut directory = std::fs::canonicalize(start).ok()?;
loop {
for name in [".clippy.toml", "clippy.toml"] {
let candidate = directory.join(name);
if candidate.is_file() {
return Some(candidate);
}
}
if !directory.pop() {
return None;
}
}
}
pub(crate) fn get_rustc_commit_hash(rustc: &Path) -> Option<String> {
let vv = get_rustc_version(rustc).ok()?;
vv.lines()
.find_map(|l| l.strip_prefix("commit-hash:"))
.map(|h| h.trim().to_string())
.filter(|h| !h.is_empty() && h != "unknown")
}
pub(crate) fn get_rustc_sysroot(args: &RustcArgs) -> Option<PathBuf> {
if let Some(sysroot) = &args.sysroot {
return Some(sysroot.clone());
}
default_rustc_sysroot(&args.rustc)
}
fn default_rustc_sysroot(rustc: &Path) -> Option<PathBuf> {
if let Some(cached) = read_tool_version_cache(rustc, "rustc-sysroot") {
return Some(PathBuf::from(cached));
}
let output = std::process::Command::new(rustc)
.arg("--print")
.arg("sysroot")
.output()
.ok()?;
if !output.status.success() {
return None;
}
let sysroot = String::from_utf8_lossy(&output.stdout).trim().to_string();
if sysroot.is_empty() {
return None;
}
write_tool_version_cache(rustc, "rustc-sysroot", &sysroot);
Some(PathBuf::from(sysroot))
}
fn rustc_tool_dirs(args: &RustcArgs, host: &str) -> Result<Vec<PathBuf>> {
let rustc = args.inner_rustc.as_deref().unwrap_or(&args.rustc);
let sysroot = default_rustc_sysroot(rustc).context("rustc --print sysroot failed")?;
Ok(args
.sysroot
.iter()
.cloned()
.chain([sysroot])
.map(|sysroot| crate::native_link_key::rustc_tool_dir(&sysroot, host))
.collect())
}
fn toolchain_codegen_backends(
args: &RustcArgs,
rustc_version: &str,
name: &str,
file_hasher: &FileHasher,
) -> Result<Vec<(String, String)>> {
let host = rustc_host_triple(rustc_version).context("rustc -vV reports no host")?;
let sysroots = args
.sysroot
.iter()
.cloned()
.chain(default_rustc_sysroot(&args.rustc));
let found = codegen_backend_libraries(sysroots, host, name, file_hasher)?;
anyhow::ensure!(
!found.is_empty(),
"codegen backend {name} is not installed in the toolchain"
);
Ok(found)
}
fn codegen_backend_libraries(
sysroots: impl IntoIterator<Item = PathBuf>,
host: &str,
name: &str,
file_hasher: &FileHasher,
) -> Result<Vec<(String, String)>> {
let stem = format!("rustc_codegen_{name}");
let mut found = Vec::new();
for sysroot in sysroots {
let directory = sysroot
.join("lib")
.join("rustlib")
.join(host)
.join("codegen-backends");
let entries = match std::fs::read_dir(&directory) {
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
entries => entries.with_context(|| format!("listing {}", directory.display()))?,
};
let mut libraries = BTreeMap::new();
for entry in entries {
let path = entry?.path();
if let Some(library) = path
.file_name()
.and_then(|file| file.to_str())
.filter(|file| is_codegen_backend_library(file, &stem))
{
libraries.insert(library.to_string(), path.clone());
}
}
for (library, path) in libraries {
let content = file_hasher
.hash(&path)
.with_context(|| format!("hashing codegen backend {}", path.display()))?;
found.push((library, content));
}
}
Ok(found)
}
fn is_codegen_backend_library(file: &str, stem: &str) -> bool {
file.strip_prefix(std::env::consts::DLL_PREFIX)
.and_then(|file| file.strip_suffix(std::env::consts::DLL_SUFFIX))
.and_then(|file| file.strip_prefix(stem))
.is_some_and(|release| release.is_empty() || release.starts_with('-'))
}
fn read_tool_version_cache(binary: &Path, prefix: &str) -> Option<String> {
let cache_file = tool_version_cache_path(binary, prefix)?;
std::fs::read_to_string(cache_file)
.ok()
.filter(|s| !s.is_empty())
}
fn write_tool_version_cache(binary: &Path, prefix: &str, version: &str) {
if let Some(cache_file) = tool_version_cache_path(binary, prefix) {
crate::probe_memo::write_atomic(&cache_file, version);
}
}
fn tool_version_cache_path(binary: &Path, prefix: &str) -> Option<std::path::PathBuf> {
tool_version_cache_path_in(binary, prefix, std::env::var_os("PATH").as_deref())
}
fn tool_version_cache_path_in(
binary: &Path,
prefix: &str,
path_var: Option<&OsStr>,
) -> Option<std::path::PathBuf> {
let canon = std::fs::canonicalize(program_path(binary, path_var)?).ok()?;
let mtime = std::fs::metadata(&canon)
.ok()?
.modified()
.ok()?
.duration_since(std::time::UNIX_EPOCH)
.ok()?
.as_secs();
let rustup_home = rustup_home();
let key = format!(
"{}:{}:{}:{}",
canon.display(),
mtime,
toolchain_selector_fingerprint(
std::env::var_os("RUSTUP_TOOLCHAIN").as_deref(),
std::env::current_dir().ok().as_deref(),
rustup_home
.as_ref()
.map(|home| home.join("settings.toml"))
.as_deref(),
),
installed_toolchains_fingerprint(
rustup_home
.as_ref()
.map(|home| home.join("toolchains"))
.as_deref()
)
);
let hash = blake3::hash(key.as_bytes()).to_hex();
Some(crate::config::default_cache_dir().join(format!("{}-{}.txt", prefix, &hash[..16])))
}
fn toolchain_selector_fingerprint(
rustup_toolchain: Option<&std::ffi::OsStr>,
cwd: Option<&Path>,
rustup_settings: Option<&Path>,
) -> String {
let mut fp = String::new();
if let Some(toolchain) = rustup_toolchain {
fp.push_str("env:");
fp.push_str(&toolchain.to_string_lossy());
}
if let Some(cwd) = cwd {
'search: for dir in cwd.ancestors() {
let mut found = false;
for name in ["rust-toolchain", "rust-toolchain.toml"] {
let candidate = dir.join(name);
if let Some(digest) = file_digest(&candidate) {
fp.push_str(";file:");
fp.push_str(&candidate.to_string_lossy());
fp.push(':');
fp.push_str(&digest);
found = true;
}
}
if found {
break 'search;
}
}
}
if let Some(settings) = rustup_settings
&& let Some(digest) = file_digest(settings)
{
fp.push_str(";default:");
fp.push_str(&digest);
}
fp
}
fn installed_toolchains_fingerprint(toolchains: Option<&Path>) -> String {
let Some(entries) = toolchains.and_then(|dir| std::fs::read_dir(dir).ok()) else {
return String::new();
};
let rustc = format!("rustc{}", std::env::consts::EXE_SUFFIX);
let mut stamps: Vec<String> = entries
.filter_map(|entry| entry.ok())
.map(|entry| {
let name = entry.file_name().to_string_lossy().into_owned();
match std::fs::metadata(entry.path().join("bin").join(&rustc)) {
Ok(meta) => {
let mtime = meta
.modified()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map_or(0, |since| since.as_nanos());
format!("{name}={mtime}:{}", meta.len())
}
Err(_) => format!("{name}=none"),
}
})
.collect();
stamps.sort();
stamps.join(";")
}
fn rustup_home() -> Option<std::path::PathBuf> {
std::env::var_os("RUSTUP_HOME")
.map(std::path::PathBuf::from)
.or_else(|| dirs::home_dir().map(|home| home.join(".rustup")))
}
fn file_digest(path: &Path) -> Option<String> {
let bytes = std::fs::read(path).ok()?;
Some(blake3::hash(&bytes).to_hex()[..16].to_string())
}
fn host_target_triple() -> &'static str {
option_env!("TARGET").unwrap_or("unknown")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinuxLibcFamily {
Gnu,
Musl,
}
impl LinuxLibcFamily {
fn key_name(self) -> &'static str {
match self {
Self::Gnu => "gnu-libc",
Self::Musl => "musl",
}
}
}
pub(crate) fn rustc_host_triple(rustc_version: &str) -> Option<&str> {
rustc_version.lines().find_map(|line| {
line.strip_prefix("host:")
.map(str::trim)
.filter(|host| !host.is_empty())
})
}
fn linux_libc_family(target: &str) -> Option<LinuxLibcFamily> {
let mut components = target.split('-');
if !components.clone().any(|component| component == "linux") {
return None;
}
components.find_map(|component| {
if component.starts_with("gnu") {
Some(LinuxLibcFamily::Gnu)
} else if component.starts_with("musl") {
Some(LinuxLibcFamily::Musl)
} else {
None
}
})
}
fn native_linux_libc_family(
args: &RustcArgs,
rustc_version: &str,
running_on_linux: bool,
) -> Result<Option<LinuxLibcFamily>> {
let emits_link = args.emit.is_empty() || args.emit.iter().any(|kind| kind == "link");
if !running_on_linux || !args.is_executable_output() || !emits_link {
return Ok(None);
}
let host = rustc_host_triple(rustc_version)
.context("wrapped rustc -vV output has no host triple; cannot key native Linux libc")?;
let effective_target = args.target.as_deref().unwrap_or(host);
if effective_target != host {
return Ok(None);
}
if !host.split('-').any(|component| component == "linux") {
return Ok(None);
}
linux_libc_family(host)
.map(Some)
.with_context(|| format!("unsupported native Linux libc in rustc host triple {host}"))
}
fn rustc_version_for_native_link<'a, F>(
args: &RustcArgs,
outer_rustc_version: &'a str,
load_version: F,
) -> Result<Cow<'a, str>>
where
F: FnOnce(&Path) -> Result<String>,
{
match args.inner_rustc.as_deref() {
Some(inner) => load_version(inner)
.map(Cow::Owned)
.context("reading inner rustc version for native host link key"),
None => Ok(Cow::Borrowed(outer_rustc_version)),
}
}
fn fold_native_host_libc_signature<H: KeyFold, F>(
hasher: &mut H,
args: &RustcArgs,
rustc_version: &str,
running_on_linux: bool,
probe: F,
) -> Result<()>
where
F: FnOnce(LinuxLibcFamily) -> Result<String>,
{
let emits_link = args.emit.is_empty() || args.emit.iter().any(|kind| kind == "link");
if !running_on_linux || !args.is_executable_output() || !emits_link {
return Ok(());
}
let rustc_version = rustc_version_for_native_link(args, rustc_version, get_rustc_version)?;
let Some(family) = native_linux_libc_family(args, &rustc_version, running_on_linux)? else {
return Ok(());
};
let signature = probe(family).with_context(|| {
format!(
"determining native Linux {} signature for cache key",
family.key_name()
)
})?;
fold_field(
hasher,
b"host_libc.v1:",
format!("{}:{signature}", family.key_name()).as_bytes(),
);
tracing::trace!(
"[key:{}] host_libc={}:{signature}",
args.crate_name.as_deref().unwrap_or("unknown"),
family.key_name()
);
Ok(())
}
fn fold_native_link_runtime_identity<H, Crt, Sdk>(
hasher: &mut H,
args: &RustcArgs,
rustc_version: &str,
running_on_linux: bool,
running_on_macos: bool,
crt_probe: Crt,
sdk_probe: Sdk,
env: &KeyEnv,
) -> Result<()>
where
H: KeyFold,
Crt: FnOnce(&Path) -> Result<BTreeMap<String, String>>,
Sdk: FnOnce(Option<String>) -> Result<String>,
{
let emits_link = args.emit.is_empty() || args.emit.iter().any(|kind| kind == "link");
if !args.is_executable_output() || !emits_link {
return Ok(());
}
if !running_on_linux && !running_on_macos {
return Ok(());
}
let rustc_version = rustc_version_for_native_link(args, rustc_version, get_rustc_version)?;
let host = rustc_host_triple(&rustc_version)
.context("wrapped rustc -vV output has no host triple; cannot key native link runtime")?;
let effective_target = args.target.as_deref().unwrap_or(host);
if effective_target != host {
return Ok(());
}
if running_on_linux && host.split('-').any(|component| component == "linux") {
let driver = resolve_link_driver(args)
.context("native Linux link has no cc/linker driver; cannot key CRT objects")?;
let objects = crt_probe(&driver).context("determining native Linux CRT/libc identity")?;
let encoded = crate::native_link_key::encode_crt_objects(&objects);
fold_field(hasher, b"host_crt.v1:", encoded.as_bytes());
tracing::trace!(
"[key:{}] host_crt={}",
args.crate_name.as_deref().unwrap_or("unknown"),
encoded.replace('\n', ",")
);
}
if running_on_macos && host.split('-').any(|component| component == "darwin") {
let deployment_target = env.var("MACOSX_DEPLOYMENT_TARGET");
let identity = sdk_probe(env.var("SDKROOT"))
.context("determining macOS SDK identity for cache key")?;
fold_field(hasher, b"host_sdk.v1:", identity.as_bytes());
tracing::trace!(
"[key:{}] host_sdk={}",
args.crate_name.as_deref().unwrap_or("unknown"),
identity
);
if let Some(target) = deployment_target.filter(|value| !value.is_empty()) {
fold_field(hasher, b"host_deployment_target.v1:", target.as_bytes());
tracing::trace!(
"[key:{}] host_deployment_target={}",
args.crate_name.as_deref().unwrap_or("unknown"),
target
);
}
}
Ok(())
}
#[derive(Debug, PartialEq, Eq)]
struct WindowsNativeLinkSearchDirs {
rustc: Vec<PathBuf>,
linker: Vec<PathBuf>,
}
fn windows_native_link_search_dirs(args: &RustcArgs) -> Result<WindowsNativeLinkSearchDirs> {
const KNOWN_L_KINDS: [&str; 5] = ["dependency", "crate", "native", "framework", "all"];
let mut rustc = Vec::new();
for spec in &args.link_search {
let (kind, path) = match spec.split_once('=') {
Some((kind, path)) if KNOWN_L_KINDS.contains(&kind) => (Some(kind), path),
_ => (None, spec.as_str()),
};
if matches!(kind, Some("dependency") | Some("crate")) {
continue;
}
if matches!(kind, None | Some("native") | Some("all")) {
rustc.push(PathBuf::from(path));
}
}
let mut linker = rustc.clone();
for (key, value) in &args.codegen_opts {
if !matches!(key.as_str(), "link-arg" | "link-args") {
continue;
}
let Some(value) = value.as_deref() else {
continue;
};
if crate::native_link_key::windows_link_argument_has_unmodeled_input(value) {
anyhow::bail!(
"explicit Windows linker input files (.lib/.a/.obj/.o/.res/.def/.exp/.manifest) \
and file-carrying LINK options (/DEF, /DEFAULTLIB, /MANIFESTINPUT, \
/MANIFESTFILE, /PDBSTRIPPED, ...) are not hashed and are not cacheable"
);
}
if let Some(path) = windows_libpath_argument(value)? {
linker.push(PathBuf::from(path));
}
}
Ok(WindowsNativeLinkSearchDirs { rustc, linker })
}
fn windows_libpath_argument(value: &str) -> Result<Option<String>> {
let value = value.trim();
let value = value.strip_prefix("-Wl,").unwrap_or(value);
let upper = value.to_ascii_uppercase();
let marker = ["/LIBPATH:", "/LIBPATH=", "-LIBPATH:", "-LIBPATH="]
.into_iter()
.find(|marker| upper.starts_with(*marker));
let Some(marker) = marker else {
if upper.contains("/LIBPATH") || upper.contains("-LIBPATH") {
anyhow::bail!("ambiguous Windows /LIBPATH linker argument");
}
return Ok(None);
};
let path = value[marker.len()..].trim();
let path = if let Some(quoted) = path.strip_prefix('"') {
let closing = quoted
.find('"')
.context("unterminated quoted Windows /LIBPATH linker argument")?;
if !quoted[closing + 1..].trim().is_empty() {
anyhow::bail!("ambiguous Windows /LIBPATH linker argument");
}
quoted[..closing].trim()
} else {
if path.contains('"') || path.chars().any(char::is_whitespace) {
anyhow::bail!("ambiguous Windows /LIBPATH linker argument");
}
path
};
if path.is_empty() {
anyhow::bail!("empty Windows /LIBPATH linker argument");
}
Ok(Some(path.to_string()))
}
fn fold_generic_linker_identity<H, Get>(
hasher: &mut H,
args: &RustcArgs,
native_windows_msvc: bool,
get_identity: Get,
) where
H: KeyFold,
Get: FnOnce(&RustcArgs) -> Option<String>,
{
if args.is_executable_output()
&& args.emits_link()
&& !native_windows_msvc
&& let Some(linker_id) = get_identity(args)
{
hasher.update(b"linker:");
hasher.update(linker_id.as_bytes());
hasher.update(b"\n");
}
}
fn is_native_windows_msvc_link<Load>(
args: &RustcArgs,
rustc_version: &str,
running_on_windows: bool,
load_version: Load,
) -> Result<bool>
where
Load: FnOnce(&Path) -> Result<String>,
{
if !running_on_windows || !args.is_executable_output() || !args.emits_link() {
return Ok(false);
}
let rustc_version = rustc_version_for_native_link(args, rustc_version, load_version)?;
let host = rustc_host_triple(&rustc_version)
.context("wrapped rustc -vV output has no host triple; cannot key native Windows link")?;
let effective_target = args.target.as_deref().unwrap_or(host);
Ok(effective_target == host && crate::native_link_key::is_windows_msvc_target(host))
}
fn fold_native_windows_msvc_identity<H, Probe>(
hasher: &mut H,
args: &RustcArgs,
rustc_version: &str,
running_on_windows: bool,
probe: Probe,
) -> Result<()>
where
H: KeyFold,
Probe: FnOnce(Option<&Path>, &str, &str) -> Result<String>,
{
if !running_on_windows || !args.is_executable_output() || !args.emits_link() {
return Ok(());
}
let rustc_version = rustc_version_for_native_link(args, rustc_version, get_rustc_version)?;
let host = rustc_host_triple(&rustc_version)
.context("wrapped rustc -vV output has no host triple; cannot key native Windows link")?;
let effective_target = args.target.as_deref().unwrap_or(host);
if effective_target != host {
if crate::native_link_key::is_windows_msvc_target(effective_target) {
anyhow::bail!(
"cross-target Windows MSVC link identity is not modeled; passing through"
);
}
return Ok(());
}
if !crate::native_link_key::is_windows_msvc_target(host) {
return Ok(());
}
let architecture = crate::native_link_key::windows_msvc_architecture(host)
.context("native Windows MSVC host has an unsupported architecture")?;
let linker = args.get_codegen_opt("linker").map(Path::new);
if let Some(linker) = linker
&& crate::native_link_key::is_rust_lld(linker)
{
let as_lld_link = match args.get_codegen_opt("linker-flavor") {
Some(flavor) => matches!(flavor, "lld-link" | "msvc-lld"),
None => linker.file_stem().is_some_and(|stem| stem == "rust-lld"),
};
if !as_lld_link || args.get_codegen_opt("linker-features").is_some() {
anyhow::bail!(
"rustc may not run {} as lld-link; passing through",
linker.display()
);
}
}
let identity = probe(linker, architecture, host)
.context("determining native Windows MSVC link identity")?;
fold_field(&mut *hasher, b"host_windows_msvc.v1:", identity.as_bytes());
tracing::trace!(
"[key:{}] host_windows_msvc={}",
args.crate_name.as_deref().unwrap_or("unknown"),
identity.replace('\n', ",")
);
Ok(())
}
fn resolve_link_driver(args: &RustcArgs) -> Option<PathBuf> {
let linker = args.get_codegen_opt("linker").unwrap_or("cc");
let linker_path = Path::new(linker);
if linker_path.is_absolute() {
Some(linker_path.to_path_buf())
} else {
resolve_in_path(linker)
}
}
fn is_libc_version(version: &str) -> bool {
let mut parts = version.split('.');
let Some(major) = parts.next() else {
return false;
};
let Some(minor) = parts.next() else {
return false;
};
!major.is_empty()
&& !minor.is_empty()
&& major.bytes().all(|b| b.is_ascii_digit())
&& minor.bytes().all(|b| b.is_ascii_digit())
&& parts.all(|part| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit()))
}
fn parse_getconf_gnu_libc(stdout: &str) -> Option<String> {
let mut fields = stdout.split_whitespace();
let family = fields.next()?;
let version = fields.next()?;
if family == "glibc" && is_libc_version(version) && fields.next().is_none() {
Some(version.to_string())
} else {
None
}
}
fn parse_ldd_libc(text: &str) -> Option<(LinuxLibcFamily, String)> {
let lower = text.to_ascii_lowercase();
if lower.contains("musl") {
let version = text.lines().find_map(|line| {
let mut fields = line.split_whitespace();
if !fields.next()?.eq_ignore_ascii_case("version") {
return None;
}
let version = fields.next()?;
is_libc_version(version).then(|| version.to_string())
})?;
return Some((LinuxLibcFamily::Musl, version));
}
if lower.contains("glibc") || lower.contains("gnu libc") || lower.contains("gnu c library") {
let first_line = text.lines().find(|line| !line.trim().is_empty())?;
let version = first_line
.split_whitespace()
.rev()
.find(|field| is_libc_version(field))?;
return Some((LinuxLibcFamily::Gnu, version.to_string()));
}
None
}
fn probe_linux_libc_signature(expected: LinuxLibcFamily) -> Result<String> {
let _trace = crate::phase_trace::phase("native_libc_signature");
if expected == LinuxLibcFamily::Gnu
&& let Ok(output) = std::process::Command::new("getconf")
.arg("GNU_LIBC_VERSION")
.env("LC_ALL", "C")
.env("LANG", "C")
.output()
&& output.status.success()
&& let Some(version) = parse_getconf_gnu_libc(&String::from_utf8_lossy(&output.stdout))
{
return Ok(version);
}
if let Ok(output) = std::process::Command::new("ldd")
.arg("--version")
.env("LC_ALL", "C")
.env("LANG", "C")
.output()
{
let text = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
if let Some((family, version)) = parse_ldd_libc(&text)
&& family == expected
{
return Ok(version);
}
}
anyhow::bail!(
"unable to identify native Linux {} (tried getconf/ldd)",
expected.key_name()
)
}
fn get_linker_identity(args: &RustcArgs) -> Option<String> {
let _trace = crate::phase_trace::phase("linker_identity");
let linker = args.get_codegen_opt("linker").unwrap_or("cc");
let linker_path = Path::new(linker);
let resolved = if linker_path.is_absolute() {
linker_path.to_path_buf()
} else {
resolve_in_path(linker)?
};
if let Some(cached) = read_tool_version_cache(&resolved, "linker-ver") {
return Some(cached);
}
let output = std::process::Command::new(linker)
.arg("--version")
.output()
.ok()?;
let version = String::from_utf8_lossy(&output.stdout);
let first_line = version.lines().next()?.to_string();
write_tool_version_cache(&resolved, "linker-ver", &first_line);
Some(first_line)
}
fn resolve_in_path(name: &str) -> Option<std::path::PathBuf> {
resolve_in(name, &std::env::var_os("PATH")?)
}
fn resolve_in(name: &str, path_var: &OsStr) -> Option<std::path::PathBuf> {
std::env::split_paths(path_var)
.map(|dir| dir.join(name))
.find(|p| runnable_file(p))
}
fn runnable_file(path: &Path) -> bool {
let Ok(metadata) = std::fs::metadata(path) else {
return false;
};
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
metadata.is_file() && metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
metadata.is_file()
}
}
fn program_path<'a>(binary: &'a Path, path_var: Option<&OsStr>) -> Option<Cow<'a, Path>> {
let mut components = binary.components();
match (components.next(), components.next()) {
(Some(std::path::Component::Normal(name)), None) => {
bare_program_path(name, path_var).map(Cow::Owned)
}
_ => Some(Cow::Borrowed(binary)),
}
}
fn bare_program_path(name: &OsStr, path_var: Option<&OsStr>) -> Option<std::path::PathBuf> {
#[cfg(unix)]
{
resolve_in(name.to_str()?, path_var?)
}
#[cfg(not(unix))]
{
let _ = (name, path_var);
None
}
}
#[cfg(test)]
mod tests;