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

nordisk/gunnar

crates/gunnar-recipe/src/twins.rs

//! **Every cargo row of the Build Thing IS a `cargo install` line** (owner order
//! 2026-10-26, G-BOTH-2; Holger's H5 is the model).
//!
//! Gunnar builds both ways: as Build Thing rows ([`crate::variants::VARIANTS`]) and with
//! plain cargo. For every row that compiles a binary (kind `exe` or `service`), [`TWINS`]
//! names the one `cargo install` line that builds the same thing: the same package, the
//! same EFFECTIVE feature set (the package's defaults count when the line keeps them),
//! the same profile and target, and exactly the row's binary is what lands in
//! `~/.cargo/bin`. [`twin_differences`] is the judge; `tests/rows_equal_cargo_install.rs`
//! holds every row to its line, and `cargo xtask twins` (a step of `cargo xtask ci`)
//! measures both resolutions with `cargo tree` and asserts the same crates and the
//! binary named per row.

use std::collections::BTreeSet;

use crate::variants::Variant;

/// One row and its `cargo install` line.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Twin {
    /// The Build Thing row ([`Variant::name`]).
    pub row: &'static str,
    /// The `cargo install` line, exactly as a person types it in a clone of gunnar.
    pub install: &'static str,
}

/// **Every cargo row and its `cargo install` line**, in [`crate::variants::VARIANTS`]
/// order. `cargo install` builds `--release` unless told `--profile`; a line that keeps
/// the package's default features does not say `--no-default-features`.
pub const TWINS: &[Twin] = &[
    // The products (GUN-PRODUCTS): `cargo install <crate>` with the crate's defaults.
    Twin {
        row: "gunnar",
        install: "cargo install --locked --path crates/gunnar",
    },
    Twin {
        row: "gunnar-ui",
        install: "cargo install --locked --path ../gunnar-ui/crates/gunnar-ui",
    },
    Twin {
        row: "gunnar-cli",
        install: "cargo install --locked --path crates/gunnar-cli",
    },
    Twin {
        row: "gunnar-server",
        install: "cargo install --locked --path crates/gunnar-server --no-default-features --features server,engine-znippy,mimalloc,clock-fetch,resolver-probe,fleet-pull,mirror-http,sso-egress,dhcp-client,wg-client,replication,grow-runtime,live-app --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-server-narrate",
        install: "cargo install --locked --path crates/gunnar-server --no-default-features --features server,engine-znippy,mimalloc,clock-fetch,resolver-probe,fleet-pull,mirror-http,sso-egress,dhcp-client,wg-client,replication,grow-runtime,live-app,dev-narrate --profile dev-media --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-server-thin",
        install: "cargo install --locked --path crates/gunnar-server --no-default-features --features server,engine-znippy,mimalloc,clock-fetch,resolver-probe,fleet-pull,mirror-http,sso-egress,dhcp-client,replication,grow-runtime --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-server-metered",
        install: "cargo install --locked --path crates/gunnar-server --no-default-features --features server,engine-znippy,mimalloc,clock-fetch,resolver-probe,fleet-pull,mirror-http,sso-egress,dhcp-client,wg-client,replication,metered,grow-runtime,live-app,admit-share --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-server-metered-narrate",
        install: "cargo install --locked --path crates/gunnar-server --no-default-features --features server,engine-znippy,mimalloc,clock-fetch,resolver-probe,fleet-pull,mirror-http,sso-egress,dhcp-client,wg-client,replication,metered,grow-runtime,live-app,admit-share,dev-narrate --profile dev-media --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-server-metered-thin",
        install: "cargo install --locked --path crates/gunnar-server --no-default-features --features server,engine-znippy,mimalloc,clock-fetch,resolver-probe,fleet-pull,mirror-http,sso-egress,dhcp-client,replication,metered,grow-runtime,admit-share --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-cli-musl",
        install: "cargo install --locked --path crates/gunnar-cli --features client,control --bin gunnar-cli --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-cli-musl-metered",
        install: "cargo install --locked --path crates/gunnar-cli --features client,control,metered --bin gunnar-cli-metered --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-front",
        install: "cargo install --locked --path crates/gunnar-front --no-default-features --features dhcp-client,console,forges,clock-fetch,resolver-probe,acme,egress-door,mail,sso --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-front-narrate",
        install: "cargo install --locked --path crates/gunnar-front --no-default-features --features dhcp-client,console,forges,clock-fetch,resolver-probe,acme,egress-door,mail,sso,dev-narrate --profile dev-media --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-front-metered",
        install: "cargo install --locked --path crates/gunnar-front --no-default-features --features dhcp-client,metered,public-site,forges,clock-fetch,resolver-probe,acme,egress-door,mail,sso --target x86_64-unknown-linux-musl",
    },
    Twin {
        row: "gunnar-front-metered-narrate",
        install: "cargo install --locked --path crates/gunnar-front --no-default-features --features dhcp-client,metered,public-site,forges,clock-fetch,resolver-probe,acme,egress-door,mail,sso,dev-narrate --profile dev-media --target x86_64-unknown-linux-musl",
    },
];

