diff options
author | Melody Horn <melody@boringcactus.com> | 2021-03-28 14:57:03 -0600 |
---|---|---|
committer | Melody Horn <melody@boringcactus.com> | 2021-03-28 14:57:03 -0600 |
commit | bc5038e6c344803bce76add47b13ceaa61a5bde3 (patch) | |
tree | eec9a8fc36562aa4413cf8be8ef22910fdc75cac | |
parent | 82278c3a5aa93204c963a63cc3cfefea1d0fb3fd (diff) | |
download | makers-bc5038e6c344803bce76add47b13ceaa61a5bde3.tar.gz makers-bc5038e6c344803bce76add47b13ceaa61a5bde3.zip |
almost implement all functions
-rw-r--r-- | Cargo.lock | 216 | ||||
-rw-r--r-- | Cargo.toml | 5 | ||||
-rw-r--r-- | src/makefile/functions.rs | 285 | ||||
-rw-r--r-- | src/makefile/macro.rs | 189 | ||||
-rw-r--r-- | src/makefile/mod.rs | 306 | ||||
-rw-r--r-- | src/makefile/pattern.rs | 49 | ||||
-rw-r--r-- | src/makefile/token.rs | 7 |
7 files changed, 868 insertions, 189 deletions
@@ -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" @@ -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") + ], + ¯os, + ), + "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") + ], + ¯os, + ), + "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(¯o_value, subst2).to_string() + } + None => macro_value, + }; + result.push_str(¯o_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(¯o_value, subst2).to_string() - } - None => macro_value, - }; - result.push_str(¯o_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(®ex::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() } |