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

nordisk/gunnar

xtask/src/siblings.rs

//! **The estate's sibling patches live in the estate workspaces, never at the root** (G-B7,
//! owner order G-BOTH-1, 2026-10-26).
//!
//! A lone clone of gunnar builds from crates.io, and so does the root of every estate
//! checkout: no committed manifest, `[patch]` or cargo config of the root workspace points
//! out of the repository, and the committed root lock is the crates.io lock. Until G-B7 an
//! optional `.cargo/siblings.toml` include (written by every `cargo xtask` from
//! `.cargo/estate-siblings.toml`) patched the siblings in; a cargo config reaches every
//! workspace cargo runs from, the root included, so the root lock was the estate's and a lone
//! clone's `cargo build --locked` refused it.
//!
//! Now each [`ESTATE_WORKSPACES`] manifest names, in its own `[patch.crates-io]`, the
//! [`ESTATE_PATCHES`] its lock takes from the sibling checkouts beside the repository.
//! What is left here is pure: the patch names of a manifest and a lock's drift from them.

/// **The estate's sibling patches** (G-B7): every crate an estate workspace of this repository
/// takes from a sibling checkout instead of crates.io. Each [`ESTATE_WORKSPACES`] manifest
/// patches the ones its lock takes, in its own `[patch.crates-io]`; the ROOT patches none.
pub const ESTATE_PATCHES: &[&str] = &[
    "gunnar-pack",
    "gunnar-delta",
    "gunnar-bitmap",
    "gunnar-sendpack",
    "gunnar-stage",
    "gunnar-pktline",
    "nornir-git",
    "nornir-build-thing",
    "storage-git-gix",
    "git-storage-trait",
    "estate-clock",
    "disk-snapshot",
    "appliance-pid1",
    "wg-appliance-core",
    "edda-admit",
    "nornir-auth-oidc",
    "nornir-auth-saml",
    "nornir-auth-webauthn",
    "nornir-identity",
    "nornir-identity-session",
    "nornir-identity-directory",
    "nornir-identity-sso",
    "nornir-auth-sshsig",
    "nornir-service",
    "nordisk-pairing",
    "nordisk-narrate",
    "korp-installer",
    "monetize-product",
    "monetize-embed",
    "monetize-proto",
    "monetize-estate-side",
];

/// **The estate workspaces**: the detached workspaces that build against the sibling checkouts
/// (each its own lock, built with `--manifest-path`). A lone clone does not build them.
pub const ESTATE_WORKSPACES: &[&str] = &[
    "crates/gunnar-front",
    "crates/gunnar-monetize-guest",
    "crates/gunnar-loadbench",
    "crates/gunnar-growbench",
    "installtest",
    "bench",
];

/// Pure: **the crate names a manifest's `[patch.*]` tables patch** (a workspace's own
/// `Cargo.toml`): the key of every `name = { … }` line under one.
pub fn patch_names(manifest: &str) -> std::collections::BTreeSet<String> {
    let mut out = std::collections::BTreeSet::new();
    let mut in_patch = false;
    for line in manifest.lines().map(str::trim) {
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if line.starts_with('[') {
            in_patch = line.starts_with("[patch.") || line == "[patch]";
            continue;
        }
        if in_patch {
            if let Some((key, _)) = line.split_once('=') {
                out.insert(key.trim().trim_matches('"').to_string());
            }
        }
    }
    out
}

