use std::collections::HashMap; use std::env; use std::fs::{File, metadata}; use std::io::{BufRead, BufReader}; use std::path::Path; use std::time::SystemTime; use lazy_static::lazy_static; use regex::Regex; use crate::args::Args; mod token; use token::{tokenize, Token, TokenString}; pub enum RuleType { Inference, Target, } #[derive(PartialEq, Eq, Clone)] pub struct Rule { name: String, prerequisites: Vec, commands: Vec, } impl Rule { pub fn r#type(&self) -> RuleType { if self.name.contains(".") && !self.name.contains("/") { RuleType::Inference } else { RuleType::Target } } fn execute_commands(&self, file: &Makefile, target: &Target) { for command in &self.commands { command.execute(file, target); } } } #[derive(PartialEq, Eq, Clone)] pub struct Target { name: String, prerequisites: Vec, rule: Option, already_updated: bool, } impl Target { fn modified_time(&self) -> Option { metadata(&self.name) .and_then(|metadata| metadata.modified()) .ok() } fn newer_than(&self, other: &Target) -> Option { 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.already_updated && other.prerequisites.contains(&self.name) => true, (_, None) if other.already_updated && self.prerequisites.contains(&other.name) => false, _ => return None, }) } fn is_up_to_date(&self, file: &mut Makefile) -> bool { if self.already_updated { return true; } let exists = metadata(&self.name).is_ok(); if exists && self.rule.is_none() { return true; } let newer_than_all_dependencies = self.prerequisites .iter() .all(|t| self.newer_than(&file.get_target(t)).unwrap_or(false)); if exists && newer_than_all_dependencies { return true; } false } fn update(&mut self, file: &mut Makefile) { for prereq in &self.prerequisites { file.update_target(prereq); } if !self.is_up_to_date(file) { match &self.rule { Some(rule) => rule.execute_commands(file, self), None => panic!("target doesn't exist & no rule to make it"), // TODO handle this error well } } self.already_updated = true; } } #[derive(PartialEq, Eq, Clone)] 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(); loop { match text_chars.peek() { Some('-') | Some('@') | Some('+') => match text_chars.next() { Some('-') => ignore_errors = true, Some('@') => silent = true, Some('+') => always_execute = true, _ => unreachable!() }, _ => break, } } *text = text_chars.collect(); } CommandLine { ignore_errors, silent, always_execute, execution_line: line, } } fn execute(&self, file: &Makefile, target: &Target) { let avoid_execution = file.args.dry_run || file.args.question || file.args.touch; if avoid_execution && !self.always_execute { return; } let execution_line = file.expand_macros(&self.execution_line); let self_silent = self.silent && !file.args.dry_run; let special_target_silent = file.rules.get(".SILENT") .map_or(false, |silent_target| { silent_target.prerequisites.is_empty() || silent_target.prerequisites.contains(&target.name) }); let silent = self_silent || file.args.silent || special_target_silent; if !silent { println!("{}", execution_line); } let special_target_ignore = file.rules.get(".IGNORE") .map_or(false, |ignore_target| { ignore_target.prerequisites.is_empty() || ignore_target.prerequisites.contains(&target.name) }); let ignore_error = self.ignore_errors || file.args.ignore_errors || special_target_ignore; // 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!"); } } } } enum MacroSource { File, CommandLineOrMAKEFLAGS, Environment, Builtin, } pub struct Makefile { rules: HashMap, macros: HashMap, targets: HashMap, args: Args, } impl Makefile { pub fn new(args: Args) -> Makefile { Makefile { rules: HashMap::new(), macros: HashMap::new(), targets: HashMap::new(), args, } } pub fn add_builtins(&mut self) -> &mut Makefile { self.rules.extend(BUILTIN_RULES.iter().map(|(name, rule)| (name.to_string(), rule.clone()))); self } pub fn add_env(&mut self) -> &mut Makefile { self.macros.extend(env::vars() .filter_map(|(name, value)| { if name == "MAKEFLAGS" || name == "SHELL" { None } else { Some((name, (MacroSource::Environment, TokenString::from(vec![Token::Text(value)])))) } }) ); self } 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().peekable(); while lines_iter.peek().is_some() { let line = match lines_iter.next() { Some(x) => x, // fancy Rust trick: break-with-an-argument to return a value from a // `loop` expression None => break, }; // TODO handle I/O errors at all let mut line = line.expect("failed to read line of makefile!"); // handle escaped newlines (TODO exception for command lines) while line.ends_with(r"\") { let next_line = match lines_iter.next() { Some(x) => x, None => Ok("".into()), }; let next_line = next_line.expect("failed to read line of makefile!"); let next_line = next_line.trim_start(); line.push(' '); line.push_str(next_line); } // 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)); 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 // TODO have some way of linting for non-portable constructs 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); let targets = targets.split_whitespace().map(|x| x.into()).collect::>(); let (prerequisites, mut commands) = match not_targets.split_once(';') { Some((prerequisites, commands)) => (prerequisites, vec![commands]), None => (not_targets, vec![]), }; let prerequisites = self.expand_macros(&prerequisites); let prerequisites = prerequisites.split_whitespace().map(|x| x.into()).collect::>(); while lines_iter.peek().and_then(|x| x.as_ref().ok()).map_or(false, |line| line.starts_with('\t')) { let line = lines_iter.next().unwrap().unwrap(); let line = line.strip_prefix("\t").unwrap(); commands.push(line.parse().unwrap()); } let commands = commands.into_iter() .map(CommandLine::from) .collect::>(); for target in targets { match self.rules.get_mut(&target) { Some(old_rule) if commands.is_empty() => { old_rule.prerequisites.extend(prerequisites.clone()); } _ => { self.rules.insert(target.clone(), Rule { name: target, prerequisites: prerequisites.clone(), commands: commands.clone(), }); } } } }, LineType::Macro => { let (name, value) = line_tokens.split_once('=').unwrap(); let name = self.expand_macros(&name); 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, _ => {} } self.macros.insert(name, (MacroSource::File, value)); } LineType::Unknown => { panic!("Unknown line {:?}", line_tokens); } } } } self } fn get_target(&mut self, name: impl Into) -> &mut Target { let name = name.into(); { let rules_get_name = self.rules.get(&name); let make_target = || { if let Some(target_rule) = rules_get_name { return Target { name: name.clone(), prerequisites: target_rule.prerequisites.clone(), already_updated: false, rule: Some(target_rule.clone()) }; } panic!("uhhhhh i don't even know anymore bro"); }; self.targets .entry(name.clone()) .or_insert_with(make_target); } self.targets.get_mut(&name).unwrap() } fn update_target(&mut self, name: impl Into) { // This is the dumbest fucking thing I've ever had to do. // We can't leave it in the map, because then we have overlapping mutable borrows of self, // so we have to remove it from the map, do the work, and then re-insert it into the map. // Fuck this so much. // Why the goddamn hell do I even write Rust. let name = name.into(); { let _ = self.get_target(name.clone()); } let mut target = self.targets.remove(&name).unwrap(); target.update(self); self.targets.insert(name.clone(), target); } fn expand_macros(&self, text: &TokenString) -> 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 (_, macro_value) = &self.macros[name]; let macro_value = self.expand_macros(macro_value); let macro_value = match replacement { Some((subst1, subst2)) => { let subst1 = self.expand_macros(subst1); let subst1_suffix = regex::escape(&subst1); let subst1_suffix = Regex::new(&format!(r"{}\b", subst1_suffix)).unwrap(); let subst2 = self.expand_macros(subst2); subst1_suffix.replace_all(¯o_value, subst2).to_string() }, None => macro_value, }; result.push_str(¯o_value); } } } return result; } } const BUILTIN_RULES: &'static [(&'static str, Rule)] = &[]; const BUILTIN_SUFFIX_LIST: &'static [&'static str] = &[];