/// The build point a row or a line names: what decides the binary.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Point {
    /// The package directory (`--path`), relative to the repository root.
    pub path: String,
    /// `--no-default-features`.
    pub no_default_features: bool,
    /// `--features`, as a set.
    pub features: BTreeSet<String>,
    /// `--bin`, if named.
    pub bin: Option<String>,
    /// The profile (`release` when the line names none).
    pub profile: String,
    /// `--target`, if named.
    pub target: Option<String>,
}

/// The package directory each cargo row's package lives in, relative to the repository
/// root. A row whose package is not here has no `--path` to install from.
pub const PACKAGE_DIRS: &[(&str, &str)] = &[
    ("gunnar", "crates/gunnar"),
    ("gunnar-cli", "crates/gunnar-cli"),
    // The console's crate, in the gunnar-ui checkout beside this one.
    ("gunnar-ui", crate::variants::UI_MANIFEST_DIR),
    ("gunnar-front", "crates/gunnar-front"),
    ("gunnar-server", "crates/gunnar-server"),
];

/// The directory of a package ([`PACKAGE_DIRS`]).
pub fn package_dir(package: &str) -> Option<&'static str> {
    PACKAGE_DIRS
        .iter()
        .find(|(p, _)| *p == package)
        .map(|(_, d)| *d)
}

/// The package that lives in a directory ([`PACKAGE_DIRS`]).
pub fn dir_package(dir: &str) -> Option<&'static str> {
    PACKAGE_DIRS
        .iter()
        .find(|(_, d)| *d == dir.trim_end_matches('/'))
        .map(|(p, _)| *p)
}

/// Parse a `cargo install --locked --path <dir> …` line into its [`Point`]. Refuses a
/// line that is not a local, `--locked` install, and any flag this judge does not know
/// (an unknown flag could change the binary, and an ignored one would be a false green).
pub fn point_of_install(line: &str) -> Result<Point, String> {
    let words: Vec<&str> = line.split_whitespace().collect();
    if words.len() < 2 || words[0] != "cargo" || words[1] != "install" {
        return Err(format!("`{line}` is not a `cargo install` line"));
    }
    let mut p = Point {
        profile: "release".to_string(),
        ..Point::default()
    };
    let mut locked = false;
    let mut it = words[2..].iter();
    while let Some(w) = it.next() {
        let mut value = |flag: &str| {
            it.next()
                .map(|v| v.to_string())
                .ok_or_else(|| format!("`{flag}` has no value in `{line}`"))
        };
        match *w {
            "--locked" => locked = true,
            "--no-default-features" => p.no_default_features = true,
            "--path" => p.path = value("--path")?,
            "--features" => {
                for f in value("--features")?.split(',').filter(|f| !f.is_empty()) {
                    p.features.insert(f.to_string());
                }
            }
            "--bin" => p.bin = Some(value("--bin")?),
            "--profile" => p.profile = value("--profile")?,
            "--target" => p.target = Some(value("--target")?),
            other => {
                return Err(format!(
                    "`{other}` in `{line}`: not a flag this judge knows"
                ))
            }
        }
    }
    if !locked {
        return Err(format!(
            "`{line}` is not `--locked`: it would build another resolution than the row"
        ));
    }
    if p.path.is_empty() {
        return Err(format!("`{line}` names no `--path`"));
    }
    Ok(p)
}

