Files
figcraft/xtask/src/main.rs
T

158 lines
6.0 KiB
Rust

//! Project tasks: `cargo xtask <task>`.
//!
//! - `ci` fmt check, clippy -D warnings, tests, layers, assets, wasm check
//! - `layers` crate dependency layering check
//! - `assets` every file under assets/ and docs/ has a row in ATTRIBUTION.md
//! - `parity` menu-catalog coverage → docs/parity.md
//! - `version` print the version, or `version set X.Y.Z`
//! - `icon` regenerate the app icon PNGs/ICO from code (original artwork)
//! - `wasm` check the core crates and web app for wasm32
mod icon;
mod layers;
use std::process::{Command, ExitCode};
fn run(cmd: &mut Command) -> bool {
eprintln!("$ {cmd:?}");
cmd.status().map(|s| s.success()).unwrap_or(false)
}
fn cargo() -> Command {
Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()))
}
fn root() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent().map(|p| p.to_path_buf()).unwrap_or_default()
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
let task = args.first().map(String::as_str).unwrap_or("help");
let ok = match task {
"ci" => ci(),
"layers" => layers::check(&root()),
"assets" => assets(),
"parity" => parity(),
"version" => version(args.get(1).map(String::as_str), args.get(2).map(String::as_str)),
"icon" => icon::generate(&root()),
"wasm" => wasm(),
_ => {
eprintln!("tasks: ci, layers, assets, parity, version [set X.Y.Z], icon, wasm");
task == "help"
}
};
if ok { ExitCode::SUCCESS } else { ExitCode::FAILURE }
}
fn ci() -> bool {
run(cargo().args(["fmt", "--all", "--check"]))
&& run(cargo().args(["clippy", "--workspace", "--all-targets", "--", "-D", "warnings"]))
&& run(cargo().args(["test", "--workspace"]))
&& layers::check(&root())
&& assets()
&& wasm()
}
fn wasm() -> bool {
run(cargo().args(["check", "--target", "wasm32-unknown-unknown", "-p", "figcraft-engine", "-p", "figcraft-ui-egui", "-p", "figcraft-web"]))
}
/// Every asset file must be listed in ATTRIBUTION.md.
fn assets() -> bool {
let root = root();
let attribution = std::fs::read_to_string(root.join("ATTRIBUTION.md")).unwrap_or_default();
let mut missing = Vec::new();
for dir in ["assets", "docs/brand", "docs/images"] {
walk(&root.join(dir), &mut |p| {
let rel = p.strip_prefix(&root).map(|r| r.to_string_lossy().replace('\\', "/")).unwrap_or_default();
let name = p.file_name().map(|f| f.to_string_lossy().to_string()).unwrap_or_default();
if name.starts_with('.') || name.ends_with(".md") || name.ends_with(".txt") {
return;
}
// Listed by path, or by any ancestor directory written as `dir/`.
let mut listed = attribution.contains(&format!("`{rel}`"));
let mut cur = rel.as_str();
while let Some((parent, _)) = cur.rsplit_once('/') {
if attribution.contains(&format!("`{parent}/`")) {
listed = true;
break;
}
cur = parent;
}
if !listed {
missing.push(rel);
}
});
}
if !missing.is_empty() {
eprintln!("assets without an ATTRIBUTION.md row:\n {}", missing.join("\n "));
return false;
}
eprintln!("assets: ok");
true
}
fn walk(dir: &std::path::Path, f: &mut dyn FnMut(&std::path::Path)) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
walk(&p, f);
} else {
f(&p);
}
}
}
fn parity() -> bool {
let extra = ["file.open", "file.import", "file.placeImage", "file.save", "file.saveAs", "file.export", "file.exportSvg", "edit.copyAsText", "edit.copyAsCss", "edit.copyAsSvg", "edit.copyAsPng", "edit.copyLink"];
let (done, total, missing) = figcraft_engine::catalog::parity(&extra);
let pct = done as f64 * 100.0 / total.max(1) as f64;
let mut md = format!(
"# Parity report\n\nGenerated by `cargo xtask parity`: FigCraft commands registered for the incumbent's main menu and toolbar ({} commands in the engine).\n\n**{done} / {total} menu items ({pct:.1}%)**\n\n",
figcraft_engine::all_commands().len()
);
if missing.is_empty() {
md.push_str("Every catalog item has a command.\n");
} else {
md.push_str("## Missing\n\n");
for m in &missing {
md.push_str(&format!("- `{m}`\n"));
}
}
md.push_str("\nMenu coverage measures breadth only; see ROADMAP.md for depth and fidelity.\n");
let path = root().join("docs/parity.md");
if let Err(e) = std::fs::write(&path, md) {
eprintln!("{}: {e}", path.display());
return false;
}
eprintln!("parity: {done}/{total} ({pct:.1}%) → docs/parity.md");
true
}
fn version(sub: Option<&str>, value: Option<&str>) -> bool {
let path = root().join("Cargo.toml");
let Ok(text) = std::fs::read_to_string(&path) else { return false };
let cur = text.lines().skip_while(|l| !l.starts_with("[workspace.package]")).find_map(|l| l.strip_prefix("version = \"").and_then(|r| r.strip_suffix('"'))).unwrap_or("0.0.0").to_string();
match (sub, value) {
(Some("set"), Some(v)) => {
let valid = v.split(['-', '+']).next().is_some_and(|core| core.split('.').count() == 3 && core.split('.').all(|p| p.parse::<u64>().is_ok()));
if !valid {
eprintln!("version must look like 1.2.3 or 1.2.3-rc.1");
return false;
}
let new = text.replacen(&format!("version = \"{cur}\""), &format!("version = \"{v}\""), 1);
if std::fs::write(&path, new).is_err() {
return false;
}
eprintln!("version {cur} → {v}");
run(cargo().args(["update", "--workspace"]))
}
_ => {
println!("{cur}");
true
}
}
}