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

nordisk/znippy

znippy-build-thing/tests/siblings.rs

//! **Every sibling checkout a build of znippy reaches is declared in
//! `[build.toolchain] siblings`** — or ⚒ Build refuses every cell before it compiles.
//!
//! MEASURED 2026-10-16 on loki (the build-thing sweep): both cells refused, "path
//! dependencies escape this repo into sibling repo(s) the declaration does not list
//! … Add `siblings = ["openzl-sys-rs"]`" — znippy-common's default `openzl` feature
//! path-deps `../../openzl-sys-rs`. The recipe's own comment already said the list is
//! TRANSITIVE (xtask → nornir's root → …) and that the first-order scan cannot see
//! it; this walks it: the door's reader for this workspace, then the path deps cargo
//! loads for every crate those land in.

use std::collections::BTreeSet;
use std::path::{Component, Path, PathBuf};

use nornir_build_thing::cargo_container::workspace_escaping_path_deps;
use nornir_build_thing::plugin::BuildThing;
use znippy_build_thing::emit::repo_root;
use znippy_build_thing::ZnippyBuildThing;

/// `a/b/../c` → `a/c`, lexically (the dep's directory may not exist on this box).
fn normalize(p: &Path) -> PathBuf {
    let mut out = PathBuf::new();
    for c in p.components() {
        match c {
            Component::ParentDir => {
                out.pop();
            }
            Component::CurDir => {}
            other => out.push(other),
        }
    }
    out
}

/// The `[workspace.dependencies]` path of `dep` for the crate at `dir`: the nearest
/// ancestor manifest that declares a `[workspace]`.
fn workspace_dep_path(dir: &Path, dep: &str) -> Option<PathBuf> {
    let mut cur = Some(dir);
    while let Some(d) = cur {
        if let Some(doc) = std::fs::read_to_string(d.join("Cargo.toml"))
            .ok()
            .and_then(|t| t.parse::<toml::Table>().ok())
        {
            if let Some(ws) = doc.get("workspace") {
                let rel = ws.get("dependencies")?.get(dep)?.get("path")?.as_str()?;
                return Some(normalize(&d.join(rel)));
            }
        }
        cur = d.parent();
    }
    None
}

/// The path dependencies cargo LOADS for a non-member crate at `dir`: its normal and
/// build deps (plain or per-target), never its dev-deps (cargo does not resolve a
/// dependency's dev-dependencies) and never its optional ones, `workspace = true`
/// entries resolved through that crate's own workspace root.
fn loaded_path_deps(dir: &Path) -> Vec<PathBuf> {
    let Some(doc) = std::fs::read_to_string(dir.join("Cargo.toml"))
        .ok()
        .and_then(|t| t.parse::<toml::Table>().ok())
    else {
        return Vec::new();
    };
    let mut tables: Vec<&toml::Value> = Vec::new();
    for key in ["dependencies", "build-dependencies"] {
        tables.extend(doc.get(key));
    }
    if let Some(t) = doc.get("target").and_then(toml::Value::as_table) {
        for v in t.values() {
            for key in ["dependencies", "build-dependencies"] {
                tables.extend(v.get(key));
            }
        }
    }
    let on_by_default = default_enabled(&doc);
    let mut out = Vec::new();
    for t in tables.into_iter().filter_map(toml::Value::as_table) {
        for (dep, spec) in t {
            // An OPTIONAL dep of a non-member is resolved only when a feature turns it
            // on: counted when the crate's own `default` turns it on (znippy-common →
            // openzl-sys-rs), skipped otherwise (draupnir's `tunnr-vm`: not in the
            // Cargo.lock, so no tunnr checkout is needed).
            if spec.get("optional").and_then(toml::Value::as_bool) == Some(true)
                && !on_by_default.contains(dep.as_str())
            {
                continue;
            }
            if let Some(rel) = spec.get("path").and_then(toml::Value::as_str) {
                out.push(normalize(&dir.join(rel)));
            } else if spec.get("workspace").and_then(toml::Value::as_bool) == Some(true) {
                let name = spec.get("package").and_then(toml::Value::as_str).unwrap_or(dep);
                out.extend(workspace_dep_path(dir, name));
            }
        }
    }
    out
}

