Skip to main content

codegen/
app.rs

1//! CLI orchestration for manifest-authored engine content.
2
3mod cache;
4mod generation;
5mod identifiers;
6mod output_tree;
7mod ownership;
8mod spec_file;
9
10use std::process::ExitCode;
11
12use cache::{CodegenCache, ManifestInputs};
13use clap::Parser;
14use generation::generate_all_incremental;
15use output_tree::{check_freshness, write_generated_files};
16use wowlab_common::{cli, output};
17use wowlab_fs::path::PathBuf;
18use wowlab_manifest_schema::ManifestRepository;
19
20#[derive(Parser)]
21#[command(name = "codegen", about = "Wowlab spec codegen tool")]
22struct Args {
23    /// Directory containing TOML spec manifest files.
24    #[arg(long)]
25    manifests: PathBuf,
26
27    /// Directory to write generated Rust files into.
28    #[arg(long)]
29    output: PathBuf,
30
31    /// Suppress output except for errors.
32    #[arg(long, short)]
33    quiet: bool,
34
35    /// Check that generated code is up-to-date without writing. Exits non-zero if stale.
36    #[arg(long)]
37    check: bool,
38}
39
40fn run(args: &Args) -> anyhow::Result<ExitCode> {
41    let app = cli::boot(
42        "codegen",
43        env!("CARGO_PKG_VERSION"),
44        args.quiet || args.check,
45        "CODEGEN_ROOT",
46    );
47    let repository = ManifestRepository::new(args.manifests.clone());
48    let manifest_paths = repository.toml_paths()?;
49    let inputs = ManifestInputs::capture(&args.manifests, &manifest_paths)?;
50    let cache = CodegenCache::open(&app.root, &args.manifests, &args.output, &inputs);
51
52    if cache.is_complete_hit()? {
53        if !args.quiet || args.check {
54            output::success("generated code is up-to-date");
55        }
56
57        return Ok(ExitCode::SUCCESS);
58    }
59
60    let generation = generate_all_incremental(&args.manifests, &inputs, Some(&cache))?;
61
62    if args.check {
63        let exit_code = check_freshness(&generation.files, &args.output)?;
64
65        if exit_code == ExitCode::SUCCESS {
66            persist_cache(&cache, &generation, args.quiet)?;
67        }
68
69        return Ok(exit_code);
70    }
71
72    write_generated_files(&generation.files, &args.output, args.quiet)?;
73    persist_cache(&cache, &generation, args.quiet)?;
74
75    if !args.quiet {
76        let spec_count = generation
77            .files
78            .iter()
79            .filter(|(file, _)| file.starts_with("specs/") && file != "specs/mod.rs")
80            .count();
81
82        output::success(&format!(
83            "wrote {} spec files + specs/mod.rs + items/mod.rs + mod.rs to {}",
84            spec_count,
85            args.output.display()
86        ));
87    }
88
89    Ok(ExitCode::SUCCESS)
90}
91
92fn persist_cache(
93    cache: &CodegenCache,
94    generation: &generation::GenerationResult,
95    quiet: bool,
96) -> anyhow::Result<()> {
97    let Some(update) = cache.prepare_update(generation)? else {
98        return Ok(());
99    };
100
101    if let Err(error) = update.persist()
102        && !quiet
103    {
104        output::warning(&format!("codegen cache was not persisted: {error:#}"));
105    }
106
107    Ok(())
108}
109
110pub(crate) fn main() -> ExitCode {
111    let args = Args::parse();
112
113    match run(&args) {
114        Ok(code) => code,
115        Err(error) => {
116            output::error(&format!("{error:#}"));
117
118            ExitCode::FAILURE
119        }
120    }
121}
122
123#[cfg(test)]
124#[path = "app/tests.rs"]
125mod tests;