use super::ManifestValue;
use super::jinja_macros::render_template;
use crate::ast::{NetsukeManifest, Recipe, StringOrList, Target, Vars};
use crate::ir::{INS_TOKEN, OUTS_TOKEN};
use anyhow::{Context, Result};
use minijinja::Environment;
#[cfg(test)]
use std::cell::Cell;
pub fn render_manifest(
mut manifest: NetsukeManifest,
env: &Environment,
) -> Result<NetsukeManifest> {
for action in &mut manifest.actions {
render_target(action, env)?;
}
for target in &mut manifest.targets {
render_target(target, env)?;
}
let rule_vars = manifest.vars.clone();
for rule in &mut manifest.rules {
render_rule(rule, env, &rule_vars)?;
}
Ok(manifest)
}
fn render_rule(rule: &mut crate::ast::Rule, env: &Environment, vars: &Vars) -> Result<()> {
render_description(&mut rule.description, env, vars, "rule")?;
render_recipe(&mut rule.recipe, env, vars, "rule")?;
Ok(())
}
fn render_target(target: &mut Target, env: &Environment) -> Result<()> {
render_vars(&mut target.vars, env)?;
render_description(&mut target.description, env, &target.vars, "target")?;
render_string_or_list(&mut target.name, env, &target.vars)?;
render_string_or_list(&mut target.sources, env, &target.vars)?;
render_string_or_list(&mut target.deps, env, &target.vars)?;
render_string_or_list(&mut target.order_only_deps, env, &target.vars)?;
render_recipe(&mut target.recipe, env, &target.vars, "target")?;
Ok(())
}
fn render_description(
description: &mut Option<String>,
env: &Environment,
vars: &Vars,
subject: &str,
) -> Result<()> {
if let Some(desc) = description {
*desc = render_str_with(env, desc, vars, || format!("render {subject} description"))?;
}
Ok(())
}
fn render_recipe(recipe: &mut Recipe, env: &Environment, vars: &Vars, subject: &str) -> Result<()> {
match recipe {
Recipe::Command { command } => {
render_recipe_string_or_list(command, env, vars, || {
format!("render {subject} command")
})?;
}
Recipe::Script { script } => {
*script = render_str_with(env, script, vars, || format!("render {subject} script"))?;
}
Recipe::Rule { rule } => render_string_or_list(rule, env, vars)?,
}
Ok(())
}
fn render_vars(vars: &mut Vars, env: &Environment) -> Result<()> {
let snapshot = vars.clone();
for (key, value) in vars.iter_mut() {
if let ManifestValue::String(s) = value {
*s = render_str_with(env, s, &snapshot, || format!("render var '{key}'"))?;
}
}
Ok(())
}
fn render_string_or_list(value: &mut StringOrList, env: &Environment, ctx: &Vars) -> Result<()> {
match value {
StringOrList::String(s) => {
*s = render_str_with(env, s, ctx, || "render string value".into())?;
}
StringOrList::List(list) => {
for item in list {
*item = render_str_with(env, item, ctx, || "render list value".into())?;
}
}
StringOrList::Empty => {}
}
Ok(())
}
fn render_recipe_string_or_list(
value: &mut StringOrList,
env: &Environment,
ctx: &Vars,
what: impl FnOnce() -> String,
) -> Result<()> {
let label = what();
let recipe_ctx = recipe_render_context(ctx);
let render_entry = |entry: &mut String, position: Option<usize>| -> Result<()> {
*entry = render_str_with(env, entry, &recipe_ctx, || {
position.map_or_else(|| label.clone(), |index| format!("{label} entry {index}"))
})?;
Ok(())
};
match value {
StringOrList::String(s) => render_entry(s, None)?,
StringOrList::List(list) => {
for (index, item) in list.iter_mut().enumerate() {
render_entry(item, Some(index + 1))?;
}
}
StringOrList::Empty => {}
}
Ok(())
}
fn recipe_render_context(ctx: &Vars) -> Vars {
record_recipe_context_preparation();
let mut recipe_ctx = ctx.clone();
recipe_ctx.insert("ins".into(), ManifestValue::String(INS_TOKEN.into()));
recipe_ctx.insert("outs".into(), ManifestValue::String(OUTS_TOKEN.into()));
recipe_ctx
}
#[cfg(test)]
thread_local! {
static RECIPE_CONTEXT_PREPARATIONS: Cell<usize> = const { Cell::new(0) };
}
#[cfg(test)]
fn record_recipe_context_preparation() {
RECIPE_CONTEXT_PREPARATIONS.with(|count| count.set(count.get() + 1));
}
#[cfg(not(test))]
const fn record_recipe_context_preparation() {}
#[cfg(test)]
pub(super) fn reset_recipe_context_preparations() {
RECIPE_CONTEXT_PREPARATIONS.with(|count| count.set(0));
}
#[cfg(test)]
pub(super) fn recipe_context_preparations() -> usize {
RECIPE_CONTEXT_PREPARATIONS.with(Cell::get)
}
fn render_str_with(
env: &Environment,
tpl: &str,
ctx: &impl serde::Serialize,
what: impl FnOnce() -> String,
) -> Result<String> {
render_template(env, tpl, ctx).with_context(what)
}
#[cfg(test)]
#[path = "render_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "render_command_list_tests.rs"]
mod command_list_tests;