use anyhow::{Result, ensure};
pub fn make_assignment<'a>(makefile: &'a str, name: &str) -> Option<&'a str> {
makefile.lines().find_map(|line| {
let rest = line.strip_prefix(name)?.trim_start();
for operator in ["?=", ":=", "="] {
if let Some(value) = rest.strip_prefix(operator) {
return Some(value.trim());
}
}
None
})
}
pub fn expand_make_variables(makefile: &str, command: &str) -> String {
let mut expanded = command.to_owned();
for _ in 0..2 {
let mut next = String::with_capacity(expanded.len());
let mut rest = expanded.as_str();
while let Some(start) = rest.find("$(") {
let Some(end) = rest[start..].find(')').map(|offset| start + offset) else {
break;
};
next.push_str(&rest[..start]);
let name = &rest[start + 2..end];
match make_assignment(makefile, name) {
Some(value) => next.push_str(value),
None => next.push_str(&rest[start..=end]),
}
rest = &rest[end + 1..];
}
next.push_str(rest);
expanded = next;
}
expanded
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecipeCommand {
pub text: String,
pub ignores_errors: bool,
}
pub fn recipe_commands(makefile: &str, target: &str) -> Result<Vec<RecipeCommand>> {
let prefix = format!("{target}:");
let declarations = makefile
.lines()
.filter(|line| line.starts_with(&prefix))
.count();
ensure!(
declarations == 1,
concat!(
"the {target} target should be declared once, found {declarations} declarations; ",
"GNU Make would run the last and this contract would judge the first"
),
target = target,
declarations = declarations
);
let body = makefile
.lines()
.skip_while(|line| !line.starts_with(&prefix))
.skip(1);
let mut commands = Vec::new();
let mut pending: Option<RecipeCommand> = None;
for line in body {
let Some(text) = line.strip_prefix('\t') else {
if line.trim().is_empty() {
continue;
}
break;
};
let text = text.trim_end();
let continues = text.ends_with('\\');
let text = text.strip_suffix('\\').unwrap_or(text).trim();
if let Some(started) = pending.as_mut() {
started.text.push(' ');
started.text.push_str(text);
} else {
let body = text.trim_start_matches(['@', '-', '+']);
let ignores_errors = text[..text.len() - body.len()].contains('-');
pending = Some(RecipeCommand {
text: body.trim().to_owned(),
ignores_errors,
});
}
if !continues && let Some(command) = pending.take() {
push_command(&mut commands, &command);
}
}
if let Some(command) = pending {
push_command(&mut commands, &command);
}
ensure!(
!commands.is_empty(),
"the {target} target should have a recipe"
);
Ok(commands)
}
fn push_command(commands: &mut Vec<RecipeCommand>, command: &RecipeCommand) {
let text = command.text.trim();
if text.is_empty() || text.starts_with('#') {
return;
}
commands.push(RecipeCommand {
text: text.to_owned(),
ignores_errors: command.ignores_errors,
});
}