From 3538297991b54462e14afb26dd685558f18c28ef Mon Sep 17 00:00:00 2001 From: alexion Date: Wed, 9 Sep 2026 18:23:29 -0400 Subject: [PATCH] feat(ariadne): add CLI skeleton --- src/ariadne/BUILD | 12 +++-- src/ariadne/Cargo.toml | 7 +++ src/ariadne/README.md | 51 ++++++++++++++++---- src/ariadne/lib.rs | 59 +++++++++++++++++++++++ src/ariadne/main.rs | 51 ++++++++++++++++++++ src/ariadne/pipeline.rs | 101 ++++++++++++++++++++++++++++++++++++++++ src/ariadne/src/lib.rs | 3 -- src/ariadne/src/main.rs | 3 -- 8 files changed, 270 insertions(+), 17 deletions(-) create mode 100644 src/ariadne/lib.rs create mode 100644 src/ariadne/main.rs create mode 100644 src/ariadne/pipeline.rs delete mode 100644 src/ariadne/src/lib.rs delete mode 100644 src/ariadne/src/main.rs diff --git a/src/ariadne/BUILD b/src/ariadne/BUILD index 276d40d..cc0ca14 100644 --- a/src/ariadne/BUILD +++ b/src/ariadne/BUILD @@ -3,13 +3,19 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library") rust_library( name = "ariadne_lib", crate_name = "ariadne", - srcs = ["src/lib.rs"], + srcs = [ + "lib.rs", + "pipeline.rs", + ], ) rust_binary( name = "ariadne", crate_name = "ariadne_cli", - srcs = ["src/main.rs"], - deps = [":ariadne_lib"], + srcs = ["main.rs"], + deps = [ + ":ariadne_lib", + "//third_party/crates:clap", + ], ) diff --git a/src/ariadne/Cargo.toml b/src/ariadne/Cargo.toml index 73de523..9479df0 100644 --- a/src/ariadne/Cargo.toml +++ b/src/ariadne/Cargo.toml @@ -3,6 +3,13 @@ name = "ariadne" version = "0.1.0" edition = "2024" +[lib] +path = "lib.rs" + +[[bin]] +name = "ariadne" +path = "main.rs" + [dependencies] clap = { version = "4", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } diff --git a/src/ariadne/README.md b/src/ariadne/README.md index efc85c6..278f761 100644 --- a/src/ariadne/README.md +++ b/src/ariadne/README.md @@ -1,18 +1,53 @@ # Ariadne -Ariadne analyzes Clew output for a local Ren'Py game install. - -Keep project-owned source under `src/ariadne/`. -Keep game-derived files local and ignored under `src/ariadne/output/`. +Ariadne analyzes Clew-produced data for local Ren'Py projects. 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. +Ariadne consumes Clew output and builds analysis artifacts from it. +Keep project-owned source under `src/ariadne/`; keep generated or game-derived files out of Git. -## Bazel shape +## CLI -Run the current placeholder CLI with: +Run the default target, `all`: ```sh bazel run //src/ariadne:ariadne ``` + +By default, Ariadne writes output under `output/`. +Use `--output-dir` to choose a different output directory: + +```sh +bazel run //src/ariadne:ariadne -- --output-dir scratch/ariadne-output +``` + +Run through a specific target stage and its prerequisites: + +```sh +bazel run //src/ariadne:ariadne -- parse +bazel run //src/ariadne:ariadne -- add-jump-edges +``` + +Available target stages: + +- `all` +- `parse` +- `add-implicit-edges` +- `add-init-edges` +- `add-jump-edges` + +Relative `--output-dir` paths resolve from the workspace root when Ariadne runs through `bazel run`. + +## Pipeline skeleton + +Ariadne currently has pipeline scaffolding only. +Each executable stage prepares an output directory at: + +```text +/graph// +``` + +If a stage output directory already exists and is not empty, Ariadne fails unless `--force` is set. +With `--force`, Ariadne removes the existing stage output directory before recreating it. + +Stage implementations are placeholders and do not write graph artifacts yet. diff --git a/src/ariadne/lib.rs b/src/ariadne/lib.rs new file mode 100644 index 0000000..d05bbb6 --- /dev/null +++ b/src/ariadne/lib.rs @@ -0,0 +1,59 @@ +use std::fs; +use std::io; +use std::path::PathBuf; + +use crate::pipeline::Stage; + +pub mod pipeline; + +const OUTPUT_GRAPH_DIR: &str = "graph"; + +pub type Result = io::Result; + +#[derive(Debug, Clone)] +pub struct Config { + pub output_dir: PathBuf, + pub force: bool, + pub target_stage: pipeline::Stage, +} + +pub fn run(config: &Config) -> Result<()> { + for stage in config.target_stage.with_prerequisites() { + run_stage(config, stage)?; + } + + Ok(()) +} + +fn get_output_dir(config: &Config, stage: Stage) -> Result { + let output_dir = config.output_dir.join(OUTPUT_GRAPH_DIR).join(stage.name()); + + if output_dir.exists() && !output_dir.is_dir() { + return Err(io::Error::new( + io::ErrorKind::NotADirectory, + format!("output path is not a directory: {path}", path = output_dir.display()), + )); + } + + if output_dir.is_dir() && fs::read_dir(&output_dir)?.next().transpose()?.is_some() { + if !config.force { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + format!( + "output directory already exists and is not empty: {path}", + path = output_dir.display() + ), + )); + } + + fs::remove_dir_all(&output_dir)?; + } + + fs::create_dir_all(&output_dir)?; + Ok(output_dir) +} + +fn run_stage(config: &Config, stage: Stage) -> Result<()> { + let output_dir = get_output_dir(config, stage)?; + pipeline::run_stage(stage, &output_dir) +} diff --git a/src/ariadne/main.rs b/src/ariadne/main.rs new file mode 100644 index 0000000..f364e12 --- /dev/null +++ b/src/ariadne/main.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; + +use clap::Parser; + +#[derive(Debug, Parser)] +struct Args { + #[arg(long, default_value = "output")] + output_dir: PathBuf, + + #[arg(long)] + force: bool, + + #[arg(default_value = "all", value_parser = parse_stage)] + target_stage: ariadne::pipeline::Stage, +} + +impl From for ariadne::Config { + fn from(args: Args) -> Self { + ariadne::Config { + output_dir: resolve_workspace_path(args.output_dir), + force: args.force, + target_stage: args.target_stage, + } + } +} + +fn parse_stage(value: &str) -> Result { + ariadne::pipeline::Stage::ALL + .iter() + .copied() + .find(|stage| stage.name() == value) + .ok_or_else(|| format!("unknown stage: {value}")) +} + +// TODO: Move this into a shared Rust utility crate. +fn resolve_workspace_path(path: PathBuf) -> PathBuf { + if path.is_absolute() { + return path; + } + + if let Some(workspace) = std::env::var_os("BUILD_WORKSPACE_DIRECTORY") { + return PathBuf::from(workspace).join(path); + } + + path +} + +fn main() -> ariadne::Result<()> { + let config: ariadne::Config = Args::parse().into(); + ariadne::run(&config) +} diff --git a/src/ariadne/pipeline.rs b/src/ariadne/pipeline.rs new file mode 100644 index 0000000..e65215c --- /dev/null +++ b/src/ariadne/pipeline.rs @@ -0,0 +1,101 @@ +use std::collections::BTreeSet; +use std::path::Path; + +use crate::Result; + +#[derive(Debug, Default, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)] +pub enum Stage { + #[default] + All, + + Parse, + AddImplicitEdges, + AddInitEdges, + AddJumpEdges, +} + +impl Stage { + pub const ALL: &'static [Stage] = &[ + Stage::All, + Stage::Parse, + Stage::AddImplicitEdges, + Stage::AddInitEdges, + Stage::AddJumpEdges, + ]; + + pub fn name(self) -> &'static str { + match self { + Stage::All => "all", + Stage::Parse => "parse", + Stage::AddImplicitEdges => "add-implicit-edges", + Stage::AddInitEdges => "add-init-edges", + Stage::AddJumpEdges => "add-jump-edges", + } + } + + pub fn prerequisites(self) -> &'static [Stage] { + match self { + Stage::All => &[ + Stage::AddImplicitEdges, + Stage::AddInitEdges, + Stage::AddJumpEdges, + ], + Stage::Parse => &[], + Stage::AddImplicitEdges => &[Stage::Parse], + Stage::AddInitEdges => &[Stage::Parse], + Stage::AddJumpEdges => &[Stage::Parse], + } + } + + pub fn with_prerequisites(self) -> Vec { + self.with_prerequisites_impl(BTreeSet::new()) + } + + fn with_prerequisites_impl(self, seen: BTreeSet) -> Vec { + if seen.contains(&self) { + return Vec::new(); + } + + let mut seen = seen; + seen.insert(self); + + let mut stages = Vec::new(); + for prerequisite in self.prerequisites() { + let prerequisite_stages = prerequisite.with_prerequisites_impl(seen.clone()); + seen.extend(prerequisite_stages.iter().copied()); + stages.extend(prerequisite_stages); + } + + if self != Stage::All { + stages.push(self); + } + + stages + } +} + +pub fn run_stage(stage: Stage, output_dir: &Path) -> Result<()> { + match stage { + Stage::All => Ok(()), + Stage::Parse => run_parse(output_dir), + Stage::AddImplicitEdges => run_add_implicit_edges(output_dir), + Stage::AddInitEdges => run_add_init_edges(output_dir), + Stage::AddJumpEdges => run_add_jump_edges(output_dir), + } +} + +fn run_parse(_output_dir: &Path) -> Result<()> { + Ok(()) +} + +fn run_add_implicit_edges(_output_dir: &Path) -> Result<()> { + Ok(()) +} + +fn run_add_init_edges(_output_dir: &Path) -> Result<()> { + Ok(()) +} + +fn run_add_jump_edges(_output_dir: &Path) -> Result<()> { + Ok(()) +} diff --git a/src/ariadne/src/lib.rs b/src/ariadne/src/lib.rs deleted file mode 100644 index 8f9e795..0000000 --- a/src/ariadne/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn run() { - println!("Ariadne"); -} diff --git a/src/ariadne/src/main.rs b/src/ariadne/src/main.rs deleted file mode 100644 index 41b95fa..0000000 --- a/src/ariadne/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - ariadne::run(); -} -- 2.47.3