use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::env; use std::fmt; use std::fs::{metadata, File}; use std::io::{BufRead, BufReader}; use std::path::Path; use std::rc::Rc; use std::time::SystemTime; use lazy_static::lazy_static; use regex::Regex; use crate::args::Args; mod token; use token::{tokenize, Token, TokenString}; #[derive(PartialEq, Eq, Clone, Debug)] pub struct InferenceRule { /// POSIX calls this ".s1" but that's not useful. product: String, /// POSIX calls this ".s2" but that's not useful. prereq: String, commands: Vec, } impl fmt::Display for InferenceRule { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "{}{}:", &self.prereq, &self.product)?; for command in &self.commands { writeln!(f, "\t{}", command)?; } Ok(()) } } #[derive(PartialEq, Eq, Clone, Debug)] pub struct Target { name: String, prerequisites: Vec, commands: Vec, already_updated: Cell, } impl Target { fn modified_time(&self) -> Option { metadata(&self.name) .and_then(|metadata| metadata.modified()) .ok() } fn newer_than(&self, other: &Target) -> Option { let self_updated = self.already_updated.get(); let other_updated = other.already_updated.get(); Some(match (self.modified_time(), other.modified_time()) { (Some(self_mtime), Some(other_mtime)) => self_mtime >= other_mtime, // per POSIX: "If the target does not exist after the target has been // successfully made up-to-date, the target shall be treated as being // newer than any target for which it is a prerequisite." (None, _) if self_updated && other.prerequisites.contains(&self.name) => true, (_, None) if other_updated && self.prerequisites.contains(&other.name) => false, _ => return None, }) } fn is_up_to_date(&self, file: &Makefile) -> bool { if self.already_updated.get() { return true; } let exists = metadata(&self.name).is_ok(); let newer_than_all_dependencies = self.prerequisites.iter().all(|t| { self.newer_than(&file.get_target(t).borrow()) .unwrap_or(false) }); exists && newer_than_all_dependencies } pub fn update(&self, file: &Makefile) { for prereq in &self.prerequisites { file.update_target(prereq); } if !self.is_up_to_date(file) { self.execute_commands(file); } self.already_updated.set(true); } fn execute_commands(&self, file: &Makefile) { for command in &self.commands { command.execute(file, self); } } } impl fmt::Display for Target { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let prereqs = self.prerequisites.join(" "); writeln!(f, "{}: {}", &self.name, prereqs)?; for command in &self.commands { writeln!(f, "\t{}", command)?; } Ok(()) } } #[derive(PartialEq, Eq, Clone, Debug)] pub struct CommandLine { /// If the command prefix contains a , or the -i option is present, or /// the special target .IGNORE has either the current target as a prerequisite or has /// no prerequisites, any error found while executing the command shall be ignored. ignore_errors: bool, /// If the command prefix contains an at-sign and the make utility command line -n /// option is not specified, or the -s option is present, or the special target /// .SILENT has either the current target as a prerequisite or has no prerequisites, /// the command shall not be written to standard output before it is executed. silent: bool, /// If the command prefix contains a , this indicates a makefile command /// line that shall be executed even if -n, -q, or -t is specified. always_execute: bool, execution_line: TokenString, } impl CommandLine { fn from(mut line: TokenString) -> Self { let mut ignore_errors = false; let mut silent = false; let mut always_execute = false; if let Token::Text(text) = line.first_token_mut() { let mut text_chars = text.chars().peekable(); while let Some(x) = text_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { match x { '-' => ignore_errors = true, '@' => silent = true, '+' => always_execute = true, _ => unreachable!(), } } *text = text_chars.collect(); } CommandLine { ignore_errors, silent, always_execute, execution_line: line, } } fn execute(&self, file: &Makefile, target: &Target) { let ignore_error = self.ignore_errors || file.args.ignore_errors || file.special_target_has_prereq(".IGNORE", &target.name); let silent = (self.silent && !file.args.dry_run) || file.args.silent || file.special_target_has_prereq(".SILENT", &target.name); let execution_line = file.expand_macros(&self.execution_line, Some(target)); if !silent { println!("{}", execution_line); } let should_execute = self.always_execute || !(file.args.dry_run || file.args.question || file.args.touch); if !should_execute { return; } // TODO don't fuck this up let execution_line = ::std::ffi::CString::new(execution_line.as_bytes()) .expect("execution line shouldn't have a null in the middle"); // TODO pass shell "-e" if errors are not ignored let return_value = unsafe { libc::system(execution_line.as_ptr()) }; if return_value != 0 { // apparently there was an error. do we care? if !ignore_error { // TODO handle this error gracefully panic!("error from command execution!"); } } } } impl fmt::Display for CommandLine { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if self.ignore_errors { write!(f, "-")?; } if self.silent { write!(f, "@")?; } if self.always_execute { write!(f, "+")?; } let execution_line = format!("{}", &self.execution_line); let execution_line = execution_line.replace("\n", "↵\n"); write!(f, "{}", execution_line)?; Ok(()) } } enum MacroSource { File, CommandLineOrMAKEFLAGS, Environment, Builtin, } pub struct Makefile { inference_rules: Vec, macros: HashMap, targets: RefCell>>>, pub first_non_special_target: Option, args: Args, // TODO borrow warnings from Python version } impl Makefile { pub fn new(args: Args) -> Makefile { let mut inference_rules = vec![]; let mut macros = HashMap::new(); let mut targets = HashMap::new(); let first_non_special_target = None; if !args.no_builtin_rules { inference_rules.extend(builtin_inference_rules()); for (k, v) in builtin_macros() { macros.insert(k.into(), (MacroSource::Builtin, v)); } targets.extend( builtin_targets() .into_iter() .map(|t| (t.name.clone(), Rc::new(RefCell::new(t)))), ); } for (k, v) in env::vars() { if k != "MAKEFLAGS" && k != "SHELL" { macros.insert(k, (MacroSource::Environment, TokenString::text(v))); } } for r#macro in args.macros() { if let [name, value] = *r#macro.splitn(2, '=').collect::>() { macros.insert( name.into(), ( MacroSource::CommandLineOrMAKEFLAGS, TokenString::text(value), ), ); } } Makefile { inference_rules, macros, targets: RefCell::new(targets), first_non_special_target, args, } } pub fn and_read_file(&mut self, path: impl AsRef) -> &mut Makefile { let file = File::open(path); // TODO handle errors let file = file.expect("couldn't open makefile!"); let file_reader = BufReader::new(file); self.and_read(file_reader) } pub fn and_read(&mut self, source: impl BufRead) -> &mut Makefile { let mut lines_iter = source.lines().enumerate().peekable(); while let Some((line_number, line)) = lines_iter.next() { // TODO handle I/O errors at all let mut line = line.expect("failed to read line of makefile!"); // handle escaped newlines while line.ends_with('\\') { line.pop(); line.push(' '); if let Some((_, x)) = lines_iter.next() { line.push_str(x.expect("failed to read line of makefile!").trim_start()) } } // handle comments lazy_static! { static ref COMMENT: Regex = Regex::new("#.*$").unwrap(); } let line = COMMENT.replace(&line, "").into_owned(); // handle include lines if let Some(line) = line.strip_prefix("include ") { // remove extra leading space let line = line.trim_start(); let line = self.expand_macros(&tokenize(line), None); let fields = line.split_whitespace(); // POSIX says we only have to handle a single filename, but GNU make // handles arbitrarily many filenames, and it's not like that's more work for field in fields { self.and_read_file(field); } } else if line.trim().is_empty() { // handle blank lines continue; } else { // unfortunately, rules vs macros can't be determined until after // macro tokenizing. so that's suboptimal. // TODO errors let line_tokens: TokenString = line.parse().unwrap(); enum LineType { Rule, Macro, Unknown, } fn get_line_type(line_tokens: &TokenString) -> LineType { for token in line_tokens.tokens() { if let Token::Text(text) = token { let colon_idx = text.find(':'); let equals_idx = text.find('='); match (colon_idx, equals_idx) { (Some(_), None) => { return LineType::Rule; } (Some(c), Some(e)) if c < e => { return LineType::Rule; } (None, Some(_)) => { return LineType::Macro; } (Some(c), Some(e)) if e < c => { return LineType::Macro; } _ => {} } } } LineType::Unknown } let line_type = get_line_type(&line_tokens); match line_type { LineType::Rule => { let (targets, not_targets) = line_tokens.split_once(':').unwrap(); let targets = self.expand_macros(&targets, None); let targets = targets .split_whitespace() .map(Into::into) .collect::>(); let (prerequisites, mut commands) = match not_targets.split_once(';') { Some((prerequisites, mut command)) => { while command.ends_with("\\") && lines_iter.peek().is_some() { command.strip_suffix("\\"); command .extend(tokenize(&lines_iter.next().unwrap().1.unwrap())); } (prerequisites, vec![command]) } None => (not_targets, vec![]), }; let prerequisites = self.expand_macros(&prerequisites, None); let prerequisites = prerequisites .split_whitespace() .map(|x| x.into()) .collect::>(); while let Some((_, x)) = lines_iter.next_if(|(_, x)| { x.as_ref() .ok() .map_or(false, |line| line.starts_with('\t') || line.is_empty()) }) { let mut line = x.unwrap(); if !line.is_empty() { line.remove(0); } if line.is_empty() { continue; } while line.ends_with('\\') { match lines_iter.next() { Some((_, Ok(next_line))) => { let next_line = next_line.strip_prefix("\t").unwrap_or(&next_line); line.push('\n'); line.push_str(next_line); } _ => break, } } commands.push(line.parse().unwrap()); } let commands = commands .into_iter() .map(CommandLine::from) .collect::>(); if targets.is_empty() { continue; } // we don't know yet if it's a target rule or an inference rule lazy_static! { static ref INFERENCE_RULE: Regex = Regex::new(r"^(?P(\.[^/.]+)?)(?P\.[^/.]+)$").unwrap(); static ref SPECIAL_TARGET: Regex = Regex::new(r"^\.[A-Z]+$").unwrap(); } let inference_match = INFERENCE_RULE.captures(&targets[0]); let special_target_match = SPECIAL_TARGET.captures(&targets[0]); let inference_rule = targets.len() == 1 && prerequisites.is_empty() && inference_match.is_some() && special_target_match.is_none(); if inference_rule { let inference_match = inference_match.unwrap(); let new_rule = InferenceRule { product: inference_match.name("s1").unwrap().as_str().to_string(), prereq: inference_match.name("s2").unwrap().as_str().to_string(), commands, }; self.inference_rules.retain(|existing_rule| { (&existing_rule.prereq, &existing_rule.product) != (&new_rule.prereq, &new_rule.product) }); self.inference_rules.push(new_rule); } else { for target in targets { if self.first_non_special_target.is_none() && !target.starts_with('.') { self.first_non_special_target = Some(target.clone()); } let mut targets = self.targets.borrow_mut(); match targets.get_mut(&target) { Some(old_target) if commands.is_empty() && !(target == ".SUFIXES" && prerequisites.is_empty()) => { let mut old_target = old_target.borrow_mut(); let new_prerequisites = prerequisites .iter() .filter(|x| !old_target.prerequisites.contains(x)) .cloned() .collect::>(); old_target.prerequisites.extend(new_prerequisites); } _ => { let new_target = Target { name: target.clone(), prerequisites: prerequisites.clone(), commands: commands.clone(), already_updated: Cell::new(false), }; targets.insert( target.clone(), Rc::new(RefCell::new(new_target)), ); } } } } } LineType::Macro => { let (name, mut value) = line_tokens.split_once('=').unwrap(); let name = self.expand_macros(&name, None); // GNUisms are annoying, but popular let mut expand_value = false; let mut skip_if_defined = false; let mut append = false; let name = if let Some(real_name) = name.strip_suffix("::") { expand_value = true; real_name } else if let Some(real_name) = name.strip_suffix(":") { expand_value = true; real_name } else if let Some(real_name) = name.strip_suffix("?") { skip_if_defined = true; real_name } else if let Some(real_name) = name.strip_suffix("+") { append = true; real_name } else { &name }; let name = name.trim_end(); value.trim_start(); let value = if expand_value { TokenString::text(self.expand_macros(&value, None)) } else { value }; match self.macros.get(name) { // We always let command line or MAKEFLAGS macros override macros from the file. Some((MacroSource::CommandLineOrMAKEFLAGS, _)) => continue, // We let environment variables override macros from the file only if the command-line argument to do that was given Some((MacroSource::Environment, _)) if self.args.environment_overrides => { continue } _ if skip_if_defined => continue, _ => {} } let value = match self.macros.remove(name) { Some((_, mut old_value)) if append => { // TODO eagerly expand if appending to eagerly-expanded macro old_value.extend(TokenString::text(" ")); old_value.extend(value); old_value } _ => value, }; self.macros.insert(name.into(), (MacroSource::File, value)); } LineType::Unknown => { panic!( "error: line {}: unknown line {:?}", line_number, line_tokens ); } } } } self } fn special_target_has_prereq(&self, target: &str, name: &str) -> bool { let targets = self.targets.borrow(); match targets.get(target) { Some(target) => { let target = target.borrow(); target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name) } None => false, } } pub fn get_target(&self, name: &str) -> Rc> { // TODO implement .POSIX let follow_gnu = true; let vpath_options = match self.macros.get("VPATH") { Some((_, vpath)) if follow_gnu => { let vpath = self.expand_macros(vpath, None); env::split_paths(&vpath).collect() } _ => vec![], }; let targets = self.targets.borrow(); let mut new_target = None; let exists_but_infer_anyway = if follow_gnu { match targets.get(name) { Some(target) => { let target = target.borrow(); target.commands.is_empty() } None => false, } } else { false }; if !targets.contains_key(name) || exists_but_infer_anyway { // When no target rule is found to update a target, the inference rules shall // be checked. The suffix of the target to be built... let suffix = Path::new(name) .extension() .map_or_else(String::new, |ext| format!(".{}", ext.to_string_lossy())); // is compared to the list of suffixes specified by the .SUFFIXES special // targets. If the .s1 suffix is found in .SUFFIXES... if self.special_target_has_prereq(".SUFFIXES", &suffix) || suffix.is_empty() { // the inference rules shall be searched in the order defined... 'rules: for rule in &self.inference_rules { // for the first .s2.s1 rule... if rule.product == suffix { // whose prerequisite file ($*.s2) exists. let prereq_path = Path::new(name).with_extension(rule.prereq.trim_start_matches('.')); let prereq_path_options = if prereq_path.is_absolute() { vec![prereq_path] } else { let mut options = vec![prereq_path.clone()]; options .extend(vpath_options.iter().map(|vpath| vpath.join(&prereq_path))); options }; for prereq in prereq_path_options { if prereq.exists() { new_target = Some(Target { name: name.into(), prerequisites: vec![prereq.to_string_lossy().into()], commands: rule.commands.clone(), already_updated: Cell::new(false), }); break 'rules; } } } } } } if !targets.contains_key(name) && new_target.is_none() { // well, inference didn't work. is there a default? if let Some(default) = targets.get(".DEFAULT") { let default = default.borrow(); let commands = default.commands.clone(); drop(default); new_target = Some(Target { name: name.into(), prerequisites: vec![], commands, 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![], already_updated: Cell::new(true), }); } } } drop(targets); if let Some(new_target) = new_target { let mut targets = self.targets.borrow_mut(); targets.insert(new_target.name.clone(), Rc::new(RefCell::new(new_target))); } let targets = self.targets.borrow(); targets.get(name).expect("Target not found!").clone() } pub fn update_target(&self, name: &str) { let target = self.get_target(name); let target = target.borrow(); target.update(self); } fn expand_macros(&self, text: &TokenString, target: Option<&Target>) -> String { let mut result = String::new(); for token in text.tokens() { match token { Token::Text(t) => result.push_str(t), Token::MacroExpansion { name, replacement } => { let internal_macro_names = &['@', '?', '<', '*'][..]; let internal_macro_suffices = &['D', 'F'][..]; let just_internal = name.len() == 1 && name.starts_with(internal_macro_names); let suffixed_internal = name.len() == 2 && name.starts_with(internal_macro_names) && name.ends_with(internal_macro_suffices); let macro_value = if just_internal || suffixed_internal { let target = target.expect("internal macro but no current target!"); let macro_pieces = if name.starts_with('@') { // The $@ shall evaluate to the full target name of the // current target. vec![target.name.clone()] } else if 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) .borrow() .newer_than(target) .unwrap_or(false) }) .cloned() .collect() } else if 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. target.prerequisites.clone() } else if name.starts_with('*') { // The $* macro shall evaluate to the current target name with // its suffix deleted. vec![Path::new(name).with_extension("").to_string_lossy().into()] } else { unreachable!() }; let macro_pieces = if name.ends_with('D') { macro_pieces .into_iter() .map(|x| { Path::new(&x) .parent() .expect("no parent") .to_string_lossy() .into() }) .collect() } else if name.ends_with('F') { macro_pieces .into_iter() .map(|x| { Path::new(&x) .file_name() .expect("no filename") .to_string_lossy() .into() }) .collect() } else { macro_pieces }; macro_pieces.join(" ") } else { match self.macros.get(name) { Some((_, macro_value)) => self.expand_macros(¯o_value, target), None => String::new(), } }; let macro_value = match replacement { Some((subst1, subst2)) => { let subst1 = self.expand_macros(subst1, target); let subst1_suffix = regex::escape(&subst1); let subst1_suffix = Regex::new(&format!(r"{}\b", subst1_suffix)).unwrap(); let subst2 = self.expand_macros(subst2, target); subst1_suffix.replace_all(¯o_value, subst2).to_string() } None => macro_value, }; result.push_str(¯o_value); } } } result } } 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 { writeln!(f, "{}", rule)?; } writeln!(f)?; header(f, "Macros")?; for (k, (_, v)) in &self.macros { writeln!(f, "{}={}", k, v)?; } writeln!(f)?; header(f, "Targets")?; for target in self.targets.borrow().values() { writeln!(f, "{}", target.borrow())?; } 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 { product: prepend_dot!($($second)?).into(), prereq: concat!(".", stringify!($first)).into(), commands: 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_macros() -> Vec<(&'static str, TokenString)> { // Fuck it, might as well. macro_rules! handle { ($value:ident) => { stringify!($value) }; ($value:literal) => { $value }; } macro_rules! make { ($($name:ident=$value:tt)+) => {vec![$( (stringify!($name), handle!($value).parse().unwrap()) ),+]}; } make![ MAKE=makers AR=ar YACC=yacc YFLAGS="" LEX=lex LFLAGS="" LDFLAGS="" AS=as CC=cc CXX="g++" CPP="$(CC) -E" FC=f77 PC=pc CO=co GET=get LINT=lint MAKEINFO=makeinfo TEX=tex TEXI2DVI=texi2dvi WEAVE=weave CWEAVE=cweave TANGLE=tangle CTANGLE=ctangle RM="rm -f" ARFLAGS="rv" CFLAGS="" FFLAGS="" ] } 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![], already_updated: Cell::new(false), }] }