/// A row's [`Point`]: its package directory (`manifest_dir` for a detached workspace's
/// package), features, binary, profile and target.
pub fn point_of_row(v: &Variant) -> Result<Point, String> {
    let package = v
        .package
        .ok_or_else(|| format!("row {} builds no cargo package", v.name))?;
    let path = match v.manifest_dir {
        Some(d) => d.to_string(),
        None => package_dir(package)
            .ok_or_else(|| {
                format!(
                    "row {}: package {package} has no directory in PACKAGE_DIRS",
                    v.name
                )
            })?
            .to_string(),
    };
    Ok(Point {
        path,
        no_default_features: v.no_default_features,
        features: v.features.iter().map(|f| (*f).to_string()).collect(),
        bin: v.bin.map(str::to_string),
        profile: if v.profile.is_empty() {
            "dev".to_string()
        } else {
            v.profile.to_string()
        },
        target: v.target.map(str::to_string),
    })
}

/// What a package's manifest says about its binaries and default features, for the bin
/// rule: `cargo install` installs every bin whose `required-features` the build has.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Manifest {
    /// `[features] default`.
    pub default_features: BTreeSet<String>,
    /// `[features]`: each feature and the features it turns on in this same package
    /// (`dep:` and `crate/feature` entries left out).
    pub features: Vec<(String, Vec<String>)>,
    /// Each `[[bin]]`: its name and its `required-features`.
    pub bins: Vec<(String, Vec<String>)>,
}

impl Manifest {
    /// The features a build with `chosen` (and the defaults unless `no_default`) has,
    /// closed over the package's own feature table.
    pub fn effective(&self, chosen: &BTreeSet<String>, no_default: bool) -> BTreeSet<String> {
        let mut on: BTreeSet<String> = chosen.clone();
        if !no_default {
            on.extend(self.default_features.iter().cloned());
        }
        loop {
            let before = on.len();
            for (f, implies) in &self.features {
                if on.contains(f) {
                    on.extend(implies.iter().cloned());
                }
            }
            if on.len() == before {
                return on;
            }
        }
    }

    /// The bins `cargo install` leaves behind for a point: the named `--bin`, or every
    /// bin whose required features the build has.
    pub fn installed_bins(&self, p: &Point) -> BTreeSet<String> {
        let on = self.effective(&p.features, p.no_default_features);
        let qualifies = |req: &Vec<String>| req.iter().all(|r| on.contains(r));
        match &p.bin {
            Some(b) => self
                .bins
                .iter()
                .filter(|(n, req)| n == b && qualifies(req))
                .map(|(n, _)| n.clone())
                .collect(),
            None => self
                .bins
                .iter()
                .filter(|(_, req)| qualifies(req))
                .map(|(n, _)| n.clone())
                .collect(),
        }
    }
}

/// **The judge**: every way a row and its `cargo install` line build different things.
/// Empty means twins. `manifest` is the package's own manifest facts (defaults, feature
/// table, bins), read by the caller.
pub fn twin_differences(row: &Point, install: &Point, manifest: &Manifest) -> Vec<String> {
    let mut out = Vec::new();
    if row.path.trim_end_matches('/') != install.path.trim_end_matches('/') {
        out.push(format!(
            "the row builds {} and the line installs --path {}",
            row.path, install.path
        ));
    }
    let row_on = manifest.effective(&row.features, row.no_default_features);
    let line_on = manifest.effective(&install.features, install.no_default_features);
    if row_on != line_on {
        let only_row: Vec<&String> = row_on.difference(&line_on).collect();
        let only_line: Vec<&String> = line_on.difference(&row_on).collect();
        out.push(format!(
            "features differ: only the row has {only_row:?}, only the line has {only_line:?}"
        ));
    }
    if row.profile != install.profile {
        out.push(format!(
            "profile: the row builds `{}`, the line `{}`",
            row.profile, install.profile
        ));
    }
    if row.target != install.target {
        out.push(format!(
            "target: the row builds {:?}, the line {:?}",
            row.target, install.target
        ));
    }
    let want: BTreeSet<String> = row.bin.iter().cloned().collect();
    let got = manifest.installed_bins(install);
    if want.is_empty() {
        out.push("the row names no binary".to_string());
    } else if got != want {
        out.push(format!(
            "the line installs {got:?}, the row builds {want:?}"
        ));
    }
    if let Some(b) = &row.bin {
        if manifest.installed_bins(row).is_empty() {
            out.push(format!(
                "the row's own features do not build its bin `{b}` (required-features)"
            ));
        }
    }
    out
}

