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

nordisk/holger

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(&copy).ok().as_deref() == Some(template.as_slice()) {
        return Ok(Refresh::Current);
    }
    std::fs::write(&copy, &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);
    }
}