diff options
author | Melody Horn <melody@boringcactus.com> | 2021-03-23 23:27:45 -0600 |
---|---|---|
committer | Melody Horn <melody@boringcactus.com> | 2021-03-23 23:27:45 -0600 |
commit | 1844cb79ae82e71610573f133c5ed7aeeb0c50b6 (patch) | |
tree | 5f304a13473e67440d411ab0fee15d116a57d126 /src/makefile/mod.rs | |
parent | 05b8b6339c4b00b0e898c8456677be2883c8a072 (diff) | |
download | makers-1844cb79ae82e71610573f133c5ed7aeeb0c50b6.tar.gz makers-1844cb79ae82e71610573f133c5ed7aeeb0c50b6.zip |
man i don't even fuckin know anymore
Diffstat (limited to 'src/makefile/mod.rs')
-rw-r--r-- | src/makefile/mod.rs | 444 |
1 files changed, 444 insertions, 0 deletions
diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs new file mode 100644 index 0000000..75873d2 --- /dev/null +++ b/src/makefile/mod.rs @@ -0,0 +1,444 @@ +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<String>, + commands: Vec<CommandLine>, +} + +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<String>, + rule: Option<Rule>, + already_updated: bool, +} + +impl Target { + fn modified_time(&self) -> Option<SystemTime> { + metadata(&self.name) + .and_then(|metadata| metadata.modified()) + .ok() + } + + fn newer_than(&self, other: &Target) -> Option<bool> { + 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 <hyphen-minus>, 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 <plus-sign>, 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<String, Rule>, + macros: HashMap<String, (MacroSource, TokenString)>, + targets: HashMap<String, Target>, + 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<Path>) -> &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::<Vec<String>>(); + 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::<Vec<String>>(); + + 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::<Vec<_>>(); + + 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<String>) -> &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<String>) { + // 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] = &[]; |