//! `cph` — the command-line entrypoint for the checker. //! //! Owned by **WU-5**. Parses args, constructs the typst [`Engine`], runs the //! `cph-check` pipeline against an engineering file, and prints diagnostics. //! //! Convention: **diagnostics go to stderr, results go to stdout.** `check` //! exits 1 when there is any `Error`-severity diagnostic (warnings alone exit //! 0); `build` exits 1 when the PDF could not be produced. use std::path::PathBuf; use std::process::ExitCode; use clap::{Parser, Subcommand}; use cph_check::CheckReport; use cph_typst::Engine; /// The `cph` checker for curriculum engineering files. #[derive(Debug, Parser)] #[command(name = "cph", version, about, long_about = None)] struct Cli { /// Override the `cph-render` package directory (the folder containing /// `lib.typ` / `typst.toml`). Defaults to the engine's own resolution /// (`CPH_RENDER_DIR` env var, else the repo `render/`). #[arg(long, global = true, value_name = "DIR")] render_dir: Option, #[command(subcommand)] command: Command, } #[derive(Debug, Subcommand)] enum Command { /// Run the full check pipeline and print diagnostics. Exits 1 on any error. Check { /// Path to the engineering-file root (the folder with `manifest.toml`). path: PathBuf, }, /// Build a PDF for a render target. Exits 1 if the build fails. Build { /// Path to the engineering-file root (the folder with `manifest.toml`). path: PathBuf, /// Render target to export. #[arg(long, default_value = "student")] target: String, /// Output PDF path. Defaults to `/build/.pdf`. #[arg(short = 'o', long, value_name = "OUT")] out: Option, }, } fn main() -> ExitCode { let cli = Cli::parse(); let engine = match &cli.render_dir { Some(dir) => Engine::with_render_dir(dir.clone()), None => Engine::new(), }; match cli.command { Command::Check { path } => run_check(&path, &engine), Command::Build { path, target, out } => run_build(&path, &engine, &target, out), } } /// Print every diagnostic in `report` to stderr, followed by a summary line. fn print_diagnostics(report: &CheckReport) { for d in &report.diagnostics { eprintln!("{d}"); } eprintln!( "{} error{}, {} warning{}", report.error_count(), plural(report.error_count()), report.warning_count(), plural(report.warning_count()), ); } fn plural(n: usize) -> &'static str { if n == 1 { "" } else { "s" } } fn run_check(path: &std::path::Path, engine: &Engine) -> ExitCode { let report = cph_check::check(path, engine); print_diagnostics(&report); if report.has_errors() { ExitCode::FAILURE } else { println!("check passed: {}", path.display()); ExitCode::SUCCESS } } fn run_build( path: &std::path::Path, engine: &Engine, target: &str, out: Option, ) -> ExitCode { let out_path = out.unwrap_or_else(|| path.join("build").join(format!("{target}.pdf"))); let (pdf, report) = cph_check::build(path, engine, target); print_diagnostics(&report); match pdf { Some(bytes) => { if let Some(parent) = out_path.parent() { if let Err(e) = std::fs::create_dir_all(parent) { eprintln!( "error: cannot create output directory '{}': {e}", parent.display() ); return ExitCode::FAILURE; } } if let Err(e) = std::fs::write(&out_path, &bytes) { eprintln!("error: cannot write '{}': {e}", out_path.display()); return ExitCode::FAILURE; } println!("wrote {} ({} bytes)", out_path.display(), bytes.len()); ExitCode::SUCCESS } None => { eprintln!("build failed: {} errors", report.error_count()); ExitCode::FAILURE } } }