1use alloc::collections::BTreeMap;
4use core::str;
5use std::collections::HashSet;
6
7use derive_more::{AsRef, Display, Into};
8use minijinja::{AutoEscape, Environment, UndefinedBehavior, Value};
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12use crate::errors::{ParseError, ProfileError, Result, VarError};
13use crate::gate::Pattern;
14use crate::path::RelPath;
15use crate::profile::{MANIFEST, PAYLOAD};
16use crate::resolve::{Layer, LayerFile};
17use crate::target::Target;
18use crate::tree::Tree;
19
20pub(crate) const RESERVED: &str = "devset";
22
23#[derive(
25 Clone,
26 PartialEq,
27 Eq,
28 PartialOrd,
29 Ord,
30 Hash,
31 AsRef,
32 Display,
33 derive_more::Debug,
34 Into,
35 Serialize,
36 Deserialize,
37 JsonSchema,
38)]
39#[as_ref(str)]
40#[debug("{_0:?}")]
41#[into(String)]
42#[serde(try_from = "String", into = "String")]
43#[schemars(inline)]
44pub struct VarName(Box<str>);
45
46#[derive(Clone, Debug, Default, Deserialize, JsonSchema)]
48#[serde(deny_unknown_fields)]
49pub struct VarSpec {
50 #[serde(default)]
52 pub prompt: Option<String>,
53 #[serde(default)]
55 pub default: Option<String>,
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
60pub struct Question {
61 pub name: VarName,
63 pub prompt: String,
65 pub default: Option<String>,
67}
68
69impl VarName {
70 #[must_use]
72 pub fn as_str(&self) -> &str {
73 &self.0
74 }
75}
76
77impl TryFrom<String> for VarName {
78 type Error = VarError;
79
80 fn try_from(name: String) -> Result<Self, VarError> {
81 let mut chars = name.chars();
82 let valid = chars.next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
83 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_');
84 match (valid, name.as_str()) {
85 (false, _) => Err(VarError::Name { name }),
86 (true, RESERVED) => Err(VarError::Reserved),
87 (true, _) => Ok(Self(name.into_boxed_str())),
88 }
89 }
90}
91
92#[derive(Default)]
94struct Declared<'a> {
95 prompt: Option<&'a str>,
97 default: Offer<'a>,
99}
100
101#[derive(Clone, Copy, Default, PartialEq, Eq)]
103enum Offer<'a> {
104 #[default]
106 None,
107 Agreed(&'a str),
109 Disputed,
111}
112
113pub(crate) fn answers(layers: &[Layer], target: &Target) -> Result<BTreeMap<VarName, String>> {
119 let mut declared: BTreeMap<&VarName, Declared<'_>> = BTreeMap::new();
120 for (name, spec) in layers.iter().flat_map(Layer::vars) {
121 let entry = declared.entry(name).or_default();
122 entry.prompt = entry.prompt.or(spec.prompt.as_deref());
123 entry.default = match (entry.default, spec.default.as_deref()) {
124 (offer, None) => offer,
125 (Offer::None, Some(default)) => Offer::Agreed(default),
126 (Offer::Agreed(first), Some(default)) if first == default => Offer::Agreed(first),
127 (Offer::Agreed(_) | Offer::Disputed, Some(_)) => Offer::Disputed,
128 };
129 }
130 if let Some(name) = target.given().find(|name| !declared.contains_key(name)) {
131 let declared = declared.keys().map(|&name| name.clone()).collect();
132 return Err(VarError::Unknown { name: name.clone(), declared }.into());
133 }
134 let mut answers = BTreeMap::new();
135 let mut questions = Vec::new();
136 for (name, Declared { prompt, default }) in declared {
137 match target.answers().get(name) {
138 Some(answer) => {
139 answers.insert(name.clone(), answer.clone());
140 },
141 None => questions.push(Question {
142 name: name.clone(),
143 prompt: prompt.map_or_else(|| name.to_string(), str::to_owned),
144 default: match default {
145 Offer::Agreed(default) => Some(default.to_owned()),
146 Offer::None | Offer::Disputed => None,
147 },
148 }),
149 }
150 }
151 if questions.is_empty() { Ok(answers) } else { Err(VarError::Unanswered { questions }.into()) }
152}
153
154#[derive(Debug, Serialize)]
156pub(crate) struct Context {
157 target: String,
159 profiles: Vec<String>,
161 layers: Vec<LayerContext>,
163}
164
165#[derive(Debug, Serialize)]
167struct LayerContext {
168 profile: String,
170 source: String,
172 features: Vec<String>,
174 configured: bool,
176}
177
178#[derive(Serialize)]
180struct Devset<'a> {
181 target: &'a str,
183 profiles: &'a [String],
185 layers: &'a [LayerContext],
187 features: &'a [String],
189}
190
191impl Context {
192 pub(crate) fn of(target: &Target, layers: &[Layer]) -> Self {
194 let layers: Vec<LayerContext> = layers
195 .iter()
196 .map(|layer| LayerContext {
197 profile: layer.name().to_string(),
198 source: layer
199 .source_name()
200 .map_or_else(|| layer.source().to_string(), ToString::to_string),
201 features: layer.features().keys().map(ToString::to_string).collect(),
202 configured: layer.configured(),
203 })
204 .collect();
205 Self {
206 target: target.root().file_name().unwrap_or_default().to_owned(),
207 profiles: layers.iter().map(|layer| layer.profile.clone()).collect(),
208 layers,
209 }
210 }
211
212 fn vars(&self, layer: &Layer, answers: &BTreeMap<VarName, String>) -> Value {
214 let empty = Vec::new();
215 let features = self
216 .layers
217 .iter()
218 .find(|l| *layer.name() == *l.profile)
219 .map_or(&empty, |l| &l.features);
220 let devset = Devset {
221 target: &self.target,
222 profiles: &self.profiles,
223 layers: &self.layers,
224 features,
225 };
226 let mut vars: BTreeMap<&str, Value> = answers
227 .iter()
228 .map(|(name, answer)| (name.as_str(), Value::from(answer.as_str())))
229 .collect();
230 vars.insert(RESERVED, Value::from_serialize(&devset));
231 Value::from_serialize(&vars)
232 }
233}
234
235struct Renderer<'a> {
238 env: Environment<'static>,
240 vars: Value,
242 known: HashSet<String>,
244 declared: &'a BTreeMap<VarName, String>,
246}
247
248impl<'a> Renderer<'a> {
249 fn new(layer: &Layer, answers: &'a BTreeMap<VarName, String>, context: &Context) -> Self {
251 let mut env = Environment::new();
252 env.set_undefined_behavior(UndefinedBehavior::Strict);
253 env.set_keep_trailing_newline(true);
254 env.set_auto_escape_callback(|_| AutoEscape::None);
255 let mut known: HashSet<String> = env.globals().map(|(name, _)| name.to_owned()).collect();
256 known.extend(answers.keys().map(ToString::to_string));
257 known.insert(RESERVED.to_owned());
258 Self { env, vars: context.vars(layer, answers), known, declared: answers }
259 }
260
261 fn render(&self, name: &RelPath, source: &str, file: &str) -> Result<String> {
267 let failed = |e: &minijinja::Error| {
268 let message = e
269 .detail()
270 .map_or_else(|| e.kind().to_string(), |detail| format!("{}: {detail}", e.kind()));
271 ParseError { file: file.to_owned(), text: source.to_owned(), span: e.range(), message }
272 };
273 let template =
274 self.env.template_from_named_str(name.as_str(), source).map_err(|e| failed(&e))?;
275 let undeclared = template
276 .undeclared_variables(false)
277 .into_iter()
278 .filter(|name| !self.known.contains(name))
279 .min();
280 if let Some(undeclared) = undeclared {
281 let declared = self.declared.keys().cloned().collect();
282 return Err(
283 VarError::Undeclared { path: name.clone(), name: undeclared, declared }.into()
284 );
285 }
286 Ok(template.render(&self.vars).map_err(|e| failed(&e))?)
287 }
288
289 fn text(&self, written: &str, file: &str) -> Result<String> {
291 if !written.contains('{') {
292 return Ok(written.to_owned());
293 }
294 let name = RelPath::new(MANIFEST)?;
295 self.render(&name, written, file)
296 }
297}
298
299pub(crate) fn render_paths(
307 layers: &mut [Layer], answers: &BTreeMap<VarName, String>, context: &Context,
308) -> Result<()> {
309 for layer in layers {
310 let (specs, scaffolds) = layer.take_specs();
311 let templates = Renderer::new(layer, answers, context);
312 let file = layer.label(MANIFEST);
313 let pattern = |written: &str| -> Result<Pattern> {
314 let rendered = templates.text(written, &file)?;
315 Pattern::parse(&rendered).map_err(|reason| {
316 ProfileError::Pattern { profile: layer.name().clone(), pattern: rendered, reason }
317 .into()
318 })
319 };
320 let mut files: BTreeMap<RelPath, LayerFile> = BTreeMap::new();
321 for (written, spec) in specs {
322 let rendered = templates.text(written.as_str(), &file)?;
323 let path = RelPath::new(&rendered).map_err(|e| ProfileError::Manifest {
324 file: file.clone(),
325 message: format!("[files.\"{written}\"] is {rendered:?} once answered, which is not a path in the target: {e}"),
326 })?;
327 let exists =
328 spec.when.exists.iter().map(|each| pattern(each)).collect::<Result<_>>()?;
329 let entry = LayerFile { written: written.clone(), spec, exists, gated: None };
330 if let Some(first) = files.insert(path.clone(), entry) {
331 let (profile, first) = (layer.name().clone(), first.written);
332 return Err(ProfileError::SamePath { profile, path, first, second: written }.into());
333 }
334 }
335 let scaffolds = scaffolds
336 .into_iter()
337 .map(|(name, spec)| {
338 Ok((name, spec.unless.0.iter().map(|each| pattern(each)).collect::<Result<_>>()?))
339 })
340 .collect::<Result<_>>()?;
341 layer.set_files(files, scaffolds);
342 }
343 Ok(())
344}
345
346pub(crate) fn render(
354 layers: &mut [Layer], answers: &BTreeMap<VarName, String>, context: &Context,
355) -> Result<()> {
356 for layer in layers {
357 let templates = Renderer::new(layer, answers, context);
358 let (mut payload, mut starters) = (Tree::default(), Tree::default());
359 let bytes_of = |written: &RelPath, template: bool| -> Result<Vec<u8>> {
360 let bytes = layer.written().get(written).unwrap_or_default();
361 if !template {
362 return Ok(bytes.to_vec());
363 }
364 let source = str::from_utf8(bytes).map_err(|e| VarError::Template {
365 path: written.clone(),
366 reason: format!("it is not UTF-8: {e}"),
367 })?;
368 let file = layer.label(&format!("{PAYLOAD}/{written}"));
369 Ok(templates.render(written, source, &file)?.into_bytes())
370 };
371 for (path, file) in layer.files().iter().filter(|(_, file)| file.gated.is_none()) {
372 payload.put(path.clone(), &bytes_of(&file.written, file.spec.template)?);
373 if let Some(starter) = &file.spec.starter {
374 starters.put(path.clone(), &bytes_of(starter, file.spec.template)?);
375 }
376 }
377 layer.set_payload(payload, starters);
378 }
379 Ok(())
380}
381
382#[cfg(test)]
383mod tests {
384 use super::VarName;
385
386 #[test]
387 fn names_are_identifiers() {
388 for name in ["author", "_x", "target_cpu2"] {
389 assert!(VarName::try_from(name.to_owned()).is_ok(), "{name} is valid");
390 }
391 for name in ["", "2x", "a-b", "a.b", "é", "devset"] {
392 assert!(VarName::try_from(name.to_owned()).is_err(), "{name:?} is invalid");
393 }
394 }
395}