/// Pure: **where a `Cargo.lock` disagrees with the patches it was resolved under.**
///
/// cargo records every `[patch]` entry a workspace did not use as `[[patch.unused]]`. When a
/// patch is dropped from a manifest, or a sibling include goes (G-B7), every lock that records
/// the old list is rewritten by the next build in its workspace (b4c335c2 dropped
/// `plugin-door-wire` and left nine locks naming it). `cargo metadata --locked` does not see
/// it (the resolve is the same), so it is read here: an unused patch that neither `estate`
/// (patches every lock must record, used or unused) nor `own` (the workspace's own) declares,
/// and an `estate` patch that is neither used (a path `[[package]]`) nor recorded unused.
pub fn lock_drift(
    lock: &str,
    estate: &std::collections::BTreeSet<String>,
    own: &std::collections::BTreeSet<String>,
) -> Vec<String> {
    let mut unused = std::collections::BTreeSet::new();
    let mut by_path = std::collections::BTreeSet::new();
    // One table at a time: its kind, its name, whether it named a source.
    let mut table: (&str, String, bool) = ("", String::new(), false);
    let mut close =
        |t: &(&str, String, bool), unused: &mut std::collections::BTreeSet<String>| match t.0 {
            "[[patch.unused]]" => {
                unused.insert(t.1.clone());
            }
            "[[package]]" if !t.2 => {
                by_path.insert(t.1.clone());
            }
            _ => {}
        };
    for line in lock.lines().map(str::trim) {
        if line.starts_with('[') {
            close(&table, &mut unused);
            table = (
                if line == "[[patch.unused]]" {
                    "[[patch.unused]]"
                } else if line == "[[package]]" {
                    "[[package]]"
                } else {
                    ""
                },
                String::new(),
                false,
            );
        } else if let Some(v) = line.strip_prefix("name = ") {
            table.1 = v.trim_matches('"').to_string();
        } else if line.starts_with("source = ") {
            table.2 = true;
        }
    }
    close(&table, &mut unused);
    let mut out = Vec::new();
    for name in &unused {
        if !estate.contains(name) && !own.contains(name) {
            out.push(format!("records the patch `{name}` as unused, and no patch of that name is declared any more"));
        }
    }
    for name in estate {
        if !unused.contains(name) && !by_path.contains(name) {
            out.push(format!(
                "does not record the estate's patch `{name}` (neither used nor unused)"
            ));
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_estate_patch_list_names_each_crate_once() {
        let set: std::collections::BTreeSet<_> = ESTATE_PATCHES.iter().collect();
        assert_eq!(
            set.len(),
            ESTATE_PATCHES.len(),
            "a name twice in ESTATE_PATCHES"
        );
        assert!(ESTATE_WORKSPACES
            .iter()
            .all(|w| !w.is_empty() && !w.starts_with('/')));
    }

    #[test]
    fn the_patch_names_are_the_keys_under_a_patch_table() {
        let m = "[workspace]\nmembers = []\n[patch.crates-io]\n# old = { path = \"x\" }\ngunnar-pack = { path = \"../edda/p\" }\n\"quoted\" = { path = \"q\" }\n[profile.release]\nlto = true\n";
        assert_eq!(
            patch_names(m).into_iter().collect::<Vec<_>>(),
            vec!["gunnar-pack", "quoted"]
        );
    }

    #[test]
    fn a_lock_naming_a_patch_nobody_declares_or_missing_one_is_drift() {
        let set = |v: &[&str]| {
            v.iter()
                .map(|s| s.to_string())
                .collect::<std::collections::BTreeSet<_>>()
        };
        let lock = "version = 4\n\n[[package]]\nname = \"used\"\nversion = \"0.1.0\"\n\n[[package]]\nname = \"rayon\"\nversion = \"1.0.0\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n\n[[patch.unused]]\nname = \"idle\"\nversion = \"0.1.0\"\n\n[[patch.unused]]\nname = \"gone\"\nversion = \"0.1.1\"\n";
        assert_eq!(
            lock_drift(lock, &set(&["used", "idle", "gone"]), &set(&[])),
            Vec::<String>::new(),
            "used by path, idle and gone unused: current"
        );
        let drift = lock_drift(lock, &set(&["used", "idle", "rayon"]), &set(&[]));
        assert_eq!(drift.len(), 2, "{drift:?}");
        assert!(
            drift[0].contains("`gone`") && drift[1].contains("`rayon`"),
            "{drift:?}"
        );
        assert_eq!(
            lock_drift(lock, &set(&["used", "idle"]), &set(&["gone"])),
            Vec::<String>::new(),
            "the workspace's own patch may be unused"
        );
    }
}