Commit
Message
Changed Files (18)
-
modified .gitignore
diff --git a/.gitignore b/.gitignore index 2f7896d..147adbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ target/ +public/ -
modified Cargo.lock
diff --git a/Cargo.lock b/Cargo.lock index 5583d94..76d5813 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,3 +5,1714 @@ version = 4 [[package]] name = "abbaye2" version = "0.1.0" +dependencies = [ + "figment", + "flate2", + "ignore", + "miette", + "pulldown-cmark", + "semver", + "serde", + "serde_json", + "sha1", + "tar", + "tera", + "thiserror", + "tokio", + "toml 1.1.2+spec-1.1.0", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[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 = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[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.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +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 = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "chrono-tz" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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 = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "deunicode" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "pear", + "serde", + "toml 0.8.23", + "uncased", + "version_check", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[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.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "serde", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[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 = "parse-zoneinfo" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24" +dependencies = [ + "regex", +] + +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[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 = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", + "yansi", +] + +[[package]] +name = "pulldown-cmark" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[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", +] + +[[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", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[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", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[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 = "slug" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[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 = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tera" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8004bca281f2d32df3bacd59bc67b312cb4c70cea46cbd79dbe8ac5ed206722" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "unicode-segmentation", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[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", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[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-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[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 = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -
modified Cargo.toml
diff --git a/Cargo.toml b/Cargo.toml index a4cc79d..fc8fb36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,22 @@ name = "abbaye2" version = "0.1.0" authors = ["ololduck <ololduck@vit.am>"] license = "AGPL-3.0-or-later" +edition = "2024" [dependencies] +figment = { version = "0.10.19", features = ["env", "toml"] } +flate2 = "1" +ignore = "0.4" +miette = { version = "7.6.0", features = ["fancy", "serde"] } +pulldown-cmark = "0.12" +semver = "1" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1" +sha1 = "0.11.0" +tar = "0.4" +tera = "1" +thiserror = "2.0.18" +tokio = { version = "1.52.3", features = ["full"] } +toml = { version = "1.1.2", features = ["serde"] } +tracing = { version = "0.1.44", features = ["log", "log-always"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } -
added abbaye.toml
diff --git a/abbaye.toml b/abbaye.toml new file mode 100644 index 0000000..5aef144 --- /dev/null +++ b/abbaye.toml @@ -0,0 +1,17 @@ +[site] +name = "Abbaye2" + +[version_extractor] +type = "cargo" + +[changelog] + +[[builders]] +type = "cargo" + +[[builders]] +type = "cargo_doc" +no_deps = true + +[[builders]] +type = "archive" -
modified mise.toml
diff --git a/mise.toml b/mise.toml index 609bbd1..c2885e7 100644 --- a/mise.toml +++ b/mise.toml @@ -7,7 +7,7 @@ rust = "1.95" taplo = "latest" [env] -RUST_LOG = "trace" +RUST_LOG = "abbaye2=trace,info" [tasks] run = { alias = "r", run = "cargo run" } -
added source.tar.gz
diff --git a/source.tar.gz b/source.tar.gz new file mode 100644 index 0000000..2625cff Binary files /dev/null and b/source.tar.gz differ -
added src/builders/archive.rs
diff --git a/src/builders/archive.rs b/src/builders/archive.rs new file mode 100644 index 0000000..ec169eb --- /dev/null +++ b/src/builders/archive.rs @@ -0,0 +1,114 @@ +use std::{ + fs::File, + path::{Path, PathBuf}, +}; + +use flate2::{Compression, write::GzEncoder}; +use ignore::WalkBuilder; +use miette::{IntoDiagnostic, Result}; +use serde::Deserialize; + +use crate::builders::{ArtifactPath, Builder}; + +/// Configuration for [`ArchiveBuilder`]. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct ArchiveBuilderConfig { + /// Root directory to archive. Defaults to the current working directory. + pub source_dir: Option<PathBuf>, + + /// Output path for the generated `.tar.gz` archive. + /// Defaults to `source.tar.gz` in the current working directory. + pub output: Option<PathBuf>, + + /// Prefix applied to every entry path inside the archive. + /// For example, `"myproject-1.0.0"` produces entries like + /// `myproject-1.0.0/src/main.rs`. + /// Defaults to the source directory's name. + pub prefix: Option<String>, +} + +/// Creates a `.tar.gz` archive of the source tree, honouring all `.gitignore` +/// rules found in the directory hierarchy. +pub struct ArchiveBuilder; + +impl Builder for ArchiveBuilder { + type ConfigType = ArchiveBuilderConfig; + + async fn build(&self, config: Self::ConfigType) -> Result<Vec<ArtifactPath>> { + let source_dir = config + .source_dir + .unwrap_or_else(|| PathBuf::from(".")) + .canonicalize() + .into_diagnostic()?; + + let output = config + .output + .unwrap_or_else(|| PathBuf::from("source.tar.gz")); + + let prefix = config.prefix.unwrap_or_else(|| { + source_dir + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "source".to_owned()) + }); + + let archive_path = + tokio::task::spawn_blocking(move || create_archive(&source_dir, &output, &prefix)) + .await + .into_diagnostic()??; + + let name = archive_path + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_default(); + + let hash = Some(super::hash_file(&archive_path).await?); + + Ok(vec![ArtifactPath { + path: archive_path, + name, + hash, + }]) + } +} + +/// Walks `source_dir` respecting `.gitignore` rules and writes a `.tar.gz` +/// archive to `output`, prefixing every entry with `prefix`. +fn create_archive(source_dir: &Path, output: &Path, prefix: &str) -> Result<PathBuf> { + let file = File::create(output).into_diagnostic()?; + let encoder = GzEncoder::new(file, Compression::default()); + let mut archive = tar::Builder::new(encoder); + + for result in WalkBuilder::new(source_dir) + .hidden(false) // include dotfiles such as .rustfmt.toml + .build() + { + let entry = result.into_diagnostic()?; + let path = entry.path(); + + // Never descend into the .git directory. + if path.components().any(|c| c.as_os_str() == ".git") { + continue; + } + + if !path.is_file() { + continue; + } + + let relative = path.strip_prefix(source_dir).into_diagnostic()?; + let entry_path = Path::new(prefix).join(relative); + + archive + .append_path_with_name(path, &entry_path) + .into_diagnostic()?; + } + + // Finalise the tar stream, then flush and close the gzip layer. + archive + .into_inner() + .into_diagnostic()? + .finish() + .into_diagnostic()?; + + Ok(output.to_path_buf()) +} -
added src/builders/cargo.rs
diff --git a/src/builders/cargo.rs b/src/builders/cargo.rs new file mode 100644 index 0000000..f4e8b4a --- /dev/null +++ b/src/builders/cargo.rs @@ -0,0 +1,303 @@ +use std::{ + path::{Path, PathBuf}, + process::Stdio, +}; + +use miette::{IntoDiagnostic, Result, miette}; +use serde::Deserialize; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::Command; + +use super::hash_file; +use crate::builders::{ArtifactPath, Builder}; + +/// Configuration for [`CargoBuilder`]. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct CargoBuilderConfig { + /// Cargo target triples to build for (e.g. `"x86_64-unknown-linux-musl"`). + /// + /// Each entry is passed as `--target <triple>` in a separate `cargo build` + /// invocation. When the list is empty, cargo builds for the host target. + #[serde(default)] + pub targets: Vec<String>, + + /// Optional path to the Cargo.toml manifest. + /// + /// Passed verbatim as `--manifest-path`. Defaults to the manifest in the + /// current working directory when absent. + pub manifest_path: Option<PathBuf>, + + /// Restrict collected artifacts to these binary (or cdylib) target names. + /// + /// When empty every artifact produced by a **workspace member or local + /// path-dependency** is kept. Use this to avoid picking up extra binaries + /// from dev-tools or examples that live in the same workspace. + /// + /// ```toml + /// [[builders]] + /// type = "cargo" + /// bins = ["my_binary", "my_cdylib"] + /// ``` + #[serde(default)] + pub bins: Vec<String>, +} + +/// Runs `cargo build --release` and returns the produced artifacts. +pub struct CargoBuilder; + +impl Builder for CargoBuilder { + type ConfigType = CargoBuilderConfig; + + async fn build(&self, config: Self::ConfigType) -> Result<Vec<ArtifactPath>> { + let version = read_crate_version(config.manifest_path.as_deref()).await?; + + if config.targets.is_empty() { + let host = get_host_target().await?; + run_cargo_build(&config, None, &host, &version).await + } else { + let mut all_artifacts = Vec::new(); + for target in &config.targets { + let artifacts = + run_cargo_build(&config, Some(target.as_str()), target, &version).await?; + all_artifacts.extend(artifacts); + } + Ok(all_artifacts) + } + } +} + +// ── Internals ──────────────────────────────────────────────────────────────── + +/// Minimal representation of the JSON messages emitted by +/// `cargo build --message-format=json`. +#[derive(Deserialize)] +struct CargoMessage { + reason: String, + /// Identifies the crate that produced this artifact. + /// Local packages (workspace members and path-deps) always contain + /// `path+file://`; external registry/git crates do not. + package_id: Option<String>, + target: Option<CargoMessageTarget>, + filenames: Option<Vec<String>>, +} + +#[derive(Deserialize)] +struct CargoMessageTarget { + name: String, +} + +/// Spawn `cargo build --release --message-format=json [--target <triple>] +/// [--manifest-path <path>]` and collect every artifact path from the +/// `compiler-artifact` messages. +async fn run_cargo_build( + config: &CargoBuilderConfig, + target: Option<&str>, + triple: &str, + version: &str, +) -> Result<Vec<ArtifactPath>> { + let mut cmd = Command::new("cargo"); + cmd.args(["build", "--release", "--message-format=json"]); + + if let Some(t) = target { + cmd.args(["--target", t]); + } + + if let Some(manifest) = &config.manifest_path { + cmd.arg("--manifest-path").arg(manifest); + } + + let mut child = cmd + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) // let cargo's human-readable errors reach the terminal + .spawn() + .into_diagnostic()?; + + let stdout = child.stdout.take().expect("stdout was piped"); + let mut lines = BufReader::new(stdout).lines(); + + let mut artifacts = Vec::new(); + + while let Some(line) = lines.next_line().await.into_diagnostic()? { + let Ok(msg) = serde_json::from_str::<CargoMessage>(&line) else { + continue; + }; + + if msg.reason != "compiler-artifact" { + continue; + } + + // Skip artifacts from external (registry / git) dependencies. + // Both the old package_id format ("name ver (path+file://...)") and the + // newer spec format ("path+file://...#name@ver") contain "path+file://" + // for every local crate, so a substring check is version-agnostic. + if !msg + .package_id + .as_deref() + .is_some_and(|id| id.contains("path+file://")) + { + continue; + } + + // If the caller named specific targets, restrict to those. + if !config.bins.is_empty() { + let target_name = msg.target.as_ref().map(|t| t.name.as_str()).unwrap_or(""); + if !config.bins.iter().any(|b| b == target_name) { + continue; + } + } + + for filename in msg.filenames.unwrap_or_default() { + let path = PathBuf::from(&filename); + + // Skip rlib / rmeta files; we only want executables and cdylibs. + let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + if matches!(ext, "rlib" | "rmeta" | "d") { + continue; + } + + if !path.exists() { + continue; + } + + // Name the artifact as `{stem}-{version}-{triple}{ext}` so that + // binaries for different targets can coexist in the same dist dir. + let stem = path + .file_stem() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + let dot_ext = path + .extension() + .map(|e| format!(".{}", e.to_string_lossy())) + .unwrap_or_default(); + let name = format!("{stem}-{version}-{triple}{dot_ext}"); + + let hash = Some(hash_file(&path).await?); + + artifacts.push(ArtifactPath { path, name, hash }); + } + } + + let status = child.wait().await.into_diagnostic()?; + + if !status.success() { + return Err(miette!( + "cargo build --release failed with exit status: {status}" + )); + } + + Ok(artifacts) +} + +/// Query `rustc -vV` and return the host target triple +/// (e.g. `"x86_64-unknown-linux-gnu"`). +async fn get_host_target() -> Result<String> { + let output = Command::new("rustc") + .args(["-vV"]) + .output() + .await + .into_diagnostic()?; + + if !output.status.success() { + return Err(miette!("rustc -vV failed")); + } + + let stdout = String::from_utf8(output.stdout).into_diagnostic()?; + stdout + .lines() + .find(|l| l.starts_with("host:")) + .and_then(|l| l.split_whitespace().nth(1)) + .map(str::to_owned) + .ok_or_else(|| miette!("could not parse host triple from `rustc -vV` output")) +} + +/// Read `[package].version` from the Cargo.toml at `manifest_path` +/// (defaults to `Cargo.toml` in the current directory). +async fn read_crate_version(manifest_path: Option<&Path>) -> Result<String> { + let path = manifest_path.unwrap_or(Path::new("Cargo.toml")); + let content = tokio::fs::read_to_string(path).await.into_diagnostic()?; + + #[derive(Deserialize)] + struct Manifest { + package: Option<Package>, + } + #[derive(Deserialize)] + struct Package { + version: Option<String>, + } + + let manifest: Manifest = toml::from_str(&content).into_diagnostic()?; + manifest + .package + .ok_or_else(|| miette!("{} has no [package] section", path.display()))? + .version + .ok_or_else(|| miette!("no version field in [package] in {}", path.display())) +} + +// ── CargoDocBuilder ────────────────────────────────────────────────────────── + +/// Configuration for [`CargoDocBuilder`]. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct CargoDocBuilderConfig { + /// Optional path to the Cargo.toml manifest. + /// + /// Passed verbatim as `--manifest-path`. Defaults to the manifest in the + /// current working directory when absent. + pub manifest_path: Option<PathBuf>, + + /// Skip building documentation for dependencies (`--no-deps`). + #[serde(default)] + pub no_deps: bool, +} + +/// Runs `cargo doc` and returns the `index.html` entry point for each +/// documented crate as an artifact. +pub struct CargoDocBuilder; + +impl Builder for CargoDocBuilder { + type ConfigType = CargoDocBuilderConfig; + + async fn build(&self, config: Self::ConfigType) -> Result<Vec<ArtifactPath>> { + let mut cmd = Command::new("cargo"); + cmd.arg("doc"); + + if config.no_deps { + cmd.arg("--no-deps"); + } + + if let Some(manifest) = &config.manifest_path { + cmd.arg("--manifest-path").arg(manifest); + } + + let status = cmd + .stderr(Stdio::inherit()) + .status() + .await + .into_diagnostic()?; + + if !status.success() { + return Err(miette!("cargo doc failed with exit status: {status}")); + } + + // Resolve the doc output directory. When a manifest path is given the + // workspace root is its parent directory; otherwise fall back to CWD. + let doc_dir = config + .manifest_path + .as_deref() + .and_then(|p| p.parent()) + .unwrap_or_else(|| std::path::Path::new(".")) + .join("target/doc"); + + if !doc_dir.exists() { + return Err(miette!("doc directory not found at {}", doc_dir.display())); + } + + // Return the entire target/doc tree as a single artifact so that the + // shared rustdoc assets (CSS, JS, fonts, search indices) that live at + // the root of target/doc/ are preserved alongside the per-crate HTML. + Ok(vec![ArtifactPath { + path: doc_dir, + name: "doc".to_owned(), + hash: None, + }]) + } +} -
added src/builders/mod.rs
diff --git a/src/builders/mod.rs b/src/builders/mod.rs new file mode 100644 index 0000000..1d78541 --- /dev/null +++ b/src/builders/mod.rs @@ -0,0 +1,86 @@ +use miette::{IntoDiagnostic, Result}; +use serde::Deserialize; +use sha1::{Digest, Sha1}; +use std::path::PathBuf; + +pub mod archive; +pub mod cargo; + +use archive::{ArchiveBuilder, ArchiveBuilderConfig}; +use cargo::{CargoBuilder, CargoBuilderConfig, CargoDocBuilder, CargoDocBuilderConfig}; + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AnyBuilder { + /// Creates a `.tar.gz` archive of the source tree, automatically excluding + /// files and directories matched by any `.gitignore` found in the hierarchy. + /// + /// ```toml + /// [[builders]] + /// type = "archive" + /// source_dir = "." # optional, defaults to CWD + /// output = "myproject-1.0.0.tar.gz" # optional, defaults to source.tar.gz + /// prefix = "myproject-1.0.0" # optional, defaults to source_dir name + /// ``` + Archive(ArchiveBuilderConfig), + + /// Compiles the crate in release mode with `cargo build --release`. + /// One or more target triples can be specified for cross-compilation; + /// omitting `targets` builds for the host platform. + /// + /// ```toml + /// [[builders]] + /// type = "cargo" + /// targets = ["x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl"] + /// manifest_path = "Cargo.toml" # optional + /// ``` + Cargo(CargoBuilderConfig), + + /// Generates API documentation with `cargo doc`. + /// Returns the per-crate doc directory (e.g. `target/doc/my_crate`) as an + /// artifact so it can be published or archived by a later pipeline step. + /// + /// ```toml + /// [[builders]] + /// type = "cargo_doc" + /// no_deps = true # optional, skip dependency docs + /// manifest_path = "Cargo.toml" # optional + /// ``` + CargoDoc(CargoDocBuilderConfig), +} + +impl AnyBuilder { + pub async fn build(&self) -> Result<Vec<ArtifactPath>> { + match self { + Self::Archive(config) => ArchiveBuilder.build(config.clone()).await, + Self::Cargo(config) => CargoBuilder.build(config.clone()).await, + Self::CargoDoc(config) => CargoDocBuilder.build(config.clone()).await, + } + } +} + +/// Read the file at `path` and return its SHA1 digest as a lowercase hex string. +async fn hash_file(path: &PathBuf) -> Result<String> { + let contents = tokio::fs::read(path).await.into_diagnostic()?; + let mut hasher = Sha1::new(); + hasher.update(&contents); + Ok(hasher + .finalize() + .iter() + .map(|b| format!("{b:02x}")) + .collect()) +} + +pub struct ArtifactPath { + pub path: PathBuf, + pub name: String, + /// Lowercase hexadecimal SHA1 digest of the artifact's contents, if computed. + pub hash: Option<String>, +} + +#[allow(async_fn_in_trait)] +pub trait Builder { + type ConfigType: Default + for<'de> Deserialize<'de> + Clone; + + async fn build(&self, config: Self::ConfigType) -> Result<Vec<ArtifactPath>>; +} -
added src/changelog.rs
diff --git a/src/changelog.rs b/src/changelog.rs new file mode 100644 index 0000000..65b51c3 --- /dev/null +++ b/src/changelog.rs @@ -0,0 +1,184 @@ +use std::path::PathBuf; + +use miette::{IntoDiagnostic, Result, miette}; +use serde::Deserialize; + +/// Configuration for [`ChangelogExtractor`]. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct ChangelogConfig { + /// Directory that contains the `CHANGELOG.md`. + /// Defaults to the current working directory. + pub directory: Option<PathBuf>, +} + +/// Reads version information from a `CHANGELOG.md` file. +/// +/// Implements [`VersionExtractor`] by returning the latest (topmost) version +/// heading found in the file. Use [`ChangelogExtractor::section`] to retrieve +/// the full release notes for a specific version. +pub struct ChangelogExtractor; + +impl ChangelogExtractor { + /// Returns the latest (topmost) version found in the changelog. + pub async fn latest_version(&self, config: ChangelogConfig) -> Result<String> { + let changelog = Changelog::load(config).await?; + changelog + .latest_version() + .map(str::to_owned) + .ok_or_else(|| miette!("no version headings found in CHANGELOG.md")) + } + + /// Returns the body of the changelog section for `version`. + /// + /// `version` should match the string as it appears in the heading, without + /// brackets (e.g. `"1.2.0"` matches both `## [1.2.0]` and `## 1.2.0`). + pub async fn section(&self, config: ChangelogConfig, version: &str) -> Result<String> { + let changelog = Changelog::load(config).await?; + changelog + .section(version) + .map(str::to_owned) + .ok_or_else(|| miette!("version {version} not found in CHANGELOG.md")) + } +} + +// ── Parsing ─────────────────────────────────────────────────────────────────── + +struct ChangelogSection { + version: String, + body: String, +} + +struct Changelog { + sections: Vec<ChangelogSection>, +} + +impl Changelog { + async fn load(config: ChangelogConfig) -> Result<Self> { + let path = config + .directory + .unwrap_or_else(|| PathBuf::from(".")) + .join("CHANGELOG.md"); + + let content = tokio::fs::read_to_string(&path).await.into_diagnostic()?; + + Ok(Self::parse(&content)) + } + + fn parse(content: &str) -> Self { + let mut sections: Vec<ChangelogSection> = Vec::new(); + let mut current_version: Option<String> = None; + let mut current_body: Vec<&str> = Vec::new(); + + for line in content.lines() { + if let Some(version) = parse_version_heading(line) { + // Flush the previous section before starting a new one. + if let Some(ver) = current_version.take() { + sections.push(ChangelogSection { + version: ver, + body: current_body.join("\n").trim().to_owned(), + }); + current_body.clear(); + } + current_version = Some(version); + } else if current_version.is_some() { + current_body.push(line); + } + } + + // Flush the final section. + if let Some(ver) = current_version { + sections.push(ChangelogSection { + version: ver, + body: current_body.join("\n").trim().to_owned(), + }); + } + + Self { sections } + } + + /// Returns the version string from the first (latest) section. + fn latest_version(&self) -> Option<&str> { + self.sections.first().map(|s| s.version.as_str()) + } + + /// Returns the body of the section whose version matches `version`. + fn section(&self, version: &str) -> Option<&str> { + self.sections + .iter() + .find(|s| s.version == version) + .map(|s| s.body.as_str()) + } +} + +/// Parses an `##`-level Markdown heading and returns the version string, or +/// `None` if the heading does not look like a version entry. +/// +/// Recognised formats: +/// - `## [1.2.0] - 2024-01-15` (Keep a Changelog bracketed) +/// - `## [1.2.0]` (bracketed, no date) +/// - `## 1.2.0 - 2024-01-15` (unbracketed with date) +/// - `## 1.2.0` (unbracketed, no date) +fn parse_version_heading(line: &str) -> Option<String> { + let rest = line.strip_prefix("## ")?; + let version = if rest.starts_with('[') { + let end = rest.find(']')?; + &rest[1..end] + } else { + // Everything before an optional ` - <date>` suffix. + rest.split(" - ").next()?.trim() + }; + if version.is_empty() { + None + } else { + Some(version.to_owned()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE: &str = "\ +# Changelog + +## [1.2.0] - 2024-03-01 + +### Added +- New feature + +## [1.1.0] - 2024-01-15 + +### Fixed +- Bug fix + +## 1.0.0 + +Initial release. +"; + + #[test] + fn latest_version_is_first_heading() { + let cl = Changelog::parse(SAMPLE); + assert_eq!(cl.latest_version(), Some("1.2.0")); + } + + #[test] + fn section_body_is_trimmed() { + let cl = Changelog::parse(SAMPLE); + let body = cl.section("1.2.0").unwrap(); + assert!(body.starts_with("### Added")); + assert!(body.ends_with("- New feature")); + } + + #[test] + fn unbracketed_heading_is_parsed() { + let cl = Changelog::parse(SAMPLE); + assert!(cl.section("1.0.0").is_some()); + } + + #[test] + fn unknown_version_returns_none() { + let cl = Changelog::parse(SAMPLE); + assert!(cl.section("9.9.9").is_none()); + } +} -
added src/config.rs
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ec9838d --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +use std::path::PathBuf; + +use serde::Deserialize; + +use crate::{ + builders::AnyBuilder, changelog::ChangelogConfig, version_extractors::AnyVersionExtractor, +}; + +/// General website metadata. +#[derive(Debug, Deserialize)] +pub struct SiteConfig { + /// Display name of the project, used in page titles and headings. + pub name: String, + /// Path to the README file rendered on each version page. + /// Defaults to `README.md` in the current working directory. + pub readme: Option<PathBuf>, +} + +#[derive(Debug, Deserialize)] +pub struct Abbaye2Config { + pub site: SiteConfig, + pub version_extractor: AnyVersionExtractor, + pub changelog: ChangelogConfig, + pub builders: Vec<AnyBuilder>, + #[serde(default = "abbaye2_output_dir")] + pub output_dir: Option<PathBuf>, +} + +fn abbaye2_output_dir() -> Option<PathBuf> { + Some(PathBuf::from("public")) +} -
modified src/main.rs
diff --git a/src/main.rs b/src/main.rs index e7a11a9..ac5becd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,48 @@ -fn main() { - println!("Hello, world!"); +//! # Abbaye +//! +//! Abbaye is a Static Site Generator (SSG) for your software. As GitHub, +//! Gitea, Forgejo and consorts offer, Abbaye can be used to generate a +//! website with your software's presentation, documentation, and distribution, per version. +//! +//! Here's an example file structure: +//! +//! ```text +//! . +//! ├── index.html # the main page of the website, enabling choosing a version, defaults to "latest" (contains a list of available versions and a iframe to the selected version?) +//! ├── latest -> v2.0.0 # symlink to the latest version (biggest version number) +//! ├── v1.0.0/ # the directory containing the version 1.0.0 of the software +//! │ ├── index.html # the main page of the version 1.0.0, from the README.md file. +//! │ │ # Contains a sidebar with links to the documentation and distribution. +//! │ │ # After the readme content, A changelog is displayed. +//! │ ├── docs/ # the directory containing the documentation of the version 1.0.0 +//! │ │ ├── index.html # the main page of the documentation of the version 1.0.0 +//! │ │ └── … +//! │ ├── docs.tar.gz # the tarball containing the documentation of the version 1.0.0 +//! │ └── dist/ # the directory containing the distribution of the version 1.0.0 +//! │ ├── source.tgz # the source code of the version 1.0.0 +//! │ ├── mybin-v1.0.0-x86_64-unknown-linux-gnu +//! │ └── mybin-v1.0.0-x86_64-unknown-linux-musl +//! └── v2.0.0/ # the directory containing the version 2.0.0 of the software +//! ├── index.html # the main page of the version 2.0.0 +//! ├── … +//! └── … +//! ``` + +use miette::Result; + +pub mod builders; +pub mod changelog; +pub mod config; +pub mod site; +pub mod version_extractors; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_timer(tracing_subscriber::fmt::time::SystemTime) + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); + + let config = site::load_config()?; + site::build_site(config).await } -
added src/site.rs
diff --git a/src/site.rs b/src/site.rs new file mode 100644 index 0000000..45c8027 --- /dev/null +++ b/src/site.rs @@ -0,0 +1,373 @@ +use std::{ + future::Future, + path::{Path, PathBuf}, + pin::Pin, +}; + +use figment::{ + Figment, + providers::{Format, Toml}, +}; +use flate2::{Compression, write::GzEncoder}; +use miette::{IntoDiagnostic, Result}; +use pulldown_cmark::{Options, Parser, html}; +use tera::{Context, Tera}; +use tracing::warn; + +use crate::{changelog::ChangelogExtractor, config::Abbaye2Config}; + +const TEMPLATE_ROOT_INDEX: &str = include_str!("templates/root_index.html"); +const TEMPLATE_VERSION_INDEX: &str = include_str!("templates/version_index.html"); + +// ── Public API ──────────────────────────────────────────────────────────────── + +/// Load the Abbaye2 configuration from the current working directory. +/// +/// Looks for `.abbaye.toml` first, then `abbaye.toml`; when both are present +/// `abbaye.toml` takes precedence (last merge wins). +pub fn load_config() -> Result<Abbaye2Config> { + let cwd = std::env::current_dir().into_diagnostic()?; + Figment::new() + .merge(Toml::file(cwd.join(".abbaye.toml"))) + .merge(Toml::file(cwd.join("abbaye.toml"))) + .extract() + .into_diagnostic() +} + +/// Build the full website into `config.output_dir` (defaults to `public/`). +/// +/// # Steps +/// +/// 1. Extract the current version via the configured [`AnyVersionExtractor`]. +/// 2. Run every configured builder and split the resulting [`ArtifactPath`]s +/// into *dist* (regular files) and *docs* (directories). +/// 3. Copy dist artifacts to `<output>/<version>/dist/`. +/// 4. Copy doc directories to `<output>/<version>/docs/<crate>/` and archive +/// the whole `docs/` tree as `docs.tar.gz`. +/// 5. Render `<output>/<version>/index.html` from the project README and the +/// matching changelog section. +/// 6. Re-render the root `<output>/index.html`, listing all known versions +/// (newest first). +/// 7. Update the `<output>/latest` symlink (Unix) or redirect page (other). +pub async fn build_site(config: Abbaye2Config) -> Result<()> { + let output_dir = config + .output_dir + .clone() + .unwrap_or_else(|| PathBuf::from("public")); + + tokio::fs::create_dir_all(&output_dir) + .await + .into_diagnostic()?; + + // ── 1. Version ──────────────────────────────────────────────────────────── + let version = config.version_extractor.extract().await?; + + // ── 2. Tera setup ───────────────────────────────────────────────────────── + let mut tera = Tera::default(); + tera.add_raw_template("root_index.html", TEMPLATE_ROOT_INDEX) + .into_diagnostic()?; + tera.add_raw_template("version_index.html", TEMPLATE_VERSION_INDEX) + .into_diagnostic()?; + + // ── 3. Builders ─────────────────────────────────────────────────────────── + let mut dist_artifacts = Vec::new(); + let mut doc_artifacts = Vec::new(); + + for builder in &config.builders { + for artifact in builder.build().await? { + if artifact.path.is_dir() { + doc_artifacts.push(artifact); + } else { + dist_artifacts.push(artifact); + } + } + } + + // ── 4. Lay out dist/ ────────────────────────────────────────────────────── + let version_dir = output_dir.join(&version); + let dist_dir = version_dir.join("dist"); + tokio::fs::create_dir_all(&dist_dir) + .await + .into_diagnostic()?; + + for artifact in &dist_artifacts { + tokio::fs::copy(&artifact.path, dist_dir.join(&artifact.name)) + .await + .into_diagnostic()?; + } + + let dist_file_names: Vec<String> = dist_artifacts.iter().map(|a| a.name.clone()).collect(); + let has_dist = !dist_file_names.is_empty(); + + // ── 5. Lay out docs/ ────────────────────────────────────────────────────── + let has_docs = !doc_artifacts.is_empty(); + let has_docs_tarball; + + if has_docs { + let docs_dir = version_dir.join("docs"); + + for artifact in &doc_artifacts { + // Copy the complete target/doc tree — this includes the shared + // rustdoc CSS, JS, fonts and search indices at the root of the + // directory in addition to the per-crate HTML subdirectories. + copy_dir_recursive(artifact.path.clone(), docs_dir.clone()).await?; + } + + // Rustdoc writes index.html at the root for single-crate projects. + // For workspaces it may be absent; generate a fallback listing then. + if !docs_dir.join("index.html").exists() { + let crate_names = find_doc_crates(&docs_dir).await?; + write_docs_index(&docs_dir, &crate_names).await?; + } + + let tarball = version_dir.join("docs.tar.gz"); + let docs_dir_c = docs_dir.clone(); + let tarball_c = tarball.clone(); + tokio::task::spawn_blocking(move || archive_dir(&docs_dir_c, &tarball_c)) + .await + .into_diagnostic()??; + + has_docs_tarball = true; + } else { + has_docs_tarball = false; + } + + // ── 6. README ───────────────────────────────────────────────────────────── + let readme_path = config + .site + .readme + .as_deref() + .unwrap_or(Path::new("README.md")); + + let readme_html = match tokio::fs::read_to_string(readme_path).await { + Ok(content) => render_markdown(&content), + Err(_) => { + warn!("README not found at {}", readme_path.display()); + String::new() + } + }; + + // ── 7. Changelog section ────────────────────────────────────────────────── + let changelog_html = match ChangelogExtractor + .section(config.changelog.clone(), &version) + .await + { + Ok(section) => render_markdown(§ion), + Err(_) => { + warn!("No changelog entry found for version {version}"); + String::new() + } + }; + + // ── 8. Version index.html ───────────────────────────────────────────────── + let mut version_ctx = Context::new(); + version_ctx.insert("project_name", &config.site.name); + version_ctx.insert("version", &version); + version_ctx.insert("readme_html", &readme_html); + version_ctx.insert("changelog_html", &changelog_html); + version_ctx.insert("has_docs", &has_docs); + version_ctx.insert("has_docs_tarball", &has_docs_tarball); + version_ctx.insert("has_dist", &has_dist); + version_ctx.insert("dist_files", &dist_file_names); + + let version_html = tera + .render("version_index.html", &version_ctx) + .into_diagnostic()?; + tokio::fs::write(version_dir.join("index.html"), version_html) + .await + .into_diagnostic()?; + + // ── 9. Root index.html ──────────────────────────────────────────────────── + // Collect every version that already has an index.html in the output dir, + // then merge in the current version and sort newest-first. + let mut versions = list_existing_versions(&output_dir).await?; + if !versions.contains(&version) { + versions.push(version.clone()); + } + versions.sort_by(|a, b| compare_versions(b, a)); + + let mut root_ctx = Context::new(); + root_ctx.insert("project_name", &config.site.name); + root_ctx.insert("versions", &versions); + + let root_html = tera + .render("root_index.html", &root_ctx) + .into_diagnostic()?; + tokio::fs::write(output_dir.join("index.html"), root_html) + .await + .into_diagnostic()?; + + // ── 10. `latest` symlink ────────────────────────────────────────────────── + if let Some(latest) = versions.first() { + update_latest_symlink(&output_dir, latest)?; + } + + Ok(()) +} + +// ── Private helpers ─────────────────────────────────────────────────────────── + +/// Render Markdown to an HTML string. +fn render_markdown(md: &str) -> String { + let opts = Options::ENABLE_TABLES | Options::ENABLE_STRIKETHROUGH; + let parser = Parser::new_ext(md, opts); + let mut buf = String::new(); + html::push_html(&mut buf, parser); + buf +} + +/// Scan `docs_dir` for subdirectories that contain an `index.html` and return +/// their names. Used to build a fallback listing when rustdoc itself did not +/// generate a root `index.html` (typical for multi-crate workspaces). +async fn find_doc_crates(docs_dir: &Path) -> Result<Vec<String>> { + let mut names = Vec::new(); + let mut entries = tokio::fs::read_dir(docs_dir).await.into_diagnostic()?; + while let Some(entry) = entries.next_entry().await.into_diagnostic()? { + let path = entry.path(); + if path.is_dir() && path.join("index.html").exists() { + names.push(entry.file_name().to_string_lossy().into_owned()); + } + } + Ok(names) +} + +/// Write a `docs/index.html` that either redirects straight to the single +/// crate's docs (one crate) or lists all crates (multiple crates). +async fn write_docs_index(docs_dir: &Path, crate_names: &[String]) -> Result<()> { + let html = if crate_names.len() == 1 { + format!( + "<!DOCTYPE html><html><head>\ + <meta http-equiv=\"refresh\" content=\"0;url={}/index.html\">\ + </head></html>", + crate_names[0] + ) + } else { + let items = crate_names + .iter() + .map(|n| format!(" <li><a href=\"{n}/index.html\">{n}</a></li>")) + .collect::<Vec<_>>() + .join("\n"); + format!( + "<!DOCTYPE html>\n<html><head><meta charset=\"utf-8\">\ + <title>Documentation</title></head>\ + <body><h1>Documentation</h1><ul>\n{items}\n</ul></body></html>" + ) + }; + + tokio::fs::write(docs_dir.join("index.html"), html) + .await + .into_diagnostic() +} + +/// Recursively copy the contents of `src` into `dst`. +/// +/// Uses explicit boxing to satisfy the compiler for the async recursive call. +fn copy_dir_recursive( + src: PathBuf, + dst: PathBuf, +) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> { + Box::pin(async move { + tokio::fs::create_dir_all(&dst).await.into_diagnostic()?; + let mut entries = tokio::fs::read_dir(&src).await.into_diagnostic()?; + while let Some(entry) = entries.next_entry().await.into_diagnostic()? { + let src_path = entry.path(); + let dst_path = dst.join(entry.file_name()); + if src_path.is_dir() { + copy_dir_recursive(src_path, dst_path).await?; + } else { + tokio::fs::copy(&src_path, &dst_path) + .await + .into_diagnostic()?; + } + } + Ok(()) + }) +} + +/// Pack `src` directory into a `.tar.gz` archive at `dest`. +/// +/// The top-level entry inside the archive is named after the source directory. +fn archive_dir(src: &Path, dest: &Path) -> Result<()> { + let dir_name = src + .file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "docs".to_owned()); + + let file = std::fs::File::create(dest).into_diagnostic()?; + let enc = GzEncoder::new(file, Compression::default()); + let mut archive = tar::Builder::new(enc); + archive.append_dir_all(&dir_name, src).into_diagnostic()?; + archive + .into_inner() + .into_diagnostic()? + .finish() + .into_diagnostic()?; + Ok(()) +} + +/// Scan `output_dir` for subdirectories that contain an `index.html`, +/// excluding the `latest` symlink/directory. +async fn list_existing_versions(output_dir: &Path) -> Result<Vec<String>> { + let mut versions = Vec::new(); + let mut entries = tokio::fs::read_dir(output_dir).await.into_diagnostic()?; + while let Some(entry) = entries.next_entry().await.into_diagnostic()? { + let name = entry.file_name().to_string_lossy().into_owned(); + if name == "latest" { + continue; + } + let path = entry.path(); + if path.is_dir() && path.join("index.html").exists() { + versions.push(name); + } + } + Ok(versions) +} + +/// Compare two version strings, preferring semver ordering and falling back +/// to lexicographic comparison for non-semver strings (e.g. git describe output). +fn strip_v(s: &str) -> &str { + s.strip_prefix('v').unwrap_or(s) +} + +fn compare_versions(a: &str, b: &str) -> std::cmp::Ordering { + match ( + semver::Version::parse(strip_v(a)), + semver::Version::parse(strip_v(b)), + ) { + (Ok(va), Ok(vb)) => va.cmp(&vb), + _ => a.cmp(b), + } +} + +/// Create or replace the `latest` symlink in `output_dir`, pointing to +/// `version_dir_name`. +/// +/// On non-Unix platforms a meta-refresh redirect page is written instead. +fn update_latest_symlink(output_dir: &Path, version_dir_name: &str) -> Result<()> { + let link = output_dir.join("latest"); + + #[cfg(unix)] + { + // Remove any stale symlink or file before (re-)creating it. + if link.exists() || link.is_symlink() { + std::fs::remove_file(&link).into_diagnostic()?; + } + std::os::unix::fs::symlink(version_dir_name, &link).into_diagnostic()?; + } + + #[cfg(not(unix))] + { + std::fs::create_dir_all(&link).into_diagnostic()?; + std::fs::write( + link.join("index.html"), + format!( + "<!DOCTYPE html><html><head>\ + <meta http-equiv=\"refresh\" content=\"0;url=../{version_dir_name}/\">\ + </head></html>" + ), + ) + .into_diagnostic()?; + } + + Ok(()) +} -
added src/templates/root_index.html
diff --git a/src/templates/root_index.html b/src/templates/root_index.html new file mode 100644 index 0000000..0d43e10 --- /dev/null +++ b/src/templates/root_index.html @@ -0,0 +1,106 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{{ project_name }}</title> + <style> + *, + *::before, + *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + background: #f5efe4; + color: #2e2416; + line-height: 1.6; + } + header { + background: #3d5732; + color: #f0e8d8; + padding: 1.5rem 2rem; + } + header h1 { + font-size: 1.75rem; + font-weight: 700; + letter-spacing: 0.01em; + } + main { + max-width: 760px; + margin: 2.5rem auto; + padding: 0 1.5rem; + } + h2 { + font-size: 0.75rem; + font-weight: 700; + color: #7a6855; + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 1rem; + } + ul { + list-style: none; + } + li { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: #fdfaf5; + border: 1px solid #c9baa8; + border-radius: 5px; + margin-bottom: 0.5rem; + } + li:hover { + border-color: #a06020; + } + a.version-link { + font-size: 1.05rem; + font-weight: 500; + color: #7a4429; + text-decoration: none; + } + a.version-link:hover { + text-decoration: underline; + color: #a05a3a; + } + .badge-latest { + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.07em; + background: #a06020; + color: #fdf6ec; + padding: 0.2em 0.6em; + border-radius: 999px; + } + </style> + </head> + <body> + <header> + <h1>{{ project_name }}</h1> + </header> + <main> + <h2>Versions</h2> + <ul> + {% for version in versions %} + <li> + <a class="version-link" href="{{ version }}/" + >{{ version }}</a + > + {% if loop.first %}<span class="badge-latest">latest</span + >{% endif %} + </li> + {% endfor %} + </ul> + </main> + </body> +</html> -
added src/templates/version_index.html
diff --git a/src/templates/version_index.html b/src/templates/version_index.html new file mode 100644 index 0000000..15ae928 --- /dev/null +++ b/src/templates/version_index.html @@ -0,0 +1,243 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>{{ project_name }} — {{ version }}</title> + <style> + *, + *::before, + *::after { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + sans-serif; + color: #2e2416; + line-height: 1.6; + } + /* ── Header ── */ + header { + background: #3d5732; + color: #f0e8d8; + padding: 0.75rem 1.5rem; + display: flex; + align-items: center; + gap: 1.5rem; + } + header a { + color: #c8bbaa; + text-decoration: none; + font-size: 0.9rem; + } + header a:hover { + text-decoration: underline; + color: #f0e8d8; + } + header .title { + font-weight: 600; + font-size: 1rem; + } + /* ── Layout ── */ + .layout { + display: flex; + min-height: calc(100vh - 44px); + } + /* ── Sidebar ── */ + aside { + width: 230px; + flex-shrink: 0; + background: #ede5d8; + border-right: 1px solid #c9baa8; + padding: 1.5rem 1rem; + font-size: 0.875rem; + } + aside h3 { + font-size: 0.65rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: #7a6855; + margin-top: 1.5rem; + margin-bottom: 0.5rem; + } + aside h3:first-child { + margin-top: 0; + } + aside ul { + list-style: none; + } + aside li { + padding: 0.25rem 0; + } + aside a { + color: #7a4429; + text-decoration: none; + } + aside a:hover { + text-decoration: underline; + color: #a05a3a; + } + /* ── Main content ── */ + main { + flex: 1; + padding: 2rem 2.5rem; + max-width: 860px; + overflow-x: hidden; + background: #fdfaf5; + } + /* ── Markdown typography ── */ + main h1, + main h2, + main h3, + main h4, + main h5, + main h6 { + margin-top: 1.5em; + margin-bottom: 0.5em; + line-height: 1.3; + color: #2e2416; + } + main h1 { + font-size: 1.9rem; + border-bottom: 2px solid #c9baa8; + padding-bottom: 0.4rem; + } + main h2 { + font-size: 1.4rem; + border-bottom: 1px solid #ddd4c4; + padding-bottom: 0.3rem; + } + main h3 { + font-size: 1.15rem; + } + main p { + margin-bottom: 1em; + } + main a { + color: #7a4429; + } + main a:hover { + color: #a05a3a; + } + main img { + max-width: 100%; + height: auto; + } + main pre { + background: #eae2d4; + border: 1px solid #c9baa8; + border-radius: 5px; + padding: 1em 1.25em; + overflow-x: auto; + margin-bottom: 1em; + font-size: 0.875em; + } + main code { + font-family: + "SFMono-Regular", Consolas, "Liberation Mono", Menlo, + monospace; + background: #eae2d4; + padding: 0.15em 0.4em; + border-radius: 3px; + font-size: 0.875em; + } + main pre code { + background: none; + padding: 0; + font-size: inherit; + } + main ul, + main ol { + padding-left: 1.5em; + margin-bottom: 1em; + } + main li { + margin-bottom: 0.2em; + } + main table { + border-collapse: collapse; + margin-bottom: 1em; + width: 100%; + } + main th, + main td { + border: 1px solid #c9baa8; + padding: 0.4em 0.75em; + text-align: left; + } + main th { + background: #ede5d8; + font-weight: 600; + } + main blockquote { + border-left: 4px solid #c9baa8; + padding: 0.5em 1em; + margin: 0 0 1em; + color: #7a6855; + font-style: italic; + } + /* ── Changelog section ── */ + .changelog-divider { + border: none; + border-top: 2px solid #c9baa8; + margin: 2.5rem 0; + } + .changelog-heading { + font-size: 1.3rem; + font-weight: 600; + color: #5a4030; + margin-bottom: 1rem; + } + /* ── Responsive ── */ + @media (max-width: 640px) { + .layout { + flex-direction: column; + } + aside { + width: 100%; + border-right: none; + border-bottom: 1px solid #c9baa8; + } + } + </style> + </head> + <body> + <header> + <a href="../">← All versions</a> + <span class="title">{{ project_name }} — {{ version }}</span> + </header> + <div class="layout"> + <aside> + {% if has_docs %} + <h3>Documentation</h3> + <ul> + <li><a href="docs/">📖 API docs</a></li> + {% if has_docs_tarball %} + <li><a href="docs.tar.gz">⬇ docs.tar.gz</a></li> + {% endif %} + </ul> + {% endif %} {% if has_dist %} + <h3>Downloads</h3> + <ul> + {% for file in dist_files %} + <li><a href="dist/{{ file }}">⬇ {{ file }}</a></li> + {% endfor %} + </ul> + {% endif %} + </aside> + <main> + {{ readme_html | safe }} + <hr class="changelog-divider" /> + <h2 class="changelog-heading">Changes in {{ version }}</h2> + {{ changelog_html | safe }} + </main> + </div> + </body> +</html> -
added src/version_extractors/cargo.rs
diff --git a/src/version_extractors/cargo.rs b/src/version_extractors/cargo.rs new file mode 100644 index 0000000..bf13a26 --- /dev/null +++ b/src/version_extractors/cargo.rs @@ -0,0 +1,48 @@ +use std::path::PathBuf; + +use miette::{IntoDiagnostic, Result, miette}; +use serde::Deserialize; + +use super::VersionExtractor; + +/// Configuration for [`CargoVersion`]. +#[derive(Debug, Default, Clone, Deserialize)] +pub struct CargoVersionConfig { + /// Path to the `Cargo.toml` to read the version from. + /// Defaults to `Cargo.toml` in the current working directory. + pub manifest_path: Option<PathBuf>, +} + +/// Extracts the package version by reading it directly from `Cargo.toml`. +pub struct CargoVersion; + +// Internal types used only for TOML parsing. +#[derive(Deserialize)] +struct CargoManifest { + package: Option<CargoPackage>, +} + +#[derive(Deserialize)] +struct CargoPackage { + version: Option<String>, +} + +impl VersionExtractor for CargoVersion { + type ConfigType = CargoVersionConfig; + + async fn extract(&self, config: Self::ConfigType) -> Result<String> { + let path = config + .manifest_path + .unwrap_or_else(|| PathBuf::from("Cargo.toml")); + + let content = tokio::fs::read_to_string(&path).await.into_diagnostic()?; + + let manifest: CargoManifest = toml::from_str(&content).into_diagnostic()?; + + manifest + .package + .ok_or_else(|| miette!("{} has no [package] section", path.display()))? + .version + .ok_or_else(|| miette!("no version field in [package] in {}", path.display())) + } +} -
added src/version_extractors/git.rs
diff --git a/src/version_extractors/git.rs b/src/version_extractors/git.rs new file mode 100644 index 0000000..75a148b --- /dev/null +++ b/src/version_extractors/git.rs @@ -0,0 +1,63 @@ +use miette::{IntoDiagnostic, Result, miette}; +use serde::Deserialize; + +use super::VersionExtractor; + +fn default_dirty_suffix() -> String { + "-dirty".to_owned() +} + +/// Configuration for [`GitVersion`]. +#[derive(Debug, Clone, Deserialize)] +pub struct GitVersionConfig { + /// Strip this prefix from the tag name before returning the version. + /// For example, `"v"` turns `"v1.2.3"` into `"1.2.3"`. + pub tag_prefix: Option<String>, + + /// Suffix appended to the version when the working tree has uncommitted + /// changes. Forwarded verbatim as `--dirty=<suffix>` to `git describe`. + /// Defaults to `"-dirty"`. + #[serde(default = "default_dirty_suffix")] + pub dirty_suffix: String, +} + +impl Default for GitVersionConfig { + fn default() -> Self { + Self { + tag_prefix: None, + dirty_suffix: default_dirty_suffix(), + } + } +} + +/// Extracts the version by running `git describe --tags --always`. +pub struct GitVersion; + +impl VersionExtractor for GitVersion { + type ConfigType = GitVersionConfig; + + async fn extract(&self, config: Self::ConfigType) -> Result<String> { + let dirty_arg = format!("--dirty={}", config.dirty_suffix); + + let output = tokio::process::Command::new("git") + .args(["describe", "--tags", "--always", &dirty_arg]) + .output() + .await + .into_diagnostic()?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(miette!("git describe failed:\n{stderr}")); + } + + let raw = String::from_utf8(output.stdout).into_diagnostic()?; + let version = raw.trim(); + + let version = match &config.tag_prefix { + Some(prefix) => version.strip_prefix(prefix.as_str()).unwrap_or(version), + None => version, + }; + + Ok(version.to_owned()) + } +} -
added src/version_extractors/mod.rs
diff --git a/src/version_extractors/mod.rs b/src/version_extractors/mod.rs new file mode 100644 index 0000000..5f621a0 --- /dev/null +++ b/src/version_extractors/mod.rs @@ -0,0 +1,51 @@ +use miette::Result; +use serde::Deserialize; + +pub mod cargo; +pub mod git; + +use cargo::{CargoVersion, CargoVersionConfig}; +use git::{GitVersion, GitVersionConfig}; + +#[allow(async_fn_in_trait)] +pub trait VersionExtractor { + type ConfigType: Default + for<'de> Deserialize<'de> + Clone; + + async fn extract(&self, config: Self::ConfigType) -> Result<String>; +} + +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum AnyVersionExtractor { + /// Reads the version from the `version` field in the `[package]` section + /// of a `Cargo.toml` file. + /// + /// ```toml + /// [version_extractor] + /// type = "cargo" + /// manifest_path = "Cargo.toml" # optional, defaults to ./Cargo.toml + /// ``` + Cargo(CargoVersionConfig), + + /// Derives the version from the most recent Git tag using + /// `git describe --tags --always`. + /// Supports stripping a tag prefix (e.g. `"v"`) and customising the + /// suffix appended when the working tree has uncommitted changes. + /// + /// ```toml + /// [version_extractor] + /// type = "git" + /// tag_prefix = "v" # optional, strips leading "v" + /// dirty_suffix = "-dev" # optional, defaults to "-dirty" + /// ``` + Git(GitVersionConfig), +} + +impl AnyVersionExtractor { + pub async fn extract(&self) -> Result<String> { + match self { + Self::Cargo(config) => CargoVersion.extract(config.clone()).await, + Self::Git(config) => GitVersion.extract(config.clone()).await, + } + } +}