aboutsummaryrefslogtreecommitdiff
path: root/src/makefile/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/makefile/mod.rs')
-rw-r--r--src/makefile/mod.rs444
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(&macro_value, subst2).to_string()
+ },
+ None => macro_value,
+ };
+ result.push_str(&macro_value);
+ }
+ }
+ }
+ return result;
+ }
+}
+
+const BUILTIN_RULES: &'static [(&'static str, Rule)] = &[];
+const BUILTIN_SUFFIX_LIST: &'static [&'static str] = &[];