From 695f30d6782bce946f31ce9199ac7d725a527a57 Mon Sep 17 00:00:00 2001 From: Marvin Friedrich Date: Sun, 17 May 2026 23:23:13 +0200 Subject: [PATCH] first --- .dockerignore | 4 + .gitignore | 4 + Cargo.lock | 2900 +++++++++++++++++++++++++++++ Cargo.toml | 23 + Dockerfile | 58 + config.star | 35 + host-recipes/binutils/recipe.star | 43 + host-recipes/gcc/recipe.star | 49 + lib/common.star | 82 + recipes/linux/recipe.star | 38 + recipes/musl/recipe.star | 32 + src/apk.rs | 46 + src/build.rs | 921 +++++++++ src/cli.rs | 119 ++ src/config.rs | 37 + src/graph.rs | 135 ++ src/log.rs | 35 + src/main.rs | 21 + src/patches.rs | 19 + src/phase.rs | 214 +++ src/recipe.rs | 452 +++++ src/rewrite.rs | 39 + src/source.rs | 73 + src/starlark_eval.rs | 365 ++++ src/update.rs | 256 +++ 25 files changed, 6000 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 config.star create mode 100644 host-recipes/binutils/recipe.star create mode 100644 host-recipes/gcc/recipe.star create mode 100644 lib/common.star create mode 100644 recipes/linux/recipe.star create mode 100644 recipes/musl/recipe.star create mode 100644 src/apk.rs create mode 100644 src/build.rs create mode 100644 src/cli.rs create mode 100644 src/config.rs create mode 100644 src/graph.rs create mode 100644 src/log.rs create mode 100644 src/main.rs create mode 100644 src/patches.rs create mode 100644 src/phase.rs create mode 100644 src/recipe.rs create mode 100644 src/rewrite.rs create mode 100644 src/source.rs create mode 100644 src/starlark_eval.rs create mode 100644 src/update.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fa7510f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +target +build +.git +*.bak diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..51ebdec --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +/build +*.bak +*.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..62281f6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2900 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocative" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fac2ce611db8b8cee9b2aa886ca03c924e9da5e5295d0dbd0526e5d0b0710f7" +dependencies = [ + "allocative_derive", + "bumpalo", + "ctor", + "hashbrown 0.14.5", + "num-bigint", +] + +[[package]] +name = "allocative_derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe233a377643e0fc1a56421d7c90acdec45c291b30345eb9f08e8d0ddce5a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "beef" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmp_any" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "debugserver-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf6834a70ed14e8e4e41882df27190bea150f1f6ecf461f1033f8739cd8af4a" +dependencies = [ + "schemafy", + "serde", + "serde_json", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "display_container" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a110a75c96bedec8e65823dea00a1d710288b7a369d95fd8a0f5127639466fa" +dependencies = [ + "either", + "indenter", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "distro" +version = "0.1.0" +dependencies = [ + "allocative", + "anyhow", + "clap", + "hex", + "reqwest", + "serde", + "serde_json", + "sha2", + "shell-escape", + "starlark", + "starlark_derive", + "tempfile", + "walkdir", +] + +[[package]] +name = "dupe" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed2bc011db9c93fbc2b6cdb341a53737a55bafb46dbb74cf6764fc33a2fbf9c" +dependencies = [ + "dupe_derive", +] + +[[package]] +name = "dupe_derive" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e195b4945e88836d826124af44fdcb262ec01ef94d44f14f4fb5103f19892a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabffdaee24bd1bf95c5ef7cec31260444317e72ea56c4c91750e8b7ee58d5f1" +dependencies = [ + "log", +] + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c" +dependencies = [ + "serde", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[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-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[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.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lalrpop" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b" +dependencies = [ + "ascii-canvas", + "bit-set", + "diff", + "ena", + "is-terminal", + "itertools 0.10.5", + "lalrpop-util", + "petgraph", + "regex", + "regex-syntax 0.6.29", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "lalrpop-util" +version = "0.19.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed" +dependencies = [ + "regex", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "logos" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf8b031682c67a8e3d5446840f9573eb7fe26efe7ec8d195c9ac4c0647c502f1" +dependencies = [ + "logos-derive", +] + +[[package]] +name = "logos-derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d849148dbaf9661a6151d1ca82b13bb4c4c128146a88d05253b38d4e2f496c" +dependencies = [ + "beef", + "fnv", + "proc-macro2", + "quote", + "regex-syntax 0.6.29", + "syn 1.0.109", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[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 = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.10", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustyline" +version = "14.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7803e8936da37efd9b6d4478277f4b2b9bb5cdb37a113e8d63222e58da647e63" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "clipboard-win", + "fd-lock", + "home", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemafy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aea5ba40287dae331f2c48b64dbc8138541f5e97ee8793caa7948c1f31d86d5" +dependencies = [ + "Inflector", + "schemafy_core", + "schemafy_lib", + "serde", + "serde_derive", + "serde_json", + "serde_repr", + "syn 1.0.109", +] + +[[package]] +name = "schemafy_core" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41781ae092f4fd52c9287efb74456aea0d3b90032d2ecad272bd14dbbcb0511b" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "schemafy_lib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e953db32579999ca98c451d80801b6f6a7ecba6127196c5387ec0774c528befa" +dependencies = [ + "Inflector", + "proc-macro2", + "quote", + "schemafy_core", + "serde", + "serde_derive", + "serde_json", + "syn 1.0.109", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[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 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "starlark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f53849859f05d9db705b221bd92eede93877fd426c1b4a3c3061403a5912a8f" +dependencies = [ + "allocative", + "anyhow", + "bumpalo", + "cmp_any", + "debugserver-types", + "derivative", + "derive_more", + "display_container", + "dupe", + "either", + "erased-serde", + "hashbrown 0.14.5", + "inventory", + "itertools 0.13.0", + "maplit", + "memoffset", + "num-bigint", + "num-traits", + "once_cell", + "paste", + "ref-cast", + "regex", + "rustyline", + "serde", + "serde_json", + "starlark_derive", + "starlark_map", + "starlark_syntax", + "static_assertions", + "strsim 0.10.0", + "textwrap", + "thiserror 1.0.69", +] + +[[package]] +name = "starlark_derive" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe58bc6c8b7980a1fe4c9f8f48200c3212db42ebfe21ae6a0336385ab53f082a" +dependencies = [ + "dupe", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "starlark_map" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92659970f120df0cc1c0bb220b33587b7a9a90e80d4eecc5c5af5debb950173d" +dependencies = [ + "allocative", + "dupe", + "equivalent", + "fxhash", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "starlark_syntax" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe53b3690d776aafd7cb6b9fed62d94f83280e3b87d88e3719cc0024638461b3" +dependencies = [ + "allocative", + "annotate-snippets", + "anyhow", + "derivative", + "derive_more", + "dupe", + "lalrpop", + "lalrpop-util", + "logos", + "lsp-types", + "memchr", + "num-bigint", + "num-traits", + "once_cell", + "starlark_map", + "thiserror 1.0.69", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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 = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[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 = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7cdd860 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "distro" +version = "0.1.0" +edition = "2024" +license = "MIT" + +[dependencies] +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } +hex = "0.4" +reqwest = { version = "0.12", default-features = false, features = [ + "blocking", + "rustls-tls", +] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha2 = "0.10" +shell-escape = "0.1" +starlark = "0.13" +starlark_derive = "0.13" +allocative = "0.3" +tempfile = "3.10" +walkdir = "2.5" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e0eaf5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +FROM docker.io/library/alpine:edge + +RUN apk upgrade --no-cache && \ + apk add --no-cache \ + alpine-sdk \ + apk-tools \ + autoconf \ + automake \ + bash \ + bc \ + bison \ + bzip2 \ + ca-certificates \ + cmake \ + coreutils \ + curl \ + file \ + findutils \ + flex \ + gettext-dev \ + git \ + gzip \ + elfutils-dev \ + gmp-dev \ + mpfr-dev \ + mpc1-dev \ + libtool \ + linux-headers \ + meson \ + ninja \ + openssl \ + openssl-dev \ + patch \ + pkgconf \ + python3 \ + tar \ + texinfo \ + xz \ + zstd + +RUN rm -rf /tmp/mkpkg-root /tmp/distro-preflight.apk /tmp/APKINDEX.adb /tmp/distro-preflight.rsa && \ + openssl genrsa -out /tmp/distro-preflight.rsa 2048 >/dev/null 2>&1 && \ + openssl rsa -in /tmp/distro-preflight.rsa -pubout -out /etc/apk/keys/distro-preflight.rsa.pub >/dev/null 2>&1 && \ + mkdir -p /tmp/mkpkg-root/usr/share/distro && \ + printf ok > /tmp/mkpkg-root/usr/share/distro/preflight && \ + apk --sign-key /tmp/distro-preflight.rsa mkpkg \ + --files /tmp/mkpkg-root \ + --output /tmp/distro-preflight.apk \ + --info name:distro-preflight \ + --info version:0-r0 \ + --info arch:noarch \ + --info description:preflight \ + --info license:MIT >/dev/null && \ + apk --sign-key /tmp/distro-preflight.rsa mkndx -o /tmp/APKINDEX.adb /tmp/distro-preflight.apk >/dev/null && \ + test -s /tmp/distro-preflight.apk && \ + test -s /tmp/APKINDEX.adb + +WORKDIR /work diff --git a/config.star b/config.star new file mode 100644 index 0000000..40093b3 --- /dev/null +++ b/config.star @@ -0,0 +1,35 @@ +container_runtime = "podman" +container_image = "localhost/distro-builder:latest" +container_dockerfile = "Dockerfile" + +signing_key = "build/keys/distro.rsa" +signing_pubkey = "build/keys/distro.rsa.pub" + +target_arch = "x86_64" + +host_cflags = "-O2 -pipe" +host_cxxflags = "" +host_ldflags = "-Wl,-O1 -Wl,--sort-common -Wl,--as-needed" + +target_cflags = host_cflags +target_cxxflags = host_cxxflags +target_ldflags = host_ldflags + " -Wl,-z,now" + +if target_arch == "x86_64": + flags = " -march=x86-64-v3 -mtune=generic -fstack-clash-protection -fstack-protector-strong -fcf-protection" + target_cflags += flags + target_cxxflags += flags + target_ldflags += " -Wl,-z,pack-relative-relocs" + +options = { + "libc": "musl", + "target_triple": "x86_64-linux-musl", + "host_cflags": host_cflags, + "host_cxxflags": host_cxxflags, + "host_ldflags": host_ldflags, + "cflags": target_cflags, + "cxxflags": target_cxxflags, + "ldflags": target_ldflags, + "wayland": True, + "x11": True, +} diff --git a/host-recipes/binutils/recipe.star b/host-recipes/binutils/recipe.star new file mode 100644 index 0000000..2268936 --- /dev/null +++ b/host-recipes/binutils/recipe.star @@ -0,0 +1,43 @@ +name = "binutils" +version = "2.46.0" +revision = 1 +description = "GNU binutils cross-compiled for the target triple" +license = "GPL-3.0-or-later" + +source = { + "url": "https://ftp.gnu.org/gnu/binutils/binutils-" + version + ".tar.xz", + "sha256": "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2", + "strip_components": 1, +} + +host_deps = [] + +def configure(ctx): + triple = ctx.options["target_triple"] + ctx.run([ + ctx.source_dir + "/configure", + "--prefix=" + ctx.prefix, + "--target=" + triple, + "--with-sysroot=" + ctx.prefix + "/" + triple, + "--disable-nls", + "--disable-werror", + "--enable-deterministic-archives", + "--enable-ld=default", + "--enable-plugins", + "--enable-threads", + "--with-system-zlib", + # gprofng's libcollector does not build against musl/recent gcc. + "--disable-gprofng", + ], env = { + "CFLAGS": ctx.options["host_cflags"], + "CXXFLAGS": ctx.options["host_cxxflags"], + "LDFLAGS": ctx.options["host_ldflags"], + }) + +def build(ctx): + ctx.run(["make", "-j" + str(ctx.jobs)]) + +def install(ctx, pkg): + ctx.run(["make", "DESTDIR=" + pkg.destdir, "install"]) + # Drop static archives we don't need on the cross side. + ctx.run(["sh", "-c", "rm -f " + pkg.destdir + ctx.prefix + "/lib/*.a"]) diff --git a/host-recipes/gcc/recipe.star b/host-recipes/gcc/recipe.star new file mode 100644 index 0000000..36d2063 --- /dev/null +++ b/host-recipes/gcc/recipe.star @@ -0,0 +1,49 @@ +name = "gcc" +version = "16.1.0" +revision = 1 +description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)" +license = "GPL-3.0-or-later" + +source = { + "url": f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz", + "sha256": "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79", + "strip_components": 1, +} + +host_deps = ["binutils"] + +def configure(ctx): + triple = ctx.options["target_triple"] + ctx.run([ + ctx.source_dir + "/configure", + "--prefix=" + ctx.prefix, + "--target=" + triple, + "--with-sysroot=" + ctx.prefix + "/" + triple, + "--without-headers", + "--with-newlib", + "--enable-languages=c,c++", + "--enable-default-pie", + "--enable-default-ssp", + "--disable-nls", + "--disable-shared", + "--disable-threads", + "--disable-libssp", + "--disable-libgomp", + "--disable-libquadmath", + "--disable-libatomic", + "--disable-libvtv", + "--disable-multilib", + ], env = { + "CFLAGS": ctx.options["host_cflags"], + "CXXFLAGS": ctx.options["host_cxxflags"], + "LDFLAGS": ctx.options["host_ldflags"], + }) + +def build(ctx): + jobs = "-j" + str(ctx.jobs) + ctx.run(["make", jobs, "all-gcc"]) + ctx.run(["make", jobs, "all-target-libgcc"]) + +def install(ctx, pkg): + ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-gcc"]) + ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-target-libgcc"]) diff --git a/lib/common.star b/lib/common.star new file mode 100644 index 0000000..7882cac --- /dev/null +++ b/lib/common.star @@ -0,0 +1,82 @@ +# Commonly used helpers, auto-loaded into every recipe. + +def autotools_configure(ctx, extra_args = []): + args = [ + ctx.source_dir + "/configure", + "--prefix=" + ctx.prefix, + "--sysconfdir=/etc", + "--localstatedir=/var", + ] + args.extend(extra_args) + ctx.run(args, env = _toolchain_env(ctx)) + +def autotools_build(ctx, extra_args = []): + args = ["make", "-j" + str(ctx.jobs)] + args.extend(extra_args) + ctx.run(args) + +def autotools_check(ctx, extra_args = []): + args = ["make", "check", "-j" + str(ctx.jobs)] + args.extend(extra_args) + ctx.run(args) + +def autotools_install(ctx, pkg, extra_args = []): + args = ["make", "install", "DESTDIR=" + pkg.destdir] + args.extend(extra_args) + ctx.run(args) + +def autotools(configure_args = [], build_args = [], install_args = []): + def _configure(ctx): + autotools_configure(ctx, extra_args = configure_args) + def _build(ctx): + autotools_build(ctx, extra_args = build_args) + def _install(ctx, pkg): + autotools_install(ctx, pkg, extra_args = install_args) + return _configure, _build, _install + +def meson_configure(ctx, extra_args = []): + args = [ + "meson", + "setup", + ctx.build_dir, + ctx.source_dir, + "--prefix=" + ctx.prefix, + ] + args.extend(extra_args) + ctx.run(args, env = _toolchain_env(ctx)) + +def meson_build(ctx): + ctx.run(["meson", "compile", "-C", ctx.build_dir, "-j" + str(ctx.jobs)]) + +def meson_install(ctx, pkg): + ctx.run(["meson", "install", "-C", ctx.build_dir, "--destdir", pkg.destdir]) + + +def meson(configure_args = [], build_args = [], install_args = []): + def _configure(ctx): + meson_configure(ctx, extra_args = configure_args) + def _build(ctx): + meson_build(ctx, extra_args = build_args) + def _install(ctx, pkg): + meson_install(ctx, pkg, extra_args = install_args) + return _configure, _build, _install + +def make(ctx, target = None, extra_args = []): + args = ["make", "-C", ctx.source_dir, "O=" + ctx.build_dir, + "-j" + str(ctx.jobs)] + args.extend(extra_args) + if target: + args.append(target) + ctx.run(args) + +def make_install(ctx, pkg, extra_args = []): + args = ["make", "-C", ctx.build_dir, "DESTDIR=" + pkg.destdir, "install"] + args.extend(extra_args) + ctx.run(args) + +def _toolchain_env(ctx): + env = {} + for key, var in [("cflags", "CFLAGS"), ("cxxflags", "CXXFLAGS"), ("ldflags", "LDFLAGS")]: + if key in ctx.options: + env[var] = ctx.options[key] + return env diff --git a/recipes/linux/recipe.star b/recipes/linux/recipe.star new file mode 100644 index 0000000..6ead550 --- /dev/null +++ b/recipes/linux/recipe.star @@ -0,0 +1,38 @@ +name = "linux" +version = "7.0.9" +revision = 1 +description = "Linux kernel" +license = "GPL-2.0-only" + +source = { + "url": f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz", + "sha256": "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4", + "strip_components": 1, +} + +host_deps = ["binutils", "gcc"] + +def _make_args(ctx, *args): + triple = ctx.options["target_triple"] + result = [ + "make", + "-C", ctx.source_dir, + "O=" + ctx.build_dir, + "ARCH=x86_64", + f"CROSS_COMPILE={triple}-", + "-j" + str(ctx.jobs), + ] + result.extend(args) + return result + +def configure(ctx): + ctx.run(_make_args(ctx, "defconfig")) + +def build(ctx): + ctx.run(_make_args(ctx, "bzImage")) + +def install(ctx, pkg): + ctx.install( + ctx.build_dir + "/arch/x86/boot/bzImage", + pkg.destdir + "/boot/vmlinuz-" + version, + ) diff --git a/recipes/musl/recipe.star b/recipes/musl/recipe.star new file mode 100644 index 0000000..477e13c --- /dev/null +++ b/recipes/musl/recipe.star @@ -0,0 +1,32 @@ +name = "musl" +version = "1.2.6" +revision = 1 +description = "Musl C library" +license = "MIT" + +source = { + "url": f"https://musl.libc.org/releases/musl-{version}.tar.gz", + "sha256": "d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a", + "strip_components": 1, +} + +host_deps = ["binutils", "gcc"] + +def configure(ctx): + triple = ctx.options["target_triple"] + ctx.run( + [ + ctx.source_dir + "/configure", + "--prefix=/usr", + "--syslibdir=/lib", + "--target=" + triple, + ], + env = { + "CC": triple + "-gcc", + "CFLAGS": ctx.options["cflags"], + "LDFLAGS": ctx.options["ldflags"], + }, + ) + +build = autotools_build +install = autotools_install diff --git a/src/apk.rs b/src/apk.rs new file mode 100644 index 0000000..976c80d --- /dev/null +++ b/src/apk.rs @@ -0,0 +1,46 @@ +use crate::config::Config; +use crate::recipe::OutputPackage; +use std::path::Path; + +#[derive(Debug, Clone)] +pub struct ApkPackagePlan { + pub args: Vec, +} + +pub fn mkpkg_plan( + config: &Config, + package: &OutputPackage, + files_root: &Path, + output_dir: &Path, +) -> ApkPackagePlan { + let file_name = format!( + "{}-{}-r{}.apk", + package.name, package.version, package.revision + ); + let output = output_dir.join(file_name); + let version = format!("{}-r{}", package.version, package.revision); + let mut args = vec![ + "mkpkg".to_owned(), + "--files".to_owned(), + files_root.display().to_string(), + "--output".to_owned(), + output.display().to_string(), + "--info".to_owned(), + format!("name:{}", package.name), + "--info".to_owned(), + format!("version:{version}"), + "--info".to_owned(), + format!("arch:{}", config.target_arch), + "--info".to_owned(), + format!("description:{}", package.description), + "--info".to_owned(), + format!("license:{}", package.license), + "--info".to_owned(), + format!("origin:{}", package.recipe), + ]; + for dep in &package.run_deps { + args.push("--info".to_owned()); + args.push(format!("depends:{dep}")); + } + ApkPackagePlan { args } +} diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..476910d --- /dev/null +++ b/src/build.rs @@ -0,0 +1,921 @@ +use crate::apk; +use crate::config::Config; +use crate::graph::PackageGraph; +use crate::log; +use crate::patches; +use crate::phase::{PhaseCommand, PhaseEnv, SourceDir, collect_phase_commands}; +use crate::recipe::{OutputPackage, PackageKind, Recipe, RecipeSet}; +use crate::source; +use anyhow::{Context, Result, anyhow, bail}; +use sha2::{Digest, Sha256}; +use std::cell::{Cell, RefCell}; +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +const C_SOURCE: &str = "/source"; +const C_BUILD: &str = "/builddir"; +const C_DEST: &str = "/dest"; +const C_SYSROOT: &str = "/sysroot"; +const HOST_PREFIX: &str = "/usr/local"; +const TARGET_PREFIX: &str = "/usr"; + +#[derive(Debug)] +pub struct Builder { + repo: PathBuf, + config: Config, + skip_checks: bool, + preflight_done: Cell, + built_recipes: RefCell>, +} + +impl Builder { + pub fn new(repo: PathBuf, config: Config, skip_checks: bool) -> Self { + Self { + repo, + config, + skip_checks, + preflight_done: Cell::new(false), + built_recipes: RefCell::default(), + } + } + + pub fn build( + &self, + recipes: &RecipeSet, + graph: &PackageGraph, + package: Option<&str>, + rebuild: bool, + ) -> Result<()> { + self.preflight_container()?; + let (label, order) = match package { + Some(p) => (p.to_owned(), graph.build_order(p)?), + None => ("".to_owned(), graph.build_order_all()?), + }; + log::step( + "plan", + &format!("{label}: {} package(s) [{}]", order.len(), order.join(", ")), + ); + for output_name in order { + let output = graph.output(&output_name)?; + let recipe = recipes.recipe_for_package(&output_name)?; + self.ensure_source(recipe)?; + // Only force a rebuild of the explicitly requested package; + // dependencies stay cached when their manifest is up to date. + // With no package (build all), every package honours --rebuild. + let force = rebuild && package.map_or(true, |p| p == output_name); + match output.kind { + PackageKind::Host => self.build_host(recipe, output, force)?, + PackageKind::Target => self.build_target(recipe, output, force)?, + } + } + self.repo_index()?; + Ok(()) + } + + pub fn fetch(&self, recipes: &RecipeSet, package: &str) -> Result<()> { + let recipe = recipes.recipe_by_user_ref(package)?; + log::step("fetch", &recipe.key()); + source::fetch_sources(recipe, &self.source_cache_dir())?; + Ok(()) + } + + pub fn shell(&self, recipes: &RecipeSet, graph: &PackageGraph, package: &str) -> Result<()> { + graph.output(package)?; + let recipe = recipes.recipe_for_package(package)?; + self.preflight_container()?; + let source = self.unpacked_source_dir(recipe); + let build = self.build_dir(recipe); + fs::create_dir_all(&source)?; + fs::create_dir_all(&build)?; + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-it") + .arg("-v") + .arg(format!("{}:{C_SOURCE}:ro", source.display())) + .arg("-v") + .arg(format!("{}:{C_BUILD}", build.display())) + .arg("-w") + .arg(C_BUILD) + .arg(&self.config.container_image) + .arg("/bin/sh") + .status()?; + if !status.success() { + bail!("container shell exited with {status}"); + } + Ok(()) + } + + pub fn repo_index(&self) -> Result<()> { + self.preflight_container()?; + let repo = self.pkgs_dir(); + fs::create_dir_all(&repo)?; + log::step( + "index", + &format!("signing repository at {}", repo.display()), + ); + let key = self.abs_config_path(&self.config.signing_key); + let pubkey = self.abs_config_path(&self.config.signing_pubkey); + if !key.exists() || !pubkey.exists() { + bail!("signing key is not configured or missing; run `distro init-key` first"); + } + let index_name = "APKINDEX.adb"; + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/repo", repo.display())) + .arg("-v") + .arg(format!("{}:/keys/private.rsa:ro", key.display())) + .arg("-v") + .arg(format!( + "{}:/etc/apk/keys/distro.rsa.pub:ro", + pubkey.display() + )) + .arg(&self.config.container_image) + .arg("/bin/sh") + .arg("-lc") + .arg(format!( + "cd /repo && apk --sign-key /keys/private.rsa mkndx -o {index_name} *.apk" + )) + .status() + .context("failed to run repository index command")?; + if !status.success() { + bail!("repository index command failed with {status}"); + } + Ok(()) + } + + pub fn init_key(&self) -> Result<()> { + let key = self.abs_config_path(&self.config.signing_key); + let pubkey = self.abs_config_path(&self.config.signing_pubkey); + if key.exists() && pubkey.exists() { + log::skip("init-key", "signing key already present"); + return Ok(()); + } + log::step("init-key", &format!("generating {}", key.display())); + let key_dir = key + .parent() + .ok_or_else(|| anyhow!("invalid signing key path"))?; + let key_name = key + .file_name() + .ok_or_else(|| anyhow!("invalid signing key path"))? + .to_string_lossy(); + fs::create_dir_all(key_dir)?; + self.preflight_container()?; + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/keys", key_dir.display())) + .arg(&self.config.container_image) + .arg("/bin/sh") + .arg("-lc") + .arg(format!( + "openssl genrsa -out /keys/{key_name} 4096 && \ + openssl rsa -in /keys/{key_name} -pubout -out /keys/{key_name}.pub" + )) + .status() + .context("failed to run key generation command")?; + if !status.success() { + bail!("key generation failed with {status}"); + } + Ok(()) + } + + pub fn rootfs(&self, root: &Path, packages: &[String]) -> Result<()> { + self.preflight_container()?; + let pubkey = self.abs_config_path(&self.config.signing_pubkey); + if !pubkey.exists() { + bail!("rootfs requires a configured public signing key; run `distro init-key` first"); + } + fs::create_dir_all(root)?; + log::step( + "rootfs", + &format!( + "{} -> {} [{}]", + packages.join(", "), + root.display(), + root.display() + ), + ); + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/rootfs", root.display())) + .arg("-v") + .arg(format!("{}:/repo:ro", self.pkgs_dir().display())) + .arg("-v") + .arg(format!( + "{}:/etc/apk/keys/distro.rsa.pub:ro", + pubkey.display() + )) + .arg(&self.config.container_image) + .arg("apk") + .arg("--root") + .arg("/rootfs") + .arg("--repository") + .arg("/repo/APKINDEX.adb") + .arg("add") + .args(packages) + .status() + .context("failed to run rootfs apk command")?; + if !status.success() { + bail!("rootfs creation failed with {status}"); + } + Ok(()) + } + + // --- per-recipe build steps ------------------------------------------- + + fn build_host(&self, recipe: &Recipe, output: &OutputPackage, rebuild: bool) -> Result<()> { + let manifest = self.manifest_path(&output.key()); + let hash = self.manifest_hash(recipe)?; + if !rebuild && fs::read_to_string(&manifest).ok().as_deref() == Some(hash.as_str()) { + log::skip("up-to-date", &output.key()); + return Ok(()); + } + log::step( + "build", + &format!("{} {}-r{}", output.key(), recipe.version, recipe.revision), + ); + let build_dir = self.host_build_dir(recipe); + let dest_dir = self.host_pkg_dir(recipe); + let source_dir = self.unpacked_source_dir(recipe); + // A rebuild starts from a clean build dir; sources stay shared. + if rebuild { + Self::recreate(&build_dir)?; + } else { + fs::create_dir_all(&build_dir)?; + } + Self::recreate(&dest_dir)?; + fs::create_dir_all(&build_dir)?; + + let env = PhaseEnv { + source_dir: source_dir_env(recipe), + build_dir: C_BUILD, + dest_dir: C_DEST, + prefix: HOST_PREFIX, + sysroot: C_SYSROOT, + }; + self.run_recipe_phases( + recipe, + output, + &env, + &source_dir, + &build_dir, + &dest_dir, + None, + )?; + + fs::create_dir_all(manifest.parent().unwrap())?; + fs::write(manifest, hash)?; + Ok(()) + } + + fn build_target(&self, recipe: &Recipe, output: &OutputPackage, rebuild: bool) -> Result<()> { + let manifest = self.manifest_path(&output.key()); + let hash = self.manifest_hash(recipe)?; + if !rebuild && fs::read_to_string(&manifest).ok().as_deref() == Some(hash.as_str()) { + log::skip("up-to-date", &output.key()); + return Ok(()); + } + log::step( + "build", + &format!("{} {}-r{}", output.key(), recipe.version, recipe.revision), + ); + let build_dir = self.build_dir(recipe); + let source_dir = self.unpacked_source_dir(recipe); + // A rebuild starts from a clean build dir; sources stay shared. + if rebuild { + Self::recreate(&build_dir)?; + } else { + fs::create_dir_all(&build_dir)?; + } + + let dest_tmp = + tempfile::tempdir_in(&build_dir).context("failed to create ephemeral destdir")?; + let dest_dir = dest_tmp.path(); + + let sysroot = self.materialize_sysroot(recipe)?; + let env = PhaseEnv { + source_dir: source_dir_env(recipe), + build_dir: C_BUILD, + dest_dir: C_DEST, + prefix: TARGET_PREFIX, + sysroot: C_SYSROOT, + }; + self.run_recipe_phases( + recipe, + output, + &env, + &source_dir, + &build_dir, + dest_dir, + sysroot.as_deref(), + )?; + + self.apk_mkpkg(output, dest_dir)?; + + fs::create_dir_all(manifest.parent().unwrap())?; + fs::write(manifest, hash)?; + Ok(()) + } + + fn run_recipe_phases( + &self, + recipe: &Recipe, + output: &OutputPackage, + env: &PhaseEnv<'_>, + source_dir: &Path, + build_dir: &Path, + dest_dir: &Path, + target_sysroot: Option<&Path>, + ) -> Result<()> { + let host_sandbox = self.materialize_host_sandbox(recipe)?; + let recipe_already_built = self.built_recipes.borrow().contains(&recipe.key()); + + // configure/build/check run at most once per recipe per invocation. + let mut shared_phases: Vec<(&str, bool)> = Vec::new(); + if !recipe_already_built { + if let Some(p) = recipe.configure_fn.as_deref() { + shared_phases.push((p, false)); + } + if let Some(p) = recipe.build_fn.as_deref() { + shared_phases.push((p, false)); + } + if !self.skip_checks { + if let Some(p) = recipe.check_fn.as_deref() { + shared_phases.push((p, false)); + } + } + } + // install runs per output (each output has its own destdir/install_fn). + let install_phase = (output.install_fn.as_str(), true); + + let mut commands: Vec = Vec::new(); + for (phase, takes_pkg) in shared_phases.iter().chain(std::iter::once(&install_phase)) { + let pkg = if *takes_pkg { + Some((output.name.as_str(), C_DEST)) + } else { + None + }; + let owner = if *takes_pkg { + output.key() + } else { + recipe.key() + }; + log::info("phase", &format!("{phase} {owner}")); + commands.extend(collect_phase_commands( + &recipe.path, + &self.repo, + &self.config, + phase, + env, + pkg, + )?); + } + if commands.is_empty() { + self.built_recipes.borrow_mut().insert(recipe.key()); + return Ok(()); + } + + self.run_in_container( + source_dir, + build_dir, + dest_dir, + host_sandbox.as_ref().map(|s| s.path()), + target_sysroot, + &commands, + )?; + self.built_recipes.borrow_mut().insert(recipe.key()); + Ok(()) + } + + fn run_in_container( + &self, + source_dir: &Path, + build_dir: &Path, + dest_dir: &Path, + host_sandbox: Option<&Path>, + target_sysroot: Option<&Path>, + commands: &[PhaseCommand], + ) -> Result<()> { + fs::create_dir_all(dest_dir)?; + let mut process = Command::new(&self.config.container_runtime); + process + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:{C_SOURCE}:ro", source_dir.display())) + .arg("-v") + .arg(format!("{}:{C_BUILD}", build_dir.display())) + .arg("-v") + .arg(format!("{}:{C_DEST}", dest_dir.display())) + .arg("-w") + .arg(C_BUILD); + if let Some(sandbox) = host_sandbox { + // Host packages are configured with --prefix=/usr/local, so we + // bind the assembled tree exactly there (matching Jinx). This + // means rpaths, --print-search-dirs, pkg-config lookups, etc. + // all keep working with no extra environment fiddling. + process + .arg("-v") + .arg(format!("{}:{HOST_PREFIX}:ro", sandbox.display())); + } + if let Some(sysroot) = target_sysroot { + process + .arg("-v") + .arg(format!("{}:{C_SYSROOT}:ro", sysroot.display())) + .arg("-e") + .arg(format!("PKG_CONFIG_SYSROOT_DIR={C_SYSROOT}")) + .arg("-e") + .arg(format!("SYSROOT={C_SYSROOT}")); + } + process + .arg(&self.config.container_image) + .arg("/bin/sh") + .arg("-c") + .arg(build_phase_script(commands)); + let status = process + .status() + .context("failed to start container for phase commands")?; + if !status.success() { + bail!("phase commands failed with {status}"); + } + Ok(()) + } + + fn apk_mkpkg(&self, output: &OutputPackage, dest_dir: &Path) -> Result<()> { + let repo = self.pkgs_dir(); + fs::create_dir_all(&repo)?; + let signing_key = self.abs_config_path(&self.config.signing_key); + if !signing_key.exists() { + bail!("package signing key is missing; run `distro init-key` first"); + } + log::step("package", &format!("{} -> {}", output.name, repo.display())); + let plan = apk::mkpkg_plan(&self.config, output, Path::new(C_DEST), Path::new("/out")); + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:{C_DEST}:ro", dest_dir.display())) + .arg("-v") + .arg(format!("{}:/out", repo.display())) + .arg("-v") + .arg(format!("{}:/keys/private.rsa:ro", signing_key.display())) + .arg(&self.config.container_image) + .arg("apk") + .arg("--sign-key") + .arg("/keys/private.rsa") + .args(plan.args) + .status() + .context("failed to run apk mkpkg command")?; + if !status.success() { + bail!("apk mkpkg failed with {status}"); + } + Ok(()) + } + + // --- source preparation (Jinx-style persistent sources//) ------ + + fn ensure_source(&self, recipe: &Recipe) -> Result<()> { + let cached = source::fetch_sources(recipe, &self.source_cache_dir())?; + + let unpacked = self.unpacked_source_dir(recipe); + let version_stamp = self.source_stamp(recipe, "version"); + let want_version = format!("{}-r{}", recipe.version, recipe.revision); + let need_extract = + fs::read_to_string(&version_stamp).ok().as_deref() != Some(&want_version); + + if need_extract { + log::info("unpack", &recipe.key()); + if unpacked.exists() { + fs::remove_dir_all(&unpacked)?; + } + fs::create_dir_all(&unpacked)?; + for (src, tarball) in recipe.sources.iter().zip(cached.iter()) { + let dest = if src.name.is_empty() { + unpacked.clone() + } else { + unpacked.join(&src.name) + }; + self.extract_tarball(tarball, &dest, src.strip_components)?; + } + fs::write(&version_stamp, &want_version)?; + let patched = self.source_stamp(recipe, "patched"); + let _ = fs::remove_file(&patched); + } + + let patched_stamp = self.source_stamp(recipe, "patched"); + if !patched_stamp.exists() { + self.apply_patches(recipe, &unpacked)?; + fs::write(&patched_stamp, "")?; + } + Ok(()) + } + + fn extract_tarball(&self, tarball: &Path, dest: &Path, strip_components: u32) -> Result<()> { + fs::create_dir_all(dest)?; + log::info( + "extract", + &format!("{} -> {}", tarball.display(), dest.display()), + ); + let strip = if strip_components > 0 { + format!("--strip-components={strip_components}") + } else { + String::new() + }; + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/in.tar:ro", tarball.display())) + .arg("-v") + .arg(format!("{}:/out", dest.display())) + .arg(&self.config.container_image) + .arg("/bin/sh") + .arg("-c") + .arg(format!("tar -xf /in.tar -C /out {strip}")) + .status() + .context("failed to unpack source archive")?; + if !status.success() { + bail!("source unpack failed with {status}"); + } + Ok(()) + } + + fn apply_patches(&self, recipe: &Recipe, source_dir: &Path) -> Result<()> { + let patches = patches::discover(&recipe.dir)?; + if patches.is_empty() { + return Ok(()); + } + if recipe.sources.iter().any(|s| !s.name.is_empty()) { + bail!( + "recipe `{}` has patches/ but uses multi-source layout; \ + apply patches yourself via ctx.run in the configure phase", + recipe.id + ); + } + for patch in patches { + log::info( + "patch", + &format!( + "{} <- {}", + recipe.id, + patch + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(), + ), + ); + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/source", source_dir.display())) + .arg("-v") + .arg(format!("{}:/patch.diff:ro", patch.display())) + .arg(&self.config.container_image) + .arg("patch") + .arg("-d") + .arg("/source") + .arg("-p1") + .arg("-i") + .arg("/patch.diff") + .status() + .context("failed to apply patch")?; + if !status.success() { + bail!("patch {} failed with {status}", patch.display()); + } + } + Ok(()) + } + + // --- host sandbox / target sysroot materialization -------------------- + + /// Assemble all `host_deps` into a single ephemeral tree that we can mount + /// at `/usr/local` inside the container. Following Jinx, we hard-link + /// rather than byte-copy so this is essentially free. The returned + /// `TempDir` cleans up when dropped. + fn materialize_host_sandbox(&self, recipe: &Recipe) -> Result> { + if recipe.host_deps.is_empty() { + return Ok(None); + } + fs::create_dir_all(self.repo.join("build"))?; + let sandbox = tempfile::Builder::new() + .prefix(&format!("host-sandbox-{}-", recipe.id)) + .tempdir_in(self.repo.join("build")) + .context("failed to create host sandbox tempdir")?; + log::info( + "host-sandbox", + &format!("{} <- [{}]", recipe.id, recipe.host_deps.join(", ")), + ); + for dep in &recipe.host_deps { + let source = self.host_pkg_dir_by_id(dep).join("usr/local"); + if !source.exists() { + bail!( + "host dependency `{dep}` has not been built at {}", + source.display() + ); + } + hardlink_tree(&source, sandbox.path())?; + } + Ok(Some(sandbox)) + } + + fn materialize_sysroot(&self, recipe: &Recipe) -> Result> { + let mut deps: Vec = recipe + .build_deps + .iter() + .chain(recipe.run_deps.iter()) + .cloned() + .collect(); + deps.sort(); + deps.dedup(); + if deps.is_empty() { + return Ok(None); + } + let pubkey = self.abs_config_path(&self.config.signing_pubkey); + if !pubkey.exists() { + bail!("target dependency sysroot requires a configured public signing key"); + } + let sysroot = self.repo.join("build/sysroots").join(&recipe.id); + log::info( + "sysroot", + &format!("{} <- [{}]", recipe.id, deps.join(", ")), + ); + Self::recreate(&sysroot)?; + let status = Command::new(&self.config.container_runtime) + .arg("run") + .arg("--rm") + .arg("-v") + .arg(format!("{}:/sysroot", sysroot.display())) + .arg("-v") + .arg(format!("{}:/repo:ro", self.pkgs_dir().display())) + .arg("-v") + .arg(format!( + "{}:/etc/apk/keys/distro.rsa.pub:ro", + pubkey.display() + )) + .arg(&self.config.container_image) + .arg("apk") + .arg("--root") + .arg("/sysroot") + .arg("--repository") + .arg("/repo/APKINDEX.adb") + .arg("add") + .args(&deps) + .status() + .context("failed to install target dependency sysroot")?; + if !status.success() { + bail!("target dependency sysroot install failed with {status}"); + } + Ok(Some(sysroot)) + } + + // --- preflight & container image -------------------------------------- + + fn preflight_container(&self) -> Result<()> { + if self.preflight_done.get() { + return Ok(()); + } + let status = Command::new(&self.config.container_runtime) + .arg("--version") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .with_context(|| { + format!( + "{} is required but was not found", + self.config.container_runtime + ) + })?; + if !status.success() { + bail!( + "{} preflight failed with {status}", + self.config.container_runtime + ); + } + let dockerfile = self.abs_config_path(&self.config.container_dockerfile); + self.ensure_container_image(&dockerfile)?; + self.preflight_done.set(true); + Ok(()) + } + + fn ensure_container_image(&self, dockerfile: &Path) -> Result<()> { + if !dockerfile.exists() { + bail!( + "configured container Dockerfile does not exist: {}", + dockerfile.display() + ); + } + let hash = self.container_build_hash(dockerfile)?; + let stamp = self.repo.join("build/container-image.hash"); + if fs::read_to_string(&stamp).ok().as_deref() == Some(hash.as_str()) + && self.container_image_exists()? + { + return Ok(()); + } + log::step( + "image", + &format!( + "building {} from {}", + self.config.container_image, + dockerfile.display() + ), + ); + let status = Command::new(&self.config.container_runtime) + .arg("build") + .arg("-f") + .arg(dockerfile) + .arg("-t") + .arg(&self.config.container_image) + .arg(&self.repo) + .status() + .with_context(|| { + format!( + "failed to build container image `{}` from {}", + self.config.container_image, + dockerfile.display() + ) + })?; + if !status.success() { + bail!( + "container image build failed for `{}` with {status}", + self.config.container_image + ); + } + fs::create_dir_all(stamp.parent().unwrap())?; + fs::write(stamp, hash)?; + Ok(()) + } + + fn container_image_exists(&self) -> Result { + let status = Command::new(&self.config.container_runtime) + .arg("image") + .arg("exists") + .arg(&self.config.container_image) + .status() + .with_context(|| { + format!( + "failed to inspect container image `{}`", + self.config.container_image + ) + })?; + Ok(status.success()) + } + + fn container_build_hash(&self, dockerfile: &Path) -> Result { + let mut hasher = Sha256::new(); + hasher.update(fs::read(dockerfile)?); + hasher.update(self.config.container_image.as_bytes()); + Ok(hex::encode(hasher.finalize())) + } + + fn manifest_hash(&self, recipe: &Recipe) -> Result { + let mut hasher = Sha256::new(); + hasher.update(self.config.target_arch.as_bytes()); + hasher.update(recipe.version.as_bytes()); + hasher.update(recipe.revision.to_le_bytes()); + for source in &recipe.sources { + hasher.update(source.url.as_bytes()); + hasher.update(source.sha256.as_bytes()); + } + for patch in patches::discover(&recipe.dir)? { + hasher.update(patch.display().to_string().as_bytes()); + hasher.update(fs::read(patch)?); + } + Ok(hex::encode(hasher.finalize())) + } + + // --- path helpers ----------------------------------------------------- + + fn source_cache_dir(&self) -> PathBuf { + self.repo.join("build/cache/sources") + } + fn unpacked_source_dir(&self, recipe: &Recipe) -> PathBuf { + self.repo.join("build/sources").join(recipe.slug()) + } + fn source_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf { + self.repo + .join("build/sources") + .join(format!("{}.{kind}", recipe.slug())) + } + fn build_dir(&self, recipe: &Recipe) -> PathBuf { + self.repo.join("build/builds").join(&recipe.id) + } + fn host_build_dir(&self, recipe: &Recipe) -> PathBuf { + self.repo.join("build/host-builds").join(&recipe.id) + } + fn host_pkg_dir(&self, recipe: &Recipe) -> PathBuf { + self.repo.join("build/host-pkgs").join(&recipe.id) + } + fn host_pkg_dir_by_id(&self, host_recipe_id: &str) -> PathBuf { + self.repo.join("build/host-pkgs").join(host_recipe_id) + } + fn pkgs_dir(&self) -> PathBuf { + self.repo.join("build/pkgs") + } + fn manifest_path(&self, output_key: &str) -> PathBuf { + // Output keys may contain `:` (e.g. `host:gcc`); the manifest file + // name uses the filesystem-safe slug form instead. + let safe = output_key.replace(':', "-"); + self.repo + .join("build/manifests") + .join(format!("{safe}.hash")) + } + fn abs_config_path(&self, path: &Path) -> PathBuf { + if path.is_absolute() { + path.to_path_buf() + } else { + self.repo.join(path) + } + } + + fn recreate(path: &Path) -> Result<()> { + if path.exists() { + fs::remove_dir_all(path)?; + } + fs::create_dir_all(path)?; + Ok(()) + } +} + +fn build_phase_script(commands: &[PhaseCommand]) -> String { + let mut parts: Vec = Vec::with_capacity(commands.len() + 1); + parts.push("set -e".to_owned()); + for cmd in commands { + let mut tokens: Vec = Vec::with_capacity(cmd.env.len() + cmd.argv.len()); + for (k, v) in &cmd.env { + let value = shell_escape::unix::escape(v.as_str().into()).into_owned(); + tokens.push(format!("{k}={value}")); + } + for arg in &cmd.argv { + tokens.push(shell_escape::unix::escape(arg.as_str().into()).into_owned()); + } + parts.push(tokens.join(" ")); + } + parts.join("\n") +} + +/// Expose `/source` as a string for single-source recipes, or a struct of +/// `/source/` paths for multi-source recipes. +fn source_dir_env(recipe: &Recipe) -> SourceDir { + if recipe.sources.iter().all(|s| s.name.is_empty()) { + SourceDir::Single(C_SOURCE.to_owned()) + } else { + let map = recipe + .sources + .iter() + .map(|s| (s.name.clone(), format!("{C_SOURCE}/{}", s.name))) + .collect(); + SourceDir::Many(map) + } +} + +/// Mirror `src` into `dst` using hard links for regular files (and preserving +/// symlinks). This matches Jinx's `cp -Pplr`: no bytes are copied, just inode +/// references, so assembling a host-deps sandbox is essentially free. +fn hardlink_tree(src: &Path, dst: &Path) -> Result<()> { + fs::create_dir_all(dst)?; + for entry in walkdir::WalkDir::new(src) { + let entry = entry?; + let relative = entry.path().strip_prefix(src)?; + if relative.as_os_str().is_empty() { + continue; + } + let target = dst.join(relative); + let file_type = entry.file_type(); + if file_type.is_dir() { + fs::create_dir_all(&target)?; + } else if file_type.is_symlink() { + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + let link_target = fs::read_link(entry.path())?; + // Replace any pre-existing entry (multiple host deps may ship the + // same symlink path). + let _ = fs::remove_file(&target); + std::os::unix::fs::symlink(&link_target, &target)?; + } else if file_type.is_file() { + if let Some(parent) = target.parent() { + fs::create_dir_all(parent)?; + } + if target.exists() { + continue; + } + fs::hard_link(entry.path(), &target).with_context(|| { + format!( + "failed to hard-link {} -> {}", + entry.path().display(), + target.display() + ) + })?; + } + } + Ok(()) +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..b43ee75 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,119 @@ +use crate::build::Builder; +use crate::config::Config; +use crate::graph::PackageGraph; +use crate::recipe::RecipeSet; +use anyhow::{Context, Result, bail}; +use clap::{Parser, Subcommand}; +use std::fs; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +#[command(name = "distro", version)] +pub struct Cli { + #[arg(long, default_value = ".")] + pub repo: PathBuf, + + #[arg(long)] + pub skip_checks: bool, + + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + Build { + package: Option, + }, + Rebuild { + package: Option, + }, + Fetch { + package: String, + }, + Graph { + package: Option, + }, + List, + Info { + package: String, + }, + Clean, + Shell { + package: String, + }, + RepoIndex, + InitKey, + Rootfs { + root: PathBuf, + packages: Vec, + }, + Update { + #[arg(long)] + bump: bool, + packages: Vec, + }, +} + +pub fn run(cli: Cli) -> Result<()> { + let repo = cli.repo.canonicalize().unwrap_or(cli.repo); + match &cli.command { + Command::Clean => { + let build = repo.join("build"); + if build.exists() { + fs::remove_dir_all(&build) + .with_context(|| format!("failed to remove {}", build.display()))?; + } + Ok(()) + } + Command::InitKey => { + let config = Config::load(&repo.join("config.star"))?; + Builder::new(repo, config, cli.skip_checks).init_key() + } + _ => { + let config = Config::load(&repo.join("config.star"))?; + let recipes = RecipeSet::load(&repo, &config)?; + if let Command::Update { bump, packages } = &cli.command { + return crate::update::run(&recipes, packages, *bump); + } + let graph = PackageGraph::new(&recipes)?; + let builder = Builder::new(repo, config, cli.skip_checks); + match cli.command { + Command::Build { package } => { + builder.build(&recipes, &graph, package.as_deref(), false) + } + Command::Rebuild { package } => { + builder.build(&recipes, &graph, package.as_deref(), true) + } + Command::Fetch { package } => builder.fetch(&recipes, &package), + Command::Graph { package } => { + for line in graph.render(package.as_deref())? { + println!("{line}"); + } + Ok(()) + } + Command::List => { + for output in graph.outputs() { + println!("{output}"); + } + Ok(()) + } + Command::Info { package } => { + let output = graph.output(&package)?; + println!("{}", serde_json::to_string_pretty(output)?); + Ok(()) + } + Command::Shell { package } => builder.shell(&recipes, &graph, &package), + Command::RepoIndex => builder.repo_index(), + Command::Rootfs { root, packages } => { + if packages.is_empty() { + bail!("rootfs requires at least one package"); + } + builder.rootfs(&root, &packages) + } + Command::Update { .. } => unreachable!(), + Command::Clean | Command::InitKey => unreachable!(), + } + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..442669f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,37 @@ +use crate::starlark_eval::{eval_file, get_json_map, get_string, get_string_default}; +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Config { + pub target_arch: String, + pub options: BTreeMap, + pub container_runtime: String, + pub container_image: String, + pub container_dockerfile: PathBuf, + pub signing_key: PathBuf, + pub signing_pubkey: PathBuf, +} + +impl Config { + pub fn load(path: &Path) -> Result { + let module = eval_file(path, None, None) + .with_context(|| format!("failed to evaluate {}", path.display()))?; + Ok(Self { + target_arch: get_string(&module, "target_arch")?, + options: get_json_map(&module, "options")?, + container_runtime: get_string_default(&module, "container_runtime", "podman")?, + container_image: get_string(&module, "container_image")?, + container_dockerfile: PathBuf::from(get_string_default( + &module, + "container_dockerfile", + "Dockerfile", + )?), + signing_key: PathBuf::from(get_string(&module, "signing_key")?), + signing_pubkey: PathBuf::from(get_string(&module, "signing_pubkey")?), + }) + } +} diff --git a/src/graph.rs b/src/graph.rs new file mode 100644 index 0000000..29a5c11 --- /dev/null +++ b/src/graph.rs @@ -0,0 +1,135 @@ +use crate::recipe::{OutputPackage, PackageKind, RecipeSet, unresolved_deps}; +use anyhow::{Result, anyhow, bail}; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Debug, Clone)] +pub struct PackageGraph { + outputs: BTreeMap, + target_edges: BTreeMap>, + host_edges: BTreeMap>, +} + +impl PackageGraph { + pub fn new(recipes: &RecipeSet) -> Result { + let missing = unresolved_deps(recipes); + if !missing.is_empty() { + bail!("unresolved local dependencies:\n{}", missing.join("\n")); + } + let mut target_edges = BTreeMap::new(); + let mut host_edges = BTreeMap::new(); + for recipe in recipes.recipes.values() { + // host_deps always resolve into the host namespace. + let host_dep_keys: Vec = recipe + .host_deps + .iter() + .map(|d| PackageKind::Host.key(d)) + .collect(); + for output in &recipe.outputs { + let key = output.key(); + let mut edges = output.all_target_deps(); + if output.name == recipe.name { + edges.extend(recipe.build_deps.iter().cloned()); + edges.extend(recipe.run_deps.iter().cloned()); + } + match output.kind { + PackageKind::Host => { + host_edges.insert(key, host_dep_keys.clone()); + } + PackageKind::Target => { + let mut deps = host_dep_keys.clone(); + deps.extend(edges); + target_edges.insert(key, deps); + } + } + } + } + Ok(Self { + outputs: recipes.outputs.clone(), + target_edges, + host_edges, + }) + } + + pub fn output(&self, package: &str) -> Result<&OutputPackage> { + self.outputs + .get(package) + .ok_or_else(|| anyhow!("unknown package `{package}`")) + } + + pub fn outputs(&self) -> impl Iterator { + self.outputs.keys().map(String::as_str) + } + + pub fn build_order(&self, package: &str) -> Result> { + self.output(package)?; + let mut visiting = BTreeSet::new(); + let mut visited = BTreeSet::new(); + let mut order = Vec::new(); + self.visit(package, &mut visiting, &mut visited, &mut order)?; + Ok(order) + } + + /// Topologically-ordered list of every output in the graph (host + target). + pub fn build_order_all(&self) -> Result> { + let mut visiting = BTreeSet::new(); + let mut visited = BTreeSet::new(); + let mut order = Vec::new(); + for package in self.outputs.keys() { + self.visit(package, &mut visiting, &mut visited, &mut order)?; + } + Ok(order) + } + + fn visit( + &self, + package: &str, + visiting: &mut BTreeSet, + visited: &mut BTreeSet, + order: &mut Vec, + ) -> Result<()> { + if visited.contains(package) { + return Ok(()); + } + if !visiting.insert(package.to_owned()) { + bail!("dependency cycle involving `{package}`"); + } + let deps = self + .target_edges + .get(package) + .or_else(|| self.host_edges.get(package)) + .cloned() + .unwrap_or_default(); + for dep in deps { + self.visit(&dep, visiting, visited, order)?; + } + visiting.remove(package); + visited.insert(package.to_owned()); + order.push(package.to_owned()); + Ok(()) + } + + pub fn render(&self, package: Option<&str>) -> Result> { + match package { + Some(package) => { + let order = self.build_order(package)?; + Ok(order + .into_iter() + .map(|pkg| format!("{pkg}: {:?}", self.edges(&pkg))) + .collect()) + } + None => Ok(self + .outputs + .keys() + .map(|pkg| format!("{pkg}: {:?}", self.edges(pkg))) + .collect()), + } + } + + fn edges(&self, package: &str) -> Vec { + self.target_edges + .get(package) + .or_else(|| self.host_edges.get(package)) + .cloned() + .unwrap_or_default() + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..37d6e43 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,35 @@ +//! Tiny stderr logger. We use a consistent `==> :
` prefix +//! so progress messages are easy to scan during long builds. + +use std::io::{IsTerminal, Write}; + +const ARROW: &str = "==>"; + +fn paint(color: &str, text: &str) -> String { + if std::io::stderr().is_terminal() { + format!("\x1b[{color}m{text}\x1b[0m") + } else { + text.to_owned() + } +} + +fn emit(color: &str, action: &str, details: &str) { + let arrow = paint(color, ARROW); + let action = paint("1", action); + let _ = writeln!(std::io::stderr(), "{arrow} {action} {details}"); +} + +/// Major step, e.g. starting a build or packaging an output. +pub fn step(action: &str, details: &str) { + emit("1;34", action, details); // bold blue +} + +/// Cache hit / skipped work. +pub fn skip(action: &str, details: &str) { + emit("1;33", action, details); // bold yellow +} + +/// Sub-step inside a larger action. +pub fn info(action: &str, details: &str) { + emit("1;32", action, details); // bold green +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8393e8f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,21 @@ +mod apk; +mod build; +mod cli; +mod config; +mod graph; +mod log; +mod patches; +mod phase; +mod recipe; +mod rewrite; +mod source; +mod starlark_eval; +mod update; + +use anyhow::Result; +use clap::Parser; + +fn main() -> Result<()> { + let cli = cli::Cli::parse(); + cli::run(cli) +} diff --git a/src/patches.rs b/src/patches.rs new file mode 100644 index 0000000..cb7c512 --- /dev/null +++ b/src/patches.rs @@ -0,0 +1,19 @@ +use anyhow::Result; +use std::path::{Path, PathBuf}; + +pub fn discover(recipe_dir: &Path) -> Result> { + let patch_dir = recipe_dir.join("patches"); + if !patch_dir.exists() { + return Ok(Vec::new()); + } + let mut patches = Vec::new(); + for entry in std::fs::read_dir(&patch_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().and_then(|ext| ext.to_str()) == Some("patch") { + patches.push(path); + } + } + patches.sort(); + Ok(patches) +} diff --git a/src/phase.rs b/src/phase.rs new file mode 100644 index 0000000..0fb9227 --- /dev/null +++ b/src/phase.rs @@ -0,0 +1,214 @@ +use crate::config::Config; +use crate::starlark_eval::{eval_content_with_extra, options_literal, prepend_common_lib_load}; +use allocative::Allocative; +use anyhow::{Result, anyhow, bail}; +use serde::{Deserialize, Serialize}; +use starlark::environment::{GlobalsBuilder, LibraryExtension}; +use starlark::eval::Evaluator; +use starlark::starlark_module; +use starlark::values::none::NoneType; +use starlark::values::{ProvidesStaticType, Value}; +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::path::Path; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Allocative)] +pub struct PhaseCommand { + pub argv: Vec, + /// Extra environment variables exported just for this command, in the + /// order the recipe supplied them. Empty means "inherit only". + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub env: Vec<(String, String)>, +} + +/// How `ctx.source_dir` is exposed to Starlark. +/// +/// * `Single` is a single container path (e.g. `/source`), used when the +/// recipe declared a `source = {...}` form. +/// * `Many` is a map of source name to container path, exposed as a +/// `struct(...)` (e.g. `ctx.source_dir.linux`), used when the recipe +/// declared `sources = {"linux": {...}, ...}`. +#[derive(Debug, Clone)] +pub enum SourceDir { + Single(String), + Many(BTreeMap), +} + +/// Per-phase paths exposed to Starlark as `ctx.source_dir`, `ctx.build_dir`, +/// `ctx.dest_dir`, `ctx.prefix`, `ctx.sysroot` (Jinx-inspired). +#[derive(Debug, Clone)] +pub struct PhaseEnv<'a> { + pub source_dir: SourceDir, + pub build_dir: &'a str, + pub dest_dir: &'a str, + pub prefix: &'a str, + pub sysroot: &'a str, +} + +#[derive(Debug, Default, ProvidesStaticType, Allocative)] +struct CommandStore { + commands: RefCell>, +} + +impl CommandStore { + fn push(&self, command: PhaseCommand) { + self.commands.borrow_mut().push(command); + } +} + +#[starlark_module] +fn phase_globals(builder: &mut GlobalsBuilder) { + fn ctx_run<'v>( + argv: Value<'v>, + #[starlark(require = named, default = NoneType)] env: Value<'v>, + eval: &mut Evaluator, + ) -> anyhow::Result { + let json = argv.to_json()?; + let values: Vec = serde_json::from_str(&json) + .map_err(|err| anyhow!("ctx.run expects a list of strings: {err}"))?; + if values.is_empty() { + bail!("ctx.run argv cannot be empty"); + } + let env_vars = parse_env(env)?; + store(eval)?.push(PhaseCommand { + argv: values, + env: env_vars, + }); + Ok(NoneType) + } + + fn ctx_install( + src: &str, + dst: &str, + #[starlark(require = named, default = "644")] mode: &str, + eval: &mut Evaluator, + ) -> anyhow::Result { + store(eval)?.push(PhaseCommand { + argv: vec![ + "install".to_owned(), + format!("-Dm{mode}"), + src.to_owned(), + dst.to_owned(), + ], + env: Vec::new(), + }); + Ok(NoneType) + } +} + +fn parse_env(value: Value<'_>) -> anyhow::Result> { + if value.is_none() { + return Ok(Vec::new()); + } + let json = value.to_json()?; + let map: serde_json::Map = serde_json::from_str(&json) + .map_err(|err| anyhow!("ctx.run env must be a dict of string -> string: {err}"))?; + let mut out = Vec::with_capacity(map.len()); + for (key, val) in map { + let serde_json::Value::String(val) = val else { + bail!("ctx.run env value for `{key}` must be a string"); + }; + if key.is_empty() || key.contains('=') { + bail!("ctx.run env key `{key}` is not a valid variable name"); + } + out.push((key, val)); + } + Ok(out) +} + +fn store<'a, 'b, 'c, 'd>(eval: &'a Evaluator<'b, 'c, 'd>) -> anyhow::Result<&'a CommandStore> { + eval.extra + .ok_or_else(|| anyhow!("ctx command used without command store"))? + .downcast_ref::() + .ok_or_else(|| anyhow!("command store has the wrong type")) +} + +pub fn collect_phase_commands( + recipe_path: &Path, + repo_root: &Path, + config: &Config, + phase: &str, + env: &PhaseEnv<'_>, + package: Option<(&str, &str)>, +) -> Result> { + validate_identifier(phase)?; + let raw = std::fs::read_to_string(recipe_path)?; + // Auto-load helpers from `lib/common.star` so recipes never need an + // explicit `load()` for the canonical helpers. + let mut content = prepend_common_lib_load(Some(repo_root), &raw)?; + let jobs = std::thread::available_parallelism() + .map(|j| j.get()) + .unwrap_or(1); + let options = options_literal(config)?; + let source_dir_expr = source_dir_literal(&env.source_dir)?; + let ctx_literal = format!( + "struct(run = ctx_run, install = ctx_install, jobs = {jobs}, options = {options}, \ + source_dir = {sd}, build_dir = {bd}, dest_dir = {dd}, prefix = {pf}, sysroot = {sr})", + sd = source_dir_expr, + bd = serde_json::to_string(env.build_dir)?, + dd = serde_json::to_string(env.dest_dir)?, + pf = serde_json::to_string(env.prefix)?, + sr = serde_json::to_string(env.sysroot)?, + ); + let call = match package { + Some((name, destdir)) => format!( + "\n__ctx = {ctx_literal}\n__pkg = struct(name = {n}, destdir = {d})\n{phase}(__ctx, __pkg)\n", + n = serde_json::to_string(name)?, + d = serde_json::to_string(destdir)?, + ), + None => format!("\n__ctx = {ctx_literal}\n{phase}(__ctx)\n"), + }; + content.push_str(&call); + let globals = GlobalsBuilder::extended_by(&[LibraryExtension::StructType]) + .with(phase_globals) + .build(); + let cmd_store = CommandStore::default(); + eval_content_with_extra( + recipe_path, + content, + Some(config), + Some(repo_root), + globals, + Some(&cmd_store), + )?; + Ok(cmd_store.commands.into_inner()) +} + +fn source_dir_literal(source_dir: &SourceDir) -> Result { + match source_dir { + SourceDir::Single(path) => Ok(serde_json::to_string(path)?), + SourceDir::Many(map) => { + let mut fields = Vec::with_capacity(map.len()); + for (name, path) in map { + if !is_valid_field_name(name) { + bail!("source name `{name}` is not a valid Starlark identifier"); + } + fields.push(format!("{name} = {}", serde_json::to_string(path)?)); + } + Ok(format!("struct({})", fields.join(", "))) + } + } +} + +fn is_valid_field_name(name: &str) -> bool { + let mut chars = name.chars(); + match chars.next() { + Some(c) if c == '_' || c.is_ascii_alphabetic() => {} + _ => return false, + } + chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) +} + +fn validate_identifier(name: &str) -> Result<()> { + let mut chars = name.chars(); + let Some(first) = chars.next() else { + bail!("phase function name cannot be empty"); + }; + if !(first == '_' || first.is_ascii_alphabetic()) { + bail!("invalid phase function name `{name}`"); + } + if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) { + bail!("invalid phase function name `{name}`"); + } + Ok(()) +} diff --git a/src/recipe.rs b/src/recipe.rs new file mode 100644 index 0000000..ae55f25 --- /dev/null +++ b/src/recipe.rs @@ -0,0 +1,452 @@ +use crate::config::Config; +use crate::starlark_eval::{ + eval_content, get_i32_default, get_json, get_string, get_string_default, get_string_vec, + has_name, prepend_common_lib_load, +}; +use anyhow::{Result, anyhow, bail}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use walkdir::WalkDir; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum PackageKind { + Host, + Target, +} + +impl PackageKind { + /// Canonical `host:`/bare key form used across the graph, CLI and + /// manifest layer. Host and target trees are completely separate + /// namespaces — they may share names. + pub fn key(&self, name: &str) -> String { + match self { + PackageKind::Host => format!("host:{name}"), + PackageKind::Target => name.to_owned(), + } + } + + /// Filesystem-safe variant of [`PackageKind::key`] (no `:`), used to + /// derive build/source/manifest directory names. + pub fn slug(&self, name: &str) -> String { + match self { + PackageKind::Host => format!("host-{name}"), + PackageKind::Target => name.to_owned(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Source { + /// Empty for the single-`source` form, otherwise the dict key from `sources`. + pub name: String, + pub url: String, + pub sha256: String, + /// Number of leading path components to strip when extracting (tar's + /// `--strip-components`). `0` means strip nothing. + pub strip_components: u32, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct OutputPackage { + pub name: String, + /// Canonical key of the owning recipe (see [`PackageKind::key`]). + pub recipe: String, + pub kind: PackageKind, + pub version: String, + pub revision: i32, + pub description: String, + pub license: String, + /// Target packages installed into the build sysroot. Not propagated as + /// apk `depends:` metadata. + pub build_deps: Vec, + /// Target packages declared as runtime dependencies (apk `depends:`). + /// Also installed into the sysroot so the recipe can link against them. + pub run_deps: Vec, + pub install_fn: String, +} + +impl OutputPackage { + pub fn key(&self) -> String { + self.kind.key(&self.name) + } + + /// Union of build- and run-dependencies (used to materialize the sysroot + /// and to compute the build graph). + pub fn all_target_deps(&self) -> Vec { + let mut out = self.build_deps.clone(); + out.extend(self.run_deps.iter().cloned()); + out + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Recipe { + pub id: String, + pub path: PathBuf, + pub dir: PathBuf, + pub name: String, + pub kind: PackageKind, + pub version: String, + pub revision: i32, + pub description: String, + pub license: String, + pub sources: Vec, + pub host_deps: Vec, + pub build_deps: Vec, + pub run_deps: Vec, + pub outputs: Vec, + pub configure_fn: Option, + pub build_fn: Option, + pub check_fn: Option, +} + +impl Recipe { + /// Canonical key (`host:` or ``), used as the recipe-level + /// identifier in graphs, manifests and CLI references. + pub fn key(&self) -> String { + self.kind.key(&self.id) + } + + /// Filesystem-safe variant of [`Recipe::key`]. + pub fn slug(&self) -> String { + self.kind.slug(&self.id) + } +} + +#[derive(Debug, Clone)] +pub struct RecipeSet { + pub recipes: BTreeMap, + pub outputs: BTreeMap, +} + +impl RecipeSet { + /// Discover recipes under `repo_root`: + /// * `recipes//recipe.star` → target packages + /// * `host-recipes//recipe.star` → host packages + pub fn load(repo_root: &Path, config: &Config) -> Result { + let mut recipes = BTreeMap::new(); + for (subdir, kind) in [ + ("recipes", PackageKind::Target), + ("host-recipes", PackageKind::Host), + ] { + let root = repo_root.join(subdir); + if !root.exists() { + continue; + } + for entry in WalkDir::new(&root).follow_links(false) { + let entry = entry?; + if entry.file_type().is_file() && entry.file_name() == "recipe.star" { + let recipe = Recipe::load(entry.path(), config, repo_root, kind.clone())?; + let key = recipe.key(); + if recipes.insert(key.clone(), recipe).is_some() { + bail!("duplicate recipe `{key}` below {}", root.display()); + } + } + } + } + + let mut outputs = BTreeMap::new(); + for recipe in recipes.values() { + for output in &recipe.outputs { + let key = output.key(); + if outputs.insert(key.clone(), output.clone()).is_some() { + bail!("duplicate package output `{key}`"); + } + } + } + Ok(Self { recipes, outputs }) + } + + /// Look up a recipe by the package key produced by an output. + pub fn recipe_for_package(&self, package: &str) -> Result<&Recipe> { + let output = self + .outputs + .get(package) + .ok_or_else(|| anyhow!("unknown package `{package}`"))?; + self.recipes.get(&output.recipe).ok_or_else(|| { + anyhow!( + "package `{package}` references missing recipe `{}`", + output.recipe + ) + }) + } + + /// Resolve a user-supplied reference (recipe key, output key, or bare + /// id — provided it isn't ambiguous between the host and target trees). + pub fn recipe_by_user_ref(&self, name: &str) -> Result<&Recipe> { + if let Some(recipe) = self.recipes.get(name) { + return Ok(recipe); + } + if self.outputs.contains_key(name) { + return self.recipe_for_package(name); + } + // Bare id: search both trees, error on ambiguity. + let host_key = PackageKind::Host.key(name); + let target_key = PackageKind::Target.key(name); + match (self.recipes.get(&host_key), self.recipes.get(&target_key)) { + (Some(_), Some(_)) => bail!( + "`{name}` is ambiguous: matches both `{host_key}` and `{target_key}`; \ + use the explicit form" + ), + (Some(r), None) | (None, Some(r)) => Ok(r), + (None, None) => bail!("unknown recipe `{name}`"), + } + } +} + +impl Recipe { + pub fn load(path: &Path, config: &Config, repo_root: &Path, kind: PackageKind) -> Result { + // Auto-load helpers from `lib/common.star` so recipes never need an + // explicit `load()` for the canonical helpers. + let raw = std::fs::read_to_string(path)?; + let content = prepend_common_lib_load(Some(repo_root), &raw)?; + let module = eval_content( + path, + content, + Some(config), + Some(repo_root), + starlark::environment::Globals::standard(), + )?; + let dir = path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_path_buf(); + let id = dir + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow!("recipe path has no package directory: {}", path.display()))? + .to_owned(); + let name = get_string(&module, "name")?; + let version = get_string(&module, "version")?; + let revision = get_i32_default(&module, "revision", 0)?; + let description = get_string_default(&module, "description", "???")?; + let license = get_string_default(&module, "license", "???")?; + let build_deps = get_string_vec(&module, "build_deps")?; + let run_deps = get_string_vec(&module, "run_deps")?; + if has_name(&module, "deps") { + bail!( + "recipe `{id}` uses removed `deps`; split it into `build_deps` \ + (sysroot only) and `run_deps` (apk depends + sysroot)" + ); + } + let host_deps = get_string_vec(&module, "host_deps")?; + let sources = parse_sources(get_json(&module, "sources")?, get_json(&module, "source")?)?; + let subpackages = parse_subpackages(get_json(&module, "subpackages")?)?; + let mut outputs = Vec::new(); + let recipe_key = kind.key(&id); + outputs.push(OutputPackage { + name: name.clone(), + recipe: recipe_key.clone(), + kind: kind.clone(), + version: version.clone(), + revision, + description: description.clone(), + license: license.clone(), + build_deps: build_deps.clone(), + run_deps: run_deps.clone(), + install_fn: "install".to_owned(), + }); + for subpkg in subpackages { + let sub_name = subpkg + .get("name") + .and_then(JsonValue::as_str) + .ok_or_else(|| anyhow!("subpackage in `{name}` is missing string `name`"))? + .to_owned(); + if subpkg.contains_key("deps") { + bail!( + "subpackage `{sub_name}` in `{id}` uses removed `deps`; use \ + `build_deps` and/or `run_deps`" + ); + } + outputs.push(OutputPackage { + name: sub_name, + recipe: recipe_key.clone(), + kind: kind.clone(), + version: version.clone(), + revision, + description: subpkg + .get("description") + .and_then(JsonValue::as_str) + .unwrap_or(&description) + .to_owned(), + license: subpkg + .get("license") + .and_then(JsonValue::as_str) + .unwrap_or(&license) + .to_owned(), + build_deps: json_string_list(subpkg.get("build_deps"), "subpackage build_deps")? + .unwrap_or_default(), + run_deps: json_string_list(subpkg.get("run_deps"), "subpackage run_deps")? + .unwrap_or_default(), + install_fn: subpkg + .get("install") + .and_then(JsonValue::as_str) + .unwrap_or("install") + .to_owned(), + }); + } + validate_required(&outputs)?; + Ok(Self { + id, + path: path.to_path_buf(), + dir, + name, + kind, + version, + revision, + description, + license, + sources, + host_deps, + build_deps, + run_deps, + outputs, + configure_fn: has_name(&module, "configure").then_some("configure".to_owned()), + build_fn: has_name(&module, "build").then_some("build".to_owned()), + check_fn: has_name(&module, "check").then_some("check".to_owned()), + }) + } +} + +fn validate_required(outputs: &[OutputPackage]) -> Result<()> { + for output in outputs { + if output.name.trim().is_empty() { + bail!("package output name cannot be empty"); + } + for (field, value) in [ + ("version", &output.version), + ("description", &output.description), + ("license", &output.license), + ] { + if value.trim().is_empty() { + bail!( + "package `{}` has empty required field `{field}`", + output.name + ); + } + } + } + Ok(()) +} + +fn parse_sources( + sources: Option, + legacy_source: Option, +) -> Result> { + match (sources, legacy_source) { + (Some(_), Some(_)) => bail!("recipe defines both `sources` and `source`; use only one"), + (None, None) => Ok(Vec::new()), + (None, Some(single)) => { + let obj = single + .as_object() + .ok_or_else(|| anyhow!("`source` must be a dict"))?; + Ok(vec![parse_source_entry(String::new(), obj)?]) + } + (Some(multi), None) => { + let obj = multi + .as_object() + .ok_or_else(|| anyhow!("`sources` must be a dict of {{name: source}}"))?; + obj.iter() + .map(|(name, value)| { + if name.is_empty() { + bail!("source name in `sources` cannot be empty"); + } + let entry = value + .as_object() + .ok_or_else(|| anyhow!("source `{name}` must be a dict"))?; + parse_source_entry(name.clone(), entry) + }) + .collect() + } + } +} + +fn parse_source_entry(name: String, obj: &serde_json::Map) -> Result { + let url = obj + .get("url") + .and_then(JsonValue::as_str) + .ok_or_else(|| anyhow!("source entry missing string `url`"))? + .to_owned(); + let sha256 = obj + .get("sha256") + .and_then(JsonValue::as_str) + .unwrap_or("???") + .to_owned(); + let strip_components = match obj.get("strip_components") { + None => 0, + Some(JsonValue::Number(n)) => n + .as_u64() + .and_then(|v| u32::try_from(v).ok()) + .ok_or_else(|| anyhow!("source `strip_components` must be a non-negative integer"))?, + Some(_) => bail!("source `strip_components` must be an integer"), + }; + Ok(Source { + name, + url, + sha256, + strip_components, + }) +} + +fn parse_subpackages(value: Option) -> Result>> { + match value { + Some(JsonValue::Array(values)) => values + .into_iter() + .map(|value| { + value + .as_object() + .cloned() + .ok_or_else(|| anyhow!("subpackages entries must be objects")) + }) + .collect(), + Some(_) => bail!("subpackages must be a list of objects"), + None => Ok(Vec::new()), + } +} + +fn json_string_list(value: Option<&JsonValue>, label: &str) -> Result>> { + match value { + Some(JsonValue::Array(values)) => values + .iter() + .map(|value| { + value + .as_str() + .map(ToOwned::to_owned) + .ok_or_else(|| anyhow!("{label} must contain only strings")) + }) + .collect::>>() + .map(Some), + Some(_) => bail!("{label} must be a string list"), + None => Ok(None), + } +} + +pub fn unresolved_deps(recipes: &RecipeSet) -> Vec { + let names: BTreeSet<_> = recipes.outputs.keys().cloned().collect(); + let mut missing = Vec::new(); + for recipe in recipes.recipes.values() { + // host_deps always refer to host outputs (canonical `host:`); + // build_deps / run_deps refer to target outputs (bare names). + for dep in &recipe.host_deps { + let key = PackageKind::Host.key(dep); + if !names.contains(&key) { + missing.push(format!("{} -> {key}", recipe.key())); + } + } + for dep in recipe.build_deps.iter().chain(recipe.run_deps.iter()) { + if !names.contains(dep) { + missing.push(format!("{} -> {dep}", recipe.key())); + } + } + for output in &recipe.outputs { + for dep in output.build_deps.iter().chain(output.run_deps.iter()) { + if !names.contains(dep) { + missing.push(format!("{} -> {dep}", output.key())); + } + } + } + } + missing +} diff --git a/src/rewrite.rs b/src/rewrite.rs new file mode 100644 index 0000000..0ee478a --- /dev/null +++ b/src/rewrite.rs @@ -0,0 +1,39 @@ +use anyhow::{Context, Result}; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Rewrite { + pub field: String, + pub old: String, + pub new: String, +} + +pub fn backup_path(path: &Path) -> PathBuf { + let mut backup = path.as_os_str().to_os_string(); + backup.push(".bak"); + PathBuf::from(backup) +} + +pub fn rewrite_placeholders(path: &Path, rewrites: &[Rewrite]) -> Result { + if rewrites.is_empty() { + return Ok(false); + } + let original = + fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let mut updated = original.clone(); + for rewrite in rewrites { + let quoted_old = format!("{} = \"{}\"", rewrite.field, rewrite.old); + let quoted_new = format!("{} = \"{}\"", rewrite.field, rewrite.new); + updated = updated.replacen("ed_old, "ed_new, 1); + let dict_old = format!("\"{}\": \"{}\"", rewrite.field, rewrite.old); + let dict_new = format!("\"{}\": \"{}\"", rewrite.field, rewrite.new); + updated = updated.replacen(&dict_old, &dict_new, 1); + } + if updated == original { + return Ok(false); + } + fs::write(backup_path(path), original)?; + fs::write(path, updated)?; + Ok(true) +} diff --git a/src/source.rs b/src/source.rs new file mode 100644 index 0000000..899d431 --- /dev/null +++ b/src/source.rs @@ -0,0 +1,73 @@ +use crate::log; +use crate::recipe::Recipe; +use crate::rewrite::{Rewrite, rewrite_placeholders}; +use anyhow::{Context, Result, bail}; +use sha2::{Digest, Sha256}; +use std::fs; +use std::io::Read; +use std::path::{Path, PathBuf}; + +pub fn fetch_sources(recipe: &Recipe, cache_dir: &Path) -> Result> { + fs::create_dir_all(cache_dir)?; + let mut rewrites = Vec::new(); + let mut paths = Vec::new(); + for source in &recipe.sources { + let label = if source.name.is_empty() { + recipe.key() + } else { + format!("{}:{}", recipe.key(), source.name) + }; + let cached = source.sha256 != "???" && cache_dir.join(&source.sha256).exists(); + if cached { + log::skip("cached", &format!("{label} ({})", source.url)); + paths.push(cache_dir.join(&source.sha256)); + continue; + } + log::step("fetch", &format!("{label} <- {}", source.url)); + let bytes = download(&source.url)?; + let actual = sha256_hex(&bytes); + if source.sha256 == "???" { + log::info("sha256", &format!("{label} = {actual}")); + rewrites.push(Rewrite { + field: "sha256".into(), + old: "???".into(), + new: actual.clone(), + }); + } else if source.sha256 != actual { + bail!( + "checksum mismatch for {}: expected {}, got {}", + source.url, + source.sha256, + actual + ); + } + let path = cache_dir.join(&actual); + if !path.exists() { + fs::write(&path, &bytes) + .with_context(|| format!("failed to write {}", path.display()))?; + } + paths.push(path); + } + rewrite_placeholders(&recipe.path, &rewrites)?; + Ok(paths) +} + +fn download(url: &str) -> Result> { + if let Some(path) = url.strip_prefix("file://") { + return Ok(fs::read(path)?); + } + // Some mirrors reject requests without a User-Agent with HTTP 403, so set an explicit one. + let client = reqwest::blocking::Client::builder() + .user_agent(concat!("distro/", env!("CARGO_PKG_VERSION"))) + .build()?; + let mut response = client.get(url).send()?.error_for_status()?; + let mut bytes = Vec::new(); + response.read_to_end(&mut bytes)?; + Ok(bytes) +} + +pub fn sha256_hex(bytes: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hex::encode(hasher.finalize()) +} diff --git a/src/starlark_eval.rs b/src/starlark_eval.rs new file mode 100644 index 0000000..b619932 --- /dev/null +++ b/src/starlark_eval.rs @@ -0,0 +1,365 @@ +use crate::config::Config; +use allocative::Allocative; +use anyhow::{Result, anyhow, bail}; +use serde_json::Value as JsonValue; +use starlark::environment::{FrozenModule, Globals, Module}; +use starlark::eval::{Evaluator, FileLoader}; +use starlark::starlark_simple_value; +use starlark::syntax::{AstModule, Dialect}; +use starlark::values::dict::AllocDict; +use starlark::values::{AnyLifetime, Heap, NoSerialize, ProvidesStaticType, StarlarkValue, Value}; +use starlark_derive::starlark_value; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::{self, Display}; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, ProvidesStaticType, NoSerialize, Allocative)] +pub struct SettingsValue { + target_arch: String, + container_runtime: String, + container_image: String, + container_dockerfile: String, + options: BTreeMap, +} + +impl From<&Config> for SettingsValue { + fn from(config: &Config) -> Self { + Self { + target_arch: config.target_arch.clone(), + container_runtime: config.container_runtime.clone(), + container_image: config.container_image.clone(), + container_dockerfile: config.container_dockerfile.display().to_string(), + options: config + .options + .iter() + .map(|(key, value)| (key.clone(), option_value_to_string(value))) + .collect(), + } + } +} + +impl Display for SettingsValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "settings") + } +} + +starlark_simple_value!(SettingsValue); + +#[starlark_value(type = "settings")] +impl<'v> StarlarkValue<'v> for SettingsValue { + fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option> { + match attr { + "target_arch" => Some(heap.alloc(self.target_arch.as_str())), + "container_runtime" => Some(heap.alloc(self.container_runtime.as_str())), + "container_image" => Some(heap.alloc(self.container_image.as_str())), + "container_dockerfile" => Some(heap.alloc(self.container_dockerfile.as_str())), + "options" => Some(heap.alloc(AllocDict(self.options.clone()))), + _ => None, + } + } +} + +pub fn eval_file( + path: &Path, + settings: Option<&Config>, + repo_root: Option<&Path>, +) -> Result { + let content = std::fs::read_to_string(path)?; + eval_content(path, content, settings, repo_root, Globals::standard()) +} + +/// Path of the implicit helper library auto-loaded into every recipe. +pub const COMMON_LIB_MODULE: &str = "//lib:common.star"; +const COMMON_LIB_RELATIVE: &str = "lib/common.star"; + +/// Names exported by `lib/common.star`, if the file exists. Empty otherwise. +pub fn common_lib_names(repo_root: &Path) -> Result> { + let path = repo_root.join(COMMON_LIB_RELATIVE); + if !path.exists() { + return Ok(Vec::new()); + } + let module = eval_file(&path, None, Some(repo_root))?; + Ok(module + .names() + .map(|n| n.as_str().to_owned()) + .filter(|n| !n.starts_with('_') && n != "settings") + .collect()) +} + +/// Prepend an implicit `load("//lib:common.star", ...)` so every recipe sees +/// the shared helpers without an explicit import. Does nothing if there's no +/// `lib/common.star` or no repo root. +pub fn prepend_common_lib_load(repo_root: Option<&Path>, content: &str) -> Result { + let Some(root) = repo_root else { + return Ok(content.to_owned()); + }; + let names = common_lib_names(root)?; + if names.is_empty() { + return Ok(content.to_owned()); + } + let names_lit = names + .iter() + .map(|n| format!("{:?}", n)) + .collect::>() + .join(", "); + Ok(format!( + "load(\"{COMMON_LIB_MODULE}\", {names_lit})\n{content}" + )) +} + +pub fn eval_content( + path: &Path, + content: String, + settings: Option<&Config>, + repo_root: Option<&Path>, + globals: Globals, +) -> Result { + eval_content_with_extra(path, content, settings, repo_root, globals, None) +} + +pub fn eval_content_with_extra<'a>( + path: &Path, + content: String, + settings: Option<&Config>, + repo_root: Option<&Path>, + globals: Globals, + extra: Option<&'a dyn AnyLifetime<'a>>, +) -> Result { + let filename = path.display().to_string(); + let ast = AstModule::parse( + &filename, + content, + &Dialect { + enable_f_strings: true, + enable_top_level_stmt: true, + ..Dialect::Standard + }, + ) + .map_err(|err| anyhow!("{err}"))?; + let loader = match repo_root { + Some(root) => Some(RepoFileLoader::new(root, settings, globals.clone(), &ast)?), + None if !ast.loads().is_empty() => { + bail!( + "load() requires a repo root while evaluating {}", + path.display() + ) + } + None => None, + }; + let module = Module::new(); + if let Some(config) = settings { + module.set("settings", module.heap().alloc(SettingsValue::from(config))); + } + { + let mut eval = Evaluator::new(&module); + if let Some(loader) = &loader { + eval.set_loader(loader); + } + eval.extra = extra; + eval.eval_module(ast, &globals) + .map_err(|err| anyhow!("{err}"))?; + } + Ok(module) +} + +#[derive(Clone)] +struct RepoFileLoader { + modules: HashMap, +} + +impl RepoFileLoader { + fn new( + repo_root: &Path, + settings: Option<&Config>, + globals: Globals, + ast: &AstModule, + ) -> Result { + let mut modules = HashMap::new(); + for load in ast.loads() { + load_module( + repo_root, + settings, + globals.clone(), + load.module_id, + &mut modules, + )?; + } + Ok(Self { modules }) + } +} + +impl FileLoader for RepoFileLoader { + fn load(&self, path: &str) -> starlark::Result { + self.modules + .get(path) + .cloned() + .ok_or_else(|| starlark::Error::new_other(anyhow!("unknown Starlark module `{path}`"))) + } +} + +fn load_module( + repo_root: &Path, + settings: Option<&Config>, + globals: Globals, + module_id: &str, + modules: &mut HashMap, +) -> Result<()> { + if modules.contains_key(module_id) { + return Ok(()); + } + let path = resolve_load_path(repo_root, module_id)?; + let content = std::fs::read_to_string(&path)?; + let filename = path.display().to_string(); + let ast = + AstModule::parse(&filename, content, &Dialect::Standard).map_err(|err| anyhow!("{err}"))?; + for load in ast.loads() { + load_module( + repo_root, + settings, + globals.clone(), + load.module_id, + modules, + )?; + } + let nested_loader = RepoFileLoader { + modules: modules.clone(), + }; + let module = Module::new(); + if let Some(config) = settings { + module.set("settings", module.heap().alloc(SettingsValue::from(config))); + } + { + let mut eval = Evaluator::new(&module); + eval.set_loader(&nested_loader); + eval.eval_module(ast, &globals) + .map_err(|err| anyhow!("{err}"))?; + } + let frozen = module.freeze().map_err(|err| anyhow!("{err:?}"))?; + modules.insert(module_id.to_owned(), frozen); + Ok(()) +} + +fn resolve_load_path(repo_root: &Path, module_id: &str) -> Result { + let relative = if let Some(stripped) = module_id.strip_prefix("//") { + stripped.replace(':', "/") + } else { + module_id.to_owned() + }; + let path = repo_root.join(relative); + let canonical_root = repo_root + .canonicalize() + .unwrap_or_else(|_| repo_root.to_path_buf()); + let canonical_path = path.canonicalize().unwrap_or(path); + if !canonical_path.starts_with(&canonical_root) { + bail!("Starlark load escapes repo root: {module_id}"); + } + Ok(canonical_path) +} + +pub fn options_literal(config: &Config) -> Result { + json_to_starlark_literal(&JsonValue::Object( + config.options.clone().into_iter().collect(), + )) +} + +fn option_value_to_string(value: &JsonValue) -> String { + match value { + JsonValue::String(value) => value.clone(), + JsonValue::Bool(value) => value.to_string(), + JsonValue::Number(value) => value.to_string(), + JsonValue::Null => "null".to_owned(), + JsonValue::Array(_) | JsonValue::Object(_) => value.to_string(), + } +} + +fn json_to_starlark_literal(value: &JsonValue) -> Result { + Ok(match value { + JsonValue::Null => "None".to_owned(), + JsonValue::Bool(true) => "True".to_owned(), + JsonValue::Bool(false) => "False".to_owned(), + JsonValue::Number(value) => value.to_string(), + JsonValue::String(value) => serde_json::to_string(value)?, + JsonValue::Array(values) => format!( + "[{}]", + values + .iter() + .map(json_to_starlark_literal) + .collect::>>()? + .join(", ") + ), + JsonValue::Object(values) => format!( + "{{{}}}", + values + .iter() + .map(|(key, value)| { + Ok(format!( + "{}: {}", + serde_json::to_string(key)?, + json_to_starlark_literal(value)? + )) + }) + .collect::>>()? + .join(", ") + ), + }) +} + +pub fn get_string(module: &Module, name: &str) -> Result { + module + .get(name) + .and_then(|v| v.unpack_str().map(ToOwned::to_owned)) + .ok_or_else(|| anyhow!("missing or non-string Starlark variable `{name}`")) +} + +pub fn get_string_default(module: &Module, name: &str, default: &str) -> Result { + Ok(match module.get(name) { + Some(value) => value + .unpack_str() + .ok_or_else(|| anyhow!("non-string Starlark variable `{name}`"))? + .to_owned(), + None => default.to_owned(), + }) +} + +pub fn get_i32_default(module: &Module, name: &str, default: i32) -> Result { + Ok(match module.get(name) { + Some(value) => value + .unpack_i32() + .ok_or_else(|| anyhow!("non-integer Starlark variable `{name}`"))?, + None => default, + }) +} + +pub fn has_name(module: &Module, name: &str) -> bool { + module.get(name).is_some() +} + +pub fn get_json(module: &Module, name: &str) -> Result> { + match module.get(name) { + Some(value) => Ok(Some(serde_json::from_str(&value.to_json()?)?)), + None => Ok(None), + } +} + +pub fn get_string_vec(module: &Module, name: &str) -> Result> { + match get_json(module, name)? { + Some(JsonValue::Array(values)) => values + .into_iter() + .map(|value| match value { + JsonValue::String(s) => Ok(s), + _ => bail!("`{name}` must be a list of strings"), + }) + .collect(), + Some(_) => bail!("`{name}` must be a list of strings"), + None => Ok(Vec::new()), + } +} + +pub fn get_json_map(module: &Module, name: &str) -> Result> { + match get_json(module, name)? { + Some(JsonValue::Object(values)) => Ok(values.into_iter().collect()), + Some(_) => bail!("`{name}` must be a dict"), + None => Ok(BTreeMap::new()), + } +} diff --git a/src/update.rs b/src/update.rs new file mode 100644 index 0000000..3c7232c --- /dev/null +++ b/src/update.rs @@ -0,0 +1,256 @@ +//! `distro update` — check repology for newer upstream versions and bump +//! recipes in-place. +//! +//! For each requested recipe we query +//! `https://repology.org/api/v1/project/` and pick the highest version +//! among entries whose `status` is `"newest"` or `"unique"` (i.e. not +//! outdated, not rolling/devel, not ignored). If that version is strictly +//! greater than the recipe's current `version`, we rewrite: +//! +//! * `version = "..."` → the new upstream version +//! * `revision = N` → `revision = 1` +//! * every `"sha256": "..."` → `"sha256": "???"` so the next fetch fills it +//! +//! Repology asks API clients to set a descriptive User-Agent. + +use crate::log; +use crate::recipe::{Recipe, RecipeSet}; +use anyhow::{Context, Result, bail}; +use serde::Deserialize; +use std::cmp::Ordering; +use std::fs; +use std::path::Path; + +const REPOLOGY_API: &str = "https://repology.org/api/v1/project"; + +#[derive(Debug, Deserialize)] +struct RepologyEntry { + #[serde(default)] + version: String, + #[serde(default)] + status: String, +} + +pub fn run(recipes: &RecipeSet, names: &[String], bump: bool) -> Result<()> { + let targets: Vec<&Recipe> = if names.is_empty() { + recipes.recipes.values().collect() + } else { + let mut out = Vec::with_capacity(names.len()); + for name in names { + out.push(recipes.recipe_by_user_ref(name)?); + } + out + }; + + let client = reqwest::blocking::Client::builder() + .user_agent(concat!( + "distro/", + env!("CARGO_PKG_VERSION"), + " (+repology version checker)" + )) + .build()?; + + let mut outdated = 0usize; + let mut bumped = 0usize; + let mut up_to_date = 0usize; + let mut errored = 0usize; + + for recipe in targets { + match check_one(&client, recipe) { + Ok(Some(new_version)) => { + if bump { + bump_recipe(&recipe.path, &new_version)?; + log::step( + "bump", + &format!("{}: {} -> {}", recipe.key(), recipe.version, new_version), + ); + bumped += 1; + } else { + log::step( + "outdated", + &format!("{}: {} -> {}", recipe.key(), recipe.version, new_version), + ); + outdated += 1; + } + } + Ok(None) => { + log::skip( + "up-to-date", + &format!("{} {}", recipe.key(), recipe.version), + ); + up_to_date += 1; + } + Err(e) => { + log::info("error", &format!("{}: {e}", recipe.key())); + errored += 1; + } + } + } + + if bump { + log::step( + "summary", + &format!("{bumped} bumped, {up_to_date} up-to-date, {errored} errored"), + ); + } else { + log::step( + "summary", + &format!( + "{outdated} outdated, {up_to_date} up-to-date, {errored} errored (re-run with --bump to rewrite recipes)" + ), + ); + } + Ok(()) +} + +fn check_one(client: &reqwest::blocking::Client, recipe: &Recipe) -> Result> { + let url = format!("{REPOLOGY_API}/{}", recipe.name); + let resp = client + .get(&url) + .send() + .with_context(|| format!("GET {url}"))?; + if resp.status() == reqwest::StatusCode::NOT_FOUND { + bail!("repology has no project named `{}`", recipe.name); + } + let entries: Vec = serde_json::from_slice(&resp.error_for_status()?.bytes()?) + .context("failed to parse repology response")?; + let latest = entries + .iter() + .filter(|e| matches!(e.status.as_str(), "newest" | "unique")) + .map(|e| e.version.as_str()) + .max_by(|a, b| natural_cmp(a, b)); + match latest { + Some(v) if natural_cmp(v, &recipe.version) == Ordering::Greater => Ok(Some(v.to_owned())), + _ => Ok(None), + } +} + +fn bump_recipe(path: &Path, new_version: &str) -> Result<()> { + let original = + fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let mut out = String::with_capacity(original.len()); + let mut version_replaced = false; + let mut revision_replaced = false; + for line in original.split_inclusive('\n') { + let trimmed = line.trim_start(); + if !version_replaced && trimmed.starts_with("version") { + if let Some(replaced) = replace_string_assignment(line, "version", new_version) { + out.push_str(&replaced); + version_replaced = true; + continue; + } + } + if !revision_replaced && trimmed.starts_with("revision") { + if let Some(replaced) = replace_int_assignment(line, "revision", 1) { + out.push_str(&replaced); + revision_replaced = true; + continue; + } + } + out.push_str(line); + } + if !version_replaced { + bail!("could not find `version = \"...\"` in {}", path.display()); + } + // Reset every sha256 placeholder so the next fetch re-derives it. + out = out.replace("\"sha256\": \"", "\x00sha256_marker\x00\""); + out = regex_lite_replace_sha(&out); + out = out.replace("\x00sha256_marker\x00\"", "\"sha256\": \""); + fs::write(path, out).with_context(|| format!("failed to write {}", path.display()))?; + Ok(()) +} + +/// Replace every quoted sha256 value with `"???"`, leaving keys/whitespace +/// alone. Avoids a full regex dep by walking the string by hand. +fn regex_lite_replace_sha(input: &str) -> String { + let mut out = String::with_capacity(input.len()); + let bytes = input.as_bytes(); + let needle = b"\x00sha256_marker\x00\""; + let mut i = 0; + while i < bytes.len() { + if bytes[i..].starts_with(needle) { + out.push_str("\x00sha256_marker\x00\"???\""); + i += needle.len(); + // skip the original value up to the closing quote + while i < bytes.len() && bytes[i] != b'"' { + i += 1; + } + if i < bytes.len() { + i += 1; // consume the closing quote we replaced + } + } else { + out.push(bytes[i] as char); + i += 1; + } + } + out +} + +fn replace_string_assignment(line: &str, key: &str, new_value: &str) -> Option { + // Match `="value"` while preserving surrounding text + // (indentation, trailing newline). + let stripped = line.strip_prefix(key)?; + let rest = stripped.trim_start_matches(|c: char| c == ' ' || c == '\t'); + let after_eq = rest.strip_prefix('=')?.trim_start(); + let after_quote = after_eq.strip_prefix('"')?; + let end = after_quote.find('"')?; + let trailing = &after_quote[end + 1..]; + Some(format!("{key} = \"{new_value}\"{trailing}")) +} + +fn replace_int_assignment(line: &str, key: &str, new_value: i32) -> Option { + let stripped = line.strip_prefix(key)?; + let rest = stripped.trim_start_matches(|c: char| c == ' ' || c == '\t'); + let after_eq = rest.strip_prefix('=')?.trim_start(); + let end = after_eq.find(|c: char| !c.is_ascii_digit())?; + let trailing = &after_eq[end..]; + Some(format!("{key} = {new_value}{trailing}")) +} + +/// dpkg-ish natural comparison: split into runs of digits and non-digits and +/// compare numerically where both sides are digits, lexicographically +/// otherwise. Good enough for upstream tarball versions. +fn natural_cmp(a: &str, b: &str) -> Ordering { + let mut ai = a.chars().peekable(); + let mut bi = b.chars().peekable(); + loop { + match (ai.peek().copied(), bi.peek().copied()) { + (None, None) => return Ordering::Equal, + (None, _) => return Ordering::Less, + (_, None) => return Ordering::Greater, + (Some(x), Some(y)) if x.is_ascii_digit() && y.is_ascii_digit() => { + let mut na = String::new(); + while let Some(&c) = ai.peek() { + if c.is_ascii_digit() { + na.push(c); + ai.next(); + } else { + break; + } + } + let mut nb = String::new(); + while let Some(&c) = bi.peek() { + if c.is_ascii_digit() { + nb.push(c); + bi.next(); + } else { + break; + } + } + let xa: u64 = na.parse().unwrap_or(0); + let xb: u64 = nb.parse().unwrap_or(0); + match xa.cmp(&xb) { + Ordering::Equal => continue, + other => return other, + } + } + (Some(x), Some(y)) => match x.cmp(&y) { + Ordering::Equal => { + ai.next(); + bi.next(); + } + other => return other, + }, + } + } +}