use std::fs;
use std::path::Path;
use super::{Gap, RebuildCheck, relative_parts, split_relative};
pub(crate) struct MakeTargets;
impl RebuildCheck for MakeTargets {
fn tools(&self) -> &'static [&'static str] {
&["make", "gmake", "mingw32-make"]
}
fn gap(&self, repo_path: &Path, _tool: &str, args: &[&str]) -> Option<Gap> {
make_target_gap(repo_path, args)
}
}
fn make_target_gap(repo_path: &Path, args: &[&str]) -> Option<Gap> {
let mut dir = None;
let mut file = None;
let mut target = None;
let mut i = 0;
while i < args.len() {
let arg = args[i];
if arg == "--" {
return None;
}
if let Some(value) = arg.strip_prefix("--directory=") {
dir = Some(value.to_string());
} else if let Some(value) = arg
.strip_prefix("--file=")
.or_else(|| arg.strip_prefix("--makefile="))
{
file = Some(value.to_string());
} else if arg == "-C" || arg == "--directory" {
dir = Some((*args.get(i + 1)?).to_string());
i += 1;
} else if arg == "-f" || arg == "--file" || arg == "--makefile" {
file = Some((*args.get(i + 1)?).to_string());
i += 1;
} else if let Some(value) = arg.strip_prefix("-C").filter(|v| !v.is_empty()) {
dir = Some(value.to_string());
} else if let Some(value) = arg.strip_prefix("-f").filter(|v| !v.is_empty()) {
file = Some(value.to_string());
} else if arg.starts_with('-') {
return None;
} else if !arg.contains('=') {
target = Some(arg.to_string());
break;
}
i += 1;
}
let target = target?;
let base = relative_parts(dir.as_deref())?;
let candidates: Vec<Vec<String>> = match &file {
Some(name) => vec![split_relative(name).ok()?],
None => ["Makefile", "makefile", "GNUmakefile"]
.iter()
.map(|name| vec![(*name).to_string()])
.collect(),
};
let (parts, content) = candidates.into_iter().find_map(|name| {
let mut parts = base.clone();
parts.extend(name);
let full = parts
.iter()
.fold(repo_path.to_path_buf(), |acc, part| acc.join(part));
fs::read_to_string(full).ok().map(|text| (parts, text))
})?;
if make_targets(&content)?.contains(&target) {
return None;
}
let manifest = parts.join("/");
Some(Gap {
what: format!("`{manifest}` defines no `{target}` target"),
fix: format!("Add a `{target}` target to `{manifest}`, or fix the command."),
})
}
fn make_targets(content: &str) -> Option<Vec<String>> {
let mut targets = Vec::new();
for raw in content.lines() {
if raw.starts_with('\t') {
continue;
}
let line = raw.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with("include")
|| line.starts_with("-include")
|| line.starts_with("sinclude")
{
return None;
}
let Some(colon) = line.find(':') else {
continue;
};
let rest = &line[colon..];
if rest.starts_with(":=") || rest.starts_with("::=") {
continue;
}
let head = &line[..colon];
if head.contains('=') {
continue;
}
if head.contains('%') || head.contains("$(") || head.contains("${") {
return None;
}
targets.extend(head.split_whitespace().map(str::to_string));
if head.split_whitespace().any(|word| word == ".PHONY") {
targets.extend(rest[1..].split_whitespace().map(str::to_string));
}
}
if targets.is_empty() {
None
} else {
Some(targets)
}
}