/// Every feature and dep name the crate's `default` feature turns on, following its
/// own `[features]` table (`default = ["openzl"]`, `openzl = ["dep:openzl-sys-rs"]`).
fn default_enabled(doc: &toml::Table) -> BTreeSet<String> {
    let features = doc.get("features").and_then(toml::Value::as_table);
    let mut on = BTreeSet::new();
    let mut queue = vec!["default".to_string()];
    while let Some(f) = queue.pop() {
        if !on.insert(f.clone()) {
            continue;
        }
        let Some(list) = features.and_then(|t| t.get(&f)).and_then(toml::Value::as_array) else {
            continue;
        };
        for item in list.iter().filter_map(toml::Value::as_str) {
            // `dep:x` and `x?/feat` / `x/feat` all name the dependency `x`.
            let name = item.trim_start_matches("dep:");
            let name = name.split('/').next().unwrap_or(name).trim_end_matches('?');
            queue.push(name.to_string());
        }
    }
    on
}

/// The sibling checkouts a build of this workspace reaches, by name: every path dep
/// that leaves the repo (the door's own reader, members' dev-deps included), and then
/// TRANSITIVELY the path deps cargo loads for each crate those land in. cargo reads
/// every one of those manifests, so the container must carry every one of those
/// checkouts (2026-10-16: znippy-common → `../openzl-sys-rs`). A sibling not checked out on this box ends its branch of
/// the walk; the anchor in the test keeps it from going vacuous.
fn reached_siblings(root: &Path) -> BTreeSet<String> {
    let parent = root.parent().expect("the repo has a parent").to_path_buf();
    let mut names = BTreeSet::new();
    let mut seen: BTreeSet<PathBuf> = BTreeSet::new();
    let mut queue: Vec<PathBuf> =
        workspace_escaping_path_deps(root).into_iter().map(|(_, _, r)| r).collect();
    // The ROOT's `[patch.<registry>]` paths are loaded too when this repo is the build
    // root — a patch is not a dependency table, but cargo reads every one of them.
    if let Some(patch) = std::fs::read_to_string(root.join("Cargo.toml"))
        .ok()
        .and_then(|t| t.parse::<toml::Table>().ok())
        .and_then(|d| d.get("patch").cloned())
    {
        for reg in patch.as_table().into_iter().flat_map(|t| t.values()) {
            for spec in reg.as_table().into_iter().flat_map(|t| t.values()) {
                if let Some(rel) = spec.get("path").and_then(toml::Value::as_str) {
                    queue.push(normalize(&root.join(rel)));
                }
            }
        }
    }
    while let Some(dir) = queue.pop() {
        if dir.starts_with(root) || !seen.insert(dir.clone()) {
            continue;
        }
        if let Some(c) = dir.strip_prefix(&parent).ok().and_then(|r| r.components().next()) {
            names.insert(c.as_os_str().to_string_lossy().into_owned());
        }
        queue.extend(loaded_path_deps(&dir));
    }
    names
}

#[test]
fn every_reached_sibling_is_declared() {
    let root = repo_root();
    let reached = reached_siblings(&root);
    // NON-VACUOUS: znippy path-deps edda and znippy-zoomies directly.
    assert!(reached.contains("edda") && reached.contains("znippy-zoomies"), "the walk found too little: {reached:?}");
    let tc = ZnippyBuildThing.variants().toolchain.expect("znippy declares [build.toolchain]");
    let declared: BTreeSet<String> = tc.siblings.iter().cloned().collect();
    let missing: Vec<&String> = reached.difference(&declared).collect();
    assert!(
        missing.is_empty(),
        "path deps reach sibling repo(s) [build.toolchain] `siblings` does not list — the \
         build container would not have them and ⚒ refuses every cell: {missing:?}"
    );
}