mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-28 20:36:55 -06:00
158 lines
6.0 KiB
Rust
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
|
|
}
|
|
}
|
|
}
|