diff --git a/src/ariadne/BUILD b/src/ariadne/BUILD index 48315d8..cc0ca14 100644 --- a/src/ariadne/BUILD +++ b/src/ariadne/BUILD @@ -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( diff --git a/src/ariadne/lib.rs b/src/ariadne/lib.rs index a23853f..d05bbb6 100644 --- a/src/ariadne/lib.rs +++ b/src/ariadne/lib.rs @@ -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 = io::Result; @@ -9,99 +14,46 @@ pub type Result = io::Result; 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 { - fn visit(stage: Stage, seen: &mut HashSet, planned: &mut Vec) { - if !seen.insert(stage) { - return; +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() + ), + )); } - 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) } diff --git a/src/ariadne/main.rs b/src/ariadne/main.rs index 4b23101..f364e12 100644 --- a/src/ariadne/main.rs +++ b/src/ariadne/main.rs @@ -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 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(()) +}