feat(ariadne): add pipeline skeleton

This commit is contained in:
2026-09-09 16:51:11 -04:00
parent 0718ca1d53
commit 5ccdc1db54
4 changed files with 179 additions and 99 deletions

View File

@@ -3,7 +3,10 @@ load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library")
rust_library(
name = "ariadne_lib",
crate_name = "ariadne",
srcs = ["lib.rs"],
srcs = [
"lib.rs",
"pipeline.rs",
],
)
rust_binary(

View File

@@ -1,7 +1,12 @@
use std::collections::HashSet;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::io;
use std::path::PathBuf;
use crate::pipeline::Stage;
pub mod pipeline;
const OUTPUT_GRAPH_DIR: &str = "graph";
pub type Result<T> = io::Result<T>;
@@ -9,99 +14,46 @@ pub type Result<T> = io::Result<T>;
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 target_stage: pipeline::Stage,
}
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)?;
for stage in config.target_stage.with_prerequisites() {
run_stage(config, stage)?;
}
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;
fn get_output_dir(config: &Config, stage: Stage) -> Result<PathBuf> {
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()
),
));
}
for prerequisite in stage.prerequisites() {
visit(*prerequisite, seen, planned);
}
planned.push(stage);
fs::remove_dir_all(&output_dir)?;
}
let mut seen = HashSet::new();
let mut planned = Vec::new();
for stage in stages {
visit(*stage, &mut seen, &mut planned);
}
planned
fs::create_dir_all(&output_dir)?;
Ok(output_dir)
}
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(())
fn run_stage(config: &Config, stage: Stage) -> Result<()> {
let output_dir = get_output_dir(config, stage)?;
pipeline::run_stage(stage, &output_dir)
}

View File

@@ -1,27 +1,51 @@
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 {
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,
}
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
impl From<Args> 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, String> {
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)
}

101
src/ariadne/pipeline.rs Normal file
View File

@@ -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<Stage> {
self.with_prerequisites_impl(BTreeSet::new())
}
fn with_prerequisites_impl(self, seen: BTreeSet<Stage>) -> Vec<Stage> {
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(())
}