diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/args.rs | 230 | ||||
-rw-r--r-- | src/main.rs | 22 | ||||
-rw-r--r-- | src/makefile/command_line.rs | 91 | ||||
-rw-r--r-- | src/makefile/conditional.rs | 4 | ||||
-rw-r--r-- | src/makefile/eval_context.rs | 53 | ||||
-rw-r--r-- | src/makefile/functions.rs | 549 | ||||
-rw-r--r-- | src/makefile/inference_rules.rs | 138 | ||||
-rw-r--r-- | src/makefile/input.rs | 635 | ||||
-rw-r--r-- | src/makefile/lookup_internal.rs | 164 | ||||
-rw-r--r-- | src/makefile/macro.rs | 323 | ||||
-rw-r--r-- | src/makefile/macro_scope.rs | 229 | ||||
-rw-r--r-- | src/makefile/mod.rs | 210 | ||||
-rw-r--r-- | src/makefile/parse.rs | 19 | ||||
-rw-r--r-- | src/makefile/target.rs | 20 | ||||
-rw-r--r-- | src/makefile/token.rs | 64 |
15 files changed, 1940 insertions, 811 deletions
diff --git a/src/args.rs b/src/args.rs index 9318202..8489c2d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,17 +1,18 @@ use std::env; use std::ffi::OsString; use std::iter; +use std::ops::AddAssign; use std::path::PathBuf; -use structopt::StructOpt; +use clap::Parser; -#[derive(StructOpt, Debug, PartialEq, Eq, Clone)] -#[structopt(author, about)] +#[derive(clap::Parser, Debug, PartialEq, Eq, Clone)] +#[clap(author, about)] #[allow(clippy::struct_excessive_bools)] pub struct Args { /// Cause environment variables, including those with null values, to override macro /// assignments within makefiles. - #[structopt(short, long)] + #[clap(short, long)] pub environment_overrides: bool, /// Specify a different makefile (or '-' for standard input). @@ -21,12 +22,12 @@ pub struct Args { /// be multiple instances of this option, and they shall be processed in the order /// specified. The effect of specifying the same option-argument more than once is /// unspecified. - #[structopt( - short = "f", + #[clap( + short = 'f', long = "file", visible_alias = "makefile", number_of_values = 1, - parse(from_os_str) + value_parser )] pub makefile: Vec<PathBuf>, @@ -34,17 +35,17 @@ pub struct Args { /// /// This mode is the same as if the special target .IGNORE were specified without /// prerequisites. - #[structopt(short, long)] + #[clap(short, long)] pub ignore_errors: bool, /// Continue to update other targets that do not depend on the current target if a /// non-ignored error occurs while executing the commands to bring a target /// up-to-date. - #[structopt( + #[clap( short, long, - overrides_with = "keep-going", - overrides_with = "no-keep-going" + overrides_with = "keep_going", + overrides_with = "no_keep_going" )] pub keep_going: bool, @@ -54,8 +55,8 @@ pub struct Args { /// However, lines with a <plus-sign> ( '+' ) prefix shall be executed. In this mode, /// lines with an at-sign ( '@' ) character prefix shall be written to standard /// output. - #[structopt( - short = "n", + #[clap( + short = 'n', long, visible_alias = "just-print", visible_alias = "recon" @@ -66,7 +67,7 @@ pub struct Args { /// descriptions. /// /// The output format is unspecified. - #[structopt(short, long, visible_alias = "print-data-base")] + #[clap(short, long, visible_alias = "print-data-base")] pub print_everything: bool, /// Return a zero exit value if the target file is up-to-date; otherwise, return an @@ -75,11 +76,11 @@ pub struct Args { /// Targets shall not be updated if this option is specified. However, a makefile /// command line (associated with the targets) with a <plus-sign> ( '+' ) prefix /// shall be executed. - #[structopt(short, long)] + #[clap(short, long)] pub question: bool, /// Clear the suffix list and do not use the built-in rules. - #[structopt(short = "r", long)] + #[clap(short = 'r', long)] pub no_builtin_rules: bool, /// Terminate make if an error occurs while executing the commands to bring a target @@ -87,13 +88,13 @@ pub struct Args { /// reason). /// /// This shall be the default and the opposite of -k. - #[structopt( - short = "S", + #[clap( + short = 'S', long, visible_alias = "stop", - hidden = true, - overrides_with = "keep-going", - overrides_with = "no-keep-going" + hide = true, + overrides_with = "keep_going", + overrides_with = "no_keep_going" )] pub no_keep_going: bool, @@ -102,7 +103,7 @@ pub struct Args { /// /// This mode shall be the same as if the special target .SILENT were specified /// without prerequisites. - #[structopt(short, long, visible_alias = "quiet")] + #[clap(short, long, visible_alias = "quiet")] pub silent: bool, /// Update the modification time of each target as though a touch target had been @@ -113,14 +114,35 @@ pub struct Args { /// target file indicating the name of the file and that it was touched. Normally, /// the makefile command lines associated with each target are not executed. However, /// a command line with a <plus-sign> ( '+' ) prefix shall be executed. - #[structopt(short, long)] + #[clap(short, long)] pub touch: bool, /// Change to the given directory before running. #[cfg(feature = "full")] - #[structopt(short = "C", long, parse(from_os_str))] + #[clap(short = 'C', long, value_parser)] pub directory: Option<PathBuf>, + /// Print the working directory when starting. + // TODO implement + // TODO automatically with -C or recursion or decide that this is a bad GNU feature + #[cfg(feature = "full")] + #[clap( + short = 'w', + long, + overrides_with = "print_directory", + overrides_with = "no_print_directory" + )] + pub print_directory: bool, + + /// Do not print the working directory when starting, even when running with -C or recursively. + #[cfg(feature = "full")] + #[clap( + long, + overrides_with = "print_directory", + overrides_with = "no_print_directory" + )] + pub no_print_directory: bool, + /// Target names or macro definitions. /// /// If no target is specified, while make is processing the makefiles, the first @@ -137,14 +159,13 @@ impl Args { // POSIX spec says "Any options specified in the MAKEFLAGS environment variable // shall be evaluated before any options specified on the make utility command // line." - // TODO allow macro definitions in MAKEFLAGS // POSIX says we have to accept // > The characters are option letters without the leading <hyphen-minus> // > characters or <blank> separation used on a make utility command line. let makeflags_given = !env_makeflags.is_empty(); let makeflags_spaces = env_makeflags.contains(' '); let makeflags_leading_dash = env_makeflags.starts_with('-'); - let makeflags_has_equals = env_makeflags.starts_with('='); + let makeflags_has_equals = env_makeflags.contains('='); let makeflags_obviously_full = makeflags_spaces || makeflags_leading_dash || makeflags_has_equals; let env_makeflags = if makeflags_given && !makeflags_obviously_full { @@ -152,16 +173,17 @@ impl Args { } else { env_makeflags }; - let env_makeflags = env_makeflags.split_whitespace().map(OsString::from); + let env_makeflags = shlex::split(&env_makeflags) + .expect("Bad args?") + .into_iter() + .map(OsString::from); // per the structopt docs, the first argument will be used as the binary name, // so we need to make sure it goes in before MAKEFLAGS let arg_0 = args.next().unwrap_or_else(|| env!("CARGO_PKG_NAME").into()); - let args = iter::once(arg_0) - .chain(env_makeflags.into_iter()) - .chain(args); + let args = iter::once(arg_0).chain(env_makeflags).chain(args); - Self::from_iter(args) + Self::parse_from(args) } pub fn from_env_and_args() -> Self { @@ -192,37 +214,66 @@ impl Args { } pub fn makeflags(&self) -> String { - let mut result = String::new(); + let mut flags = String::new(); if self.environment_overrides { - result.push('e'); + flags.push('e'); } if self.ignore_errors { - result.push('i'); + flags.push('i'); } if self.keep_going { - result.push('k'); + flags.push('k'); } if self.dry_run { - result.push('n'); + flags.push('n'); } if self.print_everything { - result.push('p'); + flags.push('p'); } if self.question { - result.push('q'); + flags.push('q'); } if self.no_builtin_rules { - result.push('r'); + flags.push('r'); } if self.no_keep_going { - result.push('S'); + flags.push('S'); } if self.silent { - result.push('s'); + flags.push('s'); } if self.touch { - result.push('t'); + flags.push('t'); } + + let macros = self + .targets_or_macros + .iter() + .map(|x| shlex::try_quote(x).expect("Bad quoting?")) + .filter(|x| x.contains('=')) + .collect::<Vec<_>>() + .join(" "); + + let mut result = String::new(); + if !flags.is_empty() && !macros.is_empty() { + result.push('-'); + } + result.add_assign(&flags); + + #[cfg(feature = "full")] + if self.no_print_directory { + if !result.is_empty() { + result.push(' '); + } + result.push_str("--no-print-directory"); + } + + // TODO consider -- to separate flags from macros - GNU does it but it would require + // gnarly splicing to not override recursive macros + if !result.is_empty() && !macros.is_empty() { + result += " "; + } + result.add_assign(¯os); result } } @@ -232,6 +283,12 @@ mod test { use super::*; #[test] + fn clap_validate() { + use clap::CommandFactory; + Args::command().debug_assert(); + } + + #[test] fn no_args() { let args: Vec<OsString> = vec!["makers".into()]; let args = Args::from_given_args_and_given_env(args.into_iter(), String::new()); @@ -251,6 +308,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -279,6 +340,10 @@ mod test { touch: true, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec!["bar".into(), "baz=yeet".into()], } ); @@ -307,6 +372,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -335,6 +404,43 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, + targets_or_macros: vec![], + } + ); + } + + #[test] + #[cfg(feature = "full")] + fn print_directory_wrestling() { + let args = "makers -w --no-print-directory -w -w --no-print-directory --no-print-directory -w --no-print-directory"; + let args = Args::from_given_args_and_given_env( + args.split_whitespace().map(OsString::from), + String::new(), + ); + assert_eq!( + args, + Args { + environment_overrides: false, + makefile: vec![], + ignore_errors: false, + keep_going: false, + dry_run: false, + print_everything: false, + question: false, + no_builtin_rules: false, + no_keep_going: false, + silent: false, + touch: false, + #[cfg(feature = "full")] + directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: true, targets_or_macros: vec![], } ); @@ -361,6 +467,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -387,6 +497,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -394,7 +508,7 @@ mod test { #[test] fn nightmare() { - let makeflags = "-nrs -k foo=bar"; + let makeflags = "-nrs -k -- foo=bar"; let args = "makers -eipqtSf foo -f bruh bar baz=yeet"; let args = Args::from_given_args_and_given_env( args.split_whitespace().map(OsString::from), @@ -416,8 +530,36 @@ mod test { touch: true, #[cfg(feature = "full")] directory: None, - targets_or_macros: vec!["foo=bar".into(), "bar".into(), "baz=yeet".into()], + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, + targets_or_macros: vec!["bar".into(), "baz=yeet".into(), "foo=bar".into()], } ); + assert_eq!(args.makeflags(), "-einpqrSst -- baz=yeet foo=bar"); + } + + #[cfg(feature = "full")] + #[test] + fn makeflags_no_print_directory() { + let args = Args { + environment_overrides: false, + makefile: vec![], + ignore_errors: false, + keep_going: false, + dry_run: false, + print_everything: false, + question: false, + no_builtin_rules: false, + no_keep_going: false, + silent: false, + touch: false, + directory: None, + print_directory: false, + no_print_directory: true, + targets_or_macros: vec!["V=1".into()], + }; + assert_eq!(args.makeflags(), "--no-print-directory 'V=1'"); } } diff --git a/src/main.rs b/src/main.rs index 8fa4efd..0b3e9f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ clippy::nursery, clippy::str_to_string, clippy::unwrap_used, - clippy::integer_arithmetic, + clippy::arithmetic_side_effects, clippy::panic, clippy::unimplemented, clippy::todo, @@ -32,7 +32,7 @@ mod args; mod makefile; use args::Args; -use makefile::{Makefile, MakefileReader}; +use makefile::{MacroScopeStack, MacroSet, Makefile, MakefileReader}; const DEFAULT_PATHS: &[&str] = &[ #[cfg(feature = "full")] @@ -43,7 +43,7 @@ const DEFAULT_PATHS: &[&str] = &[ fn main() -> Result<()> { env_logger::init(); - jane_eyre::install()?; + color_eyre::install()?; let mut args = Args::from_env_and_args(); #[cfg(feature = "full")] @@ -66,15 +66,16 @@ fn main() -> Result<()> { let mut makefile = Makefile::new(&args); let paths = Default::default(); for filename in &args.makefile { + let stack = MacroScopeStack::default().with_scope(&makefile.macros); if filename == &PathBuf::from("-") { - let macros = makefile.macros.with_overlay(); - let file = MakefileReader::read(&args, macros, stdin().lock(), "-", Rc::clone(&paths))? - .finish(); + let macros = MacroSet::new(); + let file = + MakefileReader::read(&args, stack, macros, stdin().lock(), "-", Rc::clone(&paths))? + .finish(); makefile.extend(file)?; } else { - let macros = makefile.macros.with_overlay(); let file = - MakefileReader::read_file(&args, macros, filename, Rc::clone(&paths))?.finish(); + MakefileReader::read_file(&args, stack, filename, Rc::clone(&paths))?.finish(); makefile.extend(file)?; }; } @@ -92,9 +93,8 @@ fn main() -> Result<()> { for outdated in makefiles_outdated { eprintln!("makefile {} out of date, rebuilding", outdated); makefile.update_target(&outdated)?; - let macros = makefile.macros.with_overlay(); - let file = - MakefileReader::read_file(&args, macros, &outdated, Default::default())?.finish(); + let stack = MacroScopeStack::default().with_scope(&makefile.macros); + let file = MakefileReader::read_file(&args, stack, &outdated, Default::default())?.finish(); // TODO forget the stale data // TODO reread all the things, not just this one makefile.extend(file)?; diff --git a/src/makefile/command_line.rs b/src/makefile/command_line.rs index 7d4915d..9641941 100644 --- a/src/makefile/command_line.rs +++ b/src/makefile/command_line.rs @@ -3,9 +3,12 @@ use std::fmt; use std::process::{Command, ExitStatus}; use eyre::{bail, Error}; +#[cfg(feature = "full")] use lazy_static::lazy_static; +#[cfg(feature = "full")] use regex::Regex; +#[cfg(feature = "full")] use super::r#macro::Set as MacroSet; use super::target::Target; use super::token::{Token, TokenString}; @@ -15,7 +18,7 @@ use super::Makefile; fn execute_command_line( command_line: &str, ignore_errors: bool, - macros: &MacroSet, + #[cfg(feature = "full")] macros: &MacroSet, ) -> Result<ExitStatus, Error> { let (program, args) = if cfg!(windows) { let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()); @@ -33,50 +36,18 @@ fn execute_command_line( let mut command = Command::new(program); command.args(args); #[cfg(feature = "full")] - command.envs(macros.resolve_exports()?); + command.envs(macros.resolve_exports::<&[u8]>(None)?); Ok(command.status()?) } #[derive(PartialEq, Eq, Clone, Debug)] 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 { - pub 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(); - while let Some(x) = text_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { - match x { - '-' => ignore_errors = true, - '@' => silent = true, - '+' => always_execute = true, - _ => unreachable!(), - } - } - *text = text_chars.collect(); - } - + pub const fn from(line: TokenString) -> Self { Self { - ignore_errors, - silent, - always_execute, execution_line: line, } } @@ -91,14 +62,13 @@ impl CommandLine { #[cfg(feature = "full")] { let is_just_one_macro_expansion = self.execution_line.tokens().count() == 1 - && self.execution_line.tokens().all(|x| match x { - Token::MacroExpansion { .. } => true, - Token::FunctionCall { .. } => true, - _ => false, + && self.execution_line.tokens().all(|x| { + matches!(x, Token::MacroExpansion { .. } | Token::FunctionCall { .. }) }); // unfortunately, if we had a multiline macro somewhere with non-escaped newlines, now we have to run each of them as separate lines lazy_static! { - static ref UNESCAPED_NEWLINE: Regex = Regex::new(r"([^\\])\n").unwrap(); + static ref UNESCAPED_NEWLINE: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"([^\\])\n").unwrap(); } if is_just_one_macro_expansion && UNESCAPED_NEWLINE.is_match(&execution_line) { let lines = UNESCAPED_NEWLINE @@ -111,28 +81,31 @@ impl CommandLine { } } log::trace!("executing {}", &execution_line); - let mut self_ignore_errors = self.ignore_errors; - let mut self_silent = self.silent; - let mut self_always_execute = self.always_execute; + let mut ignore_errors = false; + let mut silent = false; + let mut always_execute = false; - // apparently some makefiles will just throw this shit in in macros? bruh moment tbh + // sometimes this is defined in macros rather than statically let execution_line: String = { - let mut line_chars = execution_line.chars().peekable(); + let mut line_chars = execution_line + .chars() + .skip_while(char::is_ascii_whitespace) + .peekable(); while let Some(x) = line_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { match x { - '-' => self_ignore_errors = true, - '@' => self_silent = true, - '+' => self_always_execute = true, + '-' => ignore_errors = true, + '@' => silent = true, + '+' => always_execute = true, _ => unreachable!(), } } line_chars.collect() }; - let ignore_error = self_ignore_errors + let ignore_error = ignore_errors || file.args.ignore_errors || file.special_target_has_prereq(".IGNORE", &target.name); - let silent = (self_silent && !file.args.dry_run) + let silent = (silent && !file.args.dry_run) || file.args.silent || file.special_target_has_prereq(".SILENT", &target.name); @@ -140,14 +113,19 @@ impl CommandLine { println!("{}", execution_line); } - let should_execute = self_always_execute + let should_execute = always_execute || is_recursive || !(file.args.dry_run || file.args.question || file.args.touch); if !should_execute { return Ok(()); } - let return_value = execute_command_line(&execution_line, ignore_error, &file.macros); + let return_value = execute_command_line( + &execution_line, + ignore_error, + #[cfg(feature = "full")] + &file.macros, + ); let errored = return_value.map_or(true, |status| !status.success()); if errored { // apparently there was an error. do we care? @@ -162,15 +140,6 @@ impl CommandLine { impl fmt::Display for CommandLine { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.ignore_errors { - write!(f, "-")?; - } - if self.silent { - write!(f, "@")?; - } - if self.always_execute { - write!(f, "+")?; - } let execution_line = format!("{}", &self.execution_line); let execution_line = execution_line.replace("\n", "↵\n"); write!(f, "{}", execution_line)?; diff --git a/src/makefile/conditional.rs b/src/makefile/conditional.rs index 98400e6..6eed14a 100644 --- a/src/makefile/conditional.rs +++ b/src/makefile/conditional.rs @@ -99,7 +99,7 @@ fn decode_condition_args(line_body: &str) -> Option<(TokenString, TokenString)> impl Line { pub fn from( line: &str, - expand_macro: impl Fn(&TokenString) -> Result<String>, + mut expand_macro: impl FnMut(&TokenString) -> Result<String>, ) -> Result<Option<Self>> { let line = line.trim_start(); Ok(Some(if let Some(line) = line.strip_prefix("ifeq ") { @@ -134,7 +134,7 @@ impl Line { &self, current_state: Option<&State>, is_macro_defined: impl Fn(&str) -> bool, - expand_macro: impl Fn(&TokenString) -> Result<String>, + mut expand_macro: impl FnMut(&TokenString) -> Result<String>, ) -> Result<StateAction> { Ok(match self { Self::IfEqual(arg1, arg2) => { diff --git a/src/makefile/eval_context.rs b/src/makefile/eval_context.rs new file mode 100644 index 0000000..87edafd --- /dev/null +++ b/src/makefile/eval_context.rs @@ -0,0 +1,53 @@ +use eyre::{Result, WrapErr}; +use std::io::{BufRead, Cursor}; +use std::rc::Rc; + +use super::{FinishedMakefileReader, MacroSet, MakefileReader}; + +pub struct DeferredEvalContext<'parent, 'args, 'grandparent, R: BufRead> { + parent: &'parent MakefileReader<'args, 'grandparent, R>, + children: Vec<FinishedMakefileReader>, +} + +impl<'parent, 'args, 'grandparent, R: BufRead> + DeferredEvalContext<'parent, 'args, 'grandparent, R> +{ + pub const fn new(parent: &'parent MakefileReader<'args, 'grandparent, R>) -> Self { + Self { + parent, + children: Vec::new(), + } + } + + pub fn push(&mut self, child: FinishedMakefileReader) { + self.children.push(child); + } + + pub fn eval(&mut self, to_eval: String) -> Result<()> { + let child_stack = self.parent.stack.with_scope(&self.parent.macros); + let child_macros = MacroSet::new(); + let child = MakefileReader::read( + self.parent.args, + child_stack, + child_macros, + Cursor::new(to_eval), + "<eval>", + Rc::clone(&self.parent.file_names), + ) + .context("while evaling")? + .finish(); + self.push(child); + Ok(()) + } +} + +impl<'parent, 'args, 'grandparent, R: BufRead> IntoIterator + for DeferredEvalContext<'parent, 'args, 'grandparent, R> +{ + type Item = FinishedMakefileReader; + type IntoIter = std::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + self.children.into_iter() + } +} diff --git a/src/makefile/functions.rs b/src/makefile/functions.rs index a78d582..a6a2db1 100644 --- a/src/makefile/functions.rs +++ b/src/makefile/functions.rs @@ -1,128 +1,142 @@ -use std::cell::RefCell; use std::env; +use std::io::BufRead; use std::process::{Command, Stdio}; -use std::rc::Rc; use eyre::{bail, Result, WrapErr}; +use super::eval_context::DeferredEvalContext; use super::pattern::r#match; -use super::r#macro::{Macro, Set as MacroSet}; -use super::token::TokenString; -use super::ItemSource; +use super::r#macro::Macro; +use super::{ItemSource, MacroScopeStack, MacroSet, TokenString}; +pub const NO_EVAL: Option<&mut DeferredEvalContext<&[u8]>> = None; + +#[allow(clippy::cognitive_complexity)] pub fn expand_call( name: &str, args: &[TokenString], - macros: &MacroSet, - to_eval: Option<Rc<RefCell<Vec<String>>>>, + stack: &MacroScopeStack, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { match name { "subst" => { assert_eq!(args.len(), 3); - text::subst(macros, &args[0], &args[1], &args[2]) + text::subst(stack, &args[0], &args[1], &args[2], eval_context) } "patsubst" => { assert_eq!(args.len(), 3); - text::patsubst(macros, &args[0], &args[1], &args[2]) + text::patsubst(stack, &args[0], &args[1], &args[2], eval_context) } "strip" => { assert_eq!(args.len(), 1); - text::strip(macros, &args[0]) + text::strip(stack, &args[0], eval_context) } "findstring" => { assert_eq!(args.len(), 2); - text::findstring(macros, &args[0], &args[1]) + text::findstring(stack, &args[0], &args[1], eval_context) } "filter" => { assert_eq!(args.len(), 2); - text::filter(macros, &args[0], &args[1]) + text::filter(stack, &args[0], &args[1], eval_context) } "filter-out" => { assert_eq!(args.len(), 2); - text::filter_out(macros, &args[0], &args[1]) + text::filter_out(stack, &args[0], &args[1], eval_context) } "sort" => { assert_eq!(args.len(), 1); - text::sort(macros, &args[0]) + text::sort(stack, &args[0], eval_context) } "word" => { assert_eq!(args.len(), 2); - text::word(macros, &args[0], &args[1]) + text::word(stack, &args[0], &args[1], eval_context) } "words" => { assert_eq!(args.len(), 1); - text::words(macros, &args[0]) + text::words(stack, &args[0], eval_context) } "firstword" => { assert_eq!(args.len(), 1); - text::firstword(macros, &args[0]) + text::firstword(stack, &args[0], eval_context) } "lastword" => { assert_eq!(args.len(), 1); - text::lastword(macros, &args[0]) + text::lastword(stack, &args[0], eval_context) } "dir" => { assert_eq!(args.len(), 1); - file_name::dir(macros, &args[0]) + file_name::dir(stack, &args[0], eval_context) } "notdir" => { assert_eq!(args.len(), 1); - file_name::notdir(macros, &args[0]) + file_name::notdir(stack, &args[0], eval_context) } "basename" => { assert_eq!(args.len(), 1); - file_name::basename(macros, &args[0]) + file_name::basename(stack, &args[0], eval_context) } "addsuffix" => { assert_eq!(args.len(), 2); - file_name::addsuffix(macros, &args[0], &args[1]) + file_name::addsuffix(stack, &args[0], &args[1], eval_context) } "addprefix" => { assert_eq!(args.len(), 2); - file_name::addprefix(macros, &args[0], &args[1]) + file_name::addprefix(stack, &args[0], &args[1], eval_context) } "wildcard" => { assert_eq!(args.len(), 1); - file_name::wildcard(macros, &args[0]) + file_name::wildcard(stack, &args[0], eval_context) } "realpath" => { assert_eq!(args.len(), 1); - file_name::realpath(macros, &args[0]) + file_name::realpath(stack, &args[0], eval_context) } "abspath" => { assert_eq!(args.len(), 1); - file_name::abspath(macros, &args[0]) + file_name::abspath(stack, &args[0], eval_context) } "if" => { assert!(args.len() == 2 || args.len() == 3); - conditional::r#if(macros, &args[0], &args[1], args.get(2)) + conditional::r#if(stack, &args[0], &args[1], args.get(2), eval_context) } "or" => { assert!(!args.is_empty()); - conditional::or(macros, args.iter()) + conditional::or(stack, args.iter(), eval_context) } "and" => { assert!(!args.is_empty()); - conditional::and(macros, args.iter()) + conditional::and(stack, args.iter(), eval_context) + } + "intcmp" => { + assert!(2 <= args.len() && args.len() <= 5); + conditional::intcmp( + stack, + &args[0], + &args[1], + args.get(2), + args.get(3), + args.get(4), + eval_context, + ) } "foreach" => { assert_eq!(args.len(), 3); - foreach(macros, &args[0], &args[1], &args[2]) + foreach(stack, &args[0], &args[1], &args[2], eval_context) } "call" => { assert!(!args.is_empty()); - call(macros, args.iter()) + call(stack, args.iter(), eval_context) } "eval" => { assert_eq!(args.len(), 1); - let should_eval = eval(macros, &args[0])?; - if let Some(to_eval) = to_eval { - to_eval.borrow_mut().push(should_eval); + let should_eval = eval(stack, &args[0], eval_context.as_deref_mut())?; + if let Some(eval_context) = eval_context { + eval_context.eval(should_eval)?; } else { bail!("tried to eval something but no eval back-channel was available"); } @@ -131,17 +145,17 @@ pub fn expand_call( "origin" => { assert_eq!(args.len(), 1); - origin(macros, &args[0]) + origin(stack, &args[0], eval_context) } "error" => { assert_eq!(args.len(), 1); - meta::error(macros, &args[0]) + meta::error(stack, &args[0], eval_context) } "shell" => { assert_eq!(args.len(), 1); - shell(macros, &args[0]) + shell(stack, &args[0], eval_context) } // fallback @@ -154,54 +168,58 @@ mod text { use super::*; pub fn subst( - macros: &MacroSet, + stack: &MacroScopeStack, from: &TokenString, to: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let from = macros.expand(from)?; - let to = macros.expand(to)?; - let text = macros.expand(text)?; + let from = stack.expand(from, eval_context.as_deref_mut())?; + let to = stack.expand(to, eval_context.as_deref_mut())?; + let text = stack.expand(text, eval_context)?; Ok(text.replace(&from, &to)) } pub fn patsubst( - macros: &MacroSet, + stack: &MacroScopeStack, from: &TokenString, to: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let from = macros.expand(from)?; - let to = macros.expand(to)?; - let text = macros.expand(text)?; - let words = text - .split_whitespace() - .map(|word| { - let pattern_match = r#match(&from, word)?.and_then(|x| x.get(1)); - Ok(if let Some(pm) = pattern_match { - to.replace('%', pm.as_str()) - } else { - word.to_owned() + let from = stack.expand(from, eval_context.as_deref_mut())?; + let to = stack.expand(to, eval_context.as_deref_mut())?; + let text = stack.expand(text, eval_context)?; + let words = + text.split_whitespace() + .map(|word| { + let pattern_match = r#match(&from, word)?.and_then(|x| x.get(1)); + Ok(pattern_match + .map_or_else(|| word.to_owned(), |pm| to.replace('%', pm.as_str()))) }) - }) - .collect::<Result<Vec<_>>>()?; + .collect::<Result<Vec<_>>>()?; Ok(words.join(" ")) } - pub fn strip(macros: &MacroSet, text: &TokenString) -> Result<String> { - let text = macros.expand(text)?; + pub fn strip( + stack: &MacroScopeStack, + text: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let text = stack.expand(text, eval_context)?; // TODO don't allocate this vec let words = text.split_whitespace().collect::<Vec<_>>(); Ok(words.join(" ")) } pub fn findstring( - macros: &MacroSet, + stack: &MacroScopeStack, needle: &TokenString, haystack: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let needle = macros.expand(needle)?; - let haystack = macros.expand(haystack)?; + let needle = stack.expand(needle, eval_context.as_deref_mut())?; + let haystack = stack.expand(haystack, eval_context)?; if haystack.contains(&needle) { Ok(needle) } else { @@ -209,10 +227,15 @@ mod text { } } - pub fn filter(macros: &MacroSet, patterns: &TokenString, text: &TokenString) -> Result<String> { - let patterns = macros.expand(patterns)?; + pub fn filter( + stack: &MacroScopeStack, + patterns: &TokenString, + text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let patterns = stack.expand(patterns, eval_context.as_deref_mut())?; let patterns = patterns.split_whitespace().collect::<Vec<_>>(); - let text = macros.expand(text)?; + let text = stack.expand(text, eval_context)?; let text = text.split_whitespace(); let mut result_pieces = vec![]; for word in text { @@ -227,13 +250,14 @@ mod text { } pub fn filter_out( - macros: &MacroSet, + stack: &MacroScopeStack, patterns: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let patterns = macros.expand(patterns)?; + let patterns = stack.expand(patterns, eval_context.as_deref_mut())?; let patterns = patterns.split_whitespace().collect::<Vec<_>>(); - let text = macros.expand(text)?; + let text = stack.expand(text, eval_context)?; let text = text.split_whitespace(); let mut result_pieces = vec![]; for word in text { @@ -247,18 +271,27 @@ mod text { Ok(result_pieces.join(" ")) } - pub fn sort(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn sort( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let mut words = words.split_whitespace().collect::<Vec<_>>(); words.sort_unstable(); words.dedup(); Ok(words.join(" ")) } - pub fn word(macros: &MacroSet, n: &TokenString, text: &TokenString) -> Result<String> { - let n = macros.expand(n)?; + pub fn word( + stack: &MacroScopeStack, + n: &TokenString, + text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let n = stack.expand(n, eval_context.as_deref_mut())?; let n: usize = n.parse().wrap_err("while calling `word`")?; - let text = macros.expand(text)?; + let text = stack.expand(text, eval_context)?; Ok(text .split_whitespace() .nth(n.saturating_add(1)) @@ -266,18 +299,30 @@ mod text { .to_owned()) } - pub fn words(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn words( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; Ok(words.split_whitespace().count().to_string()) } - pub fn firstword(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; - Ok(words.split_whitespace().nth(0).unwrap_or("").to_owned()) + pub fn firstword( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; + Ok(words.split_whitespace().next().unwrap_or("").to_owned()) } - pub fn lastword(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn lastword( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; Ok(words.split_whitespace().last().unwrap_or("").to_owned()) } } @@ -287,14 +332,19 @@ mod file_name { use std::env; use std::ffi::OsStr; use std::fs; + use std::io::BufRead; use std::path::{Path, MAIN_SEPARATOR}; - use eyre::WrapErr; - use super::*; + use crate::makefile::eval_context::DeferredEvalContext; + use eyre::WrapErr; - pub fn dir(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn dir( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { @@ -309,8 +359,12 @@ mod file_name { Ok(words.join(" ")) } - pub fn notdir(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn notdir( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { @@ -323,8 +377,12 @@ mod file_name { Ok(words.join(" ")) } - pub fn basename(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn basename( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { @@ -338,12 +396,13 @@ mod file_name { } pub fn addsuffix( - macros: &MacroSet, + stack: &MacroScopeStack, suffix: &TokenString, targets: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let suffix = macros.expand(suffix)?; - let targets = macros.expand(targets)?; + let suffix = stack.expand(suffix, eval_context.as_deref_mut())?; + let targets = stack.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", t, suffix)) @@ -352,12 +411,13 @@ mod file_name { } pub fn addprefix( - macros: &MacroSet, + stack: &MacroScopeStack, prefix: &TokenString, targets: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let prefix = macros.expand(prefix)?; - let targets = macros.expand(targets)?; + let prefix = stack.expand(prefix, eval_context.as_deref_mut())?; + let targets = stack.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", prefix, t)) @@ -365,8 +425,12 @@ mod file_name { Ok(results.join(" ")) } - pub fn wildcard(macros: &MacroSet, pattern: &TokenString) -> Result<String> { - let pattern = macros.expand(pattern)?; + pub fn wildcard( + stack: &MacroScopeStack, + pattern: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let pattern = stack.expand(pattern, eval_context)?; let home_dir = env::var("HOME") .ok() .or_else(|| dirs::home_dir().and_then(|p| p.to_str().map(String::from))); @@ -385,8 +449,12 @@ mod file_name { Ok(results.join(" ")) } - pub fn realpath(macros: &MacroSet, targets: &TokenString) -> Result<String> { - let targets = macros.expand(targets)?; + pub fn realpath( + stack: &MacroScopeStack, + targets: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let targets = stack.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|x| { @@ -398,43 +466,52 @@ mod file_name { Ok(results.join(" ")) } - pub fn abspath(macros: &MacroSet, targets: &TokenString) -> Result<String> { + pub fn abspath( + stack: &MacroScopeStack, + targets: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { // TODO don't resolve symlinks - realpath(macros, targets) + realpath(stack, targets, eval_context) } } // Functions for Conditionals mod conditional { + use std::borrow::Cow; + use std::cmp::Ordering; + use super::*; + use eyre::eyre; pub fn r#if( - macros: &MacroSet, + stack: &MacroScopeStack, condition: &TokenString, if_true: &TokenString, if_false: Option<&TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { let mut condition = condition.clone(); condition.trim_start(); condition.trim_end(); - let condition = macros.expand(&condition)?; + let condition = stack.expand(&condition, eval_context.as_deref_mut())?; if condition.is_empty() { - if let Some(if_false) = if_false { - macros.expand(if_false) - } else { - Ok(String::new()) - } + if_false.map_or_else( + || Ok(String::new()), + |if_false| stack.expand(if_false, eval_context), + ) } else { - macros.expand(if_true) + stack.expand(if_true, eval_context) } } pub fn or<'a>( - macros: &MacroSet, + stack: &MacroScopeStack, args: impl Iterator<Item = &'a TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { for arg in args { - let arg = macros.expand(arg)?; + let arg = stack.expand(arg, eval_context.as_deref_mut())?; if !arg.is_empty() { return Ok(arg); } @@ -443,33 +520,80 @@ mod conditional { } pub fn and<'a>( - macros: &MacroSet, + stack: &MacroScopeStack, args: impl Iterator<Item = &'a TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { let mut last = String::new(); for arg in args { - last = macros.expand(arg)?; + last = stack.expand(arg, eval_context.as_deref_mut())?; if last.is_empty() { return Ok(String::new()); } } Ok(last) } + + pub fn intcmp<'a>( + stack: &MacroScopeStack, + lhs: &TokenString, + rhs: &TokenString, + lt_part: Option<&TokenString>, + eq_part: Option<&TokenString>, + gt_part: Option<&TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let raw_lhs_value = stack.expand(lhs, eval_context.as_deref_mut())?; + let raw_rhs_value = stack.expand(rhs, eval_context.as_deref_mut())?; + let lhs_value: i64 = raw_lhs_value.parse()?; + let rhs_value: i64 = raw_rhs_value.parse()?; + let cmp = lhs_value.cmp(&rhs_value); + + // defaults are a bit of a mess + let mut lt_part = lt_part.map(Cow::Borrowed); + let mut eq_part = eq_part.map(Cow::Borrowed); + let mut gt_part = gt_part.map(Cow::Borrowed); + if lt_part.is_none() && eq_part.is_none() && gt_part.is_none() { + lt_part = Some(Cow::Owned(TokenString::empty())); + // not just reusing lhs param since expansion could have a side effect + eq_part = Some(Cow::Owned(TokenString::text(raw_lhs_value))); + gt_part = Some(Cow::Owned(TokenString::empty())); + } + if eq_part.is_none() { + eq_part = Some(Cow::Owned(TokenString::empty())); + } + if gt_part.is_none() { + gt_part = eq_part.clone(); + } + + let lt_part = lt_part.ok_or_else(|| eyre!("intcmp defaults failed"))?; + let eq_part = eq_part.ok_or_else(|| eyre!("intcmp defaults failed"))?; + let gt_part = gt_part.ok_or_else(|| eyre!("intcmp defaults failed"))?; + + let result = match cmp { + Ordering::Less => lt_part, + Ordering::Equal => eq_part, + Ordering::Greater => gt_part, + }; + + stack.expand(&result, eval_context.as_deref_mut()) + } } pub fn foreach( - macros: &MacroSet, + stack: &MacroScopeStack, var: &TokenString, list: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let var = macros.expand(var)?; - let list = macros.expand(list)?; + let var = stack.expand(var, eval_context.as_deref_mut())?; + let list = stack.expand(list, eval_context.as_deref_mut())?; let words = list.split_whitespace(); - let mut macros = macros.with_overlay(); let results = words .map(|word| { + let mut macros = MacroSet::new(); macros.set( var.clone(), Macro { @@ -479,20 +603,26 @@ pub fn foreach( eagerly_expanded: false, }, ); - macros.expand(text) + stack + .with_scope(¯os) + .expand(text, eval_context.as_deref_mut()) }) .collect::<Result<Vec<_>, _>>()?; Ok(results.join(" ")) } -pub fn call<'a>(macros: &MacroSet, args: impl Iterator<Item = &'a TokenString>) -> Result<String> { +pub fn call<'a>( + stack: &MacroScopeStack, + args: impl Iterator<Item = &'a TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { let args = args - .map(|arg| macros.expand(arg)) + .map(|arg| stack.expand(arg, eval_context.as_deref_mut())) .collect::<Result<Vec<_>, _>>()?; let function = args[0].clone(); // TODO if function is a builtin, call the builtin instead - let mut macros = macros.with_overlay(); + let mut macros = MacroSet::new(); for (i, x) in args.into_iter().enumerate() { macros.set( i.to_string(), @@ -504,31 +634,49 @@ pub fn call<'a>(macros: &MacroSet, args: impl Iterator<Item = &'a TokenString>) }, ); } - macros.expand(&TokenString::r#macro(function)) + stack + .with_scope(¯os) + .expand(&TokenString::r#macro(function), eval_context) } // TODO consider bringing eval logic in here since we put the Vec in MacroSet IIRC -pub fn eval(macros: &MacroSet, arg: &TokenString) -> Result<String> { - macros.expand(arg) +pub fn eval( + stack: &MacroScopeStack, + arg: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { + stack.expand(arg, eval_context) } -pub fn origin(macros: &MacroSet, variable: &TokenString) -> Result<String> { - let variable = macros.expand(variable)?; - Ok(macros.origin(&variable).to_owned()) +pub fn origin( + stack: &MacroScopeStack, + variable: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { + let variable = stack.expand(variable, eval_context)?; + Ok(stack.origin(&variable).to_owned()) } mod meta { use super::*; - pub fn error(macros: &MacroSet, text: &TokenString) -> Result<String> { - let text = macros.expand(text)?; + pub fn error( + stack: &MacroScopeStack, + text: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let text = stack.expand(text, eval_context)?; bail!("{}", text); } } -pub fn shell(macros: &MacroSet, command: &TokenString) -> Result<String> { +pub fn shell( + stack: &MacroScopeStack, + command: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { // TODO bring this in from command_line - let command = macros.expand(command)?; + let command = stack.expand(command, eval_context)?; let (program, args) = if cfg!(windows) { let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()); let args = vec!["/c", &command]; @@ -557,7 +705,8 @@ mod test { type R = Result<()>; fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> Result<String> { - super::expand_call(name, args, macros, None) + let stack = MacroScopeStack::default().with_scope(macros); + expand_call(name, args, &stack, NO_EVAL) } macro_rules! call { @@ -766,6 +915,148 @@ mod test { } #[test] + fn intcmp() -> R { + assert_eq!( + call( + "intcmp", + &[TokenString::text("1"), TokenString::text("2")], + &MacroSet::new() + )?, + "" + ); + assert_eq!( + call( + "intcmp", + &[TokenString::text("2"), TokenString::text("2")], + &MacroSet::new() + )?, + "2" + ); + + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("1"), + TokenString::text("2"), + TokenString::text("a") + ], + &MacroSet::new() + )?, + "a" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("2"), + TokenString::text("2"), + TokenString::text("a") + ], + &MacroSet::new() + )?, + "" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("3"), + TokenString::text("2"), + TokenString::text("a") + ], + &MacroSet::new() + )?, + "" + ); + + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("1"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b") + ], + &MacroSet::new() + )?, + "a" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("2"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b") + ], + &MacroSet::new() + )?, + "b" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("3"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b") + ], + &MacroSet::new() + )?, + "b" + ); + + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("1"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b"), + TokenString::text("c") + ], + &MacroSet::new() + )?, + "a" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("2"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b"), + TokenString::text("c") + ], + &MacroSet::new() + )?, + "b" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("3"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b"), + TokenString::text("c") + ], + &MacroSet::new() + )?, + "c" + ); + + Ok(()) + } + + #[test] fn foreach() -> R { let mut macros = MacroSet::new(); macros.set( diff --git a/src/makefile/inference_rules.rs b/src/makefile/inference_rules.rs index 368d72b..f841797 100644 --- a/src/makefile/inference_rules.rs +++ b/src/makefile/inference_rules.rs @@ -1,18 +1,18 @@ -use std::fmt; +use std::collections::HashMap; +use std::{fmt, mem}; -use eyre::{eyre, Result}; -use regex::Captures; - -use super::command_line::CommandLine; use super::pattern::r#match; -use super::ItemSource; +use super::{CommandLine, ItemSource, MacroSet}; +use eyre::{eyre, OptionExt, Result}; +use regex::Captures; -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(Clone, Debug)] pub struct InferenceRule { pub source: ItemSource, pub products: Vec<String>, pub prerequisites: Vec<String>, pub commands: Vec<CommandLine>, + pub macros: MacroSet, } impl InferenceRule { @@ -22,18 +22,23 @@ impl InferenceRule { s1: String, s2: String, commands: Vec<CommandLine>, + macros: MacroSet, ) -> Self { Self { source, products: vec![format!("%{}", s1)], prerequisites: vec![format!("%{}", s2)], commands, + macros, } } pub fn first_match<'s, 't: 's>(&'s self, target_name: &'t str) -> Result<Option<Captures<'t>>> { self.products .iter() + // TODO find a better way to make the self_subdir_match test pass + .flat_map(|pattern| [pattern.strip_prefix("./"), Some(pattern.as_str())]) + .filter_map(|x| x) .map(|pattern| r#match(pattern, target_name)) .try_fold(None, |x, y| y.map(|y| x.or(y))) } @@ -49,12 +54,39 @@ impl InferenceRule { let capture = self .first_match(target_name)? .ok_or_else(|| eyre!("asked non-matching inference rule for prerequisites"))?; - let percent_expansion = capture.get(1).expect("should've matched the %").as_str(); + let percent_expansion = capture + .get(1) + .ok_or_eyre("should've matched the %")? + .as_str(); Ok(self .prerequisites .iter() .map(move |p| p.replace('%', percent_expansion))) } + + fn extend(&mut self, other: Self) { + assert_eq!(&self.products, &other.products); + match (self.commands.is_empty(), other.commands.is_empty()) { + (false, false) => { + // both rules have commands, so replace this entirely + *self = other; + } + (true, false) => { + // this rule doesn't have commands, but the other one does, + // so it's the real one + let mut other = other; + mem::swap(self, &mut other); + self.extend(other); + } + (false, true) | (true, true) => { + // this rule might have commands, but the other one doesn't, + // so append non-command stuff + // TODO decide something smart about sources + self.prerequisites.extend(other.prerequisites); + self.macros.extend(other.macros); + } + } + } } impl fmt::Display for InferenceRule { @@ -72,11 +104,68 @@ impl fmt::Display for InferenceRule { } } +#[derive(Clone, Default)] +pub struct InferenceRuleSet { + /// Maps from products to a map from prerequisites to rules. + data: HashMap<Vec<String>, HashMap<Vec<String>, InferenceRule>>, +} + +impl InferenceRuleSet { + pub fn get(&self, products: &[String], prerequisites: &[String]) -> Option<&InferenceRule> { + self.data.get(products).and_then(|x| x.get(prerequisites)) + } + + fn get_mut( + &mut self, + products: &[String], + prerequisites: &[String], + ) -> Option<&mut InferenceRule> { + self.data + .get_mut(products) + .and_then(|x| x.get_mut(prerequisites)) + } + + pub fn put(&mut self, rule: InferenceRule) { + if let Some(existing_rule) = self.get_mut(&rule.products, &rule.prerequisites) { + existing_rule.extend(rule); + } else { + self.data + .entry(rule.products.clone()) + .or_default() + .insert(rule.prerequisites.clone(), rule); + } + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn extend(&mut self, other: Self) { + for other in other.data.into_values().flat_map(HashMap::into_values) { + self.put(other); + } + } + + pub fn iter(&self) -> impl Iterator<Item = &InferenceRule> { + self.data.values().flat_map(HashMap::values) + } +} + +impl From<Vec<InferenceRule>> for InferenceRuleSet { + fn from(value: Vec<InferenceRule>) -> Self { + let mut result = Self::default(); + for rule in value { + result.put(rule); + } + result + } +} + #[cfg(test)] mod test { use super::*; - type R = eyre::Result<()>; + type R = Result<()>; #[test] fn suffix_match() -> R { @@ -85,6 +174,7 @@ mod test { ".o".to_owned(), ".c".to_owned(), vec![], + MacroSet::new(), ); assert!(rule.matches("foo.o")?); assert!(rule.matches("dir/foo.o")?); @@ -104,8 +194,38 @@ mod test { "goneall.gpg".to_owned(), ], commands: vec![], + macros: MacroSet::new(), }; assert!(rule.matches("licenseListPublisher-2.2.1.jar-valid")?); Ok(()) } + + #[cfg(feature = "full")] + #[test] + fn subdir_match() -> R { + let rule = InferenceRule { + source: ItemSource::Builtin, + products: vec!["a/%.o".to_owned()], + prerequisites: vec!["a/%.c".to_owned()], + commands: vec![], + macros: MacroSet::new(), + }; + assert!(rule.matches("a/foo.o")?); + Ok(()) + } + + #[cfg(feature = "full")] + #[test] + fn self_subdir_match() -> R { + let rule = InferenceRule { + source: ItemSource::Builtin, + products: vec!["./%.o".to_owned()], + prerequisites: vec!["./%.c".to_owned()], + commands: vec![], + macros: MacroSet::new(), + }; + assert!(rule.matches("foo.o")?); + assert!(rule.matches("./foo.o")?); + Ok(()) + } } diff --git a/src/makefile/input.rs b/src/makefile/input.rs index c2f8e7b..345f465 100644 --- a/src/makefile/input.rs +++ b/src/makefile/input.rs @@ -2,7 +2,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::error::Error as StdError; use std::fs::File; -use std::io::{BufRead, BufReader, Cursor, Error as IoError, ErrorKind as IoErrorKind, Lines}; +use std::io::{BufRead, BufReader, Error as IoError, ErrorKind as IoErrorKind, Lines}; use std::iter::Peekable; use std::path::Path; use std::rc::Rc; @@ -13,19 +13,24 @@ use regex::Regex; use crate::args::Args; -use super::command_line::CommandLine; #[cfg(feature = "full")] use super::conditional::{Line as ConditionalLine, State as ConditionalState}; -use super::inference_rules::InferenceRule; +#[cfg(feature = "full")] +use super::eval_context::DeferredEvalContext; +use super::parse::{MacroAssignment, MacroAssignmentOutcome}; #[cfg(feature = "full")] use super::r#macro::ExportConfig; -use super::r#macro::{Macro, Set as MacroSet}; -use super::target::{StaticTargetSet, Target}; -use super::token::{Token, TokenString}; -use super::ItemSource; +use super::r#macro::Macro; +use super::target::StaticTargetSet; +use super::{ + builtin_targets, CommandLine, InferenceRule, InferenceRuleSet, ItemSource, LookupInternal, + MacroScopeStack, MacroSet, Target, TokenString, +}; enum LineType { Rule, + #[cfg(feature = "full")] + RuleMacro, Macro, Include, #[cfg(feature = "full")] @@ -52,32 +57,31 @@ impl LineType { if line_tokens.starts_with("unexport ") || line_tokens == "unexport" { return Self::Unexport; } - for token in line_tokens.tokens() { - if let Token::Text(text) = token { - let colon_idx = text.find(':'); - #[cfg(not(feature = "full"))] - let equals_idx = text.find('='); + let colon_idx = line_tokens.find(":"); + #[cfg(not(feature = "full"))] + let equals_idx = line_tokens.find("="); + #[cfg(feature = "full")] + let equals_idx = ["=", ":=", "::=", "?=", "+="] + .iter() + .filter_map(|p| line_tokens.find(p)) + .min(); + match (colon_idx, equals_idx) { + (Some(_), None) => { + return Self::Rule; + } + (Some(c), Some(e)) if c < e => { #[cfg(feature = "full")] - let equals_idx = ["=", ":=", "::=", "?=", "+="] - .iter() - .filter_map(|p| text.find(p)) - .min(); - match (colon_idx, equals_idx) { - (Some(_), None) => { - return Self::Rule; - } - (Some(c), Some(e)) if c < e => { - return Self::Rule; - } - (None, Some(_)) => { - return Self::Macro; - } - (Some(c), Some(e)) if e <= c => { - return Self::Macro; - } - _ => {} - } + return Self::RuleMacro; + #[cfg(not(feature = "full"))] + return Self::Rule; } + (None, Some(_)) => { + return Self::Macro; + } + (Some(c), Some(e)) if e <= c => { + return Self::Macro; + } + _ => {} } Self::Unknown } @@ -94,9 +98,11 @@ fn inference_match<'a>( prerequisites: &[String], ) -> Option<InferenceMatch<'a>> { lazy_static! { - static ref INFERENCE_RULE: Regex = - Regex::new(r"^(?P<s2>(\.[^/.]+)?)(?P<s1>\.[^/.]+)$").unwrap(); - static ref SPECIAL_TARGET: Regex = Regex::new(r"^\.[A-Z]+$").unwrap(); + static ref INFERENCE_RULE: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"^(?P<s2>(\.[^/.]+)?)(?P<s1>\.[^/.]+)$") + .unwrap(); + static ref SPECIAL_TARGET: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"^\.[A-Z]+$").unwrap(); } let inference_match = INFERENCE_RULE.captures(targets[0]); @@ -107,6 +113,7 @@ fn inference_match<'a>( && inference_match.is_some() && special_target_match.is_none(); if inference_rule { + #[allow(clippy::unwrap_used)] inference_match.map(|x| InferenceMatch { s1: x.name("s1").unwrap().as_str(), s2: x.name("s2").unwrap().as_str(), @@ -126,7 +133,7 @@ where E: StdError + Send + Sync + 'static, Inner: Iterator<Item = Result<T, E>>, { - fn new(inner: Inner) -> Self { + const fn new(inner: Inner) -> Self { Self(inner, 0) } } @@ -181,30 +188,33 @@ impl Default for NextLineSettings { pub struct MakefileReader<'a, 'parent, R: BufRead> { file_name: String, - pub inference_rules: Vec<InferenceRule>, - pub macros: MacroSet<'parent, 'static>, + pub inference_rules: InferenceRuleSet, + pub stack: MacroScopeStack<'parent>, + pub macros: MacroSet, pub targets: StaticTargetSet, built_in_targets: HashMap<String, Target>, pub first_non_special_target: Option<String>, pub failed_includes: Vec<String>, - args: &'a Args, + pub args: &'a Args, lines_iter: Peekable<LineNumbers<String, IoError, Lines<R>>>, // join with escaped_newline_replacement to get the actual line pending_line: Option<(usize, Vec<String>)>, #[cfg(feature = "full")] conditional_stack: Vec<ConditionalState>, - file_names: Rc<RefCell<Vec<String>>>, + pub file_names: Rc<RefCell<Vec<String>>>, } impl<'a, 'parent> MakefileReader<'a, 'parent, BufReader<File>> { pub fn read_file( args: &'a Args, - mut macros: MacroSet<'parent, 'static>, + stack: MacroScopeStack<'parent>, path: impl AsRef<Path>, file_names: Rc<RefCell<Vec<String>>>, ) -> Result<Self> { + let mut macros = MacroSet::new(); #[cfg(feature = "full")] - if let Some(mut old_makefile_list) = macros.pop("MAKEFILE_LIST") { + if let Some(old_makefile_list) = stack.get("MAKEFILE_LIST") { + let mut old_makefile_list = old_makefile_list.into_owned(); old_makefile_list.text.extend(TokenString::text(format!( " {}", path.as_ref().to_string_lossy() @@ -227,14 +237,15 @@ impl<'a, 'parent> MakefileReader<'a, 'parent, BufReader<File>> { // TODO handle errors let file = file.context("couldn't open makefile!")?; let file_reader = BufReader::new(file); - Self::read(args, macros, file_reader, file_name, file_names) + Self::read(args, stack, macros, file_reader, file_name, file_names) } } impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { pub fn read( args: &'a Args, - macros: MacroSet<'parent, 'static>, + stack: MacroScopeStack<'parent>, + macros: MacroSet, source: R, name: impl Into<String>, file_names: Rc<RefCell<Vec<String>>>, @@ -242,7 +253,8 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { let name = name.into(); let mut reader = Self { file_name: name.clone(), - inference_rules: Vec::new(), + inference_rules: InferenceRuleSet::default(), + stack, macros, targets: Default::default(), built_in_targets: HashMap::new(), @@ -257,18 +269,10 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { }; // TODO be smart about this instead, please if !args.no_builtin_rules { - reader.built_in_targets.insert( - ".SUFFIXES".to_owned(), - Target { - name: ".SUFFIXES".into(), - prerequisites: vec![".o", ".c", ".y", ".l", ".a", ".sh", ".f"] - .into_iter() - .map(String::from) - .collect(), - commands: vec![], - stem: None, - already_updated: Cell::new(false), - }, + reader.built_in_targets.extend( + builtin_targets() + .into_iter() + .map(|target| (target.name.clone(), target)), ); } reader @@ -300,30 +304,12 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { let line_type = LineType::of(&line_tokens); // before we actually test it, see if it's only visible after expanding macros - let (line_tokens, line_type) = if let LineType::Unknown = line_type { + let (line_tokens, line_type) = if matches!(line_type, LineType::Unknown) { let line_tokens = TokenString::text( self.expand_macros(&line_tokens) .wrap_err_with(|| format!("while parsing line {}", line_number))? .trim(), ); - // and let's eval whatever bullshit needs evaling - #[cfg(feature = "full")] - { - let eval = self.macros.to_eval.take(); - for eval in eval { - let child_macros = self.macros.with_overlay(); - let child = MakefileReader::read( - self.args, - child_macros, - Cursor::new(eval), - "<eval>", - Rc::clone(&self.file_names), - ) - .context("while evaling")? - .finish(); - self.extend(child); - } - } let line_type = LineType::of(&line_tokens); (line_tokens, line_type) } else { @@ -339,6 +325,16 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { ) })?; } + #[cfg(feature = "full")] + LineType::RuleMacro => { + self.read_rule_macro(line_tokens, line_number) + .wrap_err_with(|| { + format!( + "while parsing rule-specific macro definition starting on line {}", + line_number + ) + })?; + } LineType::Macro => { self.read_macro(line_tokens, line_number) .wrap_err_with(|| { @@ -363,8 +359,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { } else { let exported = if line_tokens.contains_text("=") { // that's an assignment! - let new_macro = self.read_macro(line_tokens, line_number)?; - new_macro + self.read_macro(line_tokens, line_number)? } else { self.expand_macros(&line_tokens)? }; @@ -399,7 +394,8 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { fn next_line(&mut self, settings: NextLineSettings) -> Option<(usize, Result<String>)> { lazy_static! { - static ref COMMENT: Regex = Regex::new(r"(^|[^\\])#.*$").unwrap(); + static ref COMMENT: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"(^|[^\\])#.*$").unwrap(); } let escaped_newline_replacement = settings.escaped_newline_replacement; if let Some((line_number, line)) = self.pending_line.take() { @@ -443,15 +439,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { Ok(x) => x, Err(err) => return Some((n, Err(err))), }; - let line = if settings.strip_comments { - COMMENT - .replace(&line, "$1") - .replace(r"\#", "#") - .trim_end() - .to_owned() - } else { - line - }; + // TODO strip comments if it's correct to line_pieces.push(line.trim_start().to_owned()); } } @@ -466,15 +454,19 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { Err(err) => return Some((line_number, Err(err))), }; if let Some(line) = cond_line { + let mut deferred_eval_context = DeferredEvalContext::new(self); let action = line .action( self.conditional_stack.last(), - |name| self.macros.is_defined(name), - |t| self.expand_macros(t), + |name| self.stack.with_scope(&self.macros).is_defined(name), + |t| self.expand_macros_deferred_eval(t, &mut deferred_eval_context), ) .wrap_err_with(|| { format!("while applying conditional on line {}", line_number) }); + for child in deferred_eval_context { + self.extend(child); + } let action = match action { Ok(x) => x, Err(err) => return Some((line_number, Err(err))), @@ -520,17 +512,13 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { } fn special_target_has_prereq(&self, target: &str, name: &str, empty_counts: bool) -> bool { - match self - .targets + self.targets .get(target) .or_else(|| self.built_in_targets.get(target)) - { - Some(target) => { + .map_or(false, |target| { (empty_counts && target.prerequisites.is_empty()) || target.prerequisites.iter().any(|e| e == name) - } - None => false, - } + }) } fn read_include(&mut self, mut line_tokens: TokenString, line_number: usize) -> Result<()> { @@ -545,10 +533,10 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { // handles arbitrarily many filenames, and it's not like that's more work for field in fields { log::trace!("{}:{}: including {}", &self.file_name, line_number, field); - let child_macros = self.macros.with_overlay(); + let child_stack = self.stack.with_scope(&self.macros); let child = MakefileReader::read_file( self.args, - child_macros, + child_stack, field, Rc::clone(&self.file_names), ) @@ -605,45 +593,39 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { None => (not_targets, vec![]), }; if prerequisites.contains_text("=") { - log::error!("rule-specific macros are not implemented yet"); - return Ok(()); + bail!("handling rule-specific macro as rule"); } + #[cfg(feature = "full")] + let mut deferred_eval_context = DeferredEvalContext::new(self); let prerequisites = self - .macros - .with_lookup(&|macro_name: &str| { - let macro_pieces = if macro_name.starts_with('@') { - // The $@ shall evaluate to the full target name of the - // current target. - targets.iter() - } else { - bail!("unknown internal macro") - }; - - let macro_pieces = if macro_name.ends_with('D') { - macro_pieces - .map(|x| { - Path::new(x) - .parent() - .ok_or_else(|| eyre!("no parent")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<Vec<String>, _>>()? - } else if macro_name.ends_with('F') { - macro_pieces - .map(|x| { - Path::new(x) - .file_name() - .ok_or_else(|| eyre!("no filename")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<Vec<String>, _>>()? - } else { - macro_pieces.map(|&x| x.to_owned()).collect::<Vec<String>>() - }; - - Ok(macro_pieces.join(" ")) - }) - .expand(&prerequisites)?; + .stack + .with_scope(&self.macros) + .with_scope(&LookupInternal::new_partial(&targets)) + .expand( + &prerequisites, + #[cfg(feature = "full")] + Some(&mut deferred_eval_context), + )?; + // https://www.gnu.org/software/make/manual/html_node/Secondary-Expansion.html + // this is supposed to be deferred but maybe i can get away with this + // TODO move this to run at runtime + #[cfg(feature = "full")] + let prerequisites = if self.targets.has(".SECONDEXPANSION") { + self.stack + .with_scope(&self.macros) + .with_scope(&LookupInternal::new_partial(&targets)) + .expand( + &prerequisites.parse()?, + #[cfg(feature = "full")] + Some(&mut deferred_eval_context), + )? + } else { + prerequisites + }; + #[cfg(feature = "full")] + for child in deferred_eval_context { + self.extend(child); + } let prerequisites = prerequisites .split_whitespace() .map(|x| x.into()) @@ -697,6 +679,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { products: targets.into_iter().map(|x| x.to_owned()).collect(), prerequisites, commands, + macros: MacroSet::new(), }; if let Some(static_targets) = static_targets { @@ -712,12 +695,14 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { .first_match(real_target)? .and_then(|x| x.get(1).map(|x| x.as_str().to_owned())), already_updated: Cell::new(false), + macros: MacroSet::new(), }; self.targets.put(new_target); } } } else { - self.inference_rules.push(new_rule); + log::debug!("pattern-based inference rule defined: {:?}", &new_rule,); + self.inference_rules.put(new_rule); } return Ok(()); } @@ -753,20 +738,17 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { inference_match.s1.to_owned(), inference_match.s2.to_owned(), commands, + MacroSet::new(), ); - log::trace!( + log::debug!( "suffix-based inference rule defined by {:?} - {:?}", &inference_match, &new_rule, ); - self.inference_rules.retain(|existing_rule| { - (&existing_rule.prerequisites, &existing_rule.products) - != (&new_rule.prerequisites, &new_rule.products) - }); - self.inference_rules.push(new_rule); + self.inference_rules.put(new_rule); } else { - log::trace!( + log::debug!( "{}:{}: new target {:?} based on {:?}", &self.file_name, line_number, @@ -784,6 +766,135 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { commands: commands.clone(), stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), + }; + self.targets.put(new_target); + } + } + + Ok(()) + } + + #[cfg(feature = "full")] + fn read_rule_macro(&mut self, line_tokens: TokenString, line_number: usize) -> Result<()> { + let (targets, macro_def) = line_tokens + .split_once(":") + .ok_or_else(|| eyre!("read_rule couldn't find a ':' on line {}", line_number))?; + lazy_static! { + // my kingdom for lookahead + static ref NON_EAGER_EXPANSION_ASSIGNMENT_COLON: Regex = #[allow(clippy::unwrap_used)] Regex::new(":[^:=]").unwrap(); + } + if macro_def.matches_regex(&NON_EAGER_EXPANSION_ASSIGNMENT_COLON) { + bail!("GNUful static patterns not yet implemented in rule-specific macros"); + }; + let targets = self.expand_macros(&targets)?; + let targets = targets.split_whitespace().collect::<Vec<_>>(); + + let (name, value) = macro_def + .split_once("=") + .ok_or_else(|| eyre!("read_macro couldn't find a '=' on line {}", line_number))?; + let macro_assignment = self.parse_macro_assignment(name, value, line_number)?; + + if targets.is_empty() { + return Ok(()); + } + + // we don't know yet if it's a target rule or an inference rule (or a GNUish "pattern rule") + let inference_match = inference_match(&targets, &[]); + let is_pattern = targets.iter().all(|x| x.contains('%')); + + // TODO resolve against existing stack + let mut macro_set = MacroSet::new(); + if let Some(outcome) = self + .check_macro_assignment_outcome(¯o_assignment, self.stack.with_scope(&self.macros)) + { + let (name, value) = self.resolve_macro_value(macro_assignment, outcome, line_number)?; + // TODO trace + macro_set.set(name, value); + } + + if is_pattern { + let new_rule = InferenceRule { + source: ItemSource::File { + name: self.file_name.clone(), + line: line_number, + }, + products: targets.into_iter().map(|x| x.to_owned()).collect(), + prerequisites: vec![], + commands: vec![], + macros: macro_set, + }; + + log::error!( + "{}:{}: inference rule specific macros not yet working", + &self.file_name, + line_number + ); + + self.inference_rules.put(new_rule); + return Ok(()); + } + + // don't interpret things like `.tmp: ; mkdir -p $@` as single-suffix rules + let inference_match = inference_match.and_then(|inference| { + if self.special_target_has_prereq(".SUFFIXES", inference.s1, false) + && (inference.s2.is_empty() + || self.special_target_has_prereq(".SUFFIXES", inference.s2, false)) + { + Some(inference) + } else { + log::info!( + "{}:{}: looks like {:?} is not a suffix rule because .SUFFIXES is {:?}", + &self.file_name, + line_number, + inference, + self.targets + .get(".SUFFIXES") + .or_else(|| self.built_in_targets.get(".SUFFIXES")) + .map(|x| &x.prerequisites) + ); + None + } + }); + + if let Some(inference_match) = inference_match { + let new_rule = InferenceRule::new_suffix( + ItemSource::File { + name: self.file_name.clone(), + line: line_number, + }, + inference_match.s1.to_owned(), + inference_match.s2.to_owned(), + vec![], + macro_set, + ); + log::error!( + "{}:{}: inference rule specific macros not yet working", + &self.file_name, + line_number + ); + + self.inference_rules.put(new_rule); + } else { + log::trace!( + "{}:{}: target {:?} gets macros {:?}", + &self.file_name, + line_number, + &targets, + ¯o_set + ); + for target in targets { + if self.first_non_special_target.is_none() && !target.starts_with('.') { + self.first_non_special_target = Some(target.into()); + } + // TODO handle appending to built-in (it's Complicated) + let new_target = Target { + name: target.into(), + prerequisites: vec![], + commands: vec![], + stem: None, + already_updated: Cell::new(false), + macros: macro_set.clone(), }; self.targets.put(new_target); } @@ -794,7 +905,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { /// If successful, returns the name of the macro which was read. fn read_macro(&mut self, mut line_tokens: TokenString, line_number: usize) -> Result<String> { - let (name, mut value) = if cfg!(feature = "full") && line_tokens.starts_with("define ") { + let (name, value) = if cfg!(feature = "full") && line_tokens.starts_with("define ") { line_tokens.strip_prefix("define "); if line_tokens.ends_with("=") { line_tokens.strip_suffix("="); @@ -822,10 +933,36 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { .split_once("=") .ok_or_else(|| eyre!("read_macro couldn't find a '=' on line {}", line_number))? }; + let macro_assignment = self.parse_macro_assignment(name, value, line_number)?; + let macro_name = macro_assignment.name.clone(); + if let Some(outcome) = self + .check_macro_assignment_outcome(¯o_assignment, self.stack.with_scope(&self.macros)) + { + let (name, value) = self.resolve_macro_value(macro_assignment, outcome, line_number)?; + log::trace!( + "{}:{}: setting macro {} to {:?}", + &self.file_name, + line_number, + &name, + &value + ); + self.macros.set(name, value); + } + Ok(macro_name) + } + + fn parse_macro_assignment( + &mut self, + name: TokenString, + mut value: TokenString, + line_number: usize, + ) -> Result<MacroAssignment> { let name = self.expand_macros(&name)?; - // GNUisms are annoying, but popular + #[cfg(feature = "full")] let mut expand_value = false; + #[cfg(feature = "full")] let mut skip_if_defined = false; + #[cfg(feature = "full")] let mut append = false; #[cfg(feature = "full")] @@ -848,6 +985,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { let name = name.trim(); value.trim_start(); + #[cfg(feature = "full")] let value = if expand_value { TokenString::text( self.expand_macros(&value) @@ -857,73 +995,124 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { value }; - let skipped = match self.macros.get(name) { + Ok(MacroAssignment { + name: name.to_owned(), + value, + #[cfg(feature = "full")] + expand_value, + #[cfg(feature = "full")] + skip_if_defined, + #[cfg(feature = "full")] + append, + }) + } + + /// For aliasing reasons, applying a macro assignment is done in three steps: + /// 1. Determine what the assignment will do, and if it will append, fetch and clone the original value. Reads both self and the stack. + /// 2. Resolve the new value of the macro, eagerly expanding if appending to an eagerly expanded macro. May write to self due to eval, and since the stack will include `&self.macros`, must be separate from 1. + /// 3. Actually perform the assignment in a [MacroSet]. Since the [MacroSet] may be `&self.macros`, must be separate from 1 and 2. + fn check_macro_assignment_outcome( + &self, + macro_assignment: &MacroAssignment, + stack: MacroScopeStack, + ) -> Option<MacroAssignmentOutcome> { + let skipped = match stack.get(¯o_assignment.name).map(|x| x.source.clone()) { // We always let command line or MAKEFLAGS macros override macros from the file. - Some(Macro { - source: ItemSource::CommandLineOrMakeflags, - .. - }) => true, + Some(ItemSource::CommandLineOrMakeflags) => true, // We let environment variables override macros from the file only if the command-line argument to do that was given - Some(Macro { - source: ItemSource::Environment, - .. - }) => self.args.environment_overrides, - Some(_) => skip_if_defined, + Some(ItemSource::Environment) => self.args.environment_overrides, + #[cfg(feature = "full")] + Some(_) => macro_assignment.skip_if_defined, + #[cfg(not(feature = "full"))] + Some(_) => false, None => false, }; if skipped { - return Ok(name.to_owned()); + None + } else { + Some(match stack.get(¯o_assignment.name) { + #[cfg(feature = "full")] + Some(old_value) if macro_assignment.append => { + MacroAssignmentOutcome::AppendedTo(old_value.into_owned()) + } + _ => MacroAssignmentOutcome::Set, + }) } + } - log::trace!( - "{}:{}: setting macro {} to {}", - &self.file_name, - line_number, - name, - &value - ); - - let value = match self.macros.pop(name) { - Some(mut old_value) if append => { + fn resolve_macro_value( + &mut self, + macro_assignment: MacroAssignment, + outcome: MacroAssignmentOutcome, + line_number: usize, + ) -> Result<(String, Macro)> { + match outcome { + MacroAssignmentOutcome::AppendedTo(mut old_value) => { + let value = macro_assignment.value; #[cfg(feature = "full")] let value = if old_value.eagerly_expanded { TokenString::text(self.expand_macros(&value).wrap_err_with(|| { - format!("while defining {} on line {}", name, line_number) + format!( + "while defining {} on line {}", + macro_assignment.name, line_number + ) })?) } else { value }; old_value.text.extend(TokenString::text(" ")); old_value.text.extend(value); - old_value + Ok((macro_assignment.name, old_value)) } - _ => Macro { - source: ItemSource::File { - name: self.file_name.clone(), - line: line_number, + MacroAssignmentOutcome::Set => Ok(( + macro_assignment.name, + Macro { + source: ItemSource::File { + name: self.file_name.clone(), + line: line_number, + }, + text: macro_assignment.value, + #[cfg(feature = "full")] + eagerly_expanded: macro_assignment.expand_value, }, - text: value, - #[cfg(feature = "full")] - eagerly_expanded: expand_value, - }, - }; - self.macros.set(name.into(), value); + )), + } + } - Ok(name.to_owned()) + fn expand_macros(&mut self, text: &TokenString) -> Result<String> { + #[cfg(feature = "full")] + let mut deferred_eval_context = DeferredEvalContext::new(self); + let result = self.expand_macros_deferred_eval( + text, + #[cfg(feature = "full")] + &mut deferred_eval_context, + ); + #[cfg(feature = "full")] + for child in deferred_eval_context { + self.extend(child); + } + result } - fn expand_macros(&self, text: &TokenString) -> Result<String> { - self.macros - .expand(text) + fn expand_macros_deferred_eval( + &self, + text: &TokenString, + #[cfg(feature = "full")] deferred_eval_context: &mut DeferredEvalContext<R>, + ) -> Result<String> { + self.stack + .with_scope(&self.macros) + .expand( + text, + #[cfg(feature = "full")] + Some(deferred_eval_context), + ) .wrap_err_with(|| format!("while expanding \"{}\"", text)) } pub fn finish(self) -> FinishedMakefileReader { FinishedMakefileReader { inference_rules: self.inference_rules, - macros: self.macros.data, - #[cfg(feature = "full")] - macro_exports: self.macros.exported, + macros: self.macros, targets: self.targets.into(), first_non_special_target: self.first_non_special_target, failed_includes: self.failed_includes, @@ -932,11 +1121,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { fn extend(&mut self, new: FinishedMakefileReader) { self.inference_rules.extend(new.inference_rules); - self.macros.extend( - new.macros, - #[cfg(feature = "full")] - new.macro_exports, - ); + self.macros.extend(new.macros); for (_, target) in new.targets { self.targets.put(target); } @@ -948,10 +1133,8 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { } pub struct FinishedMakefileReader { - pub inference_rules: Vec<InferenceRule>, - pub macros: HashMap<String, Macro>, - #[cfg(feature = "full")] - pub macro_exports: ExportConfig, + pub inference_rules: InferenceRuleSet, + pub macros: MacroSet, pub targets: HashMap<String, Target>, pub first_non_special_target: Option<String>, pub failed_includes: Vec<String>, @@ -959,7 +1142,10 @@ pub struct FinishedMakefileReader { #[cfg(test)] mod test { + use std::io::Cursor; + use super::*; + use crate::makefile::token::Token; type R = Result<()>; @@ -977,6 +1163,7 @@ a: $(x) b \\ let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1003,8 +1190,9 @@ worked = perhaps endif "; let args = Args::empty(); - let makefile = MakefileReader::read( + let mut makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1029,6 +1217,7 @@ endif let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1049,8 +1238,9 @@ baz endef "; let args = Args::empty(); - let makefile = MakefileReader::read( + let mut makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1077,8 +1267,9 @@ endif FOO = bar "; let args = Args::empty(); - let makefile = MakefileReader::read( + let mut makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1127,6 +1318,7 @@ clean: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1148,6 +1340,7 @@ info: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1161,7 +1354,8 @@ info: prerequisites: vec!["bar".to_owned(), "baz".to_owned()], commands: vec![], stem: None, - already_updated: Cell::new(false) + already_updated: Cell::new(false), + macros: MacroSet::new(), } ); assert_eq!( @@ -1171,7 +1365,8 @@ info: prerequisites: vec!["test#post".to_owned()], commands: vec![], stem: None, - already_updated: Cell::new(false) + already_updated: Cell::new(false), + macros: MacroSet::new(), } ); assert_eq!( @@ -1181,7 +1376,8 @@ info: prerequisites: vec![], commands: vec![CommandLine::from(TokenString::text("hello # there")),], stem: None, - already_updated: Cell::new(false) + already_updated: Cell::new(false), + macros: MacroSet::new(), } ); @@ -1198,6 +1394,7 @@ cursed: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1222,6 +1419,7 @@ cursed: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1243,6 +1441,7 @@ test: c let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1263,6 +1462,7 @@ test: c let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1270,11 +1470,42 @@ test: c )?; let makefile = makefile.finish(); assert_eq!( - makefile.macros.get("x").map(|x| &x.text), + makefile.macros.get_non_recursive("x").map(|x| &x.text), Some(&TokenString::text("3")) ); assert!( - matches!(makefile.macro_exports, ExportConfig::Only(exported) if exported.contains("x")) + matches!(makefile.macros.exported, ExportConfig::Only(exported) if exported.contains("x")) + ); + Ok(()) + } + + #[cfg(feature = "full")] + #[test] + fn shell_comment() -> R { + let file = r#" +FOO=$(shell \ +echo \ +#abc) + "#; + let args = Args::empty(); + let makefile = MakefileReader::read( + &args, + MacroScopeStack::default(), + MacroSet::new(), + Cursor::new(file), + "", + Default::default(), + )?; + let makefile = makefile.finish(); + assert_eq!( + makefile.macros.get_non_recursive("FOO").map(|x| &x.text), + Some(&TokenString::from(vec![ + Token::Text(String::new()), + Token::FunctionCall { + name: TokenString::text("shell"), + args: vec![TokenString::text("echo #abc")], + }, + ])) ); Ok(()) } diff --git a/src/makefile/lookup_internal.rs b/src/makefile/lookup_internal.rs new file mode 100644 index 0000000..a497ab4 --- /dev/null +++ b/src/makefile/lookup_internal.rs @@ -0,0 +1,164 @@ +use eyre::{bail, eyre, Result}; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; + +use super::target::Target; + +#[derive(Clone)] +pub enum LookupInternal<'a> { + Partial { + targets: &'a Vec<&'a str>, + }, + Complete { + target: Option<&'a Target>, + get_target: &'a dyn Fn(&str) -> Result<Rc<RefCell<Target>>>, + }, +} + +impl<'a> LookupInternal<'a> { + pub const fn new_partial(targets: &'a Vec<&str>) -> Self { + Self::Partial { targets } + } + + pub const fn new( + target: Option<&'a Target>, + get_target: &'a dyn Fn(&str) -> Result<Rc<RefCell<Target>>>, + ) -> Self { + Self::Complete { target, get_target } + } + + pub fn lookup(&self, macro_name: &str) -> Result<String> { + let macro_pieces = match macro_name.chars().next() { + Some('@') => self.target_name()?, + Some('?') => self.newer_prerequisites()?, + Some('<') => self.inference_prerequisite()?, + Some('*') => self.target_stem()?, + #[cfg(feature = "full")] + Some('^') => self.all_prerequisites()?, + _ => bail!("unknown internal macro {}", macro_name), + }; + + let macro_pieces = if macro_name.ends_with('D') { + macro_pieces + .into_iter() + .map(|x| { + Path::new(&x) + .parent() + .ok_or_else(|| eyre!("no parent")) + .map(|x| x.to_string_lossy().into()) + }) + .collect::<Result<_, _>>()? + } else if macro_name.ends_with('F') { + macro_pieces + .into_iter() + .map(|x| { + Path::new(&x) + .file_name() + .ok_or_else(|| eyre!("no filename")) + .map(|x| x.to_string_lossy().into()) + }) + .collect::<Result<_, _>>()? + } else { + macro_pieces + }; + + Ok(macro_pieces.join(" ")) + } + + /// POSIX: The $@ shall evaluate to the full target name of the current target. + fn target_name(&self) -> Result<Vec<String>> { + match self { + Self::Partial { targets } => { + Ok(targets.iter().map(|target| target.to_string()).collect()) + } + Self::Complete { + target: Some(target), + .. + } => Ok(vec![target.name.clone()]), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// POSIX: The $? macro shall evaluate to the list of prerequisites that are newer than the current target. + fn newer_prerequisites(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $? when target not defined"), + Self::Complete { + target: Some(target), + get_target, + } => Ok(target + .prerequisites + .iter() + .filter(|prereq| { + get_target(prereq) + .ok() + .and_then(|prereq| prereq.borrow().newer_than(target)) + .unwrap_or(false) + }) + .cloned() + .collect()), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// POSIX: In an inference rule, the $< macro shall evaluate to the filename whose existence allowed the inference rule to be chosen for the target. In the .DEFAULT rule, the $< macro shall evaluate to the current target name. + /// + /// GNU: The name of the first prerequisite. + fn inference_prerequisite(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $< when target not defined"), + Self::Complete { + target: Some(target), + .. + } => { + // TODO check that exists_but_inferring_anyway won’t break this + Ok(vec![target + .prerequisites + .first() + .cloned() + .unwrap_or_default()]) + } + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// POSIX: The $* macro shall evaluate to the current target name with its suffix deleted. + fn target_stem(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $* when target not defined"), + Self::Complete { + target: Some(target), + .. + } => Ok(vec![target + .stem + .as_ref() + .unwrap_or(&target.name) + .to_owned()]), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// GNU: The names of all the prerequisites. + #[cfg(feature = "full")] + fn all_prerequisites(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $^ when target not defined"), + Self::Complete { + target: Some(target), + .. + } => Ok(target.prerequisites.clone()), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } +} diff --git a/src/makefile/macro.rs b/src/makefile/macro.rs index d77557b..d27cbfa 100644 --- a/src/makefile/macro.rs +++ b/src/makefile/macro.rs @@ -1,20 +1,20 @@ -use std::cell::RefCell; use std::collections::HashMap; +#[cfg(feature = "full")] use std::collections::HashSet; use std::env; use std::fmt; -use std::rc::Rc; - -use eyre::{bail, Result, WrapErr}; -#[cfg(not(feature = "full"))] -use regex::Regex; +#[cfg(feature = "full")] +use std::io::BufRead; #[cfg(feature = "full")] -use super::functions; -use super::token::{Token, TokenString}; -use super::ItemSource; +use super::eval_context::DeferredEvalContext; +#[cfg(feature = "full")] +use super::MacroScopeStack; +use super::{ItemSource, TokenString}; +#[cfg(feature = "full")] +use eyre::Result; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Macro { pub source: ItemSource, pub text: TokenString, @@ -22,12 +22,8 @@ pub struct Macro { pub eagerly_expanded: bool, } -pub trait LookupInternal: for<'a> Fn(&'a str) -> Result<String> {} - -impl<F: for<'a> Fn(&'a str) -> Result<String>> LookupInternal for F {} - #[cfg(feature = "full")] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ExportConfig { Only(HashSet<String>), AllBut(HashSet<String>), @@ -69,13 +65,6 @@ impl ExportConfig { } } - fn same_type(&self) -> Self { - match self { - Self::Only(_) => Self::only(), - Self::AllBut(_) => Self::all_but(), - } - } - fn should_export(&self, x: &str) -> bool { match self { Self::Only(exported) => exported.contains(x), @@ -84,29 +73,19 @@ impl ExportConfig { } } -#[derive(Clone)] -pub struct Set<'parent, 'lookup> { - parent: Option<&'parent Set<'parent, 'lookup>>, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Set { pub data: HashMap<String, Macro>, - lookup_internal: Option<&'lookup dyn LookupInternal>, - #[cfg(feature = "full")] - pub to_eval: Rc<RefCell<Vec<String>>>, #[cfg(feature = "full")] pub exported: ExportConfig, - warnings: Rc<RefCell<HashSet<String>>>, } -impl<'parent, 'lookup> Set<'parent, 'lookup> { +impl Set { pub fn new() -> Self { Self { - parent: None, data: HashMap::new(), - lookup_internal: None, - #[cfg(feature = "full")] - to_eval: Rc::new(RefCell::new(Vec::new())), #[cfg(feature = "full")] exported: ExportConfig::only(), - warnings: Default::default(), } } @@ -140,49 +119,18 @@ impl<'parent, 'lookup> Set<'parent, 'lookup> { } } - fn lookup_internal(&self, name: &str) -> Result<String> { - if let Some(lookup) = self.lookup_internal { - lookup(name) - } else if let Some(parent) = self.parent { - parent.lookup_internal(name) - } else { - bail!( - "tried to lookup {:?} but no lookup function is available", - name - ) - } - } - - pub fn get(&self, name: &str) -> Option<&Macro> { - self.data - .get(name) - .or_else(|| self.parent.and_then(|parent| parent.get(name))) + /// To properly process inherited macros, use [MacroScopeStack::get]. + pub fn get_non_recursive(&self, name: &str) -> Option<&Macro> { + self.data.get(name) } pub fn set(&mut self, name: String, r#macro: Macro) { self.data.insert(name, r#macro); } - #[cfg(feature = "full")] - pub fn is_defined(&self, name: &str) -> bool { - self.get(name).map_or(false, |x| !x.text.is_empty()) - } - - // `remove` is fine, but I think for "remove-and-return" `pop` is better - pub fn pop(&mut self, name: &str) -> Option<Macro> { - // TODO figure out a better way to handle inheritance - self.data - .remove(name) - .or_else(|| self.parent.and_then(|p| p.get(name).cloned())) - } - - pub fn extend( - &mut self, - other: HashMap<String, Macro>, - #[cfg(feature = "full")] other_exports: ExportConfig, - ) { + pub fn extend(&mut self, other: Self) { #[cfg(feature = "full")] - match (&mut self.exported, other_exports) { + match (&mut self.exported, other.exported) { (ExportConfig::Only(se), ExportConfig::Only(oe)) => { se.extend(oe); } @@ -190,187 +138,47 @@ impl<'parent, 'lookup> Set<'parent, 'lookup> { sne.extend(one); } (ExportConfig::Only(se), ExportConfig::AllBut(one)) => { - se.extend(other.keys().cloned().filter(|name| !one.contains(name))); + se.extend( + other + .data + .keys() + .filter(|name| !one.contains(*name)) + .cloned(), + ); } (ExportConfig::AllBut(sne), ExportConfig::Only(oe)) => { - sne.extend(other.keys().cloned().filter(|name| !oe.contains(name))); - } - } - self.data.extend(other); - } - - fn warn(&self, text: String) { - if !self.warnings.borrow().contains(&text) { - log::warn!("{}", &text); - self.warnings.borrow_mut().insert(text); - } - } - - pub fn expand(&self, text: &TokenString) -> Result<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 name = self - .expand(name) - .wrap_err_with(|| format!("while expanding \"{}\"", name))?; - let internal_macro_names = &['@', '?', '<', '*', '^'][..]; - let internal_macro_suffices = &['D', 'F'][..]; - let just_internal = name.len() == 1 && name.starts_with(internal_macro_names); - let suffixed_internal = name.len() == 2 - && name.starts_with(internal_macro_names) - && name.ends_with(internal_macro_suffices); - let macro_value = if just_internal || suffixed_internal { - self.lookup_internal(&name) - .wrap_err_with(|| format!("while expanding $\"{}\"", name))? - } else { - self.get(&name).map_or_else( - || { - self.warn(format!("undefined macro {}", name)); - Ok(String::new()) - }, - |x| { - self.expand(&x.text) - .wrap_err_with(|| format!("while expanding \"{}\"", &x.text)) - }, - )? - }; - let macro_value = match replacement { - Some((subst1, subst2)) => { - let subst1 = self.expand(subst1)?; - #[cfg(feature = "full")] - { - let (subst1, subst2) = if subst1.contains('%') { - (subst1, subst2.clone()) - } else { - let mut real_subst2 = TokenString::text("%"); - real_subst2.extend(subst2.clone()); - (format!("%{}", subst1), real_subst2) - }; - let args = [ - TokenString::text(subst1), - subst2, - TokenString::text(macro_value), - ]; - functions::expand_call( - "patsubst", - &args, - self, - Some(Rc::clone(&self.to_eval)), - )? - } - #[cfg(not(feature = "full"))] - { - let subst1_suffix = regex::escape(&subst1); - let subst1_suffix = - Regex::new(&format!(r"{}(\s|$)", subst1_suffix)) - .context("formed invalid regex somehow")?; - let subst2 = self.expand(subst2)?; - subst1_suffix - .replace_all(¯o_value, |c: ®ex::Captures| { - format!("{}{}", subst2, c.get(1).unwrap().as_str()) - }) - .to_string() - } - } - None => macro_value, - }; - log::trace!( - "expanded {} (from {:?}) into \"{}\"", - token, - self.get(&name).map(|x| &x.source), - ¯o_value - ); - result.push_str(¯o_value); - } - #[cfg(feature = "full")] - Token::FunctionCall { name, args } => { - let name = self.expand(name)?; - let fn_result = - functions::expand_call(&name, args, self, Some(Rc::clone(&self.to_eval)))?; - log::trace!("expanded {} into \"{}\"", token, &fn_result); - result.push_str(&fn_result); - } + sne.extend( + other + .data + .keys() + .filter(|name| !oe.contains(*name)) + .cloned(), + ); } } - Ok(result) + self.data.extend(other.data); } #[cfg(feature = "full")] - pub fn origin(&self, name: &str) -> &'static str { - match self.data.get(name) { - None => self.parent.map_or("undefined", |p| p.origin(name)), - Some(Macro { - source: ItemSource::Builtin, - .. - }) => "default", - Some(Macro { - source: ItemSource::Environment, - .. - }) => "environment", - // TODO figure out when to return "environment override" - Some(Macro { - source: ItemSource::File { .. }, - .. - }) => "file", - Some(Macro { - source: ItemSource::CommandLineOrMakeflags, - .. - }) => "command line", - // TODO handle override - Some(Macro { - source: ItemSource::FunctionCall, - .. - }) => "automatic", - } - } - - pub fn with_lookup<'l, 's: 'l>(&'s self, lookup: &'l dyn LookupInternal) -> Set<'s, 'l> { - Set { - parent: Some(self), - data: HashMap::new(), - lookup_internal: Some(lookup), - #[cfg(feature = "full")] - to_eval: Rc::clone(&self.to_eval), - #[cfg(feature = "full")] - exported: self.exported.same_type(), - warnings: Rc::clone(&self.warnings), - } - } - - pub fn with_overlay<'s>(&'s self) -> Set<'s, 'lookup> { - Set { - parent: Some(self), - data: HashMap::new(), - lookup_internal: None, - #[cfg(feature = "full")] - to_eval: Rc::clone(&self.to_eval), - #[cfg(feature = "full")] - exported: self.exported.same_type(), - warnings: Rc::clone(&self.warnings), - } - } - - #[cfg(feature = "full")] - pub fn resolve_exports(&self) -> Result<Vec<(&str, String)>> { + pub fn resolve_exports<R: BufRead>( + &self, + mut eval_context: Option<&mut DeferredEvalContext<R>>, + ) -> Result<Vec<(&str, String)>> { let own_exports = self .data .iter() .filter(|(name, _)| self.exported.should_export(name)) - .map(|(name, value)| self.expand(&value.text).map(|text| (name.as_ref(), text))) + .map(|(name, value)| { + MacroScopeStack::from_scope(self) + .expand(&value.text, eval_context.as_deref_mut()) + .map(|text| (name.as_ref(), text)) + }) .collect::<Result<Vec<_>>>()?; - Ok(if let Some(parent) = self.parent { - let mut parent_exports = parent.resolve_exports()?; - parent_exports.extend(own_exports); - parent_exports - } else { - own_exports - }) + Ok(own_exports) } } -impl fmt::Display for Set<'_, '_> { +impl fmt::Display for Set { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let pieces = self .data @@ -381,9 +189,23 @@ impl fmt::Display for Set<'_, '_> { } } +impl Default for Set { + fn default() -> Self { + Self::new() + } +} + fn builtins() -> Vec<(&'static str, TokenString)> { // Fuck it, might as well. - macro_rules! handle { + macro_rules! handle_key { + ($key:ident) => { + stringify!($key) + }; + ($key:literal) => { + $key + }; + } + macro_rules! handle_value { ($value:ident) => { stringify!($value).parse().unwrap() }; @@ -395,8 +217,8 @@ fn builtins() -> Vec<(&'static str, TokenString)> { }; } macro_rules! make { - ($($name:ident=$value:tt)+) => {vec![$( - (stringify!($name), handle!($value)) + ($($name:tt=$value:tt)+) => {vec![$( + (handle_key!($name), handle_value!($value)) ),+]}; } @@ -429,12 +251,19 @@ fn builtins() -> Vec<(&'static str, TokenString)> { ARFLAGS="rv" CFLAGS="" FFLAGS="" + + // yes, Linux, this is definitely GNU Make 4.0+ + ".FEATURES"="output-sync" ] } #[cfg(test)] mod test { use super::*; + #[cfg(feature = "full")] + use crate::makefile::functions::NO_EVAL; + use crate::MacroScopeStack; + use eyre::Result; type R = Result<()>; @@ -450,7 +279,14 @@ mod test { eagerly_expanded: false, }, ); - assert_eq!(macros.expand(&"$(oof:;=?)".parse()?)?, "bruh? swag? yeet?"); + assert_eq!( + MacroScopeStack::from_scope(¯os).expand( + &"$(oof:;=?)".parse()?, + #[cfg(feature = "full")] + NO_EVAL + )?, + "bruh? swag? yeet?" + ); Ok(()) } @@ -466,7 +302,10 @@ mod test { eagerly_expanded: false, }, ); - assert_eq!(macros.expand(&"$(m:%=%-objs)".parse()?)?, "conf-objs"); + assert_eq!( + MacroScopeStack::from_scope(¯os).expand(&"$(m:%=%-objs)".parse()?, NO_EVAL)?, + "conf-objs" + ); Ok(()) } } diff --git a/src/makefile/macro_scope.rs b/src/makefile/macro_scope.rs new file mode 100644 index 0000000..c03870f --- /dev/null +++ b/src/makefile/macro_scope.rs @@ -0,0 +1,229 @@ +use std::borrow::Cow; +use std::collections::HashSet; +#[cfg(feature = "full")] +use std::io::BufRead; +use std::iter; +use std::sync::RwLock; + +use eyre::Context; +use lazy_static::lazy_static; +#[cfg(not(feature = "full"))] +use regex::Regex; + +#[cfg(feature = "full")] +use super::eval_context::DeferredEvalContext; +#[cfg(feature = "full")] +use super::functions; +use super::token::Token; +use super::{ItemSource, LookupInternal, Macro, MacroSet, TokenString}; + +pub trait MacroScope { + /// Looks up the macro with the given name and returns it if it exists. + /// + /// Uses [Cow] to allow for lazy macro definitions. + fn get(&self, name: &str) -> Option<Cow<Macro>>; +} + +impl MacroScope for MacroSet { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.get_non_recursive(name).map(Cow::Borrowed) + } +} + +impl<'a> MacroScope for LookupInternal<'a> { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.lookup(name).ok().map(|value| { + Cow::Owned(Macro { + source: ItemSource::Builtin, + text: TokenString::text(value), + #[cfg(feature = "full")] + eagerly_expanded: false, + }) + }) + } +} + +impl<T: MacroScope> MacroScope for Option<&T> { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.as_ref().and_then(|value| value.get(name)) + } +} + +// warning on undefined macros is useful but can get repetitive fast +lazy_static! { + static ref WARNINGS_EMITTED: RwLock<HashSet<String>> = Default::default(); +} + +fn warn(text: String) { + let already_warned = WARNINGS_EMITTED + .read() + .map_or(true, |warnings| warnings.contains(&text)); + if !already_warned { + log::warn!("{}", &text); + if let Ok(mut warnings) = WARNINGS_EMITTED.write() { + warnings.insert(text); + } + } +} + +#[derive(Default)] +pub struct MacroScopeStack<'a> { + scopes: Vec<&'a dyn MacroScope>, +} + +impl<'a> MacroScopeStack<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn from_scope(scope: &'a dyn MacroScope) -> Self { + Self { + scopes: vec![scope], + } + } + + pub fn with_scope(&self, new_scope: &'a dyn MacroScope) -> Self { + Self { + scopes: iter::once(new_scope).chain(self.scopes.clone()).collect(), + } + } + + pub fn get(&self, name: &str) -> Option<Cow<Macro>> { + for scope in &self.scopes { + if let Some(r#macro) = scope.get(name) { + return Some(r#macro); + } + } + None + } + + #[cfg(feature = "full")] + pub fn is_defined(&self, name: &str) -> bool { + self.get(name).map_or(false, |x| !x.text.is_empty()) + } + + pub fn expand<#[cfg(feature = "full")] R: BufRead>( + &self, + text: &TokenString, + #[cfg(feature = "full")] mut eval_context: Option<&mut DeferredEvalContext<R>>, + ) -> eyre::Result<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 name = self + .expand( + name, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + ) + .wrap_err_with(|| format!("while expanding \"{}\"", name))?; + let macro_value = self.get(&name).map_or_else( + || { + warn(format!("undefined macro {}", name)); + Ok(String::new()) + }, + |x| { + self.expand( + &x.text, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + ) + .wrap_err_with(|| format!("while expanding \"{}\"", &x.text)) + }, + )?; + let macro_value = match replacement { + Some((subst1, subst2)) => { + let subst1 = self.expand( + subst1, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + )?; + #[cfg(feature = "full")] + { + let (subst1, subst2) = if subst1.contains('%') { + (subst1, subst2.clone()) + } else { + let mut real_subst2 = TokenString::text("%"); + real_subst2.extend(subst2.clone()); + (format!("%{}", subst1), real_subst2) + }; + let args = [ + TokenString::text(subst1), + subst2, + TokenString::text(macro_value), + ]; + functions::expand_call( + "patsubst", + &args, + self, + eval_context.as_deref_mut(), + )? + } + #[cfg(not(feature = "full"))] + { + let subst1_suffix = regex::escape(&subst1); + let subst1_suffix = + Regex::new(&format!(r"{}(\s|$)", subst1_suffix)) + .context("formed invalid regex somehow")?; + let subst2 = self.expand(subst2)?; + subst1_suffix + .replace_all(¯o_value, |c: ®ex::Captures| { + format!("{}{}", subst2, c.get(1).unwrap().as_str()) + }) + .to_string() + } + } + None => macro_value, + }; + log::trace!( + "expanded {} (from {:?}) into \"{}\"", + token, + self.get(&name).map(|x| x.source.clone()), + ¯o_value + ); + result.push_str(¯o_value); + } + #[cfg(feature = "full")] + Token::FunctionCall { name, args } => { + let name = self.expand(name, eval_context.as_deref_mut())?; + let fn_result = + functions::expand_call(&name, args, self, eval_context.as_deref_mut())?; + log::trace!("expanded {} into \"{}\"", token, &fn_result); + result.push_str(&fn_result); + } + } + } + Ok(result) + } + + #[cfg(feature = "full")] + pub fn origin(&self, name: &str) -> &'static str { + match self.get(name).as_deref() { + None => "undefined", + Some(Macro { + source: ItemSource::Builtin, + .. + }) => "default", + Some(Macro { + source: ItemSource::Environment, + .. + }) => "environment", + // TODO figure out when to return "environment override" + Some(Macro { + source: ItemSource::File { .. }, + .. + }) => "file", + Some(Macro { + source: ItemSource::CommandLineOrMakeflags, + .. + }) => "command line", + // TODO handle override + Some(Macro { + source: ItemSource::FunctionCall, + .. + }) => "automatic", + } + } +} diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs index 277fbd3..d746ed1 100644 --- a/src/makefile/mod.rs +++ b/src/makefile/mod.rs @@ -8,10 +8,15 @@ use std::rc::Rc; use eyre::{bail, eyre, Result, WrapErr}; use command_line::CommandLine; -use inference_rules::InferenceRule; +#[cfg(feature = "full")] +use functions::NO_EVAL; +use inference_rules::{InferenceRule, InferenceRuleSet}; use input::FinishedMakefileReader; pub use input::MakefileReader; -use r#macro::{Macro, Set as MacroSet}; +use lookup_internal::LookupInternal; +pub use macro_scope::MacroScopeStack; +use r#macro::Macro; +pub use r#macro::Set as MacroSet; use target::{DynamicTargetSet, Target}; use token::TokenString; @@ -21,27 +26,36 @@ mod command_line; #[cfg(feature = "full")] mod conditional; #[cfg(feature = "full")] +mod eval_context; +#[cfg(feature = "full")] mod functions; mod inference_rules; mod input; +mod lookup_internal; mod r#macro; +mod macro_scope; +mod parse; mod pattern; mod target; mod token; #[derive(Debug, Clone, Eq, PartialEq)] pub enum ItemSource { - File { name: String, line: usize }, + File { + name: String, + line: usize, + }, CommandLineOrMakeflags, Environment, Builtin, + #[cfg(feature = "full")] FunctionCall, } pub struct Makefile<'a> { - inference_rules: Vec<InferenceRule>, + inference_rules: InferenceRuleSet, builtin_inference_rules: Vec<InferenceRule>, - pub macros: MacroSet<'static, 'static>, + pub macros: MacroSet, targets: DynamicTargetSet, pub first_non_special_target: Option<String>, args: &'a Args, @@ -60,7 +74,7 @@ impl<'a> Makefile<'a> { "MAKE".to_owned(), Macro { source: ItemSource::Builtin, - text: std::env::current_exe().map_or_else( + text: env::current_exe().map_or_else( |_| TokenString::text("makers"), |x| TokenString::text(x.to_string_lossy()), ), @@ -68,6 +82,15 @@ impl<'a> Makefile<'a> { eagerly_expanded: false, }, ); + macros.set( + "MAKEFLAGS".to_owned(), + Macro { + source: ItemSource::Builtin, + text: TokenString::text(args.makeflags()), + #[cfg(feature = "full")] + eagerly_expanded: false, + }, + ); if !args.no_builtin_rules { inference_rules.extend(builtin_inference_rules()); macros.add_builtins(); @@ -119,7 +142,7 @@ impl<'a> Makefile<'a> { } Makefile { - inference_rules: vec![], + inference_rules: InferenceRuleSet::default(), builtin_inference_rules: inference_rules, macros, targets, @@ -131,11 +154,7 @@ impl<'a> Makefile<'a> { pub fn extend(&mut self, new: FinishedMakefileReader) -> Result<()> { self.inference_rules.extend(new.inference_rules); - self.macros.extend( - new.macros, - #[cfg(feature = "full")] - new.macro_exports, - ); + self.macros.extend(new.macros); for (_, target) in new.targets { self.targets.put(target); } @@ -147,9 +166,9 @@ impl<'a> Makefile<'a> { self.update_target(&failed_include).wrap_err_with(|| { format!("while building missing included file {}", &failed_include) })?; - let macros = self.macros.with_overlay(); + let stack = MacroScopeStack::default().with_scope(&self.macros); let file = - MakefileReader::read_file(self.args, macros, failed_include, Default::default())? + MakefileReader::read_file(self.args, stack, failed_include, Default::default())? .finish(); self.extend(file)?; } @@ -157,13 +176,10 @@ impl<'a> Makefile<'a> { } fn special_target_has_prereq(&self, target: &str, name: &str) -> bool { - match self.targets.get(target) { - Some(target) => { - let target = target.borrow(); - target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name) - } - None => false, - } + self.targets.get(target).map_or(false, |target| { + let target = target.borrow(); + target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name) + }) } fn infer_target( @@ -184,7 +200,7 @@ impl<'a> Makefile<'a> { let follow_gnu = cfg!(feature = "full"); - let vpath_options = match self.macros.get("VPATH") { + let vpath_options = match self.macros.get_non_recursive("VPATH") { Some(Macro { text, .. }) if follow_gnu => { let vpath = self.expand_macros(text, None)?; env::split_paths(&vpath).collect() @@ -202,7 +218,12 @@ impl<'a> Makefile<'a> { .inference_rules .iter() .chain(self.builtin_inference_rules.iter()) - .filter(|rule| !banned_rules.contains(rule)) + .filter(|rule| { + !banned_rules.iter().any(|banned_rule| { + banned_rule.products == rule.products + && banned_rule.prerequisites == rule.prerequisites + }) + }) .filter(|rule| rule.matches(name).unwrap_or(false)); for rule in inference_rule_candidates { log::trace!( @@ -219,7 +240,10 @@ impl<'a> Makefile<'a> { // we can't build this based on itself! fuck outta here return None; } - if self.targets.has(&prereq_path_name) { + if self.targets.has(&prereq_path_name) + || self.special_target_has_prereq(".PHONY", &prereq_path_name) + { + // TODO consider only checking phony after transitive inference has failed return Some(prereq_path_name); } let prereq_path = PathBuf::from(&prereq_path_name); @@ -261,6 +285,7 @@ impl<'a> Makefile<'a> { .first_match(name)? .and_then(|x| x.get(1).map(|x| x.as_str().to_owned())), already_updated: Cell::new(false), + macros: MacroSet::new(), }); break; } @@ -278,7 +303,11 @@ impl<'a> Makefile<'a> { let follow_gnu = cfg!(feature = "full"); #[cfg(feature = "full")] - let name = name.strip_prefix("./").unwrap_or(name); + if let Some(name_without_leading_dot_slash) = name.strip_prefix("./") { + if let Ok(result) = self.get_target(name_without_leading_dot_slash) { + return Ok(result); + } + } let exists_but_infer_anyway = if follow_gnu { self.targets @@ -303,6 +332,7 @@ impl<'a> Makefile<'a> { commands, stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }); } else { // if it already exists, it counts as up-to-date @@ -313,6 +343,7 @@ impl<'a> Makefile<'a> { commands: vec![], stem: None, already_updated: Cell::new(true), + macros: MacroSet::new(), }); } } @@ -322,10 +353,9 @@ impl<'a> Makefile<'a> { self.targets.put(new_target); } - Ok(self - .targets + self.targets .get(name) - .ok_or_else(|| eyre!("Target {:?} not found!", name))?) + .ok_or_else(|| eyre!("Target {:?} not found!", name)) } pub fn update_target(&self, name: &str) -> Result<()> { @@ -343,74 +373,15 @@ impl<'a> Makefile<'a> { } fn expand_macros(&self, text: &TokenString, target: Option<&Target>) -> Result<String> { - let target = target.cloned(); - let lookup_internal = move |macro_name: &str| { - let target = target - .as_ref() - .ok_or_else(|| eyre!("internal macro but no current target!"))?; - let macro_pieces = if macro_name.starts_with('@') { - // The $@ shall evaluate to the full target name of the - // current target. - vec![target.name.clone()] - } else if macro_name.starts_with('?') { - // The $? macro shall evaluate to the list of prerequisites - // that are newer than the current target. - target - .prerequisites - .iter() - .filter(|prereq| { - self.get_target(prereq) - .ok() - .and_then(|prereq| prereq.borrow().newer_than(target)) - .unwrap_or(false) - }) - .cloned() - .collect() - } else if macro_name.starts_with('<') { - // In an inference rule, the $< macro shall evaluate to the - // filename whose existence allowed the inference rule to be - // chosen for the target. In the .DEFAULT rule, the $< macro - // shall evaluate to the current target name. - // TODO make that actually be the case (rn exists_but_inferring_anyway might fuck that up) - vec![target.prerequisites.get(0).cloned().unwrap_or_default()] - } else if macro_name.starts_with('*') { - // The $* macro shall evaluate to the current target name with - // its suffix deleted. (GNUism: the match stem) - vec![target.stem.as_ref().unwrap_or(&target.name).to_owned()] - } else if macro_name.starts_with('^') { - target.prerequisites.clone() - } else { - unreachable!() - }; - - let macro_pieces = if macro_name.ends_with('D') { - macro_pieces - .into_iter() - .map(|x| { - Path::new(&x) - .parent() - .ok_or_else(|| eyre!("no parent")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<_, _>>()? - } else if macro_name.ends_with('F') { - macro_pieces - .into_iter() - .map(|x| { - Path::new(&x) - .file_name() - .ok_or_else(|| eyre!("no filename")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<_, _>>()? - } else { - macro_pieces - }; - - Ok(macro_pieces.join(" ")) - }; - - self.macros.with_lookup(&lookup_internal).expand(text) + MacroScopeStack::default() + .with_scope(&self.macros) + .with_scope(&LookupInternal::new(target, &|name| self.get_target(name))) + .with_scope(&target.map(|target| &target.macros)) + .expand( + text, + #[cfg(feature = "full")] + NO_EVAL, + ) } } @@ -460,6 +431,7 @@ fn builtin_inference_rules() -> Vec<InferenceRule> { prepend_dot!($($second)?).into(), concat!(".", stringify!($first)).into(), vec![$(CommandLine::from($cmd.parse().unwrap())),+], + MacroSet::new(), ) ),+] }; @@ -515,6 +487,7 @@ fn builtin_targets() -> Vec<Target> { commands: vec![], stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }] } @@ -533,9 +506,10 @@ mod test { products: vec!["this-is-a-%-case".to_owned()], prerequisites: vec![], commands: vec![], + macros: MacroSet::new(), }; let file = Makefile { - inference_rules: vec![rule], + inference_rules: vec![rule].into(), builtin_inference_rules: vec![], macros: MacroSet::new(), targets: Default::default(), @@ -559,6 +533,7 @@ mod test { commands: vec![], stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }; let phony = Target { name: ".PHONY".to_string(), @@ -566,13 +541,14 @@ mod test { commands: vec![], stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }; let targets = DynamicTargetSet::default(); targets.put(target); targets.put(phony); let file = Makefile { - inference_rules: vec![], + inference_rules: InferenceRuleSet::default(), builtin_inference_rules: vec![], macros: MacroSet::new(), targets, @@ -584,4 +560,42 @@ mod test { assert!(file.update_target("all").is_ok()); Ok(()) } + + #[cfg(feature = "full")] + #[test] + fn missing_phony_targets_allow_inference() -> R { + let args = Args::empty(); + let rule = InferenceRule { + source: ItemSource::CommandLineOrMakeflags, + products: vec!["%ll".to_owned()], + prerequisites: vec!["missing".to_owned()], + commands: vec![], + macros: MacroSet::new(), + }; + let phony = Target { + name: ".PHONY".to_string(), + prerequisites: vec!["missing".to_owned()], + commands: vec![], + stem: None, + already_updated: Cell::new(false), + macros: MacroSet::new(), + }; + + let mut inference_rules = InferenceRuleSet::default(); + inference_rules.put(rule); + let targets = DynamicTargetSet::default(); + targets.put(phony); + let file = Makefile { + inference_rules, + builtin_inference_rules: vec![], + macros: MacroSet::new(), + targets, + first_non_special_target: None, + args: &args, + already_inferred: Default::default(), + }; + + file.update_target("all")?; + Ok(()) + } } diff --git a/src/makefile/parse.rs b/src/makefile/parse.rs new file mode 100644 index 0000000..191b7e0 --- /dev/null +++ b/src/makefile/parse.rs @@ -0,0 +1,19 @@ +use super::Macro; +use super::TokenString; + +#[derive(Debug)] +pub struct MacroAssignment { + pub name: String, + pub value: TokenString, + #[cfg(feature = "full")] + pub expand_value: bool, + #[cfg(feature = "full")] + pub skip_if_defined: bool, + #[cfg(feature = "full")] + pub append: bool, +} + +pub enum MacroAssignmentOutcome { + Set, + AppendedTo(Macro), +} diff --git a/src/makefile/target.rs b/src/makefile/target.rs index c3431e4..9f81802 100644 --- a/src/makefile/target.rs +++ b/src/makefile/target.rs @@ -8,9 +8,7 @@ use std::time::SystemTime; use eyre::{Result, WrapErr}; -use crate::makefile::command_line::CommandLine; - -use super::Makefile; +use super::{CommandLine, MacroSet, Makefile}; #[derive(PartialEq, Eq, Clone, Debug)] pub struct Target { @@ -19,10 +17,11 @@ pub struct Target { pub commands: Vec<CommandLine>, pub stem: Option<String>, pub already_updated: Cell<bool>, + pub macros: MacroSet, } impl Target { - pub fn extend(&mut self, other: Target) { + pub fn extend(&mut self, other: Self) { assert_eq!(&self.name, &other.name); match (self.commands.is_empty(), other.commands.is_empty()) { (false, false) => { @@ -43,6 +42,7 @@ impl Target { self.stem = self.stem.take().or(other.stem); let already_updated = self.already_updated.get() || other.already_updated.get(); self.already_updated.set(already_updated); + self.macros.extend(other.macros); } } } @@ -152,11 +152,15 @@ impl StaticTargetSet { self.data.insert(target.name.clone(), target); } } + + pub fn has(&self, name: &str) -> bool { + self.data.contains_key(name) + } } -impl Into<HashMap<String, Target>> for StaticTargetSet { - fn into(self) -> HashMap<String, Target> { - self.data +impl From<StaticTargetSet> for HashMap<String, Target> { + fn from(value: StaticTargetSet) -> Self { + value.data } } @@ -168,7 +172,7 @@ pub struct DynamicTargetSet { impl DynamicTargetSet { pub fn get(&self, name: &str) -> Option<Rc<RefCell<Target>>> { - self.data.borrow().get(name).map(|x| Rc::clone(x)) + self.data.borrow().get(name).map(Rc::clone) } pub fn put(&self, target: Target) { diff --git a/src/makefile/token.rs b/src/makefile/token.rs index 2721387..8507a3a 100644 --- a/src/makefile/token.rs +++ b/src/makefile/token.rs @@ -17,6 +17,7 @@ use nom::{ character::complete::{space0, space1}, multi::separated_list1, }; +use regex::Regex; trait Err<'a>: 'a + ParseError<&'a str> + ContextError<&'a str> {} impl<'a, T: 'a + ParseError<&'a str> + ContextError<&'a str>> Err<'a> for T {} @@ -128,7 +129,7 @@ impl TokenString { } pub fn is_empty(&self) -> bool { - match self.0.get(0) { + match self.0.first() { None => true, Some(Token::Text(t)) if t.is_empty() && self.0.len() == 1 => true, _ => false, @@ -144,6 +145,29 @@ impl TokenString { } }) } + + pub fn matches_regex(&self, regex: &Regex) -> bool { + self.0.iter().any(|x| { + if let Token::Text(x) = x { + regex.is_match(x) + } else { + false + } + }) + } + + /// Returns (token index within string, pattern index within token). + pub fn find(&self, pattern: &str) -> Option<(usize, usize)> { + self.0 + .iter() + .enumerate() + .find_map(|(token_index, token)| match token { + Token::Text(text) => text + .find(pattern) + .map(|pattern_index| (token_index, pattern_index)), + _ => None, + }) + } } impl fmt::Display for TokenString { @@ -231,6 +255,13 @@ impl Delimiter { Self::Braces => "}", } } + + const fn end_char(&self) -> char { + match self { + Delimiter::Parens => ')', + Delimiter::Braces => '}', + } + } } fn macro_function_name<'a, E: Err<'a>>( @@ -341,13 +372,12 @@ fn text_but_not<'a, E: Err<'a>>( } fn nested_delimiters<'a, E: Err<'a>>( - ends: Vec<char>, context: Delimiter, ) -> impl FnMut(&'a str) -> IResult<&'a str, TokenString, E> { map( tuple(( tag(context.start()), - move |x| tokens_but_not(ends.clone(), context)(x), + move |x| tokens_but_not(vec![context.end_char()], context)(x), tag(context.end()), )), |(left, center, right)| { @@ -368,7 +398,7 @@ fn single_token_but_not<'a, E: Err<'a>>( alt(( text_but_not(tbn_ends), macro_expansion, - nested_delimiters(ends, context), + nested_delimiters(context), )) } @@ -386,7 +416,7 @@ fn empty_tokens<'a, E: Err<'a>>(input: &'a str) -> IResult<&'a str, TokenString, fn fold_tokens<'a, E: Err<'a>>( parser: impl FnMut(&'a str) -> IResult<&'a str, TokenString, E>, ) -> impl FnMut(&'a str) -> IResult<&'a str, TokenString, E> { - fold_many1(parser, TokenString::empty(), |mut acc, x| { + fold_many1(parser, TokenString::empty, |mut acc, x| { acc.extend(x); acc }) @@ -614,4 +644,28 @@ mod test { ); Ok(()) } + + #[cfg(feature = "full")] + #[test] + fn quoted_function_call_comma() -> R { + let text = "$(egg $$(bug a, b/c))"; + let tokens = tokenize(text)?; + + assert_eq!( + tokens, + TokenString::just(Token::FunctionCall { + name: TokenString::text("egg"), + args: vec![TokenString::text("$(bug a, b/c)")], + }) + ); + Ok(()) + } + + #[cfg(feature = "full")] + #[test] + fn unbalanced_parentheses_rejected() -> R { + let text = "$(egg ()"; + assert!(tokenize(text).is_err()); + Ok(()) + } } |