From 40920ea9d255f704116064d0b831666c7416caf2 Mon Sep 17 00:00:00 2001 From: Melody Horn Date: Sun, 28 Mar 2021 00:36:23 -0600 Subject: add (& somewhat test!) GNUful conditionals --- src/args.rs | 7 ++ src/makefile/conditional.rs | 164 ++++++++++++++++++++++++++++++++++++++++++++ src/makefile/mod.rs | 54 +++++++++++++++ src/makefile/token.rs | 21 ++++++ 4 files changed, 246 insertions(+) create mode 100644 src/makefile/conditional.rs (limited to 'src') diff --git a/src/args.rs b/src/args.rs index f96cba4..134f785 100644 --- a/src/args.rs +++ b/src/args.rs @@ -165,6 +165,13 @@ impl Args { Self::from_given_args_and_given_env(args, env_makeflags) } + #[cfg(test)] + pub(crate) fn empty() -> Self { + let env_makeflags = String::new(); + let args = vec![OsString::from("makers")]; + Self::from_given_args_and_given_env(args.into_iter(), env_makeflags) + } + pub(crate) fn targets(&self) -> impl Iterator { self.targets_or_macros.iter().filter(|x| !x.contains('=')) } diff --git a/src/makefile/conditional.rs b/src/makefile/conditional.rs new file mode 100644 index 0000000..f9d2d60 --- /dev/null +++ b/src/makefile/conditional.rs @@ -0,0 +1,164 @@ +use super::token::TokenString; + +pub(crate) enum ConditionalLine { + /// spelled "ifeq" + IfEqual(TokenString, TokenString), + /// spelled "ifneq" + IfNotEqual(TokenString, TokenString), + /// spelled "ifdef" + IfDefined(String), + /// spelled "ifndef" + IfNotDefined(String), + /// spelled "else" + Else, + /// spelled "else condition" + ElseIf(Box), + /// spelled "endif" + EndIf, +} + +pub(crate) enum ConditionalState { + /// we saw a conditional, the condition was true, we're executing now + /// and if we hit an else we will start SkippingUntilEndIf + Executing, + /// we saw a conditional, the condition was false, we're ignoring now + /// and if we hit an else we'll start executing + /// (or if it's an else if we'll check the condition) + SkippingUntilElseOrEndIf, + /// we saw a conditional, the condition was true, we executed, and now we hit an else + /// so we don't need to stop and evaluate new conditions, because we straight up do + /// not care + SkippingUntilEndIf, +} + +impl ConditionalState { + pub(crate) const fn skipping(&self) -> bool { + match self { + Self::Executing => false, + Self::SkippingUntilElseOrEndIf | Self::SkippingUntilEndIf => true, + } + } +} + +pub(crate) enum ConditionalStateAction { + Push(ConditionalState), + Replace(ConditionalState), + Pop, +} + +impl ConditionalStateAction { + pub(crate) fn apply_to(self, stack: &mut Vec) { + match self { + Self::Push(state) => stack.push(state), + Self::Replace(state) => match stack.last_mut() { + Some(x) => *x = state, + None => panic!("applying Replace on an empty condition stack"), + }, + Self::Pop => { + stack.pop(); + } + } + } +} + +fn decode_condition_args(line_body: &str) -> Option<(TokenString, TokenString)> { + let tokens: TokenString = line_body.parse().ok()?; + let (mut arg1, mut arg2) = if tokens.starts_with("(") && tokens.ends_with(")") { + let mut tokens = tokens; + tokens.strip_prefix("("); + tokens.strip_suffix(")"); + tokens.split_once(',')? + } else { + // TODO see if i really need to implement potentially-mixed-quoted args + return None; + }; + arg1.trim_end(); + arg2.trim_start(); + Some((arg1, arg2)) +} + +impl ConditionalLine { + pub(crate) fn from(line: &str, expand_macro: impl Fn(&TokenString) -> String) -> Option { + if let Some(line) = line.strip_prefix("ifeq ") { + let (arg1, arg2) = decode_condition_args(line)?; + Some(Self::IfEqual(arg1, arg2)) + } else if let Some(line) = line.strip_prefix("ifneq ") { + let (arg1, arg2) = decode_condition_args(line)?; + Some(Self::IfNotEqual(arg1, arg2)) + } else if let Some(line) = line.strip_prefix("ifdef ") { + Some(Self::IfDefined(expand_macro(&line.parse().ok()?))) + } else if let Some(line) = line.strip_prefix("ifndef ") { + Some(Self::IfNotDefined(expand_macro(&line.parse().ok()?))) + } else if line == "else" { + Some(Self::Else) + } else if let Some(line) = line.strip_prefix("else ") { + let sub_condition = Self::from(line, expand_macro)?; + Some(Self::ElseIf(Box::new(sub_condition))) + } else if line == "endif" { + Some(Self::EndIf) + } else { + None + } + } + + pub(crate) fn action( + &self, + current_state: Option<&ConditionalState>, + is_macro_defined: impl Fn(&str) -> bool, + expand_macro: impl Fn(&TokenString) -> String, + ) -> ConditionalStateAction { + use ConditionalState as State; + use ConditionalStateAction as Action; + match self { + Self::IfEqual(arg1, arg2) => { + let arg1 = expand_macro(arg1); + let arg2 = expand_macro(arg2); + if arg1 == arg2 { + Action::Push(State::Executing) + } else { + Action::Push(State::SkippingUntilElseOrEndIf) + } + } + Self::IfNotEqual(arg1, arg2) => { + let arg1 = expand_macro(arg1); + let arg2 = expand_macro(arg2); + if arg1 == arg2 { + Action::Push(State::SkippingUntilElseOrEndIf) + } else { + Action::Push(State::Executing) + } + } + Self::IfDefined(name) => { + if is_macro_defined(name) { + Action::Push(State::Executing) + } else { + Action::Push(State::SkippingUntilElseOrEndIf) + } + } + Self::IfNotDefined(name) => { + if is_macro_defined(name) { + Action::Push(State::SkippingUntilElseOrEndIf) + } else { + Action::Push(State::Executing) + } + } + Self::Else => Action::Replace(match current_state { + Some(State::Executing) | Some(State::SkippingUntilEndIf) => { + State::SkippingUntilEndIf + } + Some(State::SkippingUntilElseOrEndIf) => State::Executing, + None => panic!("got an Else but not in a conditional"), + }), + Self::ElseIf(inner_condition) => match current_state { + Some(State::Executing) | Some(State::SkippingUntilEndIf) => { + Action::Replace(State::SkippingUntilEndIf) + } + Some(State::SkippingUntilElseOrEndIf) => { + inner_condition.action(current_state, is_macro_defined, expand_macro) + } + None => panic!("got an ElseIf but not in a conditional"), + }, + Self::EndIf => Action::Pop, + } + } +} diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs index 2f73c70..0d4cded 100644 --- a/src/makefile/mod.rs +++ b/src/makefile/mod.rs @@ -13,11 +13,13 @@ use regex::Regex; use crate::args::Args; mod command_line; +mod conditional; mod inference_rules; mod target; mod token; use command_line::CommandLine; +use conditional::{ConditionalLine, ConditionalState}; use inference_rules::InferenceRule; use target::Target; use token::{tokenize, Token, TokenString}; @@ -151,6 +153,7 @@ impl<'a> Makefile<'a> { pub(crate) fn and_read(&mut self, source: impl BufRead) { let mut lines_iter = source.lines().enumerate().peekable(); + let mut conditional_stack: Vec = vec![]; 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!"); @@ -170,6 +173,14 @@ impl<'a> Makefile<'a> { } let line = COMMENT.replace(&line, "").into_owned(); + // skip lines if we need to + if conditional_stack + .last() + .map_or(false, ConditionalState::skipping) + { + continue; + } + // handle include lines if let Some(line) = line.strip_prefix("include ") { // remove extra leading space @@ -181,6 +192,14 @@ impl<'a> Makefile<'a> { for field in fields { self.and_read_file(field); } + } else if let Some(line) = ConditionalLine::from(&line, |t| self.expand_macros(t, None)) + { + line.action( + conditional_stack.last(), + |name| self.macros.contains_key(name), + |t| self.expand_macros(t, None), + ) + .apply_to(&mut conditional_stack); } else if line.trim().is_empty() { // handle blank lines continue; @@ -728,3 +747,38 @@ fn builtin_targets() -> Vec { already_updated: Cell::new(false), }] } + +#[cfg(test)] +mod test { + use super::*; + + use std::io::Cursor; + + fn empty_makefile(args: &Args) -> Makefile { + Makefile { + inference_rules: vec![], + macros: HashMap::new(), + targets: RefCell::new(HashMap::new()), + first_non_special_target: None, + args, + } + } + + #[test] + fn basic_conditionals() { + let file = " +ifeq (1,1) +worked = yes +else ifeq (2,2) +worked = no +endif + "; + let args = Args::empty(); + let mut makefile = empty_makefile(&args); + makefile.and_read(Cursor::new(file)); + assert_eq!( + makefile.expand_macros(&"$(worked)".parse().unwrap(), None), + "yes" + ); + } +} diff --git a/src/makefile/token.rs b/src/makefile/token.rs index 811f30f..86c442b 100644 --- a/src/makefile/token.rs +++ b/src/makefile/token.rs @@ -47,6 +47,13 @@ impl TokenString { None } + pub(crate) fn starts_with(&self, pattern: &str) -> bool { + match self.0.first() { + Some(Token::Text(t)) => t.starts_with(pattern), + _ => false, + } + } + pub(crate) fn ends_with(&self, pattern: &str) -> bool { match self.0.last() { Some(Token::Text(t)) => t.ends_with(pattern), @@ -54,6 +61,14 @@ impl TokenString { } } + pub(crate) fn strip_prefix(&mut self, suffix: &str) { + if let Some(Token::Text(t)) = self.0.first_mut() { + if let Some(x) = t.strip_prefix(suffix) { + *t = x.into() + } + } + } + pub(crate) fn strip_suffix(&mut self, suffix: &str) { if let Some(Token::Text(t)) = self.0.last_mut() { if let Some(x) = t.strip_suffix(suffix) { @@ -71,6 +86,12 @@ impl TokenString { *t = t.trim_start().into(); } } + + pub(crate) fn trim_end(&mut self) { + if let Some(Token::Text(t)) = self.0.last_mut() { + *t = t.trim_end().into(); + } + } } impl fmt::Display for TokenString { -- cgit v1.2.3