diff --git a/src/runner/generation.rs b/src/runner/generation.rs new file mode 100644 index 00000000..900ac2ce --- /dev/null +++ b/src/runner/generation.rs @@ -0,0 +1,62 @@ +//! Pure Ninja-generation steps for the runner. +//! +//! Generation decomposes into three query-style steps — load the manifest, +//! build the graph, generate the Ninja text — none of which require a +//! [`crate::status::StatusReporter`]. Progress reporting stays in the thin +//! orchestration wrappers in [`super`] (`generate_ninja`, +//! `load_manifest_with_stage_reporting`), so generation can be reused as a +//! pure operation (for example for dry runs or background generation). + +use anyhow::{Context, Result}; +use camino::Utf8Path; + +use super::NinjaContent; +use crate::ast::NetsukeManifest; +use crate::ir::BuildGraph; +use crate::localization::{self, keys}; +use crate::stdlib::NetworkPolicy; +use crate::{manifest, ninja_gen}; + +/// Optional observer for manifest-loading stages. +/// +/// Callers that want progress reporting pass a callback translating +/// [`manifest::ManifestLoadStage`] values into their own reporting; passing +/// `None` keeps the pipeline free of side effects. +pub(super) type StageObserver<'a> = Option<&'a mut dyn FnMut(manifest::ManifestLoadStage)>; + +/// Load and render the Netsuke manifest at `path`. +/// +/// # Errors +/// +/// Returns an error when the manifest cannot be read, parsed, or rendered. +pub(super) fn load_manifest( + path: &Utf8Path, + policy: NetworkPolicy, + on_stage: StageObserver<'_>, +) -> Result { + manifest::from_path_with_policy(path.as_std_path(), policy, on_stage).with_context(|| { + localization::message(keys::RUNNER_CONTEXT_LOAD_MANIFEST).with_arg("path", path.as_str()) + }) +} + +/// Translate a manifest into the build graph intermediate representation. +/// +/// # Errors +/// +/// Returns an error when graph construction or validation fails (for example +/// on circular dependencies or duplicate outputs). +pub(super) fn build_graph(manifest: &NetsukeManifest) -> Result { + BuildGraph::from_manifest(manifest) + .context(localization::message(keys::RUNNER_CONTEXT_BUILD_GRAPH)) +} + +/// Generate the Ninja manifest text for a build graph. +/// +/// # Errors +/// +/// Returns an error when Ninja synthesis fails. +pub(super) fn ninja_text(graph: &BuildGraph) -> Result { + ninja_gen::generate(graph) + .map(NinjaContent::new) + .context(localization::message(keys::RUNNER_CONTEXT_GENERATE_NINJA)) +} diff --git a/src/runner/graph.rs b/src/runner/graph.rs index d1788aa4..5499a377 100644 --- a/src/runner/graph.rs +++ b/src/runner/graph.rs @@ -16,14 +16,13 @@ use crate::graph_view::GraphView; use crate::graph_view::render::GraphRenderer; use crate::graph_view::render_dot::DotRenderer; use crate::graph_view::render_html::HtmlRenderer; -use crate::ir::BuildGraph; use crate::localization::{self, keys}; use crate::status::{LocalizationKey, PipelineStage, StatusReporter, report_pipeline_stage}; use super::path_helpers::{ ensure_manifest_exists_or_error, resolve_manifest_path, resolve_output_path, }; -use super::{load_manifest_with_stage_reporting, process}; +use super::{generation, load_manifest_with_stage_reporting, process}; pub(super) fn handle_graph( cli: &Cli, @@ -43,8 +42,7 @@ pub(super) fn handle_graph( .context(localization::message(keys::RUNNER_CONTEXT_NETWORK_POLICY))?; let manifest = load_manifest_with_stage_reporting(&manifest_path, policy, reporter)?; report_pipeline_stage(reporter, PipelineStage::IrGenerationValidation, None); - let graph = BuildGraph::from_manifest(&manifest) - .context(localization::message(keys::RUNNER_CONTEXT_BUILD_GRAPH))?; + let graph = generation::build_graph(&manifest)?; let view = GraphView::from_build_graph(&graph); let status_key: LocalizationKey = if args.html { diff --git a/src/runner/mod.rs b/src/runner/mod.rs index ba35415a..4f00708d 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -10,13 +10,13 @@ pub use error::RunnerError; use crate::cli::{BuildArgs, Cli, Commands}; use crate::localization::{self, keys}; +use crate::manifest; use crate::output_mode::{self, OutputMode}; use crate::output_prefs::OutputPrefs; use crate::status::{ AccessibleReporter, IndicatifReporter, LocalizationKey, PipelineStage, SilentReporter, StatusReporter, VerboseTimingReporter, report_pipeline_stage, }; -use crate::{ir::BuildGraph, manifest, ninja_gen}; use anyhow::{Context, Result}; use camino::Utf8PathBuf; use std::borrow::Cow; @@ -30,6 +30,7 @@ pub const NINJA_PROGRAM: &str = "ninja"; /// Environment variable override for the Ninja executable. pub use ninja_env::NINJA_ENV; +mod generation; mod graph; mod path_helpers; mod process; @@ -342,43 +343,41 @@ fn generate_ninja( } report_pipeline_stage(reporter, PipelineStage::IrGenerationValidation, None); - let graph = BuildGraph::from_manifest(&manifest) - .context(localization::message(keys::RUNNER_CONTEXT_BUILD_GRAPH))?; + let graph = generation::build_graph(&manifest)?; report_pipeline_stage( reporter, PipelineStage::NinjaSynthesisAndExecution, tool_key, ); - let ninja = ninja_gen::generate(&graph) - .context(localization::message(keys::RUNNER_CONTEXT_GENERATE_NINJA))?; - Ok(NinjaContent::new(ninja)) + generation::ninja_text(&graph) } +/// Map manifest-loading stages onto the status reporter's pipeline stages. +fn stage_reporting_callback( + reporter: &dyn StatusReporter, +) -> impl FnMut(manifest::ManifestLoadStage) + '_ { + move |stage: manifest::ManifestLoadStage| { + let pipeline_stage = match stage { + manifest::ManifestLoadStage::ManifestIngestion => PipelineStage::ManifestIngestion, + manifest::ManifestLoadStage::InitialYamlParsing => PipelineStage::InitialYamlParsing, + manifest::ManifestLoadStage::TemplateExpansion => PipelineStage::TemplateExpansion, + manifest::ManifestLoadStage::FinalRendering => PipelineStage::FinalRendering, + }; + report_pipeline_stage(reporter, pipeline_stage, None); + } +} + +/// Load the manifest, translating loading stages into reporter updates. +/// +/// Thin reporting wrapper over the pure [`generation::load_manifest`] step. pub(super) fn load_manifest_with_stage_reporting( manifest_path: &Utf8PathBuf, policy: crate::stdlib::NetworkPolicy, reporter: &dyn StatusReporter, ) -> Result { - let mut on_stage = |stage: manifest::ManifestLoadStage| match stage { - manifest::ManifestLoadStage::ManifestIngestion => { - report_pipeline_stage(reporter, PipelineStage::ManifestIngestion, None); - } - manifest::ManifestLoadStage::InitialYamlParsing => { - report_pipeline_stage(reporter, PipelineStage::InitialYamlParsing, None); - } - manifest::ManifestLoadStage::TemplateExpansion => { - report_pipeline_stage(reporter, PipelineStage::TemplateExpansion, None); - } - manifest::ManifestLoadStage::FinalRendering => { - report_pipeline_stage(reporter, PipelineStage::FinalRendering, None); - } - }; - manifest::from_path_with_policy(manifest_path.as_std_path(), policy, Some(&mut on_stage)) - .with_context(|| { - localization::message(keys::RUNNER_CONTEXT_LOAD_MANIFEST) - .with_arg("path", manifest_path.as_str()) - }) + let mut on_stage = stage_reporting_callback(reporter); + generation::load_manifest(manifest_path, policy, Some(&mut on_stage)) } #[cfg(test)] diff --git a/src/runner/tests.rs b/src/runner/tests.rs index 4e2c3382..8b60c258 100644 --- a/src/runner/tests.rs +++ b/src/runner/tests.rs @@ -36,3 +36,27 @@ fn force_text_task_updates_when_required( expected ); } + +#[rstest] +fn generation_steps_run_without_reporter() -> anyhow::Result<()> { + let temp = tempfile::tempdir()?; + let manifest_path = temp.path().join("Netsukefile"); + std::fs::write( + &manifest_path, + "netsuke_version: \"1.0.0\"\ntargets:\n - name: hello\n command: echo hi\n", + )?; + let utf8_path = camino::Utf8PathBuf::from_path_buf(manifest_path) + .map_err(|path| anyhow::anyhow!("non-UTF-8 temp path: {}", path.display()))?; + + // The pure pipeline composes without any StatusReporter in sight. + let manifest = + generation::load_manifest(&utf8_path, crate::stdlib::NetworkPolicy::default(), None)?; + let graph = generation::build_graph(&manifest)?; + let ninja = generation::ninja_text(&graph)?; + anyhow::ensure!( + ninja.as_str().contains("build hello:"), + "expected generated Ninja to contain the hello build edge:\n{}", + ninja.as_str() + ); + Ok(()) +}