Skip to content

Commit 5e7caa4

Browse files
mmastracclaude
andcommitted
fix: handle directories in dep-info source file hashing
When proc_macro::tracked::path() registers a directory as a dependency, rustc's dep-info output includes the directory path. sccache previously crashed with "Is a directory" when trying to hash these paths. Now recursively hashes all files within the directory (sorted for determinism), using relative paths as delimiters. This correctly captures directory dependencies so cache invalidation works when any file in the tracked directory changes. Fixes: #2653 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d11e2e0 commit 5e7caa4

1 file changed

Lines changed: 133 additions & 4 deletions

File tree

src/util.rs

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,55 @@ impl Digest {
9999
/// the actual hash computation on a background thread in `pool`.
100100
pub async fn reader(path: PathBuf, pool: &tokio::runtime::Handle) -> Result<String> {
101101
pool.spawn_blocking(move || {
102-
let reader = File::open(&path)
103-
.with_context(|| format!("Failed to open file for hashing: {:?}", path))?;
104-
Digest::reader_sync(reader)
102+
if path.is_dir() {
103+
// For directories (e.g., from proc_macro::tracked::path()),
104+
// recursively hash all file contents in sorted order.
105+
let mut entries = Vec::new();
106+
Self::collect_dir_entries(&path, &mut entries)?;
107+
entries.sort();
108+
let mut digest = Digest::new();
109+
for entry in &entries {
110+
// Hash the relative path as a delimiter
111+
let rel = entry.strip_prefix(&path).unwrap_or(entry);
112+
digest.update(rel.to_string_lossy().as_bytes());
113+
digest.update(b"\0");
114+
let mut file = File::open(entry)
115+
.with_context(|| format!("Failed to open file for hashing: {:?}", entry))?;
116+
let mut buf = vec![0u8; HASH_BUFFER_SIZE];
117+
loop {
118+
let n = file.read(&mut buf)?;
119+
if n == 0 {
120+
break;
121+
}
122+
digest.update(&buf[..n]);
123+
}
124+
}
125+
Ok(digest.finish())
126+
} else {
127+
let reader = File::open(&path)
128+
.with_context(|| format!("Failed to open file for hashing: {:?}", path))?;
129+
Digest::reader_sync(reader)
130+
}
105131
})
106132
.await?
107133
}
108134

135+
/// Recursively collect all file paths within a directory.
136+
fn collect_dir_entries(dir: &Path, entries: &mut Vec<PathBuf>) -> Result<()> {
137+
for entry in std::fs::read_dir(dir)
138+
.with_context(|| format!("Failed to read directory for hashing: {:?}", dir))?
139+
{
140+
let entry = entry?;
141+
let path = entry.path();
142+
if path.is_dir() {
143+
Self::collect_dir_entries(&path, entries)?;
144+
} else {
145+
entries.push(path);
146+
}
147+
}
148+
Ok(())
149+
}
150+
109151
pub fn update(&mut self, bytes: &[u8]) {
110152
self.inner.update(bytes);
111153
}
@@ -1248,7 +1290,7 @@ pub fn resolve_compiler_avoiding_ccache(
12481290

12491291
#[cfg(test)]
12501292
mod tests {
1251-
use super::{OsStrExt, TimeMacroFinder, resolve_compiler_avoiding_ccache};
1293+
use super::{Digest, OsStrExt, TimeMacroFinder, resolve_compiler_avoiding_ccache};
12521294
use std::ffi::{OsStr, OsString};
12531295
use std::path::Path;
12541296

@@ -1705,4 +1747,91 @@ mod tests {
17051747
let normalized = super::normalize_win_path(input);
17061748
assert_eq!(normalized, b"");
17071749
}
1750+
1751+
#[tokio::test]
1752+
async fn test_digest_reader_hashes_file() {
1753+
let temp = tempfile::tempdir().unwrap();
1754+
let path = temp.path().join("hello.txt");
1755+
std::fs::write(&path, b"hello, world").unwrap();
1756+
let pool = tokio::runtime::Handle::current();
1757+
let hash = Digest::reader(path, &pool).await.unwrap();
1758+
assert_eq!(hash.len(), 64, "blake3 hex digest is 64 chars");
1759+
}
1760+
1761+
// Regression for https://github.com/mozilla/sccache/issues/2653: rustc's
1762+
// dep-info may list a directory path (e.g. from a proc macro that calls
1763+
// `proc_macro::tracked_path::path()` on a directory). The four tests below
1764+
// exercise Digest::reader against directories.
1765+
1766+
#[tokio::test]
1767+
async fn test_digest_reader_hashes_directory() {
1768+
let temp = tempfile::tempdir().unwrap();
1769+
let dir = temp.path();
1770+
std::fs::create_dir_all(dir.join("nested/deeper")).unwrap();
1771+
std::fs::write(dir.join("root.txt"), b"root").unwrap();
1772+
std::fs::write(dir.join("nested/inner.txt"), b"inner").unwrap();
1773+
std::fs::write(dir.join("nested/deeper/deep.txt"), b"deep").unwrap();
1774+
1775+
let pool = tokio::runtime::Handle::current();
1776+
let hash = Digest::reader(dir.to_path_buf(), &pool).await.unwrap();
1777+
assert_eq!(hash.len(), 64);
1778+
}
1779+
1780+
#[tokio::test]
1781+
async fn test_digest_reader_directory_is_deterministic() {
1782+
fn populate(dir: &std::path::Path) {
1783+
std::fs::create_dir_all(dir.join("a/b")).unwrap();
1784+
std::fs::create_dir_all(dir.join("c")).unwrap();
1785+
std::fs::write(dir.join("a/b/one.txt"), b"one").unwrap();
1786+
std::fs::write(dir.join("a/two.txt"), b"two").unwrap();
1787+
std::fs::write(dir.join("c/three.txt"), b"three").unwrap();
1788+
std::fs::write(dir.join("four.txt"), b"four").unwrap();
1789+
}
1790+
let t1 = tempfile::tempdir().unwrap();
1791+
let t2 = tempfile::tempdir().unwrap();
1792+
populate(t1.path());
1793+
populate(t2.path());
1794+
let pool = tokio::runtime::Handle::current();
1795+
let h1 = Digest::reader(t1.path().to_path_buf(), &pool)
1796+
.await
1797+
.unwrap();
1798+
let h2 = Digest::reader(t2.path().to_path_buf(), &pool)
1799+
.await
1800+
.unwrap();
1801+
assert_eq!(h1, h2);
1802+
}
1803+
1804+
#[tokio::test]
1805+
async fn test_digest_reader_directory_detects_content_change() {
1806+
let temp = tempfile::tempdir().unwrap();
1807+
let dir = temp.path();
1808+
std::fs::create_dir_all(dir.join("sub")).unwrap();
1809+
std::fs::write(dir.join("sub/data.txt"), b"before").unwrap();
1810+
let pool = tokio::runtime::Handle::current();
1811+
let before = Digest::reader(dir.to_path_buf(), &pool).await.unwrap();
1812+
std::fs::write(dir.join("sub/data.txt"), b"after").unwrap();
1813+
let after = Digest::reader(dir.to_path_buf(), &pool).await.unwrap();
1814+
assert_ne!(before, after);
1815+
}
1816+
1817+
#[tokio::test]
1818+
async fn test_digest_reader_directory_detects_path_change() {
1819+
// Two dirs with identical file *contents* but different file *names*
1820+
// must hash to different values, because collect_dir_entries mixes
1821+
// the relative path into the digest as a delimiter.
1822+
let t1 = tempfile::tempdir().unwrap();
1823+
let t2 = tempfile::tempdir().unwrap();
1824+
std::fs::create_dir_all(t1.path().join("sub")).unwrap();
1825+
std::fs::create_dir_all(t2.path().join("sub")).unwrap();
1826+
std::fs::write(t1.path().join("sub/a.txt"), b"x").unwrap();
1827+
std::fs::write(t2.path().join("sub/b.txt"), b"x").unwrap();
1828+
let pool = tokio::runtime::Handle::current();
1829+
let h1 = Digest::reader(t1.path().to_path_buf(), &pool)
1830+
.await
1831+
.unwrap();
1832+
let h2 = Digest::reader(t2.path().to_path_buf(), &pool)
1833+
.await
1834+
.unwrap();
1835+
assert_ne!(h1, h2);
1836+
}
17081837
}

0 commit comments

Comments
 (0)