xtask/src/ci.rs
//! `cargo xtask ci`: dwarves' own CI plan, and the two constellation-wide verbs.
//!
//! * `cargo xtask ci [--quick|--nightly] [--list] [--only <prefix>]`: dwarves' lock
//! check, its workspace tests, its build with holger in it (`build:holger`) and (full
//! and nightly) its declared rows;
//! * `cargo xtask ci lockcheck --siblings <dir> [--repo <name>]... [--single <crate>]...`:
//! THE LOCK CHECK over a constellation of checked-out heads. Every `Cargo.lock` of
//! every checkout under `<dir>` is asked `cargo metadata --locked` in its own
//! directory with the sibling patches on, and no crate may appear at two versions
//! where that is refused (korp-installer by name, or a sibling's crate beside a
//! registry copy of it). It lives here because dwarves is the forge: it is the
//! repo that looks at the whole constellation at once;
//! * `cargo xtask ci status --runs <dir> [--out <file>]`: the status page over every
//! receipt below `<dir>`.
//!
//! The runner and every step are nornir-xtask-support's `ci` and `lockcheck`; this
//! file only lists dwarves' steps.
use std::path::{Path, PathBuf};
use nornir_xtask_support::ci::{self, Mode, Plan, Step};
use nornir_xtask_support::lockcheck;
fn root() -> PathBuf {
// xtask/ is one level below the workspace root.
Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("."))
}
/// dwarves' plan.
pub fn plan(root: &Path) -> Plan {
Plan {
repo: "dwarves".into(),
root: root.to_path_buf(),
steps: vec![
lockcheck::step(root),
Step::cargo("test", &["test", "--workspace", "--locked"]),
// dwarves WITH holger in it, against the holger checkout beside it (on the forge:
// holger main's head). The default features link no holger, so without this step a
// holger change that breaks dwarves (Holger 3.0.0 asked for 3.0 where dwarves and
// nornir asked for 2.0) shows first in a media lane. `embed-holger` also builds
// nornir's embedded holger; `sign-step` is the media forge's feature. Not
// `--locked`: this step answers whether dwarves BUILDS against holger's head (as the
// media forge's `cargo run` does); a lock that trails a sibling is lockcheck's red.
Step::cargo(
"build:holger",
&["build", "-p", "dwarves", "--features", "embed-holger,rehearse,sign-step", "--bin", "dwarves"],
),
Step::cargo("rows:ORIGINAL", &["xtask", "build", "--suite", "ORIGINAL"]).from(Mode::Full),
],
}
}
fn value(args: &[String], flag: &str) -> Option<String> {
args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned()
}
fn values(args: &[String], flag: &str) -> Vec<String> {
let mut out = Vec::new();
let mut it = args.iter();
while let Some(a) = it.next() {
if a == flag {
if let Some(v) = it.next() {
out.push(v.clone());
}
}
}
out
}
/// `cargo xtask ci lockcheck`: a receipt like every other ci run, repo `constellation`.
fn lockcheck_verb(args: &[String]) -> i32 {
let dir = value(args, "--siblings")
.map(PathBuf::from)
.unwrap_or_else(|| root().parent().map(Path::to_path_buf).unwrap_or_else(|| PathBuf::from("..")));
let dir = std::path::absolute(&dir).unwrap_or(dir);
let named = values(args, "--repo");
let single = lockcheck::single_from_args(args);
let d = dir.clone();
let step = Step::in_process("lockcheck", move |w| {
let single: Vec<&str> = single.iter().map(String::as_str).collect();
for repo in lockcheck::repos_under(&d, &named) {
let head = ci::head(&repo);
let _ = writeln!(w, "head {} {head}", repo.file_name().map(|n| n.to_string_lossy()).unwrap_or_default());
}
let red: Vec<String> =
lockcheck::check_constellation(&d, &named, &single, w)?.iter().filter(|f| f.is_red()).map(|f| f.to_string()).collect();
if red.is_empty() {
Ok(None)
} else {
Err(format!("{} red finding(s): {}", red.len(), red.join("; ")).into())
}
});
let plan = Plan { repo: "constellation".into(), root: dir, steps: vec![step] };
ci::main(&plan, &[])
}
use std::io::Write as _;
fn status_verb(args: &[String]) -> i32 {
let Some(runs) = value(args, "--runs") else {
eprintln!("usage: cargo xtask ci status --runs <dir> [--out <file>]");
return 2;
};
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0);
let md = ci::status_md(Path::new(&runs), now);
match value(args, "--out") {
Some(out) => match std::fs::write(&out, &md) {
Ok(()) => {
println!("ci status: {out}");
0
}
Err(e) => {
eprintln!("ci status: write {out}: {e}");
1
}
},
None => {
print!("{md}");
0
}
}
}
/// `cargo xtask ci ...`.
pub fn run(args: &[String]) -> i32 {
match args.first().map(String::as_str) {
Some("lockcheck") => lockcheck_verb(&args[1..]),
Some("status") => status_verb(&args[1..]),
_ => ci::main(&plan(&root()), args),
}
}