aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args.rs230
-rw-r--r--src/main.rs22
-rw-r--r--src/makefile/command_line.rs91
-rw-r--r--src/makefile/conditional.rs4
-rw-r--r--src/makefile/eval_context.rs53
-rw-r--r--src/makefile/functions.rs549
-rw-r--r--src/makefile/inference_rules.rs138
-rw-r--r--src/makefile/input.rs635
-rw-r--r--src/makefile/lookup_internal.rs164
-rw-r--r--src/makefile/macro.rs323
-rw-r--r--src/makefile/macro_scope.rs229
-rw-r--r--src/makefile/mod.rs210
-rw-r--r--src/makefile/parse.rs19
-rw-r--r--src/makefile/target.rs20
-rw-r--r--src/makefile/token.rs64
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(&macros);
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(&macros)
+ .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(&macros)
+ .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(&macro_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,
+ &macro_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(&macro_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(&macro_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(&macro_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(&macro_value, |c: &regex::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),
- &macro_value
- );
- result.push_str(&macro_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(&macros).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(&macros).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(&macro_value, |c: &regex::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()),
+ &macro_value
+ );
+ result.push_str(&macro_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(())
+ }
}