/// The rows a twin is owed for: every row that compiles a binary.
pub fn cargo_rows() -> Vec<&'static Variant> {
    use nornir_build_thing::variant::ArtifactKind;
    crate::variants::VARIANTS
        .iter()
        .filter(|v| {
            matches!(v.kind, ArtifactKind::Exe | ArtifactKind::Service) && v.package.is_some()
        })
        .collect()
}

/// The twin of a row, if [`TWINS`] has one.
pub fn twin_of(row: &str) -> Option<&'static Twin> {
    TWINS.iter().find(|t| t.row == row)
}

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

    fn manifest() -> Manifest {
        Manifest {
            default_features: ["client".to_string()].into(),
            features: vec![(
                "solo".to_string(),
                vec!["client".to_string(), "server".to_string()],
            )],
            bins: vec![
                ("gunnar".to_string(), vec!["client".to_string()]),
                (
                    "gunnar-solo".to_string(),
                    vec!["server".to_string(), "client".to_string()],
                ),
            ],
        }
    }

    #[test]
    fn a_line_parses_into_its_point() {
        let p = point_of_install("cargo install --locked --path crates/gunnar-cli --no-default-features --features a,b --bin x --profile dev-media --target t").unwrap();
        assert_eq!(p.path, "crates/gunnar-cli");
        assert!(p.no_default_features);
        assert_eq!(p.features, ["a".to_string(), "b".to_string()].into());
        assert_eq!(p.bin.as_deref(), Some("x"));
        assert_eq!(p.profile, "dev-media");
        assert_eq!(p.target.as_deref(), Some("t"));
        assert_eq!(
            point_of_install("cargo install --locked --path p")
                .unwrap()
                .profile,
            "release",
            "cargo install builds release"
        );
    }

    #[test]
    fn a_line_that_is_not_local_and_locked_or_has_an_unknown_flag_is_refused() {
        assert!(point_of_install("cargo install --path p")
            .unwrap_err()
            .contains("--locked"));
        assert!(point_of_install("cargo install --locked gunnar")
            .unwrap_err()
            .contains("gunnar"));
        assert!(
            point_of_install("cargo install --locked --path p --all-features")
                .unwrap_err()
                .contains("--all-features")
        );
        assert!(point_of_install("cargo build -p x").is_err());
    }

    #[test]
    fn the_defaults_count_when_a_line_keeps_them() {
        let row = Point {
            path: "crates/gunnar-cli".into(),
            no_default_features: true,
            features: ["client".to_string()].into(),
            bin: Some("gunnar".into()),
            profile: "release".into(),
            target: None,
        };
        let line = point_of_install("cargo install --locked --path crates/gunnar-cli").unwrap();
        assert!(
            twin_differences(&row, &line, &manifest()).is_empty(),
            "{:?}",
            twin_differences(&row, &line, &manifest())
        );
    }

    #[test]
    fn each_difference_is_named() {
        let row = Point {
            path: "crates/gunnar-cli".into(),
            no_default_features: true,
            features: ["client".to_string(), "server".to_string()].into(),
            bin: Some("gunnar-solo".into()),
            profile: "release".into(),
            target: None,
        };
        // Both bins land: the line names no --bin.
        let line =
            point_of_install("cargo install --locked --path crates/gunnar-cli --features server")
                .unwrap();
        let d = twin_differences(&row, &line, &manifest());
        assert_eq!(d.len(), 1, "{d:?}");
        assert!(
            d[0].contains("gunnar") && d[0].contains("gunnar-solo"),
            "{d:?}"
        );
        let line = point_of_install("cargo install --locked --path crates/gunnar --features server --bin gunnar-solo --profile dev --target t").unwrap();
        let d = twin_differences(&row, &line, &manifest());
        assert!(
            d.iter().any(|x| x.contains("--path crates/gunnar")),
            "{d:?}"
        );
        assert!(d.iter().any(|x| x.contains("profile")), "{d:?}");
        assert!(d.iter().any(|x| x.contains("target")), "{d:?}");
        let line = point_of_install("cargo install --locked --path crates/gunnar-cli --no-default-features --features server --bin gunnar-solo").unwrap();
        let d = twin_differences(&row, &line, &manifest());
        assert!(
            d.iter()
                .any(|x| x.contains("features differ") && x.contains("client")),
            "{d:?}"
        );
    }

    #[test]
    fn a_feature_that_turns_on_another_is_followed() {
        let m = manifest();
        let on = m.effective(&["solo".to_string()].into(), true);
        assert!(on.contains("server") && on.contains("client"), "{on:?}");
    }
}