#[path = "dyndep_bundle.rs"]
mod bundle;
pub use bundle::{GeneratedDyndep, GeneratedNinja};
use crate::hex;
use crate::ir::{BuildEdge, BuildGraph};
use crate::localization::{self, keys};
use crate::ninja_gen::{
NamedAction, NinjaGenError, edge_requires_gates, graph_requires_dyndep, join, path_key,
reject_unsupported_path_characters, validate_action_recipe,
};
use camino::Utf8PathBuf;
use sha2::{Digest, Sha256};
use std::collections::HashSet;
use std::fmt::Write as _;
const PARENT_SCHEMA: &str = "netsuke-serial-v1";
const DYNDEP_SCHEMA: &str = "netsuke-dyndep-v1";
const SERIAL_NAMESPACE: &str = ".netsuke/serial";
const DYNDEP_NAMESPACE: &str = ".netsuke/dyndep";
pub fn generate_bundle(graph: &BuildGraph) -> Result<GeneratedNinja, NinjaGenError> {
generate_bundle_inner(graph)
}
fn generate_bundle_inner(graph: &BuildGraph) -> Result<GeneratedNinja, NinjaGenError> {
reject_unsupported_path_characters(graph)?;
reject_reserved_paths(graph)?;
let serial_present = graph_requires_dyndep(graph);
let mut out = String::new();
if serial_present {
writeln!(out, "ninja_required_version = 1.10\n")?;
}
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() {
validate_action_recipe(action, zero_based_action_index + 1)?;
write!(out, "{}", NamedAction { id, action })?;
}
let mut stages = SerialStages::default();
render_edges(graph, &mut out, &mut stages)?;
if !graph.default_targets.is_empty() {
let mut defs = graph.default_targets.clone();
defs.sort();
writeln!(out, "default {}", join(&defs))?;
}
Ok(GeneratedNinja {
build_file: out,
dyndep_files: stages.dyndep_files,
})
}
fn render_edges(
graph: &BuildGraph,
out: &mut String,
stages: &mut SerialStages,
) -> Result<(), NinjaGenError> {
let mut edges: Vec<_> = graph.targets.values().collect();
edges.sort_by_key(|a| path_key(&a.explicit_outputs));
let mut seen: HashSet<String> = HashSet::new();
for edge in edges {
let key = path_key(&edge.explicit_outputs);
if !seen.insert(key) {
continue;
}
render_edge(graph, edge, out, stages)?;
}
Ok(())
}
fn render_edge(
graph: &BuildGraph,
edge: &BuildEdge,
out: &mut String,
stages: &mut SerialStages,
) -> Result<(), NinjaGenError> {
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),
})?;
if edge_requires_gates(edge) {
return render_serial_edge(edge, action.restat, out, stages);
}
render_display_edge(edge, action.restat, &edge.implicit_deps, out)
}
fn render_serial_edge(
edge: &BuildEdge,
action_restat: bool,
out: &mut String,
stages: &mut SerialStages,
) -> Result<(), NinjaGenError> {
let mut gate_paths = Vec::new();
render_serial_block(edge, out, stages, &mut gate_paths)?;
render_display_edge(edge, action_restat, &gate_paths, out)
}
fn render_display_edge(
edge: &BuildEdge,
action_restat: bool,
implicit_deps: &[Utf8PathBuf],
out: &mut String,
) -> Result<(), NinjaGenError> {
write!(
out,
"{}",
crate::ninja_gen::DisplayEdge {
edge,
action_restat,
implicit_deps,
}
)?;
Ok(())
}
#[derive(Default)]
struct SerialStages {
dyndep_files: Vec<GeneratedDyndep>,
staged_sidecars: HashSet<Utf8PathBuf>,
}
fn render_serial_block(
edge: &BuildEdge,
out: &mut String,
stages: &mut SerialStages,
gate_paths: &mut Vec<Utf8PathBuf>,
) -> Result<(), NinjaGenError> {
use crate::ninja_gen::escape_ninja_path;
let parent = parent_identity(edge);
for (index, dep) in edge.implicit_deps.iter().enumerate() {
let gate = parent.join(format!("{index:03}"));
let content = sidecar_content(&gate, dep)?;
let digest = sidecar_digest(&content);
let sidecar = Utf8PathBuf::from(format!("{DYNDEP_NAMESPACE}/{digest}.dd"));
let sidecar_escaped = escape_ninja_path(sidecar.as_str())?;
let gate_escaped = escape_ninja_path(gate.as_str())?;
match gate_paths.last() {
None => writeln!(out, "build {sidecar_escaped}: phony")?,
Some(prev) => {
let prev_escaped = escape_ninja_path(prev.as_str())?;
writeln!(out, "build {sidecar_escaped}: phony {prev_escaped}")?;
}
}
writeln!(out, "build {gate_escaped}: phony || {sidecar_escaped}")?;
writeln!(out, " dyndep = {sidecar_escaped}")?;
writeln!(out)?;
if stages.staged_sidecars.insert(sidecar.clone()) {
stages.dyndep_files.push(GeneratedDyndep {
relative_path: sidecar,
content,
});
}
gate_paths.push(gate);
}
Ok(())
}
fn parent_identity(edge: &BuildEdge) -> Utf8PathBuf {
let canonical = edge
.explicit_outputs
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>()
.join("\u{0}");
let mut hasher = Sha256::new();
hasher.update(PARENT_SCHEMA.as_bytes());
hasher.update(b"\0");
hasher.update(canonical.as_bytes());
let digest = hex::to_lower_hex(&hasher.finalize());
Utf8PathBuf::from(format!("{SERIAL_NAMESPACE}/{digest}"))
}
fn sidecar_content(gate: &Utf8PathBuf, dep: &Utf8PathBuf) -> Result<String, NinjaGenError> {
use crate::ninja_gen::escape_ninja_path;
let gate_escaped = escape_ninja_path(gate.as_str())?;
let dep_escaped = escape_ninja_path(dep.as_str())?;
Ok(format!(
"ninja_dyndep_version = 1\nbuild {gate_escaped}: dyndep | {dep_escaped}\n"
))
}
fn sidecar_digest(content: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(DYNDEP_SCHEMA.as_bytes());
hasher.update(b"\0");
hasher.update(content.as_bytes());
hex::to_lower_hex(&hasher.finalize())
}
pub(crate) fn reject_reserved_paths(graph: &BuildGraph) -> Result<(), NinjaGenError> {
for edge in graph.targets.values() {
for path in edge
.explicit_outputs
.iter()
.chain(&edge.implicit_outputs)
.chain(&edge.inputs)
.chain(&edge.implicit_deps)
.chain(&edge.order_only_deps)
{
let as_str = path.as_str();
let is_reserved = [SERIAL_NAMESPACE, DYNDEP_NAMESPACE]
.iter()
.any(|namespace| {
as_str == *namespace
|| as_str
.strip_prefix(namespace)
.is_some_and(|suffix| suffix.starts_with('/'))
});
if is_reserved {
return Err(NinjaGenError::ReservedOutputPath {
path: path.clone(),
message: localization::message(keys::NINJA_GEN_RESERVED_OUTPUT_PATH)
.with_arg("path", as_str),
});
}
}
}
Ok(())
}
#[cfg(test)]
#[path = "dyndep_tests.rs"]
mod tests;