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", name = "ariadne",
crate_name = "ariadne_cli", crate_name = "ariadne_cli",
srcs = ["src/main.rs"], srcs = ["src/main.rs"],
deps = [":ariadne_lib"], deps = [
":ariadne_lib",
"//third_party/crates:clap",
],
) )
sh_binary( sh_binary(

View File

@@ -1,27 +1,23 @@
# Ariadne # 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 Clew owns extraction, preparation, and Ren'Py AST export.
`src/ariadne/output/`. 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 ```sh
bazel run //src/ariadne:extract_and_decompile -- \ bazel run //src/ariadne:ariadne -- --output-dir output/
--game-root /path/to/game-install
``` ```
The script defaults to `<workspace>/src/ariadne/output`. The skeleton currently declares one `parse` stage.
This path stays stable when you run the command from inside `src/ariadne`. It writes a placeholder artifact under `output/graph/parse/`.
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.
Keep the script as a thin orchestration layer. Use `--force` to remove stage output before rerunning the stage.
Bazel owns stable inputs and wrappers.
Keep large game files and recovered output outside the build graph.

View File

@@ -1,3 +1,128 @@
pub fn run() { use std::collections::HashSet;
println!("Ariadne"); 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() { use std::path::PathBuf;
ariadne::run(); 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
}
}
} }