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