pub mod dyndep;
mod path_syntax;
use dyndep::reject_reserved_paths;
pub use dyndep::{GeneratedDyndep, GeneratedNinja, generate_bundle};
pub(crate) use path_syntax::{reject_unsupported_path_characters, validated_ninja_path};
use crate::ast::{Recipe, StringOrList};
use crate::ir::{BuildEdge, BuildGraph};
use crate::localization::{self, keys};
use camino::Utf8PathBuf;
use itertools::Itertools;
use std::collections::HashSet;
use std::fmt::Write;
mod explicit_shell;
#[path = "../ninja_gen_command_list.rs"]
pub(crate) mod ninja_gen_command_list;
#[path = "../ninja_gen_error.rs"]
mod ninja_gen_error;
#[path = "../ninja_gen_escape.rs"]
mod ninja_gen_escape;
#[path = "../ninja_gen_recipe_shell.rs"]
mod ninja_gen_recipe_shell;
#[path = "../ninja_gen_validation.rs"]
mod ninja_gen_validation;
pub use crate::recipe_shell::RecipeShell;
pub use explicit_shell::generate_with_shell;
use ninja_gen_command_list::{ActionId, CommandListEntry, command_list_entry};
pub use ninja_gen_error::NinjaGenError;
use ninja_gen_escape::{ShellText, escape_metadata_value};
use ninja_gen_recipe_shell::escape_posix_script;
use ninja_gen_validation::{validate_action_metadata, validate_action_recipe};
macro_rules! write_kv {
($f:expr, $key:expr, $opt:expr) => {
if let Some(val) = $opt {
writeln!($f, " {} = {}", $key, val)?;
}
};
}
macro_rules! write_flag {
($f:expr, $key:expr, $cond:expr) => {
if $cond {
writeln!($f, " {} = 1", $key)?;
}
};
}
mod display_edge;
pub(crate) use display_edge::DisplayEdge;
pub fn generate(graph: &BuildGraph) -> Result<String, NinjaGenError> {
let mut out = String::new();
generate_into(graph, &mut out)?;
Ok(out)
}
pub fn generate_into<W: Write>(graph: &BuildGraph, out: &mut W) -> Result<(), NinjaGenError> {
generate_into_with_shell(graph, out, RecipeShell::host_default())
}
pub(crate) fn generate_into_with_shell<W: Write>(
graph: &BuildGraph,
out: &mut W,
shell: RecipeShell,
) -> Result<(), NinjaGenError> {
reject_unsupported_path_characters(graph)?;
reject_reserved_paths(graph)?;
if graph_requires_dyndep(graph) {
return Err(NinjaGenError::DyndepFilesRequired {
message: localization::message(keys::NINJA_GEN_DYNDEP_FILES_REQUIRED),
});
}
write_action_rules(graph, out, shell)?;
let mut edges: Vec<_> = graph.targets.values().collect();
edges.sort_by_key(|a| path_key(&a.explicit_outputs));
let mut seen = HashSet::new();
for edge in edges {
let key = path_key(&edge.explicit_outputs);
if !seen.insert(key.clone()) {
continue;
}
let action =
graph
.actions
.get(&edge.action_id)
.ok_or_else(|| NinjaGenError::MissingAction {
id: edge.action_id.clone(),
message: localization::message(keys::NINJA_GEN_MISSING_ACTION)
.with_arg("id", &edge.action_id),
})?;
write!(
out,
"{}",
DisplayEdge {
edge,
action_name: if action.recipe.is_dependency_only() {
"phony"
} else {
&edge.action_id
},
action_restat: action.restat,
implicit_deps: &edge.implicit_deps,
}
)?;
}
if !graph.default_targets.is_empty() {
let mut defs = graph.default_targets.clone();
defs.sort();
writeln!(out, "default {}", join(&defs))?;
}
Ok(())
}
pub(crate) fn write_action_rules<W: Write>(
graph: &BuildGraph,
out: &mut W,
shell: RecipeShell,
) -> Result<(), NinjaGenError> {
let mut actions: Vec<_> = graph.actions.iter().collect();
actions.sort_by_key(|(id, _)| *id);
for (zero_based_action_index, (id, action)) in actions.into_iter().enumerate() {
if action.recipe.is_dependency_only() {
continue;
}
validate_action_recipe(action, zero_based_action_index + 1, shell)?;
validate_action_metadata(action)?;
NamedAction { id, action, shell }.write_into(out)?;
}
Ok(())
}
pub(crate) fn join(paths: &[Utf8PathBuf]) -> String {
paths
.iter()
.map(|path| path_syntax::clone_validated_ninja_path(path.as_str()))
.join(" ")
}
pub(crate) fn path_key(paths: &[Utf8PathBuf]) -> String {
let mut parts: Vec<String> = paths.iter().map(|p| p.as_str().to_owned()).collect();
parts.sort_unstable();
parts.join(&char::from(0).to_string())
}
pub(crate) fn graph_requires_dyndep(graph: &BuildGraph) -> bool {
graph.targets.values().any(edge_requires_gates)
}
pub(crate) fn edge_requires_gates(edge: &BuildEdge) -> bool {
edge.dependency_order == crate::ir::DependencyOrder::Serial && edge.implicit_deps.len() > 1
}
pub(crate) struct NamedAction<'a> {
id: &'a str,
action: &'a crate::ir::Action,
shell: RecipeShell,
}
impl NamedAction<'_> {
fn write_metadata<W: Write>(&self, f: &mut W) -> Result<(), NinjaGenError> {
let description = escape_metadata_value(self.action.description.as_deref())?;
let depfile = escape_metadata_value(self.action.depfile.as_deref())?;
let deps_format = escape_metadata_value(self.action.deps_format.as_deref())?;
let pool = escape_metadata_value(self.action.pool.as_deref())?;
write_kv!(f, "description", &description);
write_kv!(f, "depfile", &depfile);
write_kv!(f, "deps", &deps_format);
write_kv!(f, "pool", &pool);
write_flag!(f, "restat", self.action.restat);
writeln!(f)?;
Ok(())
}
fn assert_shell_command(command: &str) {
debug_assert!(
shlex::split(command).is_some(),
"invalid command: {command}"
);
}
#[cold]
#[expect(
clippy::panic_in_result_fn,
reason = "debug builds intentionally panic to expose rule recursion"
)]
#[expect(
clippy::manual_assert,
reason = "debug-only guard escalates to panic for visibility"
)]
fn reject_rule_recipe() -> Result<ShellText, NinjaGenError> {
if cfg!(debug_assertions) {
panic!("rules do not reference other rules");
}
Err(NinjaGenError::UnsafeNinjaValue)
}
#[cold]
const fn reject_empty_command_recipe() -> Result<ShellText, NinjaGenError> {
Err(NinjaGenError::UnsafeNinjaValue)
}
fn shell_text(&self) -> Result<ShellText, NinjaGenError> {
let command = match &self.action.recipe {
Recipe::Command {
command: StringOrList::String(scalar_command),
} => {
if self.shell != RecipeShell::PowerShell {
Self::assert_shell_command(scalar_command);
}
ShellText::new(scalar_command.clone())
}
Recipe::Command {
command: StringOrList::List(items),
} => self.command_list_shell_text(items),
Recipe::Command {
command: StringOrList::Empty,
} => return Self::reject_empty_command_recipe(),
Recipe::Script { script } => self.script_shell_text(script),
Recipe::Rule { .. } => return Self::reject_rule_recipe(),
};
Ok(command)
}
fn script_shell_text(&self, script: &str) -> ShellText {
if self.shell == RecipeShell::PowerShell {
return ShellText::new(script.to_owned());
}
let escaped = escape_posix_script(script);
let cmd = format!("/bin/sh -e -c \"printf %b '{escaped}' | /bin/sh -e\"");
ShellText::new(cmd)
}
fn command_list_shell_text(&self, items: &[String]) -> ShellText {
if let Some(script) = self.shell.command_list_script(items) {
return ShellText::new(script);
}
let command_line = items
.iter()
.enumerate()
.map(|(entry_index, item)| {
command_list_entry(CommandListEntry(item), ActionId(self.id), entry_index + 1)
})
.join(" && ");
Self::assert_shell_command(&command_line);
ShellText::new(command_line)
}
fn write_into<W: Write>(&self, output: &mut W) -> Result<(), NinjaGenError> {
let command = self.shell.command_value(&self.shell_text()?)?;
writeln!(output, "rule {}", self.id)?;
writeln!(output, " command = {}", command.command())?;
if let Some(content) = command.response_file_content() {
writeln!(output, " rspfile = $out.netsuke-{}.ps1", self.id)?;
writeln!(output, " rspfile_content = {content}")?;
}
self.write_metadata(output)
}
}
#[cfg(test)]
#[path = "../ninja_gen_property_tests.rs"]
mod property_tests;
#[cfg(test)]
#[path = "../ninja_gen_test_support.rs"]
mod test_support;
#[cfg(test)]
#[path = "../ninja_gen_tests.rs"]
mod tests;