1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//! Manifest-to-IR conversion helpers.
//!
//! Implements [`BuildGraph::from_manifest`], which lowers a parsed
//! [`crate::ast::NetsukeManifest`] into a [`BuildGraph`]. Delegates
//! template rendering to [`crate::manifest::render`], command interpolation
//! to [`super::cmd_interpolate`], and cycle/missing-dependency detection to
//! [`super::cycle`].
use std::sync::Arc;
use camino::Utf8PathBuf;
use crate::ast::{MISSING_RECIPE_ERROR, NetsukeManifest, Recipe, Rule};
use crate::localization::{self, keys};
use crate::recipe_shell::RecipeShell;
use super::{
cycle::{self, CycleDetectionReport},
graph::{Action, BuildEdge, BuildGraph, DependencyOrder, IrGenError, IrHashMap},
};
#[path = "from_manifest_support.rs"]
mod support;
#[cfg(kani)]
use support::find_duplicates;
use support::{
ActionBindings, duplicate_output_error, get_target_display_name, insert_edge_for_outputs,
register_action, resolve_rule, to_paths,
};
/// Groups the stable inputs required while lowering manifest targets.
#[derive(Clone, Copy)]
struct TargetLoweringContext<'a> {
/// Rules available to rule-backed targets.
rule_map: &'a IrHashMap<String, Arc<Rule>>,
/// Interpreter that will receive completed legacy recipe text.
shell: RecipeShell,
}
impl BuildGraph {
/// Transform a manifest into a [`BuildGraph`].
///
/// # Errors
///
/// Returns [`IrGenError`] when a referenced rule is missing, multiple rules
/// are specified for a single target, no rule is provided, or a directly
/// deserialized manifest violates the recipe contract.
pub fn from_manifest(manifest: &NetsukeManifest) -> Result<Self, IrGenError> {
Self::from_manifest_for_shell(manifest, RecipeShell::host_default())
}
/// Transform a manifest into a graph for one legacy recipe interpreter.
///
/// Path placeholders are lowered at this boundary, so their quoting must
/// match the interpreter that later receives the completed recipe text.
///
/// # Errors
///
/// Returns [`IrGenError`] when a referenced rule is missing, multiple rules
/// are specified for a single target, no rule is provided, or a directly
/// deserialized manifest violates the recipe contract.
#[doc(hidden)]
pub fn from_manifest_for_shell(
manifest: &NetsukeManifest,
shell: RecipeShell,
) -> Result<Self, IrGenError> {
manifest
.validate_recipes()
.map_err(|_| IrGenError::InvalidManifest {
message: MISSING_RECIPE_ERROR,
})?;
let mut graph = Self::default();
let mut rule_map = IrHashMap::<String, Arc<Rule>>::default();
Self::process_rules(manifest, &mut rule_map);
Self::process_targets(
manifest,
&mut graph.actions,
&mut graph.targets,
TargetLoweringContext {
rule_map: &rule_map,
shell,
},
)?;
Self::process_defaults(manifest, &mut graph.default_targets);
graph.detect_cycles()?;
Ok(graph)
}
/// Collect rule templates without deduplicating them.
///
/// Rules are stored verbatim and expanded later when targets reference
/// them. This allows each target's input and output paths to be embedded in
/// the resulting command, meaning identical rule definitions may yield
/// distinct actions once interpolated. Target descriptions remain discovery
/// metadata and never take part in recipe resolution or Ninja progress.
fn process_rules(manifest: &NetsukeManifest, rule_map: &mut IrHashMap<String, Arc<Rule>>) {
for rule in &manifest.rules {
rule_map.insert(rule.name.clone(), Arc::new(rule.clone()));
}
}
/// Build an action and edge for every manifest target.
///
/// Rule-backed targets resolve a single template first; command and script
/// targets interpolate their command directly. Each resolved action is
/// registered once under its content hash, and the resulting edge is
/// attached to every explicit output.
///
/// # Errors
///
/// Returns [`IrGenError`] when a target writes an output already claimed by
/// an earlier target or by a sibling output in the same target, when a
/// rule selector does not resolve to exactly one rule, or when command
/// interpolation fails.
fn process_targets(
manifest: &NetsukeManifest,
actions: &mut IrHashMap<String, Action>,
targets: &mut IrHashMap<Utf8PathBuf, BuildEdge>,
context: TargetLoweringContext<'_>,
) -> Result<(), IrGenError> {
for target in manifest.actions.iter().chain(&manifest.targets) {
let outputs = to_paths(&target.name);
let inputs = to_paths(&target.sources);
let implicit_deps = to_paths(&target.deps);
tracing::debug!(
target = ?target.name,
implicit_deps_count = implicit_deps.len(),
"populating implicit dependencies for target",
);
if let Some(error) = duplicate_output_error(&outputs, targets) {
return Err(error);
}
let action_id = match &target.recipe {
Recipe::Rule { rule } => {
let target_name = get_target_display_name(&outputs);
let tmpl = resolve_rule(rule, context.rule_map, &target_name)?;
// Target descriptions are deliberately omitted: rule
// descriptions remain the sole source of Ninja progress
// text.
register_action(
actions,
tmpl.recipe.clone(),
tmpl.description.as_deref(),
ActionBindings {
inputs: &inputs,
outputs: &outputs,
shell: context.shell,
},
)?
}
Recipe::Command { .. } | Recipe::Script { .. } => register_action(
actions,
target.recipe.clone(),
None,
ActionBindings {
inputs: &inputs,
outputs: &outputs,
shell: context.shell,
},
)?,
};
let edge = BuildEdge {
action_id,
inputs,
implicit_deps,
dependency_order: target.dependency_order.into(),
explicit_outputs: outputs,
implicit_outputs: Vec::new(),
order_only_deps: to_paths(&target.order_only_deps),
phony: target.phony,
always: target.always,
};
insert_edge_for_outputs(targets, edge);
}
Ok(())
}
/// Append the manifest's default targets to `defaults`.
fn process_defaults(manifest: &NetsukeManifest, defaults: &mut Vec<Utf8PathBuf>) {
defaults.extend(manifest.defaults.iter().map(Utf8PathBuf::from));
}
/// Detect dependency cycles and unresolved dependencies in the built graph.
///
/// Unresolved dependencies are logged and treated as external files; a
/// detected cycle fails the whole lowering.
///
/// # Errors
///
/// Returns [`IrGenError::CircularDependency`] when the target graph
/// contains a dependency cycle.
fn detect_cycles(&self) -> Result<(), IrGenError> {
let CycleDetectionReport {
cycle,
missing_dependencies,
} = cycle::analyse(&self.targets);
for (dependent, missing) in &missing_dependencies {
tracing::info!(
dependent = %dependent,
missing = %missing,
"unresolved dependency: not a build target; assuming it is an external file",
);
}
if let Some(detected_cycle) = cycle {
let message = localization::message(keys::IR_CIRCULAR_DEPENDENCY)
.with_arg("cycle", format!("{detected_cycle:?}"));
return Err(IrGenError::CircularDependency {
cycle: detected_cycle,
missing_dependencies,
message,
});
}
tracing::info!(
count = missing_dependencies.len(),
"cycle detection complete; unresolved dependencies treated as external files",
);
Ok(())
}
}
#[cfg(kani)]
#[path = "from_manifest_verification.rs"]
mod verification;
impl From<crate::ast::DependencyOrder> for DependencyOrder {
fn from(order: crate::ast::DependencyOrder) -> Self {
match order {
crate::ast::DependencyOrder::Parallel => Self::Parallel,
crate::ast::DependencyOrder::Serial => Self::Serial,
}
}
}