diff options
-rw-r--r-- | .gitlab-ci.yml | 11 | ||||
-rw-r--r-- | Cargo.lock | 788 | ||||
-rw-r--r-- | Cargo.toml | 29 | ||||
-rw-r--r-- | LICENSE.md | 2 | ||||
-rw-r--r-- | LICENSE.txt | 51 | ||||
-rw-r--r-- | README.md | 21 | ||||
-rw-r--r-- | src/args.rs | 230 | ||||
-rw-r--r-- | src/main.rs | 22 | ||||
-rw-r--r-- | src/makefile/command_line.rs | 91 | ||||
-rw-r--r-- | src/makefile/conditional.rs | 4 | ||||
-rw-r--r-- | src/makefile/eval_context.rs | 53 | ||||
-rw-r--r-- | src/makefile/functions.rs | 549 | ||||
-rw-r--r-- | src/makefile/inference_rules.rs | 138 | ||||
-rw-r--r-- | src/makefile/input.rs | 635 | ||||
-rw-r--r-- | src/makefile/lookup_internal.rs | 164 | ||||
-rw-r--r-- | src/makefile/macro.rs | 323 | ||||
-rw-r--r-- | src/makefile/macro_scope.rs | 229 | ||||
-rw-r--r-- | src/makefile/mod.rs | 210 | ||||
-rw-r--r-- | src/makefile/parse.rs | 19 | ||||
-rw-r--r-- | src/makefile/target.rs | 20 | ||||
-rw-r--r-- | src/makefile/token.rs | 64 | ||||
-rw-r--r-- | tests/conditional_assignment_inheritance.rs | 29 | ||||
-rw-r--r-- | tests/rule_specific_macros.rs | 85 | ||||
-rw-r--r-- | tests/update_logic.rs | 14 | ||||
-rw-r--r-- | tests/utils/mod.rs | 13 |
25 files changed, 2543 insertions, 1251 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f467b90..1c079af 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -75,14 +75,9 @@ full-test-xbps: script: - cargo install --path . - cd - - wget https://www.libarchive.org/downloads/libarchive-3.5.1.tar.xz - - tar xf libarchive-*.tar.xz - - cd libarchive-*/ - - ./configure - - makers - - makers install - git clone https://github.com/void-linux/xbps.git ~/xbps - cd ~/xbps + - apt update && apt install -y libarchive-dev - ./configure - makers @@ -92,13 +87,13 @@ full-test-linux: - cargo install --path . - mkdir ~/linux - cd ~/linux - - wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.11.11.tar.xz + - wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.5.tar.xz - tar xf linux-*.tar.xz - cd linux-*/ - (makers -p 2>&1 || true) | grep 'Configuration file ".config" not found!' - makers help - makers mrproper - - apt update && apt install -y flex bison + - apt update && apt install -y flex bison libelf-dev - makers defconfig - makers allow_failure: true @@ -1,10 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 4 + [[package]] name = "addr2line" -version = "0.14.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -17,58 +19,70 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "0.7.15" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] -name = "ansi_term" -version = "0.11.0" +name = "anstream" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ - "winapi", + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", ] [[package]] -name = "arrayref" -version = "0.3.6" +name = "anstyle" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] -name = "arrayvec" -version = "0.5.2" +name = "anstyle-parse" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] [[package]] -name = "atty" -version = "0.2.14" +name = "anstyle-query" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "hermit-abi", - "libc", - "winapi", + "windows-sys 0.59.0", ] [[package]] -name = "autocfg" -version = "1.0.1" +name = "anstyle-wincon" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] [[package]] name = "backtrace" -version = "0.3.56" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", + "cc", "cfg-if", "libc", "miniz_oxide", @@ -77,38 +91,18 @@ dependencies = [ ] [[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" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] -name = "bitvec" -version = "0.19.5" +name = "cc" +version = "1.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" +checksum = "40545c26d092346d8a8dab71ee48e7685a7a9cba76e634790c215b41a4a7b4cf" dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[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", + "shlex", ] [[package]] @@ -119,24 +113,50 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" -version = "2.33.3" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ - "ansi_term", - "atty", - "bitflags", + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", "strsim", - "textwrap", - "unicode-width", - "vec_map", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", ] [[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] name = "color-eyre" -version = "0.5.10" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b29030875fd8376e4a28ef497790d5b4a7843d8d1396bf08ce46f5eec562c5c" +checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" dependencies = [ "backtrace", "color-spantrace", @@ -149,9 +169,9 @@ dependencies = [ [[package]] name = "color-spantrace" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6eee477a4a8a72f4addd4de416eb56d54bc307b284d6601bafdee1f4ea462d1" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", "owo-colors", @@ -160,122 +180,109 @@ 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" +name = "colorchoice" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" -dependencies = [ - "autocfg", - "cfg-if", - "lazy_static", -] +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "dirs" -version = "3.0.1" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.5" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] [[package]] -name = "env_logger" -version = "0.8.3" +name = "env_filter" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ - "atty", - "humantime", "log", "regex", - "termcolor", ] [[package]] -name = "eyre" -version = "0.6.5" +name = "env_logger" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "221239d1d5ea86bf5d6f91c9d6bc3646ffe471b08ff9b0f91c44f115ac969d2b" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ - "indenter", - "once_cell", + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] -name = "funty" -version = "1.1.0" +name = "errno" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] [[package]] -name = "getrandom" -version = "0.1.16" +name = "eyre" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" dependencies = [ - "cfg-if", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "indenter", + "once_cell", ] [[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + +[[package]] name = "getrandom" -version = "0.2.2" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.23.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "heck" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.1.18" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" -dependencies = [ - "libc", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "humantime" @@ -290,519 +297,498 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] -name = "jane-eyre" -version = "0.3.0" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccd957c837e264dcbc6e75b58fcf85fd329a66e4836f820779c40563a2d0be0" -dependencies = [ - "color-eyre", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "lexical-core" -version = "0.7.5" +name = "libc" +version = "0.2.162" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21f866863575d0e1d654fbeeabdc927292fdf862873dc3c96c6f753357e13374" +checksum = "18d287de67fe55fd7e1581fe933d965a5a9477b38e949cfa9f8574ef01506398" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "arrayvec", "bitflags", - "cfg-if", - "ryu", - "static_assertions", + "libc", ] [[package]] -name = "libc" -version = "0.2.91" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8916b1f6ca17130ec6568feccee27c156ad12037880833a3b842a823236502e7" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.14" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "makers" -version = "0.7.0" +version = "0.8.0" dependencies = [ + "clap", + "color-eyre", "dirs", "env_logger", "eyre", "glob", - "jane-eyre", "lazy_static", "log", "nom", "regex", - "structopt", + "shlex", "tempfile", ] [[package]] name = "memchr" -version = "2.3.4" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", - "autocfg", ] [[package]] name = "nom" -version = "6.1.2" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "bitvec", - "funty", - "lexical-core", "memchr", - "version_check", + "minimal-lexical", ] [[package]] name = "object" -version = "0.23.0" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] [[package]] name = "once_cell" -version = "1.7.2" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "owo-colors" -version = "1.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2386b4ebe91c2f7f51082d4cefa145d030e33a1842a96b12e4885cc3c01f7a55" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] -name = "ppv-lite86" -version = "0.2.10" +name = "proc-macro2" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +dependencies = [ + "unicode-ident", +] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "quote" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "proc-macro-error-attr", "proc-macro2", - "quote", - "syn", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "redox_users" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "getrandom", + "libredox", + "thiserror", ] [[package]] -name = "proc-macro2" -version = "1.0.24" +name = "regex" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ - "unicode-xid", + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", ] [[package]] -name = "quote" -version = "1.0.9" +name = "regex-automata" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ - "proc-macro2", + "aho-corasick", + "memchr", + "regex-syntax", ] [[package]] -name = "radium" -version = "0.5.3" +name = "regex-syntax" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] -name = "rand" -version = "0.8.3" +name = "rustc-demangle" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "rand_chacha" -version = "0.3.0" +name = "rustix" +version = "0.38.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "99e4ea3e1cdc4b559b8e5650f9c8e5998e3e5c1343b4eaf034565f32318d63c0" dependencies = [ - "ppv-lite86", - "rand_core", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", ] [[package]] -name = "rand_core" -version = "0.6.2" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "getrandom 0.2.2", + "lazy_static", ] [[package]] -name = "rand_hc" -version = "0.3.0" +name = "shlex" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" -dependencies = [ - "rand_core", -] +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "redox_syscall" -version = "0.1.57" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "redox_syscall" -version = "0.2.5" +name = "syn" +version = "2.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" dependencies = [ - "bitflags", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "redox_users" -version = "0.3.5" +name = "tempfile" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ - "getrandom 0.1.16", - "redox_syscall 0.1.57", - "rust-argon2", + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "regex" -version = "1.4.5" +name = "terminal_size" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "rustix", + "windows-sys 0.59.0", ] [[package]] -name = "regex-syntax" -version = "0.6.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "winapi", + "thiserror-impl", ] [[package]] -name = "rust-argon2" -version = "0.8.3" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "base64", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - -[[package]] -name = "ryu" -version = "1.0.5" +name = "thread_local" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] [[package]] -name = "sharded-slab" -version = "0.1.1" +name = "tracing" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c719719ee05df97490f80a45acfc99e5a30ce98a1e4fb67aee422745ae14e3" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "lazy_static", + "pin-project-lite", + "tracing-core", ] [[package]] -name = "static_assertions" -version = "1.1.0" +name = "tracing-core" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] [[package]] -name = "strsim" -version = "0.8.0" +name = "tracing-error" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] [[package]] -name = "structopt" -version = "0.3.21" +name = "tracing-subscriber" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5277acd7ee46e63e5168a80734c9f6ee81b1367a7d8772a2d765df2a3705d28c" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ - "clap", - "lazy_static", - "structopt-derive", + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] -name = "structopt-derive" -version = "0.4.14" +name = "unicode-ident" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba9cdfda491b814720b6b06e0cac513d922fc407582032e8706e9f137976f90" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] -name = "syn" -version = "1.0.64" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "tap" -version = "1.0.1" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "tempfile" -version = "3.2.0" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" 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", -] +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "termcolor" -version = "1.1.2" +name = "windows-sys" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "winapi-util", + "windows-targets 0.48.5", ] [[package]] -name = "textwrap" -version = "0.11.0" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "unicode-width", + "windows-targets 0.52.6", ] [[package]] -name = "thread_local" -version = "1.1.3" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "once_cell", + "windows-targets 0.52.6", ] [[package]] -name = "tracing" -version = "0.1.25" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ebdc2bb4498ab1ab5f5b73c5803825e60199229ccba0698170e3be0e7f959f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] -name = "tracing-attributes" -version = "0.1.15" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42e6fa53307c8a17e4ccd4dc81cf5ec38db9209f59b222210375b54ee40d1e2" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] -name = "tracing-core" -version = "0.1.17" +name = "windows_aarch64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f50de3927f93d202783f4513cda820ab47ef17f624b03c096e86ef00c67e6b5f" -dependencies = [ - "lazy_static", -] +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "tracing-error" -version = "0.1.2" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" -dependencies = [ - "tracing", - "tracing-subscriber", -] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "tracing-subscriber" -version = "0.2.17" +name = "windows_aarch64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "705096c6f83bf68ea5d357a6aa01829ddbdac531b357b45abeca842938085baa" -dependencies = [ - "sharded-slab", - "thread_local", - "tracing-core", -] +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "unicode-segmentation" -version = "1.7.1" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "unicode-width" -version = "0.1.8" +name = "windows_i686_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "unicode-xid" -version = "0.2.1" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "vec_map" -version = "0.8.2" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "version_check" -version = "0.9.3" +name = "windows_i686_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +name = "windows_x86_64_gnu" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] -name = "winapi" -version = "0.3.9" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "winapi-util" -version = "0.1.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "wyz" -version = "0.2.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" @@ -1,12 +1,12 @@ [package] name = "makers" -version = "0.7.0" +version = "0.8.0" authors = ["boringcactus / Melody Horn <melody@boringcactus.com>"] -edition = "2018" +edition = "2021" description = "a POSIX-compatible make implemented in Rust" readme = "README.md" repository = "https://code.boringcactus.com/makers/" -license-file = "LICENSE.md" +license-file = "LICENSE.txt" keywords = ["build", "make"] categories = ["development-tools"] @@ -15,19 +15,20 @@ default = ['full'] full = ['dirs', 'glob'] [dependencies] -dirs = { version = "3.0.1", optional = true } -env_logger = "0.8.3" -eyre = "0.6.5" -glob = { version = "0.3.0", optional = true } -jane-eyre = "0.3.0" -lazy_static = "1.4.0" -log = "0.4.14" -nom = "6.1.2" -regex = "1.4.5" -structopt = "0.3.21" +clap = { version = "4.5.20", features = ["derive", "wrap_help"] } +color-eyre = "0.6.3" +dirs = { version = "5.0.1", optional = true } +env_logger = "0.11.5" +eyre = "0.6.12" +glob = { version = "0.3.1", optional = true } +lazy_static = "1.5.0" +log = "0.4.22" +nom = "7.1.3" +regex = "1.11.0" +shlex = "1.3.0" [dev-dependencies] -tempfile = "3.2.0" +tempfile = "3.13.0" # [target.'cfg(unix)'.dependencies] # signal-hook = "0.3.7" diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 26874f0..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,2 +0,0 @@ -Released under the [Indie Code Catalog Standard Deal](https://indiecc.com/deal/3.0.0), -version 3.0.0 or later. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5ad7e33 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,51 @@ + =========================== + BIG BILL HELL'S LICENSE + =========================== + + FUCK YOU, OPEN-SOURCE! + +If you're dumb enough to want to copy this software, you're a big enough +schmuck to abide by BIG BILL HELL'S LICENSE (BBHL): + +| 1. BAD TERMS +| 2. CODE THAT BREAKS DOWN +| 3. THIEVES + +-------------- +1. BAD TERMS +-------------- + +If you think you're going to find permissive terms in BIG BILL'S, you can +kiss my ASS. It's our belief that you're such a stupid motherfucker, +you'll depend on this bullshit, GUARANTEED. If you make a derivative work: +Shove it up your ugly ASS. You heard us right: Shove it up your ugly ASS. + +-------------------------- +2. CODE THAT BREAKS DOWN +-------------------------- + +Bring your CHANGE; bring your REPRO; bring your SPOUSE--we'll fuck them! +That's right, we'll fuck your SPOUSE. Because under the BBHL, you're fucked +six ways from Sunday. + +------------ +3. THIEVES +------------ + +Make a hack with BBHL, home of CHALLENGE GITTING. That's right: + +3.a. CHALLENGE GITTING +---------------------- +How does it work? If you submit six PRs straight into main that don't need +feedback, you get no Contributor License Agreement (CLA). + + +DON'T SELL; DON'T DISTRIBUTE; DON'T FORK FROM US, OR WE'LL RIP YOUR BITS OFF. + +ONLY WITH BBHL: THE ONLY LICENSE THAT TELLS YOU TO FUCK OFF. + +HURRY UP, ASSHOLE, THESE TERMS TAKE EFFECT THE MINUTE YOU WRITE A LINE OF +CODE, AND IT BETTER NOT BREAK, OR YOU'RE A DEAD MOTHERFUCKER. + +GO TO HELL. BIG BILL HELL'S LICENSE: OPEN-SOURCE'S FILTHIEST AND EXCLUSIVE +TERMS OF THE MEANEST (SONS OF) BITCHES IN THE WHOLE COMMUNITY, GUARANTEED. @@ -4,7 +4,7 @@ A (mostly) [POSIX-compatible](https://pubs.opengroup.org/onlinepubs/9699919799/u Not explicitly aiming for full support for [every GNU make feature](https://www.gnu.org/software/make/manual/html_node/index.html), but adding whichever features are strictly necessary to be compatible with existing GNUish makefiles. You can get slightly more info by running with `RUST_LOG=makers=warn`, much more info with `RUST_LOG=makers=debug`, and an avalanche of info (probably only useful in small doses for debugging your makefile or makers itself) with `RUST_LOG=makers=trace`. -For more specific configuration, see [the documentation for the `env_logger` Rust crate](https://docs.rs/env_logger/0.8.3/env_logger/). +For more specific configuration, see [the documentation for the `env_logger` Rust crate](https://docs.rs/env_logger/0.11.5/env_logger/). ## conformance @@ -31,12 +31,27 @@ specific projects tested: list of features that are needed for version 1.0: +- actual documentation - tested with Linux - unit tests for most of the things - no TODOs left ## changelog +### next - TBD + +- propagate command line macros through `MAKEFLAGS` for recursive calls + +### v0.8.0 - 2024-11-15 + +- license is worse +- implement `-C`/`--directory` flag +- pass flags into recursive calls via `MAKEFLAGS` environment variable +- implement GNU-style `export EGG=bug` +- rebuild out-of-date makefiles (but not with as much environment preservation as GNU make provides) +- implement GNU-style rule-specific macros `target: EGG=bug` +- implement GNU-style `.SECONDEXPANSION` (but not properly deferred like in GNU make) + ### v0.7.0 - 2021-04-13 - implement GNU-style functions `abspath`, `firstword` @@ -95,9 +110,9 @@ list of features that are needed for version 1.0: ## license -Released under the [Indie Code Catalog Standard Deal](https://indiecc.com/deal/3.0.0), version 3.0.0 or later. +Released under [Big Bill Hell’s License](https://lifning.info/BBHL). -Commercial licenses are available for purchase [through Indie Code Catalog](https://indiecc.com/~boringcactus/makers). +Exceptions may be available on request. ## minuteæ diff --git a/src/args.rs b/src/args.rs index 9318202..8489c2d 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,17 +1,18 @@ use std::env; use std::ffi::OsString; use std::iter; +use std::ops::AddAssign; use std::path::PathBuf; -use structopt::StructOpt; +use clap::Parser; -#[derive(StructOpt, Debug, PartialEq, Eq, Clone)] -#[structopt(author, about)] +#[derive(clap::Parser, Debug, PartialEq, Eq, Clone)] +#[clap(author, about)] #[allow(clippy::struct_excessive_bools)] pub struct Args { /// Cause environment variables, including those with null values, to override macro /// assignments within makefiles. - #[structopt(short, long)] + #[clap(short, long)] pub environment_overrides: bool, /// Specify a different makefile (or '-' for standard input). @@ -21,12 +22,12 @@ pub struct Args { /// be multiple instances of this option, and they shall be processed in the order /// specified. The effect of specifying the same option-argument more than once is /// unspecified. - #[structopt( - short = "f", + #[clap( + short = 'f', long = "file", visible_alias = "makefile", number_of_values = 1, - parse(from_os_str) + value_parser )] pub makefile: Vec<PathBuf>, @@ -34,17 +35,17 @@ pub struct Args { /// /// This mode is the same as if the special target .IGNORE were specified without /// prerequisites. - #[structopt(short, long)] + #[clap(short, long)] pub ignore_errors: bool, /// Continue to update other targets that do not depend on the current target if a /// non-ignored error occurs while executing the commands to bring a target /// up-to-date. - #[structopt( + #[clap( short, long, - overrides_with = "keep-going", - overrides_with = "no-keep-going" + overrides_with = "keep_going", + overrides_with = "no_keep_going" )] pub keep_going: bool, @@ -54,8 +55,8 @@ pub struct Args { /// However, lines with a <plus-sign> ( '+' ) prefix shall be executed. In this mode, /// lines with an at-sign ( '@' ) character prefix shall be written to standard /// output. - #[structopt( - short = "n", + #[clap( + short = 'n', long, visible_alias = "just-print", visible_alias = "recon" @@ -66,7 +67,7 @@ pub struct Args { /// descriptions. /// /// The output format is unspecified. - #[structopt(short, long, visible_alias = "print-data-base")] + #[clap(short, long, visible_alias = "print-data-base")] pub print_everything: bool, /// Return a zero exit value if the target file is up-to-date; otherwise, return an @@ -75,11 +76,11 @@ pub struct Args { /// Targets shall not be updated if this option is specified. However, a makefile /// command line (associated with the targets) with a <plus-sign> ( '+' ) prefix /// shall be executed. - #[structopt(short, long)] + #[clap(short, long)] pub question: bool, /// Clear the suffix list and do not use the built-in rules. - #[structopt(short = "r", long)] + #[clap(short = 'r', long)] pub no_builtin_rules: bool, /// Terminate make if an error occurs while executing the commands to bring a target @@ -87,13 +88,13 @@ pub struct Args { /// reason). /// /// This shall be the default and the opposite of -k. - #[structopt( - short = "S", + #[clap( + short = 'S', long, visible_alias = "stop", - hidden = true, - overrides_with = "keep-going", - overrides_with = "no-keep-going" + hide = true, + overrides_with = "keep_going", + overrides_with = "no_keep_going" )] pub no_keep_going: bool, @@ -102,7 +103,7 @@ pub struct Args { /// /// This mode shall be the same as if the special target .SILENT were specified /// without prerequisites. - #[structopt(short, long, visible_alias = "quiet")] + #[clap(short, long, visible_alias = "quiet")] pub silent: bool, /// Update the modification time of each target as though a touch target had been @@ -113,14 +114,35 @@ pub struct Args { /// target file indicating the name of the file and that it was touched. Normally, /// the makefile command lines associated with each target are not executed. However, /// a command line with a <plus-sign> ( '+' ) prefix shall be executed. - #[structopt(short, long)] + #[clap(short, long)] pub touch: bool, /// Change to the given directory before running. #[cfg(feature = "full")] - #[structopt(short = "C", long, parse(from_os_str))] + #[clap(short = 'C', long, value_parser)] pub directory: Option<PathBuf>, + /// Print the working directory when starting. + // TODO implement + // TODO automatically with -C or recursion or decide that this is a bad GNU feature + #[cfg(feature = "full")] + #[clap( + short = 'w', + long, + overrides_with = "print_directory", + overrides_with = "no_print_directory" + )] + pub print_directory: bool, + + /// Do not print the working directory when starting, even when running with -C or recursively. + #[cfg(feature = "full")] + #[clap( + long, + overrides_with = "print_directory", + overrides_with = "no_print_directory" + )] + pub no_print_directory: bool, + /// Target names or macro definitions. /// /// If no target is specified, while make is processing the makefiles, the first @@ -137,14 +159,13 @@ impl Args { // POSIX spec says "Any options specified in the MAKEFLAGS environment variable // shall be evaluated before any options specified on the make utility command // line." - // TODO allow macro definitions in MAKEFLAGS // POSIX says we have to accept // > The characters are option letters without the leading <hyphen-minus> // > characters or <blank> separation used on a make utility command line. let makeflags_given = !env_makeflags.is_empty(); let makeflags_spaces = env_makeflags.contains(' '); let makeflags_leading_dash = env_makeflags.starts_with('-'); - let makeflags_has_equals = env_makeflags.starts_with('='); + let makeflags_has_equals = env_makeflags.contains('='); let makeflags_obviously_full = makeflags_spaces || makeflags_leading_dash || makeflags_has_equals; let env_makeflags = if makeflags_given && !makeflags_obviously_full { @@ -152,16 +173,17 @@ impl Args { } else { env_makeflags }; - let env_makeflags = env_makeflags.split_whitespace().map(OsString::from); + let env_makeflags = shlex::split(&env_makeflags) + .expect("Bad args?") + .into_iter() + .map(OsString::from); // per the structopt docs, the first argument will be used as the binary name, // so we need to make sure it goes in before MAKEFLAGS let arg_0 = args.next().unwrap_or_else(|| env!("CARGO_PKG_NAME").into()); - let args = iter::once(arg_0) - .chain(env_makeflags.into_iter()) - .chain(args); + let args = iter::once(arg_0).chain(env_makeflags).chain(args); - Self::from_iter(args) + Self::parse_from(args) } pub fn from_env_and_args() -> Self { @@ -192,37 +214,66 @@ impl Args { } pub fn makeflags(&self) -> String { - let mut result = String::new(); + let mut flags = String::new(); if self.environment_overrides { - result.push('e'); + flags.push('e'); } if self.ignore_errors { - result.push('i'); + flags.push('i'); } if self.keep_going { - result.push('k'); + flags.push('k'); } if self.dry_run { - result.push('n'); + flags.push('n'); } if self.print_everything { - result.push('p'); + flags.push('p'); } if self.question { - result.push('q'); + flags.push('q'); } if self.no_builtin_rules { - result.push('r'); + flags.push('r'); } if self.no_keep_going { - result.push('S'); + flags.push('S'); } if self.silent { - result.push('s'); + flags.push('s'); } if self.touch { - result.push('t'); + flags.push('t'); } + + let macros = self + .targets_or_macros + .iter() + .map(|x| shlex::try_quote(x).expect("Bad quoting?")) + .filter(|x| x.contains('=')) + .collect::<Vec<_>>() + .join(" "); + + let mut result = String::new(); + if !flags.is_empty() && !macros.is_empty() { + result.push('-'); + } + result.add_assign(&flags); + + #[cfg(feature = "full")] + if self.no_print_directory { + if !result.is_empty() { + result.push(' '); + } + result.push_str("--no-print-directory"); + } + + // TODO consider -- to separate flags from macros - GNU does it but it would require + // gnarly splicing to not override recursive macros + if !result.is_empty() && !macros.is_empty() { + result += " "; + } + result.add_assign(¯os); result } } @@ -232,6 +283,12 @@ mod test { use super::*; #[test] + fn clap_validate() { + use clap::CommandFactory; + Args::command().debug_assert(); + } + + #[test] fn no_args() { let args: Vec<OsString> = vec!["makers".into()]; let args = Args::from_given_args_and_given_env(args.into_iter(), String::new()); @@ -251,6 +308,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -279,6 +340,10 @@ mod test { touch: true, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec!["bar".into(), "baz=yeet".into()], } ); @@ -307,6 +372,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -335,6 +404,43 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, + targets_or_macros: vec![], + } + ); + } + + #[test] + #[cfg(feature = "full")] + fn print_directory_wrestling() { + let args = "makers -w --no-print-directory -w -w --no-print-directory --no-print-directory -w --no-print-directory"; + let args = Args::from_given_args_and_given_env( + args.split_whitespace().map(OsString::from), + String::new(), + ); + assert_eq!( + args, + Args { + environment_overrides: false, + makefile: vec![], + ignore_errors: false, + keep_going: false, + dry_run: false, + print_everything: false, + question: false, + no_builtin_rules: false, + no_keep_going: false, + silent: false, + touch: false, + #[cfg(feature = "full")] + directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: true, targets_or_macros: vec![], } ); @@ -361,6 +467,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -387,6 +497,10 @@ mod test { touch: false, #[cfg(feature = "full")] directory: None, + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, targets_or_macros: vec![], } ); @@ -394,7 +508,7 @@ mod test { #[test] fn nightmare() { - let makeflags = "-nrs -k foo=bar"; + let makeflags = "-nrs -k -- foo=bar"; let args = "makers -eipqtSf foo -f bruh bar baz=yeet"; let args = Args::from_given_args_and_given_env( args.split_whitespace().map(OsString::from), @@ -416,8 +530,36 @@ mod test { touch: true, #[cfg(feature = "full")] directory: None, - targets_or_macros: vec!["foo=bar".into(), "bar".into(), "baz=yeet".into()], + #[cfg(feature = "full")] + print_directory: false, + #[cfg(feature = "full")] + no_print_directory: false, + targets_or_macros: vec!["bar".into(), "baz=yeet".into(), "foo=bar".into()], } ); + assert_eq!(args.makeflags(), "-einpqrSst -- baz=yeet foo=bar"); + } + + #[cfg(feature = "full")] + #[test] + fn makeflags_no_print_directory() { + let args = Args { + environment_overrides: false, + makefile: vec![], + ignore_errors: false, + keep_going: false, + dry_run: false, + print_everything: false, + question: false, + no_builtin_rules: false, + no_keep_going: false, + silent: false, + touch: false, + directory: None, + print_directory: false, + no_print_directory: true, + targets_or_macros: vec!["V=1".into()], + }; + assert_eq!(args.makeflags(), "--no-print-directory 'V=1'"); } } diff --git a/src/main.rs b/src/main.rs index 8fa4efd..0b3e9f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ clippy::nursery, clippy::str_to_string, clippy::unwrap_used, - clippy::integer_arithmetic, + clippy::arithmetic_side_effects, clippy::panic, clippy::unimplemented, clippy::todo, @@ -32,7 +32,7 @@ mod args; mod makefile; use args::Args; -use makefile::{Makefile, MakefileReader}; +use makefile::{MacroScopeStack, MacroSet, Makefile, MakefileReader}; const DEFAULT_PATHS: &[&str] = &[ #[cfg(feature = "full")] @@ -43,7 +43,7 @@ const DEFAULT_PATHS: &[&str] = &[ fn main() -> Result<()> { env_logger::init(); - jane_eyre::install()?; + color_eyre::install()?; let mut args = Args::from_env_and_args(); #[cfg(feature = "full")] @@ -66,15 +66,16 @@ fn main() -> Result<()> { let mut makefile = Makefile::new(&args); let paths = Default::default(); for filename in &args.makefile { + let stack = MacroScopeStack::default().with_scope(&makefile.macros); if filename == &PathBuf::from("-") { - let macros = makefile.macros.with_overlay(); - let file = MakefileReader::read(&args, macros, stdin().lock(), "-", Rc::clone(&paths))? - .finish(); + let macros = MacroSet::new(); + let file = + MakefileReader::read(&args, stack, macros, stdin().lock(), "-", Rc::clone(&paths))? + .finish(); makefile.extend(file)?; } else { - let macros = makefile.macros.with_overlay(); let file = - MakefileReader::read_file(&args, macros, filename, Rc::clone(&paths))?.finish(); + MakefileReader::read_file(&args, stack, filename, Rc::clone(&paths))?.finish(); makefile.extend(file)?; }; } @@ -92,9 +93,8 @@ fn main() -> Result<()> { for outdated in makefiles_outdated { eprintln!("makefile {} out of date, rebuilding", outdated); makefile.update_target(&outdated)?; - let macros = makefile.macros.with_overlay(); - let file = - MakefileReader::read_file(&args, macros, &outdated, Default::default())?.finish(); + let stack = MacroScopeStack::default().with_scope(&makefile.macros); + let file = MakefileReader::read_file(&args, stack, &outdated, Default::default())?.finish(); // TODO forget the stale data // TODO reread all the things, not just this one makefile.extend(file)?; diff --git a/src/makefile/command_line.rs b/src/makefile/command_line.rs index 7d4915d..9641941 100644 --- a/src/makefile/command_line.rs +++ b/src/makefile/command_line.rs @@ -3,9 +3,12 @@ use std::fmt; use std::process::{Command, ExitStatus}; use eyre::{bail, Error}; +#[cfg(feature = "full")] use lazy_static::lazy_static; +#[cfg(feature = "full")] use regex::Regex; +#[cfg(feature = "full")] use super::r#macro::Set as MacroSet; use super::target::Target; use super::token::{Token, TokenString}; @@ -15,7 +18,7 @@ use super::Makefile; fn execute_command_line( command_line: &str, ignore_errors: bool, - macros: &MacroSet, + #[cfg(feature = "full")] macros: &MacroSet, ) -> Result<ExitStatus, Error> { let (program, args) = if cfg!(windows) { let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()); @@ -33,50 +36,18 @@ fn execute_command_line( let mut command = Command::new(program); command.args(args); #[cfg(feature = "full")] - command.envs(macros.resolve_exports()?); + command.envs(macros.resolve_exports::<&[u8]>(None)?); Ok(command.status()?) } #[derive(PartialEq, Eq, Clone, Debug)] pub struct CommandLine { - /// If the command prefix contains a <hyphen-minus>, or the -i option is present, or - /// the special target .IGNORE has either the current target as a prerequisite or has - /// no prerequisites, any error found while executing the command shall be ignored. - ignore_errors: bool, - /// If the command prefix contains an at-sign and the make utility command line -n - /// option is not specified, or the -s option is present, or the special target - /// .SILENT has either the current target as a prerequisite or has no prerequisites, - /// the command shall not be written to standard output before it is executed. - silent: bool, - /// If the command prefix contains a <plus-sign>, this indicates a makefile command - /// line that shall be executed even if -n, -q, or -t is specified. - always_execute: bool, execution_line: TokenString, } impl CommandLine { - pub fn from(mut line: TokenString) -> Self { - let mut ignore_errors = false; - let mut silent = false; - let mut always_execute = false; - - if let Token::Text(text) = line.first_token_mut() { - let mut text_chars = text.chars().peekable(); - while let Some(x) = text_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { - match x { - '-' => ignore_errors = true, - '@' => silent = true, - '+' => always_execute = true, - _ => unreachable!(), - } - } - *text = text_chars.collect(); - } - + pub const fn from(line: TokenString) -> Self { Self { - ignore_errors, - silent, - always_execute, execution_line: line, } } @@ -91,14 +62,13 @@ impl CommandLine { #[cfg(feature = "full")] { let is_just_one_macro_expansion = self.execution_line.tokens().count() == 1 - && self.execution_line.tokens().all(|x| match x { - Token::MacroExpansion { .. } => true, - Token::FunctionCall { .. } => true, - _ => false, + && self.execution_line.tokens().all(|x| { + matches!(x, Token::MacroExpansion { .. } | Token::FunctionCall { .. }) }); // unfortunately, if we had a multiline macro somewhere with non-escaped newlines, now we have to run each of them as separate lines lazy_static! { - static ref UNESCAPED_NEWLINE: Regex = Regex::new(r"([^\\])\n").unwrap(); + static ref UNESCAPED_NEWLINE: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"([^\\])\n").unwrap(); } if is_just_one_macro_expansion && UNESCAPED_NEWLINE.is_match(&execution_line) { let lines = UNESCAPED_NEWLINE @@ -111,28 +81,31 @@ impl CommandLine { } } log::trace!("executing {}", &execution_line); - let mut self_ignore_errors = self.ignore_errors; - let mut self_silent = self.silent; - let mut self_always_execute = self.always_execute; + let mut ignore_errors = false; + let mut silent = false; + let mut always_execute = false; - // apparently some makefiles will just throw this shit in in macros? bruh moment tbh + // sometimes this is defined in macros rather than statically let execution_line: String = { - let mut line_chars = execution_line.chars().peekable(); + let mut line_chars = execution_line + .chars() + .skip_while(char::is_ascii_whitespace) + .peekable(); while let Some(x) = line_chars.next_if(|x| matches!(x, '-' | '@' | '+')) { match x { - '-' => self_ignore_errors = true, - '@' => self_silent = true, - '+' => self_always_execute = true, + '-' => ignore_errors = true, + '@' => silent = true, + '+' => always_execute = true, _ => unreachable!(), } } line_chars.collect() }; - let ignore_error = self_ignore_errors + let ignore_error = ignore_errors || file.args.ignore_errors || file.special_target_has_prereq(".IGNORE", &target.name); - let silent = (self_silent && !file.args.dry_run) + let silent = (silent && !file.args.dry_run) || file.args.silent || file.special_target_has_prereq(".SILENT", &target.name); @@ -140,14 +113,19 @@ impl CommandLine { println!("{}", execution_line); } - let should_execute = self_always_execute + let should_execute = always_execute || is_recursive || !(file.args.dry_run || file.args.question || file.args.touch); if !should_execute { return Ok(()); } - let return_value = execute_command_line(&execution_line, ignore_error, &file.macros); + let return_value = execute_command_line( + &execution_line, + ignore_error, + #[cfg(feature = "full")] + &file.macros, + ); let errored = return_value.map_or(true, |status| !status.success()); if errored { // apparently there was an error. do we care? @@ -162,15 +140,6 @@ impl CommandLine { impl fmt::Display for CommandLine { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if self.ignore_errors { - write!(f, "-")?; - } - if self.silent { - write!(f, "@")?; - } - if self.always_execute { - write!(f, "+")?; - } let execution_line = format!("{}", &self.execution_line); let execution_line = execution_line.replace("\n", "↵\n"); write!(f, "{}", execution_line)?; diff --git a/src/makefile/conditional.rs b/src/makefile/conditional.rs index 98400e6..6eed14a 100644 --- a/src/makefile/conditional.rs +++ b/src/makefile/conditional.rs @@ -99,7 +99,7 @@ fn decode_condition_args(line_body: &str) -> Option<(TokenString, TokenString)> impl Line { pub fn from( line: &str, - expand_macro: impl Fn(&TokenString) -> Result<String>, + mut expand_macro: impl FnMut(&TokenString) -> Result<String>, ) -> Result<Option<Self>> { let line = line.trim_start(); Ok(Some(if let Some(line) = line.strip_prefix("ifeq ") { @@ -134,7 +134,7 @@ impl Line { &self, current_state: Option<&State>, is_macro_defined: impl Fn(&str) -> bool, - expand_macro: impl Fn(&TokenString) -> Result<String>, + mut expand_macro: impl FnMut(&TokenString) -> Result<String>, ) -> Result<StateAction> { Ok(match self { Self::IfEqual(arg1, arg2) => { diff --git a/src/makefile/eval_context.rs b/src/makefile/eval_context.rs new file mode 100644 index 0000000..87edafd --- /dev/null +++ b/src/makefile/eval_context.rs @@ -0,0 +1,53 @@ +use eyre::{Result, WrapErr}; +use std::io::{BufRead, Cursor}; +use std::rc::Rc; + +use super::{FinishedMakefileReader, MacroSet, MakefileReader}; + +pub struct DeferredEvalContext<'parent, 'args, 'grandparent, R: BufRead> { + parent: &'parent MakefileReader<'args, 'grandparent, R>, + children: Vec<FinishedMakefileReader>, +} + +impl<'parent, 'args, 'grandparent, R: BufRead> + DeferredEvalContext<'parent, 'args, 'grandparent, R> +{ + pub const fn new(parent: &'parent MakefileReader<'args, 'grandparent, R>) -> Self { + Self { + parent, + children: Vec::new(), + } + } + + pub fn push(&mut self, child: FinishedMakefileReader) { + self.children.push(child); + } + + pub fn eval(&mut self, to_eval: String) -> Result<()> { + let child_stack = self.parent.stack.with_scope(&self.parent.macros); + let child_macros = MacroSet::new(); + let child = MakefileReader::read( + self.parent.args, + child_stack, + child_macros, + Cursor::new(to_eval), + "<eval>", + Rc::clone(&self.parent.file_names), + ) + .context("while evaling")? + .finish(); + self.push(child); + Ok(()) + } +} + +impl<'parent, 'args, 'grandparent, R: BufRead> IntoIterator + for DeferredEvalContext<'parent, 'args, 'grandparent, R> +{ + type Item = FinishedMakefileReader; + type IntoIter = std::vec::IntoIter<Self::Item>; + + fn into_iter(self) -> Self::IntoIter { + self.children.into_iter() + } +} diff --git a/src/makefile/functions.rs b/src/makefile/functions.rs index a78d582..a6a2db1 100644 --- a/src/makefile/functions.rs +++ b/src/makefile/functions.rs @@ -1,128 +1,142 @@ -use std::cell::RefCell; use std::env; +use std::io::BufRead; use std::process::{Command, Stdio}; -use std::rc::Rc; use eyre::{bail, Result, WrapErr}; +use super::eval_context::DeferredEvalContext; use super::pattern::r#match; -use super::r#macro::{Macro, Set as MacroSet}; -use super::token::TokenString; -use super::ItemSource; +use super::r#macro::Macro; +use super::{ItemSource, MacroScopeStack, MacroSet, TokenString}; +pub const NO_EVAL: Option<&mut DeferredEvalContext<&[u8]>> = None; + +#[allow(clippy::cognitive_complexity)] pub fn expand_call( name: &str, args: &[TokenString], - macros: &MacroSet, - to_eval: Option<Rc<RefCell<Vec<String>>>>, + stack: &MacroScopeStack, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { match name { "subst" => { assert_eq!(args.len(), 3); - text::subst(macros, &args[0], &args[1], &args[2]) + text::subst(stack, &args[0], &args[1], &args[2], eval_context) } "patsubst" => { assert_eq!(args.len(), 3); - text::patsubst(macros, &args[0], &args[1], &args[2]) + text::patsubst(stack, &args[0], &args[1], &args[2], eval_context) } "strip" => { assert_eq!(args.len(), 1); - text::strip(macros, &args[0]) + text::strip(stack, &args[0], eval_context) } "findstring" => { assert_eq!(args.len(), 2); - text::findstring(macros, &args[0], &args[1]) + text::findstring(stack, &args[0], &args[1], eval_context) } "filter" => { assert_eq!(args.len(), 2); - text::filter(macros, &args[0], &args[1]) + text::filter(stack, &args[0], &args[1], eval_context) } "filter-out" => { assert_eq!(args.len(), 2); - text::filter_out(macros, &args[0], &args[1]) + text::filter_out(stack, &args[0], &args[1], eval_context) } "sort" => { assert_eq!(args.len(), 1); - text::sort(macros, &args[0]) + text::sort(stack, &args[0], eval_context) } "word" => { assert_eq!(args.len(), 2); - text::word(macros, &args[0], &args[1]) + text::word(stack, &args[0], &args[1], eval_context) } "words" => { assert_eq!(args.len(), 1); - text::words(macros, &args[0]) + text::words(stack, &args[0], eval_context) } "firstword" => { assert_eq!(args.len(), 1); - text::firstword(macros, &args[0]) + text::firstword(stack, &args[0], eval_context) } "lastword" => { assert_eq!(args.len(), 1); - text::lastword(macros, &args[0]) + text::lastword(stack, &args[0], eval_context) } "dir" => { assert_eq!(args.len(), 1); - file_name::dir(macros, &args[0]) + file_name::dir(stack, &args[0], eval_context) } "notdir" => { assert_eq!(args.len(), 1); - file_name::notdir(macros, &args[0]) + file_name::notdir(stack, &args[0], eval_context) } "basename" => { assert_eq!(args.len(), 1); - file_name::basename(macros, &args[0]) + file_name::basename(stack, &args[0], eval_context) } "addsuffix" => { assert_eq!(args.len(), 2); - file_name::addsuffix(macros, &args[0], &args[1]) + file_name::addsuffix(stack, &args[0], &args[1], eval_context) } "addprefix" => { assert_eq!(args.len(), 2); - file_name::addprefix(macros, &args[0], &args[1]) + file_name::addprefix(stack, &args[0], &args[1], eval_context) } "wildcard" => { assert_eq!(args.len(), 1); - file_name::wildcard(macros, &args[0]) + file_name::wildcard(stack, &args[0], eval_context) } "realpath" => { assert_eq!(args.len(), 1); - file_name::realpath(macros, &args[0]) + file_name::realpath(stack, &args[0], eval_context) } "abspath" => { assert_eq!(args.len(), 1); - file_name::abspath(macros, &args[0]) + file_name::abspath(stack, &args[0], eval_context) } "if" => { assert!(args.len() == 2 || args.len() == 3); - conditional::r#if(macros, &args[0], &args[1], args.get(2)) + conditional::r#if(stack, &args[0], &args[1], args.get(2), eval_context) } "or" => { assert!(!args.is_empty()); - conditional::or(macros, args.iter()) + conditional::or(stack, args.iter(), eval_context) } "and" => { assert!(!args.is_empty()); - conditional::and(macros, args.iter()) + conditional::and(stack, args.iter(), eval_context) + } + "intcmp" => { + assert!(2 <= args.len() && args.len() <= 5); + conditional::intcmp( + stack, + &args[0], + &args[1], + args.get(2), + args.get(3), + args.get(4), + eval_context, + ) } "foreach" => { assert_eq!(args.len(), 3); - foreach(macros, &args[0], &args[1], &args[2]) + foreach(stack, &args[0], &args[1], &args[2], eval_context) } "call" => { assert!(!args.is_empty()); - call(macros, args.iter()) + call(stack, args.iter(), eval_context) } "eval" => { assert_eq!(args.len(), 1); - let should_eval = eval(macros, &args[0])?; - if let Some(to_eval) = to_eval { - to_eval.borrow_mut().push(should_eval); + let should_eval = eval(stack, &args[0], eval_context.as_deref_mut())?; + if let Some(eval_context) = eval_context { + eval_context.eval(should_eval)?; } else { bail!("tried to eval something but no eval back-channel was available"); } @@ -131,17 +145,17 @@ pub fn expand_call( "origin" => { assert_eq!(args.len(), 1); - origin(macros, &args[0]) + origin(stack, &args[0], eval_context) } "error" => { assert_eq!(args.len(), 1); - meta::error(macros, &args[0]) + meta::error(stack, &args[0], eval_context) } "shell" => { assert_eq!(args.len(), 1); - shell(macros, &args[0]) + shell(stack, &args[0], eval_context) } // fallback @@ -154,54 +168,58 @@ mod text { use super::*; pub fn subst( - macros: &MacroSet, + stack: &MacroScopeStack, from: &TokenString, to: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let from = macros.expand(from)?; - let to = macros.expand(to)?; - let text = macros.expand(text)?; + let from = stack.expand(from, eval_context.as_deref_mut())?; + let to = stack.expand(to, eval_context.as_deref_mut())?; + let text = stack.expand(text, eval_context)?; Ok(text.replace(&from, &to)) } pub fn patsubst( - macros: &MacroSet, + stack: &MacroScopeStack, from: &TokenString, to: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let from = macros.expand(from)?; - let to = macros.expand(to)?; - let text = macros.expand(text)?; - let words = text - .split_whitespace() - .map(|word| { - let pattern_match = r#match(&from, word)?.and_then(|x| x.get(1)); - Ok(if let Some(pm) = pattern_match { - to.replace('%', pm.as_str()) - } else { - word.to_owned() + let from = stack.expand(from, eval_context.as_deref_mut())?; + let to = stack.expand(to, eval_context.as_deref_mut())?; + let text = stack.expand(text, eval_context)?; + let words = + text.split_whitespace() + .map(|word| { + let pattern_match = r#match(&from, word)?.and_then(|x| x.get(1)); + Ok(pattern_match + .map_or_else(|| word.to_owned(), |pm| to.replace('%', pm.as_str()))) }) - }) - .collect::<Result<Vec<_>>>()?; + .collect::<Result<Vec<_>>>()?; Ok(words.join(" ")) } - pub fn strip(macros: &MacroSet, text: &TokenString) -> Result<String> { - let text = macros.expand(text)?; + pub fn strip( + stack: &MacroScopeStack, + text: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let text = stack.expand(text, eval_context)?; // TODO don't allocate this vec let words = text.split_whitespace().collect::<Vec<_>>(); Ok(words.join(" ")) } pub fn findstring( - macros: &MacroSet, + stack: &MacroScopeStack, needle: &TokenString, haystack: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let needle = macros.expand(needle)?; - let haystack = macros.expand(haystack)?; + let needle = stack.expand(needle, eval_context.as_deref_mut())?; + let haystack = stack.expand(haystack, eval_context)?; if haystack.contains(&needle) { Ok(needle) } else { @@ -209,10 +227,15 @@ mod text { } } - pub fn filter(macros: &MacroSet, patterns: &TokenString, text: &TokenString) -> Result<String> { - let patterns = macros.expand(patterns)?; + pub fn filter( + stack: &MacroScopeStack, + patterns: &TokenString, + text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let patterns = stack.expand(patterns, eval_context.as_deref_mut())?; let patterns = patterns.split_whitespace().collect::<Vec<_>>(); - let text = macros.expand(text)?; + let text = stack.expand(text, eval_context)?; let text = text.split_whitespace(); let mut result_pieces = vec![]; for word in text { @@ -227,13 +250,14 @@ mod text { } pub fn filter_out( - macros: &MacroSet, + stack: &MacroScopeStack, patterns: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let patterns = macros.expand(patterns)?; + let patterns = stack.expand(patterns, eval_context.as_deref_mut())?; let patterns = patterns.split_whitespace().collect::<Vec<_>>(); - let text = macros.expand(text)?; + let text = stack.expand(text, eval_context)?; let text = text.split_whitespace(); let mut result_pieces = vec![]; for word in text { @@ -247,18 +271,27 @@ mod text { Ok(result_pieces.join(" ")) } - pub fn sort(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn sort( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let mut words = words.split_whitespace().collect::<Vec<_>>(); words.sort_unstable(); words.dedup(); Ok(words.join(" ")) } - pub fn word(macros: &MacroSet, n: &TokenString, text: &TokenString) -> Result<String> { - let n = macros.expand(n)?; + pub fn word( + stack: &MacroScopeStack, + n: &TokenString, + text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let n = stack.expand(n, eval_context.as_deref_mut())?; let n: usize = n.parse().wrap_err("while calling `word`")?; - let text = macros.expand(text)?; + let text = stack.expand(text, eval_context)?; Ok(text .split_whitespace() .nth(n.saturating_add(1)) @@ -266,18 +299,30 @@ mod text { .to_owned()) } - pub fn words(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn words( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; Ok(words.split_whitespace().count().to_string()) } - pub fn firstword(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; - Ok(words.split_whitespace().nth(0).unwrap_or("").to_owned()) + pub fn firstword( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; + Ok(words.split_whitespace().next().unwrap_or("").to_owned()) } - pub fn lastword(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn lastword( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; Ok(words.split_whitespace().last().unwrap_or("").to_owned()) } } @@ -287,14 +332,19 @@ mod file_name { use std::env; use std::ffi::OsStr; use std::fs; + use std::io::BufRead; use std::path::{Path, MAIN_SEPARATOR}; - use eyre::WrapErr; - use super::*; + use crate::makefile::eval_context::DeferredEvalContext; + use eyre::WrapErr; - pub fn dir(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn dir( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { @@ -309,8 +359,12 @@ mod file_name { Ok(words.join(" ")) } - pub fn notdir(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn notdir( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { @@ -323,8 +377,12 @@ mod file_name { Ok(words.join(" ")) } - pub fn basename(macros: &MacroSet, words: &TokenString) -> Result<String> { - let words = macros.expand(words)?; + pub fn basename( + stack: &MacroScopeStack, + words: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let words = stack.expand(words, eval_context)?; let words = words .split_whitespace() .map(|word| { @@ -338,12 +396,13 @@ mod file_name { } pub fn addsuffix( - macros: &MacroSet, + stack: &MacroScopeStack, suffix: &TokenString, targets: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let suffix = macros.expand(suffix)?; - let targets = macros.expand(targets)?; + let suffix = stack.expand(suffix, eval_context.as_deref_mut())?; + let targets = stack.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", t, suffix)) @@ -352,12 +411,13 @@ mod file_name { } pub fn addprefix( - macros: &MacroSet, + stack: &MacroScopeStack, prefix: &TokenString, targets: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let prefix = macros.expand(prefix)?; - let targets = macros.expand(targets)?; + let prefix = stack.expand(prefix, eval_context.as_deref_mut())?; + let targets = stack.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|t| format!("{}{}", prefix, t)) @@ -365,8 +425,12 @@ mod file_name { Ok(results.join(" ")) } - pub fn wildcard(macros: &MacroSet, pattern: &TokenString) -> Result<String> { - let pattern = macros.expand(pattern)?; + pub fn wildcard( + stack: &MacroScopeStack, + pattern: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let pattern = stack.expand(pattern, eval_context)?; let home_dir = env::var("HOME") .ok() .or_else(|| dirs::home_dir().and_then(|p| p.to_str().map(String::from))); @@ -385,8 +449,12 @@ mod file_name { Ok(results.join(" ")) } - pub fn realpath(macros: &MacroSet, targets: &TokenString) -> Result<String> { - let targets = macros.expand(targets)?; + pub fn realpath( + stack: &MacroScopeStack, + targets: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let targets = stack.expand(targets, eval_context)?; let results = targets .split_whitespace() .map(|x| { @@ -398,43 +466,52 @@ mod file_name { Ok(results.join(" ")) } - pub fn abspath(macros: &MacroSet, targets: &TokenString) -> Result<String> { + pub fn abspath( + stack: &MacroScopeStack, + targets: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { // TODO don't resolve symlinks - realpath(macros, targets) + realpath(stack, targets, eval_context) } } // Functions for Conditionals mod conditional { + use std::borrow::Cow; + use std::cmp::Ordering; + use super::*; + use eyre::eyre; pub fn r#if( - macros: &MacroSet, + stack: &MacroScopeStack, condition: &TokenString, if_true: &TokenString, if_false: Option<&TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { let mut condition = condition.clone(); condition.trim_start(); condition.trim_end(); - let condition = macros.expand(&condition)?; + let condition = stack.expand(&condition, eval_context.as_deref_mut())?; if condition.is_empty() { - if let Some(if_false) = if_false { - macros.expand(if_false) - } else { - Ok(String::new()) - } + if_false.map_or_else( + || Ok(String::new()), + |if_false| stack.expand(if_false, eval_context), + ) } else { - macros.expand(if_true) + stack.expand(if_true, eval_context) } } pub fn or<'a>( - macros: &MacroSet, + stack: &MacroScopeStack, args: impl Iterator<Item = &'a TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { for arg in args { - let arg = macros.expand(arg)?; + let arg = stack.expand(arg, eval_context.as_deref_mut())?; if !arg.is_empty() { return Ok(arg); } @@ -443,33 +520,80 @@ mod conditional { } pub fn and<'a>( - macros: &MacroSet, + stack: &MacroScopeStack, args: impl Iterator<Item = &'a TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { let mut last = String::new(); for arg in args { - last = macros.expand(arg)?; + last = stack.expand(arg, eval_context.as_deref_mut())?; if last.is_empty() { return Ok(String::new()); } } Ok(last) } + + pub fn intcmp<'a>( + stack: &MacroScopeStack, + lhs: &TokenString, + rhs: &TokenString, + lt_part: Option<&TokenString>, + eq_part: Option<&TokenString>, + gt_part: Option<&TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let raw_lhs_value = stack.expand(lhs, eval_context.as_deref_mut())?; + let raw_rhs_value = stack.expand(rhs, eval_context.as_deref_mut())?; + let lhs_value: i64 = raw_lhs_value.parse()?; + let rhs_value: i64 = raw_rhs_value.parse()?; + let cmp = lhs_value.cmp(&rhs_value); + + // defaults are a bit of a mess + let mut lt_part = lt_part.map(Cow::Borrowed); + let mut eq_part = eq_part.map(Cow::Borrowed); + let mut gt_part = gt_part.map(Cow::Borrowed); + if lt_part.is_none() && eq_part.is_none() && gt_part.is_none() { + lt_part = Some(Cow::Owned(TokenString::empty())); + // not just reusing lhs param since expansion could have a side effect + eq_part = Some(Cow::Owned(TokenString::text(raw_lhs_value))); + gt_part = Some(Cow::Owned(TokenString::empty())); + } + if eq_part.is_none() { + eq_part = Some(Cow::Owned(TokenString::empty())); + } + if gt_part.is_none() { + gt_part = eq_part.clone(); + } + + let lt_part = lt_part.ok_or_else(|| eyre!("intcmp defaults failed"))?; + let eq_part = eq_part.ok_or_else(|| eyre!("intcmp defaults failed"))?; + let gt_part = gt_part.ok_or_else(|| eyre!("intcmp defaults failed"))?; + + let result = match cmp { + Ordering::Less => lt_part, + Ordering::Equal => eq_part, + Ordering::Greater => gt_part, + }; + + stack.expand(&result, eval_context.as_deref_mut()) + } } pub fn foreach( - macros: &MacroSet, + stack: &MacroScopeStack, var: &TokenString, list: &TokenString, text: &TokenString, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, ) -> Result<String> { - let var = macros.expand(var)?; - let list = macros.expand(list)?; + let var = stack.expand(var, eval_context.as_deref_mut())?; + let list = stack.expand(list, eval_context.as_deref_mut())?; let words = list.split_whitespace(); - let mut macros = macros.with_overlay(); let results = words .map(|word| { + let mut macros = MacroSet::new(); macros.set( var.clone(), Macro { @@ -479,20 +603,26 @@ pub fn foreach( eagerly_expanded: false, }, ); - macros.expand(text) + stack + .with_scope(¯os) + .expand(text, eval_context.as_deref_mut()) }) .collect::<Result<Vec<_>, _>>()?; Ok(results.join(" ")) } -pub fn call<'a>(macros: &MacroSet, args: impl Iterator<Item = &'a TokenString>) -> Result<String> { +pub fn call<'a>( + stack: &MacroScopeStack, + args: impl Iterator<Item = &'a TokenString>, + mut eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { let args = args - .map(|arg| macros.expand(arg)) + .map(|arg| stack.expand(arg, eval_context.as_deref_mut())) .collect::<Result<Vec<_>, _>>()?; let function = args[0].clone(); // TODO if function is a builtin, call the builtin instead - let mut macros = macros.with_overlay(); + let mut macros = MacroSet::new(); for (i, x) in args.into_iter().enumerate() { macros.set( i.to_string(), @@ -504,31 +634,49 @@ pub fn call<'a>(macros: &MacroSet, args: impl Iterator<Item = &'a TokenString>) }, ); } - macros.expand(&TokenString::r#macro(function)) + stack + .with_scope(¯os) + .expand(&TokenString::r#macro(function), eval_context) } // TODO consider bringing eval logic in here since we put the Vec in MacroSet IIRC -pub fn eval(macros: &MacroSet, arg: &TokenString) -> Result<String> { - macros.expand(arg) +pub fn eval( + stack: &MacroScopeStack, + arg: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { + stack.expand(arg, eval_context) } -pub fn origin(macros: &MacroSet, variable: &TokenString) -> Result<String> { - let variable = macros.expand(variable)?; - Ok(macros.origin(&variable).to_owned()) +pub fn origin( + stack: &MacroScopeStack, + variable: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { + let variable = stack.expand(variable, eval_context)?; + Ok(stack.origin(&variable).to_owned()) } mod meta { use super::*; - pub fn error(macros: &MacroSet, text: &TokenString) -> Result<String> { - let text = macros.expand(text)?; + pub fn error( + stack: &MacroScopeStack, + text: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, + ) -> Result<String> { + let text = stack.expand(text, eval_context)?; bail!("{}", text); } } -pub fn shell(macros: &MacroSet, command: &TokenString) -> Result<String> { +pub fn shell( + stack: &MacroScopeStack, + command: &TokenString, + eval_context: Option<&mut DeferredEvalContext<impl BufRead>>, +) -> Result<String> { // TODO bring this in from command_line - let command = macros.expand(command)?; + let command = stack.expand(command, eval_context)?; let (program, args) = if cfg!(windows) { let cmd = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".into()); let args = vec!["/c", &command]; @@ -557,7 +705,8 @@ mod test { type R = Result<()>; fn call(name: &str, args: &[TokenString], macros: &MacroSet) -> Result<String> { - super::expand_call(name, args, macros, None) + let stack = MacroScopeStack::default().with_scope(macros); + expand_call(name, args, &stack, NO_EVAL) } macro_rules! call { @@ -766,6 +915,148 @@ mod test { } #[test] + fn intcmp() -> R { + assert_eq!( + call( + "intcmp", + &[TokenString::text("1"), TokenString::text("2")], + &MacroSet::new() + )?, + "" + ); + assert_eq!( + call( + "intcmp", + &[TokenString::text("2"), TokenString::text("2")], + &MacroSet::new() + )?, + "2" + ); + + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("1"), + TokenString::text("2"), + TokenString::text("a") + ], + &MacroSet::new() + )?, + "a" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("2"), + TokenString::text("2"), + TokenString::text("a") + ], + &MacroSet::new() + )?, + "" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("3"), + TokenString::text("2"), + TokenString::text("a") + ], + &MacroSet::new() + )?, + "" + ); + + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("1"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b") + ], + &MacroSet::new() + )?, + "a" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("2"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b") + ], + &MacroSet::new() + )?, + "b" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("3"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b") + ], + &MacroSet::new() + )?, + "b" + ); + + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("1"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b"), + TokenString::text("c") + ], + &MacroSet::new() + )?, + "a" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("2"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b"), + TokenString::text("c") + ], + &MacroSet::new() + )?, + "b" + ); + assert_eq!( + call( + "intcmp", + &[ + TokenString::text("3"), + TokenString::text("2"), + TokenString::text("a"), + TokenString::text("b"), + TokenString::text("c") + ], + &MacroSet::new() + )?, + "c" + ); + + Ok(()) + } + + #[test] fn foreach() -> R { let mut macros = MacroSet::new(); macros.set( diff --git a/src/makefile/inference_rules.rs b/src/makefile/inference_rules.rs index 368d72b..f841797 100644 --- a/src/makefile/inference_rules.rs +++ b/src/makefile/inference_rules.rs @@ -1,18 +1,18 @@ -use std::fmt; +use std::collections::HashMap; +use std::{fmt, mem}; -use eyre::{eyre, Result}; -use regex::Captures; - -use super::command_line::CommandLine; use super::pattern::r#match; -use super::ItemSource; +use super::{CommandLine, ItemSource, MacroSet}; +use eyre::{eyre, OptionExt, Result}; +use regex::Captures; -#[derive(PartialEq, Eq, Clone, Debug)] +#[derive(Clone, Debug)] pub struct InferenceRule { pub source: ItemSource, pub products: Vec<String>, pub prerequisites: Vec<String>, pub commands: Vec<CommandLine>, + pub macros: MacroSet, } impl InferenceRule { @@ -22,18 +22,23 @@ impl InferenceRule { s1: String, s2: String, commands: Vec<CommandLine>, + macros: MacroSet, ) -> Self { Self { source, products: vec![format!("%{}", s1)], prerequisites: vec![format!("%{}", s2)], commands, + macros, } } pub fn first_match<'s, 't: 's>(&'s self, target_name: &'t str) -> Result<Option<Captures<'t>>> { self.products .iter() + // TODO find a better way to make the self_subdir_match test pass + .flat_map(|pattern| [pattern.strip_prefix("./"), Some(pattern.as_str())]) + .filter_map(|x| x) .map(|pattern| r#match(pattern, target_name)) .try_fold(None, |x, y| y.map(|y| x.or(y))) } @@ -49,12 +54,39 @@ impl InferenceRule { let capture = self .first_match(target_name)? .ok_or_else(|| eyre!("asked non-matching inference rule for prerequisites"))?; - let percent_expansion = capture.get(1).expect("should've matched the %").as_str(); + let percent_expansion = capture + .get(1) + .ok_or_eyre("should've matched the %")? + .as_str(); Ok(self .prerequisites .iter() .map(move |p| p.replace('%', percent_expansion))) } + + fn extend(&mut self, other: Self) { + assert_eq!(&self.products, &other.products); + match (self.commands.is_empty(), other.commands.is_empty()) { + (false, false) => { + // both rules have commands, so replace this entirely + *self = other; + } + (true, false) => { + // this rule doesn't have commands, but the other one does, + // so it's the real one + let mut other = other; + mem::swap(self, &mut other); + self.extend(other); + } + (false, true) | (true, true) => { + // this rule might have commands, but the other one doesn't, + // so append non-command stuff + // TODO decide something smart about sources + self.prerequisites.extend(other.prerequisites); + self.macros.extend(other.macros); + } + } + } } impl fmt::Display for InferenceRule { @@ -72,11 +104,68 @@ impl fmt::Display for InferenceRule { } } +#[derive(Clone, Default)] +pub struct InferenceRuleSet { + /// Maps from products to a map from prerequisites to rules. + data: HashMap<Vec<String>, HashMap<Vec<String>, InferenceRule>>, +} + +impl InferenceRuleSet { + pub fn get(&self, products: &[String], prerequisites: &[String]) -> Option<&InferenceRule> { + self.data.get(products).and_then(|x| x.get(prerequisites)) + } + + fn get_mut( + &mut self, + products: &[String], + prerequisites: &[String], + ) -> Option<&mut InferenceRule> { + self.data + .get_mut(products) + .and_then(|x| x.get_mut(prerequisites)) + } + + pub fn put(&mut self, rule: InferenceRule) { + if let Some(existing_rule) = self.get_mut(&rule.products, &rule.prerequisites) { + existing_rule.extend(rule); + } else { + self.data + .entry(rule.products.clone()) + .or_default() + .insert(rule.prerequisites.clone(), rule); + } + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn extend(&mut self, other: Self) { + for other in other.data.into_values().flat_map(HashMap::into_values) { + self.put(other); + } + } + + pub fn iter(&self) -> impl Iterator<Item = &InferenceRule> { + self.data.values().flat_map(HashMap::values) + } +} + +impl From<Vec<InferenceRule>> for InferenceRuleSet { + fn from(value: Vec<InferenceRule>) -> Self { + let mut result = Self::default(); + for rule in value { + result.put(rule); + } + result + } +} + #[cfg(test)] mod test { use super::*; - type R = eyre::Result<()>; + type R = Result<()>; #[test] fn suffix_match() -> R { @@ -85,6 +174,7 @@ mod test { ".o".to_owned(), ".c".to_owned(), vec![], + MacroSet::new(), ); assert!(rule.matches("foo.o")?); assert!(rule.matches("dir/foo.o")?); @@ -104,8 +194,38 @@ mod test { "goneall.gpg".to_owned(), ], commands: vec![], + macros: MacroSet::new(), }; assert!(rule.matches("licenseListPublisher-2.2.1.jar-valid")?); Ok(()) } + + #[cfg(feature = "full")] + #[test] + fn subdir_match() -> R { + let rule = InferenceRule { + source: ItemSource::Builtin, + products: vec!["a/%.o".to_owned()], + prerequisites: vec!["a/%.c".to_owned()], + commands: vec![], + macros: MacroSet::new(), + }; + assert!(rule.matches("a/foo.o")?); + Ok(()) + } + + #[cfg(feature = "full")] + #[test] + fn self_subdir_match() -> R { + let rule = InferenceRule { + source: ItemSource::Builtin, + products: vec!["./%.o".to_owned()], + prerequisites: vec!["./%.c".to_owned()], + commands: vec![], + macros: MacroSet::new(), + }; + assert!(rule.matches("foo.o")?); + assert!(rule.matches("./foo.o")?); + Ok(()) + } } diff --git a/src/makefile/input.rs b/src/makefile/input.rs index c2f8e7b..345f465 100644 --- a/src/makefile/input.rs +++ b/src/makefile/input.rs @@ -2,7 +2,7 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::error::Error as StdError; use std::fs::File; -use std::io::{BufRead, BufReader, Cursor, Error as IoError, ErrorKind as IoErrorKind, Lines}; +use std::io::{BufRead, BufReader, Error as IoError, ErrorKind as IoErrorKind, Lines}; use std::iter::Peekable; use std::path::Path; use std::rc::Rc; @@ -13,19 +13,24 @@ use regex::Regex; use crate::args::Args; -use super::command_line::CommandLine; #[cfg(feature = "full")] use super::conditional::{Line as ConditionalLine, State as ConditionalState}; -use super::inference_rules::InferenceRule; +#[cfg(feature = "full")] +use super::eval_context::DeferredEvalContext; +use super::parse::{MacroAssignment, MacroAssignmentOutcome}; #[cfg(feature = "full")] use super::r#macro::ExportConfig; -use super::r#macro::{Macro, Set as MacroSet}; -use super::target::{StaticTargetSet, Target}; -use super::token::{Token, TokenString}; -use super::ItemSource; +use super::r#macro::Macro; +use super::target::StaticTargetSet; +use super::{ + builtin_targets, CommandLine, InferenceRule, InferenceRuleSet, ItemSource, LookupInternal, + MacroScopeStack, MacroSet, Target, TokenString, +}; enum LineType { Rule, + #[cfg(feature = "full")] + RuleMacro, Macro, Include, #[cfg(feature = "full")] @@ -52,32 +57,31 @@ impl LineType { if line_tokens.starts_with("unexport ") || line_tokens == "unexport" { return Self::Unexport; } - for token in line_tokens.tokens() { - if let Token::Text(text) = token { - let colon_idx = text.find(':'); - #[cfg(not(feature = "full"))] - let equals_idx = text.find('='); + let colon_idx = line_tokens.find(":"); + #[cfg(not(feature = "full"))] + let equals_idx = line_tokens.find("="); + #[cfg(feature = "full")] + let equals_idx = ["=", ":=", "::=", "?=", "+="] + .iter() + .filter_map(|p| line_tokens.find(p)) + .min(); + match (colon_idx, equals_idx) { + (Some(_), None) => { + return Self::Rule; + } + (Some(c), Some(e)) if c < e => { #[cfg(feature = "full")] - let equals_idx = ["=", ":=", "::=", "?=", "+="] - .iter() - .filter_map(|p| text.find(p)) - .min(); - match (colon_idx, equals_idx) { - (Some(_), None) => { - return Self::Rule; - } - (Some(c), Some(e)) if c < e => { - return Self::Rule; - } - (None, Some(_)) => { - return Self::Macro; - } - (Some(c), Some(e)) if e <= c => { - return Self::Macro; - } - _ => {} - } + return Self::RuleMacro; + #[cfg(not(feature = "full"))] + return Self::Rule; } + (None, Some(_)) => { + return Self::Macro; + } + (Some(c), Some(e)) if e <= c => { + return Self::Macro; + } + _ => {} } Self::Unknown } @@ -94,9 +98,11 @@ fn inference_match<'a>( prerequisites: &[String], ) -> Option<InferenceMatch<'a>> { lazy_static! { - static ref INFERENCE_RULE: Regex = - Regex::new(r"^(?P<s2>(\.[^/.]+)?)(?P<s1>\.[^/.]+)$").unwrap(); - static ref SPECIAL_TARGET: Regex = Regex::new(r"^\.[A-Z]+$").unwrap(); + static ref INFERENCE_RULE: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"^(?P<s2>(\.[^/.]+)?)(?P<s1>\.[^/.]+)$") + .unwrap(); + static ref SPECIAL_TARGET: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"^\.[A-Z]+$").unwrap(); } let inference_match = INFERENCE_RULE.captures(targets[0]); @@ -107,6 +113,7 @@ fn inference_match<'a>( && inference_match.is_some() && special_target_match.is_none(); if inference_rule { + #[allow(clippy::unwrap_used)] inference_match.map(|x| InferenceMatch { s1: x.name("s1").unwrap().as_str(), s2: x.name("s2").unwrap().as_str(), @@ -126,7 +133,7 @@ where E: StdError + Send + Sync + 'static, Inner: Iterator<Item = Result<T, E>>, { - fn new(inner: Inner) -> Self { + const fn new(inner: Inner) -> Self { Self(inner, 0) } } @@ -181,30 +188,33 @@ impl Default for NextLineSettings { pub struct MakefileReader<'a, 'parent, R: BufRead> { file_name: String, - pub inference_rules: Vec<InferenceRule>, - pub macros: MacroSet<'parent, 'static>, + pub inference_rules: InferenceRuleSet, + pub stack: MacroScopeStack<'parent>, + pub macros: MacroSet, pub targets: StaticTargetSet, built_in_targets: HashMap<String, Target>, pub first_non_special_target: Option<String>, pub failed_includes: Vec<String>, - args: &'a Args, + pub args: &'a Args, lines_iter: Peekable<LineNumbers<String, IoError, Lines<R>>>, // join with escaped_newline_replacement to get the actual line pending_line: Option<(usize, Vec<String>)>, #[cfg(feature = "full")] conditional_stack: Vec<ConditionalState>, - file_names: Rc<RefCell<Vec<String>>>, + pub file_names: Rc<RefCell<Vec<String>>>, } impl<'a, 'parent> MakefileReader<'a, 'parent, BufReader<File>> { pub fn read_file( args: &'a Args, - mut macros: MacroSet<'parent, 'static>, + stack: MacroScopeStack<'parent>, path: impl AsRef<Path>, file_names: Rc<RefCell<Vec<String>>>, ) -> Result<Self> { + let mut macros = MacroSet::new(); #[cfg(feature = "full")] - if let Some(mut old_makefile_list) = macros.pop("MAKEFILE_LIST") { + if let Some(old_makefile_list) = stack.get("MAKEFILE_LIST") { + let mut old_makefile_list = old_makefile_list.into_owned(); old_makefile_list.text.extend(TokenString::text(format!( " {}", path.as_ref().to_string_lossy() @@ -227,14 +237,15 @@ impl<'a, 'parent> MakefileReader<'a, 'parent, BufReader<File>> { // TODO handle errors let file = file.context("couldn't open makefile!")?; let file_reader = BufReader::new(file); - Self::read(args, macros, file_reader, file_name, file_names) + Self::read(args, stack, macros, file_reader, file_name, file_names) } } impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { pub fn read( args: &'a Args, - macros: MacroSet<'parent, 'static>, + stack: MacroScopeStack<'parent>, + macros: MacroSet, source: R, name: impl Into<String>, file_names: Rc<RefCell<Vec<String>>>, @@ -242,7 +253,8 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { let name = name.into(); let mut reader = Self { file_name: name.clone(), - inference_rules: Vec::new(), + inference_rules: InferenceRuleSet::default(), + stack, macros, targets: Default::default(), built_in_targets: HashMap::new(), @@ -257,18 +269,10 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { }; // TODO be smart about this instead, please if !args.no_builtin_rules { - reader.built_in_targets.insert( - ".SUFFIXES".to_owned(), - Target { - name: ".SUFFIXES".into(), - prerequisites: vec![".o", ".c", ".y", ".l", ".a", ".sh", ".f"] - .into_iter() - .map(String::from) - .collect(), - commands: vec![], - stem: None, - already_updated: Cell::new(false), - }, + reader.built_in_targets.extend( + builtin_targets() + .into_iter() + .map(|target| (target.name.clone(), target)), ); } reader @@ -300,30 +304,12 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { let line_type = LineType::of(&line_tokens); // before we actually test it, see if it's only visible after expanding macros - let (line_tokens, line_type) = if let LineType::Unknown = line_type { + let (line_tokens, line_type) = if matches!(line_type, LineType::Unknown) { let line_tokens = TokenString::text( self.expand_macros(&line_tokens) .wrap_err_with(|| format!("while parsing line {}", line_number))? .trim(), ); - // and let's eval whatever bullshit needs evaling - #[cfg(feature = "full")] - { - let eval = self.macros.to_eval.take(); - for eval in eval { - let child_macros = self.macros.with_overlay(); - let child = MakefileReader::read( - self.args, - child_macros, - Cursor::new(eval), - "<eval>", - Rc::clone(&self.file_names), - ) - .context("while evaling")? - .finish(); - self.extend(child); - } - } let line_type = LineType::of(&line_tokens); (line_tokens, line_type) } else { @@ -339,6 +325,16 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { ) })?; } + #[cfg(feature = "full")] + LineType::RuleMacro => { + self.read_rule_macro(line_tokens, line_number) + .wrap_err_with(|| { + format!( + "while parsing rule-specific macro definition starting on line {}", + line_number + ) + })?; + } LineType::Macro => { self.read_macro(line_tokens, line_number) .wrap_err_with(|| { @@ -363,8 +359,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { } else { let exported = if line_tokens.contains_text("=") { // that's an assignment! - let new_macro = self.read_macro(line_tokens, line_number)?; - new_macro + self.read_macro(line_tokens, line_number)? } else { self.expand_macros(&line_tokens)? }; @@ -399,7 +394,8 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { fn next_line(&mut self, settings: NextLineSettings) -> Option<(usize, Result<String>)> { lazy_static! { - static ref COMMENT: Regex = Regex::new(r"(^|[^\\])#.*$").unwrap(); + static ref COMMENT: Regex = #[allow(clippy::unwrap_used)] + Regex::new(r"(^|[^\\])#.*$").unwrap(); } let escaped_newline_replacement = settings.escaped_newline_replacement; if let Some((line_number, line)) = self.pending_line.take() { @@ -443,15 +439,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { Ok(x) => x, Err(err) => return Some((n, Err(err))), }; - let line = if settings.strip_comments { - COMMENT - .replace(&line, "$1") - .replace(r"\#", "#") - .trim_end() - .to_owned() - } else { - line - }; + // TODO strip comments if it's correct to line_pieces.push(line.trim_start().to_owned()); } } @@ -466,15 +454,19 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { Err(err) => return Some((line_number, Err(err))), }; if let Some(line) = cond_line { + let mut deferred_eval_context = DeferredEvalContext::new(self); let action = line .action( self.conditional_stack.last(), - |name| self.macros.is_defined(name), - |t| self.expand_macros(t), + |name| self.stack.with_scope(&self.macros).is_defined(name), + |t| self.expand_macros_deferred_eval(t, &mut deferred_eval_context), ) .wrap_err_with(|| { format!("while applying conditional on line {}", line_number) }); + for child in deferred_eval_context { + self.extend(child); + } let action = match action { Ok(x) => x, Err(err) => return Some((line_number, Err(err))), @@ -520,17 +512,13 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { } fn special_target_has_prereq(&self, target: &str, name: &str, empty_counts: bool) -> bool { - match self - .targets + self.targets .get(target) .or_else(|| self.built_in_targets.get(target)) - { - Some(target) => { + .map_or(false, |target| { (empty_counts && target.prerequisites.is_empty()) || target.prerequisites.iter().any(|e| e == name) - } - None => false, - } + }) } fn read_include(&mut self, mut line_tokens: TokenString, line_number: usize) -> Result<()> { @@ -545,10 +533,10 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { // handles arbitrarily many filenames, and it's not like that's more work for field in fields { log::trace!("{}:{}: including {}", &self.file_name, line_number, field); - let child_macros = self.macros.with_overlay(); + let child_stack = self.stack.with_scope(&self.macros); let child = MakefileReader::read_file( self.args, - child_macros, + child_stack, field, Rc::clone(&self.file_names), ) @@ -605,45 +593,39 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { None => (not_targets, vec![]), }; if prerequisites.contains_text("=") { - log::error!("rule-specific macros are not implemented yet"); - return Ok(()); + bail!("handling rule-specific macro as rule"); } + #[cfg(feature = "full")] + let mut deferred_eval_context = DeferredEvalContext::new(self); let prerequisites = self - .macros - .with_lookup(&|macro_name: &str| { - let macro_pieces = if macro_name.starts_with('@') { - // The $@ shall evaluate to the full target name of the - // current target. - targets.iter() - } else { - bail!("unknown internal macro") - }; - - let macro_pieces = if macro_name.ends_with('D') { - macro_pieces - .map(|x| { - Path::new(x) - .parent() - .ok_or_else(|| eyre!("no parent")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<Vec<String>, _>>()? - } else if macro_name.ends_with('F') { - macro_pieces - .map(|x| { - Path::new(x) - .file_name() - .ok_or_else(|| eyre!("no filename")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<Vec<String>, _>>()? - } else { - macro_pieces.map(|&x| x.to_owned()).collect::<Vec<String>>() - }; - - Ok(macro_pieces.join(" ")) - }) - .expand(&prerequisites)?; + .stack + .with_scope(&self.macros) + .with_scope(&LookupInternal::new_partial(&targets)) + .expand( + &prerequisites, + #[cfg(feature = "full")] + Some(&mut deferred_eval_context), + )?; + // https://www.gnu.org/software/make/manual/html_node/Secondary-Expansion.html + // this is supposed to be deferred but maybe i can get away with this + // TODO move this to run at runtime + #[cfg(feature = "full")] + let prerequisites = if self.targets.has(".SECONDEXPANSION") { + self.stack + .with_scope(&self.macros) + .with_scope(&LookupInternal::new_partial(&targets)) + .expand( + &prerequisites.parse()?, + #[cfg(feature = "full")] + Some(&mut deferred_eval_context), + )? + } else { + prerequisites + }; + #[cfg(feature = "full")] + for child in deferred_eval_context { + self.extend(child); + } let prerequisites = prerequisites .split_whitespace() .map(|x| x.into()) @@ -697,6 +679,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { products: targets.into_iter().map(|x| x.to_owned()).collect(), prerequisites, commands, + macros: MacroSet::new(), }; if let Some(static_targets) = static_targets { @@ -712,12 +695,14 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { .first_match(real_target)? .and_then(|x| x.get(1).map(|x| x.as_str().to_owned())), already_updated: Cell::new(false), + macros: MacroSet::new(), }; self.targets.put(new_target); } } } else { - self.inference_rules.push(new_rule); + log::debug!("pattern-based inference rule defined: {:?}", &new_rule,); + self.inference_rules.put(new_rule); } return Ok(()); } @@ -753,20 +738,17 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { inference_match.s1.to_owned(), inference_match.s2.to_owned(), commands, + MacroSet::new(), ); - log::trace!( + log::debug!( "suffix-based inference rule defined by {:?} - {:?}", &inference_match, &new_rule, ); - self.inference_rules.retain(|existing_rule| { - (&existing_rule.prerequisites, &existing_rule.products) - != (&new_rule.prerequisites, &new_rule.products) - }); - self.inference_rules.push(new_rule); + self.inference_rules.put(new_rule); } else { - log::trace!( + log::debug!( "{}:{}: new target {:?} based on {:?}", &self.file_name, line_number, @@ -784,6 +766,135 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { commands: commands.clone(), stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), + }; + self.targets.put(new_target); + } + } + + Ok(()) + } + + #[cfg(feature = "full")] + fn read_rule_macro(&mut self, line_tokens: TokenString, line_number: usize) -> Result<()> { + let (targets, macro_def) = line_tokens + .split_once(":") + .ok_or_else(|| eyre!("read_rule couldn't find a ':' on line {}", line_number))?; + lazy_static! { + // my kingdom for lookahead + static ref NON_EAGER_EXPANSION_ASSIGNMENT_COLON: Regex = #[allow(clippy::unwrap_used)] Regex::new(":[^:=]").unwrap(); + } + if macro_def.matches_regex(&NON_EAGER_EXPANSION_ASSIGNMENT_COLON) { + bail!("GNUful static patterns not yet implemented in rule-specific macros"); + }; + let targets = self.expand_macros(&targets)?; + let targets = targets.split_whitespace().collect::<Vec<_>>(); + + let (name, value) = macro_def + .split_once("=") + .ok_or_else(|| eyre!("read_macro couldn't find a '=' on line {}", line_number))?; + let macro_assignment = self.parse_macro_assignment(name, value, line_number)?; + + if targets.is_empty() { + return Ok(()); + } + + // we don't know yet if it's a target rule or an inference rule (or a GNUish "pattern rule") + let inference_match = inference_match(&targets, &[]); + let is_pattern = targets.iter().all(|x| x.contains('%')); + + // TODO resolve against existing stack + let mut macro_set = MacroSet::new(); + if let Some(outcome) = self + .check_macro_assignment_outcome(¯o_assignment, self.stack.with_scope(&self.macros)) + { + let (name, value) = self.resolve_macro_value(macro_assignment, outcome, line_number)?; + // TODO trace + macro_set.set(name, value); + } + + if is_pattern { + let new_rule = InferenceRule { + source: ItemSource::File { + name: self.file_name.clone(), + line: line_number, + }, + products: targets.into_iter().map(|x| x.to_owned()).collect(), + prerequisites: vec![], + commands: vec![], + macros: macro_set, + }; + + log::error!( + "{}:{}: inference rule specific macros not yet working", + &self.file_name, + line_number + ); + + self.inference_rules.put(new_rule); + return Ok(()); + } + + // don't interpret things like `.tmp: ; mkdir -p $@` as single-suffix rules + let inference_match = inference_match.and_then(|inference| { + if self.special_target_has_prereq(".SUFFIXES", inference.s1, false) + && (inference.s2.is_empty() + || self.special_target_has_prereq(".SUFFIXES", inference.s2, false)) + { + Some(inference) + } else { + log::info!( + "{}:{}: looks like {:?} is not a suffix rule because .SUFFIXES is {:?}", + &self.file_name, + line_number, + inference, + self.targets + .get(".SUFFIXES") + .or_else(|| self.built_in_targets.get(".SUFFIXES")) + .map(|x| &x.prerequisites) + ); + None + } + }); + + if let Some(inference_match) = inference_match { + let new_rule = InferenceRule::new_suffix( + ItemSource::File { + name: self.file_name.clone(), + line: line_number, + }, + inference_match.s1.to_owned(), + inference_match.s2.to_owned(), + vec![], + macro_set, + ); + log::error!( + "{}:{}: inference rule specific macros not yet working", + &self.file_name, + line_number + ); + + self.inference_rules.put(new_rule); + } else { + log::trace!( + "{}:{}: target {:?} gets macros {:?}", + &self.file_name, + line_number, + &targets, + ¯o_set + ); + for target in targets { + if self.first_non_special_target.is_none() && !target.starts_with('.') { + self.first_non_special_target = Some(target.into()); + } + // TODO handle appending to built-in (it's Complicated) + let new_target = Target { + name: target.into(), + prerequisites: vec![], + commands: vec![], + stem: None, + already_updated: Cell::new(false), + macros: macro_set.clone(), }; self.targets.put(new_target); } @@ -794,7 +905,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { /// If successful, returns the name of the macro which was read. fn read_macro(&mut self, mut line_tokens: TokenString, line_number: usize) -> Result<String> { - let (name, mut value) = if cfg!(feature = "full") && line_tokens.starts_with("define ") { + let (name, value) = if cfg!(feature = "full") && line_tokens.starts_with("define ") { line_tokens.strip_prefix("define "); if line_tokens.ends_with("=") { line_tokens.strip_suffix("="); @@ -822,10 +933,36 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { .split_once("=") .ok_or_else(|| eyre!("read_macro couldn't find a '=' on line {}", line_number))? }; + let macro_assignment = self.parse_macro_assignment(name, value, line_number)?; + let macro_name = macro_assignment.name.clone(); + if let Some(outcome) = self + .check_macro_assignment_outcome(¯o_assignment, self.stack.with_scope(&self.macros)) + { + let (name, value) = self.resolve_macro_value(macro_assignment, outcome, line_number)?; + log::trace!( + "{}:{}: setting macro {} to {:?}", + &self.file_name, + line_number, + &name, + &value + ); + self.macros.set(name, value); + } + Ok(macro_name) + } + + fn parse_macro_assignment( + &mut self, + name: TokenString, + mut value: TokenString, + line_number: usize, + ) -> Result<MacroAssignment> { let name = self.expand_macros(&name)?; - // GNUisms are annoying, but popular + #[cfg(feature = "full")] let mut expand_value = false; + #[cfg(feature = "full")] let mut skip_if_defined = false; + #[cfg(feature = "full")] let mut append = false; #[cfg(feature = "full")] @@ -848,6 +985,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { let name = name.trim(); value.trim_start(); + #[cfg(feature = "full")] let value = if expand_value { TokenString::text( self.expand_macros(&value) @@ -857,73 +995,124 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { value }; - let skipped = match self.macros.get(name) { + Ok(MacroAssignment { + name: name.to_owned(), + value, + #[cfg(feature = "full")] + expand_value, + #[cfg(feature = "full")] + skip_if_defined, + #[cfg(feature = "full")] + append, + }) + } + + /// For aliasing reasons, applying a macro assignment is done in three steps: + /// 1. Determine what the assignment will do, and if it will append, fetch and clone the original value. Reads both self and the stack. + /// 2. Resolve the new value of the macro, eagerly expanding if appending to an eagerly expanded macro. May write to self due to eval, and since the stack will include `&self.macros`, must be separate from 1. + /// 3. Actually perform the assignment in a [MacroSet]. Since the [MacroSet] may be `&self.macros`, must be separate from 1 and 2. + fn check_macro_assignment_outcome( + &self, + macro_assignment: &MacroAssignment, + stack: MacroScopeStack, + ) -> Option<MacroAssignmentOutcome> { + let skipped = match stack.get(¯o_assignment.name).map(|x| x.source.clone()) { // We always let command line or MAKEFLAGS macros override macros from the file. - Some(Macro { - source: ItemSource::CommandLineOrMakeflags, - .. - }) => true, + Some(ItemSource::CommandLineOrMakeflags) => true, // We let environment variables override macros from the file only if the command-line argument to do that was given - Some(Macro { - source: ItemSource::Environment, - .. - }) => self.args.environment_overrides, - Some(_) => skip_if_defined, + Some(ItemSource::Environment) => self.args.environment_overrides, + #[cfg(feature = "full")] + Some(_) => macro_assignment.skip_if_defined, + #[cfg(not(feature = "full"))] + Some(_) => false, None => false, }; if skipped { - return Ok(name.to_owned()); + None + } else { + Some(match stack.get(¯o_assignment.name) { + #[cfg(feature = "full")] + Some(old_value) if macro_assignment.append => { + MacroAssignmentOutcome::AppendedTo(old_value.into_owned()) + } + _ => MacroAssignmentOutcome::Set, + }) } + } - log::trace!( - "{}:{}: setting macro {} to {}", - &self.file_name, - line_number, - name, - &value - ); - - let value = match self.macros.pop(name) { - Some(mut old_value) if append => { + fn resolve_macro_value( + &mut self, + macro_assignment: MacroAssignment, + outcome: MacroAssignmentOutcome, + line_number: usize, + ) -> Result<(String, Macro)> { + match outcome { + MacroAssignmentOutcome::AppendedTo(mut old_value) => { + let value = macro_assignment.value; #[cfg(feature = "full")] let value = if old_value.eagerly_expanded { TokenString::text(self.expand_macros(&value).wrap_err_with(|| { - format!("while defining {} on line {}", name, line_number) + format!( + "while defining {} on line {}", + macro_assignment.name, line_number + ) })?) } else { value }; old_value.text.extend(TokenString::text(" ")); old_value.text.extend(value); - old_value + Ok((macro_assignment.name, old_value)) } - _ => Macro { - source: ItemSource::File { - name: self.file_name.clone(), - line: line_number, + MacroAssignmentOutcome::Set => Ok(( + macro_assignment.name, + Macro { + source: ItemSource::File { + name: self.file_name.clone(), + line: line_number, + }, + text: macro_assignment.value, + #[cfg(feature = "full")] + eagerly_expanded: macro_assignment.expand_value, }, - text: value, - #[cfg(feature = "full")] - eagerly_expanded: expand_value, - }, - }; - self.macros.set(name.into(), value); + )), + } + } - Ok(name.to_owned()) + fn expand_macros(&mut self, text: &TokenString) -> Result<String> { + #[cfg(feature = "full")] + let mut deferred_eval_context = DeferredEvalContext::new(self); + let result = self.expand_macros_deferred_eval( + text, + #[cfg(feature = "full")] + &mut deferred_eval_context, + ); + #[cfg(feature = "full")] + for child in deferred_eval_context { + self.extend(child); + } + result } - fn expand_macros(&self, text: &TokenString) -> Result<String> { - self.macros - .expand(text) + fn expand_macros_deferred_eval( + &self, + text: &TokenString, + #[cfg(feature = "full")] deferred_eval_context: &mut DeferredEvalContext<R>, + ) -> Result<String> { + self.stack + .with_scope(&self.macros) + .expand( + text, + #[cfg(feature = "full")] + Some(deferred_eval_context), + ) .wrap_err_with(|| format!("while expanding \"{}\"", text)) } pub fn finish(self) -> FinishedMakefileReader { FinishedMakefileReader { inference_rules: self.inference_rules, - macros: self.macros.data, - #[cfg(feature = "full")] - macro_exports: self.macros.exported, + macros: self.macros, targets: self.targets.into(), first_non_special_target: self.first_non_special_target, failed_includes: self.failed_includes, @@ -932,11 +1121,7 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { fn extend(&mut self, new: FinishedMakefileReader) { self.inference_rules.extend(new.inference_rules); - self.macros.extend( - new.macros, - #[cfg(feature = "full")] - new.macro_exports, - ); + self.macros.extend(new.macros); for (_, target) in new.targets { self.targets.put(target); } @@ -948,10 +1133,8 @@ impl<'a, 'parent, R: BufRead> MakefileReader<'a, 'parent, R> { } pub struct FinishedMakefileReader { - pub inference_rules: Vec<InferenceRule>, - pub macros: HashMap<String, Macro>, - #[cfg(feature = "full")] - pub macro_exports: ExportConfig, + pub inference_rules: InferenceRuleSet, + pub macros: MacroSet, pub targets: HashMap<String, Target>, pub first_non_special_target: Option<String>, pub failed_includes: Vec<String>, @@ -959,7 +1142,10 @@ pub struct FinishedMakefileReader { #[cfg(test)] mod test { + use std::io::Cursor; + use super::*; + use crate::makefile::token::Token; type R = Result<()>; @@ -977,6 +1163,7 @@ a: $(x) b \\ let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1003,8 +1190,9 @@ worked = perhaps endif "; let args = Args::empty(); - let makefile = MakefileReader::read( + let mut makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1029,6 +1217,7 @@ endif let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1049,8 +1238,9 @@ baz endef "; let args = Args::empty(); - let makefile = MakefileReader::read( + let mut makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1077,8 +1267,9 @@ endif FOO = bar "; let args = Args::empty(); - let makefile = MakefileReader::read( + let mut makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1127,6 +1318,7 @@ clean: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1148,6 +1340,7 @@ info: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1161,7 +1354,8 @@ info: prerequisites: vec!["bar".to_owned(), "baz".to_owned()], commands: vec![], stem: None, - already_updated: Cell::new(false) + already_updated: Cell::new(false), + macros: MacroSet::new(), } ); assert_eq!( @@ -1171,7 +1365,8 @@ info: prerequisites: vec!["test#post".to_owned()], commands: vec![], stem: None, - already_updated: Cell::new(false) + already_updated: Cell::new(false), + macros: MacroSet::new(), } ); assert_eq!( @@ -1181,7 +1376,8 @@ info: prerequisites: vec![], commands: vec![CommandLine::from(TokenString::text("hello # there")),], stem: None, - already_updated: Cell::new(false) + already_updated: Cell::new(false), + macros: MacroSet::new(), } ); @@ -1198,6 +1394,7 @@ cursed: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1222,6 +1419,7 @@ cursed: let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1243,6 +1441,7 @@ test: c let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1263,6 +1462,7 @@ test: c let args = Args::empty(); let makefile = MakefileReader::read( &args, + MacroScopeStack::default(), MacroSet::new(), Cursor::new(file), "", @@ -1270,11 +1470,42 @@ test: c )?; let makefile = makefile.finish(); assert_eq!( - makefile.macros.get("x").map(|x| &x.text), + makefile.macros.get_non_recursive("x").map(|x| &x.text), Some(&TokenString::text("3")) ); assert!( - matches!(makefile.macro_exports, ExportConfig::Only(exported) if exported.contains("x")) + matches!(makefile.macros.exported, ExportConfig::Only(exported) if exported.contains("x")) + ); + Ok(()) + } + + #[cfg(feature = "full")] + #[test] + fn shell_comment() -> R { + let file = r#" +FOO=$(shell \ +echo \ +#abc) + "#; + let args = Args::empty(); + let makefile = MakefileReader::read( + &args, + MacroScopeStack::default(), + MacroSet::new(), + Cursor::new(file), + "", + Default::default(), + )?; + let makefile = makefile.finish(); + assert_eq!( + makefile.macros.get_non_recursive("FOO").map(|x| &x.text), + Some(&TokenString::from(vec![ + Token::Text(String::new()), + Token::FunctionCall { + name: TokenString::text("shell"), + args: vec![TokenString::text("echo #abc")], + }, + ])) ); Ok(()) } diff --git a/src/makefile/lookup_internal.rs b/src/makefile/lookup_internal.rs new file mode 100644 index 0000000..a497ab4 --- /dev/null +++ b/src/makefile/lookup_internal.rs @@ -0,0 +1,164 @@ +use eyre::{bail, eyre, Result}; +use std::cell::RefCell; +use std::path::Path; +use std::rc::Rc; + +use super::target::Target; + +#[derive(Clone)] +pub enum LookupInternal<'a> { + Partial { + targets: &'a Vec<&'a str>, + }, + Complete { + target: Option<&'a Target>, + get_target: &'a dyn Fn(&str) -> Result<Rc<RefCell<Target>>>, + }, +} + +impl<'a> LookupInternal<'a> { + pub const fn new_partial(targets: &'a Vec<&str>) -> Self { + Self::Partial { targets } + } + + pub const fn new( + target: Option<&'a Target>, + get_target: &'a dyn Fn(&str) -> Result<Rc<RefCell<Target>>>, + ) -> Self { + Self::Complete { target, get_target } + } + + pub fn lookup(&self, macro_name: &str) -> Result<String> { + let macro_pieces = match macro_name.chars().next() { + Some('@') => self.target_name()?, + Some('?') => self.newer_prerequisites()?, + Some('<') => self.inference_prerequisite()?, + Some('*') => self.target_stem()?, + #[cfg(feature = "full")] + Some('^') => self.all_prerequisites()?, + _ => bail!("unknown internal macro {}", macro_name), + }; + + let macro_pieces = if macro_name.ends_with('D') { + macro_pieces + .into_iter() + .map(|x| { + Path::new(&x) + .parent() + .ok_or_else(|| eyre!("no parent")) + .map(|x| x.to_string_lossy().into()) + }) + .collect::<Result<_, _>>()? + } else if macro_name.ends_with('F') { + macro_pieces + .into_iter() + .map(|x| { + Path::new(&x) + .file_name() + .ok_or_else(|| eyre!("no filename")) + .map(|x| x.to_string_lossy().into()) + }) + .collect::<Result<_, _>>()? + } else { + macro_pieces + }; + + Ok(macro_pieces.join(" ")) + } + + /// POSIX: The $@ shall evaluate to the full target name of the current target. + fn target_name(&self) -> Result<Vec<String>> { + match self { + Self::Partial { targets } => { + Ok(targets.iter().map(|target| target.to_string()).collect()) + } + Self::Complete { + target: Some(target), + .. + } => Ok(vec![target.name.clone()]), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// POSIX: The $? macro shall evaluate to the list of prerequisites that are newer than the current target. + fn newer_prerequisites(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $? when target not defined"), + Self::Complete { + target: Some(target), + get_target, + } => Ok(target + .prerequisites + .iter() + .filter(|prereq| { + get_target(prereq) + .ok() + .and_then(|prereq| prereq.borrow().newer_than(target)) + .unwrap_or(false) + }) + .cloned() + .collect()), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// POSIX: In an inference rule, the $< macro shall evaluate to the filename whose existence allowed the inference rule to be chosen for the target. In the .DEFAULT rule, the $< macro shall evaluate to the current target name. + /// + /// GNU: The name of the first prerequisite. + fn inference_prerequisite(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $< when target not defined"), + Self::Complete { + target: Some(target), + .. + } => { + // TODO check that exists_but_inferring_anyway won’t break this + Ok(vec![target + .prerequisites + .first() + .cloned() + .unwrap_or_default()]) + } + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// POSIX: The $* macro shall evaluate to the current target name with its suffix deleted. + fn target_stem(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $* when target not defined"), + Self::Complete { + target: Some(target), + .. + } => Ok(vec![target + .stem + .as_ref() + .unwrap_or(&target.name) + .to_owned()]), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } + + /// GNU: The names of all the prerequisites. + #[cfg(feature = "full")] + fn all_prerequisites(&self) -> Result<Vec<String>> { + match self { + Self::Partial { .. } => bail!("can’t expand $^ when target not defined"), + Self::Complete { + target: Some(target), + .. + } => Ok(target.prerequisites.clone()), + Self::Complete { target: None, .. } => { + bail!("tried to expand internal macro with no target") + } + } + } +} diff --git a/src/makefile/macro.rs b/src/makefile/macro.rs index d77557b..d27cbfa 100644 --- a/src/makefile/macro.rs +++ b/src/makefile/macro.rs @@ -1,20 +1,20 @@ -use std::cell::RefCell; use std::collections::HashMap; +#[cfg(feature = "full")] use std::collections::HashSet; use std::env; use std::fmt; -use std::rc::Rc; - -use eyre::{bail, Result, WrapErr}; -#[cfg(not(feature = "full"))] -use regex::Regex; +#[cfg(feature = "full")] +use std::io::BufRead; #[cfg(feature = "full")] -use super::functions; -use super::token::{Token, TokenString}; -use super::ItemSource; +use super::eval_context::DeferredEvalContext; +#[cfg(feature = "full")] +use super::MacroScopeStack; +use super::{ItemSource, TokenString}; +#[cfg(feature = "full")] +use eyre::Result; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Macro { pub source: ItemSource, pub text: TokenString, @@ -22,12 +22,8 @@ pub struct Macro { pub eagerly_expanded: bool, } -pub trait LookupInternal: for<'a> Fn(&'a str) -> Result<String> {} - -impl<F: for<'a> Fn(&'a str) -> Result<String>> LookupInternal for F {} - #[cfg(feature = "full")] -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum ExportConfig { Only(HashSet<String>), AllBut(HashSet<String>), @@ -69,13 +65,6 @@ impl ExportConfig { } } - fn same_type(&self) -> Self { - match self { - Self::Only(_) => Self::only(), - Self::AllBut(_) => Self::all_but(), - } - } - fn should_export(&self, x: &str) -> bool { match self { Self::Only(exported) => exported.contains(x), @@ -84,29 +73,19 @@ impl ExportConfig { } } -#[derive(Clone)] -pub struct Set<'parent, 'lookup> { - parent: Option<&'parent Set<'parent, 'lookup>>, +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Set { pub data: HashMap<String, Macro>, - lookup_internal: Option<&'lookup dyn LookupInternal>, - #[cfg(feature = "full")] - pub to_eval: Rc<RefCell<Vec<String>>>, #[cfg(feature = "full")] pub exported: ExportConfig, - warnings: Rc<RefCell<HashSet<String>>>, } -impl<'parent, 'lookup> Set<'parent, 'lookup> { +impl Set { pub fn new() -> Self { Self { - parent: None, data: HashMap::new(), - lookup_internal: None, - #[cfg(feature = "full")] - to_eval: Rc::new(RefCell::new(Vec::new())), #[cfg(feature = "full")] exported: ExportConfig::only(), - warnings: Default::default(), } } @@ -140,49 +119,18 @@ impl<'parent, 'lookup> Set<'parent, 'lookup> { } } - fn lookup_internal(&self, name: &str) -> Result<String> { - if let Some(lookup) = self.lookup_internal { - lookup(name) - } else if let Some(parent) = self.parent { - parent.lookup_internal(name) - } else { - bail!( - "tried to lookup {:?} but no lookup function is available", - name - ) - } - } - - pub fn get(&self, name: &str) -> Option<&Macro> { - self.data - .get(name) - .or_else(|| self.parent.and_then(|parent| parent.get(name))) + /// To properly process inherited macros, use [MacroScopeStack::get]. + pub fn get_non_recursive(&self, name: &str) -> Option<&Macro> { + self.data.get(name) } pub fn set(&mut self, name: String, r#macro: Macro) { self.data.insert(name, r#macro); } - #[cfg(feature = "full")] - pub fn is_defined(&self, name: &str) -> bool { - self.get(name).map_or(false, |x| !x.text.is_empty()) - } - - // `remove` is fine, but I think for "remove-and-return" `pop` is better - pub fn pop(&mut self, name: &str) -> Option<Macro> { - // TODO figure out a better way to handle inheritance - self.data - .remove(name) - .or_else(|| self.parent.and_then(|p| p.get(name).cloned())) - } - - pub fn extend( - &mut self, - other: HashMap<String, Macro>, - #[cfg(feature = "full")] other_exports: ExportConfig, - ) { + pub fn extend(&mut self, other: Self) { #[cfg(feature = "full")] - match (&mut self.exported, other_exports) { + match (&mut self.exported, other.exported) { (ExportConfig::Only(se), ExportConfig::Only(oe)) => { se.extend(oe); } @@ -190,187 +138,47 @@ impl<'parent, 'lookup> Set<'parent, 'lookup> { sne.extend(one); } (ExportConfig::Only(se), ExportConfig::AllBut(one)) => { - se.extend(other.keys().cloned().filter(|name| !one.contains(name))); + se.extend( + other + .data + .keys() + .filter(|name| !one.contains(*name)) + .cloned(), + ); } (ExportConfig::AllBut(sne), ExportConfig::Only(oe)) => { - sne.extend(other.keys().cloned().filter(|name| !oe.contains(name))); - } - } - self.data.extend(other); - } - - fn warn(&self, text: String) { - if !self.warnings.borrow().contains(&text) { - log::warn!("{}", &text); - self.warnings.borrow_mut().insert(text); - } - } - - pub fn expand(&self, text: &TokenString) -> Result<String> { - let mut result = String::new(); - for token in text.tokens() { - match token { - Token::Text(t) => result.push_str(t), - Token::MacroExpansion { name, replacement } => { - let name = self - .expand(name) - .wrap_err_with(|| format!("while expanding \"{}\"", name))?; - let internal_macro_names = &['@', '?', '<', '*', '^'][..]; - let internal_macro_suffices = &['D', 'F'][..]; - let just_internal = name.len() == 1 && name.starts_with(internal_macro_names); - let suffixed_internal = name.len() == 2 - && name.starts_with(internal_macro_names) - && name.ends_with(internal_macro_suffices); - let macro_value = if just_internal || suffixed_internal { - self.lookup_internal(&name) - .wrap_err_with(|| format!("while expanding $\"{}\"", name))? - } else { - self.get(&name).map_or_else( - || { - self.warn(format!("undefined macro {}", name)); - Ok(String::new()) - }, - |x| { - self.expand(&x.text) - .wrap_err_with(|| format!("while expanding \"{}\"", &x.text)) - }, - )? - }; - let macro_value = match replacement { - Some((subst1, subst2)) => { - let subst1 = self.expand(subst1)?; - #[cfg(feature = "full")] - { - let (subst1, subst2) = if subst1.contains('%') { - (subst1, subst2.clone()) - } else { - let mut real_subst2 = TokenString::text("%"); - real_subst2.extend(subst2.clone()); - (format!("%{}", subst1), real_subst2) - }; - let args = [ - TokenString::text(subst1), - subst2, - TokenString::text(macro_value), - ]; - functions::expand_call( - "patsubst", - &args, - self, - Some(Rc::clone(&self.to_eval)), - )? - } - #[cfg(not(feature = "full"))] - { - let subst1_suffix = regex::escape(&subst1); - let subst1_suffix = - Regex::new(&format!(r"{}(\s|$)", subst1_suffix)) - .context("formed invalid regex somehow")?; - let subst2 = self.expand(subst2)?; - subst1_suffix - .replace_all(¯o_value, |c: ®ex::Captures| { - format!("{}{}", subst2, c.get(1).unwrap().as_str()) - }) - .to_string() - } - } - None => macro_value, - }; - log::trace!( - "expanded {} (from {:?}) into \"{}\"", - token, - self.get(&name).map(|x| &x.source), - ¯o_value - ); - result.push_str(¯o_value); - } - #[cfg(feature = "full")] - Token::FunctionCall { name, args } => { - let name = self.expand(name)?; - let fn_result = - functions::expand_call(&name, args, self, Some(Rc::clone(&self.to_eval)))?; - log::trace!("expanded {} into \"{}\"", token, &fn_result); - result.push_str(&fn_result); - } + sne.extend( + other + .data + .keys() + .filter(|name| !oe.contains(*name)) + .cloned(), + ); } } - Ok(result) + self.data.extend(other.data); } #[cfg(feature = "full")] - pub fn origin(&self, name: &str) -> &'static str { - match self.data.get(name) { - None => self.parent.map_or("undefined", |p| p.origin(name)), - Some(Macro { - source: ItemSource::Builtin, - .. - }) => "default", - Some(Macro { - source: ItemSource::Environment, - .. - }) => "environment", - // TODO figure out when to return "environment override" - Some(Macro { - source: ItemSource::File { .. }, - .. - }) => "file", - Some(Macro { - source: ItemSource::CommandLineOrMakeflags, - .. - }) => "command line", - // TODO handle override - Some(Macro { - source: ItemSource::FunctionCall, - .. - }) => "automatic", - } - } - - pub fn with_lookup<'l, 's: 'l>(&'s self, lookup: &'l dyn LookupInternal) -> Set<'s, 'l> { - Set { - parent: Some(self), - data: HashMap::new(), - lookup_internal: Some(lookup), - #[cfg(feature = "full")] - to_eval: Rc::clone(&self.to_eval), - #[cfg(feature = "full")] - exported: self.exported.same_type(), - warnings: Rc::clone(&self.warnings), - } - } - - pub fn with_overlay<'s>(&'s self) -> Set<'s, 'lookup> { - Set { - parent: Some(self), - data: HashMap::new(), - lookup_internal: None, - #[cfg(feature = "full")] - to_eval: Rc::clone(&self.to_eval), - #[cfg(feature = "full")] - exported: self.exported.same_type(), - warnings: Rc::clone(&self.warnings), - } - } - - #[cfg(feature = "full")] - pub fn resolve_exports(&self) -> Result<Vec<(&str, String)>> { + pub fn resolve_exports<R: BufRead>( + &self, + mut eval_context: Option<&mut DeferredEvalContext<R>>, + ) -> Result<Vec<(&str, String)>> { let own_exports = self .data .iter() .filter(|(name, _)| self.exported.should_export(name)) - .map(|(name, value)| self.expand(&value.text).map(|text| (name.as_ref(), text))) + .map(|(name, value)| { + MacroScopeStack::from_scope(self) + .expand(&value.text, eval_context.as_deref_mut()) + .map(|text| (name.as_ref(), text)) + }) .collect::<Result<Vec<_>>>()?; - Ok(if let Some(parent) = self.parent { - let mut parent_exports = parent.resolve_exports()?; - parent_exports.extend(own_exports); - parent_exports - } else { - own_exports - }) + Ok(own_exports) } } -impl fmt::Display for Set<'_, '_> { +impl fmt::Display for Set { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let pieces = self .data @@ -381,9 +189,23 @@ impl fmt::Display for Set<'_, '_> { } } +impl Default for Set { + fn default() -> Self { + Self::new() + } +} + fn builtins() -> Vec<(&'static str, TokenString)> { // Fuck it, might as well. - macro_rules! handle { + macro_rules! handle_key { + ($key:ident) => { + stringify!($key) + }; + ($key:literal) => { + $key + }; + } + macro_rules! handle_value { ($value:ident) => { stringify!($value).parse().unwrap() }; @@ -395,8 +217,8 @@ fn builtins() -> Vec<(&'static str, TokenString)> { }; } macro_rules! make { - ($($name:ident=$value:tt)+) => {vec![$( - (stringify!($name), handle!($value)) + ($($name:tt=$value:tt)+) => {vec![$( + (handle_key!($name), handle_value!($value)) ),+]}; } @@ -429,12 +251,19 @@ fn builtins() -> Vec<(&'static str, TokenString)> { ARFLAGS="rv" CFLAGS="" FFLAGS="" + + // yes, Linux, this is definitely GNU Make 4.0+ + ".FEATURES"="output-sync" ] } #[cfg(test)] mod test { use super::*; + #[cfg(feature = "full")] + use crate::makefile::functions::NO_EVAL; + use crate::MacroScopeStack; + use eyre::Result; type R = Result<()>; @@ -450,7 +279,14 @@ mod test { eagerly_expanded: false, }, ); - assert_eq!(macros.expand(&"$(oof:;=?)".parse()?)?, "bruh? swag? yeet?"); + assert_eq!( + MacroScopeStack::from_scope(¯os).expand( + &"$(oof:;=?)".parse()?, + #[cfg(feature = "full")] + NO_EVAL + )?, + "bruh? swag? yeet?" + ); Ok(()) } @@ -466,7 +302,10 @@ mod test { eagerly_expanded: false, }, ); - assert_eq!(macros.expand(&"$(m:%=%-objs)".parse()?)?, "conf-objs"); + assert_eq!( + MacroScopeStack::from_scope(¯os).expand(&"$(m:%=%-objs)".parse()?, NO_EVAL)?, + "conf-objs" + ); Ok(()) } } diff --git a/src/makefile/macro_scope.rs b/src/makefile/macro_scope.rs new file mode 100644 index 0000000..c03870f --- /dev/null +++ b/src/makefile/macro_scope.rs @@ -0,0 +1,229 @@ +use std::borrow::Cow; +use std::collections::HashSet; +#[cfg(feature = "full")] +use std::io::BufRead; +use std::iter; +use std::sync::RwLock; + +use eyre::Context; +use lazy_static::lazy_static; +#[cfg(not(feature = "full"))] +use regex::Regex; + +#[cfg(feature = "full")] +use super::eval_context::DeferredEvalContext; +#[cfg(feature = "full")] +use super::functions; +use super::token::Token; +use super::{ItemSource, LookupInternal, Macro, MacroSet, TokenString}; + +pub trait MacroScope { + /// Looks up the macro with the given name and returns it if it exists. + /// + /// Uses [Cow] to allow for lazy macro definitions. + fn get(&self, name: &str) -> Option<Cow<Macro>>; +} + +impl MacroScope for MacroSet { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.get_non_recursive(name).map(Cow::Borrowed) + } +} + +impl<'a> MacroScope for LookupInternal<'a> { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.lookup(name).ok().map(|value| { + Cow::Owned(Macro { + source: ItemSource::Builtin, + text: TokenString::text(value), + #[cfg(feature = "full")] + eagerly_expanded: false, + }) + }) + } +} + +impl<T: MacroScope> MacroScope for Option<&T> { + fn get(&self, name: &str) -> Option<Cow<Macro>> { + self.as_ref().and_then(|value| value.get(name)) + } +} + +// warning on undefined macros is useful but can get repetitive fast +lazy_static! { + static ref WARNINGS_EMITTED: RwLock<HashSet<String>> = Default::default(); +} + +fn warn(text: String) { + let already_warned = WARNINGS_EMITTED + .read() + .map_or(true, |warnings| warnings.contains(&text)); + if !already_warned { + log::warn!("{}", &text); + if let Ok(mut warnings) = WARNINGS_EMITTED.write() { + warnings.insert(text); + } + } +} + +#[derive(Default)] +pub struct MacroScopeStack<'a> { + scopes: Vec<&'a dyn MacroScope>, +} + +impl<'a> MacroScopeStack<'a> { + pub fn new() -> Self { + Self::default() + } + + pub fn from_scope(scope: &'a dyn MacroScope) -> Self { + Self { + scopes: vec![scope], + } + } + + pub fn with_scope(&self, new_scope: &'a dyn MacroScope) -> Self { + Self { + scopes: iter::once(new_scope).chain(self.scopes.clone()).collect(), + } + } + + pub fn get(&self, name: &str) -> Option<Cow<Macro>> { + for scope in &self.scopes { + if let Some(r#macro) = scope.get(name) { + return Some(r#macro); + } + } + None + } + + #[cfg(feature = "full")] + pub fn is_defined(&self, name: &str) -> bool { + self.get(name).map_or(false, |x| !x.text.is_empty()) + } + + pub fn expand<#[cfg(feature = "full")] R: BufRead>( + &self, + text: &TokenString, + #[cfg(feature = "full")] mut eval_context: Option<&mut DeferredEvalContext<R>>, + ) -> eyre::Result<String> { + let mut result = String::new(); + for token in text.tokens() { + match token { + Token::Text(t) => result.push_str(t), + Token::MacroExpansion { name, replacement } => { + let name = self + .expand( + name, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + ) + .wrap_err_with(|| format!("while expanding \"{}\"", name))?; + let macro_value = self.get(&name).map_or_else( + || { + warn(format!("undefined macro {}", name)); + Ok(String::new()) + }, + |x| { + self.expand( + &x.text, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + ) + .wrap_err_with(|| format!("while expanding \"{}\"", &x.text)) + }, + )?; + let macro_value = match replacement { + Some((subst1, subst2)) => { + let subst1 = self.expand( + subst1, + #[cfg(feature = "full")] + eval_context.as_deref_mut(), + )?; + #[cfg(feature = "full")] + { + let (subst1, subst2) = if subst1.contains('%') { + (subst1, subst2.clone()) + } else { + let mut real_subst2 = TokenString::text("%"); + real_subst2.extend(subst2.clone()); + (format!("%{}", subst1), real_subst2) + }; + let args = [ + TokenString::text(subst1), + subst2, + TokenString::text(macro_value), + ]; + functions::expand_call( + "patsubst", + &args, + self, + eval_context.as_deref_mut(), + )? + } + #[cfg(not(feature = "full"))] + { + let subst1_suffix = regex::escape(&subst1); + let subst1_suffix = + Regex::new(&format!(r"{}(\s|$)", subst1_suffix)) + .context("formed invalid regex somehow")?; + let subst2 = self.expand(subst2)?; + subst1_suffix + .replace_all(¯o_value, |c: ®ex::Captures| { + format!("{}{}", subst2, c.get(1).unwrap().as_str()) + }) + .to_string() + } + } + None => macro_value, + }; + log::trace!( + "expanded {} (from {:?}) into \"{}\"", + token, + self.get(&name).map(|x| x.source.clone()), + ¯o_value + ); + result.push_str(¯o_value); + } + #[cfg(feature = "full")] + Token::FunctionCall { name, args } => { + let name = self.expand(name, eval_context.as_deref_mut())?; + let fn_result = + functions::expand_call(&name, args, self, eval_context.as_deref_mut())?; + log::trace!("expanded {} into \"{}\"", token, &fn_result); + result.push_str(&fn_result); + } + } + } + Ok(result) + } + + #[cfg(feature = "full")] + pub fn origin(&self, name: &str) -> &'static str { + match self.get(name).as_deref() { + None => "undefined", + Some(Macro { + source: ItemSource::Builtin, + .. + }) => "default", + Some(Macro { + source: ItemSource::Environment, + .. + }) => "environment", + // TODO figure out when to return "environment override" + Some(Macro { + source: ItemSource::File { .. }, + .. + }) => "file", + Some(Macro { + source: ItemSource::CommandLineOrMakeflags, + .. + }) => "command line", + // TODO handle override + Some(Macro { + source: ItemSource::FunctionCall, + .. + }) => "automatic", + } + } +} diff --git a/src/makefile/mod.rs b/src/makefile/mod.rs index 277fbd3..d746ed1 100644 --- a/src/makefile/mod.rs +++ b/src/makefile/mod.rs @@ -8,10 +8,15 @@ use std::rc::Rc; use eyre::{bail, eyre, Result, WrapErr}; use command_line::CommandLine; -use inference_rules::InferenceRule; +#[cfg(feature = "full")] +use functions::NO_EVAL; +use inference_rules::{InferenceRule, InferenceRuleSet}; use input::FinishedMakefileReader; pub use input::MakefileReader; -use r#macro::{Macro, Set as MacroSet}; +use lookup_internal::LookupInternal; +pub use macro_scope::MacroScopeStack; +use r#macro::Macro; +pub use r#macro::Set as MacroSet; use target::{DynamicTargetSet, Target}; use token::TokenString; @@ -21,27 +26,36 @@ mod command_line; #[cfg(feature = "full")] mod conditional; #[cfg(feature = "full")] +mod eval_context; +#[cfg(feature = "full")] mod functions; mod inference_rules; mod input; +mod lookup_internal; mod r#macro; +mod macro_scope; +mod parse; mod pattern; mod target; mod token; #[derive(Debug, Clone, Eq, PartialEq)] pub enum ItemSource { - File { name: String, line: usize }, + File { + name: String, + line: usize, + }, CommandLineOrMakeflags, Environment, Builtin, + #[cfg(feature = "full")] FunctionCall, } pub struct Makefile<'a> { - inference_rules: Vec<InferenceRule>, + inference_rules: InferenceRuleSet, builtin_inference_rules: Vec<InferenceRule>, - pub macros: MacroSet<'static, 'static>, + pub macros: MacroSet, targets: DynamicTargetSet, pub first_non_special_target: Option<String>, args: &'a Args, @@ -60,7 +74,7 @@ impl<'a> Makefile<'a> { "MAKE".to_owned(), Macro { source: ItemSource::Builtin, - text: std::env::current_exe().map_or_else( + text: env::current_exe().map_or_else( |_| TokenString::text("makers"), |x| TokenString::text(x.to_string_lossy()), ), @@ -68,6 +82,15 @@ impl<'a> Makefile<'a> { eagerly_expanded: false, }, ); + macros.set( + "MAKEFLAGS".to_owned(), + Macro { + source: ItemSource::Builtin, + text: TokenString::text(args.makeflags()), + #[cfg(feature = "full")] + eagerly_expanded: false, + }, + ); if !args.no_builtin_rules { inference_rules.extend(builtin_inference_rules()); macros.add_builtins(); @@ -119,7 +142,7 @@ impl<'a> Makefile<'a> { } Makefile { - inference_rules: vec![], + inference_rules: InferenceRuleSet::default(), builtin_inference_rules: inference_rules, macros, targets, @@ -131,11 +154,7 @@ impl<'a> Makefile<'a> { pub fn extend(&mut self, new: FinishedMakefileReader) -> Result<()> { self.inference_rules.extend(new.inference_rules); - self.macros.extend( - new.macros, - #[cfg(feature = "full")] - new.macro_exports, - ); + self.macros.extend(new.macros); for (_, target) in new.targets { self.targets.put(target); } @@ -147,9 +166,9 @@ impl<'a> Makefile<'a> { self.update_target(&failed_include).wrap_err_with(|| { format!("while building missing included file {}", &failed_include) })?; - let macros = self.macros.with_overlay(); + let stack = MacroScopeStack::default().with_scope(&self.macros); let file = - MakefileReader::read_file(self.args, macros, failed_include, Default::default())? + MakefileReader::read_file(self.args, stack, failed_include, Default::default())? .finish(); self.extend(file)?; } @@ -157,13 +176,10 @@ impl<'a> Makefile<'a> { } fn special_target_has_prereq(&self, target: &str, name: &str) -> bool { - match self.targets.get(target) { - Some(target) => { - let target = target.borrow(); - target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name) - } - None => false, - } + self.targets.get(target).map_or(false, |target| { + let target = target.borrow(); + target.prerequisites.is_empty() || target.prerequisites.iter().any(|e| e == name) + }) } fn infer_target( @@ -184,7 +200,7 @@ impl<'a> Makefile<'a> { let follow_gnu = cfg!(feature = "full"); - let vpath_options = match self.macros.get("VPATH") { + let vpath_options = match self.macros.get_non_recursive("VPATH") { Some(Macro { text, .. }) if follow_gnu => { let vpath = self.expand_macros(text, None)?; env::split_paths(&vpath).collect() @@ -202,7 +218,12 @@ impl<'a> Makefile<'a> { .inference_rules .iter() .chain(self.builtin_inference_rules.iter()) - .filter(|rule| !banned_rules.contains(rule)) + .filter(|rule| { + !banned_rules.iter().any(|banned_rule| { + banned_rule.products == rule.products + && banned_rule.prerequisites == rule.prerequisites + }) + }) .filter(|rule| rule.matches(name).unwrap_or(false)); for rule in inference_rule_candidates { log::trace!( @@ -219,7 +240,10 @@ impl<'a> Makefile<'a> { // we can't build this based on itself! fuck outta here return None; } - if self.targets.has(&prereq_path_name) { + if self.targets.has(&prereq_path_name) + || self.special_target_has_prereq(".PHONY", &prereq_path_name) + { + // TODO consider only checking phony after transitive inference has failed return Some(prereq_path_name); } let prereq_path = PathBuf::from(&prereq_path_name); @@ -261,6 +285,7 @@ impl<'a> Makefile<'a> { .first_match(name)? .and_then(|x| x.get(1).map(|x| x.as_str().to_owned())), already_updated: Cell::new(false), + macros: MacroSet::new(), }); break; } @@ -278,7 +303,11 @@ impl<'a> Makefile<'a> { let follow_gnu = cfg!(feature = "full"); #[cfg(feature = "full")] - let name = name.strip_prefix("./").unwrap_or(name); + if let Some(name_without_leading_dot_slash) = name.strip_prefix("./") { + if let Ok(result) = self.get_target(name_without_leading_dot_slash) { + return Ok(result); + } + } let exists_but_infer_anyway = if follow_gnu { self.targets @@ -303,6 +332,7 @@ impl<'a> Makefile<'a> { commands, stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }); } else { // if it already exists, it counts as up-to-date @@ -313,6 +343,7 @@ impl<'a> Makefile<'a> { commands: vec![], stem: None, already_updated: Cell::new(true), + macros: MacroSet::new(), }); } } @@ -322,10 +353,9 @@ impl<'a> Makefile<'a> { self.targets.put(new_target); } - Ok(self - .targets + self.targets .get(name) - .ok_or_else(|| eyre!("Target {:?} not found!", name))?) + .ok_or_else(|| eyre!("Target {:?} not found!", name)) } pub fn update_target(&self, name: &str) -> Result<()> { @@ -343,74 +373,15 @@ impl<'a> Makefile<'a> { } fn expand_macros(&self, text: &TokenString, target: Option<&Target>) -> Result<String> { - let target = target.cloned(); - let lookup_internal = move |macro_name: &str| { - let target = target - .as_ref() - .ok_or_else(|| eyre!("internal macro but no current target!"))?; - let macro_pieces = if macro_name.starts_with('@') { - // The $@ shall evaluate to the full target name of the - // current target. - vec![target.name.clone()] - } else if macro_name.starts_with('?') { - // The $? macro shall evaluate to the list of prerequisites - // that are newer than the current target. - target - .prerequisites - .iter() - .filter(|prereq| { - self.get_target(prereq) - .ok() - .and_then(|prereq| prereq.borrow().newer_than(target)) - .unwrap_or(false) - }) - .cloned() - .collect() - } else if macro_name.starts_with('<') { - // In an inference rule, the $< macro shall evaluate to the - // filename whose existence allowed the inference rule to be - // chosen for the target. In the .DEFAULT rule, the $< macro - // shall evaluate to the current target name. - // TODO make that actually be the case (rn exists_but_inferring_anyway might fuck that up) - vec![target.prerequisites.get(0).cloned().unwrap_or_default()] - } else if macro_name.starts_with('*') { - // The $* macro shall evaluate to the current target name with - // its suffix deleted. (GNUism: the match stem) - vec![target.stem.as_ref().unwrap_or(&target.name).to_owned()] - } else if macro_name.starts_with('^') { - target.prerequisites.clone() - } else { - unreachable!() - }; - - let macro_pieces = if macro_name.ends_with('D') { - macro_pieces - .into_iter() - .map(|x| { - Path::new(&x) - .parent() - .ok_or_else(|| eyre!("no parent")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<_, _>>()? - } else if macro_name.ends_with('F') { - macro_pieces - .into_iter() - .map(|x| { - Path::new(&x) - .file_name() - .ok_or_else(|| eyre!("no filename")) - .map(|x| x.to_string_lossy().into()) - }) - .collect::<Result<_, _>>()? - } else { - macro_pieces - }; - - Ok(macro_pieces.join(" ")) - }; - - self.macros.with_lookup(&lookup_internal).expand(text) + MacroScopeStack::default() + .with_scope(&self.macros) + .with_scope(&LookupInternal::new(target, &|name| self.get_target(name))) + .with_scope(&target.map(|target| &target.macros)) + .expand( + text, + #[cfg(feature = "full")] + NO_EVAL, + ) } } @@ -460,6 +431,7 @@ fn builtin_inference_rules() -> Vec<InferenceRule> { prepend_dot!($($second)?).into(), concat!(".", stringify!($first)).into(), vec![$(CommandLine::from($cmd.parse().unwrap())),+], + MacroSet::new(), ) ),+] }; @@ -515,6 +487,7 @@ fn builtin_targets() -> Vec<Target> { commands: vec![], stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }] } @@ -533,9 +506,10 @@ mod test { products: vec!["this-is-a-%-case".to_owned()], prerequisites: vec![], commands: vec![], + macros: MacroSet::new(), }; let file = Makefile { - inference_rules: vec![rule], + inference_rules: vec![rule].into(), builtin_inference_rules: vec![], macros: MacroSet::new(), targets: Default::default(), @@ -559,6 +533,7 @@ mod test { commands: vec![], stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }; let phony = Target { name: ".PHONY".to_string(), @@ -566,13 +541,14 @@ mod test { commands: vec![], stem: None, already_updated: Cell::new(false), + macros: MacroSet::new(), }; let targets = DynamicTargetSet::default(); targets.put(target); targets.put(phony); let file = Makefile { - inference_rules: vec![], + inference_rules: InferenceRuleSet::default(), builtin_inference_rules: vec![], macros: MacroSet::new(), targets, @@ -584,4 +560,42 @@ mod test { assert!(file.update_target("all").is_ok()); Ok(()) } + + #[cfg(feature = "full")] + #[test] + fn missing_phony_targets_allow_inference() -> R { + let args = Args::empty(); + let rule = InferenceRule { + source: ItemSource::CommandLineOrMakeflags, + products: vec!["%ll".to_owned()], + prerequisites: vec!["missing".to_owned()], + commands: vec![], + macros: MacroSet::new(), + }; + let phony = Target { + name: ".PHONY".to_string(), + prerequisites: vec!["missing".to_owned()], + commands: vec![], + stem: None, + already_updated: Cell::new(false), + macros: MacroSet::new(), + }; + + let mut inference_rules = InferenceRuleSet::default(); + inference_rules.put(rule); + let targets = DynamicTargetSet::default(); + targets.put(phony); + let file = Makefile { + inference_rules, + builtin_inference_rules: vec![], + macros: MacroSet::new(), + targets, + first_non_special_target: None, + args: &args, + already_inferred: Default::default(), + }; + + file.update_target("all")?; + Ok(()) + } } diff --git a/src/makefile/parse.rs b/src/makefile/parse.rs new file mode 100644 index 0000000..191b7e0 --- /dev/null +++ b/src/makefile/parse.rs @@ -0,0 +1,19 @@ +use super::Macro; +use super::TokenString; + +#[derive(Debug)] +pub struct MacroAssignment { + pub name: String, + pub value: TokenString, + #[cfg(feature = "full")] + pub expand_value: bool, + #[cfg(feature = "full")] + pub skip_if_defined: bool, + #[cfg(feature = "full")] + pub append: bool, +} + +pub enum MacroAssignmentOutcome { + Set, + AppendedTo(Macro), +} diff --git a/src/makefile/target.rs b/src/makefile/target.rs index c3431e4..9f81802 100644 --- a/src/makefile/target.rs +++ b/src/makefile/target.rs @@ -8,9 +8,7 @@ use std::time::SystemTime; use eyre::{Result, WrapErr}; -use crate::makefile::command_line::CommandLine; - -use super::Makefile; +use super::{CommandLine, MacroSet, Makefile}; #[derive(PartialEq, Eq, Clone, Debug)] pub struct Target { @@ -19,10 +17,11 @@ pub struct Target { pub commands: Vec<CommandLine>, pub stem: Option<String>, pub already_updated: Cell<bool>, + pub macros: MacroSet, } impl Target { - pub fn extend(&mut self, other: Target) { + pub fn extend(&mut self, other: Self) { assert_eq!(&self.name, &other.name); match (self.commands.is_empty(), other.commands.is_empty()) { (false, false) => { @@ -43,6 +42,7 @@ impl Target { self.stem = self.stem.take().or(other.stem); let already_updated = self.already_updated.get() || other.already_updated.get(); self.already_updated.set(already_updated); + self.macros.extend(other.macros); } } } @@ -152,11 +152,15 @@ impl StaticTargetSet { self.data.insert(target.name.clone(), target); } } + + pub fn has(&self, name: &str) -> bool { + self.data.contains_key(name) + } } -impl Into<HashMap<String, Target>> for StaticTargetSet { - fn into(self) -> HashMap<String, Target> { - self.data +impl From<StaticTargetSet> for HashMap<String, Target> { + fn from(value: StaticTargetSet) -> Self { + value.data } } @@ -168,7 +172,7 @@ pub struct DynamicTargetSet { impl DynamicTargetSet { pub fn get(&self, name: &str) -> Option<Rc<RefCell<Target>>> { - self.data.borrow().get(name).map(|x| Rc::clone(x)) + self.data.borrow().get(name).map(Rc::clone) } pub fn put(&self, target: Target) { diff --git a/src/makefile/token.rs b/src/makefile/token.rs index 2721387..8507a3a 100644 --- a/src/makefile/token.rs +++ b/src/makefile/token.rs @@ -17,6 +17,7 @@ use nom::{ character::complete::{space0, space1}, multi::separated_list1, }; +use regex::Regex; trait Err<'a>: 'a + ParseError<&'a str> + ContextError<&'a str> {} impl<'a, T: 'a + ParseError<&'a str> + ContextError<&'a str>> Err<'a> for T {} @@ -128,7 +129,7 @@ impl TokenString { } pub fn is_empty(&self) -> bool { - match self.0.get(0) { + match self.0.first() { None => true, Some(Token::Text(t)) if t.is_empty() && self.0.len() == 1 => true, _ => false, @@ -144,6 +145,29 @@ impl TokenString { } }) } + + pub fn matches_regex(&self, regex: &Regex) -> bool { + self.0.iter().any(|x| { + if let Token::Text(x) = x { + regex.is_match(x) + } else { + false + } + }) + } + + /// Returns (token index within string, pattern index within token). + pub fn find(&self, pattern: &str) -> Option<(usize, usize)> { + self.0 + .iter() + .enumerate() + .find_map(|(token_index, token)| match token { + Token::Text(text) => text + .find(pattern) + .map(|pattern_index| (token_index, pattern_index)), + _ => None, + }) + } } impl fmt::Display for TokenString { @@ -231,6 +255,13 @@ impl Delimiter { Self::Braces => "}", } } + + const fn end_char(&self) -> char { + match self { + Delimiter::Parens => ')', + Delimiter::Braces => '}', + } + } } fn macro_function_name<'a, E: Err<'a>>( @@ -341,13 +372,12 @@ fn text_but_not<'a, E: Err<'a>>( } fn nested_delimiters<'a, E: Err<'a>>( - ends: Vec<char>, context: Delimiter, ) -> impl FnMut(&'a str) -> IResult<&'a str, TokenString, E> { map( tuple(( tag(context.start()), - move |x| tokens_but_not(ends.clone(), context)(x), + move |x| tokens_but_not(vec![context.end_char()], context)(x), tag(context.end()), )), |(left, center, right)| { @@ -368,7 +398,7 @@ fn single_token_but_not<'a, E: Err<'a>>( alt(( text_but_not(tbn_ends), macro_expansion, - nested_delimiters(ends, context), + nested_delimiters(context), )) } @@ -386,7 +416,7 @@ fn empty_tokens<'a, E: Err<'a>>(input: &'a str) -> IResult<&'a str, TokenString, fn fold_tokens<'a, E: Err<'a>>( parser: impl FnMut(&'a str) -> IResult<&'a str, TokenString, E>, ) -> impl FnMut(&'a str) -> IResult<&'a str, TokenString, E> { - fold_many1(parser, TokenString::empty(), |mut acc, x| { + fold_many1(parser, TokenString::empty, |mut acc, x| { acc.extend(x); acc }) @@ -614,4 +644,28 @@ mod test { ); Ok(()) } + + #[cfg(feature = "full")] + #[test] + fn quoted_function_call_comma() -> R { + let text = "$(egg $$(bug a, b/c))"; + let tokens = tokenize(text)?; + + assert_eq!( + tokens, + TokenString::just(Token::FunctionCall { + name: TokenString::text("egg"), + args: vec![TokenString::text("$(bug a, b/c)")], + }) + ); + Ok(()) + } + + #[cfg(feature = "full")] + #[test] + fn unbalanced_parentheses_rejected() -> R { + let text = "$(egg ()"; + assert!(tokenize(text).is_err()); + Ok(()) + } } diff --git a/tests/conditional_assignment_inheritance.rs b/tests/conditional_assignment_inheritance.rs new file mode 100644 index 0000000..24f0533 --- /dev/null +++ b/tests/conditional_assignment_inheritance.rs @@ -0,0 +1,29 @@ +#![cfg(feature = "full")] + +mod utils; + +use std::fs; +use utils::{make, R}; + +#[test] +fn conditional_assignment_inheritance_test() -> R { + let dir = tempfile::tempdir()?; + + let file_a = " +EGG = bug +include file_b.mk +check: +\t@echo $(EGG) +"; + fs::write(dir.path().join("Makefile"), file_a)?; + let file_b = " +EGG ?= nope +"; + fs::write(dir.path().join("file_b.mk"), file_b)?; + + let result = make(&dir)?; + assert!(result.status.success()); + assert_eq!(String::from_utf8(result.stdout)?.trim(), "bug"); + + Ok(()) +} diff --git a/tests/rule_specific_macros.rs b/tests/rule_specific_macros.rs new file mode 100644 index 0000000..34e7e77 --- /dev/null +++ b/tests/rule_specific_macros.rs @@ -0,0 +1,85 @@ +#![cfg(feature = "full")] + +mod utils; + +use std::fs; +use utils::{make, R}; + +#[test] +fn target_specific_macros() -> R { + let dir = tempfile::tempdir()?; + + let file = " +foo.h: EGG = bug +foo.h: +\techo $(EGG) + "; + fs::write(dir.path().join("Makefile"), file)?; + + let result = make(&dir)?; + dbg!(&result); + assert!(result.status.success()); + let stdout = String::from_utf8(result.stdout)?; + assert!(stdout.contains("echo bug")); + + Ok(()) +} + +#[test] +#[ignore = "not yet implemented"] +fn target_specific_macros_inherited() -> R { + let dir = tempfile::tempdir()?; + + // example from https://www.gnu.org/software/make/manual/html_node/Target_002dspecific.html + let file = " +CC=echo cc +prog : CFLAGS = -g +prog : prog.o foo.o bar.o + "; + fs::write(dir.path().join("Makefile"), file)?; + fs::write(dir.path().join("prog.c"), "")?; + fs::write(dir.path().join("foo.c"), "")?; + fs::write(dir.path().join("bar.c"), "")?; + + let result = make(&dir)?; + dbg!(&result); + assert!(result.status.success()); + let stdout = String::from_utf8(result.stdout)?; + assert!(stdout.contains("echo cc -g -c foo.c")); + assert!(stdout.contains("echo cc -g -c bar.o")); + assert!(stdout.contains("echo cc -g -c prog.c")); + + Ok(()) +} + +#[test] +#[ignore = "not yet implemented"] +fn inference_rule_specific_macros() -> R { + let dir = tempfile::tempdir()?; + + // example from https://www.gnu.org/software/make/manual/html_node/Pattern_002dspecific.html + let file = " +CC=echo cc +%.o: %.c +\t$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@ + +lib/%.o: CFLAGS := -fPIC -g +%.o: CFLAGS := -g + +all: foo.o lib/bar.o + "; + fs::write(dir.path().join("Makefile"), file)?; + fs::write(dir.path().join("foo.c"), "")?; + fs::create_dir(dir.path().join("lib"))?; + fs::write(dir.path().join("bar.c"), "")?; + + let result = make(&dir)?; + dbg!(&result); + assert!(result.status.success()); + let stdout = String::from_utf8(result.stdout)?; + dbg!(&stdout); + assert!(stdout.contains("echo cc -g foo.c -o foo.o")); + assert!(stdout.contains("echo cc -fPIC -g lib/bar.c -o lib/bar.o")); + + Ok(()) +} diff --git a/tests/update_logic.rs b/tests/update_logic.rs index 9e37b19..6eefe6b 100644 --- a/tests/update_logic.rs +++ b/tests/update_logic.rs @@ -1,19 +1,9 @@ use std::fs; -use std::path::Path; -use std::process::{Command, Output}; - -use eyre::{Result, WrapErr}; use std::thread::sleep; use std::time::Duration; -type R = Result<()>; - -fn make(dir: impl AsRef<Path>) -> Result<Output> { - Command::new(env!("CARGO_BIN_EXE_makers")) - .current_dir(dir) - .output() - .wrap_err("") -} +mod utils; +use utils::{make, R}; #[test] fn basic_test() -> R { diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs new file mode 100644 index 0000000..8b834c1 --- /dev/null +++ b/tests/utils/mod.rs @@ -0,0 +1,13 @@ +use std::path::Path; +use std::process::{Command, Output}; + +use eyre::Context; + +pub type R = eyre::Result<()>; + +pub fn make(dir: impl AsRef<Path>) -> eyre::Result<Output> { + Command::new(env!("CARGO_BIN_EXE_makers")) + .current_dir(dir) + .output() + .wrap_err("") +} |