use std::sync::Arc;
use camino::Utf8PathBuf;
use crate::ast::{Recipe, Rule, StringOrList};
use crate::hasher::ActionHasher;
use crate::localization::{self, keys};
use crate::recipe_shell::RecipeShell;
use super::super::{
cmd_interpolate::{
CommandBindings, interpolate_command_with_bindings, interpolate_script_with_bindings,
},
graph::{Action, BuildEdge, IrGenError, IrHashMap},
};
#[path = "sort_utils.rs"]
mod sort_utils;
#[derive(Clone, Copy)]
pub(super) struct ActionBindings<'a> {
pub(super) inputs: &'a [Utf8PathBuf],
pub(super) outputs: &'a [Utf8PathBuf],
pub(super) shell: RecipeShell,
}
pub(super) fn register_action(
actions: &mut IrHashMap<String, Action>,
recipe: Recipe,
description: Option<&str>,
bindings: ActionBindings<'_>,
) -> Result<String, IrGenError> {
let action = Action {
recipe: resolve_recipe(recipe, bindings)?,
description: description.map(ToOwned::to_owned),
depfile: None,
deps_format: None,
pool: None,
restat: false,
};
let hash = ActionHasher::hash(&action).map_err(|err| IrGenError::ActionSerialisation {
message: localization::message(keys::IR_ACTION_SERIALISATION)
.with_arg("details", err.to_string()),
source: err,
})?;
if !actions.contains_key(hash.as_str()) {
actions.insert(hash.clone(), action);
}
Ok(hash)
}
fn resolve_recipe(recipe: Recipe, bindings: ActionBindings<'_>) -> Result<Recipe, IrGenError> {
match recipe {
Recipe::Command { command } => Ok(Recipe::Command {
command: resolve_command(command, bindings)?,
}),
Recipe::Script { script } => Ok(Recipe::Script {
script: resolve_script(&script, bindings)?,
}),
rule @ Recipe::Rule { .. } => Ok(rule),
}
}
fn resolve_command(
command: StringOrList,
bindings: ActionBindings<'_>,
) -> Result<StringOrList, IrGenError> {
let command_bindings = CommandBindings::new(bindings.inputs, bindings.outputs, bindings.shell);
match command {
StringOrList::String(scalar_command) => Ok(StringOrList::String(
interpolate_command_with_bindings(&scalar_command, &command_bindings)?,
)),
StringOrList::List(items) => items
.into_iter()
.map(|item| interpolate_command_with_bindings(&item, &command_bindings))
.collect::<Result<Vec<_>, _>>()
.map(StringOrList::List),
StringOrList::Empty => Ok(StringOrList::Empty),
}
}
fn resolve_script(script: &str, bindings: ActionBindings<'_>) -> Result<String, IrGenError> {
interpolate_script_with_bindings(
script,
&CommandBindings::new(bindings.inputs, bindings.outputs, bindings.shell),
)
}
pub(super) fn duplicate_output_error(
outputs: &[Utf8PathBuf],
targets: &IrHashMap<Utf8PathBuf, BuildEdge>,
) -> Option<IrGenError> {
find_duplicates(outputs, targets).map(duplicate_output_error_from_paths)
}
pub(super) fn insert_edge_for_outputs(
targets: &mut IrHashMap<Utf8PathBuf, BuildEdge>,
edge: BuildEdge,
) {
if let Some((last_output, other_outputs)) = edge.explicit_outputs.split_last() {
for output in other_outputs {
targets.insert(output.clone(), edge.clone());
}
targets.insert(last_output.clone(), edge);
}
}
fn duplicate_output_error_from_paths(dups: Vec<Utf8PathBuf>) -> IrGenError {
let message = duplicate_outputs_message(&dups);
IrGenError::DuplicateOutput {
message,
outputs: dups.into_iter().map(|p| p.as_str().to_owned()).collect(),
}
}
fn duplicate_outputs_message(dups: &[Utf8PathBuf]) -> localization::LocalizedMessage {
add_debug_arg(
localization::message(keys::IR_DUPLICATE_OUTPUTS),
"outputs",
dups,
)
}
#[cfg(not(kani))]
fn add_arg<T: ToString + ?Sized>(
message: localization::LocalizedMessage,
key: &'static str,
value: &T,
) -> localization::LocalizedMessage {
message.with_arg(key, value.to_string())
}
#[cfg(kani)]
fn add_arg<T: ?Sized>(
message: localization::LocalizedMessage,
_key: &'static str,
_value: &T,
) -> localization::LocalizedMessage {
message
}
#[cfg(not(kani))]
fn add_debug_arg(
message: localization::LocalizedMessage,
key: &'static str,
value: impl std::fmt::Debug,
) -> localization::LocalizedMessage {
let rendered = format!("{value:?}");
add_arg(message, key, &rendered)
}
#[cfg(kani)]
fn add_debug_arg<T: ?Sized>(
message: localization::LocalizedMessage,
_key: &'static str,
_value: &T,
) -> localization::LocalizedMessage {
message
}
pub(super) fn to_paths(sol: &StringOrList) -> Vec<Utf8PathBuf> {
sol.map_each(|s| Utf8PathBuf::from(s))
}
pub(super) fn resolve_rule(
rule: &StringOrList,
rule_map: &IrHashMap<String, Arc<Rule>>,
target_name: &str,
) -> Result<Arc<Rule>, IrGenError> {
rule.as_single().map_or_else(
|| {
let mut rules = rule.to_string_vec();
if rules.is_empty() {
Err(empty_rule_error(target_name))
} else {
sort_utils::sort_strings(&mut rules);
Err(multiple_rules_error(target_name, rules))
}
},
|name| {
rule_map
.get(name)
.cloned()
.ok_or_else(|| rule_not_found_error(target_name, name))
},
)
}
fn empty_rule_error(target_name: &str) -> IrGenError {
IrGenError::EmptyRule {
target_name: target_name.to_owned(),
message: empty_rule_message(target_name),
}
}
fn multiple_rules_error(target_name: &str, rules: Vec<String>) -> IrGenError {
IrGenError::MultipleRules {
target_name: target_name.to_owned(),
message: multiple_rules_message(target_name, &rules),
rules,
}
}
fn rule_not_found_error(target_name: &str, rule_name: &str) -> IrGenError {
IrGenError::RuleNotFound {
target_name: target_name.to_owned(),
rule_name: rule_name.to_owned(),
message: rule_not_found_message(target_name, rule_name),
}
}
fn empty_rule_message(target_name: &str) -> localization::LocalizedMessage {
add_arg(
localization::message(keys::IR_EMPTY_RULE),
"target",
target_name,
)
}
fn multiple_rules_message(target_name: &str, rules: &[String]) -> localization::LocalizedMessage {
let message = localization::message(keys::IR_MULTIPLE_RULES);
let with_target = add_arg(message, "target", target_name);
add_debug_arg(with_target, "rules", rules)
}
fn rule_not_found_message(target_name: &str, rule_name: &str) -> localization::LocalizedMessage {
let message = localization::message(keys::IR_RULE_NOT_FOUND);
let with_target = add_arg(message, "target", target_name);
add_arg(with_target, "rule", rule_name)
}
pub(super) fn find_duplicates(
outputs: &[Utf8PathBuf],
targets: &IrHashMap<Utf8PathBuf, BuildEdge>,
) -> Option<Vec<Utf8PathBuf>> {
let mut seen: Vec<&Utf8PathBuf> = Vec::new();
let mut dups = Vec::new();
let mut index = 0;
while index < outputs.len() {
if let Some(output) = outputs.get(index) {
if targets.contains_key(output) || sort_utils::has_seen_output(seen.as_slice(), output)
{
dups.push(output.clone());
} else {
seen.push(output);
}
}
index += 1;
}
if dups.is_empty() {
None
} else {
if dups.len() > 1 {
sort_utils::sort_paths(&mut dups);
}
Some(dups)
}
}
pub(super) fn get_target_display_name(paths: &[Utf8PathBuf]) -> String {
paths
.first()
.map(|p: &Utf8PathBuf| p.to_string())
.unwrap_or_default()
}
#[cfg(test)]
#[path = "from_manifest_support_tests.rs"]
mod tests;