xtask/boot/src/siblings.rs
//! **The estate's sibling patches, switched on where the siblings are** (owner order
//! 2026-10-22, the lone clone).
//!
//! A lone clone of holger builds from crates.io: no committed manifest or `[patch]` may
//! point out of the repository, because cargo refuses to load a workspace with a
//! missing path in it. An estate or Build Thing checkout builds the same crates from
//! the sibling checkouts beside it. Since B7 (2026-10-26) only the estate workspaces
//! (`estate/`, `holger-estate-front/`) do, through `[patch.crates-io]` in their own
//! manifests; the root builds from crates.io. `.cargo/siblings.toml`, the uncommitted
//! copy of `.cargo/estate-siblings.toml`, is still written (holger-ui's checkout reads
//! its own copy the same way), and holger's root includes it no more.
//!
//! [`refresh`] keeps that copy current: every `cargo xtask` calls it first, so the
//! Build Thing, the installtest and the gate in an estate checkout use the same
//! patches as before the split, and `cargo xtask siblings` does only that. It runs in
//! `xtask/boot`, a detached workspace with NO dependency, so it starts even in a
//! checkout whose root workspace cannot resolve without the siblings (the chicken and
//! the egg: the real `xtask` is a root member). The real `xtask` compiles this same
//! file (`#[path]`) and refreshes too.
use std::path::Path;
/// The sibling checkouts the estate patches reach, beside the repository. Kept equal
/// to the siblings `.cargo/estate-siblings.toml` names by `xtask/tests/lone_clone.rs`.
pub const SIBLINGS: &[&str] = &[
"edda",
"Skidbladnir",
"monetize",
"tunnr",
"draupnir",
"modgunn",
"facett",
];
/// The committed template and its uncommitted, included copy (repo-relative).
pub const TEMPLATE: &str = ".cargo/estate-siblings.toml";
pub const COPY: &str = ".cargo/siblings.toml";
/// What [`refresh`] did.
#[derive(Debug, PartialEq, Eq)]
pub enum Refresh {
/// The siblings are beside the repo and `.cargo/siblings.toml` was (re)written.
Written,
/// The siblings are beside the repo and the copy was already current.
Current,
/// A sibling is missing: a lone clone. Nothing is written, nothing removed.
Lone(String),
}
/// Write `.cargo/siblings.toml` from `.cargo/estate-siblings.toml` when every sibling
/// in [`SIBLINGS`] is checked out beside `root`.
pub fn refresh(root: &Path) -> std::io::Result<Refresh> {
let parent = root.parent().unwrap_or(root);
if let Some(missing) = SIBLINGS.iter().find(|s| !parent.join(s).is_dir()) {
return Ok(Refresh::Lone((*missing).to_string()));
}
let template = std::fs::read(root.join(TEMPLATE))?;
let copy = root.join(COPY);
if std::fs::read(©).ok().as_deref() == Some(template.as_slice()) {
return Ok(Refresh::Current);
}
std::fs::write(©, &template)?;
Ok(Refresh::Written)
}
#[cfg(test)]
mod tests {
use super::*;
/// A throwaway checkout tree (no dependencies to spend here: `xtask/boot` has none).
struct Tree(std::path::PathBuf);
impl Drop for Tree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
impl Tree {
fn path(&self) -> &Path {
&self.0
}
}
fn tree(with_siblings: bool) -> Tree {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let d = Tree(std::env::temp_dir().join(format!(
"xtask-siblings-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
)));
let root = d.path().join("holger");
std::fs::create_dir_all(root.join(".cargo")).unwrap();
std::fs::write(
root.join(TEMPLATE),
"[patch.crates-io]\njera = { path = \"../edda/crates/jera\" }\n",
)
.unwrap();
if with_siblings {
for s in SIBLINGS {
std::fs::create_dir_all(d.path().join(s)).unwrap();
}
}
d
}
#[test]
fn a_lone_clone_gets_no_sibling_patches() {
let d = tree(false);
let root = d.path().join("holger");
assert_eq!(refresh(&root).unwrap(), Refresh::Lone("edda".to_string()));
assert!(!root.join(COPY).exists());
}
#[test]
fn an_estate_checkout_gets_them_and_keeps_them_current() {
let d = tree(true);
let root = d.path().join("holger");
assert_eq!(refresh(&root).unwrap(), Refresh::Written);
assert_eq!(refresh(&root).unwrap(), Refresh::Current);
std::fs::write(root.join(TEMPLATE), "[patch.crates-io]\n# changed\n").unwrap();
assert_eq!(refresh(&root).unwrap(), Refresh::Written);
}
}