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; use inference_rules::InferenceRule; use input::FinishedMakefileReader; pub use input::MakefileReader; use r#macro::{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 functions; mod inference_rules; mod input; mod r#macro; 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: Vec, builtin_inference_rules: Vec, pub macros: MacroSet<'static, 'static>, targets: DynamicTargetSet, pub first_non_special_target: Option, args: &'a Args, already_inferred: RefCell>, // TODO borrow warnings from Python version } 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: std::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::>() { 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::>(); 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: vec![], 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, #[cfg(feature = "full")] new.macro_exports, ); 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 { // try rebuilding self.update_target(&failed_include).wrap_err_with(|| { format!("while building missing included file {}", &failed_include) })?; let macros = self.macros.with_overlay(); let file = MakefileReader::read_file(self.args, macros, failed_include, Default::default())? .finish(); self.extend(file)?; } Ok(()) } fn special_target_has_prereq(&self, target: &str, name: &str) -> bool { match self.targets.get(target) { Some(target) => { let target = target.borrow(); target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name) } None => false, } } 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("VPATH") { Some(Macro { text, .. }) if follow_gnu => { let vpath = self.expand_macros(text, None)?; env::split_paths(&vpath).collect() } _ => vec![], }; // When no target rule is found to update a target, the inference rules shall // be checked. The suffix of the target to be built is compared to the list of // suffixes specified by the .SUFFIXES special targets. If the .s1 suffix is // found in .SUFFIXES... // TODO bring back .SUFFIXES for suffix-based rules // the inference rules shall be searched in the order defined... // TODO implement GNUish shortest-stem-first matching let inference_rule_candidates = self .inference_rules .iter() .chain(self.builtin_inference_rules.iter()) .filter(|rule| !banned_rules.contains(rule)) .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 ); // whose prerequisite file ($*.s2) exists. let prereq_paths = rule .prereqs(name)? .map(|prereq_path_name| { if name == prereq_path_name || banned_names.contains(&&*prereq_path_name) { // we can't build this based on itself! fuck outta here 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::>>(); 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), }); break; } } if let Some(new_target) = new_target { self.targets.put(new_target); } Ok(()) } pub fn get_target(&self, name: &str) -> Result>> { // TODO implement .POSIX 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) { // well, inference didn't work. is there a default? 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), }); } else { // if it already exists, it counts as up-to-date if Path::new(name).exists() { new_target = Some(Target { name: name.into(), prerequisites: vec![], commands: vec![], stem: None, already_updated: Cell::new(true), }); } } } if let Some(new_target) = new_target { self.targets.put(new_target); } Ok(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 { // TODO make this less janky 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 { let target = target.cloned(); let lookup_internal = move |macro_name: &str| { let target = target .as_ref() .ok_or_else(|| eyre!("internal macro but no current target!"))?; let macro_pieces = if macro_name.starts_with('@') { // The $@ shall evaluate to the full target name of the // current target. vec![target.name.clone()] } else if macro_name.starts_with('?') { // The $? macro shall evaluate to the list of prerequisites // that are newer than the current target. target .prerequisites .iter() .filter(|prereq| { self.get_target(prereq) .ok() .and_then(|prereq| prereq.borrow().newer_than(target)) .unwrap_or(false) }) .cloned() .collect() } else if macro_name.starts_with('<') { // In an inference rule, the $< macro shall evaluate to the // filename whose existence allowed the inference rule to be // chosen for the target. In the .DEFAULT rule, the $< macro // shall evaluate to the current target name. // TODO make that actually be the case (rn exists_but_inferring_anyway might fuck that up) vec![target.prerequisites.get(0).cloned().unwrap_or_default()] } else if macro_name.starts_with('*') { // The $* macro shall evaluate to the current target name with // its suffix deleted. (GNUism: the match stem) vec![target.stem.as_ref().unwrap_or(&target.name).to_owned()] } else if macro_name.starts_with('^') { target.prerequisites.clone() } else { unreachable!() }; let macro_pieces = if macro_name.ends_with('D') { macro_pieces .into_iter() .map(|x| { Path::new(&x) .parent() .ok_or_else(|| eyre!("no parent")) .map(|x| x.to_string_lossy().into()) }) .collect::>()? } else if macro_name.ends_with('F') { macro_pieces .into_iter() .map(|x| { Path::new(&x) .file_name() .ok_or_else(|| eyre!("no filename")) .map(|x| x.to_string_lossy().into()) }) .collect::>()? } else { macro_pieces }; Ok(macro_pieces.join(" ")) }; self.macros.with_lookup(&lookup_internal).expand(text) } } 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 { // This is a terrible idea. 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())),+], ) ),+] }; } 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 { // even i'm not going to do that just for this 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), }] } #[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![], }; let file = Makefile { inference_rules: vec![rule], 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), }; let phony = Target { name: ".PHONY".to_string(), prerequisites: vec!["missing".to_owned()], commands: vec![], stem: None, already_updated: Cell::new(false), }; let targets = DynamicTargetSet::default(); targets.put(target); targets.put(phony); let file = Makefile { inference_rules: vec![], 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(()) } }