From 45ff66947ad3272224fc360b44fa0d43153720e1 Mon Sep 17 00:00:00 2001 From: alexion Date: Tue, 8 Sep 2026 19:58:45 -0400 Subject: [PATCH] feat(ariadne): add CLI skeleton --- src/ariadne/BUILD | 5 +- src/ariadne/README.md | 30 ++++------ src/ariadne/src/lib.rs | 129 +++++++++++++++++++++++++++++++++++++++- src/ariadne/src/main.rs | 28 ++++++++- 4 files changed, 170 insertions(+), 22 deletions(-) diff --git a/src/ariadne/BUILD b/src/ariadne/BUILD index c3f343f..22e5815 100644 --- a/src/ariadne/BUILD +++ b/src/ariadne/BUILD @@ -10,7 +10,10 @@ rust_binary( name = "ariadne", crate_name = "ariadne_cli", srcs = ["src/main.rs"], - deps = [":ariadne_lib"], + deps = [ + ":ariadne_lib", + "//third_party/crates:clap", + ], ) sh_binary( diff --git a/src/ariadne/README.md b/src/ariadne/README.md index bbadaaa..b0474dc 100644 --- a/src/ariadne/README.md +++ b/src/ariadne/README.md @@ -1,27 +1,23 @@ # Ariadne -Ariadne prepares Ren'Py game source for later analysis. +Ariadne analyzes Clew output for a local Ren'Py game install. -## Bazel shape +Keep project-owned source under `src/ariadne/`. +Keep game-derived files local and ignored under `src/ariadne/output/`. -Keep project-owned source under `src/ariadne/`. Keep game-derived files local and ignored under -`src/ariadne/output/`. +Clew owns extraction, preparation, and Ren'Py AST export. +Ariadne consumes Clew's `clew-renpy-ast-v1` JSON from `output/ast/`. +Ariadne maps that data into internal analysis state. -The first target is a shell entry point: +## CLI skeleton + +Run all declared stages with: ```sh -bazel run //src/ariadne:extract_and_decompile -- \ - --game-root /path/to/game-install +bazel run //src/ariadne:ariadne -- --output-dir output/ ``` -The script defaults to `/src/ariadne/output`. -This path stays stable when you run the command from inside `src/ariadne`. -Use the game installation directory as the game root. -One run processes all installed season directories. -Extracted archive source keeps the archive path relative to the game root, so files with the same -internal path from different season installs do not overwrite each other. -If `rpycdec` is not available, the script uses `extract-tools.nix` to enter a Nix shell. +The skeleton currently declares one `parse` stage. +It writes a placeholder artifact under `output/graph/parse/`. -Keep the script as a thin orchestration layer. -Bazel owns stable inputs and wrappers. -Keep large game files and recovered output outside the build graph. +Use `--force` to remove stage output before rerunning the stage. diff --git a/src/ariadne/src/lib.rs b/src/ariadne/src/lib.rs index 8f9e795..3f7acde 100644 --- a/src/ariadne/src/lib.rs +++ b/src/ariadne/src/lib.rs @@ -1,3 +1,128 @@ -pub fn run() { - println!("Ariadne"); +use std::collections::HashSet; +use std::fs; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; + +pub type Result = std::result::Result; + +#[derive(Debug)] +pub enum Error { + Io(io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Io(error) => write!(formatter, "{error}"), + } + } +} + +impl std::error::Error for Error {} + +impl From for Error { + fn from(error: io::Error) -> Self { + Error::Io(error) + } +} + +#[derive(Debug, Clone)] +pub struct Config { + pub output_dir: PathBuf, + pub force: bool, +} + +impl Config { + pub fn new(output_dir: PathBuf, force: bool) -> Self { + Self { output_dir, force } + } +} + +#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)] +pub enum Stage { + Parse, +} + +impl Stage { + pub const ALL: &'static [Stage] = &[Stage::Parse]; + + pub fn name(self) -> &'static str { + match self { + Stage::Parse => "parse", + } + } + + pub fn prerequisites(self) -> &'static [Stage] { + match self { + Stage::Parse => &[], + } + } +} + +pub fn run(config: &Config) -> Result<()> { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + run_with_writer(config, &mut handle) +} + +pub fn run_with_writer(config: &Config, writer: &mut dyn Write) -> Result<()> { + for stage in plan_stages(Stage::ALL) { + run_stage(stage, config, writer)?; + } + Ok(()) +} + +fn plan_stages(stages: &[Stage]) -> Vec { + fn visit(stage: Stage, seen: &mut HashSet, planned: &mut Vec) { + if !seen.insert(stage) { + return; + } + for prerequisite in stage.prerequisites() { + visit(*prerequisite, seen, planned); + } + planned.push(stage); + } + + let mut seen = HashSet::new(); + let mut planned = Vec::new(); + for stage in stages { + visit(*stage, &mut seen, &mut planned); + } + planned +} + +fn run_stage(stage: Stage, config: &Config, writer: &mut dyn Write) -> Result<()> { + let stage_dir = stage_dir(&config.output_dir, stage); + + if stage_dir.exists() { + if config.force { + fs::remove_dir_all(&stage_dir)?; + } else { + writeln!( + writer, + "SKIP {}: output exists at {}", + stage.name(), + stage_dir.display() + )?; + return Ok(()); + } + } + + writeln!(writer, "RUN {}", stage.name())?; + match stage { + Stage::Parse => run_parse(&stage_dir, writer)?, + } + Ok(()) +} + +fn stage_dir(output_dir: &Path, stage: Stage) -> PathBuf { + output_dir.join("graph").join(stage.name()) +} + +fn run_parse(stage_dir: &Path, writer: &mut dyn Write) -> Result<()> { + fs::create_dir_all(stage_dir)?; + let summary_path = stage_dir.join("summary.txt"); + fs::write(&summary_path, "parse stage placeholder\n")?; + writeln!(writer, "WRITE {}", summary_path.display())?; + Ok(()) } diff --git a/src/ariadne/src/main.rs b/src/ariadne/src/main.rs index 41b95fa..4b23101 100644 --- a/src/ariadne/src/main.rs +++ b/src/ariadne/src/main.rs @@ -1,3 +1,27 @@ -fn main() { - ariadne::run(); +use std::path::PathBuf; +use std::process::ExitCode; + +use clap::Parser; + +#[derive(Debug, Parser)] +#[command(name = "ariadne", about = "Analyze Clew output for a Ren'Py game.")] +struct Cli { + #[arg(long, default_value = "output")] + output_dir: PathBuf, + + #[arg(long)] + force: bool, +} + +fn main() -> ExitCode { + let cli = Cli::parse(); + let config = ariadne::Config::new(cli.output_dir, cli.force); + + match ariadne::run(&config) { + Ok(()) => ExitCode::SUCCESS, + Err(error) => { + eprintln!("ERROR: {error}"); + ExitCode::FAILURE + } + } }