feat(ariadne): add CLI skeleton

This commit is contained in:
2026-09-08 19:58:45 -04:00
parent 8855b6d379
commit 45ff66947a
4 changed files with 170 additions and 22 deletions

View File

@@ -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(

View File

@@ -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 `<workspace>/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.

View File

@@ -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<T> = std::result::Result<T, Error>;
#[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<io::Error> 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<Stage> {
fn visit(stage: Stage, seen: &mut HashSet<Stage>, planned: &mut Vec<Stage>) {
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(())
}

View File

@@ -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
}
}
}