feat(ariadne): add CLI skeleton
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user