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