Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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"
        );
    }
}