xtask/src/paid_guard.rs
//! **`cargo xtask free-tree`: open Holger links nothing paid** (A5 L-Trust, Gunnar's
//! `xtask/src/paid_guard.rs` ported; lane SC 2026-10-22).
//!
//! Holger Pro lives in Vetra's private `monetize-impl` and reaches a box only as signed
//! wasm (a meter or a product guest built and signed there). Open Holger's tree therefore
//! never names a `monetize-impl` crate — not in the default build, not under `metered`, and
//! not in `Cargo.lock`. The OPEN monetize crates (`monetize-embed`, `monetize-product`,
//! `monetize-proto`, …) are allowed under `metered`: they are AGPL like Holger.
//!
//! Three checks, each naming every offender once:
//!
//! * **the lock** — `Cargo.lock` holds no paid crate by name (a path dependency on
//! `monetize-impl` would put its crate there, whatever features are on);
//! * **the default server** — `cargo tree -p holger -e normal,build` holds no paid
//! crate by name and no package from a `monetize-impl` or `private-*` checkout;
//! * **the metered server** — the same under `--features metered,meter-guests`.
use std::collections::BTreeSet;
use std::path::Path;
use std::process::Command;
/// The crates of Vetra's private `monetize-impl` workspace (never published; AGPL +
/// commercial, `publish = false`) — paid wherever they turn up.
pub const PAID_NAMES: &[&str] = &[
"grow-probe",
"gunnar-shop-bench",
"monetize-cloud-impl",
"monetize-guest-dns-cloudflare",
"monetize-guest-klarna",
"monetize-guest-njord",
"monetize-guest-scaleway",
"monetize-guest-signup-gunnar",
"monetize-guest-signup-holger",
"monetize-guest-stripe",
"monetize-guest-swish",
"monetize-guest-upcloud",
"monetize-impl-build-thing",
"monetize-pay-impl",
"monetize-plugin-holger",
"monetize-product-njord",
"monetize-server-vetra",
"monetize-signup-impl",
"monetize-tax-impl",
"money-flow",
"upcloud-storm",
];
/// The server package every check reads, and the feature sets it ships with.
pub const SERVER: &str = "holger";
pub const ROWS: &[(&str, &[&str])] = &[
("default", &[]),
("metered", &["--features", "metered,meter-guests"]),
];
/// One paid crate in a listing.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Offender {
pub name: String,
pub version: String,
}
/// Pure: **the paid crates in a `cargo tree --prefix none --format "{p}"` listing**, each once.
pub fn offenders(tree: &str) -> Vec<Offender> {
let mut seen = BTreeSet::new();
let mut out = Vec::new();
for line in tree.lines() {
let mut words = line.split_whitespace();
let (Some(name), Some(version)) =
(words.next(), words.next().and_then(|v| v.strip_prefix('v')))
else {
continue;
};
let source = line
.find(" (")
.map(|i| &line[i + 2..])
.and_then(|s| s.split(')').next())
.unwrap_or_default();
let private = source.contains("/monetize-impl/") || source.contains("/private-");
if (PAID_NAMES.contains(&name) || private) && seen.insert(name.to_owned()) {
out.push(Offender {
name: name.to_owned(),
version: version.to_owned(),
});
}
}
out
}
/// Pure: **the paid crates `Cargo.lock` names**, each once.
pub fn lock_offenders(lock: &str) -> Vec<String> {
let mut seen = BTreeSet::new();
lock.lines()
.filter_map(|l| {
l.trim()
.strip_prefix("name = \"")
.and_then(|r| r.strip_suffix('"'))
})
.filter(|n| PAID_NAMES.contains(n) && seen.insert(n.to_string()))
.map(str::to_owned)
.collect()
}
fn tree(root: &Path, cargo: &str, extra: &[&str]) -> Result<String, String> {
let mut args = vec![
"tree",
"-p",
SERVER,
"-e",
"normal,build",
"--prefix",
"none",
"--format",
"{p}",
];
args.extend_from_slice(extra);
let out = Command::new(cargo)
.current_dir(root)
.args(&args)
.output()
.map_err(|e| format!("{cargo} tree: {e}"))?;
if !out.status.success() {
return Err(format!(
"{cargo} {}: {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
/// **The three checks**; `Err` names every offender (and why), `Ok` what was read.
pub fn check(root: &Path, cargo: &str) -> Result<Vec<String>, Vec<String>> {
let mut said = Vec::new();
let mut bad = Vec::new();
match std::fs::read_to_string(root.join("Cargo.lock")) {
Ok(lock) => {
let paid = lock_offenders(&lock);
if paid.is_empty() {
said.push("Cargo.lock names no paid crate".to_owned());
}
bad.extend(paid.into_iter().map(|n| {
format!(
"Cargo.lock names {n}: Vetra's private monetize-impl, never in Holger's tree"
)
}));
}
Err(e) => bad.push(format!("Cargo.lock: {e}")),
}
for (row, extra) in ROWS {
match tree(root, cargo, extra) {
Ok(listing) => {
let paid = offenders(&listing);
let open: BTreeSet<&str> = listing
.lines()
.filter_map(|l| l.split_whitespace().next())
.filter(|n| n.starts_with("monetize"))
.collect();
if paid.is_empty() {
said.push(format!(
"{SERVER} ({row}) links no paid crate; open monetize crates: {}",
if open.is_empty() {
"none".to_owned()
} else {
open.into_iter().collect::<Vec<_>>().join(", ")
}
));
}
bad.extend(paid.into_iter().map(|o| format!("{SERVER} ({row}) links {} v{}: Vetra's private monetize-impl, never in an open build", o.name, o.version)));
}
Err(why) => bad.push(why),
}
}
if bad.is_empty() {
Ok(said)
} else {
Err(bad)
}
}
/// `cargo xtask free-tree`.
pub fn run(root: &Path, cargo: &str) -> anyhow::Result<()> {
match check(root, cargo) {
Ok(said) => {
for l in said {
println!("free-tree: {l}");
}
Ok(())
}
Err(bad) => {
for l in &bad {
eprintln!("free-tree: ✗ {l}");
}
anyhow::bail!("{} paid link(s) in open Holger", bad.len())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// ★ The listing check names each paid crate once — by name, or by the checkout it comes
/// from — and passes open monetize crates.
#[test]
fn the_free_tree_names_every_paid_crate_once_and_passes_open_monetize() {
let clean = "holger v0.7.0 (/w/holger/server/cli)\nmonetize-embed v0.1.3 (/w/monetize/crates/monetize-embed)\nmonetize-product v0.1.4\nserde v1.0.228\nserde_derive v1.0.228 (proc-macro)\nholger-server-lib v0.6.0 (/w/holger/server/lib) (*)\n";
assert!(
offenders(clean).is_empty(),
"open monetize crates are allowed"
);
let dirty = format!("{clean}monetize-pay-impl v0.3.0 (/w/monetize-impl/crates/monetize-pay-impl)\nmonetize-pay-impl v0.3.0 (/w/monetize-impl/crates/monetize-pay-impl) (*)\nrenamed-pro v0.1.0 (/w/monetize-impl/guests/pro)\nops-thing v0.1.0 (/w/private-monetize-ops/x)\nmonetize-plugin-holger v0.1.0\n");
let names: Vec<String> = offenders(&dirty).into_iter().map(|o| o.name).collect();
assert_eq!(
names,
[
"monetize-pay-impl",
"renamed-pro",
"ops-thing",
"monetize-plugin-holger"
]
);
}
/// The lock check reads `[[package]]` names.
#[test]
fn the_lock_names_no_paid_crate_or_is_refused() {
let clean = "version = 4\n\n[[package]]\nname = \"monetize-embed\"\nversion = \"0.1.3\"\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.228\"\nsource = \"registry+https://github.com/rust-lang/crates.io-index\"\n";
assert!(lock_offenders(clean).is_empty());
let dirty = format!("{clean}\n[[package]]\nname = \"monetize-plugin-holger\"\nversion = \"0.1.0\"\n\n[[package]]\nname = \"monetize-pay-impl\"\nversion = \"0.3.0\"\n");
assert_eq!(
lock_offenders(&dirty),
["monetize-plugin-holger", "monetize-pay-impl"]
);
}
}