mod dispatch;
mod error;
pub use error::RunnerError;
use crate::cli::{BuildArgs, Cli, Commands};
use crate::localization::{self, keys};
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::io::IsTerminal;
use std::path::Path;
use tracing::{debug, info};
pub const NINJA_PROGRAM: &str = "ninja";
pub const NINJA_ENV: &str = "NETSUKE_NINJA";
mod graph;
mod path_helpers;
mod process;
#[cfg(doctest)]
pub use process::doc;
pub use process::{run_ninja, run_ninja_tool};
use path_helpers::{ensure_manifest_exists_or_error, resolve_manifest_path, resolve_output_path};
struct ExecutionContext<'a> {
reporter: &'a dyn StatusReporter,
progress_enabled: bool,
ninja_program: &'a Path,
}
#[derive(Debug, Clone)]
pub struct NinjaContent(String);
impl NinjaContent {
#[must_use]
pub const fn new(content: String) -> Self {
Self(content)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BuildTargets<'a>(&'a [String]);
impl<'a> BuildTargets<'a> {
#[must_use]
pub const fn new(targets: &'a [String]) -> Self {
Self(targets)
}
#[must_use]
pub const fn as_slice(&self) -> &'a [String] {
self.0
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[expect(
clippy::derivable_impls,
reason = "Default derive requires 'static lifetime; manual impl returns empty slice."
)]
impl Default for BuildTargets<'_> {
fn default() -> Self {
Self(&[])
}
}
#[derive(Debug, Clone, Copy)]
struct ReporterOptions {
mode: OutputMode,
progress_enabled: bool,
verbose: bool,
prefs: OutputPrefs,
stdout_is_tty: bool,
}
fn make_reporter(options: ReporterOptions) -> Box<dyn StatusReporter> {
let base: Box<dyn StatusReporter> = if options.progress_enabled {
let force_text_task_updates =
should_force_text_task_updates(options.mode, options.stdout_is_tty);
match options.mode {
OutputMode::Accessible => Box::new(AccessibleReporter::new(options.prefs)),
OutputMode::Standard => Box::new(IndicatifReporter::with_force_text_task_updates(
options.prefs,
force_text_task_updates,
)),
}
} else {
Box::new(SilentReporter)
};
if options.verbose {
Box::new(VerboseTimingReporter::new(base, options.prefs))
} else {
base
}
}
const fn should_force_text_task_updates(mode: OutputMode, stdout_is_tty: bool) -> bool {
mode.is_accessible() || !stdout_is_tty
}
pub fn run(cli: &Cli, prefs: OutputPrefs) -> Result<()> {
let program = process::resolve_ninja_program();
run_with_ninja_program(cli, prefs, &program)
}
pub fn run_with_ninja_program(cli: &Cli, prefs: OutputPrefs, program: &Path) -> Result<()> {
let mode = output_mode::resolve(cli.accessibility_override(), Some(cli.color));
let progress_enabled = cli.progress_enabled() && !cli.json;
let stdout_is_tty = std::io::stdout().is_terminal();
let reporter = make_reporter(ReporterOptions {
mode,
progress_enabled,
verbose: cli.verbose && !cli.json,
prefs,
stdout_is_tty,
});
let command = cli.command.clone().unwrap_or(Commands::Build(BuildArgs {
targets: Vec::new(),
}));
let context = ExecutionContext {
reporter: reporter.as_ref(),
progress_enabled,
ninja_program: program,
};
dispatch::execute(cli, command, &context)
}
fn on_task_progress_callback(reporter: &dyn StatusReporter) -> impl FnMut(u32, u32, &str) + '_ {
move |current: u32, total: u32, description: &str| {
reporter.report_task_progress(current, total, description);
}
}
fn handle_build(cli: &Cli, args: &BuildArgs, context: &ExecutionContext<'_>) -> Result<()> {
let ninja = generate_ninja(cli, context.reporter, Some(keys::STATUS_TOOL_BUILD.into()))?;
let targets = if args.targets.is_empty() {
BuildTargets::new(&cli.default_targets)
} else {
BuildTargets::new(&args.targets)
};
let build_file = process::create_temp_ninja_file(&ninja)?;
let build_path = build_file.path();
let ctx = || {
format!(
"running {} with build file {}",
context.ninja_program.display(),
build_path.display()
)
};
if context.progress_enabled {
let mut on_task_progress = on_task_progress_callback(context.reporter);
process::run_ninja_with_status(
process::NinjaBuildRequest {
program: context.ninja_program,
cli,
build_file: build_path,
targets: &targets,
},
&mut on_task_progress,
)
.with_context(ctx)?;
} else {
run_ninja(context.ninja_program, cli, build_path, &targets).with_context(ctx)?;
}
context
.reporter
.report_complete(keys::STATUS_TOOL_BUILD.into());
Ok(())
}
#[derive(Clone, Copy)]
struct NinjaToolSpec<'a> {
name: &'a str,
key: LocalizationKey,
}
fn handle_ninja_tool(
cli: &Cli,
tool: NinjaToolSpec<'_>,
context: &ExecutionContext<'_>,
) -> Result<()> {
info!(
target: "netsuke::subcommand",
subcommand = tool.name,
"Preparing Ninja tool invocation"
);
let ninja = generate_ninja(cli, context.reporter, Some(tool.key))?;
let tmp = process::create_temp_ninja_file(&ninja)?;
let build_path = tmp.path();
let ctx = || {
format!(
"running {} -t {} with build file {}",
context.ninja_program.display(),
tool.name,
build_path.display()
)
};
if context.progress_enabled {
let mut on_task_progress = on_task_progress_callback(context.reporter);
process::run_ninja_tool_with_status(
process::NinjaToolRequest {
program: context.ninja_program,
cli,
build_file: build_path,
tool: tool.name,
},
&mut on_task_progress,
)
.with_context(ctx)?;
} else {
run_ninja_tool(context.ninja_program, cli, build_path, tool.name).with_context(ctx)?;
}
context.reporter.report_complete(tool.key);
Ok(())
}
fn generate_ninja(
cli: &Cli,
reporter: &dyn StatusReporter,
tool_key: Option<LocalizationKey>,
) -> Result<NinjaContent> {
let manifest_path = resolve_manifest_path(cli)?;
ensure_manifest_exists_or_error(cli, reporter, &manifest_path)?;
let policy = cli
.network_policy()
.context(localization::message(keys::RUNNER_CONTEXT_NETWORK_POLICY))?;
let manifest = load_manifest_with_stage_reporting(&manifest_path, policy, reporter)?;
if tracing::enabled!(tracing::Level::DEBUG) {
let ast_json = serde_json::to_string_pretty(&manifest).context(localization::message(
keys::RUNNER_CONTEXT_SERIALISE_MANIFEST,
))?;
debug!("AST:\n{ast_json}");
}
report_pipeline_stage(reporter, PipelineStage::IrGenerationValidation, None);
let graph = BuildGraph::from_manifest(&manifest)
.context(localization::message(keys::RUNNER_CONTEXT_BUILD_GRAPH))?;
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))
}
pub(super) fn load_manifest_with_stage_reporting(
manifest_path: &Utf8PathBuf,
policy: crate::stdlib::NetworkPolicy,
reporter: &dyn StatusReporter,
) -> Result<crate::ast::NetsukeManifest> {
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())
})
}
#[cfg(test)]
mod tests;