From 5d258482fb84d58c966a7ccc1b467708278d28ad Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Fri, 10 Jul 2026 16:54:57 +0800 Subject: refactor(devtools): rename dev_tools to .run and replace run-tools scripts Replace the old run-tools.ps1/run-tools.sh scripts with a new unified run.ps1/run.sh launcher that supports multiple languages (PowerShell, Rust, Python, Go, C#, Nim, Perl, Ruby, Zig, and arbitrary binaries) from the `.run/src/bin` directory. Move all development tools from `dev_tools/` to `.run/`, update Cargo workspace config and relevant documentation references. --- .cargo/config.toml | 4 +- .run/.gitignore | 7 + .run/Cargo.lock | 435 +++++++++++++++++++++++ .run/Cargo.toml | 23 ++ .run/src/bin/build-all.ps1 | 8 + .run/src/bin/build-all.sh | 6 + .run/src/bin/ci.rs | 225 ++++++++++++ .run/src/bin/clippy-fix.ps1 | 8 + .run/src/bin/clippy-fix.sh | 6 + .run/src/bin/clippy.ps1 | 8 + .run/src/bin/clippy.sh | 6 + .run/src/bin/doc.ps1 | 1 + .run/src/bin/doc.sh | 3 + .run/src/bin/docs-code-box-fix.rs | 166 +++++++++ .run/src/bin/docsify-sidebar-gen.rs | 262 ++++++++++++++ .run/src/bin/http-page-preview.ps1 | 3 + .run/src/bin/http-page-preview.sh | 2 + .run/src/bin/install-mling.ps1 | 7 + .run/src/bin/install-mling.sh | 6 + .run/src/bin/refresh-docs.rs | 178 ++++++++++ .run/src/bin/refresh-feature-mod.rs | 97 ++++++ .run/src/bin/sync-examples.rs | 131 +++++++ .run/src/bin/test-all-markdown-code.rs | 276 +++++++++++++++ .run/src/bin/test-all.ps1 | 8 + .run/src/bin/test-all.sh | 6 + .run/src/bin/test-examples.rs | 158 +++++++++ .run/src/bin/update-version.rs | 98 ++++++ .run/src/bin/windows-folder-hide.ps1 | 43 +++ .run/src/lib.rs | 331 ++++++++++++++++++ .run/src/verify.rs | 511 ++++++++++++++++++++++++++++ .run/version-files.toml | 19 ++ ABOUT-CI.md | 22 +- CONTRIBUTING.md | 54 +-- Cargo.toml | 2 +- dev_tools/Cargo.lock | 435 ----------------------- dev_tools/Cargo.toml | 21 -- dev_tools/scripts/build-all.ps1 | 8 - dev_tools/scripts/build-all.sh | 6 - dev_tools/scripts/clippy-fix.ps1 | 8 - dev_tools/scripts/clippy-fix.sh | 6 - dev_tools/scripts/clippy.ps1 | 8 - dev_tools/scripts/clippy.sh | 6 - dev_tools/scripts/doc.ps1 | 1 - dev_tools/scripts/doc.sh | 3 - dev_tools/scripts/http-page-preview.ps1 | 3 - dev_tools/scripts/http-page-preview.sh | 2 - dev_tools/scripts/install-mling.ps1 | 7 - dev_tools/scripts/install-mling.sh | 6 - dev_tools/scripts/test-all.ps1 | 8 - dev_tools/scripts/test-all.sh | 6 - dev_tools/scripts/windows-folder-hide.ps1 | 43 --- dev_tools/src/bin/ci.rs | 225 ------------ dev_tools/src/bin/docs-code-box-fix.rs | 166 --------- dev_tools/src/bin/docsify-sidebar-gen.rs | 265 --------------- dev_tools/src/bin/refresh-docs.rs | 178 ---------- dev_tools/src/bin/refresh-feature-mod.rs | 97 ------ dev_tools/src/bin/sync-examples.rs | 131 ------- dev_tools/src/bin/test-all-markdown-code.rs | 276 --------------- dev_tools/src/bin/test-examples.rs | 158 --------- dev_tools/src/bin/update-version.rs | 98 ------ dev_tools/src/lib.rs | 331 ------------------ dev_tools/src/verify.rs | 511 ---------------------------- dev_tools/version-files.toml | 19 -- run-tools.ps1 | 60 ---- run-tools.sh | 64 ---- run.ps1 | 232 +++++++++++++ run.sh | 257 ++++++++++++++ 67 files changed, 3568 insertions(+), 3197 deletions(-) create mode 100644 .run/.gitignore create mode 100644 .run/Cargo.lock create mode 100644 .run/Cargo.toml create mode 100644 .run/src/bin/build-all.ps1 create mode 100644 .run/src/bin/build-all.sh create mode 100644 .run/src/bin/ci.rs create mode 100644 .run/src/bin/clippy-fix.ps1 create mode 100644 .run/src/bin/clippy-fix.sh create mode 100644 .run/src/bin/clippy.ps1 create mode 100644 .run/src/bin/clippy.sh create mode 100644 .run/src/bin/doc.ps1 create mode 100644 .run/src/bin/doc.sh create mode 100644 .run/src/bin/docs-code-box-fix.rs create mode 100644 .run/src/bin/docsify-sidebar-gen.rs create mode 100644 .run/src/bin/http-page-preview.ps1 create mode 100644 .run/src/bin/http-page-preview.sh create mode 100644 .run/src/bin/install-mling.ps1 create mode 100644 .run/src/bin/install-mling.sh create mode 100644 .run/src/bin/refresh-docs.rs create mode 100644 .run/src/bin/refresh-feature-mod.rs create mode 100644 .run/src/bin/sync-examples.rs create mode 100644 .run/src/bin/test-all-markdown-code.rs create mode 100644 .run/src/bin/test-all.ps1 create mode 100644 .run/src/bin/test-all.sh create mode 100644 .run/src/bin/test-examples.rs create mode 100644 .run/src/bin/update-version.rs create mode 100644 .run/src/bin/windows-folder-hide.ps1 create mode 100644 .run/src/lib.rs create mode 100644 .run/src/verify.rs create mode 100644 .run/version-files.toml delete mode 100644 dev_tools/Cargo.lock delete mode 100644 dev_tools/Cargo.toml delete mode 100644 dev_tools/scripts/build-all.ps1 delete mode 100755 dev_tools/scripts/build-all.sh delete mode 100644 dev_tools/scripts/clippy-fix.ps1 delete mode 100755 dev_tools/scripts/clippy-fix.sh delete mode 100644 dev_tools/scripts/clippy.ps1 delete mode 100755 dev_tools/scripts/clippy.sh delete mode 100755 dev_tools/scripts/doc.ps1 delete mode 100755 dev_tools/scripts/doc.sh delete mode 100644 dev_tools/scripts/http-page-preview.ps1 delete mode 100755 dev_tools/scripts/http-page-preview.sh delete mode 100755 dev_tools/scripts/install-mling.ps1 delete mode 100755 dev_tools/scripts/install-mling.sh delete mode 100644 dev_tools/scripts/test-all.ps1 delete mode 100644 dev_tools/scripts/test-all.sh delete mode 100644 dev_tools/scripts/windows-folder-hide.ps1 delete mode 100644 dev_tools/src/bin/ci.rs delete mode 100644 dev_tools/src/bin/docs-code-box-fix.rs delete mode 100644 dev_tools/src/bin/docsify-sidebar-gen.rs delete mode 100644 dev_tools/src/bin/refresh-docs.rs delete mode 100644 dev_tools/src/bin/refresh-feature-mod.rs delete mode 100644 dev_tools/src/bin/sync-examples.rs delete mode 100644 dev_tools/src/bin/test-all-markdown-code.rs delete mode 100644 dev_tools/src/bin/test-examples.rs delete mode 100644 dev_tools/src/bin/update-version.rs delete mode 100644 dev_tools/src/lib.rs delete mode 100644 dev_tools/src/verify.rs delete mode 100644 dev_tools/version-files.toml delete mode 100644 run-tools.ps1 delete mode 100755 run-tools.sh create mode 100644 run.ps1 create mode 100644 run.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 0b45ca9..f375b03 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,5 +4,5 @@ target-dir = "./.temp/target" [env] [alias] -ci = "run --manifest-path dev_tools/Cargo.toml --bin ci --quiet --" -dev_tool = "run --manifest-path dev_tools/Cargo.toml --quiet --bin " +ci = "run --manifest-path .run/Cargo.toml --bin ci --quiet --" +dev_tool = "run --manifest-path .run/Cargo.toml --quiet --bin " diff --git a/.run/.gitignore b/.run/.gitignore new file mode 100644 index 0000000..0dc39b0 --- /dev/null +++ b/.run/.gitignore @@ -0,0 +1,7 @@ +# All temp build artifacts will be stored in this dir +/target + +# If you want to add some Rust crates? Remove this line +# /Cargo.* + +last_check diff --git a/.run/Cargo.lock b/.run/Cargo.lock new file mode 100644 index 0000000..bb510bd --- /dev/null +++ b/.run/Cargo.lock @@ -0,0 +1,435 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width", + "windows-sys", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "just_fmt" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" + +[[package]] +name = "just_template" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" +dependencies = [ + "just_fmt", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tools" +version = "0.1.0" +dependencies = [ + "colored", + "indicatif", + "just_fmt", + "just_template", + "serde", + "serde_json", + "tokio", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/.run/Cargo.toml b/.run/Cargo.toml new file mode 100644 index 0000000..283aa47 --- /dev/null +++ b/.run/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "tools" +version = "0.1.0" +edition = "2024" +authors = ["Weicao-CatilGrass"] +description = "Development tools for mingling" +license = "MIT OR Apache-2.0" +repository = "https://github.com/catilgrass/mingling" +readme = "../README.md" +keywords = ["cli", "development", "tools"] +categories = ["command-line-interface", "development-tools"] + +[dependencies] +just_template = "0.1.3" +just_fmt = "0.1.2" +colored = "3.1.1" +toml = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +indicatif = "0.18.4" + +[workspace] diff --git a/.run/src/bin/build-all.ps1 b/.run/src/bin/build-all.ps1 new file mode 100644 index 0000000..4f35ed8 --- /dev/null +++ b/.run/src/bin/build-all.ps1 @@ -0,0 +1,8 @@ +$starting_dir = Get-Location +Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { + $project_dir = $_.DirectoryName + Push-Location $project_dir + cargo build + Pop-Location +} +Set-Location $starting_dir diff --git a/.run/src/bin/build-all.sh b/.run/src/bin/build-all.sh new file mode 100644 index 0000000..2036b41 --- /dev/null +++ b/.run/src/bin/build-all.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +find . -name "Cargo.toml" -type f | while read -r cargo_file; do + project_dir=$(dirname "$cargo_file") + (cd "$project_dir" && cargo build) +done diff --git a/.run/src/bin/ci.rs b/.run/src/bin/ci.rs new file mode 100644 index 0000000..f8aed79 --- /dev/null +++ b/.run/src/bin/ci.rs @@ -0,0 +1,225 @@ +use std::io::Write as _; +use std::process::exit; + +use tools::{ + cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel, +}; + +fn get_ignore_dirs() -> Vec { + vec![ + ".temp".to_string(), + "mling/res".to_string(), + "mling\\res".to_string(), + ] +} + +fn print_help() { + println!( + r" +Usage: ci [options] +Options: + -h, --help Print this help message + -y Auto-confirm temporary commits + --dirty Run CI on dirty workspace (skip temp commit & clean check) + --refresh-docs Refresh documentation files + --test-docs Run documentation tests (build, clippy, test) + --test-codes Test examples and documentation code blocks + +If no specific options are given, all checks are run. + " + ); +} + +fn main() { + #[cfg(windows)] + let _ = colored::control::set_virtual_terminal(true); + println!("{}", include_str!("../../../docs/res/ci_banner.txt")); + + let args: Vec = std::env::args().collect(); + + if args.iter().any(|a| a == "-h" || a == "--help") { + print_help(); + return; + } + + let auto_yes = args.iter().any(|a| a == "-y"); + let dirty = args.iter().any(|a| a == "--dirty"); + + let test_docs = args.iter().any(|a| a == "--test-docs"); + let refresh_docs = args.iter().any(|a| a == "--refresh-docs"); + let test_codes = args.iter().any(|a| a == "--test-codes"); + let any_specified = test_docs || refresh_docs || test_codes; + let run_all = !any_specified; + + let needs_commit_temp = !dirty && !{ run_cmd!("git diff-index --quiet HEAD --").is_ok() }; + + if needs_commit_temp { + if auto_yes { + run_cmd!("git add .").unwrap(); + run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap(); + } else { + print!("Working tree is not clean, temporarily commit? [y/N]:"); + std::io::stdout().flush().unwrap(); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap(); + let input = input.trim(); + if input == "y" || input == "Y" || input == "yes" || input == "Yes" { + run_cmd!("git add .").unwrap(); + run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap(); + } else { + eprintln_cargo_style!("Aborting."); + exit(2) + } + } + } + + if let Err(exit_code) = ci(test_docs, test_codes, run_all) { + restore_workspace(needs_commit_temp).unwrap(); + exit(exit_code) + } + + if !dirty { + let is_worktree_clean = run_cmd!("git diff-index --quiet HEAD --").is_ok(); + if !is_worktree_clean { + eprintln_cargo_style!("The repository was contaminated during CI, failing!"); + + // Print git status + println!(); + let _ = run_cmd!("git status"); + + if needs_commit_temp { + restore_workspace(true).unwrap(); + } + exit(1) + } + } + + println_cargo_style!("Done: All check passed!"); + + if needs_commit_temp { + restore_workspace(true).unwrap(); + } +} + +fn restore_workspace(undo_commit: bool) -> Result<(), i32> { + run_cmd!("git reset --hard --quiet")?; + if undo_commit { + run_cmd!("git reset --soft HEAD~1 --quiet")?; + run_cmd!("git reset --quiet")?; + } + Ok(()) +} + +fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { + if run_all || test_codes { + println_cargo_style!("Phase: Scan and build all crates"); + build_all()?; + + println_cargo_style!("Phase: Run clippy for all crates"); + clippy_all()?; + + println_cargo_style!("Phase: Test all crates"); + test_all()?; + } + + if run_all || test_docs { + println_cargo_style!("Phase: Test all examples"); + test_examples()?; + + println_cargo_style!("Phase: Verify all *.md document code blocks are compilable"); + test_docs_code_blocks()?; + + println_cargo_style!("Phase: Check all documentation is up to date"); + docs_refresh()?; + } + + run_cmd!("git add --renormalize .")?; + + Ok(()) +} + +fn test_examples() -> Result<(), i32> { + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-examples") +} + +fn test_docs_code_blocks() -> Result<(), i32> { + run_cmd!( + "cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-all-markdown-code" + ) +} + +fn build_all() -> Result<(), i32> { + let ignore_dirs = get_ignore_dirs(); + let cargo_tomls = cargo_tomls(); + let mut tasks = Vec::new(); + for cargo_toml in cargo_tomls { + let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path_str = path.to_string_lossy(); + if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { + continue; + } + let label = format!("Build: {}", cargo_toml.to_string_lossy()); + let crate_name = crate_name_from(&cargo_toml); + let cmd = format!( + "cargo build --manifest-path {} --color always", + cargo_toml.to_string_lossy() + ); + tasks.push((label, crate_name, cmd)); + } + run_parallel("Building", tasks) +} + +fn clippy_all() -> Result<(), i32> { + let ignore_dirs = get_ignore_dirs(); + let cargo_tomls = cargo_tomls(); + let mut tasks = Vec::new(); + for cargo_toml in cargo_tomls { + let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path_str = path.to_string_lossy(); + if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { + continue; + } + let label = format!("Clippy: {}", cargo_toml.to_string_lossy()); + let crate_name = crate_name_from(&cargo_toml); + let cmd = format!( + "cargo clippy --manifest-path {} --color always -- -D warnings", + cargo_toml.to_string_lossy() + ); + tasks.push((label, crate_name, cmd)); + } + run_parallel("Clippy", tasks) +} + +fn test_all() -> Result<(), i32> { + let ignore_dirs = get_ignore_dirs(); + let cargo_tomls = cargo_tomls(); + let mut tasks = Vec::new(); + for cargo_toml in cargo_tomls { + let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); + let path_str = path.to_string_lossy(); + if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { + continue; + } + let label = format!("Testing: {}", cargo_toml.to_string_lossy()); + let crate_name = crate_name_from(&cargo_toml); + let cmd = format!( + "cargo test --manifest-path {} --color always", + cargo_toml.to_string_lossy() + ); + tasks.push((label, crate_name, cmd)); + } + run_parallel("Testing", tasks) +} + +fn docs_refresh() -> Result<(), i32> { + println_cargo_style!("Refresh: document at `./docs/`"); + + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docs-code-box-fix")?; + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docsify-sidebar-gen")?; + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-docs")?; + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-feature-mod")?; + run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin sync-examples")?; + run_cmd!("cargo fmt")?; + + Ok(()) +} diff --git a/.run/src/bin/clippy-fix.ps1 b/.run/src/bin/clippy-fix.ps1 new file mode 100644 index 0000000..1d24f92 --- /dev/null +++ b/.run/src/bin/clippy-fix.ps1 @@ -0,0 +1,8 @@ +$starting_dir = Get-Location +Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { + $project_dir = $_.DirectoryName + Push-Location $project_dir + cargo clippy --fix --allow-dirty --allow-no-vcs --quiet + Pop-Location +} +Set-Location $starting_dir diff --git a/.run/src/bin/clippy-fix.sh b/.run/src/bin/clippy-fix.sh new file mode 100644 index 0000000..9771ad4 --- /dev/null +++ b/.run/src/bin/clippy-fix.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +find . -name "Cargo.toml" -type f | while read -r cargo_file; do + project_dir=$(dirname "$cargo_file") + (cd "$project_dir" && cargo clippy --fix --allow-dirty --allow-no-vcs --quiet) +done diff --git a/.run/src/bin/clippy.ps1 b/.run/src/bin/clippy.ps1 new file mode 100644 index 0000000..1858873 --- /dev/null +++ b/.run/src/bin/clippy.ps1 @@ -0,0 +1,8 @@ +$starting_dir = Get-Location +Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { + $project_dir = $_.DirectoryName + Push-Location $project_dir + cargo clippy --quiet + Pop-Location +} +Set-Location $starting_dir diff --git a/.run/src/bin/clippy.sh b/.run/src/bin/clippy.sh new file mode 100644 index 0000000..b393545 --- /dev/null +++ b/.run/src/bin/clippy.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +find . -name "Cargo.toml" -type f | while read -r cargo_file; do + project_dir=$(dirname "$cargo_file") + (cd "$project_dir" && cargo clippy --quiet) +done diff --git a/.run/src/bin/doc.ps1 b/.run/src/bin/doc.ps1 new file mode 100644 index 0000000..987f0de --- /dev/null +++ b/.run/src/bin/doc.ps1 @@ -0,0 +1 @@ +cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open diff --git a/.run/src/bin/doc.sh b/.run/src/bin/doc.sh new file mode 100644 index 0000000..5e8a311 --- /dev/null +++ b/.run/src/bin/doc.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open diff --git a/.run/src/bin/docs-code-box-fix.rs b/.run/src/bin/docs-code-box-fix.rs new file mode 100644 index 0000000..21d2cce --- /dev/null +++ b/.run/src/bin/docs-code-box-fix.rs @@ -0,0 +1,166 @@ +use std::fs; +use std::path::Path; + +use tools::println_cargo_style; + +/// Docsify code blocks require that blank lines before and after code blocks are not completely empty, +/// but must contain at least one space, otherwise code block rendering will have issues. +/// +/// This tool scans all `.md` files in the docs directory, +/// and replaces completely empty lines before and after code blocks with blank lines containing a single space. +const DOCS_DIR: &str = "./docs"; + +fn main() { + println_cargo_style!("Fixing: code box empty lines in docs/**/*.md ..."); + let repo_root = find_git_repo().expect("Cannot find git repo root"); + let docs_dir = repo_root.join(DOCS_DIR); + + let mut fixed_count = 0; + let mut file_count = 0; + + collect_md_files(&docs_dir, &mut |path| { + if let Some(name) = path.file_name() { + let name = name.to_string_lossy(); + if name.to_lowercase() == "_sidebar.md" { + return; + } + } + + let content = fs::read_to_string(path).unwrap_or_default(); + if content.is_empty() { + return; + } + + let new_content = fix_code_box_empty_lines(&content); + if new_content != content { + fs::write(path, &new_content).unwrap(); + println_cargo_style!("Fixed: {}", path.display()); + fixed_count += 1; + } + file_count += 1; + }); + + println_cargo_style!( + "Done: Scanned {} files, fixed {} files.", + file_count, + fixed_count + ); +} + +fn fix_code_box_empty_lines(content: &str) -> String { + let mut result = String::new(); + let lines: Vec<&str> = content.lines().collect(); + let len = lines.len(); + + let mut i = 0; + while i < len { + let line = lines[i]; + + // detect beginning of code block: beginning with ``` + if line.trim_start().starts_with("```") { + // record the beginning line of the code block + result.push_str(line); + result.push('\n'); + i += 1; + + // find the end of the code block + let mut found_end = false; + let code_start = i; // record starting position of code content + let mut code_end = len; // index of code block end line + + while i < len { + let cline = lines[i]; + if cline.trim_start().starts_with("```") && cline.trim() != "" { + // this is the closing marker + code_end = i; + found_end = true; + break; + } + i += 1; + } + + // check the blank line before the code block + // if result ends with \n\n, add a space to turn it into \n \n + ensure_space_before_code_block(&mut result); + + // output code content + for code_line in lines.iter().take(code_end).skip(code_start) { + if code_line.is_empty() { + result.push(' '); + } else { + result.push_str(code_line); + } + result.push('\n'); + } + + if found_end { + result.push_str(lines[code_end]); + result.push('\n'); + i += 1; + + // check the blank line after the code block + // if the next line is blank, change it to one with a space + if i < len && lines[i].trim().is_empty() && lines[i].is_empty() { + // skip the original blank line, write " \n" + result.push(' '); + result.push('\n'); + i += 1; + } + } + } else { + result.push_str(line); + result.push('\n'); + i += 1; + } + } + + // remove trailing newlines + while result.ends_with('\n') { + result.pop(); + } + result.push('\n'); + + result +} + +/// ensure there is a blank line with a space before the code block +fn ensure_space_before_code_block(result: &mut String) { + // if result ends with \n\n, + // turn it into \n \n + let len = result.len(); + if len >= 2 && result[len - 2..] == *"\n\n" { + // insert a space before the last \n + result.insert(len - 1, ' '); + } +} + +/// recursively collect all .md files in the docs directory +fn collect_md_files(dir: &Path, callback: &mut dyn FnMut(&Path)) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_md_files(&path, callback); + } else if path.extension().is_some_and(|ext| ext == "md") { + callback(&path); + } + } + } +} + +fn find_git_repo() -> Option { + let mut current_dir = std::env::current_dir().ok()?; + + loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + return Some(current_dir); + } + + if !current_dir.pop() { + break; + } + } + + None +} diff --git a/.run/src/bin/docsify-sidebar-gen.rs b/.run/src/bin/docsify-sidebar-gen.rs new file mode 100644 index 0000000..5beda7f --- /dev/null +++ b/.run/src/bin/docsify-sidebar-gen.rs @@ -0,0 +1,262 @@ +use std::collections::BTreeMap; +use std::fmt::Write; +use std::path::{Path, PathBuf}; + +use tools::println_cargo_style; + +const SIDEBAR_HEAD: &str = "- [Welcome!](README)\n"; + +fn main() { + println_cargo_style!("Refresh: _sidebar.md"); + gen_all_sidebars(); +} + +/// Find all README.md under docs/, treat each as a site, and generate _sidebar.md for it. +fn gen_all_sidebars() { + let repo_root = find_git_repo().unwrap(); + let docs_root = repo_root.join("docs"); + + let readme_paths = find_all_readmes(&docs_root); + + for readme_path in &readme_paths { + let site_root = readme_path.parent().unwrap(); + + let content_dir = find_content_dir(site_root); + + if let Some(content_dir) = content_dir { + let lines = build_sidebar_content(site_root, &content_dir, SIDEBAR_HEAD); + + let sidebar_path = site_root.join("_sidebar.md"); + std::fs::write(&sidebar_path, lines).unwrap(); + println_cargo_style!("Generated: {}", sidebar_path.display()); + } + } +} + +/// Recursively find all README.md files under a directory. +fn find_all_readmes(dir: &Path) -> Vec { + let mut results = Vec::new(); + if let Ok(read_dir) = std::fs::read_dir(dir) { + let mut entries: Vec<_> = read_dir.flatten().collect(); + entries.sort_by_key(|e| e.path()); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + results.extend(find_all_readmes(&path)); + } else if path.file_name().is_some_and(|n| n == "README.md") { + results.push(path); + } + } + } + results +} + +/// Find the content directory for a site: +/// 1. Prefer `pages/` if it exists (backward compatible) +/// 2. Fall back to the first subdirectory that contains .md files +fn find_content_dir(site_root: &Path) -> Option { + // Try pages/ first + let pages_dir = site_root.join("pages"); + if pages_dir.exists() && pages_dir.is_dir() { + return Some(pages_dir); + } + + // Fall back to any subdirectory containing .md files + if let Ok(read_dir) = std::fs::read_dir(site_root) { + let mut entries: Vec<_> = read_dir.flatten().collect(); + entries.sort_by_key(|e| e.path()); + for entry in entries { + let path = entry.path(); + if path.is_dir() + && has_markdown_files(&path) { + return Some(path); + } + } + } + + None +} + +/// Check if a directory (recursively) contains any .md files. +fn has_markdown_files(dir: &Path) -> bool { + if let Ok(read_dir) = std::fs::read_dir(dir) { + for entry in read_dir.flatten() { + let path = entry.path(); + if path.is_dir() { + if has_markdown_files(&path) { + return true; + } + } else if path.extension().is_some_and(|ext| ext == "md") { + return true; + } + } + } + false +} + +/// Build sidebar content: scan .md files in `pages_dir` and return a formatted sidebar string +fn build_sidebar_content(base_dir: &Path, pages_dir: &Path, sidebar_head: &str) -> String { + let mut lines = String::from(sidebar_head); + + // Collect and sort entries at root level first + let mut root_files: Vec = Vec::new(); + // Subdirectory name -> its files + let mut sub_dirs: BTreeMap> = BTreeMap::new(); + + if let Ok(read_dir) = std::fs::read_dir(pages_dir) { + for entry in read_dir.flatten() { + let path = entry.path(); + if path.is_dir() { + let dir_name = entry.file_name().to_string_lossy().to_string(); + let entries = collect_markdown_files(&path, base_dir); + if !entries.is_empty() { + // Check for .name file to override directory display name + let display_name = get_directory_display_name(&path, &dir_name); + sub_dirs.insert(display_name, entries); + } + } else if path.extension().is_some_and(|ext| ext == "md") { + let title = extract_title(&path); + let relative = path + .strip_prefix(base_dir) + .unwrap() + .to_string_lossy() + .replace('\\', "/"); + let link = relative + .strip_suffix(".md") + .unwrap_or(&relative) + .to_string(); + root_files.push(SidebarEntry { title, link }); + } + } + } + + // Sort root files — natural order (1, 2, ..., 10, 11) + root_files.sort_by(|a, b| natural_cmp(&a.link, &b.link)); + + // Append root-level files + for f in &root_files { + let _ = writeln!(lines, "* [{}]({})", f.title, f.link); + } + + // Append subdirectory groups + for (dir_name, entries) in &sub_dirs { + let mut sorted_entries = entries.clone(); + sorted_entries.sort_by(|a, b| natural_cmp(&a.link, &b.link)); + + // Directory header with 2-space indent + let _ = writeln!(lines, "* {dir_name}"); + for f in &sorted_entries { + let _ = writeln!(lines, " * [{}]({})", f.title, f.link); + } + } + + lines +} + +#[derive(Clone)] +struct SidebarEntry { + title: String, + link: String, +} + +/// Collect all `.md` files directly under `dir` +fn collect_markdown_files(dir: &Path, base_dir: &Path) -> Vec { + let mut entries = Vec::new(); + + if let Ok(read_dir) = std::fs::read_dir(dir) { + for entry in read_dir.flatten() { + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "md") { + let title = extract_title(&path); + let relative = path + .strip_prefix(base_dir) + .unwrap() + .to_string_lossy() + .replace('\\', "/"); + let link = relative + .strip_suffix(".md") + .unwrap_or(&relative) + .to_string(); + entries.push(SidebarEntry { title, link }); + } + } + } + + entries +} + +/// Extract title from the first line `

TITLE

`. +/// Fallback to filename stem. +fn extract_title(path: &Path) -> String { + let content = std::fs::read_to_string(path).unwrap_or_default(); + if let Some(first_line) = content.lines().next() { + let trimmed = first_line.trim(); + // Find `>TITLE<` between `

` and `

` + if let Some(start) = trimmed.find('>') { + let after_start = &trimmed[start + 1..]; + if let Some(end) = after_start.find('<') { + return after_start[..end].to_string(); + } + } + } + // Fallback: use file stem + path.file_stem().map_or_else( + || "Untitled".to_string(), + |s| s.to_string_lossy().to_string(), + ) +} + +/// Read `.name` file inside a directory to get its display name for the sidebar. +/// Falls back to the directory name itself if no `.name` file exists. +fn get_directory_display_name(dir_path: &std::path::Path, fallback: &str) -> String { + let name_file = dir_path.join(".name"); + if name_file.exists() && name_file.is_file() { + std::fs::read_to_string(&name_file) + .ok() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| fallback.to_string()) + } else { + fallback.to_string() + } +} + +fn find_git_repo() -> Option { + let mut current_dir = std::env::current_dir().ok()?; + + loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + return Some(current_dir); + } + + if !current_dir.pop() { + break; + } + } + + None +} + +/// Natural (numeric-aware) comparison for sidebar links. +/// +/// Files prefixed with a number (e.g. `1-getting-started`) are sorted by that number; +/// files without a numeric prefix fall back to lexicographic order (after numbers). +fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { + let num_a = extract_leading_number(a); + let num_b = extract_leading_number(b); + num_a.cmp(&num_b).then_with(|| a.cmp(b)) +} + +/// Extract the leading numeric prefix from a sidebar link path. +/// +/// Looks at the filename stem (after the last `/`) for a number before the first `-`. +/// Returns `usize::MAX` for entries without a numeric prefix. +fn extract_leading_number(link: &str) -> usize { + if let Some(file_stem) = link.rsplit('/').next() + && let Some(num_end) = file_stem.find('-') + && let Ok(num) = file_stem[..num_end].parse::() { + return num; + } + usize::MAX +} diff --git a/.run/src/bin/http-page-preview.ps1 b/.run/src/bin/http-page-preview.ps1 new file mode 100644 index 0000000..8cc3579 --- /dev/null +++ b/.run/src/bin/http-page-preview.ps1 @@ -0,0 +1,3 @@ +$starting_dir = Get-Location +python -m http.server 3000 +Set-Location $starting_dir diff --git a/.run/src/bin/http-page-preview.sh b/.run/src/bin/http-page-preview.sh new file mode 100644 index 0000000..bed4b1c --- /dev/null +++ b/.run/src/bin/http-page-preview.sh @@ -0,0 +1,2 @@ +#!/bin/bash +python3 -m http.server 3000 diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1 new file mode 100644 index 0000000..bebe9ff --- /dev/null +++ b/.run/src/bin/install-mling.ps1 @@ -0,0 +1,7 @@ +cargo install --path mling + +New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null +# Copy all files containing _comp from the debug directory +Get-ChildItem .temp/target/release/*_comp* | ForEach-Object { + Copy-Item $_.FullName .temp/comp/ +} diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh new file mode 100644 index 0000000..5f2ee7a --- /dev/null +++ b/.run/src/bin/install-mling.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cargo install --path mling + +mkdir -p .temp/comp +cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found" diff --git a/.run/src/bin/refresh-docs.rs b/.run/src/bin/refresh-docs.rs new file mode 100644 index 0000000..82ef906 --- /dev/null +++ b/.run/src/bin/refresh-docs.rs @@ -0,0 +1,178 @@ +use std::path::Path; + +use just_fmt::snake_case; +use just_template::{Template, tmpl}; +use tools::println_cargo_style; + +const EXAMPLE_ROOT: &str = "./examples/"; +const OUTPUT_PATH: &str = "./mingling/src/example_docs.rs"; + +const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/example_docs.rs.tmpl"); + +fn main() { + gen_example_doc_module(); +} + +fn gen_example_doc_module() { + let mut template = Template::from(TEMPLATE_CONTENT); + let repo_root = find_git_repo().unwrap(); + let example_root = repo_root.join(EXAMPLE_ROOT); + let mut examples = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&example_root) { + for entry in entries.flatten() { + if let Ok(file_type) = entry.file_type() + && file_type.is_dir() + { + let example_name = entry.file_name().to_string_lossy().to_string(); + // Ignore directories that don't start with "example-" + if !example_name.starts_with("example-") { + continue; + } + let example_content = ExampleContent::read(&example_name); + examples.push(example_content); + } + } + } + + examples.sort(); + + for example in examples { + tmpl!(template += { + examples { + ( + example_header = example.header, + example_import = example.cargo_toml, + example_code = example.code, + example_name = snake_case!(&example.name) + ) + } + }); + println_cargo_style!("Refresh: {}", example.name); + } + + let template_str = template.to_string(); + let template_str = template_str + .lines() + .map(str::trim_end) + .collect::>() + .join("\n") + + "\n"; + std::fs::write(repo_root.join(OUTPUT_PATH), template_str).unwrap(); +} + +struct ExampleContent { + name: String, + header: String, + code: String, + cargo_toml: String, +} + +impl PartialOrd for ExampleContent { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for ExampleContent { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialEq for ExampleContent { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for ExampleContent {} + +impl ExampleContent { + pub fn read(name: &str) -> Self { + let repo = find_git_repo().unwrap(); + let cargo_toml = Self::read_cargo_toml(&repo, name); + let (header, code) = Self::read_header_and_code(&repo, name); + + let cargo_toml = cargo_toml + .lines() + .map(|line| format!("/// {line}")) + .collect::>() + .join("\n"); + + let header = header + .lines() + .map(|line| format!("/// {line}")) + .collect::>() + .join("\n"); + + let code = code + .lines() + .map(|line| format!("/// {line}")) + .collect::>() + .join("\n"); + + ExampleContent { + name: name.to_string(), + header, + code, + cargo_toml, + } + } + + fn read_header_and_code(repo: &Path, name: &str) -> (String, String) { + let file_path = repo + .join(EXAMPLE_ROOT) + .join(name) + .join("src") + .join("main.rs"); + let content = std::fs::read_to_string(&file_path).unwrap_or_default(); + let mut lines = content.lines(); + let mut header = String::new(); + let mut code = String::new(); + + // Collect header lines (starting with //!) + for line in lines.by_ref() { + if line.trim_start().starts_with("//!") { + let trimmed = line.trim_start_matches("//!"); + header.push_str(trimmed); + header.push('\n'); + } else { + // First non-header line found, start collecting code + code.push_str(line); + code.push('\n'); + break; + } + } + + // Collect remaining code lines + for line in lines { + code.push_str(line); + code.push('\n'); + } + + (header.trim().to_string(), code.trim().to_string()) + } + + fn read_cargo_toml(repo: &Path, name: &str) -> String { + let file_path = repo.join(EXAMPLE_ROOT).join(name).join("Cargo.toml"); + + std::fs::read_to_string(&file_path).unwrap_or_default() + } +} + +fn find_git_repo() -> Option { + let mut current_dir = std::env::current_dir().ok()?; + + loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + return Some(current_dir); + } + + if !current_dir.pop() { + break; + } + } + + None +} diff --git a/.run/src/bin/refresh-feature-mod.rs b/.run/src/bin/refresh-feature-mod.rs new file mode 100644 index 0000000..2255dbc --- /dev/null +++ b/.run/src/bin/refresh-feature-mod.rs @@ -0,0 +1,97 @@ +use std::collections::BTreeSet; +use std::path::Path; + +use just_fmt::snake_case; +use just_template::{tmpl, Template}; +use tools::println_cargo_style; + +const CARGO_TOML_PATH: &str = "./mingling/Cargo.toml"; +const OUTPUT_PATH: &str = "./mingling/src/features.rs"; + +const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/features.rs.tmpl"); + +fn main() { + gen_feature_module(); +} + +fn gen_feature_module() { + let repo_root = find_git_repo().unwrap(); + + let cargo_toml_path = repo_root.join(CARGO_TOML_PATH); + let output_path = repo_root.join(OUTPUT_PATH); + + let features = parse_features(&cargo_toml_path); + + let mut template = Template::from(TEMPLATE_CONTENT); + + for feat_name in &features { + let feat_const_name = snake_case!(feat_name).to_uppercase(); + + tmpl!(template += { + features { + ( + feat_name = feat_name, + feat_const_name = feat_const_name + ) + } + }); + println_cargo_style!("Refresh: feature `{}`", feat_name); + } + + let template_str = template.to_string(); + let template_str = template_str + .lines() + .map(str::trim_end) + .collect::>() + .join("\n") + + "\n"; + std::fs::write(&output_path, template_str).unwrap(); + + println_cargo_style!("Written: features module to {}", OUTPUT_PATH); +} + +/// Parse all feature names from the `[features]` section of a Cargo.toml. +fn parse_features(cargo_toml_path: &Path) -> Vec { + let content = std::fs::read_to_string(cargo_toml_path) + .unwrap_or_else(|e| panic!("Failed to read {}: {}", cargo_toml_path.display(), e)); + + let cargo_toml: toml::Value = content + .parse() + .unwrap_or_else(|e| panic!("Failed to parse {}: {}", cargo_toml_path.display(), e)); + + let features_table = cargo_toml + .get("features") + .and_then(|v| v.as_table()) + .unwrap_or_else(|| { + panic!( + "No [features] section found in {}", + cargo_toml_path.display() + ) + }); + + let mut feature_names: BTreeSet = BTreeSet::new(); + for key in features_table.keys() { + feature_names.insert(key.clone()); + } + + let mut result: Vec = feature_names.into_iter().collect(); + result.sort(); + result +} + +fn find_git_repo() -> Option { + let mut current_dir = std::env::current_dir().ok()?; + + loop { + let git_dir = current_dir.join(".git"); + if git_dir.exists() && git_dir.is_dir() { + return Some(current_dir); + } + + if !current_dir.pop() { + break; + } + } + + None +} diff --git a/.run/src/bin/sync-examples.rs b/.run/src/bin/sync-examples.rs new file mode 100644 index 0000000..0923b33 --- /dev/null +++ b/.run/src/bin/sync-examples.rs @@ -0,0 +1,131 @@ +use std::fs; +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use tools::println_cargo_style; + +#[derive(Serialize)] +struct ExampleMeta { + id: String, + name: String, + icon: String, + category: String, + desc: String, + tags: Vec, + files: Vec, +} + +#[derive(Deserialize)] +struct PageToml { + example: PageTomlExample, +} + +#[derive(Deserialize)] +struct PageTomlExample { + id: String, + #[serde(default)] + name: String, + #[serde(default = "default_icon")] + icon: String, + #[serde(default)] + category: String, + #[serde(default)] + desc: String, + #[serde(default)] + tags: Vec, + #[serde(default = "default_files")] + files: Vec, +} + +fn default_icon() -> String { + "📦".to_string() +} + +fn default_files() -> Vec { + vec!["Cargo.toml".to_string(), "src/main.rs".to_string()] +} + +fn main() { + #[cfg(windows)] + let _ = colored::control::set_virtual_terminal(true); + + let examples_dir = Path::new("examples"); + let output_dir = Path::new("docs/example-pages"); + fs::create_dir_all(output_dir).expect("failed to create docs/example-pages"); + + let mut examples: Vec = Vec::new(); + + let entries = fs::read_dir(examples_dir).expect("failed to read examples/"); + for entry in entries.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + let id = dir_name.to_string(); + let page_toml_path = path.join("page.toml"); + + let meta = if page_toml_path.exists() { + match fs::read_to_string(&page_toml_path) + .map_err(|e| e.to_string()) + .and_then(|content| toml::from_str::(&content).map_err(|e| e.to_string())) + { + Ok(page) => { + let ex = page.example; + ExampleMeta { + id: if ex.id.is_empty() { id.clone() } else { ex.id }, + name: if ex.name.is_empty() { + id.clone() + } else { + ex.name + }, + icon: ex.icon, + category: ex.category, + desc: ex.desc, + tags: ex.tags, + files: if ex.files.is_empty() { + default_files() + } else { + ex.files + }, + } + } + Err(e) => { + eprintln!( + "Warning: failed to parse {}: {}", + page_toml_path.display(), + e + ); + continue; + } + } + } else { + continue; + }; + + examples.push(meta); + } + + // Sort: basic first, then alphabetical + examples.sort_by(|a, b| { + if a.id == "example-basic" { + return std::cmp::Ordering::Less; + } + if b.id == "example-basic" { + return std::cmp::Ordering::Greater; + } + a.id.cmp(&b.id) + }); + + let json = serde_json::to_string_pretty(&examples).expect("failed to serialize"); + let output_path = output_dir.join("examples.json"); + fs::write(&output_path, &json).expect("failed to write examples.json"); + + println_cargo_style!( + "Sync: {} examples -> {}", + examples.len(), + output_path.display() + ); +} diff --git a/.run/src/bin/test-all-markdown-code.rs b/.run/src/bin/test-all-markdown-code.rs new file mode 100644 index 0000000..280fca7 --- /dev/null +++ b/.run/src/bin/test-all-markdown-code.rs @@ -0,0 +1,276 @@ +use std::collections::HashMap; +use std::env; +use std::path::{Path, PathBuf}; + +use colored::Colorize; +use indicatif::ProgressBar; +use tools::verify::{ + build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, generate_build_rs, + is_block_testable, parse_code_blocks, write_summary_report, +}; +use tools::{eprintln_cargo_style, println_cargo_style}; + +/// Config from verified-docs.toml +#[derive(serde::Deserialize)] +struct Config { + verified: VerifiedPaths, +} + +#[derive(serde::Deserialize)] +struct VerifiedPaths { + readme: String, + #[serde(default)] + documents_en_us: Option, + #[serde(default)] + documents_zh_cn: Option, +} + +#[tokio::main] +async fn main() { + #[cfg(windows)] + let _ = colored::control::set_virtual_terminal(true); + + let config_path = PathBuf::from("verified-docs.toml"); + if !config_path.exists() { + eprintln_cargo_style!("verified-docs.toml not found in current directory"); + std::process::exit(1); + } + + let config: Config = { + let content = std::fs::read_to_string(&config_path).unwrap_or_else(|_e| { + eprintln_cargo_style!("Failed to read verified-docs.toml"); + std::process::exit(1); + }); + toml::from_str(&content).unwrap_or_else(|_e| { + eprintln_cargo_style!("Failed to parse verified-docs.toml"); + std::process::exit(1); + }) + }; + + // Parse optional path argument from env args + let single_file: Option = { + let args: Vec = env::args().collect(); + if args.len() > 1 { + let p = PathBuf::from(&args[1]); + if p.exists() { + Some(p) + } else { + eprintln_cargo_style!("error: specified file '{}' does not exist", args[1]); + std::process::exit(1); + } + } else { + None + } + }; + + // Collect all markdown files from config + let mut files: Vec<(String, PathBuf)> = Vec::new(); + + // README + let readme_path = PathBuf::from(&config.verified.readme); + if readme_path.exists() { + files.push(("README".to_string(), readme_path)); + } + + // English docs + if let Some(pattern) = &config.verified.documents_en_us { + let base = pattern.trim_end_matches("/**").trim_end_matches('*'); + let dir = PathBuf::from(base); + if dir.exists() && dir.is_dir() { + collect_md_files(&dir, &mut files, "en"); + } + } + + // Chinese docs + if let Some(pattern) = &config.verified.documents_zh_cn { + let base = pattern.trim_end_matches("/**").trim_end_matches('*'); + let dir = PathBuf::from(base); + if dir.exists() && dir.is_dir() { + collect_md_files(&dir, &mut files, "zh_CN"); + } + } + + // If a single file was specified, filter the list to only that file + if let Some(ref target) = single_file { + let target_canon = std::fs::canonicalize(target).unwrap_or_else(|_| target.clone()); + files.retain(|(_, path)| { + std::fs::canonicalize(path) + .map(|p| p == target_canon) + .unwrap_or(false) + }); + if files.is_empty() { + eprintln_cargo_style!( + "error: specified file '{}' is not among the configured documentation files", + target.display() + ); + std::process::exit(1); + } + } + + if files.is_empty() { + eprintln_cargo_style!("No markdown files found to verify"); + std::process::exit(1); + } + + // Parse all code blocks into a flat list with global indices + let mut flat_blocks: Vec<(usize, tools::verify::CodeBlock)> = Vec::new(); + + for (label, path) in &files { + let content = std::fs::read_to_string(path).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read {}: {}", path.display(), e); + String::new() + }); + let source_file = format!("{label}/{}", path.file_name().unwrap().to_string_lossy()); + let blocks = parse_code_blocks(&content, &source_file); + let testable: Vec<_> = blocks.into_iter().filter(is_block_testable).collect(); + for block in testable { + let idx = flat_blocks.len() + 1; // 1-based global index + flat_blocks.push((idx, block)); + } + } + + let total_testable = flat_blocks.len(); + + if total_testable == 0 { + println_cargo_style!("No testable code blocks found"); + return; + } + + // Create a shared progress bar + let bar = ProgressBar::new(total_testable as u64); + bar.set_style( + indicatif::ProgressStyle::default_bar() + .template(&format!( + "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", + " Testing".bold().bright_cyan() + )) + .unwrap() + .progress_chars("=> "), + ); + bar.set_message("blocks"); + + // Group blocks by dependency hash + let mut groups: HashMap> = HashMap::new(); + for (idx, block) in flat_blocks { + let hash = compute_block_hash(&block); + groups.entry(hash).or_default().push((idx, block)); + } + + let temp_base = PathBuf::from(".temp/doc-test"); + + // Sort groups by hash for deterministic output order + let mut group_vec: Vec<(String, Vec<(usize, tools::verify::CodeBlock)>)> = + groups.into_iter().collect(); + group_vec.sort_by(|a, b| a.0.cmp(&b.0)); + + // Spawn a blocking task per group — groups run in parallel, blocks within a group are serial + let mut handles = Vec::new(); + for (hash, blocks) in group_vec { + let temp_base = temp_base.clone(); + let bar = bar.clone(); // clone shares the same underlying progress + let handle = tokio::task::spawn_blocking(move || { + let crate_dir = temp_base.join(&hash); + let src_dir = crate_dir.join("src"); + let manifest_path = crate_dir.join("Cargo.toml"); + + // Generate a single Cargo.toml for the whole group (all blocks share same deps) + let first_block = &blocks[0].1; + let cargo_toml = generate_cargo_toml(first_block, "test-doc", &manifest_path); + + let mut group_results: Vec<(String, usize, bool, String)> = Vec::new(); + for (block_idx, block) in &blocks { + let block_label = + format!("Block {block_idx} ({}:{})", block.source_file, block.line); + + bar.set_message(block_label.clone()); + + let main_rs = if block.is_build_time { + // For build-time blocks, write a stub main.rs and generate build.rs + generate_build_rs(block) + } else { + generate_main_rs(block) + }; + let (ok, err) = build_block( + &src_dir, + &manifest_path, + &cargo_toml, + &main_rs, + block.is_build_time, + ); + if ok { + bar.inc(1); + } else { + bar.inc(1); + bar.println(format!( + " {} {block_label}", + "failed".bold().bright_red() + )); + bar.println(format!(" {block_label} FAILED:\n{err}")); + } + group_results.push((block.source_file.clone(), block.line, ok, err)); + } + group_results + }); + handles.push(handle); + } + + // Collect results from all groups + let mut results: Vec<(String, usize, bool, String)> = Vec::new(); + let mut passed = 0usize; + let mut failed = 0usize; + + for handle in handles { + match handle.await { + Ok(group_results) => { + for (file, line, ok, err) in group_results { + if ok { + passed += 1; + } else { + failed += 1; + } + results.push((file, line, ok, err)); + } + } + Err(e) => { + eprintln_cargo_style!("Task panicked: {}", e); + std::process::exit(1); + } + } + } + + bar.finish_and_clear(); + + let result_msg = format!("Result: {passed}/{total_testable} blocks passed"); + println_cargo_style!(result_msg); + + write_summary_report( + Path::new(".temp/DOCS-TEST-RESULT.md"), + "Documentation Code Block Test Report", + &results, + total_testable, + passed, + failed, + ); + + if failed > 0 { + let fail_msg = format!("{failed} block(s) failed to build"); + eprintln_cargo_style!(fail_msg); + std::process::exit(1); + } + + println_cargo_style!("Done: All verified code blocks build successfully!"); +} + +/// Recursively collect all `.md` files under a directory +fn collect_md_files(dir: &Path, files: &mut Vec<(String, PathBuf)>, lang: &str) { + if let Ok(entries) = std::fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_md_files(&path, files, lang); + } else if path.extension().is_some_and(|ext| ext == "md") { + files.push((lang.to_string(), path)); + } + } + } +} diff --git a/.run/src/bin/test-all.ps1 b/.run/src/bin/test-all.ps1 new file mode 100644 index 0000000..231698a --- /dev/null +++ b/.run/src/bin/test-all.ps1 @@ -0,0 +1,8 @@ +$starting_dir = Get-Location +Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { + $project_dir = $_.DirectoryName + Push-Location $project_dir + cargo test + Pop-Location +} +Set-Location $starting_dir diff --git a/.run/src/bin/test-all.sh b/.run/src/bin/test-all.sh new file mode 100644 index 0000000..b387463 --- /dev/null +++ b/.run/src/bin/test-all.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +find . -name "Cargo.toml" -type f | while read -r cargo_file; do + project_dir=$(dirname "$cargo_file") + (cd "$project_dir" && cargo test) +done diff --git a/.run/src/bin/test-examples.rs b/.run/src/bin/test-examples.rs new file mode 100644 index 0000000..539459e --- /dev/null +++ b/.run/src/bin/test-examples.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +use colored::Colorize; +use indicatif::ProgressBar; +use serde::Deserialize; +use tools::{eprintln_cargo_style, println_cargo_style}; + +#[derive(Deserialize)] +struct TestConfig { + test: HashMap>, +} + +#[derive(Deserialize)] +struct TestCase { + command: String, + expect: Expect, +} + +#[derive(Deserialize)] +struct Expect { + #[serde(rename = "exit-code")] + exit_code: i32, + result: String, +} + +fn main() { + #[cfg(windows)] + let _ = colored::control::set_virtual_terminal(true); + + let config = load_config(); + + // Count total test cases upfront + let total: usize = config.test.values().map(|cases| cases.len()).sum(); + let bar = ProgressBar::new(total as u64); + bar.set_style( + indicatif::ProgressStyle::default_bar() + .template(&format!( + "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", + " Testing".bold().bright_cyan() + )) + .unwrap() + .progress_chars("=> "), + ); + bar.set_message("examples"); + + let passed = run_all_tests(&config, &bar); + + bar.finish_and_clear(); + + println_cargo_style!("Result: {}/{} tests passed", passed, total); + + if passed != total { + eprintln_cargo_style!("{} test(s) failed", total - passed); + std::process::exit(1); + } +} + +/// Parse test config from TOML file +fn load_config() -> TestConfig { + let content = std::fs::read_to_string("examples/test-examples.toml").unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to read TOML config file: {}", e); + std::process::exit(1); + }); + + toml::from_str(&content).unwrap_or_else(|e| { + eprintln_cargo_style!("Failed to parse TOML config: {}", e); + std::process::exit(1); + }) +} + +/// Run all example test groups, return number passed +fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize { + let mut passed = 0; + + for (example_name, test_cases) in &config.test { + bar.set_message(example_name.clone()); + + if !build_example(example_name) { + bar.inc(test_cases.len() as u64); + continue; + } + + for test_case in test_cases { + if run_single_test(example_name, test_case, bar) { + passed += 1; + } + bar.inc(1); + } + } + + passed +} + +/// Build the example binary, return true on success +fn build_example(example_name: &str) -> bool { + let manifest = format!("examples/{example_name}/Cargo.toml"); + tools::run_cmd_capture(format!( + "cargo build --manifest-path {manifest} --color always", + )) + .is_ok() +} + +/// Run a single test case, return true on pass +fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> bool { + let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name)); + let args: Vec<&str> = test_case.command.split_whitespace().collect(); + + let output = match std::process::Command::new(&binary_path) + .args(&args) + .output() + { + Ok(o) => o, + Err(e) => { + bar.println(format!("'{}' - failed to run: {}", test_case.command, e)); + return false; + } + }; + + let actual_exit_code = output.status.code().unwrap_or(-1); + let actual_stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + let actual_stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + + let exit_ok = actual_exit_code == test_case.expect.exit_code; + let result_ok = actual_stdout == test_case.expect.result + || actual_stdout.contains(&test_case.expect.result); + + if exit_ok && result_ok { + true + } else { + bar.println(format!("failed: '{}'", test_case.command)); + if !exit_ok { + bar.println(format!( + " Expected exit code: {}, actual: {}", + test_case.expect.exit_code, actual_exit_code + )); + } + if !result_ok { + bar.println(format!(" Expected output: {:?}", test_case.expect.result)); + bar.println(format!(" Actual stdout: {:?}", actual_stdout)); + if !actual_stderr.is_empty() { + bar.println(format!(" Actual stderr: {:?}", actual_stderr)); + } + } + false + } +} + +/// Resolve binary filename for the given example +/// +/// The binary name matches the package name. On Windows, the `.exe` suffix is required. +fn get_binary_name(example_name: &str) -> String { + let base = example_name; + if cfg!(target_os = "windows") { + format!("{base}.exe") + } else { + base.to_string() + } +} diff --git a/.run/src/bin/update-version.rs b/.run/src/bin/update-version.rs new file mode 100644 index 0000000..475e54b --- /dev/null +++ b/.run/src/bin/update-version.rs @@ -0,0 +1,98 @@ +use std::io::Write as _; +use std::path::Path; + +use serde::Deserialize; +use tools::println_cargo_style; + +#[derive(Deserialize)] +struct VersionFile { + file: String, + pattern: String, +} + +#[derive(Deserialize)] +struct Config { + #[serde(rename = "file")] + files: Vec, +} + +fn main() { + let args: Vec = std::env::args().collect(); + + // Get new version + let new_ver = if args.len() > 1 { + args[1].clone() + } else { + print!("Update version to: "); + std::io::stdout().flush().unwrap(); + let mut input = String::new(); + std::io::stdin().read_line(&mut input).unwrap(); + input.trim().to_string() + }; + + if new_ver.is_empty() { + eprintln!("Error: Version cannot be empty."); + std::process::exit(1); + } + + // Read current version from root Cargo.toml's workspace.package.version + let root_cargo_path = "Cargo.toml"; + let root_cargo_content = + std::fs::read_to_string(root_cargo_path).expect("Failed to read Cargo.toml"); + let cargo_value: toml::Value = root_cargo_content.parse().expect("Failed to parse Cargo.toml"); + + let current_ver = cargo_value["workspace"]["package"]["version"] + .as_str() + .expect("workspace.package.version not found in Cargo.toml") + .to_string(); + + if new_ver == current_ver { + println!("Version is already {}. Nothing to do.", current_ver); + return; + } + + println_cargo_style!("Version: {} -> {}", current_ver, new_ver); + + // Read version-files.toml + let config_path = Path::new("dev_tools").join("version-files.toml"); + let config_str = + std::fs::read_to_string(&config_path).expect("Failed to read dev_tools/version-files.toml"); + let config: Config = toml::from_str(&config_str) + .expect("Failed to parse dev_tools/version-files.toml"); + + let mut updated_count = 0; + let mut skipped_count = 0; + + for vf in &config.files { + let file_path = &vf.file; + let old_pattern = vf.pattern.replace("{VER}", ¤t_ver); + let new_pattern = vf.pattern.replace("{VER}", &new_ver); + + let content = match std::fs::read_to_string(file_path) { + Ok(c) => c, + Err(e) => { + eprintln!("Warning: Could not read {}: {}", file_path, e); + skipped_count += 1; + continue; + } + }; + + let new_content = content.replace(&old_pattern, &new_pattern); + + if new_content == content { + eprintln!( + "Warning: Pattern '{}' not found in {}", + old_pattern, file_path + ); + skipped_count += 1; + continue; + } + + std::fs::write(file_path, &new_content) + .unwrap_or_else(|e| panic!("Failed to write {}: {}", file_path, e)); + println_cargo_style!("Updated: {}", file_path); + updated_count += 1; + } + + println_cargo_style!("Done: {} file(s) updated, {} file(s) skipped", updated_count, skipped_count); +} diff --git a/.run/src/bin/windows-folder-hide.ps1 b/.run/src/bin/windows-folder-hide.ps1 new file mode 100644 index 0000000..0ab2632 --- /dev/null +++ b/.run/src/bin/windows-folder-hide.ps1 @@ -0,0 +1,43 @@ +# Check `last_check` + +$lastCheckFile = Join-Path $PSScriptRoot "last_check" +$currentTime = Get-Date +$timeThreshold = 10 + +if (Test-Path $lastCheckFile) { + $lastCheckTime = Get-Content $lastCheckFile | Get-Date + $timeDiff = ($currentTime - $lastCheckTime).TotalMinutes + + if ($timeDiff -lt $timeThreshold) { + exit + } +} + +$currentTime.ToString() | Out-File -FilePath $lastCheckFile -Force + +# Hide Files + +Set-Location -Path (Join-Path $PSScriptRoot "..\..") + +Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { + $_.FullName -notmatch '\\.temp\\' -and $_.FullName -notmatch '\\.git\\' +} | ForEach-Object { + attrib -h $_.FullName 2>&1 | Out-Null +} + +Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { + $_.Name -match '^\..*' -and $_.FullName -notmatch '\\\.\.$' -and $_.FullName -notmatch '\\\.$' +} | ForEach-Object { + attrib +h $_.FullName 2>&1 | Out-Null +} + +if (Get-Command git -ErrorAction SilentlyContinue) { + git status --ignored --short | ForEach-Object { + if ($_ -match '^!!\s+(.+)$') { + $ignoredPath = $matches[1] + if ($ignoredPath -notmatch '\.lnk$' -and (Test-Path $ignoredPath)) { + attrib +h $ignoredPath 2>&1 | Out-Null + } + } + } +} diff --git a/.run/src/lib.rs b/.run/src/lib.rs new file mode 100644 index 0000000..d38a156 --- /dev/null +++ b/.run/src/lib.rs @@ -0,0 +1,331 @@ +pub mod verify; + +use colored::Colorize; + +#[macro_export] +macro_rules! run_cmd { + ($fmt:literal, $($arg:tt)*) => { + $crate::run_cmd(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::run_cmd($cmd) + }; +} + +/// Run a shell command and capture its combined stdout+stderr output. +/// Returns `Ok(output)` on success, `Err((exit_code, stderr))` on failure. +#[macro_export] +macro_rules! run_cmd_and_capture_stderr { + ($fmt:literal, $($arg:tt)*) => { + $crate::run_cmd_capture(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::run_cmd_capture($cmd) + }; +} + +#[macro_export] +macro_rules! println_cargo_style { + ($fmt:literal, $($arg:tt)*) => { + $crate::println_cargo_style(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::println_cargo_style($cmd) + }; +} + +#[macro_export] +macro_rules! eprintln_cargo_style { + ($fmt:literal, $($arg:tt)*) => { + $crate::eprintln_cargo_style(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::eprintln_cargo_style($cmd) + }; +} + +#[macro_export] +macro_rules! wprintln_cargo_style { + ($fmt:literal, $($arg:tt)*) => { + $crate::wprintln_cargo_style(format!($fmt, $($arg)*)) + }; + ($cmd:expr) => { + $crate::wprintln_cargo_style($cmd) + }; +} + +/// Print a message in cargo style format, with bold green prefix. +/// +/// # Panics +/// +/// Panics if the prefix (text before the first `:`) exceeds 12 characters. +pub fn println_cargo_style(str: impl Into) { + let s = str.into(); + let (prefix, content) = if let Some(pos) = s.find(':') { + ( + s[..pos].trim().to_string(), + s[pos + 1..].trim_start().to_string(), + ) + } else { + (String::new(), s.trim().to_string()) + }; + + assert!( + prefix.len() <= 12, + "prefix length exceeds 12: '{}' has length {}", + prefix, + prefix.len() + ); + + let padding = " ".repeat(12 - prefix.len()); + + println!( + "{}{} {}", + padding, + prefix.bold().bright_green(), + content.trim() + ); +} + +pub fn eprintln_cargo_style(str: impl Into) { + println!("{}: {}", "error".bold().bright_red(), str.into()); +} + +/// Print a message in cargo style format, with bold yellow prefix (warning style). +/// +/// # Panics +/// +/// Panics if the prefix (text before the first `:`) exceeds 12 characters. +pub fn wprintln_cargo_style(str: impl Into) { + let s = str.into(); + let (prefix, content) = if let Some(pos) = s.find(':') { + ( + s[..pos].trim().to_string(), + s[pos + 1..].trim_start().to_string(), + ) + } else { + (String::new(), s.trim().to_string()) + }; + + assert!( + prefix.len() <= 12, + "prefix length exceeds 12: '{}' has length {}", + prefix, + prefix.len() + ); + + let padding = " ".repeat(12 - prefix.len()); + + println!( + "{}{} {}", + padding, + prefix.bold().bright_yellow(), + content.trim() + ); +} + +/// Run a shell command and return its exit status. +/// +/// # Panics +/// +/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found). +/// +/// # Errors +/// +/// Returns `Err` with the exit code if the command finishes with a non-zero exit code. +pub fn run_cmd(cmd: impl Into) -> Result<(), i32> { + let shell = if cfg!(target_os = "windows") { + "powershell" + } else { + "sh" + }; + let status = std::process::Command::new(shell) + .arg("-c") + .arg(cmd.into()) + .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) + .status() + .expect("failed to execute command"); + + let exit_code = status.code().unwrap_or(1); + if exit_code == 0 { + Ok(()) + } else { + Err(exit_code) + } +} + +/// Run a shell command and capture its combined stdout+stderr output. +/// +/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`. +/// Stderr falls back to stdout if stderr is empty. +pub fn run_cmd_capture(cmd: impl Into) -> Result { + let shell = if cfg!(target_os = "windows") { + "powershell" + } else { + "sh" + }; + let output = std::process::Command::new(shell) + .arg("-c") + .arg(cmd.into()) + .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) + .output() + .expect("failed to execute command"); + + let exit_code = output.status.code().unwrap_or(1); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let combined = if stderr.is_empty() { stdout } else { stderr }; + + if exit_code == 0 { + Ok(combined) + } else { + Err((exit_code, combined)) + } +} + +/// Extract a crate-style name from a `Cargo.toml` path. +/// +/// Examples: +/// - `mingling_core/Cargo.toml` → `mingling_core` +/// - `.` → `(root)` +pub fn crate_name_from(path: &std::path::Path) -> String { + path.parent() + .and_then(|p| p.file_name()) + .and_then(|n| n.to_str()) + .unwrap_or("(root)") + .to_string() +} + +/// Run a list of `(label_for_errors, crate_name_for_bar, shell_command)` tuples +/// in parallel with a progress bar. +/// +/// - Success: silent, the bar tracks progress: +/// ` Building [============================] 32/32: mingling_core` +/// - Failure: `pb.println()` prints the error immediately above the bar. +pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result<(), i32> { + let n = tasks.len(); + if n == 0 { + return Ok(()); + } + + // Cargo-style prefix: right-aligned to 12 chars, bold bright cyan + let padding = " ".repeat(12 - phase.len()); + let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); + + let pb = indicatif::ProgressBar::new(n as u64); + pb.set_style( + indicatif::ProgressStyle::default_bar() + .template(&format!( + "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", + styled_prefix + )) + .unwrap() + .progress_chars("=> "), + ); + pb.set_position(0); + + // Pre-extract labels for error messages + let labels: Vec = tasks.iter().map(|(l, _, _)| l.clone()).collect(); + + let (tx, rx) = std::sync::mpsc::channel::<(usize, String, Result)>(); + + for (i, (_label, crate_name, cmd)) in tasks.into_iter().enumerate() { + let tx = tx.clone(); + std::thread::spawn(move || { + let result = run_cmd_capture(&cmd); + let _ = tx.send((i, crate_name, result)); + }); + } + drop(tx); + + let mut first_exit_code = 0; + + while let Ok((i, crate_name, result)) = rx.recv() { + pb.inc(1); + pb.set_message(crate_name); + + if let Err((code, output)) = result { + if first_exit_code == 0 { + first_exit_code = code; + } + pb.println(format!( + "{}: {} failed (exit code {})", + "error".bright_red().bold(), + labels[i], + code, + )); + if !output.is_empty() { + for line in output.lines() { + pb.println(format!(" {line}")); + } + } + } + } + + pb.finish_and_clear(); + + if first_exit_code != 0 { + Err(first_exit_code) + } else { + Ok(()) + } +} + +/// Run a single shell command with a progress bar, capturing its output. +/// +/// - Success: bar clears silently. +/// - Failure: error is printed above the bar, then the bar clears. +pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<(), i32> { + let padding = " ".repeat(12 - phase.len()); + let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); + + let pb = indicatif::ProgressBar::new(1); + pb.set_style( + indicatif::ProgressStyle::default_bar() + .template(&format!( + "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", + styled_prefix + )) + .unwrap() + .progress_chars("=> "), + ); + pb.set_message(label.to_owned()); + + let result = run_cmd_capture(&cmd); + pb.inc(1); + pb.finish_and_clear(); + + match result { + Ok(_) => Ok(()), + Err((code, output)) => { + eprintln_cargo_style(format!("{} failed (exit code {})", label, code)); + if !output.is_empty() { + println!("{}", output.trim_end()); + } + Err(code) + } + } +} + +#[must_use] +pub fn cargo_tomls() -> Vec { + let mut cargo_tomls = Vec::new(); + let mut dirs = vec![std::path::PathBuf::from(".")]; + while let Some(dir) = dirs.pop() { + if let Ok(entries) = std::fs::read_dir(&dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + // Skip the dev_tools directory + if path.file_name().and_then(|n| n.to_str()) == Some("dev_tools") { + continue; + } + dirs.push(path); + } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") { + cargo_tomls.push(path); + } + } + } + } + cargo_tomls +} diff --git a/.run/src/verify.rs b/.run/src/verify.rs new file mode 100644 index 0000000..0a4b354 --- /dev/null +++ b/.run/src/verify.rs @@ -0,0 +1,511 @@ +use std::path::Path; + +use crate::println_cargo_style; + +/// Represents a parsed code block from a markdown file +#[derive(Debug, Clone)] +pub struct CodeBlock { + /// Source file path (for reporting) + pub source_file: String, + /// The line number in source file where this block starts + pub line: usize, + /// The raw Rust source code + pub code: String, + /// Feature flags extracted from `// Features: [...]` comment + pub features: Vec, + /// Whether the block had an explicit `// Features:` header + pub has_features_header: bool, + /// Whether the block has `// NOT VERIFIED` to opt out of testing + pub not_verified: bool, + /// External dependencies extracted from `// Dependencies:` comments + pub external_deps: Vec<(String, String)>, + /// Whether this block has a `fn main` entry point + pub has_main: bool, + /// Whether this block has `gen_program!()` call + pub has_gen_program: bool, + /// Whether this block has `// BUILD TIME` annotation (write to build.rs, not main.rs) + pub is_build_time: bool, +} + +/// Parse all ```rust code blocks from markdown content +pub fn parse_code_blocks(content: &str, source_file: &str) -> Vec { + let mut blocks = Vec::new(); + let lines: Vec<&str> = content.lines().collect(); + let mut i = 0; + + while i < lines.len() { + if lines[i].trim() == "```rust" { + if let Some(block) = parse_single_block(&lines, i, source_file) { + blocks.push(block); + } + i += 1; + while i < lines.len() && lines[i].trim() != "```" { + i += 1; + } + } + i += 1; + } + + blocks +} + +/// Parse a single code block starting at the ```rust line +fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option { + let line_num = start + 1; // 1-based line number + + let mut code_lines: Vec = Vec::new(); + let mut features: Vec = Vec::new(); + let mut has_features_header = false; + let mut not_verified = false; + let mut external_deps: Vec<(String, String)> = Vec::new(); + let mut has_main = false; + let mut has_gen_program = false; + let mut is_build_time = false; + + let mut idx = start + 1; + let mut in_header = true; + + while idx < lines.len() { + let raw_line = lines[idx]; + let trimmed = raw_line.trim(); + + if trimmed == "```" { + break; + } + + // @@@ lines: strip the prefix and treat as regular Rust code + // These lines are hidden in the rendered docs (filtered by a docsify plugin) + // but must still compile. + if let Some(stripped) = trimmed.strip_prefix("@@@") { + in_header = false; + // Strip @@@ and optionally one following space + let code = stripped.trim_start(); + if code.contains("fn main") { + has_main = true; + } + if code.contains("gen_program!") { + has_gen_program = true; + } + code_lines.push(code.to_string()); + idx += 1; + continue; + } + + // Parse header comments + // Check for NOT VERIFIED marker + if in_header && trimmed == "// NOT VERIFIED" { + not_verified = true; + idx += 1; + continue; + } + + if in_header && trimmed == "// BUILD TIME" { + is_build_time = true; + idx += 1; + continue; + } + + if in_header && trimmed.starts_with("// ") { + if trimmed.starts_with("// Features:") { + has_features_header = true; + let feat_str = trimmed.trim_start_matches("// Features:").trim(); + if feat_str.starts_with('[') && feat_str.ends_with(']') { + let inner = &feat_str[1..feat_str.len() - 1]; + if !inner.is_empty() { + features = inner + .split(',') + .map(|s| s.trim().trim_matches('"').to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + } + idx += 1; + continue; + } + if trimmed == "// Dependencies:" { + idx += 1; + // Collect subsequent `// crate = "version"` lines + while idx < lines.len() { + let next = lines[idx].trim(); + if next == "```" { + break; + } + if next.starts_with("// ") { + let dep_line = next.trim_start_matches("// ").trim(); + if let Some((name, ver)) = dep_line.split_once(" = ") { + external_deps.push(( + name.trim().to_string(), + ver.trim().trim_matches('"').to_string(), + )); + } + idx += 1; + } else { + break; + } + } + continue; + } + } + + in_header = false; + + if raw_line.contains("fn main") { + has_main = true; + } + if raw_line.contains("gen_program!") { + has_gen_program = true; + } + + code_lines.push(raw_line.to_string()); + idx += 1; + } + + if code_lines.is_empty() { + return None; + } + + Some(CodeBlock { + source_file: source_file.to_string(), + line: line_num, + code: code_lines.join("\n"), + features, + has_features_header, + not_verified, + external_deps, + has_main, + has_gen_program, + is_build_time, + }) +} + +/// Generate a Cargo.toml for a block +/// +/// `manifest_path` is the full path to the Cargo.toml file being written; it is used to +/// compute the relative path to the `mingling` crate. +pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str, manifest_path: &Path) -> String { + let features_str = if !block.features.is_empty() { + let feats: Vec = block.features.iter().map(|f| format!("\"{f}\"")).collect(); + format!("features = [{}]", feats.join(", ")) + } else { + String::new() + }; + + let mut extra_deps = String::new(); + for (name, version) in &block.external_deps { + if !version.starts_with('{') { + if name == "serde" || name == "clap" { + extra_deps.push_str(&format!( + "{name} = {{ version = \"{version}\", features = [\"derive\"] }}\n" + )); + } else { + extra_deps.push_str(&format!("{name} = \"{version}\"\n")); + } + } else { + extra_deps.push_str(&format!("{name} = {version}\n")); + } + } + + let mingling_path = find_mingling_relative_path(manifest_path); + + let deps_section = if features_str.is_empty() { + format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}",) + } else { + format!( + "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}", + ) + }; + + // Build-time blocks: add `builds` by default, merge with explicit features + let build_deps_section = if block.is_build_time { + let mut all_feats = vec!["builds".to_string()]; + for f in &block.features { + if f != "builds" { + all_feats.push(f.clone()); + } + } + let feats_str: Vec = all_feats.iter().map(|f| format!("\"{f}\"")).collect(); + let build_feats = format!("features = [{}]", feats_str.join(", ")); + format!( + "\n[build-dependencies]\nmingling = {{ path = \"{mingling_path}\", {build_feats} }}\n" + ) + } else { + String::new() + }; + + format!( + r#"[package] + name = "{package_name}" + version = "0.0.0" + edition = "2024" + +{deps_section}{build_deps_section} +[workspace] +"# + ) +} + +/// Compute the relative path from a Cargo.toml's parent directory to the `mingling` crate. +/// +/// The process current directory is expected to be the project root (where `mingling/` lives). +/// Returns a forward-slash path safe for embedding in TOML strings. +fn find_mingling_relative_path(manifest_path: &Path) -> String { + let manifest_dir = manifest_path + .parent() + .expect("manifest_path has no parent directory"); + let cwd = std::env::current_dir().expect("failed to get current directory"); + + // Strip cwd prefix to get the relative components of the manifest directory + let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir); + let depth = relative_to_root.components().count(); + + let mut result = String::new(); + for _ in 0..depth { + result.push_str("../"); + } + result.push_str("mingling"); + result +} + +/// Generate main.rs for a block +/// +/// Automatically prepends `use mingling::prelude::*;` if the block doesn't already have it. +pub fn generate_main_rs(block: &CodeBlock) -> String { + let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); + + if !block.code.contains("use mingling::prelude::*;") { + output.push_str("#[allow(unused_imports)]\nuse mingling::prelude::*;\n\n"); + } + + output.push_str(&block.code); + output.push('\n'); + + if !block.has_main { + output.push_str("\nfn main() {}\n"); + } + + if !block.has_gen_program { + output.push_str("\nmingling::macros::gen_program!();\n"); + } + + output +} + +/// Generate build.rs for a build-time block +/// +/// Default: `use mingling::builds::*;`, code wrapped in `fn main() { }`. +pub fn generate_build_rs(block: &CodeBlock) -> String { + let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); + + if !block.code.contains("use mingling::build::*;") { + output.push_str("#[allow(unused_imports)]\nuse mingling::build::*;\n\n"); + } + + if block.has_main { + output.push_str(&block.code); + } else { + output.push_str("fn main() {\n"); + for line in block.code.lines() { + output.push_str(" "); + output.push_str(line); + output.push('\n'); + } + output.push_str("}\n"); + } + + output +} + +/// Build a single code block as a Cargo project. +/// +/// When `is_build_time` is true, `src_content` is written to `build.rs` instead of `src/main.rs`, +/// and a minimal `src/main.rs` stub (`fn main() {}`) is created. +pub fn build_block( + src_dir: &Path, + manifest_path: &Path, + cargo_toml: &str, + src_content: &str, + is_build_time: bool, +) -> (bool, String) { + if let Err(e) = std::fs::create_dir_all(src_dir) { + return (false, format!("mkdir: {e}")); + } + + // Write Cargo.toml + if let Err(e) = std::fs::write(manifest_path, cargo_toml) { + return (false, format!("write Cargo.toml: {e}")); + } + + if is_build_time { + // Write build.rs and a stub main.rs + let crate_dir = manifest_path.parent().unwrap(); + if let Err(e) = std::fs::write(crate_dir.join("build.rs"), src_content) { + return (false, format!("write build.rs: {e}")); + } + if let Err(e) = std::fs::write(src_dir.join("main.rs"), "fn main() {}\n") { + return (false, format!("write main.rs: {e}")); + } + } else { + // Normal: write src/main.rs + if let Err(e) = std::fs::write(src_dir.join("main.rs"), src_content) { + return (false, format!("write main.rs: {e}")); + } + } + + // Check code — inherit stderr so cargo output is real-time and colored + let shell = if cfg!(target_os = "windows") { + "powershell" + } else { + "sh" + }; + let cmd = format!( + "cargo check --color=always --manifest-path {}", + manifest_path.to_string_lossy() + ); + + let mut child = match std::process::Command::new(shell) + .arg("-c") + .arg(&cmd) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::piped()) + .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) + .spawn() + { + Ok(c) => c, + Err(e) => return (false, format!("spawn: {e}")), + }; + + // Read stderr (buffered, not forwarded — groups print their own output contiguously) + use std::io::BufRead; + let stderr_handle = child.stderr.take().unwrap(); + let reader = std::io::BufReader::new(stderr_handle); + let mut captured = String::new(); + for line in reader.lines() { + match line { + Ok(l) => { + captured.push_str(&l); + captured.push('\n'); + } + Err(_) => break, + } + } + + let status = child.wait().unwrap_or_else(|_| std::process::exit(1)); + let exit_code = status.code().unwrap_or(1); + + if exit_code == 0 { + (true, String::new()) + } else { + let mut last_lines: Vec<&str> = captured.lines().rev().take(20).collect(); + last_lines.reverse(); + let detail = last_lines.join("\n"); + (false, format!("exit code {exit_code}\n{detail}")) + } +} + +/// Compute a stable hash for a code block based on its dependency configuration. +/// +/// Blocks with the same features and external dependencies produce the same hash, +/// allowing them to share a compiled crate and avoid redundant recompilation. +/// +/// Hash input (all sorted for stability): +/// - Sorted mingling feature strings +/// - Sorted external dependency names +/// - Sorted external dependency versions +/// - Sorted external deps as `name=version` pairs +pub fn compute_block_hash(block: &CodeBlock) -> String { + let mut features: Vec<&str> = block.features.iter().map(|s| s.as_str()).collect(); + features.sort(); + let features_str = features.join(","); + + let mut dep_names: Vec<&str> = block + .external_deps + .iter() + .map(|(n, _)| n.as_str()) + .collect(); + dep_names.sort(); + let dep_names_str = dep_names.join(","); + + let mut dep_versions: Vec<&str> = block + .external_deps + .iter() + .map(|(_, v)| v.as_str()) + .collect(); + dep_versions.sort(); + let dep_versions_str = dep_versions.join(","); + + let mut deps: Vec = block + .external_deps + .iter() + .map(|(n, v)| format!("{n}={v}")) + .collect(); + deps.sort(); + let deps_str = deps.join(","); + + let canonical = format!("{features_str}\n{dep_names_str}\n{dep_versions_str}\n{deps_str}"); + + // FNV-1a 64-bit hash — stable across runs (no random seed) + let mut hash: u64 = 0xcbf29ce484222325; + for &byte in canonical.as_bytes() { + hash ^= byte as u64; + hash = hash.wrapping_mul(0x100000001b3); + } + + format!("{:016x}", hash) +} + +/// Determine if a block should be treated as a test candidate. +/// A block is NOT testable only if it has `// NOT VERIFIED` marker. +pub fn is_block_testable(block: &CodeBlock) -> bool { + !block.not_verified +} + +/// Write a summary report +pub fn write_summary_report( + path: &Path, + title: &str, + results: &[(String, usize, bool, String)], + total: usize, + passed: usize, + failed: usize, +) { + let mut content = String::new(); + content.push_str(&format!("# {title}\n\n")); + content.push_str(&format!( + "Tested **{total}** code blocks: **{passed}** passed, **{failed}** failed.\n\n" + )); + content.push_str("## Results\n\n"); + content.push_str("| Block | File | Line | Status |\n"); + content.push_str("|-------|------|------|--------|\n"); + + for (i, (file, line, ok, _)) in results.iter().enumerate() { + let status = if *ok { "PASS" } else { "FAIL" }; + let short_file = file.rsplit('/').next().unwrap_or(file); + content.push_str(&format!( + "| {} | {} | {} | {status} |\n", + i + 1, + short_file, + line + )); + } + + let has_failures = results.iter().any(|(_, _, ok, _)| !ok); + if has_failures { + content.push_str("\n## Failed Blocks\n\n"); + for (i, (file, line, ok, err)) in results.iter().enumerate() { + if !ok { + content.push_str(&format!( + "### Block {} (`{}`, line {})\n\n```\n{err}\n```\n\n", + i + 1, + file, + line + )); + } + } + } + + std::fs::write(path, &content).unwrap_or_else(|e| { + eprintln!("Warning: failed to write {path:?}: {e}"); + }); + + println_cargo_style!("Report: written to {}", path.display()); +} diff --git a/.run/version-files.toml b/.run/version-files.toml new file mode 100644 index 0000000..ca104d2 --- /dev/null +++ b/.run/version-files.toml @@ -0,0 +1,19 @@ +[[file]] +file = "./Cargo.toml" +pattern = "version = \"{VER}\"" + +[[file]] +file = "./README.md" +pattern = "version = \"{VER}\"" + +[[file]] +file = "./docs/_zh_CN/pages/1-getting-started.md" +pattern = "version = \"{VER}\"" + +[[file]] +file = "./docs/pages/1-getting-started.md" +pattern = "version = \"{VER}\"" + +[[file]] +file = "./docs/res/guide.txt" +pattern = "mingling = \"{VER}\"" diff --git a/ABOUT-CI.md b/ABOUT-CI.md index 762fab8..4cbcd6f 100644 --- a/ABOUT-CI.md +++ b/ABOUT-CI.md @@ -1,6 +1,6 @@ # About Mingling CI Process -Mingling's CI process is built into the project, with its execution logic located in `dev_tools/src/bin/ci.rs`. You can run it locally via the `cargo ci` command, which produces the same results as the `CI` workflow in GitHub Actions. +Mingling's CI process is built into the project, with its execution logic located in `.run/src/bin/ci.rs`. You can run it locally via the `cargo ci` command, which produces the same results as the `CI` workflow in GitHub Actions. During development, you can run `cargo ci` at any time to verify that your code hasn't introduced regressions. @@ -10,7 +10,7 @@ An alias is defined in `.cargo/config.toml` at the project root: ```toml [alias] -ci = "run --manifest-path dev_tools/Cargo.toml --bin ci --quiet --" +ci = "run --manifest-path .run/Cargo.toml --bin ci --quiet --" ``` Simply execute: @@ -34,11 +34,11 @@ cargo ci - **Test all examples**: Runs the `test-examples` tool. - **Verify Markdown code blocks compile**: Runs the `test-all-markdown-code` tool to check code blocks in all `*.md` files. See [ABOUT_CODE_VERIFY](docs/_ABOUT_CODE_VERIFY.md) for details. - **Check if documentation is up to date**: Runs the following documentation refresh tools in sequence: - - `docs-code-box-fix` - - `docsify-sidebar-gen` - - `refresh-docs` - - `refresh-feature-mod` - - `sync-examples` + - `docs-code-box-fix` + - `docsify-sidebar-gen` + - `refresh-docs` + - `refresh-feature-mod` + - `sync-examples` - Finally, runs `cargo fmt` to unify code formatting. ### 3. File Normalization @@ -50,11 +50,11 @@ Runs `git add --renormalize .` to ensure file attributes such as line endings co To ensure reproducible CI results, `ci.rs` imposes strict requirements on the workspace state: - If the current workspace is not clean and `--dirty` has not been specified, the script will prompt whether to create a temporary commit: - - The commit message is `[DO NOT PUSH] CI TEMP [DO NOT PUSH]`. - - Use `-y` to auto-confirm without interaction. + - The commit message is `[DO NOT PUSH] CI TEMP [DO NOT PUSH]`. + - Use `-y` to auto-confirm without interaction. - After CI finishes, the script automatically restores the workspace: - - First, `git reset --hard` discards all changes. - - If a temporary commit was created, it then runs `git reset --soft HEAD~1` and unstages everything, restoring the state to before CI started. + - First, `git reset --hard` discards all changes. + - If a temporary commit was created, it then runs `git reset --soft HEAD~1` and unstages everything, restoring the state to before CI started. - If `--dirty` is specified, the temporary commit and the final cleanliness check are skipped. > **Warning**: `git reset --hard` is executed at the end of CI. If you use `--dirty`, ensure you have no unsaved important changes. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d90db4a..e8e4bed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,46 +6,46 @@ Before contributing, we recommend reading [README](README.md) to get an overview ## 1. Project Structure -| Category | Path/Name | Description | -| ---------------------------- | ------------------------- | ------------------------------------------------------------------------------------- | -| **Entry crate** | `mingling/` | Project entry point | -| **Core library** | `mingling_core/` | Imported as an external dependency | -| **Macro library** | `mingling_macros/` | Imported as an external dependency | -| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | -| **Documentation, resources** | `docs/` | All documentation and resource files | -| **Development tools** | `dev_tools/` | Contains scripts and Rust tools | -| Scripts | `dev_tools/scripts/` | Helper `.sh`/`.ps1`/`.py` scripts, executed via `./run-tools.sh` or `.\run-tools.ps1` | -| Rust tools | `dev_tools/src/bin/` | Same as above | -| CI check entry | `dev_tools/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | -| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | -| **Temporary files** | `.temp/` | Ignored by `.gitignore` | +| Category | Path/Name | Description | +| ---------------------------- | ------------------------- | ------------------------------------------------------------------------- | +| **Entry crate** | `mingling/` | Project entry point | +| **Core library** | `mingling_core/` | Imported as an external dependency | +| **Macro library** | `mingling_macros/` | Imported as an external dependency | +| **Examples** | `examples/` | To add expected output tests, modify `examples/test-examples.toml` | +| **Documentation, resources** | `docs/` | All documentation and resource files | +| **Development tools** | `.run/src/bin` | Contains scripts and Rust tools | +| Scripts | `.run/src/bin` | Helper `.sh`/`.ps1`/`.py` scripts, executed via `./run.sh` or `.\run.ps1` | +| Rust tools | `dev_tools/src/bin/` | Same as above | +| CI check entry | `dev_tools/src/bin/ci.rs` | Can be invoked directly via `cargo ci` | +| **Scaffolding tool** | `mling/` | Scaffolding tool `mingling-cli` | +| **Temporary files** | `.temp/` | Ignored by `.gitignore` | ## 2. Submission Guide 1. **Pull Request** - - Submit a GitHub Pull Request and @Reviewer **[Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)** for review - - Or send patches to **catil_grass@qq.com** + - Submit a GitHub Pull Request and @Reviewer **[Weicao-CatilGrass](https://github.com/Weicao-CatilGrass)** for review + - Or send patches to **catil_grass@qq.com** 2. **Commit Messages** - - Clearly and concisely describe the changes, no stringent requirements - - Provide more detail for complex changes, keep it brief for simple changes - - But: if you use [Conventional Commits](https://www.conventionalcommits.org/), it would make me even happier :) + - Clearly and concisely describe the changes, no stringent requirements + - Provide more detail for complex changes, keep it brief for simple changes + - But: if you use [Conventional Commits](https://www.conventionalcommits.org/), it would make me even happier :) 3. **CHANGELOG** - - If the submission includes functional changes or fixes, **the PR must include modifications to CHANGELOG.md** to describe the changes - - For minor changes like typo fixes, **CHANGELOG.md modification is not required**, and we will merge faster + - If the submission includes functional changes or fixes, **the PR must include modifications to CHANGELOG.md** to describe the changes + - For minor changes like typo fixes, **CHANGELOG.md modification is not required**, and we will merge faster 4. **Multi-commit PR** - - A PR can contain multiple commits - - However, at least one commit must modify CHANGELOG.md + - A PR can contain multiple commits + - However, at least one commit must modify CHANGELOG.md 5. **Review** - - After submission, please notify [Weicao-CatilGrass](https://github.com/Weicao-CatilGrass) for review — this is the most efficient way to get feedback + - After submission, please notify [Weicao-CatilGrass](https://github.com/Weicao-CatilGrass) for review — this is the most efficient way to get feedback 6. **Binary Resources** - - For binary resource files (images, etc.), please be cautious about adding them to avoid repository bloat + - For binary resource files (images, etc.), please be cautious about adding them to avoid repository bloat ## 3. Documentation Contribution @@ -60,11 +60,11 @@ After editing documentation, refresh relevant files: ```bash # Refresh sidebar and README sync -./run-tools.sh docsify-sidebar-gen -./run-tools.sh refresh-docs +./run.sh docsify-sidebar-gen +./run.sh refresh-docs # Fix code block blank line issues -./run-tools.sh docs-code-box-fix +./run.sh docs-code-box-fix ``` These steps are included in `cargo ci`; running `cargo ci` will execute them automatically. diff --git a/Cargo.toml b/Cargo.toml index dba7264..201f88e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ exclude = [ "examples/*", # Dev Tools - "dev_tools", + ".run", # Tests "mingling_core/tests/*", diff --git a/dev_tools/Cargo.lock b/dev_tools/Cargo.lock deleted file mode 100644 index bb510bd..0000000 --- a/dev_tools/Cargo.lock +++ /dev/null @@ -1,435 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "colored" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "console" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width", - "windows-sys", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "indicatif" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" -dependencies = [ - "console", - "portable-atomic", - "unicode-width", - "unit-prefix", - "web-time", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "just_fmt" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5454cda0d57db59778608d7a47bff5b16c6705598265869fb052b657f66cf05e" - -[[package]] -name = "just_template" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" -dependencies = [ - "just_fmt", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned", - "toml_datetime", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "tools" -version = "0.1.0" -dependencies = [ - "colored", - "indicatif", - "just_fmt", - "just_template", - "serde", - "serde_json", - "tokio", - "toml", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - -[[package]] -name = "wasm-bindgen" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.125" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/dev_tools/Cargo.toml b/dev_tools/Cargo.toml deleted file mode 100644 index 9855580..0000000 --- a/dev_tools/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "tools" -version = "0.1.0" -edition = "2024" -authors = ["Weicao-CatilGrass"] -description = "Development tools for mingling" -license = "MIT OR Apache-2.0" -repository = "https://github.com/catilgrass/mingling" -readme = "../README.md" -keywords = ["cli", "development", "tools"] -categories = ["command-line-interface", "development-tools"] - -[dependencies] -just_template = "0.1.3" -just_fmt = "0.1.2" -colored = "3.1.1" -toml = "0.8" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -indicatif = "0.18.4" diff --git a/dev_tools/scripts/build-all.ps1 b/dev_tools/scripts/build-all.ps1 deleted file mode 100644 index 4f35ed8..0000000 --- a/dev_tools/scripts/build-all.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$starting_dir = Get-Location -Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { - $project_dir = $_.DirectoryName - Push-Location $project_dir - cargo build - Pop-Location -} -Set-Location $starting_dir diff --git a/dev_tools/scripts/build-all.sh b/dev_tools/scripts/build-all.sh deleted file mode 100755 index 2036b41..0000000 --- a/dev_tools/scripts/build-all.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -find . -name "Cargo.toml" -type f | while read -r cargo_file; do - project_dir=$(dirname "$cargo_file") - (cd "$project_dir" && cargo build) -done diff --git a/dev_tools/scripts/clippy-fix.ps1 b/dev_tools/scripts/clippy-fix.ps1 deleted file mode 100644 index 1d24f92..0000000 --- a/dev_tools/scripts/clippy-fix.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$starting_dir = Get-Location -Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { - $project_dir = $_.DirectoryName - Push-Location $project_dir - cargo clippy --fix --allow-dirty --allow-no-vcs --quiet - Pop-Location -} -Set-Location $starting_dir diff --git a/dev_tools/scripts/clippy-fix.sh b/dev_tools/scripts/clippy-fix.sh deleted file mode 100755 index 9771ad4..0000000 --- a/dev_tools/scripts/clippy-fix.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -find . -name "Cargo.toml" -type f | while read -r cargo_file; do - project_dir=$(dirname "$cargo_file") - (cd "$project_dir" && cargo clippy --fix --allow-dirty --allow-no-vcs --quiet) -done diff --git a/dev_tools/scripts/clippy.ps1 b/dev_tools/scripts/clippy.ps1 deleted file mode 100644 index 1858873..0000000 --- a/dev_tools/scripts/clippy.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$starting_dir = Get-Location -Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { - $project_dir = $_.DirectoryName - Push-Location $project_dir - cargo clippy --quiet - Pop-Location -} -Set-Location $starting_dir diff --git a/dev_tools/scripts/clippy.sh b/dev_tools/scripts/clippy.sh deleted file mode 100755 index b393545..0000000 --- a/dev_tools/scripts/clippy.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -find . -name "Cargo.toml" -type f | while read -r cargo_file; do - project_dir=$(dirname "$cargo_file") - (cd "$project_dir" && cargo clippy --quiet) -done diff --git a/dev_tools/scripts/doc.ps1 b/dev_tools/scripts/doc.ps1 deleted file mode 100755 index 987f0de..0000000 --- a/dev_tools/scripts/doc.ps1 +++ /dev/null @@ -1 +0,0 @@ -cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open diff --git a/dev_tools/scripts/doc.sh b/dev_tools/scripts/doc.sh deleted file mode 100755 index 5e8a311..0000000 --- a/dev_tools/scripts/doc.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -cargo doc --workspace --no-deps --features builds,structural_renderer,repl,comp,parser,clap,extra_macros --open diff --git a/dev_tools/scripts/http-page-preview.ps1 b/dev_tools/scripts/http-page-preview.ps1 deleted file mode 100644 index 8cc3579..0000000 --- a/dev_tools/scripts/http-page-preview.ps1 +++ /dev/null @@ -1,3 +0,0 @@ -$starting_dir = Get-Location -python -m http.server 3000 -Set-Location $starting_dir diff --git a/dev_tools/scripts/http-page-preview.sh b/dev_tools/scripts/http-page-preview.sh deleted file mode 100755 index bed4b1c..0000000 --- a/dev_tools/scripts/http-page-preview.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -python3 -m http.server 3000 diff --git a/dev_tools/scripts/install-mling.ps1 b/dev_tools/scripts/install-mling.ps1 deleted file mode 100755 index bebe9ff..0000000 --- a/dev_tools/scripts/install-mling.ps1 +++ /dev/null @@ -1,7 +0,0 @@ -cargo install --path mling - -New-Item -ItemType Directory -Force -Path .temp/comp | Out-Null -# Copy all files containing _comp from the debug directory -Get-ChildItem .temp/target/release/*_comp* | ForEach-Object { - Copy-Item $_.FullName .temp/comp/ -} diff --git a/dev_tools/scripts/install-mling.sh b/dev_tools/scripts/install-mling.sh deleted file mode 100755 index 5f2ee7a..0000000 --- a/dev_tools/scripts/install-mling.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -cargo install --path mling - -mkdir -p .temp/comp -cp .temp/target/release/*_comp.* .temp/comp/ 2>/dev/null || echo "No matching files found" diff --git a/dev_tools/scripts/test-all.ps1 b/dev_tools/scripts/test-all.ps1 deleted file mode 100644 index 231698a..0000000 --- a/dev_tools/scripts/test-all.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -$starting_dir = Get-Location -Get-ChildItem -Recurse -Filter "Cargo.toml" | ForEach-Object { - $project_dir = $_.DirectoryName - Push-Location $project_dir - cargo test - Pop-Location -} -Set-Location $starting_dir diff --git a/dev_tools/scripts/test-all.sh b/dev_tools/scripts/test-all.sh deleted file mode 100644 index b387463..0000000 --- a/dev_tools/scripts/test-all.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -find . -name "Cargo.toml" -type f | while read -r cargo_file; do - project_dir=$(dirname "$cargo_file") - (cd "$project_dir" && cargo test) -done diff --git a/dev_tools/scripts/windows-folder-hide.ps1 b/dev_tools/scripts/windows-folder-hide.ps1 deleted file mode 100644 index 0ab2632..0000000 --- a/dev_tools/scripts/windows-folder-hide.ps1 +++ /dev/null @@ -1,43 +0,0 @@ -# Check `last_check` - -$lastCheckFile = Join-Path $PSScriptRoot "last_check" -$currentTime = Get-Date -$timeThreshold = 10 - -if (Test-Path $lastCheckFile) { - $lastCheckTime = Get-Content $lastCheckFile | Get-Date - $timeDiff = ($currentTime - $lastCheckTime).TotalMinutes - - if ($timeDiff -lt $timeThreshold) { - exit - } -} - -$currentTime.ToString() | Out-File -FilePath $lastCheckFile -Force - -# Hide Files - -Set-Location -Path (Join-Path $PSScriptRoot "..\..") - -Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { - $_.FullName -notmatch '\\.temp\\' -and $_.FullName -notmatch '\\.git\\' -} | ForEach-Object { - attrib -h $_.FullName 2>&1 | Out-Null -} - -Get-ChildItem -Path . -Force -Recurse -ErrorAction SilentlyContinue | Where-Object { - $_.Name -match '^\..*' -and $_.FullName -notmatch '\\\.\.$' -and $_.FullName -notmatch '\\\.$' -} | ForEach-Object { - attrib +h $_.FullName 2>&1 | Out-Null -} - -if (Get-Command git -ErrorAction SilentlyContinue) { - git status --ignored --short | ForEach-Object { - if ($_ -match '^!!\s+(.+)$') { - $ignoredPath = $matches[1] - if ($ignoredPath -notmatch '\.lnk$' -and (Test-Path $ignoredPath)) { - attrib +h $ignoredPath 2>&1 | Out-Null - } - } - } -} diff --git a/dev_tools/src/bin/ci.rs b/dev_tools/src/bin/ci.rs deleted file mode 100644 index f8aed79..0000000 --- a/dev_tools/src/bin/ci.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::io::Write as _; -use std::process::exit; - -use tools::{ - cargo_tomls, crate_name_from, eprintln_cargo_style, println_cargo_style, run_cmd, run_parallel, -}; - -fn get_ignore_dirs() -> Vec { - vec![ - ".temp".to_string(), - "mling/res".to_string(), - "mling\\res".to_string(), - ] -} - -fn print_help() { - println!( - r" -Usage: ci [options] -Options: - -h, --help Print this help message - -y Auto-confirm temporary commits - --dirty Run CI on dirty workspace (skip temp commit & clean check) - --refresh-docs Refresh documentation files - --test-docs Run documentation tests (build, clippy, test) - --test-codes Test examples and documentation code blocks - -If no specific options are given, all checks are run. - " - ); -} - -fn main() { - #[cfg(windows)] - let _ = colored::control::set_virtual_terminal(true); - println!("{}", include_str!("../../../docs/res/ci_banner.txt")); - - let args: Vec = std::env::args().collect(); - - if args.iter().any(|a| a == "-h" || a == "--help") { - print_help(); - return; - } - - let auto_yes = args.iter().any(|a| a == "-y"); - let dirty = args.iter().any(|a| a == "--dirty"); - - let test_docs = args.iter().any(|a| a == "--test-docs"); - let refresh_docs = args.iter().any(|a| a == "--refresh-docs"); - let test_codes = args.iter().any(|a| a == "--test-codes"); - let any_specified = test_docs || refresh_docs || test_codes; - let run_all = !any_specified; - - let needs_commit_temp = !dirty && !{ run_cmd!("git diff-index --quiet HEAD --").is_ok() }; - - if needs_commit_temp { - if auto_yes { - run_cmd!("git add .").unwrap(); - run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap(); - } else { - print!("Working tree is not clean, temporarily commit? [y/N]:"); - std::io::stdout().flush().unwrap(); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap(); - let input = input.trim(); - if input == "y" || input == "Y" || input == "yes" || input == "Yes" { - run_cmd!("git add .").unwrap(); - run_cmd!("git commit -m \"[DO NOT PUSH] CI TEMP [DO NOT PUSH]\"").unwrap(); - } else { - eprintln_cargo_style!("Aborting."); - exit(2) - } - } - } - - if let Err(exit_code) = ci(test_docs, test_codes, run_all) { - restore_workspace(needs_commit_temp).unwrap(); - exit(exit_code) - } - - if !dirty { - let is_worktree_clean = run_cmd!("git diff-index --quiet HEAD --").is_ok(); - if !is_worktree_clean { - eprintln_cargo_style!("The repository was contaminated during CI, failing!"); - - // Print git status - println!(); - let _ = run_cmd!("git status"); - - if needs_commit_temp { - restore_workspace(true).unwrap(); - } - exit(1) - } - } - - println_cargo_style!("Done: All check passed!"); - - if needs_commit_temp { - restore_workspace(true).unwrap(); - } -} - -fn restore_workspace(undo_commit: bool) -> Result<(), i32> { - run_cmd!("git reset --hard --quiet")?; - if undo_commit { - run_cmd!("git reset --soft HEAD~1 --quiet")?; - run_cmd!("git reset --quiet")?; - } - Ok(()) -} - -fn ci(test_docs: bool, test_codes: bool, run_all: bool) -> Result<(), i32> { - if run_all || test_codes { - println_cargo_style!("Phase: Scan and build all crates"); - build_all()?; - - println_cargo_style!("Phase: Run clippy for all crates"); - clippy_all()?; - - println_cargo_style!("Phase: Test all crates"); - test_all()?; - } - - if run_all || test_docs { - println_cargo_style!("Phase: Test all examples"); - test_examples()?; - - println_cargo_style!("Phase: Verify all *.md document code blocks are compilable"); - test_docs_code_blocks()?; - - println_cargo_style!("Phase: Check all documentation is up to date"); - docs_refresh()?; - } - - run_cmd!("git add --renormalize .")?; - - Ok(()) -} - -fn test_examples() -> Result<(), i32> { - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-examples") -} - -fn test_docs_code_blocks() -> Result<(), i32> { - run_cmd!( - "cargo run --manifest-path dev_tools/Cargo.toml --color always --bin test-all-markdown-code" - ) -} - -fn build_all() -> Result<(), i32> { - let ignore_dirs = get_ignore_dirs(); - let cargo_tomls = cargo_tomls(); - let mut tasks = Vec::new(); - for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); - let path_str = path.to_string_lossy(); - if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { - continue; - } - let label = format!("Build: {}", cargo_toml.to_string_lossy()); - let crate_name = crate_name_from(&cargo_toml); - let cmd = format!( - "cargo build --manifest-path {} --color always", - cargo_toml.to_string_lossy() - ); - tasks.push((label, crate_name, cmd)); - } - run_parallel("Building", tasks) -} - -fn clippy_all() -> Result<(), i32> { - let ignore_dirs = get_ignore_dirs(); - let cargo_tomls = cargo_tomls(); - let mut tasks = Vec::new(); - for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); - let path_str = path.to_string_lossy(); - if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { - continue; - } - let label = format!("Clippy: {}", cargo_toml.to_string_lossy()); - let crate_name = crate_name_from(&cargo_toml); - let cmd = format!( - "cargo clippy --manifest-path {} --color always -- -D warnings", - cargo_toml.to_string_lossy() - ); - tasks.push((label, crate_name, cmd)); - } - run_parallel("Clippy", tasks) -} - -fn test_all() -> Result<(), i32> { - let ignore_dirs = get_ignore_dirs(); - let cargo_tomls = cargo_tomls(); - let mut tasks = Vec::new(); - for cargo_toml in cargo_tomls { - let path = cargo_toml.parent().unwrap_or(std::path::Path::new("")); - let path_str = path.to_string_lossy(); - if ignore_dirs.iter().any(|d| path_str.contains(d.as_str())) { - continue; - } - let label = format!("Testing: {}", cargo_toml.to_string_lossy()); - let crate_name = crate_name_from(&cargo_toml); - let cmd = format!( - "cargo test --manifest-path {} --color always", - cargo_toml.to_string_lossy() - ); - tasks.push((label, crate_name, cmd)); - } - run_parallel("Testing", tasks) -} - -fn docs_refresh() -> Result<(), i32> { - println_cargo_style!("Refresh: document at `./docs/`"); - - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docs-code-box-fix")?; - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin docsify-sidebar-gen")?; - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-docs")?; - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin refresh-feature-mod")?; - run_cmd!("cargo run --manifest-path dev_tools/Cargo.toml --bin sync-examples")?; - run_cmd!("cargo fmt")?; - - Ok(()) -} diff --git a/dev_tools/src/bin/docs-code-box-fix.rs b/dev_tools/src/bin/docs-code-box-fix.rs deleted file mode 100644 index 21d2cce..0000000 --- a/dev_tools/src/bin/docs-code-box-fix.rs +++ /dev/null @@ -1,166 +0,0 @@ -use std::fs; -use std::path::Path; - -use tools::println_cargo_style; - -/// Docsify code blocks require that blank lines before and after code blocks are not completely empty, -/// but must contain at least one space, otherwise code block rendering will have issues. -/// -/// This tool scans all `.md` files in the docs directory, -/// and replaces completely empty lines before and after code blocks with blank lines containing a single space. -const DOCS_DIR: &str = "./docs"; - -fn main() { - println_cargo_style!("Fixing: code box empty lines in docs/**/*.md ..."); - let repo_root = find_git_repo().expect("Cannot find git repo root"); - let docs_dir = repo_root.join(DOCS_DIR); - - let mut fixed_count = 0; - let mut file_count = 0; - - collect_md_files(&docs_dir, &mut |path| { - if let Some(name) = path.file_name() { - let name = name.to_string_lossy(); - if name.to_lowercase() == "_sidebar.md" { - return; - } - } - - let content = fs::read_to_string(path).unwrap_or_default(); - if content.is_empty() { - return; - } - - let new_content = fix_code_box_empty_lines(&content); - if new_content != content { - fs::write(path, &new_content).unwrap(); - println_cargo_style!("Fixed: {}", path.display()); - fixed_count += 1; - } - file_count += 1; - }); - - println_cargo_style!( - "Done: Scanned {} files, fixed {} files.", - file_count, - fixed_count - ); -} - -fn fix_code_box_empty_lines(content: &str) -> String { - let mut result = String::new(); - let lines: Vec<&str> = content.lines().collect(); - let len = lines.len(); - - let mut i = 0; - while i < len { - let line = lines[i]; - - // detect beginning of code block: beginning with ``` - if line.trim_start().starts_with("```") { - // record the beginning line of the code block - result.push_str(line); - result.push('\n'); - i += 1; - - // find the end of the code block - let mut found_end = false; - let code_start = i; // record starting position of code content - let mut code_end = len; // index of code block end line - - while i < len { - let cline = lines[i]; - if cline.trim_start().starts_with("```") && cline.trim() != "" { - // this is the closing marker - code_end = i; - found_end = true; - break; - } - i += 1; - } - - // check the blank line before the code block - // if result ends with \n\n, add a space to turn it into \n \n - ensure_space_before_code_block(&mut result); - - // output code content - for code_line in lines.iter().take(code_end).skip(code_start) { - if code_line.is_empty() { - result.push(' '); - } else { - result.push_str(code_line); - } - result.push('\n'); - } - - if found_end { - result.push_str(lines[code_end]); - result.push('\n'); - i += 1; - - // check the blank line after the code block - // if the next line is blank, change it to one with a space - if i < len && lines[i].trim().is_empty() && lines[i].is_empty() { - // skip the original blank line, write " \n" - result.push(' '); - result.push('\n'); - i += 1; - } - } - } else { - result.push_str(line); - result.push('\n'); - i += 1; - } - } - - // remove trailing newlines - while result.ends_with('\n') { - result.pop(); - } - result.push('\n'); - - result -} - -/// ensure there is a blank line with a space before the code block -fn ensure_space_before_code_block(result: &mut String) { - // if result ends with \n\n, - // turn it into \n \n - let len = result.len(); - if len >= 2 && result[len - 2..] == *"\n\n" { - // insert a space before the last \n - result.insert(len - 1, ' '); - } -} - -/// recursively collect all .md files in the docs directory -fn collect_md_files(dir: &Path, callback: &mut dyn FnMut(&Path)) { - if let Ok(entries) = fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - collect_md_files(&path, callback); - } else if path.extension().is_some_and(|ext| ext == "md") { - callback(&path); - } - } - } -} - -fn find_git_repo() -> Option { - let mut current_dir = std::env::current_dir().ok()?; - - loop { - let git_dir = current_dir.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - return Some(current_dir); - } - - if !current_dir.pop() { - break; - } - } - - None -} diff --git a/dev_tools/src/bin/docsify-sidebar-gen.rs b/dev_tools/src/bin/docsify-sidebar-gen.rs deleted file mode 100644 index b926112..0000000 --- a/dev_tools/src/bin/docsify-sidebar-gen.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::collections::BTreeMap; -use std::fmt::Write; -use std::path::{Path, PathBuf}; - -use tools::println_cargo_style; - -const SIDEBAR_HEAD: &str = "- [Welcome!](README)\n"; - -fn main() { - println_cargo_style!("Refresh: _sidebar.md"); - gen_all_sidebars(); -} - -/// Find all README.md under docs/, treat each as a site, and generate _sidebar.md for it. -fn gen_all_sidebars() { - let repo_root = find_git_repo().unwrap(); - let docs_root = repo_root.join("docs"); - - let readme_paths = find_all_readmes(&docs_root); - - for readme_path in &readme_paths { - let site_root = readme_path.parent().unwrap(); - - let content_dir = find_content_dir(site_root); - - if let Some(content_dir) = content_dir { - let lines = build_sidebar_content(site_root, &content_dir, SIDEBAR_HEAD); - - let sidebar_path = site_root.join("_sidebar.md"); - std::fs::write(&sidebar_path, lines).unwrap(); - println_cargo_style!("Generated: {}", sidebar_path.display()); - } - } -} - -/// Recursively find all README.md files under a directory. -fn find_all_readmes(dir: &Path) -> Vec { - let mut results = Vec::new(); - if let Ok(read_dir) = std::fs::read_dir(dir) { - let mut entries: Vec<_> = read_dir.flatten().collect(); - entries.sort_by_key(|e| e.path()); - for entry in entries { - let path = entry.path(); - if path.is_dir() { - results.extend(find_all_readmes(&path)); - } else if path.file_name().map_or(false, |n| n == "README.md") { - results.push(path); - } - } - } - results -} - -/// Find the content directory for a site: -/// 1. Prefer `pages/` if it exists (backward compatible) -/// 2. Fall back to the first subdirectory that contains .md files -fn find_content_dir(site_root: &Path) -> Option { - // Try pages/ first - let pages_dir = site_root.join("pages"); - if pages_dir.exists() && pages_dir.is_dir() { - return Some(pages_dir); - } - - // Fall back to any subdirectory containing .md files - if let Ok(read_dir) = std::fs::read_dir(site_root) { - let mut entries: Vec<_> = read_dir.flatten().collect(); - entries.sort_by_key(|e| e.path()); - for entry in entries { - let path = entry.path(); - if path.is_dir() { - if has_markdown_files(&path) { - return Some(path); - } - } - } - } - - None -} - -/// Check if a directory (recursively) contains any .md files. -fn has_markdown_files(dir: &Path) -> bool { - if let Ok(read_dir) = std::fs::read_dir(dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - if path.is_dir() { - if has_markdown_files(&path) { - return true; - } - } else if path.extension().is_some_and(|ext| ext == "md") { - return true; - } - } - } - false -} - -/// Build sidebar content: scan .md files in `pages_dir` and return a formatted sidebar string -fn build_sidebar_content(base_dir: &Path, pages_dir: &Path, sidebar_head: &str) -> String { - let mut lines = String::from(sidebar_head); - - // Collect and sort entries at root level first - let mut root_files: Vec = Vec::new(); - // Subdirectory name -> its files - let mut sub_dirs: BTreeMap> = BTreeMap::new(); - - if let Ok(read_dir) = std::fs::read_dir(pages_dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - if path.is_dir() { - let dir_name = entry.file_name().to_string_lossy().to_string(); - let entries = collect_markdown_files(&path, base_dir); - if !entries.is_empty() { - // Check for .name file to override directory display name - let display_name = get_directory_display_name(&path, &dir_name); - sub_dirs.insert(display_name, entries); - } - } else if path.extension().is_some_and(|ext| ext == "md") { - let title = extract_title(&path); - let relative = path - .strip_prefix(base_dir) - .unwrap() - .to_string_lossy() - .replace('\\', "/"); - let link = relative - .strip_suffix(".md") - .unwrap_or(&relative) - .to_string(); - root_files.push(SidebarEntry { title, link }); - } - } - } - - // Sort root files — natural order (1, 2, ..., 10, 11) - root_files.sort_by(|a, b| natural_cmp(&a.link, &b.link)); - - // Append root-level files - for f in &root_files { - let _ = writeln!(lines, "* [{}]({})", f.title, f.link); - } - - // Append subdirectory groups - for (dir_name, entries) in &sub_dirs { - let mut sorted_entries = entries.clone(); - sorted_entries.sort_by(|a, b| natural_cmp(&a.link, &b.link)); - - // Directory header with 2-space indent - let _ = writeln!(lines, "* {dir_name}"); - for f in &sorted_entries { - let _ = writeln!(lines, " * [{}]({})", f.title, f.link); - } - } - - lines -} - -#[derive(Clone)] -struct SidebarEntry { - title: String, - link: String, -} - -/// Collect all `.md` files directly under `dir` -fn collect_markdown_files(dir: &Path, base_dir: &Path) -> Vec { - let mut entries = Vec::new(); - - if let Ok(read_dir) = std::fs::read_dir(dir) { - for entry in read_dir.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "md") { - let title = extract_title(&path); - let relative = path - .strip_prefix(base_dir) - .unwrap() - .to_string_lossy() - .replace('\\', "/"); - let link = relative - .strip_suffix(".md") - .unwrap_or(&relative) - .to_string(); - entries.push(SidebarEntry { title, link }); - } - } - } - - entries -} - -/// Extract title from the first line `

TITLE

`. -/// Fallback to filename stem. -fn extract_title(path: &Path) -> String { - let content = std::fs::read_to_string(path).unwrap_or_default(); - if let Some(first_line) = content.lines().next() { - let trimmed = first_line.trim(); - // Find `>TITLE<` between `

` and `

` - if let Some(start) = trimmed.find('>') { - let after_start = &trimmed[start + 1..]; - if let Some(end) = after_start.find('<') { - return after_start[..end].to_string(); - } - } - } - // Fallback: use file stem - path.file_stem().map_or_else( - || "Untitled".to_string(), - |s| s.to_string_lossy().to_string(), - ) -} - -/// Read `.name` file inside a directory to get its display name for the sidebar. -/// Falls back to the directory name itself if no `.name` file exists. -fn get_directory_display_name(dir_path: &std::path::Path, fallback: &str) -> String { - let name_file = dir_path.join(".name"); - if name_file.exists() && name_file.is_file() { - std::fs::read_to_string(&name_file) - .ok() - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| fallback.to_string()) - } else { - fallback.to_string() - } -} - -fn find_git_repo() -> Option { - let mut current_dir = std::env::current_dir().ok()?; - - loop { - let git_dir = current_dir.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - return Some(current_dir); - } - - if !current_dir.pop() { - break; - } - } - - None -} - -/// Natural (numeric-aware) comparison for sidebar links. -/// -/// Files prefixed with a number (e.g. `1-getting-started`) are sorted by that number; -/// files without a numeric prefix fall back to lexicographic order (after numbers). -fn natural_cmp(a: &str, b: &str) -> std::cmp::Ordering { - let num_a = extract_leading_number(a); - let num_b = extract_leading_number(b); - num_a.cmp(&num_b).then_with(|| a.cmp(b)) -} - -/// Extract the leading numeric prefix from a sidebar link path. -/// -/// Looks at the filename stem (after the last `/`) for a number before the first `-`. -/// Returns `usize::MAX` for entries without a numeric prefix. -fn extract_leading_number(link: &str) -> usize { - if let Some(file_stem) = link.rsplit('/').next() { - if let Some(num_end) = file_stem.find('-') { - if let Ok(num) = file_stem[..num_end].parse::() { - return num; - } - } - } - usize::MAX -} diff --git a/dev_tools/src/bin/refresh-docs.rs b/dev_tools/src/bin/refresh-docs.rs deleted file mode 100644 index 82ef906..0000000 --- a/dev_tools/src/bin/refresh-docs.rs +++ /dev/null @@ -1,178 +0,0 @@ -use std::path::Path; - -use just_fmt::snake_case; -use just_template::{Template, tmpl}; -use tools::println_cargo_style; - -const EXAMPLE_ROOT: &str = "./examples/"; -const OUTPUT_PATH: &str = "./mingling/src/example_docs.rs"; - -const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/example_docs.rs.tmpl"); - -fn main() { - gen_example_doc_module(); -} - -fn gen_example_doc_module() { - let mut template = Template::from(TEMPLATE_CONTENT); - let repo_root = find_git_repo().unwrap(); - let example_root = repo_root.join(EXAMPLE_ROOT); - let mut examples = Vec::new(); - if let Ok(entries) = std::fs::read_dir(&example_root) { - for entry in entries.flatten() { - if let Ok(file_type) = entry.file_type() - && file_type.is_dir() - { - let example_name = entry.file_name().to_string_lossy().to_string(); - // Ignore directories that don't start with "example-" - if !example_name.starts_with("example-") { - continue; - } - let example_content = ExampleContent::read(&example_name); - examples.push(example_content); - } - } - } - - examples.sort(); - - for example in examples { - tmpl!(template += { - examples { - ( - example_header = example.header, - example_import = example.cargo_toml, - example_code = example.code, - example_name = snake_case!(&example.name) - ) - } - }); - println_cargo_style!("Refresh: {}", example.name); - } - - let template_str = template.to_string(); - let template_str = template_str - .lines() - .map(str::trim_end) - .collect::>() - .join("\n") - + "\n"; - std::fs::write(repo_root.join(OUTPUT_PATH), template_str).unwrap(); -} - -struct ExampleContent { - name: String, - header: String, - code: String, - cargo_toml: String, -} - -impl PartialOrd for ExampleContent { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ExampleContent { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.name.cmp(&other.name) - } -} - -impl PartialEq for ExampleContent { - fn eq(&self, other: &Self) -> bool { - self.name == other.name - } -} - -impl Eq for ExampleContent {} - -impl ExampleContent { - pub fn read(name: &str) -> Self { - let repo = find_git_repo().unwrap(); - let cargo_toml = Self::read_cargo_toml(&repo, name); - let (header, code) = Self::read_header_and_code(&repo, name); - - let cargo_toml = cargo_toml - .lines() - .map(|line| format!("/// {line}")) - .collect::>() - .join("\n"); - - let header = header - .lines() - .map(|line| format!("/// {line}")) - .collect::>() - .join("\n"); - - let code = code - .lines() - .map(|line| format!("/// {line}")) - .collect::>() - .join("\n"); - - ExampleContent { - name: name.to_string(), - header, - code, - cargo_toml, - } - } - - fn read_header_and_code(repo: &Path, name: &str) -> (String, String) { - let file_path = repo - .join(EXAMPLE_ROOT) - .join(name) - .join("src") - .join("main.rs"); - let content = std::fs::read_to_string(&file_path).unwrap_or_default(); - let mut lines = content.lines(); - let mut header = String::new(); - let mut code = String::new(); - - // Collect header lines (starting with //!) - for line in lines.by_ref() { - if line.trim_start().starts_with("//!") { - let trimmed = line.trim_start_matches("//!"); - header.push_str(trimmed); - header.push('\n'); - } else { - // First non-header line found, start collecting code - code.push_str(line); - code.push('\n'); - break; - } - } - - // Collect remaining code lines - for line in lines { - code.push_str(line); - code.push('\n'); - } - - (header.trim().to_string(), code.trim().to_string()) - } - - fn read_cargo_toml(repo: &Path, name: &str) -> String { - let file_path = repo.join(EXAMPLE_ROOT).join(name).join("Cargo.toml"); - - std::fs::read_to_string(&file_path).unwrap_or_default() - } -} - -fn find_git_repo() -> Option { - let mut current_dir = std::env::current_dir().ok()?; - - loop { - let git_dir = current_dir.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - return Some(current_dir); - } - - if !current_dir.pop() { - break; - } - } - - None -} diff --git a/dev_tools/src/bin/refresh-feature-mod.rs b/dev_tools/src/bin/refresh-feature-mod.rs deleted file mode 100644 index 2255dbc..0000000 --- a/dev_tools/src/bin/refresh-feature-mod.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::collections::BTreeSet; -use std::path::Path; - -use just_fmt::snake_case; -use just_template::{tmpl, Template}; -use tools::println_cargo_style; - -const CARGO_TOML_PATH: &str = "./mingling/Cargo.toml"; -const OUTPUT_PATH: &str = "./mingling/src/features.rs"; - -const TEMPLATE_CONTENT: &str = include_str!("../../../mingling/src/features.rs.tmpl"); - -fn main() { - gen_feature_module(); -} - -fn gen_feature_module() { - let repo_root = find_git_repo().unwrap(); - - let cargo_toml_path = repo_root.join(CARGO_TOML_PATH); - let output_path = repo_root.join(OUTPUT_PATH); - - let features = parse_features(&cargo_toml_path); - - let mut template = Template::from(TEMPLATE_CONTENT); - - for feat_name in &features { - let feat_const_name = snake_case!(feat_name).to_uppercase(); - - tmpl!(template += { - features { - ( - feat_name = feat_name, - feat_const_name = feat_const_name - ) - } - }); - println_cargo_style!("Refresh: feature `{}`", feat_name); - } - - let template_str = template.to_string(); - let template_str = template_str - .lines() - .map(str::trim_end) - .collect::>() - .join("\n") - + "\n"; - std::fs::write(&output_path, template_str).unwrap(); - - println_cargo_style!("Written: features module to {}", OUTPUT_PATH); -} - -/// Parse all feature names from the `[features]` section of a Cargo.toml. -fn parse_features(cargo_toml_path: &Path) -> Vec { - let content = std::fs::read_to_string(cargo_toml_path) - .unwrap_or_else(|e| panic!("Failed to read {}: {}", cargo_toml_path.display(), e)); - - let cargo_toml: toml::Value = content - .parse() - .unwrap_or_else(|e| panic!("Failed to parse {}: {}", cargo_toml_path.display(), e)); - - let features_table = cargo_toml - .get("features") - .and_then(|v| v.as_table()) - .unwrap_or_else(|| { - panic!( - "No [features] section found in {}", - cargo_toml_path.display() - ) - }); - - let mut feature_names: BTreeSet = BTreeSet::new(); - for key in features_table.keys() { - feature_names.insert(key.clone()); - } - - let mut result: Vec = feature_names.into_iter().collect(); - result.sort(); - result -} - -fn find_git_repo() -> Option { - let mut current_dir = std::env::current_dir().ok()?; - - loop { - let git_dir = current_dir.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - return Some(current_dir); - } - - if !current_dir.pop() { - break; - } - } - - None -} diff --git a/dev_tools/src/bin/sync-examples.rs b/dev_tools/src/bin/sync-examples.rs deleted file mode 100644 index 0923b33..0000000 --- a/dev_tools/src/bin/sync-examples.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::fs; -use std::path::Path; - -use serde::{Deserialize, Serialize}; -use tools::println_cargo_style; - -#[derive(Serialize)] -struct ExampleMeta { - id: String, - name: String, - icon: String, - category: String, - desc: String, - tags: Vec, - files: Vec, -} - -#[derive(Deserialize)] -struct PageToml { - example: PageTomlExample, -} - -#[derive(Deserialize)] -struct PageTomlExample { - id: String, - #[serde(default)] - name: String, - #[serde(default = "default_icon")] - icon: String, - #[serde(default)] - category: String, - #[serde(default)] - desc: String, - #[serde(default)] - tags: Vec, - #[serde(default = "default_files")] - files: Vec, -} - -fn default_icon() -> String { - "📦".to_string() -} - -fn default_files() -> Vec { - vec!["Cargo.toml".to_string(), "src/main.rs".to_string()] -} - -fn main() { - #[cfg(windows)] - let _ = colored::control::set_virtual_terminal(true); - - let examples_dir = Path::new("examples"); - let output_dir = Path::new("docs/example-pages"); - fs::create_dir_all(output_dir).expect("failed to create docs/example-pages"); - - let mut examples: Vec = Vec::new(); - - let entries = fs::read_dir(examples_dir).expect("failed to read examples/"); - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_dir() { - continue; - } - - let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); - - let id = dir_name.to_string(); - let page_toml_path = path.join("page.toml"); - - let meta = if page_toml_path.exists() { - match fs::read_to_string(&page_toml_path) - .map_err(|e| e.to_string()) - .and_then(|content| toml::from_str::(&content).map_err(|e| e.to_string())) - { - Ok(page) => { - let ex = page.example; - ExampleMeta { - id: if ex.id.is_empty() { id.clone() } else { ex.id }, - name: if ex.name.is_empty() { - id.clone() - } else { - ex.name - }, - icon: ex.icon, - category: ex.category, - desc: ex.desc, - tags: ex.tags, - files: if ex.files.is_empty() { - default_files() - } else { - ex.files - }, - } - } - Err(e) => { - eprintln!( - "Warning: failed to parse {}: {}", - page_toml_path.display(), - e - ); - continue; - } - } - } else { - continue; - }; - - examples.push(meta); - } - - // Sort: basic first, then alphabetical - examples.sort_by(|a, b| { - if a.id == "example-basic" { - return std::cmp::Ordering::Less; - } - if b.id == "example-basic" { - return std::cmp::Ordering::Greater; - } - a.id.cmp(&b.id) - }); - - let json = serde_json::to_string_pretty(&examples).expect("failed to serialize"); - let output_path = output_dir.join("examples.json"); - fs::write(&output_path, &json).expect("failed to write examples.json"); - - println_cargo_style!( - "Sync: {} examples -> {}", - examples.len(), - output_path.display() - ); -} diff --git a/dev_tools/src/bin/test-all-markdown-code.rs b/dev_tools/src/bin/test-all-markdown-code.rs deleted file mode 100644 index 280fca7..0000000 --- a/dev_tools/src/bin/test-all-markdown-code.rs +++ /dev/null @@ -1,276 +0,0 @@ -use std::collections::HashMap; -use std::env; -use std::path::{Path, PathBuf}; - -use colored::Colorize; -use indicatif::ProgressBar; -use tools::verify::{ - build_block, compute_block_hash, generate_cargo_toml, generate_main_rs, generate_build_rs, - is_block_testable, parse_code_blocks, write_summary_report, -}; -use tools::{eprintln_cargo_style, println_cargo_style}; - -/// Config from verified-docs.toml -#[derive(serde::Deserialize)] -struct Config { - verified: VerifiedPaths, -} - -#[derive(serde::Deserialize)] -struct VerifiedPaths { - readme: String, - #[serde(default)] - documents_en_us: Option, - #[serde(default)] - documents_zh_cn: Option, -} - -#[tokio::main] -async fn main() { - #[cfg(windows)] - let _ = colored::control::set_virtual_terminal(true); - - let config_path = PathBuf::from("verified-docs.toml"); - if !config_path.exists() { - eprintln_cargo_style!("verified-docs.toml not found in current directory"); - std::process::exit(1); - } - - let config: Config = { - let content = std::fs::read_to_string(&config_path).unwrap_or_else(|_e| { - eprintln_cargo_style!("Failed to read verified-docs.toml"); - std::process::exit(1); - }); - toml::from_str(&content).unwrap_or_else(|_e| { - eprintln_cargo_style!("Failed to parse verified-docs.toml"); - std::process::exit(1); - }) - }; - - // Parse optional path argument from env args - let single_file: Option = { - let args: Vec = env::args().collect(); - if args.len() > 1 { - let p = PathBuf::from(&args[1]); - if p.exists() { - Some(p) - } else { - eprintln_cargo_style!("error: specified file '{}' does not exist", args[1]); - std::process::exit(1); - } - } else { - None - } - }; - - // Collect all markdown files from config - let mut files: Vec<(String, PathBuf)> = Vec::new(); - - // README - let readme_path = PathBuf::from(&config.verified.readme); - if readme_path.exists() { - files.push(("README".to_string(), readme_path)); - } - - // English docs - if let Some(pattern) = &config.verified.documents_en_us { - let base = pattern.trim_end_matches("/**").trim_end_matches('*'); - let dir = PathBuf::from(base); - if dir.exists() && dir.is_dir() { - collect_md_files(&dir, &mut files, "en"); - } - } - - // Chinese docs - if let Some(pattern) = &config.verified.documents_zh_cn { - let base = pattern.trim_end_matches("/**").trim_end_matches('*'); - let dir = PathBuf::from(base); - if dir.exists() && dir.is_dir() { - collect_md_files(&dir, &mut files, "zh_CN"); - } - } - - // If a single file was specified, filter the list to only that file - if let Some(ref target) = single_file { - let target_canon = std::fs::canonicalize(target).unwrap_or_else(|_| target.clone()); - files.retain(|(_, path)| { - std::fs::canonicalize(path) - .map(|p| p == target_canon) - .unwrap_or(false) - }); - if files.is_empty() { - eprintln_cargo_style!( - "error: specified file '{}' is not among the configured documentation files", - target.display() - ); - std::process::exit(1); - } - } - - if files.is_empty() { - eprintln_cargo_style!("No markdown files found to verify"); - std::process::exit(1); - } - - // Parse all code blocks into a flat list with global indices - let mut flat_blocks: Vec<(usize, tools::verify::CodeBlock)> = Vec::new(); - - for (label, path) in &files { - let content = std::fs::read_to_string(path).unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to read {}: {}", path.display(), e); - String::new() - }); - let source_file = format!("{label}/{}", path.file_name().unwrap().to_string_lossy()); - let blocks = parse_code_blocks(&content, &source_file); - let testable: Vec<_> = blocks.into_iter().filter(is_block_testable).collect(); - for block in testable { - let idx = flat_blocks.len() + 1; // 1-based global index - flat_blocks.push((idx, block)); - } - } - - let total_testable = flat_blocks.len(); - - if total_testable == 0 { - println_cargo_style!("No testable code blocks found"); - return; - } - - // Create a shared progress bar - let bar = ProgressBar::new(total_testable as u64); - bar.set_style( - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", - " Testing".bold().bright_cyan() - )) - .unwrap() - .progress_chars("=> "), - ); - bar.set_message("blocks"); - - // Group blocks by dependency hash - let mut groups: HashMap> = HashMap::new(); - for (idx, block) in flat_blocks { - let hash = compute_block_hash(&block); - groups.entry(hash).or_default().push((idx, block)); - } - - let temp_base = PathBuf::from(".temp/doc-test"); - - // Sort groups by hash for deterministic output order - let mut group_vec: Vec<(String, Vec<(usize, tools::verify::CodeBlock)>)> = - groups.into_iter().collect(); - group_vec.sort_by(|a, b| a.0.cmp(&b.0)); - - // Spawn a blocking task per group — groups run in parallel, blocks within a group are serial - let mut handles = Vec::new(); - for (hash, blocks) in group_vec { - let temp_base = temp_base.clone(); - let bar = bar.clone(); // clone shares the same underlying progress - let handle = tokio::task::spawn_blocking(move || { - let crate_dir = temp_base.join(&hash); - let src_dir = crate_dir.join("src"); - let manifest_path = crate_dir.join("Cargo.toml"); - - // Generate a single Cargo.toml for the whole group (all blocks share same deps) - let first_block = &blocks[0].1; - let cargo_toml = generate_cargo_toml(first_block, "test-doc", &manifest_path); - - let mut group_results: Vec<(String, usize, bool, String)> = Vec::new(); - for (block_idx, block) in &blocks { - let block_label = - format!("Block {block_idx} ({}:{})", block.source_file, block.line); - - bar.set_message(block_label.clone()); - - let main_rs = if block.is_build_time { - // For build-time blocks, write a stub main.rs and generate build.rs - generate_build_rs(block) - } else { - generate_main_rs(block) - }; - let (ok, err) = build_block( - &src_dir, - &manifest_path, - &cargo_toml, - &main_rs, - block.is_build_time, - ); - if ok { - bar.inc(1); - } else { - bar.inc(1); - bar.println(format!( - " {} {block_label}", - "failed".bold().bright_red() - )); - bar.println(format!(" {block_label} FAILED:\n{err}")); - } - group_results.push((block.source_file.clone(), block.line, ok, err)); - } - group_results - }); - handles.push(handle); - } - - // Collect results from all groups - let mut results: Vec<(String, usize, bool, String)> = Vec::new(); - let mut passed = 0usize; - let mut failed = 0usize; - - for handle in handles { - match handle.await { - Ok(group_results) => { - for (file, line, ok, err) in group_results { - if ok { - passed += 1; - } else { - failed += 1; - } - results.push((file, line, ok, err)); - } - } - Err(e) => { - eprintln_cargo_style!("Task panicked: {}", e); - std::process::exit(1); - } - } - } - - bar.finish_and_clear(); - - let result_msg = format!("Result: {passed}/{total_testable} blocks passed"); - println_cargo_style!(result_msg); - - write_summary_report( - Path::new(".temp/DOCS-TEST-RESULT.md"), - "Documentation Code Block Test Report", - &results, - total_testable, - passed, - failed, - ); - - if failed > 0 { - let fail_msg = format!("{failed} block(s) failed to build"); - eprintln_cargo_style!(fail_msg); - std::process::exit(1); - } - - println_cargo_style!("Done: All verified code blocks build successfully!"); -} - -/// Recursively collect all `.md` files under a directory -fn collect_md_files(dir: &Path, files: &mut Vec<(String, PathBuf)>, lang: &str) { - if let Ok(entries) = std::fs::read_dir(dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - collect_md_files(&path, files, lang); - } else if path.extension().is_some_and(|ext| ext == "md") { - files.push((lang.to_string(), path)); - } - } - } -} diff --git a/dev_tools/src/bin/test-examples.rs b/dev_tools/src/bin/test-examples.rs deleted file mode 100644 index 539459e..0000000 --- a/dev_tools/src/bin/test-examples.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::collections::HashMap; - -use colored::Colorize; -use indicatif::ProgressBar; -use serde::Deserialize; -use tools::{eprintln_cargo_style, println_cargo_style}; - -#[derive(Deserialize)] -struct TestConfig { - test: HashMap>, -} - -#[derive(Deserialize)] -struct TestCase { - command: String, - expect: Expect, -} - -#[derive(Deserialize)] -struct Expect { - #[serde(rename = "exit-code")] - exit_code: i32, - result: String, -} - -fn main() { - #[cfg(windows)] - let _ = colored::control::set_virtual_terminal(true); - - let config = load_config(); - - // Count total test cases upfront - let total: usize = config.test.values().map(|cases| cases.len()).sum(); - let bar = ProgressBar::new(total as u64); - bar.set_style( - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", - " Testing".bold().bright_cyan() - )) - .unwrap() - .progress_chars("=> "), - ); - bar.set_message("examples"); - - let passed = run_all_tests(&config, &bar); - - bar.finish_and_clear(); - - println_cargo_style!("Result: {}/{} tests passed", passed, total); - - if passed != total { - eprintln_cargo_style!("{} test(s) failed", total - passed); - std::process::exit(1); - } -} - -/// Parse test config from TOML file -fn load_config() -> TestConfig { - let content = std::fs::read_to_string("examples/test-examples.toml").unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to read TOML config file: {}", e); - std::process::exit(1); - }); - - toml::from_str(&content).unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to parse TOML config: {}", e); - std::process::exit(1); - }) -} - -/// Run all example test groups, return number passed -fn run_all_tests(config: &TestConfig, bar: &ProgressBar) -> usize { - let mut passed = 0; - - for (example_name, test_cases) in &config.test { - bar.set_message(example_name.clone()); - - if !build_example(example_name) { - bar.inc(test_cases.len() as u64); - continue; - } - - for test_case in test_cases { - if run_single_test(example_name, test_case, bar) { - passed += 1; - } - bar.inc(1); - } - } - - passed -} - -/// Build the example binary, return true on success -fn build_example(example_name: &str) -> bool { - let manifest = format!("examples/{example_name}/Cargo.toml"); - tools::run_cmd_capture(format!( - "cargo build --manifest-path {manifest} --color always", - )) - .is_ok() -} - -/// Run a single test case, return true on pass -fn run_single_test(example_name: &str, test_case: &TestCase, bar: &ProgressBar) -> bool { - let binary_path = format!(".temp/target/debug/{}", get_binary_name(example_name)); - let args: Vec<&str> = test_case.command.split_whitespace().collect(); - - let output = match std::process::Command::new(&binary_path) - .args(&args) - .output() - { - Ok(o) => o, - Err(e) => { - bar.println(format!("'{}' - failed to run: {}", test_case.command, e)); - return false; - } - }; - - let actual_exit_code = output.status.code().unwrap_or(-1); - let actual_stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let actual_stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - - let exit_ok = actual_exit_code == test_case.expect.exit_code; - let result_ok = actual_stdout == test_case.expect.result - || actual_stdout.contains(&test_case.expect.result); - - if exit_ok && result_ok { - true - } else { - bar.println(format!("failed: '{}'", test_case.command)); - if !exit_ok { - bar.println(format!( - " Expected exit code: {}, actual: {}", - test_case.expect.exit_code, actual_exit_code - )); - } - if !result_ok { - bar.println(format!(" Expected output: {:?}", test_case.expect.result)); - bar.println(format!(" Actual stdout: {:?}", actual_stdout)); - if !actual_stderr.is_empty() { - bar.println(format!(" Actual stderr: {:?}", actual_stderr)); - } - } - false - } -} - -/// Resolve binary filename for the given example -/// -/// The binary name matches the package name. On Windows, the `.exe` suffix is required. -fn get_binary_name(example_name: &str) -> String { - let base = example_name; - if cfg!(target_os = "windows") { - format!("{base}.exe") - } else { - base.to_string() - } -} diff --git a/dev_tools/src/bin/update-version.rs b/dev_tools/src/bin/update-version.rs deleted file mode 100644 index 475e54b..0000000 --- a/dev_tools/src/bin/update-version.rs +++ /dev/null @@ -1,98 +0,0 @@ -use std::io::Write as _; -use std::path::Path; - -use serde::Deserialize; -use tools::println_cargo_style; - -#[derive(Deserialize)] -struct VersionFile { - file: String, - pattern: String, -} - -#[derive(Deserialize)] -struct Config { - #[serde(rename = "file")] - files: Vec, -} - -fn main() { - let args: Vec = std::env::args().collect(); - - // Get new version - let new_ver = if args.len() > 1 { - args[1].clone() - } else { - print!("Update version to: "); - std::io::stdout().flush().unwrap(); - let mut input = String::new(); - std::io::stdin().read_line(&mut input).unwrap(); - input.trim().to_string() - }; - - if new_ver.is_empty() { - eprintln!("Error: Version cannot be empty."); - std::process::exit(1); - } - - // Read current version from root Cargo.toml's workspace.package.version - let root_cargo_path = "Cargo.toml"; - let root_cargo_content = - std::fs::read_to_string(root_cargo_path).expect("Failed to read Cargo.toml"); - let cargo_value: toml::Value = root_cargo_content.parse().expect("Failed to parse Cargo.toml"); - - let current_ver = cargo_value["workspace"]["package"]["version"] - .as_str() - .expect("workspace.package.version not found in Cargo.toml") - .to_string(); - - if new_ver == current_ver { - println!("Version is already {}. Nothing to do.", current_ver); - return; - } - - println_cargo_style!("Version: {} -> {}", current_ver, new_ver); - - // Read version-files.toml - let config_path = Path::new("dev_tools").join("version-files.toml"); - let config_str = - std::fs::read_to_string(&config_path).expect("Failed to read dev_tools/version-files.toml"); - let config: Config = toml::from_str(&config_str) - .expect("Failed to parse dev_tools/version-files.toml"); - - let mut updated_count = 0; - let mut skipped_count = 0; - - for vf in &config.files { - let file_path = &vf.file; - let old_pattern = vf.pattern.replace("{VER}", ¤t_ver); - let new_pattern = vf.pattern.replace("{VER}", &new_ver); - - let content = match std::fs::read_to_string(file_path) { - Ok(c) => c, - Err(e) => { - eprintln!("Warning: Could not read {}: {}", file_path, e); - skipped_count += 1; - continue; - } - }; - - let new_content = content.replace(&old_pattern, &new_pattern); - - if new_content == content { - eprintln!( - "Warning: Pattern '{}' not found in {}", - old_pattern, file_path - ); - skipped_count += 1; - continue; - } - - std::fs::write(file_path, &new_content) - .unwrap_or_else(|e| panic!("Failed to write {}: {}", file_path, e)); - println_cargo_style!("Updated: {}", file_path); - updated_count += 1; - } - - println_cargo_style!("Done: {} file(s) updated, {} file(s) skipped", updated_count, skipped_count); -} diff --git a/dev_tools/src/lib.rs b/dev_tools/src/lib.rs deleted file mode 100644 index d38a156..0000000 --- a/dev_tools/src/lib.rs +++ /dev/null @@ -1,331 +0,0 @@ -pub mod verify; - -use colored::Colorize; - -#[macro_export] -macro_rules! run_cmd { - ($fmt:literal, $($arg:tt)*) => { - $crate::run_cmd(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::run_cmd($cmd) - }; -} - -/// Run a shell command and capture its combined stdout+stderr output. -/// Returns `Ok(output)` on success, `Err((exit_code, stderr))` on failure. -#[macro_export] -macro_rules! run_cmd_and_capture_stderr { - ($fmt:literal, $($arg:tt)*) => { - $crate::run_cmd_capture(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::run_cmd_capture($cmd) - }; -} - -#[macro_export] -macro_rules! println_cargo_style { - ($fmt:literal, $($arg:tt)*) => { - $crate::println_cargo_style(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::println_cargo_style($cmd) - }; -} - -#[macro_export] -macro_rules! eprintln_cargo_style { - ($fmt:literal, $($arg:tt)*) => { - $crate::eprintln_cargo_style(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::eprintln_cargo_style($cmd) - }; -} - -#[macro_export] -macro_rules! wprintln_cargo_style { - ($fmt:literal, $($arg:tt)*) => { - $crate::wprintln_cargo_style(format!($fmt, $($arg)*)) - }; - ($cmd:expr) => { - $crate::wprintln_cargo_style($cmd) - }; -} - -/// Print a message in cargo style format, with bold green prefix. -/// -/// # Panics -/// -/// Panics if the prefix (text before the first `:`) exceeds 12 characters. -pub fn println_cargo_style(str: impl Into) { - let s = str.into(); - let (prefix, content) = if let Some(pos) = s.find(':') { - ( - s[..pos].trim().to_string(), - s[pos + 1..].trim_start().to_string(), - ) - } else { - (String::new(), s.trim().to_string()) - }; - - assert!( - prefix.len() <= 12, - "prefix length exceeds 12: '{}' has length {}", - prefix, - prefix.len() - ); - - let padding = " ".repeat(12 - prefix.len()); - - println!( - "{}{} {}", - padding, - prefix.bold().bright_green(), - content.trim() - ); -} - -pub fn eprintln_cargo_style(str: impl Into) { - println!("{}: {}", "error".bold().bright_red(), str.into()); -} - -/// Print a message in cargo style format, with bold yellow prefix (warning style). -/// -/// # Panics -/// -/// Panics if the prefix (text before the first `:`) exceeds 12 characters. -pub fn wprintln_cargo_style(str: impl Into) { - let s = str.into(); - let (prefix, content) = if let Some(pos) = s.find(':') { - ( - s[..pos].trim().to_string(), - s[pos + 1..].trim_start().to_string(), - ) - } else { - (String::new(), s.trim().to_string()) - }; - - assert!( - prefix.len() <= 12, - "prefix length exceeds 12: '{}' has length {}", - prefix, - prefix.len() - ); - - let padding = " ".repeat(12 - prefix.len()); - - println!( - "{}{} {}", - padding, - prefix.bold().bright_yellow(), - content.trim() - ); -} - -/// Run a shell command and return its exit status. -/// -/// # Panics -/// -/// Panics if the shell command cannot be spawned (e.g. the shell binary is not found). -/// -/// # Errors -/// -/// Returns `Err` with the exit code if the command finishes with a non-zero exit code. -pub fn run_cmd(cmd: impl Into) -> Result<(), i32> { - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let status = std::process::Command::new(shell) - .arg("-c") - .arg(cmd.into()) - .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) - .status() - .expect("failed to execute command"); - - let exit_code = status.code().unwrap_or(1); - if exit_code == 0 { - Ok(()) - } else { - Err(exit_code) - } -} - -/// Run a shell command and capture its combined stdout+stderr output. -/// -/// On success returns `Ok(combined_output)`. On failure returns `Err((exit_code, stderr))`. -/// Stderr falls back to stdout if stderr is empty. -pub fn run_cmd_capture(cmd: impl Into) -> Result { - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let output = std::process::Command::new(shell) - .arg("-c") - .arg(cmd.into()) - .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) - .output() - .expect("failed to execute command"); - - let exit_code = output.status.code().unwrap_or(1); - let stderr = String::from_utf8_lossy(&output.stderr).to_string(); - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let combined = if stderr.is_empty() { stdout } else { stderr }; - - if exit_code == 0 { - Ok(combined) - } else { - Err((exit_code, combined)) - } -} - -/// Extract a crate-style name from a `Cargo.toml` path. -/// -/// Examples: -/// - `mingling_core/Cargo.toml` → `mingling_core` -/// - `.` → `(root)` -pub fn crate_name_from(path: &std::path::Path) -> String { - path.parent() - .and_then(|p| p.file_name()) - .and_then(|n| n.to_str()) - .unwrap_or("(root)") - .to_string() -} - -/// Run a list of `(label_for_errors, crate_name_for_bar, shell_command)` tuples -/// in parallel with a progress bar. -/// -/// - Success: silent, the bar tracks progress: -/// ` Building [============================] 32/32: mingling_core` -/// - Failure: `pb.println()` prints the error immediately above the bar. -pub fn run_parallel(phase: &str, tasks: Vec<(String, String, String)>) -> Result<(), i32> { - let n = tasks.len(); - if n == 0 { - return Ok(()); - } - - // Cargo-style prefix: right-aligned to 12 chars, bold bright cyan - let padding = " ".repeat(12 - phase.len()); - let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); - - let pb = indicatif::ProgressBar::new(n as u64); - pb.set_style( - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", - styled_prefix - )) - .unwrap() - .progress_chars("=> "), - ); - pb.set_position(0); - - // Pre-extract labels for error messages - let labels: Vec = tasks.iter().map(|(l, _, _)| l.clone()).collect(); - - let (tx, rx) = std::sync::mpsc::channel::<(usize, String, Result)>(); - - for (i, (_label, crate_name, cmd)) in tasks.into_iter().enumerate() { - let tx = tx.clone(); - std::thread::spawn(move || { - let result = run_cmd_capture(&cmd); - let _ = tx.send((i, crate_name, result)); - }); - } - drop(tx); - - let mut first_exit_code = 0; - - while let Ok((i, crate_name, result)) = rx.recv() { - pb.inc(1); - pb.set_message(crate_name); - - if let Err((code, output)) = result { - if first_exit_code == 0 { - first_exit_code = code; - } - pb.println(format!( - "{}: {} failed (exit code {})", - "error".bright_red().bold(), - labels[i], - code, - )); - if !output.is_empty() { - for line in output.lines() { - pb.println(format!(" {line}")); - } - } - } - } - - pb.finish_and_clear(); - - if first_exit_code != 0 { - Err(first_exit_code) - } else { - Ok(()) - } -} - -/// Run a single shell command with a progress bar, capturing its output. -/// -/// - Success: bar clears silently. -/// - Failure: error is printed above the bar, then the bar clears. -pub fn run_cmd_with_progress(phase: &str, label: &str, cmd: String) -> Result<(), i32> { - let padding = " ".repeat(12 - phase.len()); - let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan()); - - let pb = indicatif::ProgressBar::new(1); - pb.set_style( - indicatif::ProgressStyle::default_bar() - .template(&format!( - "{} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}", - styled_prefix - )) - .unwrap() - .progress_chars("=> "), - ); - pb.set_message(label.to_owned()); - - let result = run_cmd_capture(&cmd); - pb.inc(1); - pb.finish_and_clear(); - - match result { - Ok(_) => Ok(()), - Err((code, output)) => { - eprintln_cargo_style(format!("{} failed (exit code {})", label, code)); - if !output.is_empty() { - println!("{}", output.trim_end()); - } - Err(code) - } - } -} - -#[must_use] -pub fn cargo_tomls() -> Vec { - let mut cargo_tomls = Vec::new(); - let mut dirs = vec![std::path::PathBuf::from(".")]; - while let Some(dir) = dirs.pop() { - if let Ok(entries) = std::fs::read_dir(&dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - // Skip the dev_tools directory - if path.file_name().and_then(|n| n.to_str()) == Some("dev_tools") { - continue; - } - dirs.push(path); - } else if path.file_name().and_then(|n| n.to_str()) == Some("Cargo.toml") { - cargo_tomls.push(path); - } - } - } - } - cargo_tomls -} diff --git a/dev_tools/src/verify.rs b/dev_tools/src/verify.rs deleted file mode 100644 index 2ddff0c..0000000 --- a/dev_tools/src/verify.rs +++ /dev/null @@ -1,511 +0,0 @@ -use std::path::Path; - -use crate::println_cargo_style; - -/// Represents a parsed code block from a markdown file -#[derive(Debug, Clone)] -pub struct CodeBlock { - /// Source file path (for reporting) - pub source_file: String, - /// The line number in source file where this block starts - pub line: usize, - /// The raw Rust source code - pub code: String, - /// Feature flags extracted from `// Features: [...]` comment - pub features: Vec, - /// Whether the block had an explicit `// Features:` header - pub has_features_header: bool, - /// Whether the block has `// NOT VERIFIED` to opt out of testing - pub not_verified: bool, - /// External dependencies extracted from `// Dependencies:` comments - pub external_deps: Vec<(String, String)>, - /// Whether this block has a `fn main` entry point - pub has_main: bool, - /// Whether this block has `gen_program!()` call - pub has_gen_program: bool, - /// Whether this block has `// BUILD TIME` annotation (write to build.rs, not main.rs) - pub is_build_time: bool, -} - -/// Parse all ```rust code blocks from markdown content -pub fn parse_code_blocks(content: &str, source_file: &str) -> Vec { - let mut blocks = Vec::new(); - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - - while i < lines.len() { - if lines[i].trim() == "```rust" { - if let Some(block) = parse_single_block(&lines, i, source_file) { - blocks.push(block); - } - i += 1; - while i < lines.len() && lines[i].trim() != "```" { - i += 1; - } - } - i += 1; - } - - blocks -} - -/// Parse a single code block starting at the ```rust line -fn parse_single_block(lines: &[&str], start: usize, source_file: &str) -> Option { - let line_num = start + 1; // 1-based line number - - let mut code_lines: Vec = Vec::new(); - let mut features: Vec = Vec::new(); - let mut has_features_header = false; - let mut not_verified = false; - let mut external_deps: Vec<(String, String)> = Vec::new(); - let mut has_main = false; - let mut has_gen_program = false; - let mut is_build_time = false; - - let mut idx = start + 1; - let mut in_header = true; - - while idx < lines.len() { - let raw_line = lines[idx]; - let trimmed = raw_line.trim(); - - if trimmed == "```" { - break; - } - - // @@@ lines: strip the prefix and treat as regular Rust code - // These lines are hidden in the rendered docs (filtered by a docsify plugin) - // but must still compile. - if trimmed.starts_with("@@@") { - in_header = false; - // Strip @@@ and optionally one following space - let code = trimmed[3..].trim_start(); - if code.contains("fn main") { - has_main = true; - } - if code.contains("gen_program!") { - has_gen_program = true; - } - code_lines.push(code.to_string()); - idx += 1; - continue; - } - - // Parse header comments - // Check for NOT VERIFIED marker - if in_header && trimmed == "// NOT VERIFIED" { - not_verified = true; - idx += 1; - continue; - } - - if in_header && trimmed == "// BUILD TIME" { - is_build_time = true; - idx += 1; - continue; - } - - if in_header && trimmed.starts_with("// ") { - if trimmed.starts_with("// Features:") { - has_features_header = true; - let feat_str = trimmed.trim_start_matches("// Features:").trim(); - if feat_str.starts_with('[') && feat_str.ends_with(']') { - let inner = &feat_str[1..feat_str.len() - 1]; - if !inner.is_empty() { - features = inner - .split(',') - .map(|s| s.trim().trim_matches('"').to_string()) - .filter(|s| !s.is_empty()) - .collect(); - } - } - idx += 1; - continue; - } - if trimmed == "// Dependencies:" { - idx += 1; - // Collect subsequent `// crate = "version"` lines - while idx < lines.len() { - let next = lines[idx].trim(); - if next == "```" { - break; - } - if next.starts_with("// ") { - let dep_line = next.trim_start_matches("// ").trim(); - if let Some((name, ver)) = dep_line.split_once(" = ") { - external_deps.push(( - name.trim().to_string(), - ver.trim().trim_matches('"').to_string(), - )); - } - idx += 1; - } else { - break; - } - } - continue; - } - } - - in_header = false; - - if raw_line.contains("fn main") { - has_main = true; - } - if raw_line.contains("gen_program!") { - has_gen_program = true; - } - - code_lines.push(raw_line.to_string()); - idx += 1; - } - - if code_lines.is_empty() { - return None; - } - - Some(CodeBlock { - source_file: source_file.to_string(), - line: line_num, - code: code_lines.join("\n"), - features, - has_features_header, - not_verified, - external_deps, - has_main, - has_gen_program, - is_build_time, - }) -} - -/// Generate a Cargo.toml for a block -/// -/// `manifest_path` is the full path to the Cargo.toml file being written; it is used to -/// compute the relative path to the `mingling` crate. -pub fn generate_cargo_toml(block: &CodeBlock, package_name: &str, manifest_path: &Path) -> String { - let features_str = if !block.features.is_empty() { - let feats: Vec = block.features.iter().map(|f| format!("\"{f}\"")).collect(); - format!("features = [{}]", feats.join(", ")) - } else { - String::new() - }; - - let mut extra_deps = String::new(); - for (name, version) in &block.external_deps { - if !version.starts_with('{') { - if name == "serde" || name == "clap" { - extra_deps.push_str(&format!( - "{name} = {{ version = \"{version}\", features = [\"derive\"] }}\n" - )); - } else { - extra_deps.push_str(&format!("{name} = \"{version}\"\n")); - } - } else { - extra_deps.push_str(&format!("{name} = {version}\n")); - } - } - - let mingling_path = find_mingling_relative_path(manifest_path); - - let deps_section = if features_str.is_empty() { - format!("[dependencies]\nmingling = {{ path = \"{mingling_path}\" }}\n{extra_deps}",) - } else { - format!( - "[dependencies]\nmingling = {{ path = \"{mingling_path}\", {features_str} }}\n{extra_deps}", - ) - }; - - // Build-time blocks: add `builds` by default, merge with explicit features - let build_deps_section = if block.is_build_time { - let mut all_feats = vec!["builds".to_string()]; - for f in &block.features { - if f != "builds" { - all_feats.push(f.clone()); - } - } - let feats_str: Vec = all_feats.iter().map(|f| format!("\"{f}\"")).collect(); - let build_feats = format!("features = [{}]", feats_str.join(", ")); - format!( - "\n[build-dependencies]\nmingling = {{ path = \"{mingling_path}\", {build_feats} }}\n" - ) - } else { - String::new() - }; - - format!( - r#"[package] - name = "{package_name}" - version = "0.0.0" - edition = "2024" - -{deps_section}{build_deps_section} -[workspace] -"# - ) -} - -/// Compute the relative path from a Cargo.toml's parent directory to the `mingling` crate. -/// -/// The process current directory is expected to be the project root (where `mingling/` lives). -/// Returns a forward-slash path safe for embedding in TOML strings. -fn find_mingling_relative_path(manifest_path: &Path) -> String { - let manifest_dir = manifest_path - .parent() - .expect("manifest_path has no parent directory"); - let cwd = std::env::current_dir().expect("failed to get current directory"); - - // Strip cwd prefix to get the relative components of the manifest directory - let relative_to_root = manifest_dir.strip_prefix(&cwd).unwrap_or(manifest_dir); - let depth = relative_to_root.components().count(); - - let mut result = String::new(); - for _ in 0..depth { - result.push_str("../"); - } - result.push_str("mingling"); - result -} - -/// Generate main.rs for a block -/// -/// Automatically prepends `use mingling::prelude::*;` if the block doesn't already have it. -pub fn generate_main_rs(block: &CodeBlock) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if !block.code.contains("use mingling::prelude::*;") { - output.push_str("#[allow(unused_imports)]\nuse mingling::prelude::*;\n\n"); - } - - output.push_str(&block.code); - output.push('\n'); - - if !block.has_main { - output.push_str("\nfn main() {}\n"); - } - - if !block.has_gen_program { - output.push_str("\nmingling::macros::gen_program!();\n"); - } - - output -} - -/// Generate build.rs for a build-time block -/// -/// Default: `use mingling::builds::*;`, code wrapped in `fn main() { }`. -pub fn generate_build_rs(block: &CodeBlock) -> String { - let mut output = String::from("#![allow(dead_code)]\n#![allow(unused)]\n"); - - if !block.code.contains("use mingling::build::*;") { - output.push_str("#[allow(unused_imports)]\nuse mingling::build::*;\n\n"); - } - - if block.has_main { - output.push_str(&block.code); - } else { - output.push_str("fn main() {\n"); - for line in block.code.lines() { - output.push_str(" "); - output.push_str(line); - output.push('\n'); - } - output.push_str("}\n"); - } - - output -} - -/// Build a single code block as a Cargo project. -/// -/// When `is_build_time` is true, `src_content` is written to `build.rs` instead of `src/main.rs`, -/// and a minimal `src/main.rs` stub (`fn main() {}`) is created. -pub fn build_block( - src_dir: &Path, - manifest_path: &Path, - cargo_toml: &str, - src_content: &str, - is_build_time: bool, -) -> (bool, String) { - if let Err(e) = std::fs::create_dir_all(src_dir) { - return (false, format!("mkdir: {e}")); - } - - // Write Cargo.toml - if let Err(e) = std::fs::write(manifest_path, cargo_toml) { - return (false, format!("write Cargo.toml: {e}")); - } - - if is_build_time { - // Write build.rs and a stub main.rs - let crate_dir = manifest_path.parent().unwrap(); - if let Err(e) = std::fs::write(crate_dir.join("build.rs"), src_content) { - return (false, format!("write build.rs: {e}")); - } - if let Err(e) = std::fs::write(src_dir.join("main.rs"), "fn main() {}\n") { - return (false, format!("write main.rs: {e}")); - } - } else { - // Normal: write src/main.rs - if let Err(e) = std::fs::write(src_dir.join("main.rs"), src_content) { - return (false, format!("write main.rs: {e}")); - } - } - - // Check code — inherit stderr so cargo output is real-time and colored - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let cmd = format!( - "cargo check --color=always --manifest-path {}", - manifest_path.to_string_lossy() - ); - - let mut child = match std::process::Command::new(shell) - .arg("-c") - .arg(&cmd) - .stdout(std::process::Stdio::inherit()) - .stderr(std::process::Stdio::piped()) - .current_dir(std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))) - .spawn() - { - Ok(c) => c, - Err(e) => return (false, format!("spawn: {e}")), - }; - - // Read stderr (buffered, not forwarded — groups print their own output contiguously) - use std::io::BufRead; - let stderr_handle = child.stderr.take().unwrap(); - let reader = std::io::BufReader::new(stderr_handle); - let mut captured = String::new(); - for line in reader.lines() { - match line { - Ok(l) => { - captured.push_str(&l); - captured.push('\n'); - } - Err(_) => break, - } - } - - let status = child.wait().unwrap_or_else(|_| std::process::exit(1)); - let exit_code = status.code().unwrap_or(1); - - if exit_code == 0 { - (true, String::new()) - } else { - let mut last_lines: Vec<&str> = captured.lines().rev().take(20).collect(); - last_lines.reverse(); - let detail = last_lines.join("\n"); - (false, format!("exit code {exit_code}\n{detail}")) - } -} - -/// Compute a stable hash for a code block based on its dependency configuration. -/// -/// Blocks with the same features and external dependencies produce the same hash, -/// allowing them to share a compiled crate and avoid redundant recompilation. -/// -/// Hash input (all sorted for stability): -/// - Sorted mingling feature strings -/// - Sorted external dependency names -/// - Sorted external dependency versions -/// - Sorted external deps as `name=version` pairs -pub fn compute_block_hash(block: &CodeBlock) -> String { - let mut features: Vec<&str> = block.features.iter().map(|s| s.as_str()).collect(); - features.sort(); - let features_str = features.join(","); - - let mut dep_names: Vec<&str> = block - .external_deps - .iter() - .map(|(n, _)| n.as_str()) - .collect(); - dep_names.sort(); - let dep_names_str = dep_names.join(","); - - let mut dep_versions: Vec<&str> = block - .external_deps - .iter() - .map(|(_, v)| v.as_str()) - .collect(); - dep_versions.sort(); - let dep_versions_str = dep_versions.join(","); - - let mut deps: Vec = block - .external_deps - .iter() - .map(|(n, v)| format!("{n}={v}")) - .collect(); - deps.sort(); - let deps_str = deps.join(","); - - let canonical = format!("{features_str}\n{dep_names_str}\n{dep_versions_str}\n{deps_str}"); - - // FNV-1a 64-bit hash — stable across runs (no random seed) - let mut hash: u64 = 0xcbf29ce484222325; - for &byte in canonical.as_bytes() { - hash ^= byte as u64; - hash = hash.wrapping_mul(0x100000001b3); - } - - format!("{:016x}", hash) -} - -/// Determine if a block should be treated as a test candidate. -/// A block is NOT testable only if it has `// NOT VERIFIED` marker. -pub fn is_block_testable(block: &CodeBlock) -> bool { - !block.not_verified -} - -/// Write a summary report -pub fn write_summary_report( - path: &Path, - title: &str, - results: &[(String, usize, bool, String)], - total: usize, - passed: usize, - failed: usize, -) { - let mut content = String::new(); - content.push_str(&format!("# {title}\n\n")); - content.push_str(&format!( - "Tested **{total}** code blocks: **{passed}** passed, **{failed}** failed.\n\n" - )); - content.push_str("## Results\n\n"); - content.push_str("| Block | File | Line | Status |\n"); - content.push_str("|-------|------|------|--------|\n"); - - for (i, (file, line, ok, _)) in results.iter().enumerate() { - let status = if *ok { "PASS" } else { "FAIL" }; - let short_file = file.rsplit('/').next().unwrap_or(file); - content.push_str(&format!( - "| {} | {} | {} | {status} |\n", - i + 1, - short_file, - line - )); - } - - let has_failures = results.iter().any(|(_, _, ok, _)| !ok); - if has_failures { - content.push_str("\n## Failed Blocks\n\n"); - for (i, (file, line, ok, err)) in results.iter().enumerate() { - if !ok { - content.push_str(&format!( - "### Block {} (`{}`, line {})\n\n```\n{err}\n```\n\n", - i + 1, - file, - line - )); - } - } - } - - std::fs::write(path, &content).unwrap_or_else(|e| { - eprintln!("Warning: failed to write {path:?}: {e}"); - }); - - println_cargo_style!("Report: written to {}", path.display()); -} diff --git a/dev_tools/version-files.toml b/dev_tools/version-files.toml deleted file mode 100644 index ca104d2..0000000 --- a/dev_tools/version-files.toml +++ /dev/null @@ -1,19 +0,0 @@ -[[file]] -file = "./Cargo.toml" -pattern = "version = \"{VER}\"" - -[[file]] -file = "./README.md" -pattern = "version = \"{VER}\"" - -[[file]] -file = "./docs/_zh_CN/pages/1-getting-started.md" -pattern = "version = \"{VER}\"" - -[[file]] -file = "./docs/pages/1-getting-started.md" -pattern = "version = \"{VER}\"" - -[[file]] -file = "./docs/res/guide.txt" -pattern = "mingling = \"{VER}\"" diff --git a/run-tools.ps1 b/run-tools.ps1 deleted file mode 100644 index 7aa2c9c..0000000 --- a/run-tools.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ErrorAction Stop - -# Collect all available tool names -$tools = @() - -if (Test-Path "dev_tools/scripts") { - $scripts = Get-ChildItem -Path "dev_tools/scripts/*.ps1", "dev_tools/scripts/*.py" - foreach ($script in $scripts) { - if ($script -is [System.IO.FileInfo]) { - $tools += $script.BaseName - } - } -} -if (Test-Path "dev_tools/src/bin") { - $files = Get-ChildItem -Path "dev_tools/src/bin/*.rs" - foreach ($file in $files) { - if ($file -is [System.IO.FileInfo]) { - $tools += $file.BaseName - } - } -} - -if ($args.Count -eq 0) { - Write-Host "Available:" - for ($i = 0; $i -lt $tools.Count; $i++) { - Write-Host (" [{0,2}] {1}" -f ($i + 1), $tools[$i]) - } - exit 1 -} - -$target_name = $args[0] - -# Check if input is a number -if ($target_name -match '^\d+$') { - $idx = [int]$target_name - 1 - if ($idx -ge 0 -and $idx -lt $tools.Count) { - $target_name = $tools[$idx] - } else { - Write-Host "Error: invalid number '$target_name', valid range is 1-$($tools.Count)" - exit 1 - } -} - -# Collect remaining arguments to pass to the script -$script_args = $args[1..$args.Count] - -$script_file_ps1 = "dev_tools/scripts/${target_name}.ps1" -$script_file_py = "dev_tools/scripts/${target_name}.py" -$rust_file = "dev_tools/src/bin/${target_name}.rs" - -if (Test-Path $script_file_ps1) { - & $script_file_ps1 $script_args -} elseif (Test-Path $script_file_py) { - python $script_file_py $script_args -} elseif (Test-Path $rust_file) { - cargo run --manifest-path dev_tools/Cargo.toml --bin $target_name --quiet -- $script_args -} else { - Write-Host "Error: target '$target_name' does not exist as a script or Rust program" - exit 1 -} diff --git a/run-tools.sh b/run-tools.sh deleted file mode 100755 index 9a68f0e..0000000 --- a/run-tools.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash - -cd "$(dirname "$0")" || exit 1 - -# Collect all available tool names -tools=() - -if [ -d "dev_tools/scripts" ]; then - for file in dev_tools/scripts/*.sh; do - if [ -f "$file" ]; then - tools+=("$(basename "$file" .sh)") - fi - done - for file in dev_tools/scripts/*.py; do - if [ -f "$file" ]; then - tools+=("$(basename "$file" .py)") - fi - done -fi -if [ -d "dev_tools/src/bin" ]; then - for file in dev_tools/src/bin/*.rs; do - if [ -f "$file" ]; then - tools+=("$(basename "$file" .rs)") - fi - done -fi - -if [ $# -eq 0 ]; then - echo "Available:" - for i in "${!tools[@]}"; do - printf " [%2d] %s\n" $((i + 1)) "${tools[$i]}" - done - exit 1 -fi - -target_bin="$1" -shift # Remove the first argument (tool name), keep the rest as tool arguments - -# Check if input is a number -if [[ "$target_bin" =~ ^[0-9]+$ ]]; then - idx=$((target_bin - 1)) - if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#tools[@]}" ]; then - target_bin="${tools[$idx]}" - else - echo "Error: invalid number '$target_bin', valid range is 1-${#tools[@]}" - exit 1 - fi -fi - -target_script="dev_tools/scripts/${target_bin}.sh" -target_python="dev_tools/scripts/${target_bin}.py" -target_file="dev_tools/src/bin/${target_bin}.rs" - -if [ -f "$target_script" ]; then - chmod +x "$target_script" - "$target_script" "$@" -elif [ -f "$target_python" ]; then - python "$target_python" "$@" -elif [ -f "$target_file" ]; then - cargo run --manifest-path dev_tools/Cargo.toml --bin "$target_bin" --quiet -- "$@" -else - echo "Error: target '$target_bin' does not exist" - exit 1 -fi diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 0000000..af8effe --- /dev/null +++ b/run.ps1 @@ -0,0 +1,232 @@ +# +# ██ ██████ ██ ██ ███ ██ ██████ ███████ ██ +# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ ███ +# ██ ██████ ██ ██ ██ ██ ██ ██████ ███████ ██ +# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +# ██ ██ ██ ██ ██████ ██ ████ ██ ██ ███████ ██ +# +# You can go to [https://catilgrass.github.io/run] to install it +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) -ErrorAction Stop + +$tools = @{} + +if (Test-Path ".run/src/bin/*.ps1") { + Get-ChildItem -Path ".run/src/bin/*.ps1" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "ps1"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.cs") { + Get-ChildItem -Path ".run/src/bin/*.cs" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "cs"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.exe") { + Get-ChildItem -Path ".run/src/bin/*.exe" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "exe"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.go") { + Get-ChildItem -Path ".run/src/bin/*.go" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "go"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.nim") { + Get-ChildItem -Path ".run/src/bin/*.nim" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "nim"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.pl") { + Get-ChildItem -Path ".run/src/bin/*.pl" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "pl"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.py") { + Get-ChildItem -Path ".run/src/bin/*.py" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "py"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.rb") { + Get-ChildItem -Path ".run/src/bin/*.rb" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "rb"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.rs") { + Get-ChildItem -Path ".run/src/bin/*.rs" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "rs"; Path = $_.FullName } + } +} + +if (Test-Path ".run/src/bin/*.zig") { + Get-ChildItem -Path ".run/src/bin/*.zig" | ForEach-Object { + $tools[$_.BaseName] = @{ Type = "zig"; Path = $_.FullName } + } +} + +if ($args.Count -eq 0) { + $sorted = @($tools.Keys | Sort-Object @{Expression={if ($tools[$_].Type -eq 'ps1') {0} else {1}}}, {$_}) + $total = $sorted.Count + $num_w = $total.ToString().Length + + $max_name = ($sorted | ForEach-Object { $_.Length } | Measure-Object -Maximum).Maximum + + $inner_w = 2 + $num_w + 1 + 1 + $max_name + 2 + 10 + 1 + 2 + if ($inner_w -lt 38) { $inner_w = 38 } + + $title = '.\run.ps1 [ARGS...]' + $title_len = $title.Length + $dash_total = $inner_w - 2 - $title_len + $dash_left = [Math]::Floor($dash_total / 2) + $dash_right = $dash_total - $dash_left + + Write-Host ("┌" + ("─" * $dash_left) + " " + $title + " " + ("─" * $dash_right) + "┐") + Write-Host ("│" + (" " * $inner_w) + "│") + + $i = 1 + foreach ($name in $sorted) { + $type = $tools[$name].Type + $lang = switch ($type) { + "ps1" { "PowerShell" } + "cs" { "C#" } + "exe" { "Binary" } + "go" { "Go" } + "nim" { "Nim" } + "pl" { "Perl" } + "py" { "Python" } + "rb" { "Ruby" } + "rs" { "Rust" } + "zig" { "Zig" } + } + $displayName = $name -replace '[_\-]', ' ' + $entry = " " + $i.ToString().PadRight($num_w) + ") " + $displayName.PadRight($max_name) + " [$lang]" + Write-Host ("│" + $entry.PadRight($inner_w) + "│") + $i++ + } + + Write-Host ("│" + (" " * $inner_w) + "│") + Write-Host ("└" + ("─" * $inner_w) + "┘") + exit 1 +} + +$target_name = $args[0] + +if ($target_name -match '^\d+$') { + $sorted = @($tools.Keys | Sort-Object @{Expression={if ($tools[$_].Type -eq 'ps1') {0} else {1}}}, {$_}) + $idx = [int]$target_name - 1 + if ($idx -ge 0 -and $idx -lt $sorted.Count) { + $target_name = $sorted[$idx] + } else { + Write-Host "Error: invalid number '$target_name', valid range is 1-$($sorted.Count)" + exit 1 + } +} + +if (-not $tools.ContainsKey($target_name)) { + $normalized = $target_name.ToLower() -replace '[ _\-\.]', '' + $found = $null + foreach ($key in $tools.Keys) { + $keyNormalized = $key.ToLower() -replace '[ _\-\.]', '' + if ($normalized -eq $keyNormalized) { + $found = $key + break + } + } + if ($found) { + $target_name = $found + } else { + Write-Host "Error: target '$target_name' does not exist" + exit 1 + } +} + +$script_args = $args[1..$args.Count] +$info = $tools[$target_name] + +switch ($info.Type) { + "ps1" { + & $info.Path @script_args + } + "cs" { + $temp_dir = ".run/target/csproj/$target_name" + $null = New-Item -ItemType Directory -Path $temp_dir -Force + + $props_content = @' + + + $(MSBuildThisFileDirectory)../../csharp/bin + $(MSBuildThisFileDirectory)../../csharp/obj + + +'@ + Set-Content -Path "$temp_dir/Directory.Build.props" -Value $props_content + + $csproj_content = @" + + + + Exe + net8.0 + enable + enable + + + +"@ + Set-Content -Path "$temp_dir/$target_name.csproj" -Value $csproj_content + Copy-Item -Path $info.Path -Destination "$temp_dir/Program.cs" -Force + + dotnet run --project "$temp_dir/$target_name.csproj" -- $script_args + } + "exe" { + & $info.Path $script_args + } + "go" { + go run $info.Path $script_args + } + "nim" { + nim r --hints:off $info.Path $script_args + } + "pl" { + perl $info.Path $script_args + } + "py" { + python $info.Path $script_args + } + "rb" { + ruby $info.Path $script_args + } + "rs" { + if (-not (Test-Path ".run/Cargo.toml")) { +@" +[package] +name = "run_rust" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +"@ | Set-Content -Path ".run/Cargo.toml" + } + cargo build --manifest-path ".run/Cargo.toml" --target-dir ".run/target" --bin $target_name --quiet + $binary = ".run/target/debug/$target_name.exe" + if (-not (Test-Path $binary)) { + $binary = ".run/target/debug/$target_name" + } + & $binary $script_args + } + "zig" { + zig run $info.Path $script_args + } +} + +exit $LASTEXITCODE diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..cc03790 --- /dev/null +++ b/run.sh @@ -0,0 +1,257 @@ +#!/bin/bash + +# +# ██ ██████ ██ ██ ███ ██ ███████ ██ ██ +# ██ ██ ██ ██ ██ ████ ██ ██ ██ ██ +# ██ ██████ ██ ██ ██ ██ ██ ███████ ███████ +# ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +# ██ ██ ██ ██ ██████ ██ ████ ██ ███████ ██ ██ +# +# You can go to [https://catilgrass.github.io/run] to install it +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +cd "$(dirname "$0")" || exit 1 + +declare -A tools + +for file in .run/src/bin/*.sh; do + if [ -f "$file" ]; then + name=$(basename "$file" .sh) + tools["$name"]="sh" + fi +done + +for file in .run/src/bin/*; do + if [ -f "$file" ]; then + name=$(basename "$file") + if [[ ! "$name" == *.* ]]; then + tools["$name"]="binary" + fi + fi +done + +for file in .run/src/bin/*.cs; do + if [ -f "$file" ]; then + name=$(basename "$file" .cs) + tools["$name"]="cs" + fi +done + +for file in .run/src/bin/*.go; do + if [ -f "$file" ]; then + name=$(basename "$file" .go) + tools["$name"]="go" + fi +done + +for file in .run/src/bin/*.nim; do + if [ -f "$file" ]; then + name=$(basename "$file" .nim) + tools["$name"]="nim" + fi +done + +for file in .run/src/bin/*.pl; do + if [ -f "$file" ]; then + name=$(basename "$file" .pl) + tools["$name"]="pl" + fi +done + +for file in .run/src/bin/*.py; do + if [ -f "$file" ]; then + name=$(basename "$file" .py) + tools["$name"]="py" + fi +done + +for file in .run/src/bin/*.rb; do + if [ -f "$file" ]; then + name=$(basename "$file" .rb) + tools["$name"]="rb" + fi +done + +for file in .run/src/bin/*.rs; do + if [ -f "$file" ]; then + name=$(basename "$file" .rs) + tools["$name"]="rs" + fi +done + +for file in .run/src/bin/*.zig; do + if [ -f "$file" ]; then + name=$(basename "$file" .zig) + tools["$name"]="zig" + fi +done + +if [ $# -eq 0 ]; then + sorted_names=($( + for name in "${!tools[@]}"; do + if [ "${tools[$name]}" = "sh" ]; then + echo "0 $name" + else + echo "1 $name" + fi + done | sort | while read -r _ n; do echo "$n"; done + )) + total=${#sorted_names[@]} + num_w=${#total} + + max_name=0 + for name in "${sorted_names[@]}"; do + len=${#name} + ((len > max_name)) && max_name=$len + done + + inner_w=$((2 + num_w + 1 + 1 + max_name + 2 + 6 + 1 + 2)) + ((inner_w < 38)) && inner_w=38 + + title="./run.sh [ARGS...]" + title_len=${#title} + dash_total=$((inner_w - 2 - title_len)) + dash_left=$((dash_total / 2)) + dash_right=$((dash_total - dash_left)) + + echo "┌$(printf '─%.0s' $(seq 1 $dash_left)) $title $(printf '─%.0s' $(seq 1 $dash_right))┐" + printf "│%*s│\n" $inner_w "" + + i=1 + for name in "${sorted_names[@]}"; do + type="${tools[$name]}" + case "$type" in + sh) lang="Shell";; + binary) lang="Binary";; + cs) lang="C#";; + go) lang="Go";; + nim) lang="Nim";; + pl) lang="Perl";; + py) lang="Python";; + rb) lang="Ruby";; + rs) lang="Rust";; + zig) lang="Zig";; + esac + display_name=$(echo "$name" | tr '_-' ' ') + entry=$(printf " %-*d) %-*s [%s]" $num_w $i $max_name "$display_name" $lang) + printf "│%-*s│\n" $inner_w "$entry" + i=$((i + 1)) + done + + printf "│%*s│\n" $inner_w "" + echo "└$(printf '─%.0s' $(seq 1 $inner_w))┘" + exit 1 +fi + +target_name="$1" +shift + +if [[ "$target_name" =~ ^[0-9]+$ ]]; then + sorted=($( + for name in "${!tools[@]}"; do + if [ "${tools[$name]}" = "sh" ]; then + echo "0 $name" + else + echo "1 $name" + fi + done | sort | while read -r _ n; do echo "$n"; done + )) + idx=$((target_name - 1)) + if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#sorted[@]}" ]; then + target_name="${sorted[$idx]}" + else + echo "Error: invalid number '$target_name', valid range is 1-${#sorted[@]}" + exit 1 + fi +fi + +if [ -z "${tools[$target_name]}" ]; then + normalized_user=$(echo "$target_name" | tr '[:upper:]' '[:lower:]' | sed 's/[_. -]//g') + found="" + for existing_name in "${!tools[@]}"; do + normalized_existing=$(echo "$existing_name" | tr '[:upper:]' '[:lower:]' | sed 's/[_. -]//g') + if [ "$normalized_user" = "$normalized_existing" ]; then + found="$existing_name" + break + fi + done + if [ -n "$found" ]; then + target_name="$found" + else + echo "Error: target '$target_name' does not exist" + exit 1 + fi +fi + +type="${tools[$target_name]}" + +case "$type" in + sh) + chmod +x ".run/src/bin/$target_name.sh" + ".run/src/bin/$target_name.sh" "$@" + ;; + binary) + chmod +x ".run/src/bin/$target_name" + ".run/src/bin/$target_name" "$@" + ;; + cs) + temp_dir=".run/target/csproj/$target_name" + mkdir -p "$temp_dir" + cat > "$temp_dir/Directory.Build.props" <<'PROPS' + + + $(MSBuildThisFileDirectory)../../csharp/bin + $(MSBuildThisFileDirectory)../../csharp/obj + + +PROPS + cat > "$temp_dir/$target_name.csproj" <<'CSPROJ' + + + + Exe + net8.0 + enable + enable + + + +CSPROJ + cp ".run/src/bin/$target_name.cs" "$temp_dir/Program.cs" + dotnet run --project "$temp_dir/$target_name.csproj" -- "$@" + ;; + go) + go run ".run/src/bin/$target_name.go" "$@" + ;; + nim) + nim r --hints:off ".run/src/bin/$target_name.nim" "$@" + ;; + pl) + perl ".run/src/bin/$target_name.pl" "$@" + ;; + py) + python ".run/src/bin/$target_name.py" "$@" + ;; + rb) + ruby ".run/src/bin/$target_name.rb" "$@" + ;; + rs) + if [ ! -f ".run/Cargo.toml" ]; then + cat > ".run/Cargo.toml" <<'EOF' +[package] +name = "run_rust" +version = "0.1.0" +edition = "2024" + +[workspace] + +[dependencies] +EOF + fi + cargo build --manifest-path ".run/Cargo.toml" --target-dir ".run/target" --bin "$target_name" --quiet + ".run/target/debug/$target_name" "$@" + ;; + zig) + zig run ".run/src/bin/$target_name.zig" "$@" + ;; +esac -- cgit