use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::env;
use std::fmt;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use eyre::{bail, eyre, Result, WrapErr};
use command_line::CommandLine;
#[cfg(feature = "full")]
use functions::NO_EVAL;
use inference_rules::{InferenceRule, InferenceRuleSet};
use input::FinishedMakefileReader;
pub use input::MakefileReader;
use lookup_internal::LookupInternal;
pub use macro_scope::MacroScopeStack;
use r#macro::Macro;
pub use r#macro::Set as MacroSet;
use target::{DynamicTargetSet, Target};
use token::TokenString;
use crate::args::Args;
mod command_line;
#[cfg(feature = "full")]
mod conditional;
#[cfg(feature = "full")]
mod eval_context;
#[cfg(feature = "full")]
mod functions;
mod inference_rules;
mod input;
mod lookup_internal;
mod r#macro;
mod macro_scope;
mod parse;
mod pattern;
mod target;
mod token;
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ItemSource {
File {
name: String,
line: usize,
},
CommandLineOrMakeflags,
Environment,
Builtin,
#[cfg(feature = "full")]
FunctionCall,
}
pub struct Makefile<'a> {
inference_rules: InferenceRuleSet,
builtin_inference_rules: Vec<InferenceRule>,
pub macros: MacroSet,
targets: DynamicTargetSet,
pub first_non_special_target: Option<String>,
args: &'a Args,
already_inferred: RefCell<HashSet<String>>,
}
impl<'a> Makefile<'a> {
pub fn new(args: &'a Args) -> Self {
let mut inference_rules = vec![];
let mut macros = MacroSet::new();
let targets = DynamicTargetSet::default();
let first_non_special_target = None;
macros.set(
"MAKE".to_owned(),
Macro {
source: ItemSource::Builtin,
text: env::current_exe().map_or_else(
|_| TokenString::text("makers"),
|x| TokenString::text(x.to_string_lossy()),
),
#[cfg(feature = "full")]
eagerly_expanded: false,
},
);
if !args.no_builtin_rules {
inference_rules.extend(builtin_inference_rules());
macros.add_builtins();
for target in builtin_targets() {
targets.put(target);
}
}
macros.add_env();
for r#macro in args.macros() {
if let [name, value] = *r#macro.splitn(2, '=').collect::<Vec<_>>() {
macros.set(
name.into(),
Macro {
source: ItemSource::CommandLineOrMakeflags,
text: TokenString::text(value),
#[cfg(feature = "full")]
eagerly_expanded: false,
},
);
}
}
#[cfg(feature = "full")]
{
let make_cmd_goals = args.targets().collect::<Vec<_>>();
macros.set(
"MAKECMDGOALS".to_owned(),
Macro {
source: ItemSource::Builtin,
text: TokenString::text(make_cmd_goals.join(" ")),
#[cfg(feature = "full")]
eagerly_expanded: false,
},
);
if let Ok(curdir) = env::current_dir() {
macros.set(
"CURDIR".to_owned(),
Macro {
source: ItemSource::Builtin,
text: TokenString::text(curdir.to_string_lossy()),
#[cfg(feature = "full")]
eagerly_expanded: false,
},
);
}
}
Makefile {
inference_rules: InferenceRuleSet::default(),
builtin_inference_rules: inference_rules,
macros,
targets,
first_non_special_target,
args,
already_inferred: Default::default(),
}
}
pub fn extend(&mut self, new: FinishedMakefileReader) -> Result<()> {
self.inference_rules.extend(new.inference_rules);
self.macros.extend(new.macros);
for (_, target) in new.targets {
self.targets.put(target);
}
if self.first_non_special_target.is_none() {
self.first_non_special_target = new.first_non_special_target;
}
for failed_include in new.failed_includes {
self.update_target(&failed_include).wrap_err_with(|| {
format!("while building missing included file {}", &failed_include)
})?;
let stack = MacroScopeStack::default().with_scope(&self.macros);
let file =
MakefileReader::read_file(self.args, stack, failed_include, Default::default())?
.finish();
self.extend(file)?;
}
Ok(())
}
fn special_target_has_prereq(&self, target: &str, name: &str) -> bool {
self.targets.get(target).map_or(false, |target| {
let target = target.borrow();
target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name)
})
}
fn infer_target(
&self,
name: &str,
banned_rules: Vec<&InferenceRule>,
banned_names: Vec<&str>,
) -> Result<()> {
if banned_names.contains(&name) {
bail!("no infinite recursion allowed");
}
if self.already_inferred.borrow().contains(name) {
return Ok(());
}
self.already_inferred.borrow_mut().insert(name.to_owned());
log::trace!("inferring {}, stack = {:?}", name, banned_names);
let mut new_target = None;
let follow_gnu = cfg!(feature = "full");
let vpath_options = match self.macros.get_non_recursive("VPATH") {
Some(Macro { text, .. }) if follow_gnu => {
let vpath = self.expand_macros(text, None)?;
env::split_paths(&vpath).collect()
}
_ => vec![],
};
let inference_rule_candidates = self
.inference_rules
.iter()
.chain(self.builtin_inference_rules.iter())
.filter(|rule| {
!banned_rules.iter().any(|banned_rule| {
banned_rule.products == rule.products
&& banned_rule.prerequisites == rule.prerequisites
})
})
.filter(|rule| rule.matches(name).unwrap_or(false));
for rule in inference_rule_candidates {
log::trace!(
"{} considering rule to build {:?} from {:?}",
name,
&rule.products,
&rule.prerequisites
);
let prereq_paths = rule
.prereqs(name)?
.map(|prereq_path_name| {
if name == prereq_path_name || banned_names.contains(&&*prereq_path_name) {
return None;
}
if self.targets.has(&prereq_path_name) {
return Some(prereq_path_name);
}
let prereq_path = PathBuf::from(&prereq_path_name);
let prereq_vpath_options = if prereq_path.is_absolute() {
None
} else {
Some(vpath_options.iter().map(|vpath| vpath.join(&prereq_path)))
}
.into_iter()
.flatten();
std::iter::once(prereq_path.clone())
.chain(prereq_vpath_options)
.find(|prereq| prereq.exists())
.map(|path| path.to_string_lossy().to_string())
.or_else(|| {
let mut banned_rules = banned_rules.clone();
banned_rules.push(rule);
let mut banned_names = banned_names.clone();
banned_names.push(name);
self.infer_target(&prereq_path_name, banned_rules, banned_names)
.ok()
.and_then(|_| {
if self.targets.has(&prereq_path_name) {
Some(prereq_path_name)
} else {
None
}
})
})
})
.collect::<Option<Vec<String>>>();
if let Some(prereqs) = prereq_paths {
log::trace!("oh {} is a {}", name, rule);
new_target = Some(Target {
name: name.into(),
prerequisites: prereqs,
commands: rule.commands.clone(),
stem: rule
.first_match(name)?
.and_then(|x| x.get(1).map(|x| x.as_str().to_owned())),
already_updated: Cell::new(false),
macros: MacroSet::new(),
});
break;
}
}
if let Some(new_target) = new_target {
self.targets.put(new_target);
}
Ok(())
}
pub fn get_target(&self, name: &str) -> Result<Rc<RefCell<Target>>> {
let follow_gnu = cfg!(feature = "full");
#[cfg(feature = "full")]
let name = name.strip_prefix("./").unwrap_or(name);
let exists_but_infer_anyway = if follow_gnu {
self.targets
.get(name)
.map_or(false, |target| target.borrow().commands.is_empty())
} else {
false
};
if !self.targets.has(name) || exists_but_infer_anyway {
log::trace!("trying to infer for {}", name);
self.infer_target(name, vec![], vec![])?;
}
let mut new_target = None;
if !self.targets.has(name) {
if let Some(default) = self.targets.get(".DEFAULT") {
let commands = default.borrow().commands.clone();
new_target = Some(Target {
name: name.into(),
prerequisites: vec![],
commands,
stem: None,
already_updated: Cell::new(false),
macros: MacroSet::new(),
});
} else {
if Path::new(name).exists() {
new_target = Some(Target {
name: name.into(),
prerequisites: vec![],
commands: vec![],
stem: None,
already_updated: Cell::new(true),
macros: MacroSet::new(),
});
}
}
}
if let Some(new_target) = new_target {
self.targets.put(new_target);
}
self.targets
.get(name)
.ok_or_else(|| eyre!("Target {:?} not found!", name))
}
pub fn update_target(&self, name: &str) -> Result<()> {
let target = self.get_target(name);
match target {
Err(err)
if err.to_string().contains(" not found!")
&& self.special_target_has_prereq(".PHONY", name) =>
{
Ok(())
}
_ => target?.borrow().update(self),
}
}
fn expand_macros(&self, text: &TokenString, target: Option<&Target>) -> Result<String> {
MacroScopeStack::default()
.with_scope(&self.macros)
.with_scope(&LookupInternal::new(target, &|name| self.get_target(name)))
.with_scope(&target.map(|target| &target.macros))
.expand(
text,
#[cfg(feature = "full")]
NO_EVAL,
)
}
}
impl fmt::Display for Makefile<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let header = |f: &mut fmt::Formatter, t: &str| {
writeln!(f, "{}\n{:=^width$}", t, "", width = t.len())
};
header(f, "Inference Rules")?;
for rule in self
.inference_rules
.iter()
.chain(self.builtin_inference_rules.iter())
{
writeln!(f, "{}", rule)?;
}
writeln!(f)?;
header(f, "Macros")?;
writeln!(f, "{}", &self.macros)?;
writeln!(f)?;
header(f, "Targets")?;
writeln!(f, "{}", &self.targets)?;
Ok(())
}
}
fn builtin_inference_rules() -> Vec<InferenceRule> {
macro_rules! prepend_dot {
($x:tt) => {
concat!(".", stringify!($x))
};
() => {
""
};
}
macro_rules! make {
{$(.$first:tt$(.$second:tt)?:
$($cmd:literal)+)+} => {
vec![$(
InferenceRule::new_suffix(
ItemSource::Builtin,
prepend_dot!($($second)?).into(),
concat!(".", stringify!($first)).into(),
vec![$(CommandLine::from($cmd.parse().unwrap())),+],
MacroSet::new(),
)
),+]
};
}
make! {
.c:
"$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $<"
.f:
"$(FC) $(FFLAGS) $(LDFLAGS) -o $@ $<"
.sh:
"cp $< $@"
"chmod a+x $@"
.c.o:
"$(CC) $(CFLAGS) -c $<"
.f.o:
"$(FC) $(FFLAGS) -c $<"
.y.o:
"$(YACC) $(YFLAGS) $<"
"$(CC) $(CFLAGS) -c y.tab.c"
"rm -f y.tab.c"
"mv y.tab.o $@"
.l.o:
"$(LEX) $(LFLAGS) $<"
"$(CC) $(CFLAGS) -c lex.yy.c"
"rm -f lex.yy.c"
"mv lex.yy.o $@"
.y.c:
"$(YACC) $(YFLAGS) $<"
"mv y.tab.c $@"
.l.c:
"$(LEX) $(LFLAGS) $<"
"mv lex.yy.c $@"
.c.a:
"$(CC) -c $(CFLAGS) $<"
"$(AR) $(ARFLAGS) $@ $*.o"
"rm -f $*.o"
.f.a:
"$(FC) -c $(FFLAGS) $<"
"$(AR) $(ARFLAGS) $@ $*.o"
"rm -f $*.o"
}
}
fn builtin_targets() -> Vec<Target> {
vec![Target {
name: ".SUFFIXES".into(),
prerequisites: vec![".o", ".c", ".y", ".l", ".a", ".sh", ".f"]
.into_iter()
.map(String::from)
.collect(),
commands: vec![],
stem: None,
already_updated: Cell::new(false),
macros: MacroSet::new(),
}]
}
#[cfg(test)]
mod test {
use super::*;
type R = Result<()>;
#[cfg(feature = "full")]
#[test]
fn stem() -> R {
let args = Args::empty();
let rule = InferenceRule {
source: ItemSource::Builtin,
products: vec!["this-is-a-%-case".to_owned()],
prerequisites: vec![],
commands: vec![],
macros: MacroSet::new(),
};
let file = Makefile {
inference_rules: vec![rule].into(),
builtin_inference_rules: vec![],
macros: MacroSet::new(),
targets: Default::default(),
first_non_special_target: None,
args: &args,
already_inferred: Default::default(),
};
let target = file.get_target("this-is-a-test-case")?;
assert_eq!(target.borrow().stem, Some("test".to_owned()));
Ok(())
}
#[cfg(feature = "full")]
#[test]
fn missing_phony_targets_ignored() -> R {
let args = Args::empty();
let target = Target {
name: "all".to_owned(),
prerequisites: vec!["missing".to_owned()],
commands: vec![],
stem: None,
already_updated: Cell::new(false),
macros: MacroSet::new(),
};
let phony = Target {
name: ".PHONY".to_string(),
prerequisites: vec!["missing".to_owned()],
commands: vec![],
stem: None,
already_updated: Cell::new(false),
macros: MacroSet::new(),
};
let targets = DynamicTargetSet::default();
targets.put(target);
targets.put(phony);
let file = Makefile {
inference_rules: InferenceRuleSet::default(),
builtin_inference_rules: vec![],
macros: MacroSet::new(),
targets,
first_non_special_target: None,
args: &args,
already_inferred: Default::default(),
};
assert!(file.update_target("all").is_ok());
Ok(())
}
}