aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMelody Horn <melody@boringcactus.com>2021-03-28 14:57:03 -0600
committerMelody Horn <melody@boringcactus.com>2021-03-28 14:57:03 -0600
commitbc5038e6c344803bce76add47b13ceaa61a5bde3 (patch)
treeeec9a8fc36562aa4413cf8be8ef22910fdc75cac
parent82278c3a5aa93204c963a63cc3cfefea1d0fb3fd (diff)
downloadmakers-bc5038e6c344803bce76add47b13ceaa61a5bde3.tar.gz
makers-bc5038e6c344803bce76add47b13ceaa61a5bde3.zip
almost implement all functions
-rw-r--r--Cargo.lock216
-rw-r--r--Cargo.toml5
-rw-r--r--src/makefile/functions.rs285
-rw-r--r--src/makefile/macro.rs189
-rw-r--r--src/makefile/mod.rs306
-rw-r--r--src/makefile/pattern.rs49
-rw-r--r--src/makefile/token.rs7
7 files changed, 868 insertions, 189 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 05cc7ce..925d565 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -19,6 +19,12 @@ dependencies = [
]
[[package]]
+name = "arrayref"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544"
+
+[[package]]
name = "arrayvec"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -36,6 +42,18 @@ dependencies = [
]
[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "base64"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+
+[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -54,6 +72,17 @@ dependencies = [
]
[[package]]
+name = "blake2b_simd"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
+dependencies = [
+ "arrayref",
+ "arrayvec",
+ "constant_time_eq",
+]
+
+[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -75,12 +104,77 @@ dependencies = [
]
[[package]]
+name = "constant_time_eq"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "lazy_static",
+]
+
+[[package]]
+name = "dirs"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
name = "funty"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.10.2+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
+[[package]]
name = "heck"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -127,10 +221,13 @@ checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7"
name = "makers"
version = "0.1.0"
dependencies = [
+ "dirs",
+ "glob",
"lazy_static",
"nom",
"regex",
"structopt",
+ "tempfile",
]
[[package]]
@@ -153,6 +250,12 @@ dependencies = [
]
[[package]]
+name = "ppv-lite86"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
+
+[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -201,6 +304,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
+name = "rand"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+ "rand_hc",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
+dependencies = [
+ "getrandom 0.2.2",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
+dependencies = [
+ "getrandom 0.1.16",
+ "redox_syscall 0.1.57",
+ "rust-argon2",
+]
+
+[[package]]
name = "regex"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -218,6 +387,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548"
[[package]]
+name = "remove_dir_all"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "rust-argon2"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
+dependencies = [
+ "base64",
+ "blake2b_simd",
+ "constant_time_eq",
+ "crossbeam-utils",
+]
+
+[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -277,6 +467,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
+name = "tempfile"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rand",
+ "redox_syscall 0.2.5",
+ "remove_dir_all",
+ "winapi",
+]
+
+[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -316,6 +520,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.10.2+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
+
+[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index b385dff..1e91e78 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,10 +11,15 @@ keywords = ["build", "make"]
categories = ["development-tools"]
[dependencies]
+dirs = "3.0.1"
+glob = "0.3.0"
lazy_static = "1.4.0"
nom = "6.1.2"
regex = "1.4.5"
structopt = "0.3.21"
+[dev-dependencies]
+tempfile = "3.2.0"
+
# [target.'cfg(unix)'.dependencies]
# signal-hook = "0.3.7"
diff --git a/src/makefile/functions.rs b/src/makefile/functions.rs
index bc39886..b9a6a6d 100644
--- a/src/makefile/functions.rs
+++ b/src/makefile/functions.rs
@@ -1,26 +1,160 @@
+use std::env;
+use std::path::Path;
+
+use super::pattern::r#match;
+use super::r#macro::{MacroSet, MacroSource};
use super::token::TokenString;
-pub(crate) fn call(name: &str, args: &[TokenString]) -> TokenString {
+pub(crate) struct FunctionCall {}
+
+fn filter(macros: &MacroSet, patterns: &TokenString, text: &TokenString) -> String {
+ let patterns = macros.expand(patterns);
+ let patterns = patterns.split_whitespace().collect::<Vec<_>>();
+ let text = macros.expand(&text);
+ let text = text.split_whitespace();
+ let mut result_pieces = vec![];
+ for word in text {
+ if patterns
+ .iter()
+ .any(|pattern| r#match(pattern, word).is_some())
+ {
+ result_pieces.push(word);
+ }
+ }
+ result_pieces.join(" ")
+}
+
+pub(crate) fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> String {
match name {
// Text Functions
- "filter" => todo!(),
- "filter-out" => todo!(),
- "sort" => todo!(),
+ "filter" => {
+ assert_eq!(args.len(), 2);
+ filter(macros, &args[0], &args[1])
+ }
+ "filter-out" => {
+ assert_eq!(args.len(), 2);
+ let patterns = macros.expand(&args[0]);
+ let patterns = patterns.split_whitespace().collect::<Vec<_>>();
+ let text = macros.expand(&args[1]);
+ let text = text.split_whitespace();
+ let mut result_pieces = vec![];
+ for word in text {
+ if patterns
+ .iter()
+ .all(|pattern| r#match(pattern, word).is_none())
+ {
+ result_pieces.push(word);
+ }
+ }
+ result_pieces.join(" ")
+ }
+ "sort" => {
+ assert_eq!(args.len(), 1);
+ let words = macros.expand(&args[0]);
+ let mut words = words.split_whitespace().collect::<Vec<_>>();
+ words.sort();
+ words.dedup();
+ words.join(" ")
+ }
// File Name Functions
- "notdir" => todo!(),
- "basename" => todo!(),
- "addprefix" => todo!(),
- "wildcard" => todo!(),
+ "notdir" => {
+ assert_eq!(args.len(), 1);
+ let words = macros.expand(&args[0]);
+ let words = words
+ .split_whitespace()
+ .map(|word| {
+ Path::new(word)
+ .file_name()
+ .and_then(|filename| filename.to_str())
+ .unwrap_or("")
+ })
+ .collect::<Vec<_>>();
+ words.join(" ")
+ }
+ "basename" => {
+ assert_eq!(args.len(), 1);
+ let words = macros.expand(&args[0]);
+ let words = words
+ .split_whitespace()
+ .map(|word| {
+ Path::new(word)
+ .with_extension("")
+ .to_str()
+ .map_or_else(String::new, ToString::to_string)
+ })
+ .collect::<Vec<_>>();
+ words.join(" ")
+ }
+ "addprefix" => {
+ assert_eq!(args.len(), 2);
+ let prefix = macros.expand(&args[0]);
+ let targets = macros.expand(&args[1]);
+ let results = targets
+ .split_whitespace()
+ .map(|t| format!("{}{}", prefix, t))
+ .collect::<Vec<_>>();
+ results.join(" ")
+ }
+ "wildcard" => {
+ assert_eq!(args.len(), 1);
+ let pattern = macros.expand(&args[0]);
+ let home_dir = env::var("HOME")
+ .ok()
+ .or(dirs::home_dir().and_then(|p| p.to_str().map(String::from)));
+ let pattern = if let Some(home_dir) = home_dir {
+ pattern.replace('~', &home_dir)
+ } else {
+ pattern
+ };
+ let results = glob::glob(&pattern)
+ .expect("invalid glob pattern!")
+ .filter_map(|path| path.ok())
+ .map(|path| path.to_str().map(ToString::to_string).unwrap_or_default())
+ .collect::<Vec<_>>();
+ results.join(" ")
+ }
// foreach
- "foreach" => todo!(),
+ "foreach" => {
+ assert_eq!(args.len(), 3);
+ let var = macros.expand(&args[0]);
+ let list = macros.expand(&args[1]);
+ let text = &args[2];
+ let words = list.split_whitespace();
+
+ let mut macros = macros.clone();
+ let results = words
+ .map(|word| {
+ macros.set(var.clone(), MacroSource::File, TokenString::text(word));
+ macros.expand(text)
+ })
+ .collect::<Vec<_>>();
+ results.join(" ")
+ }
// call
- "call" => todo!(),
+ "call" => {
+ assert!(args.len() > 0);
+ let args = args
+ .iter()
+ .map(|arg| macros.expand(arg))
+ .collect::<Vec<_>>();
+ let function = args[0].clone();
+
+ let mut macros = macros.clone();
+ for (i, x) in args.into_iter().enumerate() {
+ macros.set(i.to_string(), MacroSource::File, TokenString::text(x));
+ }
+ macros.expand(&TokenString::r#macro(function))
+ }
// eval
- "eval" => todo!(),
+ "eval" => {
+ assert_eq!(args.len(), 1);
+ let arg = macros.expand(&args[0]);
+ todo!()
+ }
// shell
"shell" => todo!(),
@@ -29,3 +163,132 @@ pub(crate) fn call(name: &str, args: &[TokenString]) -> TokenString {
_ => panic!("function not implemented: {}", name),
}
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> String {
+ super::call(name, args, macros, lookup_fail)
+ }
+
+ macro_rules! call {
+ ($func:literal $($arg:literal),+) => {
+ call($func, &[$(TokenString::text($arg)),+], &MacroSet::new())
+ };
+ ($func:ident $($arg:literal),+) => {
+ call(stringify!($func), &[$(TokenString::text($arg)),+], &MacroSet::new())
+ };
+ }
+
+ #[test]
+ fn filter() {
+ let result = call!(filter "word", "this contains a word inside it");
+ assert_eq!(result, "word");
+
+ let result = call!(filter "%.c %.s", "foo.c bar.c baz.s ugh.h");
+ assert_eq!(result, "foo.c bar.c baz.s");
+ }
+
+ #[test]
+ fn filter_out() {
+ let result = call!("filter-out" "main1.o main2.o", "main1.o foo.o main2.o bar.o");
+ assert_eq!(result, "foo.o bar.o");
+ }
+
+ #[test]
+ fn sort() {
+ let result = call!(sort "foo bar lose foo");
+ assert_eq!(result, "bar foo lose");
+ }
+
+ #[test]
+ fn notdir() {
+ let result = call!(notdir "src/foo.c hacks");
+ assert_eq!(result, "foo.c hacks");
+ }
+
+ #[test]
+ fn basename() {
+ let result = call!(basename "src/foo.c src-1.0/bar hacks");
+ assert_eq!(result, "src/foo src-1.0/bar hacks");
+ }
+
+ #[test]
+ fn addprefix() {
+ let result = call!(addprefix "src/", "foo bar");
+ assert_eq!(result, "src/foo src/bar");
+ }
+
+ #[test]
+ fn wildcard() {
+ use std::env::{set_current_dir, set_var};
+ use std::fs::write;
+ use std::path::MAIN_SEPARATOR;
+
+ let tempdir = tempfile::tempdir().unwrap();
+
+ write(tempdir.path().join("foo.c"), "").unwrap();
+ write(tempdir.path().join("bar.h"), "").unwrap();
+ write(tempdir.path().join("baz.txt"), "").unwrap();
+ write(tempdir.path().join("acab.c"), "ACAB").unwrap();
+ write(tempdir.path().join("based.txt"), "☭").unwrap();
+
+ set_current_dir(tempdir.path()).unwrap();
+ set_var("HOME", tempdir.path().to_str().unwrap());
+ let sort = |x: String| call("sort", &[TokenString::text(&x)], &MacroSet::new());
+ assert_eq!(sort(call!(wildcard "*.c")), "acab.c foo.c");
+ assert_eq!(
+ sort(call!(wildcard "~/ba?.*")),
+ format!(
+ "{0}{1}bar.h {0}{1}baz.txt",
+ tempdir.path().to_str().unwrap(),
+ MAIN_SEPARATOR
+ )
+ );
+ }
+
+ #[test]
+ fn foreach() {
+ let mut macros = MacroSet::new();
+ macros.set(
+ "test".to_string(),
+ MacroSource::File,
+ "worked for $(item).".parse().unwrap(),
+ );
+ assert_eq!(
+ call(
+ "foreach",
+ &[
+ TokenString::text("item"),
+ TokenString::text("a b c d"),
+ TokenString::r#macro("test")
+ ],
+ &macros,
+ ),
+ "worked for a. worked for b. worked for c. worked for d."
+ );
+ }
+
+ #[test]
+ fn call_test() {
+ let mut macros = MacroSet::new();
+ macros.set(
+ "reverse".to_string(),
+ MacroSource::File,
+ "$(2) $(1)".parse().unwrap(),
+ );
+ assert_eq!(
+ call(
+ "call",
+ &[
+ TokenString::text("reverse"),
+ TokenString::text("a"),
+ TokenString::text("b")
+ ],
+ &macros,
+ ),
+ "b a"
+ );
+ }
+}
diff --git a/src/makefile/macro.rs b/src/makefile/macro.rs
new file mode 100644
index 0000000..139903f
--- /dev/null
+++ b/src/makefile/macro.rs
@@ -0,0 +1,189 @@
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::env;
+use std::fmt;
+use std::rc::{Rc, Weak};
+
+use regex::Regex;
+
+use super::functions;
+use super::token::{Token, TokenString};
+
+#[derive(Debug, Clone)]
+pub(crate) enum MacroSource {
+ File,
+ CommandLineOrMakeflags,
+ Environment,
+ Builtin,
+}
+
+pub(crate) trait LookupInternal: for<'a> Fn(&'a str) -> String {}
+
+impl<F: for<'a> Fn(&'a str) -> String> LookupInternal for F {}
+
+fn lookup_fail(_: &str) -> String {
+ panic!("internal variables not available!");
+}
+
+#[derive(Clone)]
+pub(crate) struct MacroSet {
+ data: HashMap<String, (MacroSource, TokenString)>,
+ lookup_internal: RefCell<Weak<dyn LookupInternal>>,
+}
+
+impl MacroSet {
+ pub(crate) fn new() -> Self {
+ let lookup_fail: Rc<dyn LookupInternal> = Rc::new(lookup_fail);
+ Self {
+ data: HashMap::new(),
+ lookup_internal: RefCell::new(Rc::downgrade(&lookup_fail)),
+ }
+ }
+
+ pub(crate) fn lookup(&self, lookup: Weak<dyn LookupInternal>) {
+ self.lookup_internal.replace(lookup);
+ }
+
+ pub(crate) fn add_builtins(&mut self) {
+ for (k, v) in builtins() {
+ self.data.insert(k.into(), (MacroSource::Builtin, v));
+ }
+ }
+
+ pub(crate) fn add_env(&mut self) {
+ for (k, v) in env::vars() {
+ if k != "MAKEFLAGS" && k != "SHELL" {
+ self.data
+ .insert(k, (MacroSource::Environment, TokenString::text(v)));
+ }
+ }
+ }
+
+ pub(crate) fn set(&mut self, name: String, source: MacroSource, text: TokenString) {
+ self.data.insert(name, (source, text));
+ }
+
+ pub(crate) fn is_defined(&self, name: &str) -> bool {
+ self.data.contains_key(name)
+ }
+
+ pub(crate) fn get(&self, name: &str) -> Option<&(MacroSource, TokenString)> {
+ self.data.get(name)
+ }
+
+ // `remove` is fine, but I think for "remove-and-return" `pop` is better
+ pub(crate) fn pop(&mut self, name: &str) -> Option<(MacroSource, TokenString)> {
+ self.data.remove(name)
+ }
+
+ pub(crate) fn expand(&self, text: &TokenString) -> String {
+ let mut result = String::new();
+ for token in text.tokens() {
+ match token {
+ Token::Text(t) => result.push_str(t),
+ Token::MacroExpansion { name, replacement } => {
+ let 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 {
+ let lookup_internal = self.lookup_internal.borrow();
+ let lookup_internal = match lookup_internal.upgrade() {
+ Some(f) => f,
+ None => Rc::new(lookup_fail),
+ };
+ lookup_internal(name)
+ } else {
+ self.data
+ .get(name)
+ .map_or_else(String::new, |(_, macro_value)| self.expand(macro_value))
+ };
+ let macro_value = match replacement {
+ Some((subst1, subst2)) => {
+ let subst1 = self.expand(subst1);
+ let subst1_suffix = regex::escape(&subst1);
+ let subst1_suffix =
+ Regex::new(&format!(r"{}\b", subst1_suffix)).unwrap();
+ let subst2 = self.expand(subst2);
+ subst1_suffix.replace_all(&macro_value, subst2).to_string()
+ }
+ None => macro_value,
+ };
+ result.push_str(&macro_value);
+ }
+ Token::FunctionCall { name, args } => {
+ result.push_str(&functions::call(name, args, &self));
+ }
+ }
+ }
+ result
+ }
+}
+
+impl fmt::Debug for MacroSet {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{:?}", &self.data)
+ }
+}
+
+impl fmt::Display for MacroSet {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ let pieces = self
+ .data
+ .iter()
+ .map(|(k, (_, v))| format!("{}={}", k, v))
+ .collect::<Vec<_>>();
+ write!(f, "{}", pieces.join("\n"))
+ }
+}
+
+fn builtins() -> Vec<(&'static str, TokenString)> {
+ // Fuck it, might as well.
+ macro_rules! handle {
+ ($value:ident) => {
+ stringify!($value)
+ };
+ ($value:literal) => {
+ $value
+ };
+ }
+ macro_rules! make {
+ ($($name:ident=$value:tt)+) => {vec![$(
+ (stringify!($name), handle!($value).parse().unwrap())
+ ),+]};
+ }
+
+ make![
+ MAKE=makers
+ AR=ar
+ YACC=yacc
+ YFLAGS=""
+ LEX=lex
+ LFLAGS=""
+ LDFLAGS=""
+
+ AS=as
+ CC=cc
+ CXX="g++"
+ CPP="$(CC) -E"
+ FC=f77
+ PC=pc
+ CO=co
+ GET=get
+ LINT=lint
+ MAKEINFO=makeinfo
+ TEX=tex
+ TEXI2DVI=texi2dvi
+ WEAVE=weave
+ CWEAVE=cweave
+ TANGLE=tangle
+ CTANGLE=ctangle
+ RM="rm -f"
+
+ ARFLAGS="rv"
+ CFLAGS=""
+ FFLAGS=""
+ ]
+}
diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs
index 1fc8303..7610197 100644
--- a/src/makefile/mod.rs
+++ b/src/makefile/mod.rs
@@ -7,21 +7,23 @@ use std::io::{BufRead, BufReader, Error as IoError};
use std::path::Path;
use std::rc::Rc;
+use crate::args::Args;
use lazy_static::lazy_static;
use regex::Regex;
-use crate::args::Args;
-
mod command_line;
mod conditional;
mod functions;
mod inference_rules;
+mod r#macro;
+mod pattern;
mod target;
mod token;
use command_line::CommandLine;
use conditional::{ConditionalLine, ConditionalState};
use inference_rules::InferenceRule;
+use r#macro::{LookupInternal, MacroSet, MacroSource};
use target::Target;
use token::{tokenize, Token, TokenString};
@@ -58,13 +60,6 @@ impl LineType {
}
}
-enum MacroSource {
- File,
- CommandLineOrMakeflags,
- Environment,
- Builtin,
-}
-
fn inference_match<'a>(
targets: &[&'a str],
prerequisites: &[String],
@@ -91,7 +86,7 @@ fn inference_match<'a>(
pub(crate) struct Makefile<'a> {
inference_rules: Vec<InferenceRule>,
- macros: HashMap<String, (MacroSource, TokenString)>,
+ macros: MacroSet,
targets: RefCell<HashMap<String, Rc<RefCell<Target>>>>,
pub(crate) first_non_special_target: Option<String>,
args: &'a Args,
@@ -101,15 +96,13 @@ pub(crate) struct Makefile<'a> {
impl<'a> Makefile<'a> {
pub(crate) fn new(args: &'a Args) -> Self {
let mut inference_rules = vec![];
- let mut macros = HashMap::new();
+ let mut macros = MacroSet::new();
let mut targets = HashMap::new();
let first_non_special_target = None;
if !args.no_builtin_rules {
inference_rules.extend(builtin_inference_rules());
- for (k, v) in builtin_macros() {
- macros.insert(k.into(), (MacroSource::Builtin, v));
- }
+ macros.add_builtins();
targets.extend(
builtin_targets()
.into_iter()
@@ -117,20 +110,14 @@ impl<'a> Makefile<'a> {
);
}
- for (k, v) in env::vars() {
- if k != "MAKEFLAGS" && k != "SHELL" {
- macros.insert(k, (MacroSource::Environment, TokenString::text(v)));
- }
- }
+ macros.add_env();
for r#macro in args.macros() {
if let [name, value] = *r#macro.splitn(2, '=').collect::<Vec<_>>() {
- macros.insert(
+ macros.set(
name.into(),
- (
- MacroSource::CommandLineOrMakeflags,
- TokenString::text(value),
- ),
+ MacroSource::CommandLineOrMakeflags,
+ TokenString::text(value),
);
}
}
@@ -197,7 +184,7 @@ impl<'a> Makefile<'a> {
{
line.action(
conditional_stack.last(),
- |name| self.macros.contains_key(name),
+ |name| self.macros.is_defined(name),
|t| self.expand_macros(t, None),
)
.apply_to(&mut conditional_stack);
@@ -375,7 +362,7 @@ impl<'a> Makefile<'a> {
_ => {}
}
- let value = match self.macros.remove(name) {
+ let value = match self.macros.pop(name) {
Some((_, mut old_value)) if append => {
// TODO eagerly expand if appending to eagerly-expanded macro
old_value.extend(TokenString::text(" "));
@@ -384,7 +371,7 @@ impl<'a> Makefile<'a> {
}
_ => value,
};
- self.macros.insert(name.into(), (MacroSource::File, value));
+ self.macros.set(name.into(), MacroSource::File, value);
}
fn special_target_has_prereq(&self, target: &str, name: &str) -> bool {
@@ -501,104 +488,75 @@ impl<'a> Makefile<'a> {
}
fn expand_macros(&self, text: &TokenString, target: Option<&Target>) -> 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 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 {
- let target = target.expect("internal macro but no current target!");
- let macro_pieces = if name.starts_with('@') {
- // The $@ shall evaluate to the full target name of the
- // current target.
- vec![target.name.clone()]
- } else if 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)
- .borrow()
- .newer_than(target)
- .unwrap_or(false)
- })
- .cloned()
- .collect()
- } else if 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.
- target.prerequisites.clone()
- } else if name.starts_with('*') {
- // The $* macro shall evaluate to the current target name with
- // its suffix deleted.
- vec![Path::new(name).with_extension("").to_string_lossy().into()]
- } else {
- unreachable!()
- };
+ let target = target.cloned();
+ let lookup_internal = move |name: &str| {
+ let target = target
+ .as_ref()
+ .expect("internal macro but no current target!");
+ let macro_pieces = if name.starts_with('@') {
+ // The $@ shall evaluate to the full target name of the
+ // current target.
+ vec![target.name.clone()]
+ } else if 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)
+ .borrow()
+ .newer_than(&target)
+ .unwrap_or(false)
+ })
+ .cloned()
+ .collect()
+ } else if 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.
+ target.prerequisites.clone()
+ } else if name.starts_with('*') {
+ // The $* macro shall evaluate to the current target name with
+ // its suffix deleted.
+ vec![Path::new(name).with_extension("").to_string_lossy().into()]
+ } else {
+ unreachable!()
+ };
- let macro_pieces = if name.ends_with('D') {
- macro_pieces
- .into_iter()
- .map(|x| {
- Path::new(&x)
- .parent()
- .expect("no parent")
- .to_string_lossy()
- .into()
- })
- .collect()
- } else if name.ends_with('F') {
- macro_pieces
- .into_iter()
- .map(|x| {
- Path::new(&x)
- .file_name()
- .expect("no filename")
- .to_string_lossy()
- .into()
- })
- .collect()
- } else {
- macro_pieces
- };
+ let macro_pieces = if name.ends_with('D') {
+ macro_pieces
+ .into_iter()
+ .map(|x| {
+ Path::new(&x)
+ .parent()
+ .expect("no parent")
+ .to_string_lossy()
+ .into()
+ })
+ .collect()
+ } else if name.ends_with('F') {
+ macro_pieces
+ .into_iter()
+ .map(|x| {
+ Path::new(&x)
+ .file_name()
+ .expect("no filename")
+ .to_string_lossy()
+ .into()
+ })
+ .collect()
+ } else {
+ macro_pieces
+ };
- macro_pieces.join(" ")
- } else {
- self.macros
- .get(name)
- .map_or_else(String::new, |(_, macro_value)| {
- self.expand_macros(macro_value, target)
- })
- };
- let macro_value = match replacement {
- Some((subst1, subst2)) => {
- let subst1 = self.expand_macros(subst1, target);
- let subst1_suffix = regex::escape(&subst1);
- let subst1_suffix =
- Regex::new(&format!(r"{}\b", subst1_suffix)).unwrap();
- let subst2 = self.expand_macros(subst2, target);
- subst1_suffix.replace_all(&macro_value, subst2).to_string()
- }
- None => macro_value,
- };
- result.push_str(&macro_value);
- }
- Token::FunctionCall { name, args } => {
- result.push_str(&self.expand_macros(&functions::call(name, args), None));
- }
- }
- }
- result
+ macro_pieces.join(" ")
+ };
+
+ let lookup_internal: Rc<dyn LookupInternal> = Rc::new(lookup_internal);
+ self.macros.lookup(Rc::downgrade(&lookup_internal));
+ self.macros.expand(text)
}
}
@@ -614,9 +572,7 @@ impl fmt::Display for Makefile<'_> {
writeln!(f)?;
header(f, "Macros")?;
- for (k, (_, v)) in &self.macros {
- writeln!(f, "{}={}", k, v)?;
- }
+ writeln!(f, "{}", &self.macros)?;
writeln!(f)?;
header(f, "Targets")?;
@@ -691,54 +647,6 @@ fn builtin_inference_rules() -> Vec<InferenceRule> {
"rm -f $*.o"
}
}
-fn builtin_macros() -> Vec<(&'static str, TokenString)> {
- // Fuck it, might as well.
- macro_rules! handle {
- ($value:ident) => {
- stringify!($value)
- };
- ($value:literal) => {
- $value
- };
- }
- macro_rules! make {
- ($($name:ident=$value:tt)+) => {vec![$(
- (stringify!($name), handle!($value).parse().unwrap())
- ),+]};
- }
-
- make![
- MAKE=makers
- AR=ar
- YACC=yacc
- YFLAGS=""
- LEX=lex
- LFLAGS=""
- LDFLAGS=""
-
- AS=as
- CC=cc
- CXX="g++"
- CPP="$(CC) -E"
- FC=f77
- PC=pc
- CO=co
- GET=get
- LINT=lint
- MAKEINFO=makeinfo
- TEX=tex
- TEXI2DVI=texi2dvi
- WEAVE=weave
- CWEAVE=cweave
- TANGLE=tangle
- CTANGLE=ctangle
- RM="rm -f"
-
- ARFLAGS="rv"
- CFLAGS=""
- FFLAGS=""
- ]
-}
fn builtin_targets() -> Vec<Target> {
// even i'm not going to do that just for this
vec![Target {
@@ -754,14 +662,14 @@ fn builtin_targets() -> Vec<Target> {
#[cfg(test)]
mod test {
- use super::*;
-
use std::io::Cursor;
+ use super::*;
+
fn empty_makefile(args: &Args) -> Makefile {
Makefile {
inference_rules: vec![],
- macros: HashMap::new(),
+ macros: MacroSet::new(),
targets: RefCell::new(HashMap::new()),
first_non_special_target: None,
args,
@@ -781,8 +689,50 @@ endif
let mut makefile = empty_makefile(&args);
makefile.and_read(Cursor::new(file));
assert_eq!(
- makefile.expand_macros(&"$(worked)".parse().unwrap(), None),
+ makefile.expand_macros(&TokenString::r#macro("worked"), None),
"yes"
);
}
+
+ #[test]
+ #[ignore = "I still haven't implemented `eval` or `define` or %-based macro substitution."]
+ fn eval() {
+ // This, for the record, is a terrible misfeature.
+ // If you need this, you probably shouldn't be using Make.
+ // But a lot of people are using this and still use Make anyway, so here we go,
+ // I guess.
+
+ let file = "
+PROGRAMS = server client
+
+server_OBJS = server.o server_priv.o server_access.o
+server_LIBS = priv protocol
+
+client_OBJS = client.o client_api.o client_mem.o
+client_LIBS = protocol
+
+# Everything after this is generic
+
+.PHONY: all
+all: $(PROGRAMS)
+
+define PROGRAM_template =
+ $(1): $$($(1)_OBJS) $$($(1)_LIBS:%=-l%)
+ ALL_OBJS += $$($(1)_OBJS)
+endef
+
+$(foreach prog,$(PROGRAMS),$(eval $(call PROGRAM_template,$(prog))))
+
+$(PROGRAMS):
+ $(LINK.o) $^ $(LDLIBS) -o $@
+
+clean:
+ rm -f $(ALL_OBJS) $(PROGRAMS)
+ ";
+
+ let args = Args::empty();
+ let mut makefile = empty_makefile(&args);
+ makefile.and_read(Cursor::new(file));
+ assert!(makefile.targets.borrow().contains_key("server"))
+ }
}
diff --git a/src/makefile/pattern.rs b/src/makefile/pattern.rs
new file mode 100644
index 0000000..733721e
--- /dev/null
+++ b/src/makefile/pattern.rs
@@ -0,0 +1,49 @@
+use regex::{Captures, Regex};
+
+fn compile_pattern(pattern: &str) -> Regex {
+ let mut result = String::new();
+ for c in pattern.chars() {
+ match c {
+ // This is a nightmare, because we're escaping for regex syntax before we put
+ // things into result.
+
+ // We don't end with a backslash, so this is an unescaped wildcard.
+ '%' if !result.ends_with(r"\\") => {
+ result.push_str(r"(\w*)");
+ }
+
+ // We end with two backslashes, so this is an escaped backslash and then an
+ // unescaped wildcard.
+ '%' if result.ends_with(r"\\\\") => {
+ result = result.strip_suffix(r"\\\\").unwrap().to_string();
+ result.push_str(r"\\(\w*)");
+ }
+
+ // We end with one backslash, so this is an escaped wildcard.
+ '%' if result.ends_with(r"\\") => {
+ result = result.strip_suffix(r"\\").unwrap().to_string();
+ result.push('%');
+ }
+ _ => result.push_str(&regex::escape(&c.to_string())),
+ }
+ }
+ Regex::new(&result).expect("built invalid regex!")
+}
+
+pub(crate) fn r#match<'a>(pattern: &str, text: &'a str) -> Option<Captures<'a>> {
+ compile_pattern(pattern).captures(text)
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn pattern_backslashes() {
+ let test_case = compile_pattern(r"the\%weird\\%pattern\\");
+ assert_eq!(test_case.to_string(), r"the%weird\\(\w*)pattern\\\\");
+
+ let hell = compile_pattern(r"\\\\%");
+ assert_eq!(hell.to_string(), r"\\\\\\(\w*)");
+ }
+}
diff --git a/src/makefile/token.rs b/src/makefile/token.rs
index eca40ed..8b21f06 100644
--- a/src/makefile/token.rs
+++ b/src/makefile/token.rs
@@ -19,6 +19,13 @@ impl TokenString {
Self(vec![Token::Text(text.into())])
}
+ pub(crate) fn r#macro(name: impl Into<String>) -> Self {
+ Self(vec![Token::MacroExpansion {
+ name: name.into(),
+ replacement: None,
+ }])
+ }
+
pub(crate) fn tokens(&self) -> impl Iterator<Item = &Token> {
self.0.iter()
}