diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-08-19 05:54:00 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-08-19 06:15:20 +0800 |
| commit | ec9edc294fd5e7e29977fc7b0e6fb953422bc0e2 (patch) | |
| tree | bbf89ef4457b8de8a8a9bca5668045d612d27572 /.run | |
| parent | 06dfc27194c11e1d1033c292c759a1c5d82e780b (diff) | |
chore: reorganize dev tools into dev/ directory and consolidate configs
Move CI, dev tools, and configs from root-level scattered locations into
a
unified `dev/` directory structure. Update all references across build
scripts, documentation, and editor configurations.
Also consolidate editor config generation into build.rs for automated
synchronization of rust-analyzer settings across VS Code and Zed.
Diffstat (limited to '.run')
| -rw-r--r-- | .run/.gitignore | 5 | ||||
| -rw-r--r-- | .run/Cargo.lock | 572 | ||||
| -rw-r--r-- | .run/Cargo.toml | 26 | ||||
| -rw-r--r-- | .run/src/bin/ci.py | 75 | ||||
| -rw-r--r-- | .run/src/bin/clippy-fix.ps1 | 8 | ||||
| -rwxr-xr-x | .run/src/bin/clippy-fix.sh | 6 | ||||
| -rw-r--r-- | .run/src/bin/cov-test.rs | 571 | ||||
| -rw-r--r-- | .run/src/bin/deploy-api-docs.rs | 118 | ||||
| -rw-r--r-- | .run/src/bin/display-dependency-order.rs | 12 | ||||
| -rw-r--r-- | .run/src/bin/http-page-preview.ps1 | 3 | ||||
| -rwxr-xr-x | .run/src/bin/http-page-preview.sh | 2 | ||||
| -rw-r--r-- | .run/src/bin/install-mling.ps1 | 10 | ||||
| -rwxr-xr-x | .run/src/bin/install-mling.sh | 17 | ||||
| -rw-r--r-- | .run/src/bin/package-all.rs | 736 | ||||
| -rw-r--r-- | .run/src/bin/update-version.rs | 104 | ||||
| -rw-r--r-- | .run/src/bin/windows-folder-hide.ps1 | 115 | ||||
| -rw-r--r-- | .run/src/dependency_order.rs | 196 | ||||
| -rw-r--r-- | .run/src/lib.rs | 459 | ||||
| -rw-r--r-- | .run/src/verify.rs | 506 |
19 files changed, 0 insertions, 3541 deletions
diff --git a/.run/.gitignore b/.run/.gitignore deleted file mode 100644 index 40f407e..0000000 --- a/.run/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# All temp build artifacts will be stored in this dir -/target - -# If you want to add some Rust crates? Remove this line -# /Cargo.* diff --git a/.run/Cargo.lock b/.run/Cargo.lock deleted file mode 100644 index 3117d0b..0000000 --- a/.run/Cargo.lock +++ /dev/null @@ -1,572 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "arg-picker" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd62c395708a956e98e06b6b18200b2a65a5ae63448ed7d59cca32833aaf7265" -dependencies = [ - "arg-picker-macros", - "just_fmt 0.2.0", -] - -[[package]] -name = "arg-picker-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04fddbb5c1f26450cfc6aded0559b759b292d400dba1ff4e45128f3e0e1ffe6b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bitflags" -version = "2.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" - -[[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 = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[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 = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[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_fmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" - -[[package]] -name = "just_template" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3edb658c34b10b69c4b3b58f7ba989cd09c82c0621dee1eef51843c2327225" -dependencies = [ - "just_fmt 0.1.2", -] - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[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.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[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 = "simd-adler32" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" - -[[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.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[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 = [ - "arg-picker", - "colored", - "flate2", - "indicatif", - "just_fmt 0.1.2", - "just_template", - "serde", - "serde_json", - "tar", - "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 = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[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 deleted file mode 100644 index 4935f2d..0000000 --- a/.run/Cargo.toml +++ /dev/null @@ -1,26 +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" -flate2 = "1" -tar = "0.4" -arg-picker = "0.1.0" - -[workspace] diff --git a/.run/src/bin/ci.py b/.run/src/bin/ci.py deleted file mode 100644 index 6234a19..0000000 --- a/.run/src/bin/ci.py +++ /dev/null @@ -1,75 +0,0 @@ -"""Full CI orchestration for the mingling project. - -Runs every `cargo ci` step in order: lock the workspace, run all checks, -refresh the generated artifacts, then unlock. The final `git-unlock` doubles -as the idempotency check: it fails with a non-zero exit code when the run -left the working tree dirty. - -The script locates the git repository root and runs with it as the working -directory, so it can be invoked from anywhere inside the repo. -""" - -import os -import subprocess -import sys -from pathlib import Path - -# The pipeline steps, in execution order, as (command, args) pairs. -STEPS: list[tuple[str, list[str]]] = [ - ("git-lock", []), - ("report-clean", []), - ("build-check", []), - ("clippy-check", []), - ("test-all", []), - ("example-check", []), - ("docs-check", []), - ("example-refresh", []), - ("docsify-refresh", []), - ("features-refresh", []), - # Idempotency check: exits non-zero if CI contaminated the workspace, and - # prints the diff of the contamination before restoring. - ("git-unlock", ["--show-diff"]), -] - - -def find_repo_root() -> Path: - """Return the nearest ancestor directory containing `.git`.""" - current = Path.cwd() - for directory in (current, *current.parents): - if (directory / ".git").is_dir(): - return directory - raise SystemExit("error: not inside a git repository") - - -def main() -> int: - root = find_repo_root() - os.chdir(root) - - # Signature banner: docs/res/ci_banner.txt, relative to this script - # (.run/src/bin -> four levels up is the repo root). - banner = ( - Path(__file__).resolve().parent.parent.parent.parent - / "docs" - / "res" - / "ci_banner.txt" - ) - try: - print(banner.read_text(encoding="utf-8"), end="") - except OSError: - pass - - for command, args in STEPS: - print(f"==> cargo ci {' '.join([command, *args])}") - result = subprocess.run(["cargo", "ci", command, *args], check=False) - if result.returncode != 0: - print( - f"error: step `{command}` failed with exit code {result.returncode}", - file=sys.stderr, - ) - return result.returncode - - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.run/src/bin/clippy-fix.ps1 b/.run/src/bin/clippy-fix.ps1 deleted file mode 100644 index 1d24f92..0000000 --- a/.run/src/bin/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/.run/src/bin/clippy-fix.sh b/.run/src/bin/clippy-fix.sh deleted file mode 100755 index 9771ad4..0000000 --- a/.run/src/bin/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/.run/src/bin/cov-test.rs b/.run/src/bin/cov-test.rs deleted file mode 100644 index f62ff01..0000000 --- a/.run/src/bin/cov-test.rs +++ /dev/null @@ -1,571 +0,0 @@ -//! Coverage test generator for mingling. -//! -//! This script requires the **fork** of cargo-llvm-cov: -//! <https://github.com/Weicao-CatilGrass/cargo-llvm-cov> -//! -//! The upstream `report` command cannot include binaries of non-workspace -//! crates (examples and test crates) and unconditionally filters -//! `tests`/`examples` source files. The fork adds two flags to fix this: -//! -//! - `--object <PATH>`: include arbitrary binaries in the report -//! (upstream issue taiki-e/cargo-llvm-cov#367) -//! - `--include-examples`: stop filtering source files under the -//! `examples` directory (upstream issue taiki-e/cargo-llvm-cov#503) -//! -//! The script itself does not use `--include-examples`; it passes -//! `--no-default-ignore-filename-regex` and supplies its own filter so that -//! `tests`/`benches` directories stay in the report too. -//! -//! Install it with: -//! -//! ```bash -//! cargo install --git https://github.com/Weicao-CatilGrass/cargo-llvm-cov cargo-llvm-cov -//! ``` - -use std::fs; -use std::path::{Path, PathBuf}; - -use serde::Deserialize; -use tools::{eprintln_cargo_style, println_cargo_style, run_cmd}; - -const OUTPUT_DIR: &str = "docs/cov-test"; - -/// Shared target directory for all `cargo llvm-cov` runs. -/// -/// Pointing every run at the same target dir makes all of them share the -/// instrumented build cache and, more importantly, accumulate profraw files -/// in one place so the final `report` can merge everything. -const COV_TARGET_DIR: &str = ".temp/cov-llvm"; - -/// An example's `test.toml` (`[[runs]]` entries). -#[derive(Deserialize)] -struct TestConfig { - runs: Vec<TestCase>, -} - -/// One `[[runs]]` entry of an example's `test.toml`. -#[derive(Deserialize)] -struct TestCase { - input: Vec<String>, -} - -fn main() { - let repo_root = find_git_repo().expect("Failed to find git repository root"); - let output_path = repo_root.join(OUTPUT_DIR); - let cov_target = repo_root.join(COV_TARGET_DIR); - - // Read features from [package.metadata.docs.rs] - let features = tools::read_features().unwrap_or_else(|e| { - eprintln!("Error: {}", e); - std::process::exit(1); - }); - let features_arg = features.join(","); - - // Ensure output directory exists - std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); - std::fs::create_dir_all(&cov_target).expect("Failed to create cov target directory"); - - // All `cargo llvm-cov` invocations below share one target dir, so profraw - // files accumulate and are merged by the final `report` command. - // SAFETY: set before any thread is spawned; this process only shells out - // to subcommands via std::process. - unsafe { - std::env::set_var("CARGO_LLVM_COV_TARGET_DIR", &cov_target); - } - - // Drop stale profraw from previous runs (keep the instrumented build cache). - clean_old_profraw(&cov_target); - - println_cargo_style!("Features: {}", features_arg); - println_cargo_style!("Target: {}", cov_target.display()); - - // 1. Workspace tests - println_cargo_style!("Running: cargo llvm-cov test --workspace"); - run_cmd!(format!( - "cargo llvm-cov test --no-report --workspace --features \"{}\" --color always", - features_arg - )) - .unwrap_or_else(|code| { - eprintln_cargo_style!("workspace tests failed with exit code {}", code); - std::process::exit(code); - }); - - // 2. Integration test crates under mingling_core/tests (excluded from the - // workspace, so they need their own `--manifest-path` runs) - for manifest in find_test_crate_manifests(&repo_root) { - println_cargo_style!( - "Running: cargo llvm-cov test {}", - manifest.file_name().unwrap_or_default().to_string_lossy() - ); - run_cmd!(format!( - "cargo llvm-cov test --no-report --manifest-path \"{}\" --color always", - manifest.display() - )) - .unwrap_or_else(|code| { - eprintln_cargo_style!( - "test crate {} failed with exit code {}", - manifest.display(), - code - ); - std::process::exit(code); - }); - } - - // 3. Examples: build each example with explicit RUSTFLAGS, then execute - // every command declared in the example's test.toml directly. - // - // NOTE: `cargo llvm-cov run` cannot be used here. Its rustc wrapper - // only instruments the crates of the *current* cargo project (with - // `--manifest-path` that is the example itself), so the mingling - // libraries — being dependencies — would not be instrumented and their - // coverage would silently be lost (once_exec.rs showed 0%). Building - // with plain RUSTFLAGS instruments the whole dependency graph. - // - // RUSTFLAGS/CARGO_TARGET_DIR are set process-wide here because only the - // `report` step (which does not compile) follows. Non-zero exit codes - // are expected for some examples (e.g. `--help` exits with 2); profraw - // is still written. - unsafe { - std::env::set_var("RUSTFLAGS", "-Cinstrument-coverage"); - std::env::set_var("CARGO_TARGET_DIR", &cov_target); - } - let examples = load_example_commands(&repo_root); - let mut built = std::collections::HashSet::new(); - for (example, input) in &examples { - if built.insert(example.clone()) { - println_cargo_style!("Building: {}", example); - run_cmd!(format!( - "cargo build --manifest-path examples/{}/Cargo.toml --color always", - example - )) - .unwrap_or_else(|code| { - eprintln_cargo_style!( - "build of example {} failed with exit code {}", - example, - code - ); - std::process::exit(code); - }); - } - let binary = cov_target.join("debug").join(get_binary_name(example)); - let profraw = format!( - "{}/example-{}.%p.profraw", - cov_target.to_string_lossy(), - example - ); - match std::process::Command::new(&binary) - .args(input) - .env("LLVM_PROFILE_FILE", &profraw) - .status() - { - Ok(status) if status.success() => {} - Ok(status) => println_cargo_style!( - "Warning: example {} exited with {:?}, profraw still recorded", - example, - status.code() - ), - Err(e) => eprintln_cargo_style!("Failed to run example {}: {}", example, e), - } - } - - // 4. Collect the binaries of non-workspace crates (examples + test crates). - // The automatic object-file detection only knows workspace members, so - // these must be passed explicitly via --object. - let member_names = workspace_member_names(&repo_root); - let object_args = collect_object_args(&cov_target, &member_names); - - // 5. Generate the merged HTML report. - // - // --no-default-ignore-filename-regex: the default regex unconditionally - // excludes `examples`/`tests` directories, which is exactly what we want - // to include here, so we take over the filter ourselves. - let ignore_re = build_ignore_regex(&cov_target); - println_cargo_style!("Running: cargo llvm-cov report --html"); - run_cmd!(format!( - "cargo llvm-cov report --html --output-dir \"{}\" --no-default-ignore-filename-regex --ignore-filename-regex \"{}\" {} --color always", - output_path.to_string_lossy(), - ignore_re, - object_args - )) - .unwrap_or_else(|code| { - eprintln_cargo_style!("cargo llvm-cov report failed with exit code {}", code); - std::process::exit(code); - }); - - // Move files from <output_path>/html/ to <output_path> - let html_dir = output_path.join("html"); - if html_dir.exists() && html_dir.is_dir() { - println_cargo_style!("Moving files from {}/html/ to {}/", OUTPUT_DIR, OUTPUT_DIR); - - for entry in fs::read_dir(&html_dir).expect("Failed to read html directory") { - let entry = entry.expect("Failed to read entry"); - let entry_path = entry.path(); - let file_name = entry - .file_name() - .to_str() - .expect("Invalid filename") - .to_owned(); - - let dest_path = output_path.join(&file_name); - if dest_path.exists() { - if dest_path.is_dir() { - fs::remove_dir_all(&dest_path).unwrap_or_else(|e| { - eprintln!( - "Warning: could not remove directory {}: {}", - dest_path.display(), - e - ); - }); - } else { - fs::remove_file(&dest_path).unwrap_or_else(|e| { - eprintln!( - "Warning: could not remove file {}: {}", - dest_path.display(), - e - ); - }); - } - } - fs::rename(&entry_path, &dest_path).unwrap_or_else(|e| { - eprintln!("Warning: could not move {}: {}", entry_path.display(), e); - }); - } - - fs::remove_dir(&html_dir).unwrap_or_else(|e| { - eprintln!("Warning: could not remove html directory: {}", e); - }); - - println_cargo_style!("Files moved successfully."); - } - - // 6. Recolor the per-file coverage summary with project-specific - // thresholds: 0-50% red, 51-80% yellow, 81-100% green. llvm-cov's - // built-in thresholds differ, and the color is assigned when the HTML - // is generated, so the summary table is rewritten here. - let index_path = output_path.join("index.html"); - if let Err(e) = recolor_report_index(&index_path) { - eprintln_cargo_style!("Warning: failed to recolor {}: {}", index_path.display(), e); - } - - println_cargo_style!( - "Done: coverage report generated at {}/index.html", - OUTPUT_DIR - ); -} - -/// Remove `*.profraw` from the shared target dir so stale data from previous -/// runs does not pollute the merged report. The instrumented build cache -/// (everything else) is kept. -fn clean_old_profraw(cov_target: &Path) { - if let Ok(entries) = fs::read_dir(cov_target) { - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().is_some_and(|e| e == "profraw") { - let _ = fs::remove_file(&path); - } - } - } -} - -/// All `mingling_core/tests/<crate>/Cargo.toml` manifests. -fn find_test_crate_manifests(repo_root: &Path) -> Vec<PathBuf> { - let tests_dir = repo_root.join("mingling_core/tests"); - let mut manifests = Vec::new(); - if let Ok(entries) = fs::read_dir(&tests_dir) { - for entry in entries.flatten() { - let manifest = entry.path().join("Cargo.toml"); - if manifest.is_file() { - manifests.push(manifest); - } - } - } - manifests.sort(); - manifests -} - -/// Parse every `examples/<name>/test.toml` into `(example_name, input)` pairs. -fn load_example_commands(repo_root: &Path) -> Vec<(String, Vec<String>)> { - let examples_dir = repo_root.join("examples"); - let mut entries: Vec<_> = std::fs::read_dir(&examples_dir) - .unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to read {}: {}", examples_dir.display(), e); - std::process::exit(1); - }) - .flatten() - .collect(); - entries.sort_by_key(|e| e.file_name()); - - let mut pairs = Vec::new(); - for entry in entries { - let path = entry.path(); - if !path.is_dir() { - continue; - } - let test_toml = path.join("test.toml"); - if !test_toml.is_file() { - continue; - } - let name = path - .file_name() - .and_then(|n| n.to_str()) - .unwrap_or_default() - .to_string(); - let content = fs::read_to_string(&test_toml).unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to read {}: {}", test_toml.display(), e); - std::process::exit(1); - }); - let config: TestConfig = toml::from_str(&content).unwrap_or_else(|e| { - eprintln_cargo_style!("Failed to parse {}: {}", test_toml.display(), e); - std::process::exit(1); - }); - for case in config.runs { - pairs.push((name.clone(), case.input)); - } - } - pairs -} - -/// Names of all workspace members, from `cargo metadata --no-deps`. -fn workspace_member_names(repo_root: &Path) -> Vec<String> { - let Ok(output) = tools::run_cmd_capture_with_dir( - "cargo metadata --no-deps --format-version 1".to_string(), - repo_root, - ) else { - return Vec::new(); - }; - let Ok(json) = serde_json::from_str::<serde_json::Value>(&output) else { - return Vec::new(); - }; - json["packages"] - .as_array() - .into_iter() - .flatten() - .filter_map(|p| p["name"].as_str().map(str::to_owned)) - .collect() -} - -/// Collect the binaries of non-workspace crates (examples and test crates) -/// from the shared target dir, as `--object <path>` arguments. -/// -/// - `debug/` root: example binaries (built via `cargo llvm-cov run`). -/// - `debug/deps/`: test crate binaries (e.g. `integration-<hash>`); their -/// names do not follow a single pattern, so anything that is not a -/// workspace-member binary and not a proc-macro `.so` is collected. -/// -/// Workspace member binaries are detected automatically by `report` and must -/// NOT be passed again (duplicate `-object` entries produce duplicated -/// output). Hard links to the same file are deduplicated by inode. -fn collect_object_args(cov_target: &Path, member_names: &[String]) -> String { - let debug_dir = cov_target.join("debug"); - let mut objects = Vec::new(); - let mut seen = std::collections::HashSet::new(); - - for dir in [debug_dir.clone(), debug_dir.join("deps")] { - let Ok(entries) = fs::read_dir(&dir) else { - continue; - }; - for entry in entries.flatten() { - let path = entry.path(); - if !path.is_file() || !is_executable(&path) { - continue; - } - if !seen.insert(file_id(&path)) { - continue; - } - let Some(name) = path.file_name().and_then(|s| s.to_str()) else { - continue; - }; - // Proc-macro shared objects are either workspace members (picked - // up automatically) or external deps (excluded from the report - // by the ignore regex), so never pass them explicitly. - if name.starts_with("lib") && name.ends_with(".so") { - continue; - } - if is_workspace_member_binary(name, member_names) { - continue; - } - objects.push(path); - } - } - - objects.sort(); - objects - .iter() - .map(|p| format!("--object \"{}\"", p.to_string_lossy())) - .collect::<Vec<_>>() - .join(" ") -} - -/// True if the binary name (e.g. `mingling_core-fea14a01b88afcaa`) belongs to -/// a workspace member. -fn is_workspace_member_binary(name: &str, member_names: &[String]) -> bool { - let stem = strip_cargo_hash(name); - member_names.iter().any(|m| stem == m) -} - -/// Strip the cargo-generated hash suffix: `mingling_core-fea14a01b88afcaa` -> -/// `mingling_core`. Returns the input unchanged if there is no such suffix. -fn strip_cargo_hash(name: &str) -> &str { - let Some(idx) = name.rfind('-') else { - return name; - }; - let (head, tail) = name.split_at(idx); - let hash = &tail[1..]; - if hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit()) { - head - } else { - name - } -} - -/// A stable identity for deduplicating hard links: device+inode on Unix, -/// canonicalized path elsewhere. -fn file_id(path: &Path) -> String { - #[cfg(unix)] - { - use std::os::unix::fs::MetadataExt as _; - if let Ok(metadata) = fs::metadata(path) { - return format!("{}:{}", metadata.dev(), metadata.ino()); - } - } - fs::canonicalize(path) - .unwrap_or_else(|_| path.to_path_buf()) - .to_string_lossy() - .into_owned() -} - -/// 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() - } -} - -/// Rewrite the per-file coverage colors in `index.html` with project-specific -/// thresholds: 0-50% red, 51-80% yellow, 81-100% green. -fn recolor_report_index(index_path: &Path) -> std::io::Result<()> { - let content = fs::read_to_string(index_path)?; - fs::write(index_path, recolor_coverage_table(&content)) -} - -/// Recolor every `<td class='column-entry-...'><pre>XX% ...</pre></td>` cell -/// in the coverage summary table according to the new thresholds. Cells with -/// no data (e.g. branch coverage `- (0/0)`, class `gray`) are left as-is. -fn recolor_coverage_table(input: &str) -> String { - const TD: &str = "<td class='column-entry-"; - let mut out = String::with_capacity(input.len()); - let mut rest = input; - while let Some(pos) = rest.find(TD) { - out.push_str(&rest[..pos + TD.len()]); - rest = &rest[pos + TD.len()..]; - let Some(pre_end) = rest.find("'><pre>") else { - out.push_str(rest); - return out; - }; - let color = &rest[..pre_end]; - let tail = &rest[pre_end + "'><pre>".len()..]; - let pct: String = tail - .trim_start() - .chars() - .take_while(|c| c.is_ascii_digit() || *c == '.') - .collect(); - let new_color = match pct.parse::<f64>() { - Ok(v) if v <= 50.0 => "red", - Ok(v) if v <= 80.0 => "yellow", - Ok(_) => "green", - Err(_) => color, // no data (e.g. gray branch column) - }; - out.push_str(new_color); - out.push_str("'><pre>"); - rest = tail; - } - out.push_str(rest); - out -} - -/// True if the file is executable: mode bits on Unix, `.exe` on Windows. -fn is_executable(path: &Path) -> bool { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt as _; - let Ok(metadata) = std::fs::metadata(path) else { - return false; - }; - metadata.permissions().mode() & 0o111 != 0 - } - #[cfg(not(unix))] - { - path.extension() - .is_some_and(|e| e.eq_ignore_ascii_case("exe")) - } -} - -/// Regex that keeps only the project's own sources in the report: -/// excludes the shared llvm-cov target dir, the standard library, and -/// external dependencies. -fn build_ignore_regex(cov_target: &Path) -> String { - let target = regex_escape_path(cov_target); - format!( - "^{target}($|/)|/rustc/([0-9a-f]+|[0-9]+\\.[0-9]+\\.[0-9]+)/|/\\.cargo/(registry|git)/|/\\.rustup/toolchains($|/)" - ) -} - -/// Escape a path for use inside a regular expression (as a literal prefix). -fn regex_escape_path(path: &Path) -> String { - let s = path.to_string_lossy().replace('\\', "/"); - let mut escaped = String::with_capacity(s.len()); - for ch in s.chars() { - if ch == '.' || ch == '-' { - escaped.push('\\'); - } - escaped.push(ch); - } - escaped -} - -fn find_git_repo() -> Option<std::path::PathBuf> { - 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 -} - -#[cfg(test)] -mod tests { - use super::recolor_coverage_table; - - #[test] - fn recolor_thresholds() { - let input = concat!( - "<td class='column-entry-red'><pre> 50.00% (2/4)</pre></td>", - "<td class='column-entry-yellow'><pre> 51.23% (32/52)</pre></td>", - "<td class='column-entry-red'><pre> 80.00% (48/89)</pre></td>", - "<td class='column-entry-green'><pre> 81.00% (1/1)</pre></td>", - "<td class='column-entry-yellow'><pre> 90.00% (6/7)</pre></td>", - "<td class='column-entry-gray'><pre>- (0/0)</pre></td>", - ); - let out = recolor_coverage_table(input); - assert!(out.contains("class='column-entry-red'><pre> 50.00%")); - assert!(out.contains("class='column-entry-yellow'><pre> 51.23%")); - assert!(out.contains("class='column-entry-yellow'><pre> 80.00%")); - assert!(out.contains("class='column-entry-green'><pre> 81.00%")); - assert!(out.contains("class='column-entry-green'><pre> 90.00%")); - assert!(out.contains("class='column-entry-gray'><pre>- (0/0)")); - } -} diff --git a/.run/src/bin/deploy-api-docs.rs b/.run/src/bin/deploy-api-docs.rs deleted file mode 100644 index 961eb04..0000000 --- a/.run/src/bin/deploy-api-docs.rs +++ /dev/null @@ -1,118 +0,0 @@ -use std::path::Path; - -use arg_picker::{Picker, macros::arg}; -use tools::{println_cargo_style, run_cmd}; - -const OUTPUT_DIR: &str = "docs/api-docs"; - -fn main() { - let using_docsrs = Picker::from_args() - .pick_or_default(&arg![docsrs: bool]) - .unwrap(); - - let repo_root = find_git_repo().expect("Failed to find git repository root"); - - // Read features from [package.metadata.docs.rs] - let features = tools::read_features().unwrap_or_else(|e| { - eprintln!("Error: {}", e); - std::process::exit(1); - }); - - let features_arg = features.join(","); - - // Ensure output directory exists - let output_path = repo_root.join(OUTPUT_DIR); - std::fs::create_dir_all(&output_path).expect("Failed to create output directory"); - - // Build cargo doc command - let cmd = if using_docsrs { - format!( - "cargo +nightly rustdoc --features \"{}\" -p mingling --target-dir \"{}\" --color always -- --cfg docsrs", - features_arg, - output_path.join("target").to_string_lossy() - ) - } else { - format!( - "cargo doc --no-deps --features \"{}\" -p mingling --target-dir \"{}\" --color always", - features_arg, - output_path.join("target").to_string_lossy() - ) - }; - - println_cargo_style!("Features: {}", features_arg); - println_cargo_style!("Output: {}", output_path.display()); - - // Run cargo doc, then copy generated docs to output directory - println_cargo_style!("Building: docs (cargo doc --no-deps)"); - run_cmd!(&cmd).unwrap_or_else(|code| { - eprintln!("Error: cargo doc failed with exit code {}", code); - std::process::exit(code); - }); - - // Copy generated docs from target/doc to OUTPUT_DIR (top level) - let doc_source = output_path.join("target").join("doc"); - let doc_dest = &output_path; - - if doc_source.exists() { - println_cargo_style!("Copying: docs to output directory"); - // Remove old docs in destination (except target/) - if let Ok(entries) = std::fs::read_dir(doc_dest) { - for entry in entries.flatten() { - let path = entry.path(); - if path.file_name().and_then(|n| n.to_str()) == Some("target") { - continue; - } - if path.is_dir() { - std::fs::remove_dir_all(&path).ok(); - } else { - std::fs::remove_file(&path).ok(); - } - } - } - copy_dir_recursively(&doc_source, doc_dest).expect("Failed to copy documentation"); - } - - // Clean up the intermediate target directory to save space - std::fs::remove_dir_all(output_path.join("target")).ok(); - - println_cargo_style!("Done: API docs deployed to {}", output_path.display()); -} - -fn copy_dir_recursively(src: &Path, dst: &Path) -> std::io::Result<()> { - if !dst.exists() { - std::fs::create_dir_all(dst)?; - } - - for entry in std::fs::read_dir(src)? { - let entry = entry?; - let file_type = entry.file_type()?; - let src_path = entry.path(); - let file_name = src_path.file_name().expect("Failed to get file name"); - let dst_path = dst.join(file_name); - - if file_type.is_dir() { - copy_dir_recursively(&src_path, &dst_path)?; - } else { - std::fs::copy(&src_path, &dst_path)?; - } - } - - Ok(()) -} - -fn find_git_repo() -> Option<std::path::PathBuf> { - 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/display-dependency-order.rs b/.run/src/bin/display-dependency-order.rs deleted file mode 100644 index a31c67a..0000000 --- a/.run/src/bin/display-dependency-order.rs +++ /dev/null @@ -1,12 +0,0 @@ -use tools::{dependency_order::display_dependency_order, eprintln_cargo_style}; - -fn main() { - let order = display_dependency_order(); - if order.is_empty() { - eprintln_cargo_style!("could not find workspace root or mingling crates"); - std::process::exit(1); - } - for path in order { - println!("{}", path.display()); - } -} diff --git a/.run/src/bin/http-page-preview.ps1 b/.run/src/bin/http-page-preview.ps1 deleted file mode 100644 index 8cc3579..0000000 --- a/.run/src/bin/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/.run/src/bin/http-page-preview.sh b/.run/src/bin/http-page-preview.sh deleted file mode 100755 index bed4b1c..0000000 --- a/.run/src/bin/http-page-preview.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -python3 -m http.server 3000 diff --git a/.run/src/bin/install-mling.ps1 b/.run/src/bin/install-mling.ps1 deleted file mode 100644 index 2b55a09..0000000 --- a/.run/src/bin/install-mling.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$ErrorActionPreference = "Stop" - -cargo build --release --manifest-path mingling_cli/Cargo.toml - -New-Item -ItemType Directory -Force -Path .temp/mling/bin, .temp/mling/scripts | Out-Null - -Copy-Item .temp/target/release/mling.exe .temp/mling/bin/ -Copy-Item .temp/target/release/mingling-cli.exe .temp/mling/bin/ -Copy-Item .temp/target/mingling/mling_comp.ps1 .temp/mling/scripts/mling_comp.ps1 -Copy-Item mingling_cli/scripts/load_mling.ps1 .temp/mling/ diff --git a/.run/src/bin/install-mling.sh b/.run/src/bin/install-mling.sh deleted file mode 100755 index e8cfa18..0000000 --- a/.run/src/bin/install-mling.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -e - -cargo build --release --manifest-path mingling_cli/Cargo.toml - -mkdir -p .temp/mling/bin .temp/mling/scripts - -cp .temp/target/release/mling .temp/mling/bin/ -cp .temp/target/release/mingling-cli .temp/mling/bin/ - -for comp in zsh sh fish; do - cp ".temp/target/mingling/mling_comp.$comp" ".temp/mling/scripts/mling_comp.$comp" -done -cp mingling_cli/scripts/load_mling.zsh .temp/mling/ -cp mingling_cli/scripts/load_mling.sh .temp/mling/ -cp mingling_cli/scripts/load_mling.fish .temp/mling/ diff --git a/.run/src/bin/package-all.rs b/.run/src/bin/package-all.rs deleted file mode 100644 index ecdd133..0000000 --- a/.run/src/bin/package-all.rs +++ /dev/null @@ -1,736 +0,0 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; - -use flate2::read::GzDecoder; -use serde::Deserialize; -use tar::Archive; -use toml::Table as TomlTable; -use tools::{ - dependency_order::find_workspace_root, eprintln_cargo_style, println_cargo_style, - run_cmd_capture_with_dir, wprintln_cargo_style, -}; - -/// A single member from `cargo metadata` output. -#[derive(Deserialize, Debug)] -struct MetadataPackage { - name: String, - version: String, - manifest_path: String, -} - -/// The top-level metadata structure. -#[derive(Deserialize, Debug)] -struct Metadata { - #[allow(dead_code)] - workspace_root: String, - packages: Vec<MetadataPackage>, -} - -fn main() { - // 1. Determine project root - let cwd = std::env::current_dir().expect("failed to get current working directory"); - let workspace_root = find_workspace_root(&cwd).expect("not inside a Cargo workspace"); - println_cargo_style!("Workspace: {}", workspace_root.display()); - - let pre_release_dir = workspace_root.join(".temp/pre-release"); - - // 2. Clean `.temp/pre-release/` - println_cargo_style!("Clean: .temp/pre-release/"); - let _ = std::fs::remove_dir_all(&pre_release_dir); - std::fs::create_dir_all(&pre_release_dir).expect("failed to create .temp/pre-release/"); - - // 3. Run `cargo metadata` to get workspace members info - println_cargo_style!("Metadata: querying workspace members"); - let metadata_json = run_cmd_capture_with_dir( - "cargo metadata --format-version 1 --no-deps", - &workspace_root, - ) - .unwrap_or_else(|(code, _msg)| { - eprintln_cargo_style!(format!("cargo metadata failed (exit {code}):\n{{msg}}")); - std::process::exit(1); - }); - - let metadata: Metadata = serde_json::from_str(&metadata_json).unwrap_or_else(|e| { - eprintln_cargo_style!("failed to parse cargo metadata: {}", e); - std::process::exit(1); - }); - - // Filter workspace members: skip the root virtual manifest - let workspace_root_str = workspace_root.to_string_lossy().replace('\\', "/"); - let members: Vec<&MetadataPackage> = metadata - .packages - .iter() - .filter(|p| { - let mp = p.manifest_path.replace('\\', "/"); - mp.starts_with(&workspace_root_str) - && mp != format!("{}/Cargo.toml", workspace_root_str) - }) - .collect(); - - if members.is_empty() { - eprintln_cargo_style!("No workspace members found!"); - std::process::exit(1); - } - - // Print member info - for m in &members { - println_cargo_style!("Member: {}@{}", m.name, m.version); - } - - // Build version map: crate_name -> version - let mut version_map: HashMap<String, String> = HashMap::new(); - for m in &members { - version_map.insert(m.name.clone(), m.version.clone()); - } - - // Collect unique member directories that need to be copied - let mut member_dirs: Vec<PathBuf> = Vec::new(); - for m in &members { - let dir = Path::new(&m.manifest_path) - .parent() - .expect("manifest_path has no parent"); - let canonical_dir = std::fs::canonicalize(dir).unwrap_or_else(|_| dir.to_path_buf()); - let canonical_root = - std::fs::canonicalize(&workspace_root).unwrap_or_else(|_| workspace_root.to_path_buf()); - let relative = canonical_dir - .strip_prefix(&canonical_root) - .map(|p| p.to_path_buf()) - .unwrap_or_else(|_| PathBuf::from(dir.file_name().unwrap_or_default())); - if !member_dirs.contains(&relative) { - member_dirs.push(relative); - } - } - - // 4. Copy files to the temp directory, preserving the workspace directory structure - println_cargo_style!("Copy: project structure to .temp/pre-release/"); - - copy_dir( - &workspace_root.join(".cargo"), - &pre_release_dir.join(".cargo"), - ); - - for dir in &member_dirs { - let src = workspace_root.join(dir); - let dst = pre_release_dir.join(dir); - copy_dir(&src, &dst); - } - - copy_file( - &workspace_root.join("Cargo.toml"), - &pre_release_dir.join("Cargo.toml"), - ); - copy_file( - &workspace_root.join("Cargo.lock"), - &pre_release_dir.join("Cargo.lock"), - ); - - // 5. Fully resolve ALL workspace inheritance in every member's Cargo.toml, - // so each crate becomes monomorphic (no `workspace = true` references). - // Then strip `[workspace.dependencies]` and `[workspace.package]` from the - // root Cargo.toml, since they are no longer needed. - println_cargo_style!("Resolve: inline all workspace inheritance"); - - // Parse workspace config from the COPIED root Cargo.toml - let root_cargo_path = pre_release_dir.join("Cargo.toml"); - let root_content = std::fs::read_to_string(&root_cargo_path) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", root_cargo_path.display())); - - let (ws_package, ws_deps) = parse_workspace_config(&root_content); - - // Resolve each member's Cargo.toml - for dir in &member_dirs { - let member_cargo = pre_release_dir.join(dir).join("Cargo.toml"); - if !member_cargo.exists() { - continue; - } - let member_content = std::fs::read_to_string(&member_cargo) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", member_cargo.display())); - let resolved = - resolve_member_manifest(&member_content, dir, &ws_package, &ws_deps, &version_map); - std::fs::write(&member_cargo, &resolved) - .unwrap_or_else(|e| panic!("failed to write {}: {e}", member_cargo.display())); - } - - // Strip [workspace.dependencies], [workspace.package], and root [package] - // from root Cargo.toml, making it a pure virtual manifest. - let stripped = strip_workspace_config(&root_content); - std::fs::write(&root_cargo_path, &stripped) - .unwrap_or_else(|e| panic!("failed to write {}: {e}", root_cargo_path.display())); - - println_cargo_style!("Package: running cargo package --workspace --no-verify"); - - // 6. Run cargo package in the temp directory - let package_ok = run_cmd_capture_with_dir( - "cargo package --workspace --no-verify --color always", - &pre_release_dir, - ); - - match &package_ok { - Ok(out) => { - println!("{out}"); - } - Err((code, msg)) => { - // Print output but don't fail yet - eprintln_cargo_style!(format!("cargo package exited with code {code}:")); - println!("{msg}"); - } - } - - // 7. Copy built packages back to .temp/target/package - let temp_package_dir = workspace_root.join(".temp/target/package"); - std::fs::create_dir_all(&temp_package_dir) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", temp_package_dir.display())); - - // cargo package puts .crate files in target/package - let pre_release_target_package = pre_release_dir.join(".temp/target/package"); - if pre_release_target_package.exists() { - println_cargo_style!("Copy: packages to .temp/target/package"); - copy_dir_contents(&pre_release_target_package, &temp_package_dir); - } else { - wprintln_cargo_style!("No packages found in .temp/pre-release/.temp/target/package"); - } - - // 8. Export each crate as a standalone project from the .crate packages. - // The .crate files contain the final publish-ready Cargo.toml with all - // workspace/path deps already resolved by `cargo package`. - let release_dir = workspace_root.join(".temp/release"); - println_cargo_style!("Export: standalone crates to .temp/release/"); - let _ = std::fs::remove_dir_all(&release_dir); - std::fs::create_dir_all(&release_dir) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", release_dir.display())); - - for entry in std::fs::read_dir(&temp_package_dir).expect("failed to read target/package") { - let entry = entry.expect("failed to read entry"); - let path = entry.path(); - if path.extension().is_none_or(|e| e != "crate") { - continue; - } - - // .crate files are gzipped tarballs. Extract to .temp/release/<name>/ - // fname is like "mingling-0.3.0" - let fname = path.file_stem().unwrap().to_string_lossy().to_string(); - - // Derive crate directory name by stripping the version suffix - // mingling-0.3.0 -> mingling, arg-picker-0.1.0 -> arg-picker - let crate_dir_name = fname - .rfind('-') - .and_then(|dash| { - // Check if what follows looks like a semver - let ver_part = &fname[dash + 1..]; - if ver_part.chars().next().is_some_and(|c| c.is_ascii_digit()) { - Some(&fname[..dash]) - } else { - None - } - }) - .unwrap_or(&fname) - .to_string(); - - let target_dir = release_dir.join(&crate_dir_name); - std::fs::create_dir_all(&target_dir) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", target_dir.display())); - - // Extract using flate2 + tar (cross-platform) - let file = match std::fs::File::open(&path) { - Ok(f) => f, - Err(e) => { - eprintln_cargo_style!("Failed to open {}: {e}", path.display()); - continue; - } - }; - let decoder = GzDecoder::new(file); - let mut archive = Archive::new(decoder); - if let Err(e) = archive.unpack(&target_dir) { - eprintln_cargo_style!("Failed to extract {}: {e}", fname); - continue; - } - - // Move the contents from the inner dir up one level - // .crate contains a single top-level dir named after the package - let inner = target_dir.join(&fname); - if inner.exists() { - for inner_entry in std::fs::read_dir(&inner).expect("failed to read inner dir") { - let inner_entry = inner_entry.expect("failed to read entry"); - let inner_path = inner_entry.path(); - let dest = target_dir.join(inner_path.file_name().unwrap()); - if dest.exists() { - let _ = std::fs::remove_dir_all(&dest); - } - std::fs::rename(&inner_path, &dest).unwrap_or_else(|e| { - panic!( - "failed to rename {} -> {}: {e}", - inner_path.display(), - dest.display() - ) - }); - } - let _ = std::fs::remove_dir_all(&inner); - } - - // Clean up: remove .orig file (only the normalized Cargo.toml is needed) - let _ = std::fs::remove_file(target_dir.join("Cargo.toml.orig")); - // Also remove Cargo.lock — standalone crate doesn't need it for publish - let _ = std::fs::remove_file(target_dir.join("Cargo.lock")); - - // Append an empty [workspace] section so each crate is a valid workspace root - let cargo_toml_path = target_dir.join("Cargo.toml"); - if cargo_toml_path.exists() { - let mut cargo_content = std::fs::read_to_string(&cargo_toml_path) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", cargo_toml_path.display())); - // Only append if there isn't already a [workspace] section - if !cargo_content.contains("\n[workspace]\n") - && !cargo_content.ends_with("\n[workspace]\n") - { - cargo_content.push_str("\n[workspace]\n"); - std::fs::write(&cargo_toml_path, &cargo_content).unwrap_or_else(|e| { - panic!("failed to write {}: {e}", cargo_toml_path.display()) - }); - } - } - - println_cargo_style!("Export: {}", crate_dir_name); - } - - println_cargo_style!("Done: .temp/release/ is ready"); - - // If package failed, report it - if package_ok.is_err() { - eprintln_cargo_style!("cargo package reported errors above"); - std::process::exit(1); - } -} - -/// Parse `[workspace.package]` and `[workspace.dependencies]` from the root Cargo.toml. -/// Returns (package_fields, dep_values). -fn parse_workspace_config( - content: &str, -) -> (HashMap<String, String>, HashMap<String, toml::Value>) { - let table: TomlTable = content.parse().expect("failed to parse root Cargo.toml"); - - let mut package_fields = HashMap::new(); - if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table()) - && let Some(pkg_table) = workspace.get("package").and_then(|p| p.as_table()) - { - for (k, v) in pkg_table { - if let Some(s) = v.as_str() { - package_fields.insert(k.clone(), s.to_string()); - } - } - } - - let mut dep_values = HashMap::new(); - if let Some(workspace) = table.get("workspace").and_then(|w| w.as_table()) - && let Some(deps_table) = workspace.get("dependencies").and_then(|d| d.as_table()) - { - for (k, v) in deps_table { - dep_values.insert(k.clone(), v.clone()); - } - } - - (package_fields, dep_values) -} - -/// Serialize a `toml::Value` into Cargo-toml-compatible inline representation. -fn toml_value_str(v: &toml::Value) -> String { - match v { - toml::Value::String(s) => format!("\"{}\"", s), - toml::Value::Table(t) => { - let items: Vec<String> = t - .iter() - .map(|(k, val)| format!("{} = {}", k, toml_value_str(val))) - .collect(); - format!("{{ {} }}", items.join(", ")) - } - toml::Value::Array(a) => { - let items: Vec<String> = a.iter().map(toml_value_str).collect(); - format!("[{}]", items.join(", ")) - } - toml::Value::Boolean(b) => b.to_string(), - toml::Value::Integer(i) => i.to_string(), - toml::Value::Float(f) => f.to_string(), - toml::Value::Datetime(dt) => format!("\"{}\"", dt), - } -} - -/// Compute the relative path from `member_rel_dir` to `target_path`. -/// Both are relative to workspace root. -/// e.g. member_rel_dir="mingling", target_path="mingling_core" → "../mingling_core" -fn make_path_relative_to_member(target_path: &str, member_rel_dir: &Path) -> String { - if member_rel_dir.as_os_str().is_empty() || member_rel_dir == Path::new(".") { - return target_path.to_string(); - } - let depth = member_rel_dir.components().count(); - let mut result = PathBuf::new(); - for _ in 0..depth { - result.push(".."); - } - result.push(target_path); - result.to_string_lossy().to_string() -} - -/// Resolve a dependency definition from `[workspace.dependencies]` to an inline string. -/// If the definition contains a path to a workspace member, add `version = "..."` -/// and adjust the path to be relative to the member's directory. -fn resolve_dep_def( - dep_name: &str, - dep_def: &toml::Value, - member_rel_dir: &Path, - version_map: &HashMap<String, String>, -) -> String { - match dep_def { - toml::Value::String(ver) => { - format!("\"{}\"", ver) - } - toml::Value::Table(t) => { - let mut resolved = t.clone(); - let has_path = t.contains_key("path"); - let is_ws_member = version_map.contains_key(dep_name); - - // Fix path to be relative to member's directory - if has_path && let Some(path_val) = t.get("path").and_then(|v| v.as_str()) { - let rel = make_path_relative_to_member(path_val, member_rel_dir); - resolved.insert("path".to_string(), toml::Value::String(rel)); - } - - // Add version for workspace member path deps - if has_path - && is_ws_member - && let Some(version) = version_map.get(dep_name) - { - resolved.insert("version".to_string(), toml::Value::String(version.clone())); - } - - let items: Vec<String> = resolved - .iter() - .map(|(k, val)| format!("{} = {}", k, toml_value_str(val))) - .collect(); - format!("{{ {} }}", items.join(", ")) - } - _ => toml_value_str(dep_def), - } -} - -/// Merge an inline `{ workspace = true, optional = true, ... }` with the workspace definition. -/// Returns the full resolved dependency value string (without the leading `dep_name = `). -fn merge_inline_dep( - inline_rest: &str, - dep_name: &str, - dep_def: &toml::Value, - member_rel_dir: &Path, - version_map: &HashMap<String, String>, -) -> String { - // inline_rest is the part after `=`: `{ workspace = true, optional = true }` - let inner = inline_rest - .trim() - .strip_prefix('{') - .and_then(|s| s.strip_suffix('}')) - .unwrap_or(""); - - match dep_def { - toml::Value::String(ver) => { - // Workspace def is just a version string - // Collect extras: everything except `workspace = true` - let extras: Vec<&str> = inner - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty() && *s != "workspace = true") - .collect(); - - if extras.is_empty() { - format!("\"{}\"", ver) - } else { - // Serialize as inline table: version + extras - let mut parts = vec![format!("version = \"{}\"", ver)]; - parts.extend(extras.iter().map(|s| s.to_string())); - format!("{{ {} }}", parts.join(", ")) - } - } - toml::Value::Table(t) => { - // Start from workspace def - let mut merged = t.clone(); - - // Fix path to be relative to member's directory - if let Some(path_val) = t.get("path").and_then(|v| v.as_str()) { - let rel = make_path_relative_to_member(path_val, member_rel_dir); - merged.insert("path".to_string(), toml::Value::String(rel)); - } - - // If this dep is a workspace member with a path dep, add version - if t.contains_key("path") - && version_map.contains_key(dep_name) - && let Some(version) = version_map.get(dep_name) - { - merged.insert("version".to_string(), toml::Value::String(version.clone())); - } - - // Apply extra fields from the inline - for piece in inner.split(',').map(|s| s.trim()) { - let piece = piece.trim(); - if piece.is_empty() || piece == "workspace = true" { - continue; - } - // Parse `key = value` pairs - if let Some((raw_key, raw_val)) = piece.split_once('=') { - let k = raw_key.trim(); - let v = raw_val.trim(); - if k.is_empty() { - continue; - } - // Try to infer the value type - if v == "true" { - merged.insert(k.to_string(), toml::Value::Boolean(true)); - } else if v == "false" { - merged.insert(k.to_string(), toml::Value::Boolean(false)); - } else if v.starts_with('"') && v.ends_with('"') { - merged.insert( - k.to_string(), - toml::Value::String(v[1..v.len() - 1].to_string()), - ); - } else if v.starts_with('[') && v.ends_with(']') { - // Simple array parsing: strings only - let arr: Vec<toml::Value> = v[1..v.len() - 1] - .split(',') - .map(|s| { - let s = s.trim().trim_matches('"'); - toml::Value::String(s.to_string()) - }) - .collect(); - merged.insert(k.to_string(), toml::Value::Array(arr)); - } else if let Ok(n) = v.parse::<i64>() { - merged.insert(k.to_string(), toml::Value::Integer(n)); - } else if let Ok(f) = v.parse::<f64>() { - merged.insert(k.to_string(), toml::Value::Float(f)); - } else { - // Treat as string - merged.insert(k.to_string(), toml::Value::String(v.to_string())); - } - } - } - - let items: Vec<String> = merged - .iter() - .map(|(k, val)| format!("{} = {}", k, toml_value_str(val))) - .collect(); - format!("{{ {} }}", items.join(", ")) - } - _ => toml_value_str(dep_def), - } -} - -/// Resolve ALL workspace inheritance in a single member crate's Cargo.toml: -/// - `version.workspace = true` → `version = "0.3.0"` -/// - `dep.workspace = true` → inline the full definition from ws_deps -/// - `dep = { workspace = true, ... }` → merge with ws_deps definition -fn resolve_member_manifest( - content: &str, - member_rel_dir: &Path, - ws_package: &HashMap<String, String>, - ws_deps: &HashMap<String, toml::Value>, - version_map: &HashMap<String, String>, -) -> String { - let mut result = String::new(); - let mut in_package = false; - let mut in_section_with_deps = false; - - for line in content.lines() { - let trimmed = line.trim(); - let indent: String = line.chars().take_while(|c| c.is_whitespace()).collect(); - - // Track sections - if trimmed.starts_with('[') { - in_package = trimmed == "[package]"; - in_section_with_deps = trimmed.starts_with("[dependencies") - || trimmed.starts_with("[build-dependencies") - || trimmed.starts_with("[dev-dependencies"); - result.push_str(line); - result.push('\n'); - continue; - } - - // [package] section: resolve `field.workspace = true` - if in_package && trimmed.ends_with(".workspace = true") { - let key = trimmed.strip_suffix(".workspace = true").unwrap().trim(); - if let Some(value) = ws_package.get(key) { - result.push_str(&format!("{indent}{key} = \"{value}\"\n")); - continue; - } - // Also check workspace.dependencies (for fields like `version.workspace`) - // when the member has its own version field inherited from workspace.package - } - - // Dependency sections - if in_section_with_deps { - // Shorthand: `foo.workspace = true` - if trimmed.ends_with(".workspace = true") { - let key = trimmed.strip_suffix(".workspace = true").unwrap().trim(); - if let Some(dep_def) = ws_deps.get(key) { - let resolved = resolve_dep_def(key, dep_def, member_rel_dir, version_map); - result.push_str(&format!("{indent}{key} = {resolved}\n")); - continue; - } - } - - // Inline: `foo = { workspace = true, optional = true, ... }` - if let Some(eq_pos) = trimmed.find("= {") - && trimmed.contains("workspace = true") - { - let dep_name = trimmed[..eq_pos].trim(); - if let Some(dep_def) = ws_deps.get(dep_name) { - let after_eq = trimmed[eq_pos + 1..].trim(); - let merged = - merge_inline_dep(after_eq, dep_name, dep_def, member_rel_dir, version_map); - result.push_str(&format!("{indent}{dep_name} = {merged}\n")); - continue; - } - } - } - - result.push_str(line); - result.push('\n'); - } - - result -} - -/// Remove `[workspace.dependencies]`, `[workspace.package]`, and the root `[package]` -/// section from root Cargo.toml, making it a pure virtual manifest. -/// Keeps `[workspace]` with `members`, `resolver`, `exclude` so packaging still works. -fn strip_workspace_config(content: &str) -> String { - let mut result = String::new(); - let mut in_ws_deps = false; - let mut in_ws_package = false; - let mut in_root_package = false; - - for line in content.lines() { - let trimmed = line.trim(); - - if trimmed == "[workspace.dependencies]" { - in_ws_deps = true; - continue; - } - if trimmed == "[workspace.package]" { - in_ws_package = true; - continue; - } - if trimmed == "[package]" && !in_ws_deps && !in_ws_package { - // Remove the root [package] section entirely (virtual manifest) - in_root_package = true; - continue; - } - - if in_ws_deps { - if trimmed.starts_with('[') { - in_ws_deps = false; - } else { - continue; - } - } - - if in_ws_package { - if trimmed.starts_with('[') { - in_ws_package = false; - } else { - continue; - } - } - - if in_root_package { - if trimmed.starts_with('[') { - in_root_package = false; - } else { - continue; - } - } - - result.push_str(line); - result.push('\n'); - } - - result -} - -/// Recursively copy a directory. -fn copy_dir(src: &Path, dst: &Path) { - copy_dir_filtered(src, dst, &|_: &Path| true) -} - -/// Recursively copy a directory with a filter function. -/// The filter receives the source path and returns `true` if the entry should be copied. -fn copy_dir_filtered(src: &Path, dst: &Path, filter: &dyn Fn(&Path) -> bool) { - if !src.exists() { - return; - } - if !filter(src) { - return; - } - std::fs::create_dir_all(dst) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", dst.display())); - - for entry in std::fs::read_dir(src).expect("failed to read directory") { - let entry = entry.expect("failed to read entry"); - let entry_type = entry.file_type().expect("failed to get file type"); - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - - if !filter(&src_path) { - continue; - } - - if entry_type.is_dir() { - copy_dir_filtered(&src_path, &dst_path, filter); - } else if entry_type.is_file() || entry_type.is_symlink() { - copy_file(&src_path, &dst_path); - } - } -} - -/// Copy a file, creating parent directories as needed. -/// If src is a symlink, copies the target content (follow symlinks). -fn copy_file(src: &Path, dst: &Path) { - if let Some(parent) = dst.parent() { - std::fs::create_dir_all(parent) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", parent.display())); - } - - let resolved = if src.is_symlink() { - let target = std::fs::read_link(src) - .unwrap_or_else(|e| panic!("failed to read symlink {}: {e}", src.display())); - if target.is_relative() { - src.parent().unwrap().join(target) - } else { - target - } - } else { - src.to_path_buf() - }; - - std::fs::copy(&resolved, dst).unwrap_or_else(|e| { - panic!( - "failed to copy {} -> {}: {e}", - resolved.display(), - dst.display() - ) - }); -} - -/// Copy all files/directories from one directory into another. -fn copy_dir_contents(src: &Path, dst: &Path) { - if !src.exists() { - return; - } - std::fs::create_dir_all(dst) - .unwrap_or_else(|e| panic!("failed to create {}: {e}", dst.display())); - - for entry in std::fs::read_dir(src).expect("failed to read directory") { - let entry = entry.expect("failed to read entry"); - let entry_type = entry.file_type().expect("failed to get file type"); - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - - if entry_type.is_dir() { - copy_dir(&src_path, &dst_path); - } else if entry_type.is_file() || entry_type.is_symlink() { - copy_file(&src_path, &dst_path); - } - } -} diff --git a/.run/src/bin/update-version.rs b/.run/src/bin/update-version.rs deleted file mode 100644 index 2283662..0000000 --- a/.run/src/bin/update-version.rs +++ /dev/null @@ -1,104 +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<VersionFile>, -} - -fn main() { - let args: Vec<String> = 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(".config").join("version-files.toml"); - let config_str = - std::fs::read_to_string(&config_path).expect("Failed to read .config/version-files.toml"); - let config: Config = - toml::from_str(&config_str).expect("Failed to parse .config/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 deleted file mode 100644 index ff53202..0000000 --- a/.run/src/bin/windows-folder-hide.ps1 +++ /dev/null @@ -1,115 +0,0 @@ -$skipDirs = @('.git', '.temp', 'target', 'node_modules', '.pnpm') -$selfPath = (Get-Item -LiteralPath $MyInvocation.MyCommand.Path).Directory.FullName - -function Test-InSkipDir { - param( - [object]$Item - ) - $path = if ($Item -is [string]) { - $Item - } elseif ($Item.PSPath) { - $Item.PSPath -replace '^.*::', '' - } else { - $Item.FullName - } - - $parts = $path.Split([System.IO.Path]::DirectorySeparatorChar) - for ($i = 0; $i -lt $parts.Length - 1; $i++) { - if ($parts[$i] -in $skipDirs) { - return $true - } - } - return $false -} - -function Invoke-UnhideRecursive { - param([string]$Path) - Get-ChildItem -LiteralPath $Path -Force | ForEach-Object { - if ($_.PSIsContainer) { - if ($_.Name -in $skipDirs) { - if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) { - Write-Host " -> unhiding skip directory (self only): `"$($_.FullName)`"" - $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden - } - return - } - Invoke-UnhideRecursive $_.FullName - } else { - if ($_.Attributes -band [System.IO.FileAttributes]::Hidden) { - Write-Host " -> unhiding: `"$($_.FullName)`"" - $_.Attributes = $_.Attributes -bxor [System.IO.FileAttributes]::Hidden - } - } - } -} - -function Test-GitPathSkippable { - param([string]$GitPath) - $parts = $GitPath.Split(@('/', '\')) - for ($i = 0; $i -lt $parts.Length - 1; $i++) { - if ($parts[$i] -in $skipDirs) { - return $true - } - } - return $false -} - -Write-Host "Step 1: Unhiding all files and directories (skipping $($skipDirs -join ', '))..." - -Invoke-UnhideRecursive -Path (Get-Location).Path - -Write-Host "Step 2: Hiding git-ignored items..." - -git ls-files --others --ignored --exclude-standard | Where-Object { - -not (Test-GitPathSkippable $_) -} | ForEach-Object { - $itemPath = $_ - Write-Host "... checking: `"$itemPath`"" - $item = Get-Item $_ -Force -ErrorAction SilentlyContinue - if (-not $item) { return } - - if ($item.FullName -eq $selfPath) { return } - - if (Test-InSkipDir $item) { - Write-Host " -> skipping (inside skip directory)" - return - } - - if ($item.PSIsContainer) { - if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding directory (non-recursive)" - $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden - } - } else { - if (-not ($item.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding" - $item.Attributes = $item.Attributes -bor [System.IO.FileAttributes]::Hidden - } - } -} - -Write-Host "Step 3: Hiding dot-prefixed items..." -Get-ChildItem -Path . -Force -Directory | Where-Object { $_.Name -match '^\.' } | ForEach-Object { - Write-Host "... checking: `"$($_.FullName)`"" - if (Test-InSkipDir $_) { - Write-Host " -> skipping (inside skip directory)" - return - } - if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding directory" - $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden - } -} - -Get-ChildItem -Path . -Force -File | Where-Object { $_.Name -match '^\.' } | ForEach-Object { - if ($_.FullName -eq $selfPath) { return } - Write-Host "... checking: `"$($_.FullName)`"" - if (Test-InSkipDir $_) { - Write-Host " -> skipping (inside skip directory)" - return - } - if (-not ($_.Attributes -band [System.IO.FileAttributes]::Hidden)) { - Write-Host " -> hiding file" - $_.Attributes = $_.Attributes -bor [System.IO.FileAttributes]::Hidden - } -} diff --git a/.run/src/dependency_order.rs b/.run/src/dependency_order.rs deleted file mode 100644 index 145bdbd..0000000 --- a/.run/src/dependency_order.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::path::{Path, PathBuf}; - -/// Parse Cargo.toml content and return dependency names that start with `prefix`. -fn parse_mingling_deps(content: &str, prefix: &str) -> Vec<String> { - let value: toml::Value = match content.parse() { - Ok(v) => v, - Err(_) => return Vec::new(), - }; - - let mut names = Vec::new(); - - // Check [dependencies] - if let Some(deps) = value.get("dependencies").and_then(|d| d.as_table()) { - for key in deps.keys() { - if key.starts_with(prefix) { - names.push(key.clone()); - } - } - } - - // Check [build-dependencies] - if let Some(deps) = value.get("build-dependencies").and_then(|d| d.as_table()) { - for key in deps.keys() { - if key.starts_with(prefix) { - names.push(key.clone()); - } - } - } - - names -} - -/// Read workspace members from the root Cargo.toml. -fn get_workspace_members(workspace_root: &std::path::Path) -> Vec<String> { - let cargo_path = workspace_root.join("Cargo.toml"); - let content = match std::fs::read_to_string(&cargo_path) { - Ok(c) => c, - Err(_) => return Vec::new(), - }; - - let value: toml::Value = match content.parse() { - Ok(v) => v, - Err(_) => return Vec::new(), - }; - - value - .get("workspace") - .and_then(|w| w.get("members")) - .and_then(|m| m.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default() -} - -/// Hierarchical topological sort (process layer by layer, sort siblings alphabetically). -/// -/// `dep_map` maps each crate to the list of crates it depends on. -/// Returns the dependency order (dependent crates come first, dependents come later), -/// with crates at the same layer (which can be built in parallel) sorted alphabetically. -fn topological_sort( - all_crates: &HashSet<String>, - dep_map: &HashMap<String, Vec<String>>, -) -> Vec<String> { - // in_degree[crate] = number of remaining mingling_* dependencies not yet processed - let mut in_degree: HashMap<&str, usize> = HashMap::new(); - // reverse[dependency] = list of crates that depend on it - let mut reverse: HashMap<&str, Vec<&str>> = HashMap::new(); - - for name in all_crates { - in_degree.entry(name.as_str()).or_insert(0); - reverse.entry(name.as_str()).or_default(); - } - - for (crate_name, deps) in dep_map { - for dep in deps { - if all_crates.contains(dep.as_str()) { - reverse - .get_mut(dep.as_str()) - .unwrap() - .push(crate_name.as_str()); - *in_degree.get_mut(crate_name.as_str()).unwrap() += 1; - } - } - } - - let mut result: Vec<String> = Vec::new(); - - // Process layer by layer: all crates with in_degree == 0 in one batch form a layer - loop { - let mut current: Vec<&str> = all_crates - .iter() - .filter(|n| in_degree.get(n.as_str()).copied().unwrap_or(0) == 0) - .filter(|n| !result.iter().any(|r| r.as_str() == n.as_str())) - .map(|s| s.as_str()) - .collect(); - - if current.is_empty() { - break; - } - - current.sort(); - result.extend(current.iter().map(|s| s.to_string())); - - for &node in ¤t { - if let Some(dependents) = reverse.get(node) { - for &dependent in dependents { - if let Some(degree) = in_degree.get_mut(dependent) { - *degree -= 1; - } - } - } - } - } - - result -} - -/// Strip the `\\?\` prefix that `std::fs::canonicalize` may add on Windows. -fn strip_verbatim_prefix(p: &Path) -> PathBuf { - let s = p.to_string_lossy(); - let s_ref: &str = &s; - if let Some(rest) = s_ref.strip_prefix("\\\\?\\") { - PathBuf::from(rest) - } else { - p.to_path_buf() - } -} - -/// Find the workspace root by looking for a Cargo.toml with `[workspace]` members. -/// Starts from `start` and walks up the directory tree. -pub fn find_workspace_root(start: &std::path::Path) -> Option<PathBuf> { - let mut current = Some(strip_verbatim_prefix( - &std::fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()), - )); - while let Some(dir) = current { - let members = get_workspace_members(&dir); - if !members.is_empty() { - return Some(dir); - } - current = dir.parent().map(|p| p.to_path_buf()); - } - None -} - -/// Output all crate paths in dependency order -#[allow(unused)] -pub fn display_dependency_order() -> Vec<PathBuf> { - let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); - - let workspace_root = match find_workspace_root(&cwd) { - Some(root) => root, - None => return Vec::new(), - }; - - // Read workspace members from root Cargo.toml - let members = get_workspace_members(&workspace_root); - - // Filter to crates starting with "mingling" or "arg" - let mingling_crates: HashSet<String> = members - .into_iter() - .filter(|m| m.starts_with("mingling") || m.starts_with("arg")) - .collect(); - - if mingling_crates.is_empty() { - return Vec::new(); - } - - // Build dependency graph - let mut dep_map: HashMap<String, Vec<String>> = HashMap::new(); - - for crate_name in &mingling_crates { - let cargo_path = workspace_root.join(crate_name).join("Cargo.toml"); - let content = match std::fs::read_to_string(&cargo_path) { - Ok(c) => c, - Err(_) => { - dep_map.insert(crate_name.clone(), Vec::new()); - continue; - } - }; - let deps = parse_mingling_deps(&content, "mingling"); - // Only keep deps that are actually in our set - let filtered: Vec<String> = deps - .into_iter() - .filter(|d| mingling_crates.contains(d.as_str())) - .collect(); - dep_map.insert(crate_name.clone(), filtered); - } - - let sorted = topological_sort(&mingling_crates, &dep_map); - - sorted.into_iter().map(PathBuf::from).collect() -} diff --git a/.run/src/lib.rs b/.run/src/lib.rs deleted file mode 100644 index b17a61f..0000000 --- a/.run/src/lib.rs +++ /dev/null @@ -1,459 +0,0 @@ -pub mod dependency_order; -pub mod verify; - -use colored::Colorize; - -use std::io::IsTerminal as _; - -#[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<String>) { - 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<String>) { - 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<String>) { - 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 in the current directory 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<String>) -> Result<(), i32> { - let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - run_cmd_with_dir(cmd.into(), &cwd) -} - -/// Run a shell command in the specified directory 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_with_dir(cmd: impl Into<String>, dir: &std::path::Path) -> 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(dir) - .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<String>) -> Result<String, (i32, String)> { - let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - run_cmd_capture_with_dir(cmd.into(), &cwd) -} - -/// Run a shell command in the specified directory 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_with_dir( - cmd: impl Into<String>, - dir: &std::path::Path, -) -> Result<String, (i32, String)> { - let shell = if cfg!(target_os = "windows") { - "powershell" - } else { - "sh" - }; - let output = std::process::Command::new(shell) - .arg("-c") - .arg(cmd.into()) - .current_dir(dir) - .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(); - // Keep both streams so a failure is never hidden: when stderr carries - // warnings, the real failure details (e.g. the failing test name and - // assertion diff) usually live on stdout and must not be dropped. - let combined = match (stdout.trim().is_empty(), stderr.trim().is_empty()) { - (false, false) => format!("{stdout}\n{stderr}"), - (false, true) => stdout, - (true, false) => stderr, - (true, true) => stdout, - }; - - 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<String> = tasks.iter().map(|(l, _, _)| l.clone()).collect(); - - let (tx, rx) = std::sync::mpsc::channel::<(usize, String, Result<String, (i32, String)>)>(); - - 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; - } - let msg = format!( - "{}: {} failed (exit code {})", - "error".bright_red().bold(), - labels[i], - code, - ); - let mut lines = Vec::new(); - if !output.is_empty() { - lines.extend(output.lines().map(|l| format!(" {l}"))); - } - if std::io::stdout().is_terminal() { - // On a TTY, render errors through the progress bar so they - // appear above it. - pb.println(&msg); - for line in &lines { - pb.println(line); - } - } else { - // On a non-TTY (CI, piped output), `ProgressBar::println` can - // be swallowed, hiding the failure. Emit to plain stdout so the - // failure is always visible. - println!("{msg}"); - for line in &lines { - println!("{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) - } - } -} - -/// Read `[package.metadata.docs.rs].features` from `mingling/Cargo.toml`. -/// -/// Finds the git repository root, reads `mingling/Cargo.toml`, parses it as TOML, -/// and extracts the feature list under `[package.metadata.docs.rs].features`. -/// -/// # Errors -/// -/// Returns `std::io::Error` if: -/// - The git repository root cannot be found. -/// - The manifest file cannot be read. -/// - The TOML cannot be parsed. -/// - The `[package.metadata.docs.rs].features` key is missing or empty. -pub fn read_features() -> Result<Vec<String>, std::io::Error> { - // Find git repo root - let mut current_dir = std::env::current_dir()?; - let repo_root = loop { - let git_dir = current_dir.join(".git"); - if git_dir.exists() && git_dir.is_dir() { - break Some(current_dir); - } - if !current_dir.pop() { - break None; - } - }; - let repo_root = repo_root.ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::NotFound, - "Failed to find git repository root", - ) - })?; - - let manifest_path = repo_root.join("mingling/Cargo.toml"); - if !manifest_path.exists() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Manifest not found at {}", manifest_path.display()), - )); - } - - let manifest_content = std::fs::read_to_string(&manifest_path)?; - let cargo_toml: toml::Value = manifest_content.parse().map_err(|e| { - std::io::Error::new( - std::io::ErrorKind::InvalidData, - format!("Failed to parse Cargo.toml: {}", e), - ) - })?; - - let doc_features = cargo_toml - .get("package") - .and_then(|p| p.get("metadata")) - .and_then(|m| m.get("docs")) - .and_then(|d| d.get("rs")) - .and_then(|rs| rs.get("features")) - .and_then(|f| f.as_array()) - .ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::NotFound, - "[package.metadata.docs.rs] or its 'features' key not found in mingling/Cargo.toml", - ) - })?; - - let features: Vec<String> = doc_features - .iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect(); - - if features.is_empty() { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "No features defined in [package.metadata.docs.rs]", - )); - } - - Ok(features) -} - -#[must_use] -pub fn cargo_tomls() -> Vec<std::path::PathBuf> { - 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 .run directory - if path.file_name().and_then(|n| n.to_str()) == Some(".run") { - 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 deleted file mode 100644 index b79bb73..0000000 --- a/.run/src/verify.rs +++ /dev/null @@ -1,506 +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<String>, - /// 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<CodeBlock> { - 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<CodeBlock> { - let line_num = start + 1; // 1-based line number - - let mut code_lines: Vec<String> = Vec::new(); - let mut features: Vec<String> = 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<String> = 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: mirror the declared features into [build-dependencies] - // so that build.rs can use the same feature set as the crate itself. - let build_deps_section = if block.is_build_time { - let feats_str: Vec<String> = block.features.iter().map(|f| format!("\"{f}\"")).collect(); - let build_feats = if feats_str.is_empty() { - String::new() - } else { - 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: 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.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<String> = 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()); -} |
