src/deepscan.rs
//! The workspace's **dependency closure**, read in-process from each member's
//! `Cargo.lock` (no `cargo` subprocess), and its deterministic generation id.
//!
//! The deep-scan that parsed every dependency's sources with `syn` into
//! `symbol_facts` under repo `deps` was removed with Mímir v2 (PLAN-MIMIR-V2
//! §7.3b): the universe ring is the rustc-driver dump, once per lockfile (lane 3).
//! What stays is the non-parser half other code reads: the closure resolution,
//! its generation hash, and the `deps` repo name the old rows were filed under.
use std::collections::BTreeMap;
use std::path::PathBuf;
use anyhow::Result;
/// Repo name the dependency-closure rows are filed under.
pub const DEPS_REPO: &str = "deps";
/// Map an extracted crate source dir to its `.crate` gzip tarball:
/// `…/registry/src/<reg>/<name-ver>/` → `…/registry/cache/<reg>/<name-ver>.crate`.
/// `None` when `dir` isn't a registry `src` dir (path/git deps) → caller falls
/// back to scanning the on-disk tree.
fn crate_tarball_path(dir: &str) -> Option<PathBuf> {
let dir = dir.trim_end_matches('/');
let cache = dir.replacen("/registry/src/", "/registry/cache/", 1);
if cache == dir {
return None;
}
Some(PathBuf::from(format!("{cache}.crate")))
}
/// `<CARGO_HOME>`, falling back to `~/.cargo`.
fn cargo_home() -> PathBuf {
if let Some(h) = std::env::var_os("CARGO_HOME") {
return PathBuf::from(h);
}
if let Some(h) = std::env::var_os("HOME") {
return PathBuf::from(h).join(".cargo");
}
PathBuf::from(".cargo")
}
/// Registry index dir name(s) under `<cargo_home>/registry/{cache,src}` (usually
/// a single `index.crates.io-<hash>`; we scan all so a mirror/extra index still
/// resolves).
fn registry_index_dirs(cargo_home: &std::path::Path) -> Vec<String> {
let mut names = BTreeMap::new();
for sub in ["cache", "src"] {
if let Ok(rd) = std::fs::read_dir(cargo_home.join("registry").join(sub)) {
for e in rd.flatten() {
if e.path().is_dir() {
if let Some(n) = e.file_name().to_str() {
names.insert(n.to_string(), ());
}
}
}
}
}
names.into_keys().collect()
}
/// The `Cargo.lock` governing `dir`: the member's own lock, else the nearest
/// ancestor's (a virtual-workspace root lock).
fn find_cargo_lock(dir: &std::path::Path) -> Option<PathBuf> {
let mut cur = Some(dir);
while let Some(d) = cur {
let lock = d.join("Cargo.lock");
if lock.is_file() {
return Some(lock);
}
cur = d.parent();
}
None
}
/// Resolve the full transitive dependency closure of `member_dirs` **in-process,
/// NO shelling** — parse each member's committed `Cargo.lock` (the exact resolved
/// version set cargo already wrote) instead of spawning `cargo fetch` + `cargo
/// metadata`. Returns deduped `(name-version, registry-src-dir)` pairs for
/// external **registry** deps — every `[[package]]` that carries a `source` —
/// and EXCLUDES the workspace/path members (a `[[package]]` with no `source`).
///
/// The src dir is constructed at `<CARGO_HOME>/registry/src/<index>/<name-ver>`;
/// its `.crate` tarball sits beside it ([`crate_tarball_path`]). No `cargo
/// fetch`: a crate whose `.crate` was never downloaded simply isn't on disk. Git deps
/// (`source = "git+…"`, rare) live under `registry/git` not `registry/src`, so
/// they don't resolve here and are likewise skipped.
pub fn resolve_dep_closure(member_dirs: &[PathBuf]) -> Result<Vec<(String, PathBuf)>> {
let cargo_home = cargo_home();
let index_dirs = registry_index_dirs(&cargo_home);
let src_root = cargo_home.join("registry").join("src");
let mut out: BTreeMap<String, PathBuf> = BTreeMap::new();
for dir in member_dirs {
let Some(lock_path) = find_cargo_lock(dir) else {
continue;
};
let Ok(text) = std::fs::read_to_string(&lock_path) else {
continue;
};
let doc: toml::Value = match toml::from_str(&text) {
Ok(d) => d,
Err(_) => continue,
};
let Some(pkgs) = doc.get("package").and_then(|p| p.as_array()) else {
continue;
};
for pkg in pkgs {
// No `source` ⇒ a path/workspace member — skip (matches the old
// `workspace_members` exclusion). Only registry deps carry a source.
if pkg.get("source").and_then(|s| s.as_str()).is_none() {
continue;
}
let (Some(name), Some(version)) = (
pkg.get("name").and_then(|v| v.as_str()),
pkg.get("version").and_then(|v| v.as_str()),
) else {
continue;
};
let label = format!("{name}-{version}");
if out.contains_key(&label) {
continue; // first-wins, like the old `.or_insert`
}
let dirname = format!("{name}-{version}");
// Prefer the index where the source/`.crate` actually lives; fall
// back to the first index so the path is at least well-formed (the
// caller sees nothing on disk there).
let chosen = index_dirs.iter().find_map(|idx| {
let cand = src_root.join(idx).join(&dirname);
let has_src = cand.is_dir();
let has_crate = crate_tarball_path(&cand.display().to_string())
.map(|p| p.is_file())
.unwrap_or(false);
(has_src || has_crate).then_some(cand)
});
let path = chosen.unwrap_or_else(|| {
let idx = index_dirs
.first()
.map(String::as_str)
.unwrap_or("index.crates.io");
src_root.join(idx).join(&dirname)
});
out.insert(label, path);
}
}
Ok(out.into_iter().collect())
}
/// FNV-1a 64 offset basis.
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
/// FNV-1a 64 over `bytes`, continuing from `seed`.
fn fnv1a(seed: u64, bytes: &[u8]) -> u64 {
let mut h = seed;
for b in bytes {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
/// **What the dependency CLOSURE is**, as a deterministic string.
///
/// Hashes the resolved `name-version` LABELS and nothing else — not the source
/// dirs (derived from `CARGO_HOME`), not the member order, not a clock. So the
/// same `Cargo.lock` set gives the same generation on any host and at any time,
/// and a single crate moving one patch version moves it.
///
/// A re-scan of an unchanged closure must be a clean no-op. Deep-scan could not honour
/// that contract while it stamped a fresh `Uuid::new_v4()` on every sweep —
/// every run committed new `symbol_facts` rows, which moved the lens generation,
/// which refolded all eleven lenses over a dependency graph that had not changed.
pub fn deps_generation(closure: &[(String, PathBuf)]) -> String {
// `resolve_dep_closure` returns a `BTreeMap`'s order, so this is already
// sorted; sort anyway so no future caller can move the generation by
// reordering its input.
let mut labels: Vec<&str> = closure.iter().map(|(l, _)| l.as_str()).collect();
labels.sort_unstable();
labels.dedup();
let mut h = FNV_OFFSET;
for l in &labels {
h = fnv1a(h, l.as_bytes());
h = fnv1a(h, b"\n");
}
format!("d-{h:016x}")
}
/// The `snapshot_id` a closure generation is written under — deterministic, so
/// re-scanning an unchanged closure targets the row set that is already there
/// (and is therefore SKIPPED rather than duplicated).
pub fn deps_snapshot_id(generation: &str) -> uuid::Uuid {
let hi = fnv1a(FNV_OFFSET, generation.as_bytes());
let lo = fnv1a(hi ^ 0x9e37_79b9_7f4a_7c15, generation.as_bytes());
let mut b = [0u8; 16];
b[..8].copy_from_slice(&hi.to_be_bytes());
b[8..].copy_from_slice(&lo.to_be_bytes());
b[6] = (b[6] & 0x0f) | 0x80; // RFC 4122 version 8 — a name-derived custom id
b[8] = (b[8] & 0x3f) | 0x80; // RFC 4122 variant
uuid::Uuid::from_bytes(b)
}
#[cfg(test)]
mod tests {
use super::*;
/// **Half (a) and half (b) of the deterministic deps generation, together.**
/// A test that showed only (a) would pass for a constant.
#[test]
fn deps_generation_is_stable_for_an_unchanged_closure_and_moves_on_one_version() {
let p = |s: &str| (s.to_string(), PathBuf::from(format!("/reg/{s}")));
let a = vec![p("serde-1.0.219"), p("syn-2.0.90"), p("uuid-1.11.0")];
let same = vec![p("serde-1.0.219"), p("syn-2.0.90"), p("uuid-1.11.0")];
// (a) unchanged closure ⇒ identical hash ⇒ identical snapshot id.
assert_eq!(deps_generation(&a), deps_generation(&same));
assert_eq!(
deps_snapshot_id(&deps_generation(&a)),
deps_snapshot_id(&deps_generation(&same)),
);
// (b) ONE crate's version bumped by a patch ⇒ the hash MOVES.
let bumped = vec![p("serde-1.0.219"), p("syn-2.0.91"), p("uuid-1.11.0")];
assert_ne!(
deps_generation(&a),
deps_generation(&bumped),
"one changed version must move the generation"
);
assert_ne!(
deps_snapshot_id(&deps_generation(&a)),
deps_snapshot_id(&deps_generation(&bumped)),
);
// A crate ADDED moves it too, and a crate REMOVED moves it back.
let mut plus = a.clone();
plus.push(p("anyhow-1.0.95"));
assert_ne!(deps_generation(&a), deps_generation(&plus));
plus.pop();
assert_eq!(deps_generation(&a), deps_generation(&plus));
// The generation is a fact about the LOCK, not about this host: the same
// labels at different `CARGO_HOME` paths are the same closure.
let elsewhere: Vec<(String, PathBuf)> = a
.iter()
.map(|(l, _)| (l.clone(), PathBuf::from(format!("/other/home/{l}"))))
.collect();
assert_eq!(
deps_generation(&a),
deps_generation(&elsewhere),
"the src dir is derived, not part of the closure's identity"
);
// Order must not matter — the caller could sort differently tomorrow.
let mut shuffled = a.clone();
shuffled.reverse();
assert_eq!(deps_generation(&a), deps_generation(&shuffled));
// The id must be a well-formed RFC 4122 value, since it is written into a
// `snapshot_id` column beside `Uuid::new_v4()`s from other producers.
let id = deps_snapshot_id(&deps_generation(&a));
assert_eq!(id.get_variant(), uuid::Variant::RFC4122, "{id}");
}
/// LAW (inject-assert): `resolve_dep_closure` reads `Cargo.lock` IN-PROCESS
/// (no `cargo` subprocess) — every `[[package]]` with a `source` is an
/// external registry dep (included, mapped to its `<CARGO_HOME>/registry/src`
/// dir); a package WITHOUT a `source` is a path/workspace member (excluded).
#[test]
fn resolve_dep_closure_reads_cargo_lock_in_process_excludes_path_members() {
// Mutates the GLOBAL `CARGO_HOME`, which corrupts any concurrent test that
// spawns `cargo metadata` (e.g. release::publish::derive_publish_order) →
// serialize against the shared lock.
let _g = crate::serial_guard();
let tmp = tempfile::tempdir().unwrap();
let home = tmp.path().join("cargo_home");
let idx = "index.crates.io-1234567890abcdef";
let serde_src = home
.join("registry")
.join("src")
.join(idx)
.join("serde-1.0.0");
std::fs::create_dir_all(&serde_src).unwrap();
let member = tmp.path().join("member");
std::fs::create_dir_all(&member).unwrap();
std::fs::write(
member.join("Cargo.lock"),
"[[package]]\nname = \"member\"\nversion = \"0.1.0\"\n\n\
[[package]]\nname = \"serde\"\nversion = \"1.0.0\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n",
)
.unwrap();
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::set_var("CARGO_HOME", &home) };
let closure = resolve_dep_closure(&[member.clone()]).unwrap();
// TODO: Audit that the environment access only happens in single-threaded code.
unsafe { std::env::remove_var("CARGO_HOME") };
let labels: Vec<&str> = closure.iter().map(|(l, _)| l.as_str()).collect();
assert!(
labels.contains(&"serde-1.0.0"),
"registry dep included: {labels:?}"
);
assert!(
!labels.iter().any(|l| l.starts_with("member")),
"path/workspace member (no `source`) excluded: {labels:?}"
);
// The registry dep resolves to its extracted src dir under CARGO_HOME.
let (_, dir) = closure.iter().find(|(l, _)| l == "serde-1.0.0").unwrap();
assert_eq!(
dir, &serde_src,
"registry dep maps to its CARGO_HOME src dir"
);
}
}