aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelody Horn <melody@boringcactus.com>2021-03-28 00:36:23 -0600
committerMelody Horn <melody@boringcactus.com>2021-03-28 00:36:23 -0600
commit40920ea9d255f704116064d0b831666c7416caf2 (patch)
treebd0021cec78eb94cb2cb998c638d6b4cb52e3484
parent2cd4d0f968c8097566edc0c6fbee10baf4b2647a (diff)
downloadmakers-40920ea9d255f704116064d0b831666c7416caf2.tar.gz
makers-40920ea9d255f704116064d0b831666c7416caf2.zip
add (& somewhat test!) GNUful conditionals
-rw-r--r--src/args.rs7
-rw-r--r--src/makefile/conditional.rs164
-rw-r--r--src/makefile/mod.rs54
-rw-r--r--src/makefile/token.rs21
4 files changed, 246 insertions, 0 deletions
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<Item = &String> {
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<ConditionalLine>),
+ /// 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<ConditionalState>) {
+ 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<Self> {
+ 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<ConditionalState> = 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<Target> {
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 {