mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-28 20:17:18 -06:00
167 lines
7.8 KiB
Rust
167 lines
7.8 KiB
Rust
//! Workspace tooling: `cargo xtask <command>`.
|
|
//!
|
|
//! Pure Rust (std + serde_json; flate2/brotli for the web bundle). External tools (`cargo`, `curl`, `tar`) are
|
|
//! invoked through `std::process::Command`.
|
|
|
|
mod assets;
|
|
mod bench;
|
|
mod ci_lock;
|
|
mod ci_quick;
|
|
mod ci_report;
|
|
mod ico;
|
|
mod layers;
|
|
mod parity;
|
|
mod stats;
|
|
mod version;
|
|
mod web;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, ExitCode};
|
|
|
|
mod ci;
|
|
mod corpus;
|
|
mod raw_samples;
|
|
mod wasm;
|
|
|
|
use ci::{available_ram_mb, ci_jobs, cmd_ci, total_ram_gb};
|
|
use corpus::cmd_corpus;
|
|
use layers::cmd_layers;
|
|
use wasm::cmd_wasm;
|
|
|
|
const USAGE: &str = "\
|
|
usage: cargo xtask <command>
|
|
|
|
commands:
|
|
assets every image/icon/font/media file is attributed in assets/ATTRIBUTION.md; no Adobe assets
|
|
bench [FILE] [--strict] [--threshold PCT]
|
|
run the render benchmark, append to target/bench/history.jsonl, compare CPU time with
|
|
the previous run (default input: corpus/raw/arw-sony-a7m3-compressed.arw)
|
|
ico <out.ico> <in.png>...
|
|
pack square PNGs (<= 256 px) into a Windows .ico (see packaging/icons.sh)
|
|
layers enforce the crate dependency layering (plan/architecture.md §3)
|
|
parity [--write]
|
|
check docs/parity-checklist.md (every cmd:/ctl: id and path it cites exists) and print the
|
|
Lightroom parity summary; --write refreshes the summary table in the document
|
|
wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates (+ the web app), in one call
|
|
web [--serve [port]] [--dev]
|
|
build the browser app (apps/lightcraft-web) into <target>/web-site/;
|
|
--serve serves it on http://127.0.0.1:<port> (default 8080)
|
|
ci [--hardware] [--summary-md] [--quick [--base REF]]
|
|
fmt --check, clippy -D warnings, heif, test, GPU probe, parity refs, layers, assets, wasm
|
|
(stops at first failure). Tests run under cargo test (cargo nextest with
|
|
LIGHTCRAFT_NEXTEST=1), without examples, then doctests with cargo test --doc; after
|
|
them it prints the translation gaps report (never a failure). Writes
|
|
target/ci/report.json (machine, GPU adapters, data,
|
|
steps, skipped tests) and target/ci/summary.md; --hardware also runs the GPU tests on
|
|
every backend with a hardware adapter; --summary-md prints the Markdown summary.
|
|
--quick: only what the changes since the merge base with origin/main (or REF) can
|
|
break: clippy, the changed packages' tests, and heif / parity / wasm when they apply.
|
|
One ci runs at a time per machine; others wait (LIGHTCRAFT_CI_SLOTS=N for N at once,
|
|
LIGHTCRAFT_CI_LOCK=off to skip the lock)
|
|
corpus [--download]
|
|
show where test corpora live; --download fetches PngSuite and CC0 raw samples (raw.pixls.us) into corpus/ and checks their sha256
|
|
stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`)
|
|
raw-frame [--json] FILE…
|
|
measure where each raw's embedded camera JPEG sits in its sensor data, which edge bands hold no
|
|
picture and what the decoder's crop keeps beyond the camera frame (any make with a full-size
|
|
embedded JPEG); print image-area entries for Nikon bodies that record no crop (MEASURED_BODIES in
|
|
crates/raw/src/vendor/nef.rs)
|
|
";
|
|
|
|
fn main() -> ExitCode {
|
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
|
let rest: Vec<&str> = args.iter().skip(1).map(String::as_str).collect();
|
|
let result = match args.first().map(String::as_str) {
|
|
Some("ico") => ico::run(&rest),
|
|
Some("version") => version::run(&root(), &rest),
|
|
Some("layers") => cmd_layers(),
|
|
Some("assets") => assets::run(&root()),
|
|
Some("bench") => bench::run(&root(), &rest),
|
|
Some("parity") => parity::run(&root(), rest.contains(&"--write")),
|
|
Some("wasm") => cmd_wasm(),
|
|
Some("web") => web::run(&rest),
|
|
Some("ci") => cmd_ci(&rest),
|
|
Some("corpus") => cmd_corpus(rest.contains(&"--download")),
|
|
Some("stats") => stats::run(&root(), rest.contains(&"--exact")),
|
|
Some("raw-frame") => cmd_raw_frame(&rest),
|
|
Some("-h" | "--help" | "help") | None => {
|
|
print!("{USAGE}");
|
|
Ok(())
|
|
}
|
|
Some(other) => Err(format!("unknown command `{other}`\n\n{USAGE}")),
|
|
};
|
|
match result {
|
|
Ok(()) => ExitCode::SUCCESS,
|
|
Err(e) => {
|
|
eprintln!("error: {e}");
|
|
ExitCode::FAILURE
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `cargo xtask raw-frame FILE…`: runs the `raw_frame` example of `lightcraft-engine` on the files.
|
|
fn cmd_raw_frame(files: &[&str]) -> Result<(), String> {
|
|
if files.is_empty() {
|
|
return Err("raw-frame needs one or more raw files (several per camera, with detail across the frame)".into());
|
|
}
|
|
eprintln!("$ cargo run --release -p lightcraft-engine --example raw_frame -- {}", files.join(" "));
|
|
let status = cargo()
|
|
.args(["run", "--release", "-q", "-p", "lightcraft-engine", "--example", "raw_frame", "--"])
|
|
.args(files)
|
|
.status()
|
|
.map_err(|e| format!("raw_frame: {e}"))?;
|
|
if status.success() { Ok(()) } else { Err("raw_frame failed".into()) }
|
|
}
|
|
|
|
/// Workspace root (parent of the xtask crate).
|
|
pub fn root() -> PathBuf {
|
|
// `cargo run` sets it at run time; prefer that over the compile-time value, which goes stale
|
|
// when the checkout moves and the cached xtask binary isn't rebuilt
|
|
let dir = std::env::var_os("CARGO_MANIFEST_DIR").map(PathBuf::from).filter(|d| d.join("Cargo.toml").is_file());
|
|
let dir = dir.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")));
|
|
dir.parent().expect("xtask has a parent dir").to_path_buf()
|
|
}
|
|
|
|
pub fn cargo() -> Command {
|
|
let mut c = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into()));
|
|
c.current_dir(root());
|
|
// Full Windows debuginfo plus one linker per CPU can exhaust RAM before any
|
|
// tests run. Scope these overridable defaults to CI, including parity/WASM
|
|
// subprocesses, without changing ordinary developer builds or GPU coverage.
|
|
if std::env::args().nth(1).as_deref() == Some("ci") {
|
|
static JOBS: std::sync::OnceLock<(String, String)> = std::sync::OnceLock::new();
|
|
let (build, threads) = JOBS.get_or_init(|| {
|
|
let ram_mb = available_ram_mb().or_else(|| total_ram_gb().map(|gb| gb.saturating_mul(1024) / 2));
|
|
let jobs = ci_jobs(ram_mb, std::thread::available_parallelism().map_or(4, |n| n.get()));
|
|
// More test threads barely shorten CI (a few heavy tests dominate) but make the
|
|
// wall-clock frame-budget tests flaky on a loaded machine.
|
|
(jobs.to_string(), jobs.min(4).to_string())
|
|
});
|
|
for (key, value) in [
|
|
("CARGO_PROFILE_DEV_DEBUG", "line-tables-only"),
|
|
("CARGO_BUILD_JOBS", build.as_str()),
|
|
("RUST_TEST_THREADS", threads.as_str()),
|
|
("NEXTEST_TEST_THREADS", threads.as_str()),
|
|
] {
|
|
if std::env::var_os(key).is_none() {
|
|
c.env(key, value);
|
|
}
|
|
}
|
|
}
|
|
c
|
|
}
|
|
|
|
pub fn run(mut cmd: Command, what: &str) -> Result<(), String> {
|
|
eprintln!("$ {what}");
|
|
let status = cmd.status().map_err(|e| format!("{what}: failed to spawn: {e}"))?;
|
|
if status.success() { Ok(()) } else { Err(format!("{what}: exited with {status}")) }
|
|
}
|
|
|
|
pub fn metadata() -> Result<serde_json::Value, String> {
|
|
let out = cargo().args(["metadata", "--format-version", "1", "--no-deps"]).output().map_err(|e| format!("cargo metadata: {e}"))?;
|
|
if !out.status.success() {
|
|
return Err(format!("cargo metadata failed:\n{}", String::from_utf8_lossy(&out.stderr)));
|
|
}
|
|
serde_json::from_slice(&out.stdout).map_err(|e| format!("cargo metadata: bad JSON: {e}"))
|
|
}
|