From 3a5024abd38cb2657b446298e50cd8e234c379ef Mon Sep 17 00:00:00 2001 From: Rabindra Dhakal Date: Tue, 28 Jul 2026 14:29:45 +0545 Subject: [PATCH] refactor!: replace the SBUILD toolchain with the port format Nothing invokes lint, info or meta since the package tree became declarative, so those commands and the linter and metadata modules are gone. What replaces them is sbuild port: validate, new, resolve, hashfill, meta and audit. audit is the one that earns its place: it downloads each archive and checks every install path resolves inside it, which static checks cannot do and which found six wrong maps. 11687 lines across 4 crates -> ~1900 in 1. --- Cargo.lock | 4227 +++++++------------------ Cargo.toml | 54 +- sbuild-cache/CHANGELOG.md | 20 - sbuild-cache/Cargo.toml | 22 - sbuild-cache/src/error.rs | 27 - sbuild-cache/src/export.rs | 80 - sbuild-cache/src/lib.rs | 19 - sbuild-cache/src/models.rs | 205 -- sbuild-cache/src/mongo.rs | 771 ----- sbuild-cache/src/schema.rs | 151 - sbuild-cache/src/sqlite.rs | 807 ----- sbuild-linter/CHANGELOG.md | 58 - sbuild-linter/Cargo.toml | 20 - sbuild-linter/src/build_config/mod.rs | 231 -- sbuild-linter/src/categories | 143 - sbuild-linter/src/comments.rs | 64 - sbuild-linter/src/description.rs | 37 - sbuild-linter/src/error.rs | 56 - sbuild-linter/src/lib.rs | 387 --- sbuild-linter/src/logger.rs | 152 - sbuild-linter/src/semaphore.rs | 29 - sbuild-linter/src/validator.rs | 919 ------ sbuild-linter/src/xexec.rs | 80 - sbuild-meta/CHANGELOG.md | 40 - sbuild-meta/Cargo.toml | 25 - sbuild-meta/src/error.rs | 48 - sbuild-meta/src/hash.rs | 116 - sbuild-meta/src/lib.rs | 22 - sbuild-meta/src/manifest.rs | 229 -- sbuild-meta/src/metadata.rs | 544 ---- sbuild-meta/src/recipe.rs | 772 ----- sbuild-meta/src/registry.rs | 377 --- sbuild/Cargo.toml | 35 - sbuild/src/builder.rs | 969 ------ sbuild/src/checksum.rs | 158 - sbuild/src/cleanup.rs | 158 - sbuild/src/commands/build.rs | 1100 ------- sbuild/src/commands/cache.rs | 871 ----- sbuild/src/commands/info.rs | 188 -- sbuild/src/commands/lint.rs | 199 -- sbuild/src/commands/meta.rs | 638 ---- sbuild/src/commands/mod.rs | 5 - sbuild/src/constant.rs | 17 - sbuild/src/ghcr.rs | 262 -- sbuild/src/lib.rs | 176 - sbuild/src/main.rs | 64 - sbuild/src/onelf.rs | 207 -- sbuild/src/signing.rs | 181 -- sbuild/src/types.rs | 37 - sbuild/src/utils.rs | 267 -- sbuild/tests/onelf_extract.rs | 67 - src/commands/mod.rs | 1 + src/commands/port.rs | 301 ++ src/lib.rs | 7 + src/main.rs | 33 + src/port/audit.rs | 198 ++ src/port/hashfill.rs | 223 ++ src/port/meta.rs | 288 ++ src/port/mod.rs | 11 + src/port/model.rs | 210 ++ src/port/new.rs | 101 + src/port/resolve.rs | 349 ++ src/port/tree.rs | 75 + src/port/validate.rs | 80 + 64 files changed, 3017 insertions(+), 15191 deletions(-) delete mode 100644 sbuild-cache/CHANGELOG.md delete mode 100644 sbuild-cache/Cargo.toml delete mode 100644 sbuild-cache/src/error.rs delete mode 100644 sbuild-cache/src/export.rs delete mode 100644 sbuild-cache/src/lib.rs delete mode 100644 sbuild-cache/src/models.rs delete mode 100644 sbuild-cache/src/mongo.rs delete mode 100644 sbuild-cache/src/schema.rs delete mode 100644 sbuild-cache/src/sqlite.rs delete mode 100644 sbuild-linter/CHANGELOG.md delete mode 100644 sbuild-linter/Cargo.toml delete mode 100644 sbuild-linter/src/build_config/mod.rs delete mode 100644 sbuild-linter/src/categories delete mode 100644 sbuild-linter/src/comments.rs delete mode 100644 sbuild-linter/src/description.rs delete mode 100644 sbuild-linter/src/error.rs delete mode 100644 sbuild-linter/src/lib.rs delete mode 100644 sbuild-linter/src/logger.rs delete mode 100644 sbuild-linter/src/semaphore.rs delete mode 100644 sbuild-linter/src/validator.rs delete mode 100644 sbuild-linter/src/xexec.rs delete mode 100644 sbuild-meta/CHANGELOG.md delete mode 100644 sbuild-meta/Cargo.toml delete mode 100644 sbuild-meta/src/error.rs delete mode 100644 sbuild-meta/src/hash.rs delete mode 100644 sbuild-meta/src/lib.rs delete mode 100644 sbuild-meta/src/manifest.rs delete mode 100644 sbuild-meta/src/metadata.rs delete mode 100644 sbuild-meta/src/recipe.rs delete mode 100644 sbuild-meta/src/registry.rs delete mode 100644 sbuild/Cargo.toml delete mode 100644 sbuild/src/builder.rs delete mode 100644 sbuild/src/checksum.rs delete mode 100644 sbuild/src/cleanup.rs delete mode 100644 sbuild/src/commands/build.rs delete mode 100644 sbuild/src/commands/cache.rs delete mode 100644 sbuild/src/commands/info.rs delete mode 100644 sbuild/src/commands/lint.rs delete mode 100644 sbuild/src/commands/meta.rs delete mode 100644 sbuild/src/commands/mod.rs delete mode 100644 sbuild/src/constant.rs delete mode 100644 sbuild/src/ghcr.rs delete mode 100644 sbuild/src/lib.rs delete mode 100644 sbuild/src/main.rs delete mode 100644 sbuild/src/onelf.rs delete mode 100644 sbuild/src/signing.rs delete mode 100644 sbuild/src/types.rs delete mode 100644 sbuild/src/utils.rs delete mode 100644 sbuild/tests/onelf_extract.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/port.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/port/audit.rs create mode 100644 src/port/hashfill.rs create mode 100644 src/port/meta.rs create mode 100644 src/port/mod.rs create mode 100644 src/port/model.rs create mode 100644 src/port/new.rs create mode 100644 src/port/resolve.rs create mode 100644 src/port/tree.rs create mode 100644 src/port/validate.rs diff --git a/Cargo.lock b/Cargo.lock index 658198b..28d5665 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,34 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.3", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -39,17 +11,11 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -62,15 +28,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -86,27 +52,15 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "arrayref" version = "0.3.9" @@ -115,20 +69,9 @@ checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" [[package]] name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-trait" -version = "0.1.89" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "atomic-waker" @@ -136,17 +79,11 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - [[package]] name = "aws-lc-rs" -version = "1.15.4" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -154,49 +91,15 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", "dunce", "fs_extra", -] - -[[package]] -name = "backhand" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "125b16dea79c81d8c8bf0af7398170f729d00ab8c7bd8197df03b62669eb148d" -dependencies = [ - "deku", - "flate2", - "liblzma", - "lz4_flex", - "no_std_io2", - "rayon", - "solana-nohash-hasher", - "thiserror 2.0.18", - "tracing", - "xxhash-rust", - "zstd", - "zstd-safe", -] - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", + "pkg-config", ] [[package]] @@ -207,95 +110,50 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" - -[[package]] -name = "bitvec" -version = "1.0.1" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "blake3" -version = "1.8.2" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", "cc", "cfg-if", "constant_time_eq", + "cpufeatures", ] [[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 = "bson" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3f109694c4f45353972af96bf97d8a057f82e2d6e496457f4d135b9867a518c" -dependencies = [ - "ahash", - "base64", - "bitvec", - "chrono", - "getrandom 0.3.3", - "hex", - "indexmap", - "js-sys", - "rand", - "serde", - "serde_bytes", - "simdutf8", - "thiserror 2.0.18", - "time", - "uuid", -] - -[[package]] -name = "bstr" -version = "1.12.1" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ - "memchr", - "regex-automata", - "serde", + "hybrid-array", ] [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytes" -version = "1.10.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.56" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" dependencies = [ "find-msvc-tools", "jobserver", @@ -303,39 +161,34 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] -name = "chrono" -version = "0.4.41" +name = "chacha20" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ - "num-traits", - "serde", + "cfg-if", + "cpufeatures", + "rand_core", ] [[package]] name = "clap" -version = "4.5.53" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "d91e0c145792ef73a6ad36d27c75ac09f1832222a3c209689d90f534685ee5b7" dependencies = [ "clap_builder", "clap_derive", @@ -343,9 +196,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -355,36 +208,36 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 3.0.3", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -406,39 +259,16 @@ dependencies = [ ] [[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" +name = "const-oid" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "tiny-keccak", -] +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - -[[package]] -name = "convert_case" -version = "0.10.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" [[package]] name = "core-foundation" @@ -458,3503 +288,1730 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.17" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] -name = "crc32fast" -version = "1.4.2" +name = "crypto-common" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ - "cfg-if", + "hybrid-array", ] [[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" +name = "digest" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "crossbeam-utils", + "block-buffer", + "const-oid", + "crypto-common", ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "displaydoc" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "crossbeam-epoch" -version = "0.9.18" +name = "dunce" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "crunchy" -version = "0.2.4" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "fastrand" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] -name = "darling" -version = "0.21.3" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core", - "darling_macro", -] +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] -name = "darling_core" -version = "0.21.3" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "darling_macro" -version = "0.21.3" +name = "form_urlencoded" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ - "darling_core", - "quote", - "syn", + "percent-encoding", ] [[package]] -name = "data-encoding" -version = "2.10.0" +name = "fs_extra" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] -name = "deku" -version = "0.20.3" +name = "futures" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebf55291257a2a5c90cf50ae17b6bbaabc3fd13642cf3895a71c412513c19630" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ - "bitvec", - "deku_derive", - "no_std_io2", - "rustversion", + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] -name = "deku_derive" -version = "0.20.3" +name = "futures-channel" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec2a42b511fc5efd9183f4f71c17885d627b17e7fd9a61a92089406caa4397e" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ - "darling", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", + "futures-core", + "futures-sink", ] [[package]] -name = "deranged" -version = "0.5.8" +name = "futures-core" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] -name = "derive-syn-parse" -version = "0.2.0" +name = "futures-executor" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65d7ce8132b7c0e54497a4d9a55a1c2a0912a0d786cf894472ba818fba45762" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ - "proc-macro2", - "quote", - "syn", + "futures-core", + "futures-task", + "futures-util", ] [[package]] -name = "derive-where" -version = "1.6.0" +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] -name = "derive_more" -version = "2.1.1" +name = "futures-sink" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] -name = "derive_more-impl" -version = "2.1.1" +name = "futures-task" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", -] +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] -name = "digest" -version = "0.10.7" +name = "futures-util" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ - "block-buffer", - "crypto-common", - "subtle", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", ] [[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encoding_rs" -version = "0.8.35" +name = "getrandom" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", ] [[package]] -name = "enum-as-inner" -version = "0.6.1" +name = "glob" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] -name = "env_filter" -version = "1.0.0" +name = "h2" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ - "log", - "regex", + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", ] [[package]] -name = "env_home" -version = "0.1.0" +name = "hashbrown" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] -name = "env_logger" -version = "0.11.9" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "equivalent" -version = "1.0.2" +name = "http" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] [[package]] -name = "errno" -version = "0.3.12" +name = "http-body" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ - "libc", - "windows-sys 0.59.0", + "bytes", + "http", ] [[package]] -name = "fallible-iterator" -version = "0.3.0" +name = "http-body-util" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] [[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" +name = "httparse" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] -name = "fastrand" -version = "2.3.0" +name = "hybrid-array" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "hyper" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] [[package]] -name = "flate2" -version = "1.1.1" +name = "hyper-rustls" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "crc32fast", - "libz-rs-sys", - "miniz_oxide", + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "hyper-util" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] [[package]] -name = "foldhash" -version = "0.1.5" +name = "icu_collections" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] [[package]] -name = "foldhash" -version = "0.2.0" +name = "icu_locale_core" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "icu_normalizer" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ - "percent-encoding", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", ] [[package]] -name = "fs_extra" -version = "1.3.0" +name = "icu_normalizer_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] [[package]] -name = "funty" -version = "2.0.0" +name = "icu_properties_data" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] -name = "futures" -version = "0.3.31" +name = "icu_provider" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", ] [[package]] -name = "futures-channel" -version = "0.3.31" +name = "idna" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ - "futures-core", - "futures-sink", + "idna_adapter", + "smallvec", + "utf8_iter", ] [[package]] -name = "futures-core" -version = "0.3.31" +name = "idna_adapter" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] [[package]] -name = "futures-executor" -version = "0.3.31" +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "equivalent", + "hashbrown", ] [[package]] -name = "futures-io" -version = "0.3.31" +name = "ipnet" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "futures-macro" -version = "0.3.31" +name = "is_terminal_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] -name = "futures-sink" -version = "0.3.31" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "futures-task" -version = "0.3.31" +name = "jni" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] [[package]] -name = "futures-util" -version = "0.3.31" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", ] [[package]] -name = "generic-array" -version = "0.14.7" +name = "jni-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" dependencies = [ - "typenum", - "version_check", + "jni-sys-macros", ] [[package]] -name = "getrandom" -version = "0.2.16" +name = "jni-sys-macros" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "wasm-bindgen", + "quote", + "syn 2.0.119", ] [[package]] -name = "getrandom" -version = "0.3.3" +name = "jobserver" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" dependencies = [ - "cfg-if", - "js-sys", + "getrandom 0.4.3", "libc", - "r-efi 5.3.0", - "wasi 0.14.2+wasi-0.2.4", - "wasm-bindgen", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "js-sys" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", + "futures-util", + "wasm-bindgen", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "libc" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] -name = "glob" -version = "0.3.2" +name = "linux-raw-sys" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] -name = "goblin" -version = "0.10.5" +name = "litemap" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9" -dependencies = [ - "log", - "plain", - "scroll", -] +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] -name = "h2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.3", -] - -[[package]] -name = "hashlink" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hickory-proto" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" -dependencies = [ - "async-trait", - "cfg-if", - "data-encoding", - "enum-as-inner", - "futures-channel", - "futures-io", - "futures-util", - "idna", - "ipnet", - "once_cell", - "rand", - "ring", - "thiserror 2.0.18", - "tinyvec", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "hickory-resolver" -version = "0.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" -dependencies = [ - "cfg-if", - "futures-util", - "hickory-proto", - "ipconfig", - "moka", - "once_cell", - "parking_lot", - "rand", - "resolv-conf", - "smallvec", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http", - "http-body", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "itoa", - "pin-project-lite", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" -dependencies = [ - "http", - "hyper", - "hyper-util", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http", - "http-body", - "hyper", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" - -[[package]] -name = "icu_properties" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "potential_utf", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" - -[[package]] -name = "icu_provider" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" -dependencies = [ - "displaydoc", - "icu_locale_core", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "ipconfig" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" -dependencies = [ - "socket2 0.5.10", - "widestring", - "windows-sys 0.48.0", - "winreg", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jiff" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.182" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" - -[[package]] -name = "liblzma" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6033b77c21d1f56deeae8014eb9fbe7bdf1765185a6c508b5ca82eeaed7f899" -dependencies = [ - "liblzma-sys", - "num_cpus", -] - -[[package]] -name = "liblzma-sys" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2db66f3268487b5033077f266da6777d057949b8f93c8ad82e441df25e6186" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "libsqlite3-sys" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libz-rs-sys" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c10501e7805cee23da17c7790e59df2870c0d4043ec6d03f67d31e2b53e77415" -dependencies = [ - "zlib-rs", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" - -[[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.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "lru" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8cc7106155f10bdf99a6f379688f543ad6596a415375b36a59a054ceda1198" -dependencies = [ - "hashbrown 0.15.3", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "lz4_flex" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab6473172471198271ff72e9379150e9dfd70d8e533e0752a27e515b48dd375e" - -[[package]] -name = "macro_magic" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc33f9f0351468d26fbc53d9ce00a096c8522ecb42f19b50f34f2c422f76d21d" -dependencies = [ - "macro_magic_core", - "macro_magic_macros", - "quote", - "syn", -] - -[[package]] -name = "macro_magic_core" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1687dc887e42f352865a393acae7cf79d98fab6351cde1f58e9e057da89bf150" -dependencies = [ - "const-random", - "derive-syn-parse", - "macro_magic_core_macros", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "macro_magic_core_macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b02abfe41815b5bd98dbd4260173db2c116dda171dc0fe7838cb206333b83308" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "macro_magic_macros" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ea28ee64b88876bf45277ed9a5817c1817df061a74f2b988971a12570e5869" -dependencies = [ - "macro_magic_core", - "quote", - "syn", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "memmap2" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" -dependencies = [ - "libc", -] - -[[package]] -name = "miniz_oxide" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" -dependencies = [ - "libc", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", -] - -[[package]] -name = "moka" -version = "0.12.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "mongocrypt" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da0cd419a51a5fb44819e290fbdb0665a54f21dead8923446a799c7f4d26ad9" -dependencies = [ - "bson", - "mongocrypt-sys", - "once_cell", - "serde", -] - -[[package]] -name = "mongocrypt-sys" -version = "0.1.5+1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224484c5d09285a7b8cb0a0c117e847ebd14cb6e4470ecf68cdb89c503b0edb9" - -[[package]] -name = "mongodb" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803dd859e8afa084c255a8effd8000ff86f7c8076a50cd6d8c99e8f3496f75c2" -dependencies = [ - "base64", - "bitflags", - "bson", - "derive-where", - "derive_more", - "futures-core", - "futures-io", - "futures-util", - "hex", - "hickory-proto", - "hickory-resolver", - "hmac", - "macro_magic", - "md-5", - "mongocrypt", - "mongodb-internal-macros", - "pbkdf2", - "percent-encoding", - "rand", - "rustc_version_runtime", - "rustls", - "rustversion", - "serde", - "serde_bytes", - "serde_with", - "sha1", - "sha2", - "socket2 0.6.2", - "stringprep", - "strsim", - "take_mut", - "thiserror 2.0.18", - "tokio", - "tokio-rustls", - "tokio-util", - "typed-builder", - "uuid", - "webpki-roots", -] - -[[package]] -name = "mongodb-internal-macros" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973ef3dd3dbc6f6e65bbdecfd9ec5e781b9e7493b0f369a7c62e35d8e5ae2c8" -dependencies = [ - "macro_magic", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "no_std_io2" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" -dependencies = [ - "memchr", -] - -[[package]] -name = "num-conv" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "onelf-format" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ea6fc6dc882b7a99192c1100fc10b2375b5f1df330b30b8d443063db76d7b4" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "ordered-float" -version = "5.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" -dependencies = [ - "num-traits", -] - -[[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 = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", -] - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "positioned-io" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ec4b80060f033312b99b6874025d9503d2af87aef2dd4c516e253fbfcdada7" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "potential_utf" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quinn" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2 0.5.10", - "thiserror 2.0.18", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.3", - "lru-slab", - "rand", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.18", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4e529991f949c5e25755532370b8af5d114acae52326361d68d47af64aa842" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.5.10", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.3", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "reqwest" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" -dependencies = [ - "base64", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "serde", - "serde_json", - "sync_wrapper", - "tokio", - "tokio-rustls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", -] - -[[package]] -name = "resolv-conf" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "rsqlite-vfs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", -] - -[[package]] -name = "rusqlite" -version = "0.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink 0.11.0", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustc_version_runtime" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dd18cd2bae1820af0b6ad5e54f4a51d0f3fcc53b05f845675074efcc7af071d" -dependencies = [ - "rustc_version", - "semver", -] - -[[package]] -name = "rustix" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.23.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[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 = "saphyr" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3767dfe8889ebb55a21409df2b6f36e66abfbe1eb92d64ff76ae799d3f91016" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink 0.10.0", - "ordered-float", - "saphyr-parser", -] - -[[package]] -name = "saphyr-parser" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb771b59f6b1985d1406325ec28f97cfb14256abcec4fdfb37b36a1766d6af7" -dependencies = [ - "arraydeque", - "hashlink 0.10.0", -] - -[[package]] -name = "sbuild" -version = "0.2.0" -dependencies = [ - "blake3", - "chrono", - "clap", - "colored", - "env_logger", - "futures", - "glob", - "goblin", - "log", - "memmap2", - "onelf-format", - "reqwest", - "saphyr", - "sbuild-cache", - "sbuild-linter", - "sbuild-meta", - "serde", - "serde_json", - "sha2", - "squishy", - "tempfile", - "thiserror 2.0.18", - "tokio", - "which", - "zstd", -] - -[[package]] -name = "sbuild-cache" -version = "0.1.0" -dependencies = [ - "bson", - "chrono", - "mongodb", - "rusqlite", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", -] - -[[package]] -name = "sbuild-linter" -version = "0.5.0" -dependencies = [ - "colored", - "indexmap", - "saphyr", - "tempfile", - "url", - "which", -] - -[[package]] -name = "sbuild-meta" -version = "0.1.0" -dependencies = [ - "blake3", - "glob", - "log", - "regex", - "reqwest", - "rusqlite", - "saphyr", - "semver", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "scroll" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc4f90c27b57691bbaf11d8ecc7cfbfe98a4da6dbe60226115d322aa80c06e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "security-framework" -version = "3.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321c8673b092a9a42605034a9879d73cb79101ed5fd117bc9a597b89b4e9e61a" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[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_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[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.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "indexmap", - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" -dependencies = [ - "serde_core", - "serde_with_macros", -] - -[[package]] -name = "serde_with_macros" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[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 = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "solana-nohash-hasher" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" - -[[package]] -name = "sqlite-wasm-rs" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] - -[[package]] -name = "squishy" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4b346e9ccc1a5034f30f300a2a9227ebe85ca4cc6ee68d5eda2fbeb4879fd9" -dependencies = [ - "backhand", - "bstr", - "goblin", - "lru", - "positioned-io", - "rayon", - "serde", - "sha2", - "thiserror 2.0.18", - "xxhash-rust", - "zerocopy", - "zstd-safe", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "take_mut" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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 = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.1" +name = "log" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" -dependencies = [ - "displaydoc", - "zerovec", -] +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] -name = "tinyvec" -version = "1.9.0" +name = "lru-slab" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "tinyvec_macros" -version = "0.1.1" +name = "memchr" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] -name = "tokio" -version = "1.45.1" +name = "mio" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ - "backtrace", - "bytes", "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.5.10", - "tokio-macros", - "windows-sys 0.52.0", -] - -[[package]] -name = "tokio-macros" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", + "wasi", + "windows-sys 0.61.2", ] [[package]] -name = "toml_parser" -version = "1.0.9+spec-1.1.0" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" -dependencies = [ - "winnow", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "tower" -version = "0.5.2" +name = "once_cell_polyfill" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags", - "bytes", - "futures-util", - "http", - "http-body", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "tower-layer" -version = "0.3.3" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "tower-service" -version = "0.3.3" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "tracing" -version = "0.1.41" +name = "pkg-config" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", + "zerovec", ] [[package]] -name = "tracing-attributes" -version = "0.1.28" +name = "proc-macro2" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ - "proc-macro2", - "quote", - "syn", + "unicode-ident", ] [[package]] -name = "tracing-core" -version = "0.1.33" +name = "quinn" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ - "once_cell", + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", ] [[package]] -name = "try-lock" -version = "0.2.5" +name = "quinn-proto" +version = "0.11.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] [[package]] -name = "typed-builder" -version = "0.22.0" +name = "quinn-udp" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "398a3a3c918c96de527dc11e6e846cd549d4508030b8a33e1da12789c856b81a" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" dependencies = [ - "typed-builder-macro", + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", ] [[package]] -name = "typed-builder-macro" -version = "0.22.0" +name = "quote" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e48cea23f68d1f78eb7bc092881b6bb88d3d6b5b7e6234f6f9c911da1ffb221" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", - "quote", - "syn", ] [[package]] -name = "typenum" -version = "1.19.0" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "unicode-bidi" -version = "0.3.18" +name = "rand" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "rand_core" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] -name = "unicode-normalization" -version = "0.1.25" +name = "rand_pcg" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" dependencies = [ - "tinyvec", + "rand_core", ] [[package]] -name = "unicode-properties" -version = "0.1.4" +name = "regex" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] [[package]] -name = "unicode-segmentation" -version = "1.12.0" +name = "regex-automata" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] [[package]] -name = "unicode-xid" -version = "0.2.6" +name = "regex-syntax" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] -name = "untrusted" -version = "0.9.0" +name = "reqwest" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] [[package]] -name = "url" -version = "2.5.4" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "utf8_iter" -version = "1.0.4" +name = "rustc-hash" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] -name = "utf8parse" -version = "0.2.2" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] -name = "uuid" -version = "1.22.0" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "getrandom 0.4.2", - "js-sys", - "serde_core", - "wasm-bindgen", + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", ] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "rustls" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] [[package]] -name = "version_check" -version = "0.9.5" +name = "rustls-native-certs" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] [[package]] -name = "walkdir" -version = "2.5.0" +name = "rustls-pki-types" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ - "same-file", - "winapi-util", + "web-time", + "zeroize", ] [[package]] -name = "want" -version = "0.3.1" +name = "rustls-platform-verifier" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ - "try-lock", + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", ] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "rustls-platform-verifier-android" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "rustls-webpki" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "wit-bindgen-rt", + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" +name = "rustversion" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "wit-bindgen", + "winapi-util", ] [[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +name = "sbuild" +version = "0.2.0" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "blake3", + "clap", + "colored", + "futures", + "glob", + "indexmap", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2", + "tempfile", + "tokio", + "toml", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.58" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "cfg-if", - "futures-util", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", + "windows-sys 0.61.2", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "quote", - "wasm-bindgen-macro-support", + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "core-foundation-sys", + "libc", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "serde" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ - "leb128fmt", - "wasmparser", + "serde_core", + "serde_derive", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "serde_core" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "serde_derive", ] [[package]] -name = "wasm-streams" -version = "0.5.0" +name = "serde_derive" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "serde_json" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ - "bitflags", - "hashbrown 0.15.3", - "indexmap", - "semver", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] -name = "web-sys" -version = "0.3.85" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "js-sys", - "wasm-bindgen", + "serde_core", ] [[package]] -name = "web-time" -version = "1.1.0" +name = "sha2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ - "js-sys", - "wasm-bindgen", + "cfg-if", + "cpufeatures", + "digest", ] [[package]] -name = "webpki-root-certs" -version = "1.0.6" +name = "shlex" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] -name = "webpki-roots" -version = "1.0.6" +name = "simd_cesu8" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ - "rustls-pki-types", + "rustc_version", + "simdutf8", ] [[package]] -name = "which" -version = "8.0.0" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" -dependencies = [ - "env_home", - "rustix", - "winsafe", -] +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] -name = "widestring" -version = "1.2.1" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] -name = "winapi" -version = "0.3.9" +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "stable_deref_trait" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] -name = "winapi-util" -version = "0.1.11" +name = "strsim" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] -name = "windows-link" -version = "0.2.1" +name = "syn" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] [[package]] -name = "windows-sys" -version = "0.45.0" +name = "syn" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" dependencies = [ - "windows-targets 0.42.2", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "sync_wrapper" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" dependencies = [ - "windows-targets 0.48.5", + "futures-core", ] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "synstructure" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "windows-sys" -version = "0.59.0" +name = "tempfile" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "windows-targets 0.52.6", + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", ] [[package]] -name = "windows-sys" -version = "0.60.2" +name = "thiserror" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "windows-targets 0.53.5", + "thiserror-impl", ] [[package]] -name = "windows-sys" -version = "0.61.2" +name = "thiserror-impl" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ - "windows-link", + "proc-macro2", + "quote", + "syn 3.0.3", ] [[package]] -name = "windows-targets" -version = "0.42.2" +name = "tinystr" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "displaydoc", + "zerovec", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "tinyvec" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "tinyvec_macros", ] [[package]] -name = "windows-targets" -version = "0.52.6" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", ] [[package]] -name = "windows-targets" -version = "0.53.5" +name = "tokio-macros" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" +name = "tokio-rustls" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "tokio-util" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" +name = "toml" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" +name = "toml_parser" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +name = "toml_writer" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" +name = "tower" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" +name = "tower-http" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] [[package]] -name = "windows_i686_gnu" -version = "0.42.2" +name = "tower-layer" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] -name = "windows_i686_gnu" -version = "0.48.5" +name = "tower-service" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] -name = "windows_i686_gnu" -version = "0.52.6" +name = "tracing" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] [[package]] -name = "windows_i686_gnu" -version = "0.53.1" +name = "tracing-core" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "try-lock" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" +name = "typenum" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] -name = "windows_i686_msvc" -version = "0.42.2" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "windows_i686_msvc" -version = "0.48.5" +name = "untrusted" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "url" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "utf8_iter" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" +name = "utf8parse" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "walkdir" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +name = "wasm-bindgen" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "wasm-bindgen-futures" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wasm-bindgen-macro" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "wasm-bindgen-macro-support" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" +name = "wasm-bindgen-shared" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "wasm-streams" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "web-sys" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] -name = "winnow" -version = "0.7.14" +name = "webpki-root-certs" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ - "memchr", + "rustls-pki-types", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "winapi-util" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] -name = "winsafe" -version = "0.0.19" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "windows-sys" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "wit-bindgen-rust-macro", + "windows-targets", ] [[package]] -name = "wit-bindgen-core" -version = "0.51.0" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "anyhow", - "heck", - "wit-parser", + "windows-link", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "bitflags", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] -name = "wit-component" -version = "0.244.0" +name = "windows_i686_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "wit-parser" -version = "0.244.0" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "writeable" -version = "0.6.1" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "wyz" -version = "0.5.1" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "xxhash-rust" -version = "0.8.15" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "yoke" -version = "0.8.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "yoke-derive" -version = "0.8.0" +name = "winnow" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] -name = "zerocopy" -version = "0.8.25" +name = "yoke" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ - "zerocopy-derive", + "stable_deref_trait", + "yoke-derive", + "zerofrom", ] [[package]] -name = "zerocopy-derive" -version = "0.8.25" +name = "yoke-derive" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", + "synstructure", ] [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3963,9 +2020,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3974,45 +2031,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "zlib-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40990edd51aae2c2b6907af74ffb635029d5788228222c4bb811e9351c0caad3" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", + "syn 2.0.119", ] [[package]] -name = "zstd-sys" -version = "2.0.15+zstd.1.5.7" +name = "zmij" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index c6d745b..766a532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,57 +1,27 @@ -[workspace] -resolver = "2" -members = [ - "sbuild", - "sbuild-linter", - "sbuild-meta", - "sbuild-cache" -] -default-members = ["sbuild"] - -[workspace.package] +[package] +name = "sbuild" +version = "0.2.0" license = "MIT" edition = "2021" -description = "Toolchain for building, linting, metadata generation, and cache management of SBUILD packages" +description = "Tooling for the soarpkgs package format" repository = "https://github.com/pkgforge/sbuilder" -keywords = ["soar", "sbuild", "lint", "build", "package"] - -[workspace.dependencies] -# Internal crates -sbuild-cache = { path = "sbuild-cache" } -sbuild-linter = { path = "sbuild-linter" } -sbuild-meta = { path = "sbuild-meta" } +keywords = ["soar", "sbuild", "package", "metadata"] -# External -base64 = "0.22" -mongodb = { version = "3", default-features = false, features = ["rustls-tls", "bson-3", "compat-3-3-0", "dns-resolver"] } -blake3 = "1.8" -chrono = { version = "0.4", default-features = false } +[dependencies] +blake3 = "1.8.5" clap = { version = "4", features = ["derive", "env"] } colored = "3.1" -env_logger = "0.11" futures = "0.3" glob = "0.3" -goblin = { version = "0.10", default-features = false, features = ["elf64", "elf32", "endian_fd", "std"] } indexmap = "2.13" -log = "0.4" -memmap2 = "0.9" -onelf-format = "0.2.7" -rayon = "1.11" regex = "1" -reqwest = { version = "0.13", default-features = false } -rusqlite = { version = "0.38", features = ["bundled"] } -semver = "1" +reqwest = { version = "0.13", default-features = false, features = ["http2", "json", "rustls", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" -saphyr = "0.0.6" -sha2 = "0.10" -squishy = { version = "0.5.0", features = ["appimage", "dwarfs"] } -tempfile = "3.25" -thiserror = "2" -tokio = "1" -url = "2.5" -which = "8.0" -zstd = "0.13" +sha2 = "0.11" +tempfile = "3.27.0" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +toml = "1.1" [profile.release] strip = true diff --git a/sbuild-cache/CHANGELOG.md b/sbuild-cache/CHANGELOG.md deleted file mode 100644 index 62ae06d..0000000 --- a/sbuild-cache/CHANGELOG.md +++ /dev/null @@ -1,20 +0,0 @@ - -## [0.1.0] - 2026-02-22 - -### Added - -- Add revision tracking for rolling package builds - ([0e7b88a](https://github.com/pkgforge/sbuilder/commit/0e7b88a9f8cd082a804aab30d6aa099d0c03e583)) - -### Fixed - -- Use recipe pkg name instead of provides entry for pkg_name metadata - ([9a0f8e5](https://github.com/pkgforge/sbuilder/commit/9a0f8e5a0f1bfd5373a6d9515f57b3aa3d879e5f)) -- Return all matching packages in name-based cache lookups - ([ff60c11](https://github.com/pkgforge/sbuilder/commit/ff60c11fe42af8d18c31417caaa873cec5cf52f6)) -- Use actual pkg name from SBUILD recipe for cache entries - ([c1c8c19](https://github.com/pkgforge/sbuilder/commit/c1c8c19f3794a82ba434ffeecdb813bca2c8070a)) - -### Other - -- Centralize dependencies under [workspace.dependencies] - ([92db2b9](https://github.com/pkgforge/sbuilder/commit/92db2b9d21dc910adbce96cc146b9ba13d959a85)) -- Consolidate binaries into unified sbuild CLI with subcommands - ([9818b33](https://github.com/pkgforge/sbuilder/commit/9818b33710e5a53a4b80e7fe86b3e1a9a77ce4ad)) -- Remove cache separation, normalize host - ([03bcf4d](https://github.com/pkgforge/sbuilder/commit/03bcf4dbeb9b21dbb87d5a0135043a7d70c65a35)) -- Handle remote_pkgver - ([500b1bd](https://github.com/pkgforge/sbuilder/commit/500b1bd3034b9b458b91f43393e245b878a8309a)) -- Update, introduce sbuild-meta and sbuild-cache - ([1cc6cd3](https://github.com/pkgforge/sbuilder/commit/1cc6cd399fe16b69eae8dc4895dc52c451453842)) diff --git a/sbuild-cache/Cargo.toml b/sbuild-cache/Cargo.toml deleted file mode 100644 index 861cdce..0000000 --- a/sbuild-cache/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "sbuild-cache" -version = "0.1.0" -license.workspace = true -edition.workspace = true -description = "Historical build cache for SBUILD packages" -repository = "https://github.com/pkgforge/sbuilder" -keywords = ["soar", "sbuild", "cache", "sqlite", "build-history"] - -[dependencies] -bson = { version = "3", features = ["chrono-0_4"] } -chrono = { workspace = true, features = ["serde", "now"] } -mongodb.workspace = true -rusqlite.workspace = true -serde.workspace = true -serde_json.workspace = true -thiserror.workspace = true -tokio = { workspace = true, features = ["rt"] } - -[lib] -name = "sbuild_cache" -path = "src/lib.rs" diff --git a/sbuild-cache/src/error.rs b/sbuild-cache/src/error.rs deleted file mode 100644 index 978ef49..0000000 --- a/sbuild-cache/src/error.rs +++ /dev/null @@ -1,27 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("SQLite error: {0}")] - Sqlite(#[from] rusqlite::Error), - - #[error("MongoDB error: {0}")] - Mongo(#[from] mongodb::error::Error), - - #[error("JSON error: {0}")] - Json(#[from] serde_json::Error), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("Package not found: {0}")] - PackageNotFound(String), - - #[error("Invalid status: {0}")] - InvalidStatus(String), - - #[error("{0}")] - Other(String), -} - -pub type Result = std::result::Result; diff --git a/sbuild-cache/src/export.rs b/sbuild-cache/src/export.rs deleted file mode 100644 index 6f00d2f..0000000 --- a/sbuild-cache/src/export.rs +++ /dev/null @@ -1,80 +0,0 @@ -//! Export MongoDB data to SQLite for user consumption - -use std::path::Path; - -use crate::error::Result; -use crate::models::*; -use crate::mongo::MongoDatabase; -use crate::sqlite::CacheDatabase; - -/// Export all data from MongoDB to a fresh SQLite file -pub async fn export_to_sqlite(mongo: &MongoDatabase, output_path: &Path) -> Result<()> { - if output_path.exists() { - std::fs::remove_file(output_path)?; - } - - let sqlite_db = CacheDatabase::open(output_path)?; - let packages = mongo.list_all_packages().await?; - - for pkg_doc in &packages { - let record = PackageRecord { - id: None, - pkg_id: pkg_doc.pkg_id.clone(), - pkg_name: pkg_doc.pkg_name.clone(), - pkg_family: pkg_doc.pkg_family.clone(), - build_script: String::new(), - ghcr_pkg: String::new(), - host_triplet: pkg_doc.host_triplet.clone(), - current_version: pkg_doc.current_version.clone(), - upstream_version: pkg_doc.upstream_version.clone(), - is_outdated: pkg_doc.is_outdated, - recipe_hash: pkg_doc.recipe_hash.clone(), - base_version: pkg_doc.base_version.clone(), - remote_version: pkg_doc.remote_version.clone(), - revision: pkg_doc.revision, - last_build_date: pkg_doc - .build_history - .last() - .map(|h| h.build_date.to_chrono()), - last_build_id: pkg_doc - .build_history - .last() - .and_then(|h| h.build_id.clone()), - last_build_status: pkg_doc - .build_history - .last() - .and_then(|h| BuildStatus::from_str(&h.build_status)), - ghcr_tag: pkg_doc - .build_history - .last() - .and_then(|h| h.ghcr_tag.clone()), - snapshots: pkg_doc.snapshots.clone(), - created_at: pkg_doc.created_at.to_chrono(), - updated_at: pkg_doc.updated_at.to_chrono(), - }; - - sqlite_db.import_package(&record)?; - - for hist in &pkg_doc.build_history { - let entry = BuildHistoryEntry { - id: None, - package_id: 0, - build_id: hist.build_id.clone().unwrap_or_default(), - version: hist.version.clone(), - build_date: hist.build_date.to_chrono(), - build_status: BuildStatus::from_str(&hist.build_status) - .unwrap_or(BuildStatus::Pending), - duration_seconds: hist.duration_seconds, - artifact_size_bytes: None, - ghcr_tag: hist.ghcr_tag.clone(), - ghcr_digest: None, - build_log_url: hist.build_log_url.clone(), - error_message: hist.error_message.clone(), - }; - - sqlite_db.import_build_history(&pkg_doc.pkg_id, &pkg_doc.host_triplet, &entry)?; - } - } - - Ok(()) -} diff --git a/sbuild-cache/src/lib.rs b/sbuild-cache/src/lib.rs deleted file mode 100644 index 8f39358..0000000 --- a/sbuild-cache/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! sbuild-cache: Historical build cache for SBUILD packages -//! -//! This crate provides: -//! - SQLite-based build history tracking -//! - Version comparison caching -//! - Package status management -//! - Rebuild decision support - -pub mod error; -pub mod export; -pub mod models; -pub mod mongo; -pub mod schema; -pub mod sqlite; - -pub use error::{Error, Result}; -pub use models::*; -pub use mongo::MongoDatabase; -pub use sqlite::CacheDatabase; diff --git a/sbuild-cache/src/models.rs b/sbuild-cache/src/models.rs deleted file mode 100644 index dbc40f8..0000000 --- a/sbuild-cache/src/models.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! Data models for the cache database - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; - -/// Build status enum -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum BuildStatus { - Success, - Failed, - Pending, - Skipped, -} - -impl BuildStatus { - pub fn as_str(&self) -> &'static str { - match self { - BuildStatus::Success => "success", - BuildStatus::Failed => "failed", - BuildStatus::Pending => "pending", - BuildStatus::Skipped => "skipped", - } - } - - pub fn from_str(s: &str) -> Option { - match s.to_lowercase().as_str() { - "success" => Some(BuildStatus::Success), - "failed" => Some(BuildStatus::Failed), - "pending" => Some(BuildStatus::Pending), - "skipped" => Some(BuildStatus::Skipped), - _ => None, - } - } -} - -impl std::fmt::Display for BuildStatus { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.as_str()) - } -} - -/// Package record in the cache -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PackageRecord { - pub id: Option, - pub pkg_id: String, - pub pkg_name: String, - pub pkg_family: Option, - pub build_script: String, - pub ghcr_pkg: String, - pub host_triplet: String, - - // Current state - pub current_version: Option, - pub upstream_version: Option, - pub is_outdated: bool, - pub recipe_hash: Option, - - // Revision tracking - pub base_version: Option, - pub remote_version: Option, - pub revision: i32, - - // Build info - pub last_build_date: Option>, - pub last_build_id: Option, - pub last_build_status: Option, - pub ghcr_tag: Option, - - // Snapshots - all historical versions ever built - pub snapshots: Vec, - - // Timestamps - pub created_at: DateTime, - pub updated_at: DateTime, -} - -impl PackageRecord { - pub fn new(pkg_id: String, pkg_name: String, host_triplet: String) -> Self { - let now = Utc::now(); - Self { - id: None, - pkg_id, - pkg_name: pkg_name.clone(), - pkg_family: Some(pkg_name), - build_script: String::new(), - ghcr_pkg: String::new(), - host_triplet, - current_version: None, - upstream_version: None, - is_outdated: false, - recipe_hash: None, - base_version: None, - remote_version: None, - revision: 0, - last_build_date: None, - last_build_id: None, - last_build_status: None, - ghcr_tag: None, - snapshots: Vec::new(), - created_at: now, - updated_at: now, - } - } -} - -/// Build history entry -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BuildHistoryEntry { - pub id: Option, - pub package_id: i64, - pub build_id: String, - pub version: String, - pub build_date: DateTime, - pub build_status: BuildStatus, - pub duration_seconds: Option, - pub artifact_size_bytes: Option, - pub ghcr_tag: Option, - pub ghcr_digest: Option, - pub build_log_url: Option, - pub error_message: Option, -} - -/// Version cache entry -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VersionCacheEntry { - pub id: Option, - pub package_id: i64, - pub upstream_source: Option, - pub upstream_version: String, - pub checked_at: DateTime, - pub expires_at: DateTime, -} - -/// Failed package tracking -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FailedPackage { - pub id: Option, - pub package_id: i64, - pub failure_count: i32, - pub last_failure_date: DateTime, - pub last_error_message: Option, - pub next_retry_date: Option>, -} - -/// Statistics for build operations -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct BuildStats { - pub total_packages: i64, - pub successful: i64, - pub failed: i64, - pub pending: i64, - pub outdated: i64, -} - -/// Reason for rebuilding a package -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "reason")] -pub enum RebuildReason { - /// New package, never built before - NewPackage, - /// User forced rebuild - Forced, - /// Recipe content changed - RecipeChanged { old_hash: String, new_hash: String }, - /// Version field was updated (bot PR merged) - VersionUpdated { - old_version: String, - new_version: String, - }, - /// Previous build failed, retrying - RetryFailed { attempt: i32, last_error: String }, - /// Build is too old - StaleBuild { - last_build_days_ago: i64, - threshold_days: i64, - }, -} - -/// Decision about whether to rebuild a package -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct RebuildDecision { - pub should_rebuild: bool, - pub reason: Option, - pub priority: u8, // 1 = highest, 5 = lowest -} - -impl RebuildDecision { - pub fn skip() -> Self { - Self { - should_rebuild: false, - reason: None, - priority: 5, - } - } - - pub fn rebuild(reason: RebuildReason, priority: u8) -> Self { - Self { - should_rebuild: true, - reason: Some(reason), - priority, - } - } -} diff --git a/sbuild-cache/src/mongo.rs b/sbuild-cache/src/mongo.rs deleted file mode 100644 index 472b47d..0000000 --- a/sbuild-cache/src/mongo.rs +++ /dev/null @@ -1,771 +0,0 @@ -//! MongoDB backend for the build cache - -use bson::{doc, Bson, Document}; -use chrono::{Duration, Utc}; -use mongodb::{ - options::{ClientOptions, FindOneOptions, FindOptions, IndexOptions, UpdateOptions}, - Client, Collection, IndexModel, -}; -use serde::{Deserialize, Serialize}; - -use crate::error::{Error, Result}; -use crate::models::*; - -/// MongoDB document for a package -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PackageDocument { - pub pkg_id: String, - pub pkg_name: String, - #[serde(default)] - pub pkg_family: Option, - pub host_triplet: String, - #[serde(default)] - pub current_version: Option, - #[serde(default)] - pub upstream_version: Option, - #[serde(default)] - pub is_outdated: bool, - #[serde(default)] - pub recipe_hash: Option, - #[serde(default)] - pub base_version: Option, - #[serde(default)] - pub remote_version: Option, - #[serde(default)] - pub revision: i32, - #[serde(default)] - pub build_history: Vec, - #[serde(default)] - pub snapshots: Vec, - pub created_at: bson::DateTime, - pub updated_at: bson::DateTime, -} - -/// MongoDB subdocument for build history -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BuildHistoryDocument { - #[serde(default)] - pub build_id: Option, - pub version: String, - pub build_date: bson::DateTime, - pub build_status: String, - #[serde(default)] - pub ghcr_tag: Option, - #[serde(default)] - pub recipe_hash: Option, - #[serde(default)] - pub duration_seconds: Option, - #[serde(default)] - pub error_message: Option, - #[serde(default)] - pub build_log_url: Option, -} - -/// MongoDB cache database -pub struct MongoDatabase { - collection: Collection, - raw_collection: Collection, -} - -impl MongoDatabase { - /// Connect to MongoDB using a connection URI - pub async fn connect(uri: &str) -> Result { - let options = ClientOptions::parse(uri).await?; - let client = Client::with_options(options)?; - - let db_name = client - .default_database() - .map(|db| db.name().to_string()) - .unwrap_or_else(|| "sbuild_cache".to_string()); - - let db = client.database(&db_name); - let collection = db.collection::("packages"); - let raw_collection = db.collection::("packages"); - - let mongo_db = Self { - collection, - raw_collection, - }; - mongo_db.ensure_indexes().await?; - Ok(mongo_db) - } - - /// Create indexes for efficient queries - async fn ensure_indexes(&self) -> Result<()> { - let unique_index = IndexModel::builder() - .keys(doc! { "pkg_id": 1, "host_triplet": 1 }) - .options(IndexOptions::builder().unique(true).build()) - .build(); - - let name_index = IndexModel::builder() - .keys(doc! { "pkg_name": 1, "host_triplet": 1 }) - .build(); - - let outdated_index = IndexModel::builder() - .keys(doc! { "is_outdated": 1 }) - .build(); - - self.raw_collection - .create_indexes(vec![unique_index, name_index, outdated_index]) - .await?; - - Ok(()) - } - - /// Get or create a package record - pub async fn get_or_create_package( - &self, - pkg_id: &str, - pkg_name: &str, - host_triplet: &str, - ) -> Result { - let now = Utc::now(); - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let update = doc! { - "$setOnInsert": { - "pkg_id": pkg_id, - "pkg_name": pkg_name, - "pkg_family": Bson::Null, - "host_triplet": host_triplet, - "current_version": Bson::Null, - "upstream_version": Bson::Null, - "is_outdated": false, - "recipe_hash": Bson::Null, - "base_version": Bson::Null, - "remote_version": Bson::Null, - "revision": 0_i32, - "build_history": Bson::Array(vec![]), - "snapshots": Bson::Array(vec![]), - "created_at": bson::DateTime::from_chrono(now), - "updated_at": bson::DateTime::from_chrono(now), - } - }; - let options = UpdateOptions::builder().upsert(true).build(); - - self.raw_collection - .update_one(filter, update) - .with_options(options) - .await?; - - self.get_package(pkg_id, host_triplet) - .await? - .ok_or_else(|| Error::Other("Failed to create package".to_string())) - } - - /// Get a package by ID and host - pub async fn get_package( - &self, - pkg_id: &str, - host_triplet: &str, - ) -> Result> { - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let result = self.collection.find_one(filter).await?; - Ok(result.map(|d| pkg_doc_to_record(&d))) - } - - /// Find packages by name (or pkg_id suffix) and host - pub async fn find_packages_by_name( - &self, - name: &str, - host_triplet: &str, - ) -> Result> { - let escaped = regex_escape(name); - let filter = doc! { - "$or": [ - { "pkg_name": name, "host_triplet": host_triplet }, - { "pkg_id": { "$regex": format!(r"\.{}$", escaped) }, "host_triplet": host_triplet }, - ] - }; - let options = FindOptions::builder().sort(doc! { "pkg_id": 1 }).build(); - - let mut cursor = self.collection.find(filter).with_options(options).await?; - let mut results = Vec::new(); - while cursor.advance().await? { - let doc = cursor.deserialize_current()?; - results.push(pkg_doc_to_record(&doc)); - } - Ok(results) - } - - /// Update package after a build - pub async fn update_build_result( - &self, - pkg_id: &str, - host_triplet: &str, - version: &str, - status: BuildStatus, - build_id: Option<&str>, - ghcr_tag: Option<&str>, - recipe_hash: Option<&str>, - base_version: Option<&str>, - remote_version: Option<&str>, - revision: i32, - duration_seconds: Option, - error_message: Option<&str>, - build_log_url: Option<&str>, - ) -> Result<()> { - if version.is_empty() || version == "unknown" { - return Err(Error::Other(format!( - "Invalid version '{}': must not be empty or 'unknown'", - version - ))); - } - - let now = Utc::now(); - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - - let history_entry = doc! { - "build_id": build_id, - "version": version, - "build_date": bson::DateTime::from_chrono(now), - "build_status": status.as_str(), - "ghcr_tag": ghcr_tag, - "recipe_hash": recipe_hash, - "duration_seconds": duration_seconds, - "error_message": error_message, - "build_log_url": build_log_url, - }; - - // On success, also add version to snapshots (if not already present) - let update = if status == BuildStatus::Success { - doc! { - "$set": { - "current_version": version, - "is_outdated": false, - "recipe_hash": recipe_hash, - "base_version": base_version, - "remote_version": remote_version, - "revision": revision, - "updated_at": bson::DateTime::from_chrono(now), - }, - "$push": { - "build_history": { - "$each": [history_entry], - "$slice": -3_i32, - } - }, - "$addToSet": { - "snapshots": version, - } - } - } else { - doc! { - "$set": { - "current_version": version, - "is_outdated": false, - "recipe_hash": recipe_hash, - "base_version": base_version, - "remote_version": remote_version, - "revision": revision, - "updated_at": bson::DateTime::from_chrono(now), - }, - "$push": { - "build_history": { - "$each": [history_entry], - "$slice": -3_i32, - } - } - } - }; - - self.raw_collection.update_one(filter, update).await?; - Ok(()) - } - - /// Get the next revision number for a package version - pub async fn get_revision( - &self, - pkg_id: &str, - host_triplet: &str, - base_version: &str, - remote_version: Option<&str>, - recipe_hash: Option<&str>, - ) -> Result { - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let options = FindOneOptions::builder() - .projection( - doc! { "base_version": 1, "remote_version": 1, "revision": 1, "recipe_hash": 1 }, - ) - .build(); - - let result = self - .raw_collection - .find_one(filter) - .with_options(options) - .await?; - - match result { - Some(doc) => { - let stored_base = doc.get_str("base_version").ok(); - let stored_revision = doc.get_i32("revision").unwrap_or(0); - let stored_hash = doc.get_str("recipe_hash").ok(); - let stored_remote = doc.get_str("remote_version").ok(); - - if stored_base == Some(base_version) { - // Same base version but different remote version → new upstream, reset - if let (Some(new_remote), Some(old_remote)) = (remote_version, stored_remote) { - if new_remote != old_remote { - return Ok(0); - } - } - - match (recipe_hash, stored_hash) { - // Both hashes known and differ: bump revision - (Some(new), Some(old)) if new != old => Ok(stored_revision + 1), - // Same hash or unknown: reuse current revision - _ => Ok(stored_revision), - } - } else { - Ok(0) - } - } - None => Ok(0), - } - } - - /// Mark package as outdated - pub async fn mark_outdated( - &self, - pkg_id: &str, - host_triplet: &str, - upstream_version: &str, - ) -> Result<()> { - let now = Utc::now(); - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let update = doc! { - "$set": { - "is_outdated": true, - "upstream_version": upstream_version, - "updated_at": bson::DateTime::from_chrono(now), - } - }; - - self.raw_collection.update_one(filter, update).await?; - Ok(()) - } - - /// Update recipe hash for a package - pub async fn update_recipe_hash( - &self, - pkg_id: &str, - host_triplet: &str, - hash: &str, - ) -> Result<()> { - let now = Utc::now(); - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let update = doc! { - "$set": { - "recipe_hash": hash, - "updated_at": bson::DateTime::from_chrono(now), - } - }; - - self.raw_collection.update_one(filter, update).await?; - Ok(()) - } - - /// Get snapshots for a package - pub async fn get_snapshots(&self, pkg_id: &str, host_triplet: &str) -> Result> { - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let options = FindOneOptions::builder() - .projection(doc! { "snapshots": 1 }) - .build(); - - let result = self - .raw_collection - .find_one(filter) - .with_options(options) - .await?; - - match result { - Some(doc) => { - let snapshots = doc - .get_array("snapshots") - .ok() - .and_then(|arr| { - arr.iter() - .map(|v| v.as_str().map(|s| s.to_string())) - .collect::>>() - }) - .unwrap_or_default(); - Ok(snapshots) - } - None => Ok(Vec::new()), - } - } - - /// Add a snapshot to a package - pub async fn add_snapshot( - &self, - pkg_id: &str, - host_triplet: &str, - snapshot: &str, - ) -> Result<()> { - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let update = doc! { - "$addToSet": { "snapshots": snapshot } - }; - self.raw_collection.update_one(filter, update).await?; - Ok(()) - } - - /// Get packages needing rebuild for a host - pub async fn get_packages_needing_rebuild( - &self, - host_triplet: &str, - ) -> Result> { - // First: outdated or never built - let filter = doc! { - "host_triplet": host_triplet, - "$or": [ - { "is_outdated": true }, - { "build_history": { "$size": 0 } }, - { "build_history": { "$exists": false } }, - ] - }; - let options = FindOptions::builder().sort(doc! { "pkg_name": 1 }).build(); - - let mut cursor = self.collection.find(filter).with_options(options).await?; - let mut results = Vec::new(); - while cursor.advance().await? { - let doc = cursor.deserialize_current()?; - results.push(pkg_doc_to_record(&doc)); - } - - // Also check for failed packages with retry allowed - let all_filter = doc! { - "host_triplet": host_triplet, - "is_outdated": false, - }; - let mut all_cursor = self - .collection - .find(all_filter) - .with_options(FindOptions::builder().sort(doc! { "pkg_name": 1 }).build()) - .await?; - - while all_cursor.advance().await? { - let pkg_doc = all_cursor.deserialize_current()?; - if let Some(last) = pkg_doc.build_history.last() { - if last.build_status == "failed" - && is_retry_allowed_from_history(&pkg_doc.build_history) - { - results.push(pkg_doc_to_record(&pkg_doc)); - } - } - } - - Ok(results) - } - - /// Get build statistics for a host - pub async fn get_stats(&self, host_triplet: &str) -> Result { - let pipeline = vec![ - doc! { "$match": { "host_triplet": host_triplet } }, - doc! { - "$group": { - "_id": Bson::Null, - "total": { "$sum": 1 }, - "successful": { - "$sum": { - "$cond": [ - { "$eq": [{ "$last": "$build_history.build_status" }, "success"] }, - 1, 0 - ] - } - }, - "failed": { - "$sum": { - "$cond": [ - { "$eq": [{ "$last": "$build_history.build_status" }, "failed"] }, - 1, 0 - ] - } - }, - "pending": { - "$sum": { - "$cond": [ - { "$or": [ - { "$eq": [{ "$size": { "$ifNull": ["$build_history", []] } }, 0] }, - { "$eq": [{ "$last": "$build_history.build_status" }, "pending"] }, - ]}, - 1, 0 - ] - } - }, - "outdated": { - "$sum": { - "$cond": ["$is_outdated", 1, 0] - } - }, - } - }, - ]; - - let mut cursor = self.raw_collection.aggregate(pipeline).await?; - - if cursor.advance().await? { - let doc = cursor.deserialize_current()?; - Ok(BuildStats { - total_packages: doc.get_i32("total").unwrap_or(0) as i64, - successful: doc.get_i32("successful").unwrap_or(0) as i64, - failed: doc.get_i32("failed").unwrap_or(0) as i64, - pending: doc.get_i32("pending").unwrap_or(0) as i64, - outdated: doc.get_i32("outdated").unwrap_or(0) as i64, - }) - } else { - Ok(BuildStats::default()) - } - } - - /// Check if retry is allowed for a package - pub async fn is_retry_allowed(&self, pkg_id: &str, host_triplet: &str) -> Result { - let filter = doc! { "pkg_id": pkg_id, "host_triplet": host_triplet }; - let result = self.collection.find_one(filter).await?; - - match result { - Some(doc) => Ok(is_retry_allowed_from_history(&doc.build_history)), - None => Ok(true), - } - } - - /// List packages with optional status filter - pub async fn list_packages( - &self, - host_triplet: &str, - status_filter: Option, - include_outdated: bool, - ) -> Result> { - let mut filter = doc! { "host_triplet": host_triplet }; - - match (&status_filter, include_outdated) { - (Some(status), true) => { - filter.insert( - "$or", - vec![ - doc! { - "$expr": { - "$eq": [ - { "$arrayElemAt": ["$build_history.build_status", -1] }, - status.as_str() - ] - } - }, - doc! { "is_outdated": true }, - ], - ); - } - (Some(status), false) => { - filter.insert( - "$expr", - doc! { - "$eq": [ - { "$arrayElemAt": ["$build_history.build_status", -1] }, - status.as_str() - ] - }, - ); - } - (None, true) => { - filter.insert("is_outdated", true); - } - (None, false) => {} - } - - let options = FindOptions::builder().sort(doc! { "pkg_name": 1 }).build(); - - let mut cursor = self.collection.find(filter).with_options(options).await?; - let mut results = Vec::new(); - while cursor.advance().await? { - let doc = cursor.deserialize_current()?; - results.push(pkg_doc_to_record(&doc)); - } - Ok(results) - } - - /// List all packages (for export) - pub async fn list_all_packages(&self) -> Result> { - let options = FindOptions::builder() - .sort(doc! { "pkg_id": 1, "host_triplet": 1 }) - .build(); - - let mut cursor = self.collection.find(doc! {}).with_options(options).await?; - let mut results = Vec::new(); - while cursor.advance().await? { - results.push(cursor.deserialize_current()?); - } - Ok(results) - } - - /// Get recent build history - pub async fn get_recent_builds( - &self, - host_triplet: &str, - limit: i64, - ) -> Result> { - let pipeline = vec![ - doc! { "$match": { "host_triplet": host_triplet } }, - doc! { "$unwind": "$build_history" }, - doc! { "$sort": { "build_history.build_date": -1 } }, - doc! { "$limit": limit }, - ]; - - let mut cursor = self.raw_collection.aggregate(pipeline).await?; - let mut results = Vec::new(); - - while cursor.advance().await? { - let doc: Document = cursor.deserialize_current()?; - // Deserialize the parent as a partial PackageDocument - let pkg_id = doc.get_str("pkg_id").unwrap_or_default(); - let pkg_name = doc.get_str("pkg_name").unwrap_or_default(); - let host = doc.get_str("host_triplet").unwrap_or_default(); - - let record = PackageRecord { - id: None, - pkg_id: pkg_id.to_string(), - pkg_name: pkg_name.to_string(), - pkg_family: doc.get_str("pkg_family").ok().map(|s| s.to_string()), - build_script: String::new(), - ghcr_pkg: String::new(), - host_triplet: host.to_string(), - current_version: doc.get_str("current_version").ok().map(|s| s.to_string()), - upstream_version: doc.get_str("upstream_version").ok().map(|s| s.to_string()), - is_outdated: doc.get_bool("is_outdated").unwrap_or(false), - recipe_hash: doc.get_str("recipe_hash").ok().map(|s| s.to_string()), - base_version: doc.get_str("base_version").ok().map(|s| s.to_string()), - remote_version: doc.get_str("remote_version").ok().map(|s| s.to_string()), - revision: doc.get_i32("revision").unwrap_or(0), - last_build_date: None, - last_build_id: None, - last_build_status: None, - ghcr_tag: None, - snapshots: doc - .get_array("snapshots") - .ok() - .and_then(|arr| { - arr.iter() - .map(|v| v.as_str().map(|s| s.to_string())) - .collect::>>() - }) - .unwrap_or_default(), - created_at: doc - .get_datetime("created_at") - .ok() - .map(|dt| dt.to_chrono()) - .unwrap_or_else(Utc::now), - updated_at: doc - .get_datetime("updated_at") - .ok() - .map(|dt| dt.to_chrono()) - .unwrap_or_else(Utc::now), - }; - - if let Ok(hist_doc) = doc.get_document("build_history") { - let entry = BuildHistoryEntry { - id: None, - package_id: 0, - build_id: hist_doc - .get_str("build_id") - .ok() - .unwrap_or_default() - .to_string(), - version: hist_doc.get_str("version").unwrap_or_default().to_string(), - build_date: hist_doc - .get_datetime("build_date") - .ok() - .map(|dt| dt.to_chrono()) - .unwrap_or_else(Utc::now), - build_status: hist_doc - .get_str("build_status") - .ok() - .and_then(BuildStatus::from_str) - .unwrap_or(BuildStatus::Pending), - duration_seconds: hist_doc - .get_i64("duration_seconds") - .ok() - .or_else(|| hist_doc.get_i32("duration_seconds").ok().map(|v| v as i64)), - artifact_size_bytes: None, - ghcr_tag: hist_doc.get_str("ghcr_tag").ok().map(|s| s.to_string()), - ghcr_digest: None, - build_log_url: hist_doc - .get_str("build_log_url") - .ok() - .map(|s| s.to_string()), - error_message: hist_doc - .get_str("error_message") - .ok() - .map(|s| s.to_string()), - }; - results.push((record, entry)); - } - } - Ok(results) - } -} - -/// Check if retry is allowed based on build history -fn is_retry_allowed_from_history(history: &[BuildHistoryDocument]) -> bool { - let consecutive_failures = history - .iter() - .rev() - .take_while(|entry| entry.build_status == "failed") - .count(); - - if consecutive_failures == 0 { - return true; - } - - // Exponential backoff: 1h, 2h, 4h - let backoff_hours = std::cmp::min(1_i64 << (consecutive_failures - 1), 24); - - if let Some(last) = history.last() { - let last_date = last.build_date.to_chrono(); - let next_retry = last_date + Duration::hours(backoff_hours); - Utc::now() >= next_retry - } else { - true - } -} - -/// Escape special regex characters -fn regex_escape(s: &str) -> String { - let specials = [ - '.', '^', '$', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|', '\\', - ]; - let mut result = String::with_capacity(s.len() * 2); - for c in s.chars() { - if specials.contains(&c) { - result.push('\\'); - } - result.push(c); - } - result -} - -/// Convert a PackageDocument to a PackageRecord -fn pkg_doc_to_record(doc: &PackageDocument) -> PackageRecord { - let last_build = doc.build_history.last(); - - PackageRecord { - id: None, - pkg_id: doc.pkg_id.clone(), - pkg_name: doc.pkg_name.clone(), - pkg_family: doc.pkg_family.clone(), - build_script: String::new(), - ghcr_pkg: String::new(), - host_triplet: doc.host_triplet.clone(), - current_version: doc.current_version.clone(), - upstream_version: doc.upstream_version.clone(), - is_outdated: doc.is_outdated, - recipe_hash: doc.recipe_hash.clone(), - base_version: doc.base_version.clone(), - remote_version: doc.remote_version.clone(), - revision: doc.revision, - last_build_date: last_build.map(|h| h.build_date.to_chrono()), - last_build_id: last_build.and_then(|h| h.build_id.clone()), - last_build_status: last_build.and_then(|h| BuildStatus::from_str(&h.build_status)), - ghcr_tag: last_build.and_then(|h| h.ghcr_tag.clone()), - snapshots: doc.snapshots.clone(), - created_at: doc.created_at.to_chrono(), - updated_at: doc.updated_at.to_chrono(), - } -} diff --git a/sbuild-cache/src/schema.rs b/sbuild-cache/src/schema.rs deleted file mode 100644 index 7fa24f2..0000000 --- a/sbuild-cache/src/schema.rs +++ /dev/null @@ -1,151 +0,0 @@ -//! SQLite schema definitions - -/// Current schema version -pub const SCHEMA_VERSION: i32 = 4; - -/// SQL to create the database schema -pub const CREATE_SCHEMA: &str = r#" --- Schema version tracking -CREATE TABLE IF NOT EXISTS schema_info ( - version INTEGER PRIMARY KEY, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - description TEXT -); - --- Core package tracking table -CREATE TABLE IF NOT EXISTS packages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - pkg_id TEXT NOT NULL, - pkg_name TEXT NOT NULL COLLATE NOCASE, - pkg_family TEXT COLLATE NOCASE, - build_script TEXT NOT NULL DEFAULT '', - ghcr_pkg TEXT NOT NULL DEFAULT '', - host_triplet TEXT NOT NULL, - - -- Current state - current_version TEXT, - upstream_version TEXT, - is_outdated INTEGER DEFAULT 0, - recipe_hash TEXT, - - -- Revision tracking - base_version TEXT, - remote_version TEXT, - revision INTEGER DEFAULT 0, - - -- Build info - last_build_date TEXT, - last_build_id TEXT, - last_build_status TEXT CHECK(last_build_status IN ('success', 'failed', 'skipped', 'pending')), - ghcr_tag TEXT, - - -- Snapshots (JSON array of historical versions) - snapshots TEXT DEFAULT '[]', - - -- Timestamps - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - - UNIQUE(pkg_id, host_triplet) -); - --- Build history (rolling window, keep last N builds) -CREATE TABLE IF NOT EXISTS build_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - package_id INTEGER NOT NULL, - build_id TEXT, - version TEXT NOT NULL, - build_date TEXT NOT NULL, - build_status TEXT CHECK(build_status IN ('success', 'failed', 'skipped')), - duration_seconds INTEGER, - artifact_size_bytes INTEGER, - ghcr_tag TEXT, - ghcr_digest TEXT, - build_log_url TEXT, - error_message TEXT, - - FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE -); - --- Version comparison cache (avoid re-fetching upstream) -CREATE TABLE IF NOT EXISTS version_cache ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - package_id INTEGER NOT NULL, - upstream_source TEXT, - upstream_version TEXT, - checked_at TEXT NOT NULL DEFAULT (datetime('now')), - expires_at TEXT NOT NULL, - - FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE, - UNIQUE(package_id, upstream_source) -); - --- Failed package tracking (for retry logic) -CREATE TABLE IF NOT EXISTS failed_packages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - package_id INTEGER NOT NULL, - failure_count INTEGER DEFAULT 1, - last_failure_date TEXT NOT NULL, - last_error_message TEXT, - next_retry_date TEXT, - - FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE, - UNIQUE(package_id) -); - --- Indexes for common queries -CREATE INDEX IF NOT EXISTS idx_packages_host ON packages(host_triplet); -CREATE INDEX IF NOT EXISTS idx_packages_outdated ON packages(is_outdated) WHERE is_outdated = 1; -CREATE INDEX IF NOT EXISTS idx_packages_status ON packages(last_build_status); -CREATE INDEX IF NOT EXISTS idx_packages_pkg_name ON packages(pkg_name); -CREATE INDEX IF NOT EXISTS idx_build_history_date ON build_history(build_date); -CREATE INDEX IF NOT EXISTS idx_build_history_package ON build_history(package_id); -CREATE INDEX IF NOT EXISTS idx_version_cache_expires ON version_cache(expires_at); -CREATE INDEX IF NOT EXISTS idx_failed_packages_retry ON failed_packages(next_retry_date); -"#; - -/// SQL to migrate from schema v1 to v2 -pub const MIGRATE_V1_TO_V2: &str = r#" -ALTER TABLE packages ADD COLUMN base_version TEXT; -ALTER TABLE packages ADD COLUMN revision INTEGER DEFAULT 0; -"#; - -/// SQL to migrate from schema v2 to v3 -pub const MIGRATE_V2_TO_V3: &str = r#" -ALTER TABLE packages ADD COLUMN remote_version TEXT; -"#; - -/// SQL to migrate from schema v3 to v4 -pub const MIGRATE_V3_TO_V4: &str = r#" -ALTER TABLE packages ADD COLUMN snapshots TEXT DEFAULT '[]'; -"#; - -/// SQL for views -pub const CREATE_VIEWS: &str = r#" --- View for packages needing rebuild -CREATE VIEW IF NOT EXISTS v_packages_needing_rebuild AS -SELECT p.*, - bh.build_date as last_success_date, - vc.upstream_version as cached_upstream_version -FROM packages p -LEFT JOIN build_history bh ON p.id = bh.package_id AND bh.build_status = 'success' -LEFT JOIN version_cache vc ON p.id = vc.package_id -LEFT JOIN failed_packages fp ON p.id = fp.package_id -WHERE (p.is_outdated = 1 - OR p.last_build_status IS NULL - OR p.last_build_status = 'pending') - AND (fp.next_retry_date IS NULL OR fp.next_retry_date <= datetime('now')) -ORDER BY p.pkg_family, p.pkg_name; - --- View for build statistics per host -CREATE VIEW IF NOT EXISTS v_build_stats AS -SELECT - host_triplet, - COUNT(*) as total_packages, - SUM(CASE WHEN last_build_status = 'success' THEN 1 ELSE 0 END) as successful, - SUM(CASE WHEN last_build_status = 'failed' THEN 1 ELSE 0 END) as failed, - SUM(CASE WHEN last_build_status = 'pending' THEN 1 ELSE 0 END) as pending, - SUM(CASE WHEN is_outdated = 1 THEN 1 ELSE 0 END) as outdated -FROM packages -GROUP BY host_triplet; -"#; diff --git a/sbuild-cache/src/sqlite.rs b/sbuild-cache/src/sqlite.rs deleted file mode 100644 index 1a8b87c..0000000 --- a/sbuild-cache/src/sqlite.rs +++ /dev/null @@ -1,807 +0,0 @@ -//! Database operations for the build cache - -use chrono::{DateTime, Duration, Utc}; -use rusqlite::{params, Connection, OptionalExtension}; -use std::path::Path; - -use crate::error::{Error, Result}; -use crate::models::*; -use crate::schema::{ - CREATE_SCHEMA, CREATE_VIEWS, MIGRATE_V1_TO_V2, MIGRATE_V2_TO_V3, MIGRATE_V3_TO_V4, - SCHEMA_VERSION, -}; - -/// SQLite cache database -pub struct CacheDatabase { - conn: Connection, -} - -impl CacheDatabase { - /// Open or create a cache database - pub fn open(path: &Path) -> Result { - let conn = Connection::open(path)?; - let db = Self { conn }; - db.initialize()?; - Ok(db) - } - - /// Create an in-memory database (for testing) - pub fn in_memory() -> Result { - let conn = Connection::open_in_memory()?; - let db = Self { conn }; - db.initialize()?; - Ok(db) - } - - /// Initialize the database schema, running migrations if needed - fn initialize(&self) -> Result<()> { - let has_schema: bool = self - .conn - .query_row( - "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='schema_info'", - [], - |row| row.get::<_, i64>(0), - ) - .map(|count| count > 0)?; - - if !has_schema { - // Fresh database: create schema at current version - self.conn.execute_batch(CREATE_SCHEMA)?; - self.conn.execute_batch(CREATE_VIEWS)?; - self.conn.execute( - "INSERT INTO schema_info (version, description) VALUES (?1, ?2)", - params![SCHEMA_VERSION, "Initial schema"], - )?; - return Ok(()); - } - - // Existing database: check version and migrate if needed - let current_version: i32 = self - .conn - .query_row( - "SELECT COALESCE(MAX(version), 1) FROM schema_info", - [], - |row| row.get(0), - ) - .unwrap_or(1); - - if current_version < 2 { - // Migrate v1 -> v2: add revision tracking columns - self.conn.execute_batch(MIGRATE_V1_TO_V2)?; - self.conn.execute( - "INSERT INTO schema_info (version, description) VALUES (?1, ?2)", - params![2, "Add revision tracking columns"], - )?; - } - - if current_version < 3 { - // Migrate v2 -> v3: add remote_version column - self.conn.execute_batch(MIGRATE_V2_TO_V3)?; - self.conn.execute( - "INSERT INTO schema_info (version, description) VALUES (?1, ?2)", - params![3, "Add remote_version column"], - )?; - } - - if current_version < 4 { - // Migrate v3 -> v4: add snapshots column - self.conn.execute_batch(MIGRATE_V3_TO_V4)?; - self.conn.execute( - "INSERT INTO schema_info (version, description) VALUES (?1, ?2)", - params![4, "Add snapshots column for version history"], - )?; - } - - Ok(()) - } - - /// Get or create a package record - pub fn get_or_create_package( - &self, - pkg_id: &str, - pkg_name: &str, - host_triplet: &str, - ) -> Result { - // Try to find existing - if let Some(record) = self.get_package(pkg_id, host_triplet)? { - return Ok(record); - } - - // Create new - let now = Utc::now().to_rfc3339(); - self.conn.execute( - "INSERT INTO packages (pkg_id, pkg_name, host_triplet, created_at, updated_at) - VALUES (?1, ?2, ?3, ?4, ?5)", - params![pkg_id, pkg_name, host_triplet, now, now], - )?; - - self.get_package(pkg_id, host_triplet)? - .ok_or_else(|| Error::Other("Failed to create package".to_string())) - } - - /// Get a package by ID and host - pub fn get_package(&self, pkg_id: &str, host_triplet: &str) -> Result> { - let result = self - .conn - .query_row( - "SELECT id, pkg_id, pkg_name, pkg_family, build_script, ghcr_pkg, host_triplet, - current_version, upstream_version, is_outdated, recipe_hash, - base_version, remote_version, revision, - last_build_date, last_build_id, last_build_status, ghcr_tag, - snapshots, created_at, updated_at - FROM packages WHERE pkg_id = ?1 AND host_triplet = ?2", - params![pkg_id, host_triplet], - Self::row_to_package_record, - ) - .optional()?; - - Ok(result) - } - - /// Find packages by name (or pkg_id suffix) and host. - /// Returns all matches since multiple packages can share the same name - /// (e.g., coreutils from gnu, uutils, vlang). - pub fn find_packages_by_name( - &self, - name: &str, - host_triplet: &str, - ) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT id, pkg_id, pkg_name, pkg_family, build_script, ghcr_pkg, host_triplet, - current_version, upstream_version, is_outdated, recipe_hash, - base_version, remote_version, revision, - last_build_date, last_build_id, last_build_status, ghcr_tag, - snapshots, created_at, updated_at - FROM packages - WHERE (pkg_name = ?1 OR pkg_id LIKE '%.' || ?1) - AND host_triplet = ?2 - ORDER BY pkg_id", - )?; - - let rows = stmt.query_map(params![name, host_triplet], Self::row_to_package_record)?; - - rows.collect::, _>>() - .map_err(Error::Sqlite) - } - - /// Get snapshots for a package - pub fn get_snapshots(&self, pkg_id: &str, host_triplet: &str) -> Result> { - let snapshots_json: Option = self - .conn - .query_row( - "SELECT snapshots FROM packages WHERE pkg_id = ?1 AND host_triplet = ?2", - params![pkg_id, host_triplet], - |row| row.get(0), - ) - .optional()?; - - match snapshots_json { - Some(json) => { - let snapshots: Vec = serde_json::from_str(&json).unwrap_or_default(); - Ok(snapshots) - } - None => Ok(Vec::new()), - } - } - - /// Update package after a build - pub fn update_build_result( - &self, - pkg_id: &str, - host_triplet: &str, - version: &str, - status: BuildStatus, - build_id: Option<&str>, - ghcr_tag: Option<&str>, - recipe_hash: Option<&str>, - base_version: Option<&str>, - remote_version: Option<&str>, - revision: i32, - ) -> Result<()> { - // Input validation - if version.is_empty() || version == "unknown" { - return Err(Error::Other(format!( - "Invalid version '{}': must not be empty or 'unknown'", - version - ))); - } - - let now = Utc::now().to_rfc3339(); - let status_str = status.as_str(); - - self.conn.execute( - "UPDATE packages SET - current_version = ?1, - last_build_date = ?2, - last_build_status = ?3, - last_build_id = ?4, - ghcr_tag = ?5, - recipe_hash = ?6, - base_version = ?7, - remote_version = ?8, - revision = ?9, - is_outdated = 0, - updated_at = ?10 - WHERE pkg_id = ?11 AND host_triplet = ?12", - params![ - version, - now, - status_str, - build_id, - ghcr_tag, - recipe_hash, - base_version, - remote_version, - revision, - now, - pkg_id, - host_triplet - ], - )?; - - // Add to build history - if let Some(record) = self.get_package(pkg_id, host_triplet)? { - if let Some(id) = record.id { - self.conn.execute( - "INSERT INTO build_history (package_id, build_id, version, build_date, build_status, ghcr_tag) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![id, build_id, version, now, status_str, ghcr_tag], - )?; - } - } - - Ok(()) - } - - /// Get the next revision number for a package version - pub fn get_revision( - &self, - pkg_id: &str, - host_triplet: &str, - base_version: &str, - remote_version: Option<&str>, - recipe_hash: Option<&str>, - ) -> Result { - let result = self - .conn - .query_row( - "SELECT base_version, revision, recipe_hash, remote_version FROM packages - WHERE pkg_id = ?1 AND host_triplet = ?2", - params![pkg_id, host_triplet], - |row| { - Ok(( - row.get::<_, Option>(0)?, - row.get::<_, i32>(1)?, - row.get::<_, Option>(2)?, - row.get::<_, Option>(3)?, - )) - }, - ) - .optional()?; - - match result { - Some((Some(stored_base), stored_revision, stored_hash, stored_remote)) - if stored_base == base_version => - { - // Same base version but different remote version → new upstream, reset - if let (Some(new_remote), Some(old_remote)) = - (remote_version, stored_remote.as_deref()) - { - if new_remote != old_remote { - return Ok(0); - } - } - - match (recipe_hash, stored_hash.as_deref()) { - // Both hashes known and differ: bump revision - (Some(new), Some(old)) if new != old => Ok(stored_revision + 1), - // Same hash or unknown: reuse current revision - _ => Ok(stored_revision), - } - } - _ => { - // Different base version or no record: start at 0 - Ok(0) - } - } - } - - /// Update recipe hash for a package - pub fn update_recipe_hash(&self, pkg_id: &str, host_triplet: &str, hash: &str) -> Result<()> { - let now = Utc::now().to_rfc3339(); - self.conn.execute( - "UPDATE packages SET recipe_hash = ?1, updated_at = ?2 WHERE pkg_id = ?3 AND host_triplet = ?4", - params![hash, now, pkg_id, host_triplet], - )?; - Ok(()) - } - - /// Mark package as outdated - pub fn mark_outdated( - &self, - pkg_id: &str, - host_triplet: &str, - upstream_version: &str, - ) -> Result<()> { - let now = Utc::now().to_rfc3339(); - self.conn.execute( - "UPDATE packages SET is_outdated = 1, upstream_version = ?1, updated_at = ?2 - WHERE pkg_id = ?3 AND host_triplet = ?4", - params![upstream_version, now, pkg_id, host_triplet], - )?; - Ok(()) - } - - /// Get packages needing rebuild for a host - pub fn get_packages_needing_rebuild(&self, host_triplet: &str) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT id, pkg_id, pkg_name, pkg_family, build_script, ghcr_pkg, host_triplet, - current_version, upstream_version, is_outdated, recipe_hash, - base_version, remote_version, revision, - last_build_date, last_build_id, last_build_status, ghcr_tag, - snapshots, created_at, updated_at - FROM packages - WHERE host_triplet = ?1 - AND (is_outdated = 1 OR last_build_status IS NULL OR last_build_status = 'pending') - ORDER BY pkg_name", - )?; - - let rows = stmt.query_map(params![host_triplet], Self::row_to_package_record)?; - - rows.collect::, _>>() - .map_err(Error::Sqlite) - } - - /// Get build statistics for a host - pub fn get_stats(&self, host_triplet: &str) -> Result { - self.conn - .query_row( - "SELECT - COUNT(*) as total, - COALESCE(SUM(CASE WHEN last_build_status = 'success' THEN 1 ELSE 0 END), 0) as successful, - COALESCE(SUM(CASE WHEN last_build_status = 'failed' THEN 1 ELSE 0 END), 0) as failed, - COALESCE(SUM(CASE WHEN last_build_status = 'pending' OR last_build_status IS NULL THEN 1 ELSE 0 END), 0) as pending, - COALESCE(SUM(CASE WHEN is_outdated = 1 THEN 1 ELSE 0 END), 0) as outdated - FROM packages WHERE host_triplet = ?1", - params![host_triplet], - |row| { - Ok(BuildStats { - total_packages: row.get(0)?, - successful: row.get(1)?, - failed: row.get(2)?, - pending: row.get(3)?, - outdated: row.get(4)?, - }) - }, - ) - .map_err(Error::Sqlite) - } - - /// Record a failed build with retry backoff - pub fn record_failure( - &self, - pkg_id: &str, - host_triplet: &str, - error_message: &str, - ) -> Result<()> { - let record = self - .get_package(pkg_id, host_triplet)? - .ok_or_else(|| Error::PackageNotFound(pkg_id.to_string()))?; - - let package_id = record.id.unwrap(); - let now = Utc::now(); - - // Get current failure count - let failure_count: i32 = self - .conn - .query_row( - "SELECT failure_count FROM failed_packages WHERE package_id = ?1", - params![package_id], - |row| row.get(0), - ) - .unwrap_or(0); - - let new_count = failure_count + 1; - - // Exponential backoff: 1h, 2h, 4h, 8h, max 24h - let backoff_hours = std::cmp::min(1 << failure_count, 24); - let next_retry = now + Duration::hours(backoff_hours as i64); - - self.conn.execute( - "INSERT INTO failed_packages (package_id, failure_count, last_failure_date, last_error_message, next_retry_date) - VALUES (?1, ?2, ?3, ?4, ?5) - ON CONFLICT(package_id) DO UPDATE SET - failure_count = ?2, - last_failure_date = ?3, - last_error_message = ?4, - next_retry_date = ?5", - params![ - package_id, - new_count, - now.to_rfc3339(), - error_message, - next_retry.to_rfc3339() - ], - )?; - - Ok(()) - } - - /// Clear failure record after successful build - pub fn clear_failure(&self, pkg_id: &str, host_triplet: &str) -> Result<()> { - if let Some(record) = self.get_package(pkg_id, host_triplet)? { - if let Some(id) = record.id { - self.conn.execute( - "DELETE FROM failed_packages WHERE package_id = ?1", - params![id], - )?; - } - } - Ok(()) - } - - /// List all packages with optional status filter - pub fn list_packages( - &self, - host_triplet: &str, - status_filter: Option, - include_outdated: bool, - ) -> Result> { - let base_query = - "SELECT id, pkg_id, pkg_name, pkg_family, build_script, ghcr_pkg, host_triplet, - current_version, upstream_version, is_outdated, recipe_hash, - base_version, remote_version, revision, - last_build_date, last_build_id, last_build_status, ghcr_tag, - snapshots, created_at, updated_at - FROM packages - WHERE host_triplet = ?1"; - - let query = match (&status_filter, include_outdated) { - (Some(_), true) => format!( - "{} AND (last_build_status = ?2 OR is_outdated = 1) ORDER BY pkg_name", - base_query - ), - (Some(_), false) => format!( - "{} AND last_build_status = ?2 ORDER BY pkg_name", - base_query - ), - (None, true) => format!("{} AND is_outdated = 1 ORDER BY pkg_name", base_query), - (None, false) => format!("{} ORDER BY pkg_name", base_query), - }; - - let mut stmt = self.conn.prepare(&query)?; - - let mut results = Vec::new(); - - if let Some(status) = status_filter { - let mut rows = stmt.query(params![host_triplet, status.as_str()])?; - while let Some(row) = rows.next()? { - results.push(Self::row_to_package_record(row)?); - } - } else { - let mut rows = stmt.query(params![host_triplet])?; - while let Some(row) = rows.next()? { - results.push(Self::row_to_package_record(row)?); - } - } - - Ok(results) - } - - /// Get recent build history - pub fn get_recent_builds( - &self, - host_triplet: &str, - limit: i64, - ) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT p.id, p.pkg_id, p.pkg_name, p.pkg_family, p.build_script, p.ghcr_pkg, p.host_triplet, - p.current_version, p.upstream_version, p.is_outdated, p.recipe_hash, - p.base_version, p.remote_version, p.revision, - p.last_build_date, p.last_build_id, p.last_build_status, p.ghcr_tag, - p.created_at, p.updated_at, - bh.id, bh.build_id, bh.version, bh.build_date, bh.build_status, - bh.duration_seconds, bh.ghcr_tag, bh.error_message - FROM packages p - JOIN build_history bh ON p.id = bh.package_id - WHERE p.host_triplet = ?1 - ORDER BY bh.build_date DESC - LIMIT ?2", - )?; - - let rows = stmt.query_map(params![host_triplet, limit], |row| { - let pkg = Self::row_to_package_record(row)?; - let history = BuildHistoryEntry { - id: Some(row.get(20)?), - package_id: pkg.id.unwrap_or(0), - build_id: row.get::<_, Option>(21)?.unwrap_or_default(), - version: row.get(22)?, - build_date: row - .get::<_, String>(23) - .ok() - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)) - .unwrap_or_else(Utc::now), - build_status: row - .get::<_, String>(24) - .ok() - .and_then(|s| BuildStatus::from_str(&s)) - .unwrap_or(BuildStatus::Pending), - duration_seconds: row.get(25).ok(), - artifact_size_bytes: None, - ghcr_tag: row.get(26).ok(), - ghcr_digest: None, - build_log_url: None, - error_message: row.get(27).ok(), - }; - Ok((pkg, history)) - })?; - - rows.collect::, _>>() - .map_err(Error::Sqlite) - } - - /// Helper to convert row to PackageRecord - /// - /// Expected column order: - /// 0: id, 1: pkg_id, 2: pkg_name, 3: pkg_family, 4: build_script, - /// 5: ghcr_pkg, 6: host_triplet, 7: current_version, 8: upstream_version, - /// 9: is_outdated, 10: recipe_hash, 11: base_version, 12: remote_version, - /// 13: revision, 14: last_build_date, 15: last_build_id, - /// 16: last_build_status, 17: ghcr_tag, 18: snapshots, 19: created_at, 20: updated_at - fn row_to_package_record(row: &rusqlite::Row) -> rusqlite::Result { - let snapshots_json: String = row - .get::<_, Option>(18)? - .unwrap_or_else(|| "[]".to_string()); - let snapshots: Vec = serde_json::from_str(&snapshots_json).unwrap_or_default(); - - Ok(PackageRecord { - id: Some(row.get(0)?), - pkg_id: row.get(1)?, - pkg_name: row.get(2)?, - pkg_family: row.get(3)?, - build_script: row.get(4)?, - ghcr_pkg: row.get(5)?, - host_triplet: row.get(6)?, - current_version: row.get(7)?, - upstream_version: row.get(8)?, - is_outdated: row.get::<_, i32>(9)? != 0, - recipe_hash: row.get(10)?, - base_version: row.get(11)?, - remote_version: row.get(12)?, - revision: row.get::<_, Option>(13)?.unwrap_or(0), - last_build_date: row - .get::<_, Option>(14)? - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)), - last_build_id: row.get(15)?, - last_build_status: row - .get::<_, Option>(16)? - .and_then(|s| BuildStatus::from_str(&s)), - ghcr_tag: row.get(17)?, - snapshots, - created_at: row - .get::<_, String>(19) - .ok() - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)) - .unwrap_or_else(Utc::now), - updated_at: row - .get::<_, String>(20) - .ok() - .and_then(|s| DateTime::parse_from_rfc3339(&s).ok()) - .map(|dt| dt.with_timezone(&Utc)) - .unwrap_or_else(Utc::now), - }) - } - - /// Prune old build history entries - pub fn prune_history(&self, keep_last: i64) -> Result { - let result = self.conn.execute( - "DELETE FROM build_history WHERE id NOT IN ( - SELECT id FROM build_history bh2 - WHERE bh2.package_id = build_history.package_id - ORDER BY build_date DESC - LIMIT ?1 - )", - params![keep_last], - )?; - Ok(result as i64) - } - - /// List all packages (no filters) - used for export - pub fn list_all_packages(&self) -> Result> { - let mut stmt = self.conn.prepare( - "SELECT id, pkg_id, pkg_name, pkg_family, build_script, ghcr_pkg, host_triplet, - current_version, upstream_version, is_outdated, recipe_hash, - base_version, remote_version, revision, - last_build_date, last_build_id, last_build_status, ghcr_tag, - snapshots, created_at, updated_at - FROM packages ORDER BY pkg_id, host_triplet", - )?; - - let rows = stmt.query_map([], Self::row_to_package_record)?; - - rows.collect::, _>>() - .map_err(Error::Sqlite) - } - - /// Import a package record (used by export from MongoDB) - pub fn import_package(&self, record: &PackageRecord) -> Result<()> { - let created = record.created_at.to_rfc3339(); - let updated = record.updated_at.to_rfc3339(); - let build_date = record.last_build_date.map(|d| d.to_rfc3339()); - let status_str = record.last_build_status.map(|s| s.as_str().to_string()); - let snapshots_json = - serde_json::to_string(&record.snapshots).unwrap_or_else(|_| "[]".to_string()); - - self.conn.execute( - "INSERT OR REPLACE INTO packages ( - pkg_id, pkg_name, pkg_family, build_script, ghcr_pkg, host_triplet, - current_version, upstream_version, is_outdated, recipe_hash, - base_version, remote_version, revision, - last_build_date, last_build_id, last_build_status, ghcr_tag, - snapshots, created_at, updated_at - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", - params![ - record.pkg_id, - record.pkg_name, - record.pkg_family, - record.build_script, - record.ghcr_pkg, - record.host_triplet, - record.current_version, - record.upstream_version, - record.is_outdated as i32, - record.recipe_hash, - record.base_version, - record.remote_version, - record.revision, - build_date, - record.last_build_id, - status_str, - record.ghcr_tag, - snapshots_json, - created, - updated, - ], - )?; - Ok(()) - } - - /// Import a build history entry (used by export from MongoDB) - pub fn import_build_history( - &self, - pkg_id: &str, - host_triplet: &str, - entry: &BuildHistoryEntry, - ) -> Result<()> { - let record = self.get_package(pkg_id, host_triplet)?; - let package_id = match record { - Some(r) => { - r.id.ok_or_else(|| Error::PackageNotFound(pkg_id.to_string()))? - } - None => return Err(Error::PackageNotFound(pkg_id.to_string())), - }; - - self.conn.execute( - "INSERT INTO build_history (package_id, build_id, version, build_date, build_status, duration_seconds, ghcr_tag, error_message, build_log_url) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - params![ - package_id, - entry.build_id, - entry.version, - entry.build_date.to_rfc3339(), - entry.build_status.as_str(), - entry.duration_seconds, - entry.ghcr_tag, - entry.error_message, - entry.build_log_url, - ], - )?; - Ok(()) - } - - /// Check if retry is allowed for a package - pub fn is_retry_allowed(&self, pkg_id: &str, host_triplet: &str) -> Result { - let record = self.get_package(pkg_id, host_triplet)?; - - if record.is_none() { - return Ok(true); // New package, allow - } - - let package_id = record.unwrap().id.unwrap(); - let now = Utc::now().to_rfc3339(); - - let allowed: bool = self - .conn - .query_row( - "SELECT next_retry_date IS NULL OR next_retry_date <= ?1 - FROM failed_packages WHERE package_id = ?2", - params![now, package_id], - |row| row.get(0), - ) - .unwrap_or(true); - - Ok(allowed) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_database() { - let db = CacheDatabase::in_memory().unwrap(); - let stats = db.get_stats("x86_64-linux").unwrap(); - assert_eq!(stats.total_packages, 0); - } - - #[test] - fn test_package_crud() { - let db = CacheDatabase::in_memory().unwrap(); - - // Create - let record = db - .get_or_create_package("github.com.test.pkg", "testpkg", "x86_64-linux") - .unwrap(); - assert_eq!(record.pkg_name, "testpkg"); - - // Read - let found = db - .get_package("github.com.test.pkg", "x86_64-linux") - .unwrap(); - assert!(found.is_some()); - - // Update - db.update_build_result( - "github.com.test.pkg", - "x86_64-linux", - "1.0.0", - BuildStatus::Success, - Some("build-123"), - Some("v1.0.0-x86_64-linux"), - Some("abc123"), - Some("1.0.0"), - None, - 0, - ) - .unwrap(); - - let updated = db - .get_package("github.com.test.pkg", "x86_64-linux") - .unwrap() - .unwrap(); - assert_eq!(updated.current_version, Some("1.0.0".to_string())); - assert_eq!(updated.last_build_status, Some(BuildStatus::Success)); - } - - #[test] - fn test_stats() { - let db = CacheDatabase::in_memory().unwrap(); - - db.get_or_create_package("pkg1", "pkg1", "x86_64-linux") - .unwrap(); - db.get_or_create_package("pkg2", "pkg2", "x86_64-linux") - .unwrap(); - - db.update_build_result( - "pkg1", - "x86_64-linux", - "1.0", - BuildStatus::Success, - Some("b1"), - None, - None, - Some("1.0"), - None, - 0, - ) - .unwrap(); - - let stats = db.get_stats("x86_64-linux").unwrap(); - assert_eq!(stats.total_packages, 2); - assert_eq!(stats.successful, 1); - } -} diff --git a/sbuild-linter/CHANGELOG.md b/sbuild-linter/CHANGELOG.md deleted file mode 100644 index 593ddd3..0000000 --- a/sbuild-linter/CHANGELOG.md +++ /dev/null @@ -1,58 +0,0 @@ - -## [0.5.0] - 2026-02-22 - -### Added - -- Add x_exec.container, build_deps, and checksum_bsum metadata - ([2f0964f](https://github.com/pkgforge/sbuilder/commit/2f0964f78e295687f2006444655a24c73715a2e2)) -- Add packages field for multi-package recipes and fix push logic - ([76bdd66](https://github.com/pkgforge/sbuilder/commit/76bdd66283e4736d041060bca93e80a6b53aeb2d)) - -### Fixed - -- Write remote_pkgver to pkgver file when defined in config - ([d47bfbe](https://github.com/pkgforge/sbuilder/commit/d47bfbeb86c1d8d01924206d350e3805b18350cd)) -- Fix snapshot and ghcr_pkg handling - ([3f9dc5e](https://github.com/pkgforge/sbuilder/commit/3f9dc5e71bd2ad60ac55707e2b641647815f8672)) -- Fix temp file and update sbuild file logging - ([1c85eca](https://github.com/pkgforge/sbuilder/commit/1c85eca2d7aedf66f9e7713004b331dd018b518b)) -- Fix sbuild and update workflow - ([6456561](https://github.com/pkgforge/sbuilder/commit/645656121abf239c381747bd065de82ac6ad23f8)) -- Fix pkgver - ([b3e27b2](https://github.com/pkgforge/sbuilder/commit/b3e27b26416827139c8605499de803678161edaf)) -- Fix validated output, add multi-file support - ([f908efd](https://github.com/pkgforge/sbuilder/commit/f908efd44fa3b91bb7daa6958b7fd30546ad3272)) -- Fix multi-line output - ([d1a60b9](https://github.com/pkgforge/sbuilder/commit/d1a60b9769e0057d5a53498c42533e1ee48b4e59)) -- Fix validated output identation and string quotes - ([9f6fc17](https://github.com/pkgforge/sbuilder/commit/9f6fc172f62c6eae0d1260135952ad8d2266d15f)) - -### Other - -- Make run optional - ([b7ff918](https://github.com/pkgforge/sbuilder/commit/b7ff91876078dfcade44030924c92970cde2537f)) -- Replace serde_yml/serde_yaml with saphyr and simplify schema - ([2f1f48e](https://github.com/pkgforge/sbuilder/commit/2f1f48e053ade96a5614e86170202ffb80c3ea5c)) -- Centralize dependencies under [workspace.dependencies] - ([92db2b9](https://github.com/pkgforge/sbuilder/commit/92db2b9d21dc910adbce96cc146b9ba13d959a85)) -- Consolidate binaries into unified sbuild CLI with subcommands - ([9818b33](https://github.com/pkgforge/sbuilder/commit/9818b33710e5a53a4b80e7fe86b3e1a9a77ce4ad)) -- Expose REMOTE_PKGVER - ([c3e305b](https://github.com/pkgforge/sbuilder/commit/c3e305b64b5d2bb9972c0797c8573eaf7d201cf4)) -- Handle remote_pkgver - ([500b1bd](https://github.com/pkgforge/sbuilder/commit/500b1bd3034b9b458b91f43393e245b878a8309a)) -- Update version - ([ba368ef](https://github.com/pkgforge/sbuilder/commit/ba368ef98da9602089727198f900d70cf099fe07)) -- Handle ghcr_pkg - ([be8a7c8](https://github.com/pkgforge/sbuilder/commit/be8a7c8ac2feb57837b13aea86e95e1ed101917a)) -- Update - ([6c7c7cb](https://github.com/pkgforge/sbuilder/commit/6c7c7cb287850fd4abfc7ae467f7ce9498d7d47a)) -- Update dependencies and release - ([96590b8](https://github.com/pkgforge/sbuilder/commit/96590b81fda153fd191ec262c3c69efddb620b2f)) -- Quote non-alphanumeric keys - ([108657b](https://github.com/pkgforge/sbuilder/commit/108657b5aab74900658e5c9ccb29d1a4b380b7ce)) -- Parse boolean as string in description key - ([4695270](https://github.com/pkgforge/sbuilder/commit/469527077ff155ee3fca1140cff14810b577430a)) -- Use url library for url validation - ([87e96fb](https://github.com/pkgforge/sbuilder/commit/87e96fb2c4ca3f3e019d85f7d493500bd49529f8)) -- Handle provides and automate dynamic appimage conversion - ([6cbc70a](https://github.com/pkgforge/sbuilder/commit/6cbc70af94b2653a3c4003627228068e8a731abc)) -- Support custom outdir, timeout and debug mode - ([0fd4e14](https://github.com/pkgforge/sbuilder/commit/0fd4e14342b905b7eeb817e380ad7c86e38b36d6)) -- Support entrypoint, licenses, and host checks - ([ffe3ea9](https://github.com/pkgforge/sbuilder/commit/ffe3ea9f8a877854639f3fedae2b6c03bd3cdaad)) -- Update logger and fix _disabled_reason validation - ([0e2dca5](https://github.com/pkgforge/sbuilder/commit/0e2dca5abe2347c4a44c36ef89557855f7021032)) -- Disable app_id generation, add custom timeout - ([2ddf03b](https://github.com/pkgforge/sbuilder/commit/2ddf03b7636d1cd92aaaadd50c0d94c69c015146)) -- Fix x_exec validation - ([9f14133](https://github.com/pkgforge/sbuilder/commit/9f141334c7f5ad79068d39c632acdfa84e117b25)) -- Extend license and x_exec - ([d466b61](https://github.com/pkgforge/sbuilder/commit/d466b613d7e3a911206141b021f2f5c90cca4b42)) -- Add support for _disabled_reason, extended description/desktop/icon - ([ee28ff6](https://github.com/pkgforge/sbuilder/commit/ee28ff65eb4dd94f7d35eb4050308ed1662e3631)) -- Initialize sbuilder - ([e688ee1](https://github.com/pkgforge/sbuilder/commit/e688ee17ae8cae9b4ffada40355155b524abcab6)) -- Don't overwrite success/fail file, return buildconfig on lint - ([59c6981](https://github.com/pkgforge/sbuilder/commit/59c6981b163556560340662fe676d1db9b63aff2)) -- Introduce pkgver fetch timeout, flag to write success/fail to file - ([e82b024](https://github.com/pkgforge/sbuilder/commit/e82b024eacb88d330d204c9b4b9f15b5b14585b7)) -- Add support for replacing original file on success - ([2abeae5](https://github.com/pkgforge/sbuilder/commit/2abeae58077b821037c8db896a109bbe55fb5956)) -- Sync logging, fix validation to include all error for a field - ([f9278aa](https://github.com/pkgforge/sbuilder/commit/f9278aaf07925a9b82f7673cb217abe9bfb02eb1)) -- Refactor codebase - ([e923c61](https://github.com/pkgforge/sbuilder/commit/e923c613dd789bad7dc9ace1f2f1922353b0a8aa)) -- Sbuild-linter v0.2.0 - ([17b9927](https://github.com/pkgforge/sbuilder/commit/17b9927e5a59a7f1325489aac0c173ea2e53620f)) -- Add parallel support - ([b17af7a](https://github.com/pkgforge/sbuilder/commit/b17af7ac6baebdee973e0893aa2578ecffb9ecc6)) -- Write scripts to tmp file - ([b02eb60](https://github.com/pkgforge/sbuilder/commit/b02eb60aa1266f34089d137a0593a3d02dd1b930)) -- Remove set -e in script - ([6ce5787](https://github.com/pkgforge/sbuilder/commit/6ce5787453ef72b555a4d553dbf933d89ddf6acb)) -- Better error message for invalid files - ([b7502ef](https://github.com/pkgforge/sbuilder/commit/b7502ef9c7973a6872fae757266693f09267fc5f)) -- Add release workflow - ([1516d35](https://github.com/pkgforge/sbuilder/commit/1516d35b5722743a83b5a08c3a3c5a517cde50b7)) -- Refactor pkgver processing - ([fa62453](https://github.com/pkgforge/sbuilder/commit/fa624535bf7ee5d6a3bd59e904bd1adcb4ce27be)) -- Add x_exec checks, fix pkg_id - ([001b9a7](https://github.com/pkgforge/sbuilder/commit/001b9a7620baece8fab69de4ad26b4b9a4999305)) -- Reduce unused attributes - ([c5f9233](https://github.com/pkgforge/sbuilder/commit/c5f923398d02ac67235b92ab329c5514c10b1f0a)) -- Intialize sbuild linter - ([1e2af7b](https://github.com/pkgforge/sbuilder/commit/1e2af7b6520c443b19c3d435b0c092a322bfc58f)) diff --git a/sbuild-linter/Cargo.toml b/sbuild-linter/Cargo.toml deleted file mode 100644 index a00120b..0000000 --- a/sbuild-linter/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "sbuild-linter" -description = "Linter for SBUILD package files" -version = "0.5.0" -license.workspace = true -edition.workspace = true -repository.workspace = true -keywords.workspace = true - -[dependencies] -colored.workspace = true -indexmap.workspace = true -saphyr.workspace = true -tempfile.workspace = true -url.workspace = true -which.workspace = true - -[lib] -name = "sbuild_linter" -path = "src/lib.rs" diff --git a/sbuild-linter/src/build_config/mod.rs b/sbuild-linter/src/build_config/mod.rs deleted file mode 100644 index b93abdb..0000000 --- a/sbuild-linter/src/build_config/mod.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::{ - fs::File, - io::{self, BufWriter, Write}, -}; - -use crate::{comments::Comments, description::Description, get_pkg_id, xexec::XExec, BuildAsset}; - -/// Per-package configuration for multi-package recipes -#[derive(Debug, Clone, Default)] -pub struct PackageConfig { - pub provides: Vec, -} - -#[derive(Debug, Default)] -pub struct BuildConfig { - pub _disabled: bool, - pub pkg: String, - pub pkg_id: String, - pub pkg_type: Option, - pub pkgver: Option, - pub remote_pkgver: Option, - pub app_id: Option, - pub build_util: Option>, - pub build_asset: Option>, - pub build_deps: Option>, - pub category: Vec, - pub description: Option, - pub homepage: Option>, - pub maintainer: Option>, - pub license: Option>, - pub note: Option>, - pub provides: Option>, - pub packages: Option>, - pub repology: Option>, - pub src_url: Vec, - pub tag: Option>, - pub ghcr_pkg: Option, - pub snapshots: Option>, - pub x_exec: XExec, -} - -impl BuildConfig { - pub fn set_pkg_id_from_src_url(&mut self) { - if self.pkg_id.is_empty() && !self.src_url.is_empty() { - self.pkg_id = get_pkg_id(&self.src_url[0]); - } - } - - pub fn write_yaml( - &self, - writer: &mut BufWriter, - indent: usize, - comments: Comments, - ) -> io::Result<()> { - let indent_str = " ".repeat(indent); - - for c in &comments.header_comments { - writeln!(writer, "{}", c)?; - } - - let write_field_comments = |writer: &mut BufWriter, field: &str| -> io::Result<()> { - if let Some(comments) = comments.field_comments.get(field) { - for comment in comments { - writeln!(writer, "{}", comment)?; - } - } - Ok(()) - }; - - write_field_comments(writer, "_disabled")?; - writeln!(writer, "{}_disabled: {}\n", indent_str, self._disabled)?; - - write_field_comments(writer, "pkg")?; - writeln!(writer, "{}pkg: \"{}\"", indent_str, self.pkg)?; - - write_field_comments(writer, "pkg_id")?; - writeln!(writer, "{}pkg_id: \"{}\"", indent_str, self.pkg_id)?; - - write_field_comments(writer, "pkg_type")?; - if let Some(ref pkg_type) = self.pkg_type { - writeln!(writer, "{}pkg_type: \"{}\"", indent_str, pkg_type)?; - } - - write_field_comments(writer, "pkgver")?; - if let Some(ref pkgver) = self.pkgver { - writeln!(writer, "{}pkgver: \"{}\"", indent_str, pkgver)?; - } - - write_field_comments(writer, "remote_pkgver")?; - if let Some(ref remote_pkgver) = self.remote_pkgver { - writeln!(writer, "{}remote_pkgver: \"{}\"", indent_str, remote_pkgver)?; - } - - write_field_comments(writer, "app_id")?; - if let Some(ref app_id) = self.app_id { - writeln!(writer, "{}app_id: \"{}\"", indent_str, app_id)?; - } - - write_field_comments(writer, "build_util")?; - if let Some(ref build_util) = self.build_util { - writeln!(writer, "{}build_util:", indent_str)?; - for util in build_util { - writeln!(writer, "{} - \"{}\"", indent_str, util)?; - } - } - - write_field_comments(writer, "build_asset")?; - if let Some(ref build_asset) = self.build_asset { - writeln!(writer, "{}build_asset:", indent_str)?; - for asset in build_asset { - writeln!(writer, "{} - url: \"{}\"", indent_str, asset.url)?; - writeln!(writer, "{} out: \"{}\"", indent_str, asset.out)?; - } - } - - write_field_comments(writer, "build_deps")?; - if let Some(ref build_deps) = self.build_deps { - writeln!(writer, "{}build_deps:", indent_str)?; - for dep in build_deps { - writeln!(writer, "{} - \"{}\"", indent_str, dep)?; - } - } - - write_field_comments(writer, "ghcr_pkg")?; - if let Some(ref ghcr_pkg) = self.ghcr_pkg { - writeln!(writer, "{}ghcr_pkg: \"{}\"", indent_str, ghcr_pkg)?; - } - - write_field_comments(writer, "category")?; - writeln!(writer, "{}category:", indent_str)?; - for cat in &self.category { - writeln!(writer, "{} - \"{}\"", indent_str, cat)?; - } - - write_field_comments(writer, "description")?; - self.description - .clone() - .unwrap() - .write_yaml(writer, indent)?; - - write_field_comments(writer, "homepage")?; - if let Some(ref homepage) = self.homepage { - writeln!(writer, "{}homepage:", indent_str)?; - for url in homepage { - writeln!(writer, "{} - \"{}\"", indent_str, url)?; - } - } - - write_field_comments(writer, "maintainer")?; - if let Some(ref maintainer) = self.maintainer { - writeln!(writer, "{}maintainer:", indent_str)?; - for m in maintainer { - writeln!(writer, "{} - \"{}\"", indent_str, m)?; - } - } - - write_field_comments(writer, "license")?; - if let Some(ref license) = self.license { - writeln!(writer, "{}license:", indent_str)?; - for lc in license { - writeln!(writer, "{} - \"{}\"", indent_str, lc)?; - } - } - - write_field_comments(writer, "note")?; - if let Some(ref note) = self.note { - writeln!(writer, "{}note:", indent_str)?; - for n in note { - writeln!(writer, "{} - \"{}\"", indent_str, n)?; - } - } - - write_field_comments(writer, "provides")?; - if let Some(ref provides) = self.provides { - writeln!(writer, "{}provides:", indent_str)?; - for p in provides { - writeln!(writer, "{} - \"{}\"", indent_str, p)?; - } - } - - write_field_comments(writer, "packages")?; - if let Some(ref packages) = self.packages { - writeln!(writer, "{}packages:", indent_str)?; - for (name, pkg) in packages { - writeln!(writer, "{} {}:", indent_str, name)?; - if !pkg.provides.is_empty() { - writeln!(writer, "{} provides:", indent_str)?; - for p in &pkg.provides { - writeln!(writer, "{} - \"{}\"", indent_str, p)?; - } - } - } - } - - write_field_comments(writer, "repology")?; - if let Some(ref repology) = self.repology { - writeln!(writer, "{}repology:", indent_str)?; - for r in repology { - writeln!(writer, "{} - \"{}\"", indent_str, r)?; - } - } - - write_field_comments(writer, "src_url")?; - writeln!(writer, "{}src_url:", indent_str)?; - for url in &self.src_url { - writeln!(writer, "{} - \"{}\"", indent_str, url)?; - } - - write_field_comments(writer, "tag")?; - if let Some(ref tag) = self.tag { - writeln!(writer, "{}tag:", indent_str)?; - for t in tag { - writeln!(writer, "{} - \"{}\"", indent_str, t)?; - } - } - - write_field_comments(writer, "snapshots")?; - if let Some(ref snapshots) = self.snapshots { - writeln!(writer, "{}snapshots:", indent_str)?; - for s in snapshots { - writeln!(writer, "{} - \"{}\"", indent_str, s)?; - } - } - - write_field_comments(writer, "x_exec")?; - writeln!(writer, "{}x_exec:", indent_str)?; - self.x_exec.write_yaml(writer, indent + 2)?; - - Ok(()) - } -} diff --git a/sbuild-linter/src/categories b/sbuild-linter/src/categories deleted file mode 100644 index ff3914b..0000000 --- a/sbuild-linter/src/categories +++ /dev/null @@ -1,143 +0,0 @@ -2DGraphics -3DGraphics -Accessibility -ActionGame -Adult -AdventureGame -Amusement -Applet -ArcadeGame -Archiving -Art -ArtificialIntelligence -Astronomy -Audio -AudioVideo -AudioVideoEditing -Biology -BlocksGame -BoardGame -Building -COSMIC -Calculator -Calendar -CardGame -Chart -Chat -Chemistry -Clock -Compression -ComputerScience -ConsoleOnly -Construction -ContactManagement -Core -DDE -DataVisualization -Database -Debugger -DesktopSettings -Development -Dialup -Dictionary -DiscBurning -Documentation -Economy -Education -Electricity -Electronics -Email -Emulator -Feed -FileManager -FileTools -FileTransfer -Filesystem -Finance -FlowChart -GNOME -GTK -GUIDesigner -Game -Geography -Geology -Geoscience -Graphics -HamRadio -HardwareSettings -History -Humanities -IDE -IRCClient -ImageProcessing -InstantMessaging -Java -KDE -KidsGame -Languages -Literature -LogicGame -Maps -Math -MedicalSoftware -Midi -Mixer -Monitor -Motif -Music -Network -News -NumericalAnalysis -OCR -Office -P2P -PDA -PackageManager -ParallelComputing -Photography -Physics -Player -Presentation -Printing -Profiling -ProjectManagement -Publishing -Qt -RasterGraphics -Recorder -RemoteAccess -RevisionControl -Robotics -RolePlaying -Scanning -Science -Screensaver -Security -Sequencer -Settings -Shell -Shooter -Simulation -Sports -SportsGame -Spreadsheet -StrategyGame -System -TV -Telephony -TelephonyTools -TerminalEmulator -TextEditor -TextTools -Translation -TrayIcon -Tuner -Utility -VectorGraphics -Video -VideoConference -Viewer -WebBrowser -WebDevelopment -WordProcessor -XFCE diff --git a/sbuild-linter/src/comments.rs b/sbuild-linter/src/comments.rs deleted file mode 100644 index 9b4b155..0000000 --- a/sbuild-linter/src/comments.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::{ - fs::File, - io::{self, BufRead, BufReader}, -}; - -use indexmap::IndexMap; - -fn extract_field_name(line: &str) -> Option { - line.split(':').next().map(|s| s.trim().to_string()) -} - -#[derive(Default, Debug)] -pub struct Comments { - pub field_comments: IndexMap>, - pub header_comments: Vec, - pub field_order: Vec, -} - -impl Comments { - pub fn new() -> Self { - Self::default() - } - - // this only works for root level fields - // inner comments are assigned to adjacent lines, so won't work. - pub fn parse_comments(&mut self, file_path: &str) -> io::Result<()> { - let file = File::open(file_path)?; - let reader = BufReader::new(file); - let mut current_comments = Vec::new(); - let mut shebang_added = false; - - for line in reader.lines() { - let line = line?; - let trimmed = line.trim(); - - if trimmed.starts_with("#!/SBUILD") { - if !shebang_added { - self.header_comments.push(trimmed.to_string()); - shebang_added = true; - } - continue; - } - - if trimmed.starts_with('#') { - current_comments.push(line.to_string()); - } else if !trimmed.is_empty() { - if let Some(field_name) = extract_field_name(trimmed) { - if !current_comments.is_empty() { - self.field_comments - .insert(field_name.clone(), current_comments.clone()); - current_comments.clear(); - } - self.field_order.push(field_name); - } - } - } - - if !current_comments.is_empty() { - self.header_comments.extend(current_comments); - } - - Ok(()) - } -} diff --git a/sbuild-linter/src/description.rs b/sbuild-linter/src/description.rs deleted file mode 100644 index b216957..0000000 --- a/sbuild-linter/src/description.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::{ - fs::File, - io::{self, BufWriter, Write}, -}; - -use indexmap::IndexMap; - -#[derive(Debug, Clone)] -pub enum Description { - Simple(String), - Map(IndexMap), -} - -impl Description { - pub fn write_yaml(&self, writer: &mut BufWriter, indent: usize) -> io::Result<()> { - let indent_str = " ".repeat(indent); - - match self { - Description::Simple(value) => { - writeln!(writer, "{}description: \"{}\"", indent_str, value)?; - } - Description::Map(map) => { - writeln!(writer, "{}description:", indent_str)?; - for (key, value) in map { - let key = if key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { - key.to_string() - } else { - format!("\"{}\"", key) - }; - writeln!(writer, "{} {}: \"{}\"", indent_str, key, value)?; - } - } - } - - Ok(()) - } -} diff --git a/sbuild-linter/src/error.rs b/sbuild-linter/src/error.rs deleted file mode 100644 index 87eeb1d..0000000 --- a/sbuild-linter/src/error.rs +++ /dev/null @@ -1,56 +0,0 @@ -use colored::Colorize; - -use crate::logger::TaskLogger; - -#[derive(Debug)] -pub enum Severity { - Warn, - Error, -} - -#[derive(Debug)] -pub struct ErrorDetails { - pub field: String, - pub message: String, - pub line_number: usize, - pub severity: Severity, -} - -pub fn highlight_error_line( - yaml_str: &str, - line_number: usize, - is_fatal: bool, - logger: &TaskLogger, -) { - let context_range = 3; - let start_line = line_number.saturating_sub(context_range); - let end_line = if line_number + context_range < yaml_str.lines().count() { - line_number + context_range - } else { - yaml_str.lines().count() - }; - - let lines: Vec<&str> = yaml_str - .lines() - .skip(start_line) - .take(end_line - start_line) - .collect(); - - for (index, line) in lines.iter().enumerate() { - let current_line_number = start_line + index + 1; - if current_line_number == line_number { - let msg = format!("--> {}: {}", current_line_number, line); - logger.custom_error(&format!( - "{}", - if is_fatal { - msg.red().bold() - } else { - msg.yellow().bold() - } - )); - } else { - logger.custom_error(&format!(" {}: {}", current_line_number, line)); - } - } - logger.custom_error(""); -} diff --git a/sbuild-linter/src/lib.rs b/sbuild-linter/src/lib.rs deleted file mode 100644 index 34e2fb2..0000000 --- a/sbuild-linter/src/lib.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::{ - env, - fmt::Display, - fs::{File, Permissions}, - io::{BufRead, BufReader, BufWriter, Write}, - os::unix::fs::PermissionsExt, - path::Path, - process::{Command, ExitStatus}, - sync, thread, - time::Duration, -}; - -use build_config::BuildConfig; -use colored::Colorize; -use comments::Comments; -use logger::TaskLogger; -use saphyr::{LoadableYamlNode, MarkedYamlOwned}; -use tempfile::NamedTempFile; -use validator::ValidationContext; - -pub mod build_config; -pub mod comments; -pub mod description; -pub mod error; -pub mod logger; -pub mod semaphore; -pub mod validator; -pub mod xexec; - -pub const VALID_PKG_TYPES: [&str; 10] = [ - "appbundle", - "appimage", - "archive", - "dynamic", - "flatimage", - "gameimage", - "nixappimage", - "onelf", - "runimage", - "static", -]; -pub const VALID_CATEGORIES: &str = include_str!("categories"); -pub const VALID_ARCH: [&str; 4] = ["aarch64", "loongarch64", "riscv64", "x86_64"]; -pub const VALID_OS: [&str; 6] = ["freebsd", "illumos", "linux", "netbsd", "openbsd", "redox"]; - -#[derive(Debug, Clone)] -pub struct BuildAsset { - pub url: String, - pub out: String, -} - -pub struct Linter { - logger: TaskLogger, - timeout: Duration, -} - -impl Linter { - pub fn new(logger: TaskLogger, timeout: Duration) -> Self { - Linter { logger, timeout } - } - - pub fn lint( - &self, - file_path: &str, - inplace: bool, - disable_shellcheck: bool, - pkgver: bool, - ) -> Option { - let logger = &self.logger; - let yaml_str = match self.read_yaml(file_path) { - Ok(y) => y, - Err(err) => { - eprintln!("{}", err); - return None; - } - }; - - let path = Path::new(&file_path); - let real_path = if path.is_absolute() { - path - } else { - let current_dir = env::current_dir().expect("Failed to get current directory"); - ¤t_dir.join(path) - }; - logger.info(format!("Linting {} ({})\n", file_path, real_path.display())); - match self.validate_yaml(&yaml_str) { - Ok(config) => { - if disable_shellcheck { - logger.info("Skipping shellcheck"); - } else { - logger.info("Performing shellcheck"); - if !self.is_shellcheck_success(&config) { - return None; - } - logger.success("Shellcheck passed"); - } - if let Some(pkgver_path) = pkgver.then(|| format!("{}.pkgver", file_path)) { - if !self.generate_pkgver(&config, &pkgver_path) { - return None; - } - }; - - let mut comments = Comments::new(); - comments.parse_comments(file_path).unwrap(); - - let output_path = inplace - .then_some(file_path.to_string()) - .unwrap_or_else(|| format!("{}.validated", file_path)); - let file = File::create(&output_path).unwrap(); - let mut writer = BufWriter::new(file); - - config.write_yaml(&mut writer, 0, comments).unwrap(); - logger.info("SBUILD validation successful."); - logger.info(&format!( - "Validated YAML has been written to {}", - output_path - )); - return Some(config); - } - Err(_) => { - logger.error("SBUILD validation failed."); - } - }; - None - } - - fn validate_yaml(&self, yaml_str: &str) -> Result { - let docs = MarkedYamlOwned::load_from_str(yaml_str) - .map_err(|e: saphyr::ScanError| e.to_string())?; - let doc = docs.into_iter().next().ok_or("Empty YAML")?; - let mut ctx = ValidationContext::new(yaml_str, self.logger.clone()); - ctx.validate(&doc).ok_or_else(|| "Validation failed".into()) - } - - fn read_yaml(&self, file_path: &str) -> Result { - let logger = &self.logger; - let Ok(file) = File::open(file_path) else { - return Err(FileError::NotFound(file_path.into())); - }; - let reader = BufReader::new(file); - - let mut yaml_content = String::new(); - let mut lines = reader.lines(); - - if let Some(line) = lines.next() { - let line = line.map_err(|_| FileError::InvalidFile(file_path.into()))?; - if !line.trim_start().starts_with("#!/SBUILD") { - logger.warn("File doesn't start with '#!/SBUILD'"); - } - } else { - return Err(FileError::InvalidFile(file_path.into())); - } - - for line in lines { - let line = line.map_err(|_| FileError::InvalidFile(file_path.into()))?; - yaml_content.push_str(&line); - yaml_content.push('\n'); - } - - Ok(yaml_content) - } - - fn run_shellcheck(&self, script: &str, severity: &str) -> std::io::Result { - let tmp = temp_script_file(script); - - let out = Command::new("shellcheck") - .arg(format!("--severity={}", severity)) - .arg(tmp.path()) - .status(); - - tmp.close()?; - out - } - - fn shellcheck(&self, script: &str) -> std::io::Result<()> { - if !self.run_shellcheck(script, "error")?.success() { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Shellcheck emitted errors.", - )); - } - - let _ = self.run_shellcheck(script, "warning"); - - Ok(()) - } - - pub fn generate_pkgver(&self, config: &BuildConfig, pkgver_path: &str) -> bool { - let logger = &self.logger; - let x_exec = &config.x_exec; - let mut success = false; - - match config.pkgver { - Some(ref pkgver) => { - logger.info("Using hard-coded pkgver"); - let file = File::create(pkgver_path).unwrap(); - let mut writer = BufWriter::new(file); - let _ = writer.write_all(pkgver.as_bytes()); - - if let Some(ref remote_pkgver) = config.remote_pkgver { - let _ = writer.write_all(b"\n"); - let _ = writer.write_all(remote_pkgver.as_bytes()); - logger.success(&format!( - "Version ({}) with remote_pkgver ({}) written to {}", - pkgver, - remote_pkgver, - pkgver_path.bright_cyan() - )); - } else { - logger.success(&format!( - "Version ({}) from pkgver written to {}", - pkgver, - pkgver_path.bright_cyan() - )); - } - success = true; - } - None => { - if let Some(ref pkgver) = x_exec.pkgver { - let script = format!("#!/usr/bin/env {}\n{}", x_exec.shell, pkgver); - let tmp = temp_script_file(&script); - let tmp_path = tmp.into_temp_path(); - - let (tx, rx) = sync::mpsc::channel(); - thread::spawn({ - let tmp_path = tmp_path.to_path_buf(); - move || { - let cmd = Command::new(&tmp_path).output(); - let _ = tx.send(cmd); - } - }); - - match rx.recv_timeout(self.timeout) { - Ok(cmd_result) => { - match cmd_result { - Ok(cmd) => { - if cmd.status.success() { - if !cmd.stderr.is_empty() { - logger.error("x.exec.pkgver script produced error."); - logger.error(String::from_utf8_lossy(&cmd.stderr)); - } else { - let out = cmd.stdout; - let output_str = String::from_utf8_lossy(&out); - let output_str = output_str.trim(); - if output_str.is_empty() { - logger.warn("x_exec.pkgver produced empty result. Skipping..."); - } else { - let lines: Vec<&str> = output_str.lines().collect(); - if lines.len() > 2 { - logger.error( - "x_exec.pkgver should produce at most two outputs (pkgver and optionally remote_pkgver)", - ); - lines.iter().for_each(|line| { - logger.info(format!("-> {}", line.trim())); - }); - } else { - let pkgver = lines[0].trim(); - let file = File::create(pkgver_path).unwrap(); - let mut writer = BufWriter::new(file); - let _ = writer.write_all(pkgver.as_bytes()); - - if lines.len() == 2 { - let remote_pkgver = lines[1].trim(); - let _ = writer.write_all(b"\n"); - let _ = writer - .write_all(remote_pkgver.as_bytes()); - logger.success(format!("Fetched version ({}) with remote_pkgver ({}) using x_exec.pkgver written to {}", - pkgver, remote_pkgver, pkgver_path.bright_cyan())); - } else if let Some(ref remote_pkgver) = - config.remote_pkgver - { - let _ = writer.write_all(b"\n"); - let _ = writer - .write_all(remote_pkgver.as_bytes()); - logger.success(format!("Fetched version ({}) with remote_pkgver ({}) from config written to {}", - pkgver, remote_pkgver, pkgver_path.bright_cyan())); - } else { - logger.success(format!("Fetched version ({}) using x_exec.pkgver written to {}", pkgver, pkgver_path.bright_cyan())); - } - success = true; - } - } - } - } else { - logger.error(format!("{} -> Failed to read output from pkgver script. Please make sure the script is valid.", "x_exec.pkgver".bold())); - if !cmd.stderr.is_empty() { - logger.error(String::from_utf8_lossy(&cmd.stderr)); - } - } - } - Err(err) => { - logger.error(format!( - "{} -> pkgver script failed to execute. {}", - "x_exec.pkgver".bold(), - err - )); - } - } - } - Err(_) => { - logger.error(format!( - "{} -> pkgver script timed out after {:#?}", - "x_exec.pkgver".bold(), - self.timeout - )); - } - } - - let _ = tmp_path.close(); - } else { - // we don't care if the pkgver is not set - success = true; - } - } - } - success - } - - fn is_shellcheck_success(&self, config: &BuildConfig) -> bool { - let logger = &self.logger; - let x_exec = &config.x_exec; - let mut success = true; - - if let Some(ref run) = x_exec.run { - let script = format!("#!/usr/bin/env {}\n{}", x_exec.shell, run); - if self.shellcheck(&script).is_err() { - logger.error(format!( - "{} -> Shellcheck verification failed.", - "x_exec.run".bold() - )); - success = false; - } - } - - if let Some(ref pkgver) = x_exec.pkgver { - let script = format!("#!/usr/bin/env {}\n{}", x_exec.shell, pkgver); - if self.shellcheck(&script).is_err() { - logger.error(format!( - "{} -> Shellcheck verification failed.", - "x_exec.pkgver".bold() - )); - success = false; - } - } - - success - } -} - -enum FileError { - InvalidFile(String), - NotFound(String), -} - -impl Display for FileError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FileError::InvalidFile(fp) => { - writeln!(f, "Invalid file {}. Please provide a valid YAML file.", fp) - } - FileError::NotFound(fp) => writeln!(f, "File {} not found.", fp), - } - } -} - -fn get_pkg_id(src: &str) -> String { - let (_, url) = src.split_once("://").unwrap(); - let (url, _) = url.split_once('?').unwrap_or((url, "")); - url.replace('/', ".").trim_matches('.').to_string() -} - -fn temp_script_file(script: &str) -> NamedTempFile { - let mut named_temp_file = tempfile::Builder::new() - .prefix("sbuild-linter-") - .permissions(Permissions::from_mode(0o755)) - .rand_bytes(8) - .disable_cleanup(true) - .tempfile() - .expect("Failed to create temp file"); - - let file = named_temp_file.as_file_mut(); - - file.write_all(script.as_bytes()) - .expect("Failed to write to temporary script file"); - - named_temp_file -} diff --git a/sbuild-linter/src/logger.rs b/sbuild-linter/src/logger.rs deleted file mode 100644 index 056a528..0000000 --- a/sbuild-linter/src/logger.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::{ - fs::{self, File, OpenOptions}, - io::Write, - path::{Path, PathBuf}, - sync::{mpsc::Sender, Arc, Mutex}, - time::Instant, -}; - -pub enum LogMessage { - Info(String), - Error(String), - Warn(String), - Success(String), - CustomError(String), - Done, -} - -#[derive(Clone)] -pub struct LogManager { - sender: Sender, -} - -impl LogManager { - pub fn new(sender: Sender) -> Self { - Self { sender } - } - - pub fn done(&self) { - let _ = self.sender.send(LogMessage::Done); - } - - pub fn create_logger>(&self, file_path: Option

) -> TaskLogger { - let file = if let Some(file_path) = file_path { - let file_path = file_path.as_ref(); - let file = OpenOptions::new() - .create(true) - .append(true) - .open(file_path) - .unwrap(); - Some(Arc::new(Mutex::new(LogFile { - file, - path: file_path.to_path_buf(), - }))) - } else { - None - }; - TaskLogger { - sender: self.sender.clone(), - file, - start_time: Instant::now(), - } - } -} - -#[derive(Clone)] -pub struct TaskLogger { - sender: Sender, - file: Option>>, - start_time: Instant, -} - -struct LogFile { - file: File, - path: PathBuf, -} - -impl TaskLogger { - pub fn write_to_file(&self, msg: impl Into) { - let msg = msg.into(); - if let Some(file) = &self.file { - if let Ok(mut file_guard) = file.lock() { - let elapsed = self.start_time.elapsed(); - - let total_seconds = elapsed.as_secs(); - let minutes = total_seconds / 60; - let seconds = total_seconds % 60; - let milliseconds = (elapsed.subsec_millis()) as u64; - - let msg = format!("{}\n", msg); - for line in msg.lines() { - let timestamp = format!("[{:02}:{:02}.{:03}]", minutes, seconds, milliseconds); - - let line = if line.is_empty() { - format!("{}", timestamp) - } else { - format!("{}➜ {}", timestamp, line) - }; - let _ = writeln!(file_guard.file, "{}", line); - } - } - } - } - - pub fn move_log_file>(&self, new_path: P) -> std::io::Result<()> { - if let Some(file) = &self.file { - let mut file_guard = file.lock().map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to acquire lock on log file", - ) - })?; - - file_guard.file.flush()?; - let old_path = file_guard.path.clone(); - - drop(file_guard); - - if old_path.exists() { - fs::copy(&old_path, &new_path)?; - fs::remove_file(&old_path)?; - } - - let new_file = OpenOptions::new().append(true).open(new_path.as_ref())?; - - if let Ok(mut file_guard) = file.lock() { - file_guard.file = new_file; - file_guard.path = new_path.as_ref().to_path_buf(); - } - } - Ok(()) - } - - pub fn info(&self, msg: impl Into) { - let msg = msg.into(); - self.write_to_file(&msg); - let _ = self.sender.send(LogMessage::Info(msg.to_string())); - } - - pub fn warn(&self, msg: impl Into) { - let msg = msg.into(); - self.write_to_file(&msg); - let _ = self.sender.send(LogMessage::Warn(msg.to_string())); - } - - pub fn error(&self, msg: impl Into) { - let msg = msg.into(); - self.write_to_file(&msg); - let _ = self.sender.send(LogMessage::Error(msg.to_string())); - } - - pub fn success(&self, msg: impl Into) { - let msg = msg.into(); - self.write_to_file(&msg); - let _ = self.sender.send(LogMessage::Success(msg.to_string())); - } - - pub fn custom_error(&self, msg: impl Into) { - let msg = msg.into(); - self.write_to_file(&msg); - let _ = self.sender.send(LogMessage::CustomError(msg.to_string())); - } -} diff --git a/sbuild-linter/src/semaphore.rs b/sbuild-linter/src/semaphore.rs deleted file mode 100644 index 12b0bc8..0000000 --- a/sbuild-linter/src/semaphore.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::sync::{Condvar, Mutex}; - -pub struct Semaphore { - permits: Mutex, - condvar: Condvar, -} - -impl Semaphore { - pub fn new(count: usize) -> Self { - Semaphore { - permits: Mutex::new(count), - condvar: Condvar::new(), - } - } - - pub fn acquire(&self) { - let mut permits = self.permits.lock().unwrap(); - while *permits == 0 { - permits = self.condvar.wait(permits).unwrap(); - } - *permits -= 1; - } - - pub fn release(&self) { - let mut permits = self.permits.lock().unwrap(); - *permits += 1; - self.condvar.notify_one(); - } -} diff --git a/sbuild-linter/src/validator.rs b/sbuild-linter/src/validator.rs deleted file mode 100644 index 3f735aa..0000000 --- a/sbuild-linter/src/validator.rs +++ /dev/null @@ -1,919 +0,0 @@ -use std::collections::HashSet; - -use colored::Colorize; -use indexmap::IndexMap; -use saphyr::MarkedYamlOwned; -use url::Url; - -use crate::{ - build_config::BuildConfig, - description::Description, - error::{highlight_error_line, ErrorDetails, Severity}, - logger::TaskLogger, - xexec::XExec, - BuildAsset, VALID_ARCH, VALID_CATEGORIES, VALID_OS, VALID_PKG_TYPES, -}; - -pub struct ValidationContext { - yaml_str: String, - logger: TaskLogger, - errors: Vec, - visited: HashSet, -} - -impl ValidationContext { - pub fn new(yaml_str: &str, logger: TaskLogger) -> Self { - Self { - yaml_str: yaml_str.to_string(), - logger, - errors: Vec::new(), - visited: HashSet::new(), - } - } - - fn line_of(node: &MarkedYamlOwned) -> usize { - let line = node.span.start.line(); - if line != 0 { - return line; - } - // Saphyr reports line 0 for block-style containers (sequences/mappings - // that start on a new line after their key). Fall back to first child's line. - if let Some(seq) = node.data.as_sequence() { - if let Some(first) = seq.first() { - return first.span.start.line(); - } - } - if let Some(map) = node.data.as_mapping() { - if let Some((first_key, _)) = map.iter().next() { - return first_key.span.start.line(); - } - } - 0 - } - - fn error(&mut self, field: &str, message: &str, line: usize) { - self.errors.push(ErrorDetails { - field: field.to_string(), - message: message.to_string(), - line_number: line, - severity: Severity::Error, - }); - } - - fn warn(&mut self, field: &str, message: &str, line: usize) { - self.errors.push(ErrorDetails { - field: field.to_string(), - message: message.to_string(), - line_number: line, - severity: Severity::Warn, - }); - } - - fn expect_bool(&mut self, node: &MarkedYamlOwned, field: &str) -> Option { - let line = Self::line_of(node); - if let Some(b) = node.data.as_bool() { - Some(b) - } else { - self.error(field, &format!("'{}' field must be a boolean", field), line); - None - } - } - - fn expect_string(&mut self, node: &MarkedYamlOwned, field: &str) -> Option { - let line = Self::line_of(node); - if let Some(s) = node.data.as_str() { - Some(s.to_string()) - } else { - self.error(field, &format!("'{}' field must be a string", field), line); - None - } - } - - fn expect_non_empty_string(&mut self, node: &MarkedYamlOwned, field: &str) -> Option { - let line = Self::line_of(node); - if let Some(s) = node.data.as_str() { - if s.trim().is_empty() { - self.error(field, &format!("'{}' field cannot be empty", field), line); - None - } else { - Some(s.to_string()) - } - } else { - self.error(field, &format!("'{}' field must be a string", field), line); - None - } - } - - fn expect_string_array( - &mut self, - node: &MarkedYamlOwned, - field: &str, - required: bool, - ) -> Option> { - let line = Self::line_of(node); - if let Some(seq) = node.data.as_sequence() { - let mut seen = HashSet::new(); - let valid: Vec = seq - .iter() - .filter_map(|v| { - if let Some(s) = v.data.as_str() { - if !s.trim().is_empty() { - Some(s.to_string()) - } else { - None - } - } else { - if !v.data.is_null() { - self.error( - field, - &format!("'{}' field must only contain sequence of strings", field), - Self::line_of(v), - ); - } - None - } - }) - .filter(|s| seen.insert(s.clone())) - .collect(); - - if valid.is_empty() { - if required { - self.error( - field, - &format!("'{}' field must contain at least 1 non-empty string", field), - line, - ); - } - None - } else { - if valid.len() != seq.len() { - self.warn( - field, - &format!( - "'{}' field contains duplicates. Removed automatically..", - field - ), - line, - ); - } - Some(valid) - } - } else { - if required { - self.error(field, &format!("'{}' field must be an array", field), line); - } - None - } - } - - fn mapping_get<'a>(node: &'a MarkedYamlOwned, key: &str) -> Option<&'a MarkedYamlOwned> { - node.data.as_mapping_get(key) - } - - fn validate_packages( - &mut self, - node: &MarkedYamlOwned, - ) -> Option> { - let line = Self::line_of(node); - let Some(mapping) = node.data.as_mapping() else { - self.error("packages", "'packages' must be a mapping.", line); - return None; - }; - - let mut packages = Vec::new(); - for (key_node, val_node) in mapping { - let Some(pkg_name) = key_node.data.as_str() else { - self.error( - "packages", - "Package name must be a string.", - Self::line_of(key_node), - ); - continue; - }; - - let pkg_line = Self::line_of(val_node); - let Some(pkg_map) = val_node.data.as_mapping() else { - self.error( - &format!("packages.{}", pkg_name), - "Package entry must be a mapping.", - pkg_line, - ); - continue; - }; - - let provides = pkg_map - .iter() - .find(|(k, _)| k.data.as_str() == Some("provides")) - .and_then(|(_, v)| { - v.data.as_sequence().map(|seq| { - seq.iter() - .filter_map(|item| item.data.as_str().map(|s| s.to_string())) - .collect::>() - }) - }) - .unwrap_or_default(); - - if provides.is_empty() { - self.warn( - &format!("packages.{}", pkg_name), - &format!("Package '{}' has no provides.", pkg_name), - pkg_line, - ); - } - - packages.push(( - pkg_name.to_string(), - crate::build_config::PackageConfig { provides }, - )); - } - - if packages.is_empty() { - self.error("packages", "'packages' must not be empty.", line); - return None; - } - - Some(packages) - } - - fn validate_x_exec(&mut self, node: &MarkedYamlOwned) -> Option { - let line = Self::line_of(node); - if node.data.as_mapping().is_none() { - self.error("x_exec", "Must be an object", line); - return None; - } - - let mut valid = true; - let mut x_exec = XExec::default(); - - // container (optional) - if let Some(container_node) = Self::mapping_get(node, "container") { - if let Some(s) = self.expect_non_empty_string(container_node, "x_exec.container") { - x_exec.container = Some(s); - } - } - - // shell (required) - if let Some(shell_node) = Self::mapping_get(node, "shell") { - if let Some(s) = self.expect_non_empty_string(shell_node, "x_exec.shell") { - if x_exec.container.is_some() || which::which_global(&s).is_ok() { - x_exec.shell = s; - } else { - self.error( - "x_exec.shell", - &format!("{} is not installed.", s), - Self::line_of(shell_node), - ); - valid = false; - } - } else { - valid = false; - } - } else { - self.error("x_exec", "Missing required 'shell' field", line); - valid = false; - } - - // run (optional) - if let Some(run_node) = Self::mapping_get(node, "run") { - if let Some(s) = self.expect_non_empty_string(run_node, "x_exec.run") { - x_exec.run = Some(s); - } else { - valid = false; - } - } - - // pkgver (optional) - if let Some(pkgver_node) = Self::mapping_get(node, "pkgver") { - if let Some(s) = self.expect_string(pkgver_node, "x_exec.pkgver") { - x_exec.pkgver = Some(s); - } else { - valid = false; - } - } - - // entrypoint (optional) - if let Some(ep_node) = Self::mapping_get(node, "entrypoint") { - if let Some(s) = self.expect_string(ep_node, "x_exec.entrypoint") { - x_exec.entrypoint = Some(s); - } else { - valid = false; - } - } - - // arch (optional) - if let Some(arch_node) = Self::mapping_get(node, "arch") { - if let Some(arr) = self.expect_lowered_string_array(arch_node, "x_exec.arch") { - for s in &arr { - if !VALID_ARCH.contains(&s.as_str()) { - self.error( - "x_exec.arch", - &format!("'{}' is not a supported architecture.", s), - Self::line_of(arch_node), - ); - valid = false; - } - } - if valid { - x_exec.arch = Some(arr); - } - } else { - valid = false; - } - } - - // os (optional) - if let Some(os_node) = Self::mapping_get(node, "os") { - if let Some(arr) = self.expect_lowered_string_array(os_node, "x_exec.os") { - for s in &arr { - if !VALID_OS.contains(&s.as_str()) { - self.error( - "x_exec.os", - &format!("'{}' is not a supported OS.", s), - Self::line_of(os_node), - ); - valid = false; - } - } - if valid { - x_exec.os = Some(arr); - } - } else { - valid = false; - } - } - - // host (optional) - if let Some(host_node) = Self::mapping_get(node, "host") { - if let Some(arr) = self.expect_lowered_string_array(host_node, "x_exec.host") { - for s in &arr { - let parts: Vec<&str> = s.split('-').collect(); - if !(parts.len() == 2 - && VALID_ARCH.contains(&parts[0]) - && VALID_OS.contains(&parts[1])) - { - self.error( - "x_exec.host", - &format!("'{}' is not a supported `arch-os` combination.", s), - Self::line_of(host_node), - ); - valid = false; - } - } - if valid { - x_exec.host = Some(arr); - } - } else { - valid = false; - } - } - - // conflicts (optional) - if let Some(conflicts_node) = Self::mapping_get(node, "conflicts") { - if let Some(arr) = self.expect_lowered_string_array(conflicts_node, "x_exec.conflicts") - { - x_exec.conflicts = Some(arr); - } - } - - // depends (optional) - if let Some(depends_node) = Self::mapping_get(node, "depends") { - if let Some(arr) = self.expect_lowered_string_array(depends_node, "x_exec.depends") { - x_exec.depends = Some(arr); - } - } - - if valid { - Some(x_exec) - } else { - None - } - } - - fn expect_lowered_string_array( - &mut self, - node: &MarkedYamlOwned, - field: &str, - ) -> Option> { - let line = Self::line_of(node); - if let Some(seq) = node.data.as_sequence() { - let mut seen = HashSet::new(); - let valid: Vec = seq - .iter() - .filter_map(|v| { - if let Some(s) = v.data.as_str() { - if !s.trim().is_empty() { - Some(s.to_lowercase()) - } else { - None - } - } else { - if !v.data.is_null() { - self.error( - field, - &format!("'{}' must only contain sequence of strings", field), - Self::line_of(v), - ); - } - None - } - }) - .filter(|s| seen.insert(s.clone())) - .collect(); - - if valid.len() != seq.len() { - self.warn( - field, - &format!("'{}' contains duplicates. Removed automatically..", field), - line, - ); - } - Some(valid) - } else { - self.error( - field, - &format!("'{}' must be an array of strings", field), - line, - ); - None - } - } - - fn validate_description(&mut self, node: &MarkedYamlOwned) -> Option { - let line = Self::line_of(node); - match node.data.as_str() { - Some(s) => { - if s.trim().is_empty() { - self.error("description", "'description' field cannot be empty", line); - None - } else { - Some(Description::Simple(s.to_string())) - } - } - None => { - if let Some(map) = node.data.as_mapping() { - if map.is_empty() { - self.error("description", "'description' field cannot be empty", line); - return None; - } - let mut valid = true; - let mut result = IndexMap::new(); - - for (k, v) in map { - let key_str = match k.data.as_str() { - Some(s) => s.to_string(), - None => { - if let Some(b) = k.data.as_bool() { - b.to_string() - } else { - self.error( - "description", - "Description key must be a string", - Self::line_of(k), - ); - valid = false; - continue; - } - } - }; - - if let Some(val_str) = v.data.as_str() { - if !val_str.trim().is_empty() { - result.insert(key_str, val_str.to_string()); - } else { - self.error( - &format!("description.{}", key_str), - "Description value cannot be empty", - Self::line_of(v), - ); - valid = false; - } - } else { - self.error( - &format!("description.{}", key_str), - "Description value must be a string", - Self::line_of(v), - ); - valid = false; - } - } - - if valid && !result.is_empty() { - Some(Description::Map(result)) - } else { - None - } - } else { - self.error( - "description", - "'description' field must be either a string or a mapping of strings", - line, - ); - None - } - } - } - } - - fn validate_build_asset(&mut self, node: &MarkedYamlOwned) -> Option> { - let line = Self::line_of(node); - let seq = match node.data.as_sequence() { - Some(s) => s, - None => { - self.error("build_asset", "Must be an array of build assets", line); - return None; - } - }; - - let mut assets = Vec::new(); - for asset_node in seq { - let asset_line = Self::line_of(asset_node); - if asset_node.data.as_mapping().is_none() { - self.error( - "build_asset", - "Each build asset must be an object", - asset_line, - ); - continue; - } - - let mut valid = true; - let mut url = String::new(); - let mut out = String::new(); - - if let Some(url_node) = Self::mapping_get(asset_node, "url") { - if let Some(u) = self.expect_non_empty_string(url_node, "build_asset.url") { - if !u.contains("${") && !is_valid_url(&u) { - self.error( - "build_asset.url", - &format!("'{}' is not a valid URL.", u), - Self::line_of(url_node), - ); - valid = false; - } else { - url = u; - } - } else { - valid = false; - } - } else { - self.error("build_asset", "Missing required 'url' field", asset_line); - valid = false; - } - - if let Some(out_node) = Self::mapping_get(asset_node, "out") { - if let Some(o) = self.expect_non_empty_string(out_node, "build_asset.out") { - out = o; - } else { - valid = false; - } - } else { - self.error("build_asset", "Missing required 'out' field", asset_line); - valid = false; - } - - if valid { - assets.push(BuildAsset { url, out }); - } - } - - if assets.is_empty() { - self.error("build_asset", "No valid build assets found", line); - None - } else { - Some(assets) - } - } - - pub fn validate(&mut self, doc: &MarkedYamlOwned) -> Option { - let map = match doc.data.as_mapping() { - Some(m) => m, - None => { - self.error("root", "YAML document must be a mapping", 0); - self.report_errors(); - return None; - } - }; - - let mut config = BuildConfig::default(); - let mut has_disabled = false; - let mut has_pkg = false; - let mut has_description = false; - let mut has_src_url = false; - let mut has_x_exec = false; - - for (key_node, val_node) in map { - let key = match key_node.data.as_str() { - Some(s) => s.to_string(), - None => continue, - }; - let line = Self::line_of(key_node); - - if self.visited.contains(&key) { - self.error(&key, &format!("'{}' field is duplicated", key), line); - continue; - } - self.visited.insert(key.clone()); - - match key.as_str() { - "_disabled" => { - if let Some(b) = self.expect_bool(val_node, "_disabled") { - config._disabled = b; - has_disabled = true; - } - } - "pkg" => { - if let Some(v) = self.expect_non_empty_string(val_node, "pkg") { - if !is_valid_alpha(&v) { - self.error( - "pkg", - &format!( - "Invalid 'pkg': '{}'. Value should only contain alphanumeric, +, -, _, .", - v - ), - line, - ); - } - config.pkg = v; - has_pkg = true; - } - } - "pkg_id" => { - if let Some(v) = self.expect_non_empty_string(val_node, "pkg_id") { - if !is_valid_alpha(&v) { - self.error( - "pkg_id", - &format!( - "Invalid 'pkg_id': '{}'. Value should only contain alphanumeric, +, -, _, .", - v - ), - line, - ); - } - config.pkg_id = v; - } - } - "app_id" => { - if let Some(v) = self.expect_non_empty_string(val_node, "app_id") { - if !is_valid_alpha(&v) { - self.error( - "app_id", - &format!( - "Invalid 'app_id': '{}'. Value should only contain alphanumeric, +, -, _, .", - v - ), - line, - ); - } - config.app_id = Some(v); - } - } - "pkg_type" => { - if let Some(v) = self.expect_non_empty_string(val_node, "pkg_type") { - if !VALID_PKG_TYPES.contains(&v.as_str()) { - self.error( - "pkg_type", - &format!( - "Invalid 'pkg_type': '{}'. Valid values are: {:?}", - v, VALID_PKG_TYPES - ), - line, - ); - } - config.pkg_type = Some(v); - } - } - "pkgver" | "version" => { - if let Some(v) = self.expect_string(val_node, &key) { - if !v.trim().is_empty() { - config.pkgver = Some(v); - } - } - } - "remote_pkgver" => { - if let Some(v) = self.expect_string(val_node, "remote_pkgver") { - if !v.trim().is_empty() { - config.remote_pkgver = Some(v); - } - } - } - "build_util" => { - config.build_util = self.expect_string_array(val_node, "build_util", false); - } - "build_asset" => { - config.build_asset = self.validate_build_asset(val_node); - } - "build_deps" => { - config.build_deps = self.expect_string_array(val_node, "build_deps", false); - } - "category" => { - if let Some(cats) = self.expect_string_array(val_node, "category", false) { - for c in &cats { - if !is_valid_category(c) { - self.error( - "category", - &format!( - "Invalid 'category': '{}' is not a valid category.", - c - ), - line, - ); - } - } - config.category = cats; - } - } - "description" => { - if let Some(desc) = self.validate_description(val_node) { - config.description = Some(desc); - has_description = true; - } - } - "homepage" => { - if let Some(urls) = self.expect_string_array(val_node, "homepage", false) { - for u in &urls { - if !is_valid_url(u) { - self.error( - "homepage", - &format!("Invalid 'homepage': '{}' is not a valid URL.", u), - line, - ); - } - } - config.homepage = Some(urls); - } - } - "maintainer" => { - config.maintainer = self.expect_string_array(val_node, "maintainer", false); - } - "license" => { - config.license = self.expect_string_array(val_node, "license", false); - } - "note" => { - config.note = self.expect_string_array(val_node, "note", false); - } - "provides" => { - config.provides = self.expect_string_array(val_node, "provides", false); - } - "packages" => { - config.packages = self.validate_packages(val_node); - } - "repology" => { - config.repology = self.expect_string_array(val_node, "repology", false); - } - "src_url" => { - if let Some(urls) = self.expect_string_array(val_node, "src_url", true) { - for u in &urls { - if !is_valid_url(u) { - self.error( - "src_url", - &format!("Invalid 'src_url': '{}' is not a valid URL.", u), - line, - ); - } - } - config.src_url = urls; - has_src_url = true; - } - } - "tag" => { - config.tag = self.expect_string_array(val_node, "tag", false); - } - "ghcr_pkg" => { - if let Some(v) = self.expect_string(val_node, "ghcr_pkg") { - if !v.trim().is_empty() { - config.ghcr_pkg = Some(v); - } - } - } - "snapshots" => { - config.snapshots = self.expect_string_array(val_node, "snapshots", false); - } - "x_exec" => { - if let Some(x) = self.validate_x_exec(val_node) { - config.x_exec = x; - has_x_exec = true; - } - } - unknown => { - self.warn( - unknown, - &format!("'{}' is not a valid field.", unknown), - line, - ); - } - } - } - - // Check required fields - if !has_disabled { - self.error("_disabled", "Missing required field: _disabled", 0); - } - if !has_pkg { - self.error("pkg", "Missing required field: pkg", 0); - } - if !has_description { - self.error("description", "Missing required field: description", 0); - } - if !has_src_url { - self.error("src_url", "Missing required field: src_url", 0); - } - if !has_x_exec { - self.error("x_exec", "Missing required field: x_exec", 0); - } - - // Set default category if empty - if config.category.is_empty() { - config.category = vec!["Utility".to_string()]; - } - - // Derive pkg_id from src_url if not explicitly set - config.set_pkg_id_from_src_url(); - - if self.has_fatal_errors() { - self.report_errors(); - None - } else { - self.report_errors(); - Some(config) - } - } - - fn has_fatal_errors(&self) -> bool { - self.errors - .iter() - .any(|e| matches!(e.severity, Severity::Error)) - } - - fn report_errors(&self) { - if self.errors.is_empty() { - return; - } - - let fatal_count = self - .errors - .iter() - .filter(|e| matches!(e.severity, Severity::Error)) - .count(); - let warn_count = self.errors.len() - fatal_count; - - for error in &self.errors { - let is_fatal = matches!(error.severity, Severity::Error); - if is_fatal { - self.logger.error(&format!( - "{} -> {}", - error.field.bold(), - error.message.red() - )); - } else { - self.logger.warn(&format!( - "{} -> {}", - error.field.bold(), - error.message.yellow() - )); - } - if error.line_number != 0 { - highlight_error_line(&self.yaml_str, error.line_number, is_fatal, &self.logger); - } - } - - if fatal_count > 0 { - self.logger.custom_error(&format!( - "{}{} found during deserialization.", - format!("{} error(s)", fatal_count).red(), - if warn_count > 0 { - format!(" & {} warning(s)", warn_count).yellow() - } else { - "".yellow() - } - )); - } else { - self.logger.custom_error(&format!( - "{} found during deserialization.", - format!("{} warning(s)", warn_count).yellow() - )); - } - } -} - -pub fn is_valid_alpha(value: &str) -> bool { - value - .chars() - .all(|c| c.is_alphanumeric() || c == '+' || c == '-' || c == '_' || c == '.') -} - -pub fn is_valid_category(value: &str) -> bool { - VALID_CATEGORIES.lines().any(|line| line.trim() == value) -} - -pub fn is_valid_url(value: &str) -> bool { - let Ok(url) = Url::parse(value) else { - return false; - }; - - let scheme = url.scheme(); - if scheme.is_empty() || !["http", "https", "ftp"].contains(&scheme) { - return false; - } - - true -} diff --git a/sbuild-linter/src/xexec.rs b/sbuild-linter/src/xexec.rs deleted file mode 100644 index 1db3706..0000000 --- a/sbuild-linter/src/xexec.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::{ - fs::File, - io::{self, BufWriter, Write}, -}; - -#[derive(Debug, Default, Clone)] -pub struct XExec { - pub arch: Option>, - pub os: Option>, - pub host: Option>, - pub conflicts: Option>, - pub depends: Option>, - pub entrypoint: Option, - pub pkgver: Option, - pub container: Option, - pub shell: String, - pub run: Option, -} - -impl XExec { - pub fn write_yaml(&self, writer: &mut BufWriter, indent: usize) -> io::Result<()> { - let indent_str = " ".repeat(indent); - - if let Some(ref arch) = self.arch { - writeln!(writer, "{}arch:", indent_str)?; - for a in arch { - writeln!(writer, "{} - \"{}\"", indent_str, a)?; - } - } - if let Some(ref os) = self.os { - writeln!(writer, "{}os:", indent_str)?; - for o in os { - writeln!(writer, "{} - \"{}\"", indent_str, o)?; - } - } - if let Some(ref host) = self.host { - writeln!(writer, "{}host:", indent_str)?; - for h in host { - writeln!(writer, "{} - \"{}\"", indent_str, h)?; - } - } - if let Some(ref conflicts) = self.conflicts { - writeln!(writer, "{}conflicts:", indent_str)?; - for c in conflicts { - writeln!(writer, "{} - \"{}\"", indent_str, c)?; - } - } - if let Some(ref depends) = self.depends { - writeln!(writer, "{}depends:", indent_str)?; - for d in depends { - writeln!(writer, "{} - \"{}\"", indent_str, d)?; - } - } - if let Some(ref entrypoint) = self.entrypoint { - writeln!(writer, "{}entrypoint: \"{}\"", indent_str, entrypoint)?; - } - - if let Some(ref container) = self.container { - writeln!(writer, "{}container: \"{}\"", indent_str, container)?; - } - - writeln!(writer, "{}shell: \"{}\"", indent_str, self.shell)?; - - if let Some(ref pkgver) = self.pkgver { - writeln!(writer, "{}pkgver: |", indent_str)?; - for line in pkgver.lines() { - writeln!(writer, "{} {}", indent_str, line)?; - } - } - - if let Some(ref run) = self.run { - writeln!(writer, "{}run: |", indent_str)?; - for line in run.lines() { - writeln!(writer, "{} {}", indent_str, line)?; - } - } - - Ok(()) - } -} diff --git a/sbuild-meta/CHANGELOG.md b/sbuild-meta/CHANGELOG.md deleted file mode 100644 index c598ffc..0000000 --- a/sbuild-meta/CHANGELOG.md +++ /dev/null @@ -1,40 +0,0 @@ - -## [0.1.0] - 2026-02-22 - -### Added - -- Add x_exec.container, build_deps, and checksum_bsum metadata - ([2f0964f](https://github.com/pkgforge/sbuilder/commit/2f0964f78e295687f2006444655a24c73715a2e2)) -- Add packages field for multi-package recipes and fix push logic - ([76bdd66](https://github.com/pkgforge/sbuilder/commit/76bdd66283e4736d041060bca93e80a6b53aeb2d)) - -### Fixed - -- Use recipe pkg name instead of provides entry for pkg_name metadata - ([9a0f8e5](https://github.com/pkgforge/sbuilder/commit/9a0f8e5a0f1bfd5373a6d9515f57b3aa3d879e5f)) -- Fix generate metadata - ([7237a8c](https://github.com/pkgforge/sbuilder/commit/7237a8c200eea97e3c6fca046a328c8f931d7050)) -- Fix metadata, minisign, rank by downloads - ([441ca5a](https://github.com/pkgforge/sbuilder/commit/441ca5a3c7ecd2fed8d80599480616ee15991b06)) -- Fix download_url in metadata - ([91901a1](https://github.com/pkgforge/sbuilder/commit/91901a1a053281943b876321b19ab1bd17cc1761)) -- Fix uploaded file - ([7f881bf](https://github.com/pkgforge/sbuilder/commit/7f881bf947f4bfb7545a715e2b6c6351a9fa2484)) -- Fix ghcr path and version - ([11bc4c0](https://github.com/pkgforge/sbuilder/commit/11bc4c0f95d90f990c35efefc43b952268ede333)) - -### Other - -- Replace serde_yml/serde_yaml with saphyr and simplify schema - ([2f1f48e](https://github.com/pkgforge/sbuilder/commit/2f1f48e053ade96a5614e86170202ffb80c3ea5c)) -- Centralize dependencies under [workspace.dependencies] - ([92db2b9](https://github.com/pkgforge/sbuilder/commit/92db2b9d21dc910adbce96cc146b9ba13d959a85)) -- Remove duplicate code and dead functions after CLI consolidation - ([1e400ac](https://github.com/pkgforge/sbuilder/commit/1e400ac379205cd2925f537fbd45aa4c1140aa68)) -- Consolidate binaries into unified sbuild CLI with subcommands - ([9818b33](https://github.com/pkgforge/sbuilder/commit/9818b33710e5a53a4b80e7fe86b3e1a9a77ce4ad)) -- Add @name syntax for binary-only provides entries - ([80fe9f6](https://github.com/pkgforge/sbuilder/commit/80fe9f616c710ec8d4b19d89f01366fce314830c)) -- Remove build restrictions on binary types - ([346822b](https://github.com/pkgforge/sbuilder/commit/346822b4e93bdc4e8a9f9c27f0a25dcfe988cce6)) -- Remove cache separation, normalize host - ([03bcf4d](https://github.com/pkgforge/sbuilder/commit/03bcf4dbeb9b21dbb87d5a0135043a7d70c65a35)) -- Update version check - ([723868b](https://github.com/pkgforge/sbuilder/commit/723868b7358730576de0750c22d3c90de2a3043a)) -- Handle remote_pkgver - ([500b1bd](https://github.com/pkgforge/sbuilder/commit/500b1bd3034b9b458b91f43393e245b878a8309a)) -- Use annotations for metadata values - ([c361752](https://github.com/pkgforge/sbuilder/commit/c36175267d54213cfa5f3baf2b2efc1e8707b432)) -- Determine cache type based on pkg_type - ([1b420de](https://github.com/pkgforge/sbuilder/commit/1b420dea52e213062698cfa86b4dd977bb360398)) -- Remove cache type requirement in ghcr path - ([875b25f](https://github.com/pkgforge/sbuilder/commit/875b25fb0e00cc85c348827cd3ec61846a8a1f12)) -- Set default log level to info - ([0db7b25](https://github.com/pkgforge/sbuilder/commit/0db7b256c1c2ab055082b47f96917637ec86d028)) -- Make ghcr_pkg in sbuild portable - ([cf97c6d](https://github.com/pkgforge/sbuilder/commit/cf97c6dc585e6479f830d525192d4ca3b5ee476c)) -- Use version placeholders - ([48f7e58](https://github.com/pkgforge/sbuilder/commit/48f7e58c944da5b9cb88ea3198332dde96c349fa)) -- Handle snapshots - ([a0f1314](https://github.com/pkgforge/sbuilder/commit/a0f13143f3e52be0c218543dc6153007f8d4f8c6)) -- Remove rank, download count - ([93720c8](https://github.com/pkgforge/sbuilder/commit/93720c8eb4fe34731b34f0bc0d387ae1746e9cb4)) -- Handle provide in own directory - ([afc3afc](https://github.com/pkgforge/sbuilder/commit/afc3afc8624c0cb49500811fa5e2030a147a00be)) -- Update manifest - ([cd43725](https://github.com/pkgforge/sbuilder/commit/cd4372526c38cb4017a77a117e8a84cc78be71b4)) -- Update - ([6c7c7cb](https://github.com/pkgforge/sbuilder/commit/6c7c7cb287850fd4abfc7ae467f7ce9498d7d47a)) -- Update, introduce sbuild-meta and sbuild-cache - ([1cc6cd3](https://github.com/pkgforge/sbuilder/commit/1cc6cd399fe16b69eae8dc4895dc52c451453842)) diff --git a/sbuild-meta/Cargo.toml b/sbuild-meta/Cargo.toml deleted file mode 100644 index 60130bf..0000000 --- a/sbuild-meta/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "sbuild-meta" -version = "0.1.0" -license.workspace = true -edition.workspace = true -description = "Metadata generator for SBUILD packages" -repository = "https://github.com/pkgforge/sbuilder" -keywords = ["soar", "sbuild", "metadata", "ghcr", "package"] - -[dependencies] -blake3.workspace = true -glob.workspace = true -log.workspace = true -regex.workspace = true -reqwest = { workspace = true, features = ["json", "rustls"] } -rusqlite.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -saphyr.workspace = true -thiserror.workspace = true - -[lib] -name = "sbuild_meta" -path = "src/lib.rs" diff --git a/sbuild-meta/src/error.rs b/sbuild-meta/src/error.rs deleted file mode 100644 index 4ebf976..0000000 --- a/sbuild-meta/src/error.rs +++ /dev/null @@ -1,48 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("HTTP request failed: {0}")] - Http(#[from] reqwest::Error), - - #[error("JSON parsing failed: {0}")] - Json(#[from] serde_json::Error), - - #[error("YAML parsing failed: {0}")] - Yaml(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - - #[error("SQLite error: {0}")] - Sqlite(#[from] rusqlite::Error), - - #[error("Registry error: {0}")] - Registry(String), - - #[error("Manifest not found: {0}")] - ManifestNotFound(String), - - #[error("Recipe error: {0}")] - Recipe(String), - - #[error("No pkgver script in recipe")] - NoPkgver, - - #[error("pkgver execution failed: {0}")] - PkgverFailed(String), - - #[error("Version parse error: {0}")] - VersionParse(String), - - #[error("Glob pattern error: {0}")] - Glob(#[from] glob::PatternError), - - #[error("Regex error: {0}")] - Regex(#[from] regex::Error), - - #[error("{0}")] - Other(String), -} - -pub type Result = std::result::Result; diff --git a/sbuild-meta/src/hash.rs b/sbuild-meta/src/hash.rs deleted file mode 100644 index 14a45df..0000000 --- a/sbuild-meta/src/hash.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Recipe hashing for change detection -//! -//! Computes a deterministic hash of SBUILD recipe content, -//! ignoring whitespace, empty lines, and comments for stability. - -use crate::Result; - -/// Compute a normalized hash of recipe content. -/// -/// The normalization process: -/// 1. Removes empty lines -/// 2. Removes comment-only lines (starting with #) -/// 3. Trims whitespace from each line -/// 4. Optionally excludes the `version` field for rebuild detection -/// -/// This ensures minor formatting changes don't trigger rebuilds. -pub fn compute_recipe_hash(content: &str) -> String { - compute_recipe_hash_internal(content, false) -} - -/// Compute hash excluding the version field. -/// -/// Used for detecting recipe changes that should trigger rebuilds, -/// where version changes are handled separately by the bot. -pub fn compute_recipe_hash_excluding_version(content: &str) -> String { - compute_recipe_hash_internal(content, true) -} - -fn compute_recipe_hash_internal(content: &str, exclude_version: bool) -> String { - let normalized: String = content - .lines() - .filter(|line| { - let trimmed = line.trim(); - // Skip empty lines - if trimmed.is_empty() { - return false; - } - // Skip comment-only lines (but keep shebang) - if trimmed.starts_with('#') && !trimmed.starts_with("#!") { - return false; - } - // Optionally skip version field - if exclude_version && trimmed.starts_with("version:") { - return false; - } - true - }) - .map(|line| line.trim()) - .collect::>() - .join("\n"); - - blake3::hash(normalized.as_bytes()).to_hex().to_string() -} - -/// Verify that a hash matches the expected value. -pub fn verify_hash(content: &str, expected: &str) -> bool { - compute_recipe_hash(content) == expected -} - -/// Compute hash from a file path. -pub fn hash_file(path: &std::path::Path) -> Result { - let content = std::fs::read_to_string(path)?; - Ok(compute_recipe_hash(&content)) -} - -/// Compute hash from a file, excluding version field. -pub fn hash_file_excluding_version(path: &std::path::Path) -> Result { - let content = std::fs::read_to_string(path)?; - Ok(compute_recipe_hash_excluding_version(&content)) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_hash_consistency() { - let content = "pkg: test\ndescription: foo"; - // Hash should be consistent for same content - assert_eq!( - compute_recipe_hash(content), - compute_recipe_hash("pkg: test\ndescription: foo") - ); - } - - #[test] - fn test_hash_ignores_empty_lines() { - let content1 = "pkg: test\n\n\ndescription: foo"; - let content2 = "pkg: test\ndescription: foo"; - assert_eq!(compute_recipe_hash(content1), compute_recipe_hash(content2)); - } - - #[test] - fn test_hash_ignores_comments() { - let content1 = "pkg: test\n# This is a comment\ndescription: foo"; - let content2 = "pkg: test\ndescription: foo"; - assert_eq!(compute_recipe_hash(content1), compute_recipe_hash(content2)); - } - - #[test] - fn test_hash_preserves_shebang() { - let content1 = "#!/SBUILD ver @v1.0.0\npkg: test"; - let content2 = "pkg: test"; - assert_ne!(compute_recipe_hash(content1), compute_recipe_hash(content2)); - } - - #[test] - fn test_hash_excludes_version() { - let content1 = "pkg: test\nversion: 1.0.0\ndescription: foo"; - let content2 = "pkg: test\nversion: 2.0.0\ndescription: foo"; - assert_eq!( - compute_recipe_hash_excluding_version(content1), - compute_recipe_hash_excluding_version(content2) - ); - } -} diff --git a/sbuild-meta/src/lib.rs b/sbuild-meta/src/lib.rs deleted file mode 100644 index 0d5134c..0000000 --- a/sbuild-meta/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! sbuild-meta: Metadata generator for SBUILD packages -//! -//! This crate provides tools for: -//! - Fetching OCI manifests from GHCR -//! - Generating package metadata from SBUILD recipes -//! - Recipe hashing for change detection -//! - Version comparison and update detection -//! - Historical cache management - -pub mod error; -pub mod hash; -pub mod manifest; -pub mod metadata; -pub mod recipe; -pub mod registry; - -pub use error::{Error, Result}; -pub use hash::compute_recipe_hash; -pub use manifest::OciManifest; -pub use metadata::{format_size, PackageMetadata}; -pub use recipe::{sanitize_oci_name, GhcrPackageInfo, SBuildRecipe}; -pub use registry::RegistryClient; diff --git a/sbuild-meta/src/manifest.rs b/sbuild-meta/src/manifest.rs deleted file mode 100644 index 69d5142..0000000 --- a/sbuild-meta/src/manifest.rs +++ /dev/null @@ -1,229 +0,0 @@ -//! OCI Manifest parsing -//! -//! Parses OCI image manifests to extract package metadata -//! stored in annotations. - -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::{metadata::format_size, Error, Result}; - -/// OCI manifest layer descriptor -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct LayerDescriptor { - /// Media type of the layer - #[serde(rename = "mediaType")] - pub media_type: String, - - /// Size in bytes - pub size: u64, - - /// Content digest (sha256:...) - pub digest: String, - - /// Layer annotations - #[serde(default)] - pub annotations: HashMap, -} - -impl LayerDescriptor { - /// Get the filename from annotations - pub fn filename(&self) -> Option<&str> { - self.annotations - .get("org.opencontainers.image.title") - .map(|s| s.as_str()) - } -} - -/// OCI image manifest -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct OciManifest { - /// Schema version - #[serde(rename = "schemaVersion")] - pub schema_version: u32, - - /// Media type - #[serde(rename = "mediaType", default)] - pub media_type: Option, - - /// Config descriptor - #[serde(default)] - pub config: Option, - - /// Layer descriptors - #[serde(default)] - pub layers: Vec, - - /// Manifest annotations - #[serde(default)] - pub annotations: HashMap, -} - -impl OciManifest { - /// Parse manifest from JSON string - pub fn from_json(json: &str) -> Result { - serde_json::from_str(json).map_err(Error::Json) - } - - /// Get annotation value by key - pub fn get_annotation(&self, key: &str) -> Option<&str> { - self.annotations.get(key).map(|s| s.as_str()) - } - - /// Get the embedded package JSON from annotations - pub fn get_package_json(&self) -> Result> { - match self.annotations.get("dev.pkgforge.soar.json") { - Some(json_str) => { - let value: serde_json::Value = serde_json::from_str(json_str)?; - Ok(Some(value)) - } - None => Ok(None), - } - } - - /// Get GHCR package identifier from annotations - pub fn ghcr_pkg(&self) -> Option<&str> { - self.get_annotation("dev.pkgforge.soar.ghcr_pkg") - } - - /// Get build action URL from annotations - pub fn build_action(&self) -> Option<&str> { - self.get_annotation("dev.pkgforge.soar.build_gha") - } - - /// Get build ID from annotations - pub fn build_id(&self) -> Option<&str> { - self.get_annotation("dev.pkgforge.soar.build_id") - } - - /// Get total size of all layers - pub fn total_size(&self) -> u64 { - self.layers.iter().map(|l| l.size).sum() - } - - /// Get human-readable size - pub fn total_size_human(&self) -> String { - format_size(self.total_size()) - } - - /// Get list of filenames in manifest - pub fn filenames(&self) -> Vec<&str> { - self.layers.iter().filter_map(|l| l.filename()).collect() - } - - /// Get layer by filename - pub fn get_layer_by_filename(&self, filename: &str) -> Option<&LayerDescriptor> { - self.layers.iter().find(|l| l.filename() == Some(filename)) - } - - /// Get blob reference for a file (ghcr_pkg@digest format) - pub fn get_blob_ref(&self, filename: &str) -> Option { - let ghcr_pkg = self.ghcr_pkg()?; - let base_pkg = ghcr_pkg.split(':').next()?; - let layer = self.get_layer_by_filename(filename)?; - Some(format!("{}@{}", base_pkg, layer.digest)) - } -} - -/// Extract metadata from manifest annotations into structured format -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct ManifestMetadata { - pub ghcr_pkg: Option, - pub build_action: Option, - pub build_id: Option, - pub build_date: Option, - pub build_log: Option, - pub version: Option, - pub pkg_name: Option, - pub description: Option, - pub total_size: u64, - pub files: Vec, -} - -impl ManifestMetadata { - /// Extract metadata from an OCI manifest - pub fn from_manifest(manifest: &OciManifest) -> Self { - // Try to get embedded JSON first - let pkg_json = manifest.get_package_json().ok().flatten(); - - let get_json_field = |field: &str| -> Option { - pkg_json - .as_ref() - .and_then(|j| j.get(field)) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - }; - - Self { - ghcr_pkg: manifest.ghcr_pkg().map(|s| s.to_string()), - build_action: manifest.build_action().map(|s| s.to_string()), - build_id: manifest.build_id().map(|s| s.to_string()), - build_date: get_json_field("build_date"), - build_log: get_json_field("build_log"), - version: get_json_field("version"), - pkg_name: get_json_field("pkg_name"), - description: get_json_field("description"), - total_size: manifest.total_size(), - files: manifest - .filenames() - .into_iter() - .map(|s| s.to_string()) - .collect(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_manifest() { - let json = r#"{ - "schemaVersion": 2, - "mediaType": "application/vnd.oci.image.manifest.v1+json", - "layers": [ - { - "mediaType": "application/octet-stream", - "size": 1024, - "digest": "sha256:abc123", - "annotations": { - "org.opencontainers.image.title": "mybin" - } - } - ], - "annotations": { - "dev.pkgforge.soar.ghcr_pkg": "ghcr.io/pkgforge/mybin:v1.0" - } - }"#; - - let manifest = OciManifest::from_json(json).unwrap(); - assert_eq!(manifest.schema_version, 2); - assert_eq!(manifest.layers.len(), 1); - assert_eq!(manifest.ghcr_pkg(), Some("ghcr.io/pkgforge/mybin:v1.0")); - assert_eq!(manifest.filenames(), vec!["mybin"]); - } - - #[test] - fn test_format_size() { - assert_eq!(format_size(0), "0 B"); - assert_eq!(format_size(1023), "1023 B"); - assert_eq!(format_size(1024), "1.00 KB"); - assert_eq!(format_size(1536), "1.50 KB"); - assert_eq!(format_size(1048576), "1.00 MB"); - } - - #[test] - fn test_total_size() { - let json = r#"{ - "schemaVersion": 2, - "layers": [ - {"mediaType": "application/octet-stream", "size": 100, "digest": "sha256:a"}, - {"mediaType": "application/octet-stream", "size": 200, "digest": "sha256:b"} - ] - }"#; - - let manifest = OciManifest::from_json(json).unwrap(); - assert_eq!(manifest.total_size(), 300); - } -} diff --git a/sbuild-meta/src/metadata.rs b/sbuild-meta/src/metadata.rs deleted file mode 100644 index 24ab2ce..0000000 --- a/sbuild-meta/src/metadata.rs +++ /dev/null @@ -1,544 +0,0 @@ -//! Package metadata generation -//! -//! Builds complete package metadata by combining: -//! - SBUILD recipe data -//! - OCI manifest annotations -//! - Registry information - -use crate::manifest::OciManifest; -use crate::recipe::SBuildRecipe; -use serde::{Deserialize, Serialize}; - -/// Helper to skip serializing empty vectors -fn is_empty_vec(v: &Option>) -> bool { - v.as_ref().map(|v| v.is_empty()).unwrap_or(true) -} - -/// Helper to skip serializing empty strings -fn is_empty_string(s: &str) -> bool { - s.is_empty() -} - -/// Format bytes as human-readable string -pub fn format_size(bytes: u64) -> String { - const KB: u64 = 1024; - const MB: u64 = KB * 1024; - const GB: u64 = MB * 1024; - - if bytes >= GB { - format!("{:.2} GB", bytes as f64 / GB as f64) - } else if bytes >= MB { - format!("{:.2} MB", bytes as f64 / MB as f64) - } else if bytes >= KB { - format!("{:.2} KB", bytes as f64 / KB as f64) - } else { - format!("{} B", bytes) - } -} - -/// Complete package metadata (compatible with soarql RemotePackage) -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct PackageMetadata { - // Core identifiers - ordered to match expected format - #[serde(rename = "_disabled", skip_serializing_if = "Option::is_none")] - pub disabled: Option, - - #[serde(skip_serializing_if = "is_empty_string")] - pub pkg: String, - - #[serde(skip_serializing_if = "is_empty_string")] - pub pkg_id: String, - - #[serde(skip_serializing_if = "is_empty_string")] - pub pkg_name: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub pkg_family: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub pkg_type: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub pkg_webpage: Option, - - #[serde(skip_serializing_if = "is_empty_string")] - pub description: String, - - #[serde(skip_serializing_if = "is_empty_string")] - pub version: String, - - // Download info - #[serde(skip_serializing_if = "is_empty_string")] - pub download_url: String, - - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub size_raw: Option, - - // GHCR info - #[serde(skip_serializing_if = "Option::is_none")] - pub ghcr_pkg: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ghcr_size: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ghcr_size_raw: Option, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub ghcr_files: Option>, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ghcr_blob: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub ghcr_url: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub manifest_url: Option, - - // Source info - #[serde(skip_serializing_if = "is_empty_vec")] - pub src_url: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub homepage: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub license: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub maintainer: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub note: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub tag: Option>, - - // Checksums - #[serde(skip_serializing_if = "Option::is_none")] - pub bsum: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub shasum: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub checksum_bsum: Option, - - // Build info - #[serde(skip_serializing_if = "Option::is_none")] - pub build_id: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub build_date: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub build_gha: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub build_script: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub build_log: Option, - - // Categories and provides - #[serde(skip_serializing_if = "is_empty_vec")] - pub category: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub provides: Option>, - - // Desktop/App info - #[serde(skip_serializing_if = "Option::is_none")] - pub icon: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub desktop: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub appstream: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub app_id: Option, - - // Flags - all as proper booleans - #[serde(skip_serializing_if = "Option::is_none")] - pub deprecated: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub desktop_integration: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub portable: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub recurse_provides: Option, - - // Additional - #[serde(skip_serializing_if = "is_empty_vec")] - pub snapshots: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub replaces: Option>, - - #[serde(skip_serializing_if = "is_empty_vec")] - pub repology: Option>, -} - -impl PackageMetadata { - /// Create metadata from SBUILD recipe (minimal, without registry data) - pub fn from_recipe(recipe: &SBuildRecipe) -> Self { - Self { - pkg: recipe.pkg.clone(), - pkg_id: recipe.pkg_id.clone(), - pkg_name: recipe.pkg.clone(), - pkg_family: Some(recipe.pkg.clone()), - pkg_type: recipe.pkg_type.clone(), - description: recipe.description.clone(), - version: recipe.pkgver.clone().unwrap_or_default(), - src_url: if recipe.src_url.is_empty() { - None - } else { - Some(recipe.src_url.clone()) - }, - homepage: if recipe.homepage.is_empty() { - None - } else { - Some(recipe.homepage.clone()) - }, - license: if recipe.license.is_empty() { - None - } else { - Some(recipe.license.clone()) - }, - maintainer: if recipe.maintainer.is_empty() { - None - } else { - Some(recipe.maintainer.clone()) - }, - note: if recipe.note.is_empty() { - None - } else { - Some(recipe.note.clone()) - }, - tag: if recipe.tag.is_empty() { - None - } else { - Some(recipe.tag.clone()) - }, - category: if recipe.category.is_empty() { - None - } else { - Some(recipe.category.clone()) - }, - provides: if recipe.provides.is_empty() { - None - } else { - Some(recipe.provides.clone()) - }, - repology: if recipe.repology.is_empty() { - None - } else { - Some(recipe.repology.clone()) - }, - snapshots: if recipe.snapshots.is_empty() { - None - } else { - Some(recipe.snapshots.clone()) - }, - disabled: if recipe.disabled { Some(true) } else { None }, - ..Default::default() - } - } - - /// Enrich metadata with OCI manifest data - /// - /// `ghcr_path` is the repository path (e.g., "pkgforge/hello/static") - /// `arch` is the target architecture (e.g., "x86_64-linux") - pub fn enrich_from_manifest(&mut self, manifest: &OciManifest, ghcr_path: &str, arch: &str) { - // Get embedded JSON if available - if let Ok(Some(pkg_json)) = manifest.get_package_json() { - self.merge_from_json(&pkg_json); - } - - // GHCR info - construct with version placeholder for snapshots support - let ghcr_pkg = format!("ghcr.io/{}:{{{{version}}}}-{}", ghcr_path, arch); - self.ghcr_pkg = Some(ghcr_pkg); - self.ghcr_url = Some(format!("https://ghcr.io/{}", ghcr_path)); - - let size = manifest.total_size(); - self.ghcr_size_raw = Some(size); - self.ghcr_size = Some(format_size(size)); - self.ghcr_files = Some( - manifest - .filenames() - .into_iter() - .map(|s| s.to_string()) - .collect(), - ); - - // Version from annotations - if let Some(version) = manifest.get_annotation("dev.pkgforge.soar.version") { - self.version = version.to_string(); - } else if let Some(version) = manifest.get_annotation("org.opencontainers.image.version") { - self.version = version.to_string(); - } - - // Build date from annotations - if let Some(date) = manifest.get_annotation("dev.pkgforge.soar.push_date") { - self.build_date = Some(date.to_string()); - } else if let Some(date) = manifest.get_annotation("org.opencontainers.image.created") { - self.build_date = Some(date.to_string()); - } - - // Build info from annotations - if self.build_id.is_none() { - self.build_id = manifest.build_id().map(|s| s.to_string()); - } - - // Build GHA URL from annotation only (cannot determine repo without cache_type) - if let Some(build_gha) = manifest.get_annotation("dev.pkgforge.soar.build_gha") { - self.build_gha = Some(build_gha.to_string()); - } - - // Build script from annotations (prefer annotation over default) - if let Some(script) = manifest.get_annotation("dev.pkgforge.soar.build_script") { - self.build_script = Some(script.to_string()); - } - - // Checksums from annotations - if self.bsum.is_none() { - if let Some(bsum) = manifest.get_annotation("dev.pkgforge.soar.bsum") { - self.bsum = Some(bsum.to_string()); - } - } - if self.shasum.is_none() { - if let Some(shasum) = manifest.get_annotation("dev.pkgforge.soar.shasum") { - self.shasum = Some(shasum.to_string()); - } - } - if self.checksum_bsum.is_none() { - if let Some(cb) = manifest.get_annotation("dev.pkgforge.soar.checksum_bsum") { - self.checksum_bsum = Some(cb.to_string()); - } - } - - // Generate blob reference and download URLs - let filenames = manifest.filenames(); - - // Find the main binary - prefer pkg_name, then exclude known auxiliary files - let main_file = filenames - .iter() - .find(|f| { - let name = f.rsplit('/').next().unwrap_or(f); - name == self.pkg_name || name == &self.pkg - }) - .or_else(|| { - // Fallback: find first file that's not an auxiliary file - filenames.iter().find(|f| { - let name = f.rsplit('/').next().unwrap_or(f); - !name.ends_with(".version") - && !name.ends_with(".log") - && !name.ends_with(".sig") - && !name.ends_with(".json") - && name != "CHECKSUM" - && name != "SBUILD" - && name != "LICENSE" - && name != "BUILD.log" - }) - }) - .or_else(|| filenames.first()); - - if let Some(filename) = main_file { - self.ghcr_blob = manifest.get_blob_ref(filename); - - // Generate download URL and manifest URL with version placeholder for snapshots support - let version_tag = format!("{{{{version}}}}-{}", arch); - self.download_url = format!( - "https://api.ghcr.pkgforge.dev/{}?tag={}&download={}", - ghcr_path, version_tag, filename - ); - self.manifest_url = Some(format!( - "https://api.ghcr.pkgforge.dev/{}?tag={}&manifest", - ghcr_path, version_tag - )); - - // Get size of main binary if available - if let Some(layer) = manifest.get_layer_by_filename(filename) { - self.size_raw = Some(layer.size); - self.size = Some(format_size(layer.size)); - } else { - self.size_raw = self.ghcr_size_raw; - self.size = self.ghcr_size.clone(); - } - } - } - - /// Merge data from embedded JSON - fn merge_from_json(&mut self, json: &serde_json::Value) { - let get_str = |key: &str| -> Option { - json.get(key) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - }; - - let get_vec = |key: &str| -> Option> { - json.get(key).and_then(|v| v.as_array()).map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(|s| s.to_string()) - .collect() - }) - }; - - // Override with manifest values if present - if let Some(v) = get_str("version") { - self.version = v; - } - if let Some(v) = get_str("description") { - self.description = v; - } - if let Some(v) = get_str("build_date") { - self.build_date = Some(v); - } - if let Some(v) = get_str("build_log") { - self.build_log = Some(v); - } - if let Some(v) = get_str("build_script") { - self.build_script = Some(v); - } - if let Some(v) = get_str("bsum") { - self.bsum = Some(v); - } - if let Some(v) = get_str("shasum") { - self.shasum = Some(v); - } - if let Some(v) = get_str("checksum_bsum") { - self.checksum_bsum = Some(v); - } - if let Some(v) = get_str("icon") { - self.icon = Some(v); - } - if let Some(v) = get_str("desktop") { - self.desktop = Some(v); - } - if let Some(v) = get_str("appstream") { - self.appstream = Some(v); - } - - // Array fields - if self.provides.is_none() { - self.provides = get_vec("provides"); - } - } - - /// Merge snapshots from an external source (e.g., MongoDB cache) - /// Combines existing snapshots with new ones, deduplicating - pub fn merge_snapshots(&mut self, external_snapshots: &[String]) { - let mut combined = self.snapshots.take().unwrap_or_default(); - for snapshot in external_snapshots { - if !combined.contains(snapshot) { - combined.push(snapshot.clone()); - } - } - if !combined.is_empty() { - self.snapshots = Some(combined); - } - } - - /// Parse flags from notes and filter out internal flag messages - /// Sets deprecated flag and removes [DEPRECATED], [EXTERNAL], [NO_INSTALL], [UNTRUSTED] messages - pub fn parse_note_flags(&mut self) { - if let Some(notes) = self.note.take() { - // Check for deprecated flag before filtering - if notes.iter().any(|n| n.contains("[DEPRECATED]")) { - self.deprecated = Some(true); - } - - // Filter out internal flag messages - these are for CI/internal use only - let filtered: Vec = notes - .into_iter() - .filter(|note| { - !note.contains("[DEPRECATED]") - && !note.contains("[EXTERNAL]") - && !note.contains("[NO_INSTALL]") - && !note.contains("[UNTRUSTED]") - && !note.contains("[DO NOT RUN]") - }) - .collect(); - - // Only set note if there are remaining notes after filtering - if !filtered.is_empty() { - self.note = Some(filtered); - } - } - } - - /// Validate that required fields are present - pub fn is_valid(&self) -> bool { - !self.pkg.is_empty() - && !self.pkg_id.is_empty() - && !self.pkg_name.is_empty() - && !self.description.is_empty() - && !self.version.is_empty() - && !self.download_url.is_empty() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::recipe::SBuildRecipe; - - #[test] - fn test_from_recipe() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -version: "1.0.0" -description: A test package -category: - - Utility -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let metadata = PackageMetadata::from_recipe(&recipe); - - assert_eq!(metadata.pkg, "test"); - assert_eq!(metadata.pkg_id, "example.com.test"); - assert_eq!(metadata.version, "1.0.0"); - } - - #[test] - fn test_parse_note_flags() { - let mut metadata = PackageMetadata::default(); - metadata.note = Some(vec![ - "[DEPRECATED] Old package".to_string(), - "[NO_INSTALL] Do not install".to_string(), - "This is a real note".to_string(), - ]); - - metadata.parse_note_flags(); - - // Deprecated flag should be set - assert_eq!(metadata.deprecated, Some(true)); - // Internal flag messages should be filtered out, only real notes remain - assert_eq!(metadata.note, Some(vec!["This is a real note".to_string()])); - } - - #[test] - fn test_parse_note_flags_all_filtered() { - let mut metadata = PackageMetadata::default(); - metadata.note = Some(vec![ - "[DEPRECATED] Old package".to_string(), - "[DO NOT RUN] CI only".to_string(), - ]); - - metadata.parse_note_flags(); - - // All notes were internal flags, so note should be None - assert_eq!(metadata.note, None); - } -} diff --git a/sbuild-meta/src/recipe.rs b/sbuild-meta/src/recipe.rs deleted file mode 100644 index 8c6d943..0000000 --- a/sbuild-meta/src/recipe.rs +++ /dev/null @@ -1,772 +0,0 @@ -//! SBUILD recipe parsing and handling - -use saphyr::{LoadableYamlNode, YamlOwned}; -use std::path::Path; - -use crate::{Error, Result}; - -/// Sanitize a name to be OCI repository name compliant -/// OCI repository names must be lowercase and only contain [a-z0-9._-] -pub fn sanitize_oci_name(name: &str) -> String { - // First replace ++ with pp (common convention: c++ -> cpp, g++ -> gpp) - let name = name.replace("++", "pp"); - - name.to_lowercase() - .chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '.' || c == '_' || c == '-' { - c - } else { - '-' // Replace other invalid chars with - - } - }) - .collect::() - // Remove consecutive dashes - .split('-') - .filter(|s| !s.is_empty()) - .collect::>() - .join("-") -} - -/// GHCR package information with path components -#[derive(Debug, Clone)] -pub struct GhcrPackageInfo { - /// Full GHCR repository path (e.g., "pkgforge/bincache/hello/static") - pub ghcr_path: String, - /// Binary/package name (e.g., "hello") - pub pkg_name: String, - /// Package family/directory name (e.g., "hello") - pub pkg_family: String, - /// Recipe name without extension (e.g., "static", "appimage.cat.stable") - pub recipe_name: String, -} - -impl GhcrPackageInfo { - /// Get the GHCR URL - pub fn ghcr_url(&self) -> String { - format!("https://ghcr.io/{}", self.ghcr_path) - } - - /// Get the package webpage URL - pub fn pkg_webpage(&self, arch: &str) -> String { - let arch_lower = arch.to_lowercase(); - format!( - "https://pkgs.pkgforge.dev/repo/soarpkgs/{}/{}/{}/{}", - arch_lower, self.pkg_family, self.recipe_name, self.pkg_name - ) - } -} - -/// Per-package configuration for multi-package recipes -#[derive(Debug, Clone, Default)] -pub struct PackageConfig { - pub provides: Vec, -} - -/// Execution configuration for building packages -#[derive(Debug, Clone, Default)] -pub struct ExecConfig { - pub host: Vec, - pub arch: Vec, - pub os: Vec, - pub shell: Option, - pub pkgver: Option, - pub run: Option, -} - -/// Parsed SBUILD recipe -#[derive(Debug, Clone, Default)] -pub struct SBuildRecipe { - pub disabled: bool, - pub pkg: String, - pub pkg_id: String, - pub pkg_type: Option, - pub pkgver: Option, - pub remote_pkgver: Option, - pub category: Vec, - pub description: String, - pub homepage: Vec, - pub license: Vec, - pub maintainer: Vec, - pub note: Vec, - pub provides: Vec, - pub packages: Vec<(String, PackageConfig)>, - pub repology: Vec, - pub src_url: Vec, - pub tag: Vec, - pub snapshots: Vec, - pub ghcr_pkg: Option, - pub x_exec: Option, -} - -fn get_str(yaml: &YamlOwned, key: &str) -> Option { - yaml.as_mapping_get(key) - .and_then(|v: &YamlOwned| v.as_str()) - .map(|s| s.to_string()) -} - -fn get_bool(yaml: &YamlOwned, key: &str) -> Option { - yaml.as_mapping_get(key) - .and_then(|v: &YamlOwned| v.as_bool()) -} - -fn get_string_vec(yaml: &YamlOwned, key: &str) -> Vec { - yaml.as_mapping_get(key) - .and_then(|v: &YamlOwned| v.as_sequence()) - .map(|seq| { - seq.iter() - .filter_map(|v: &YamlOwned| v.as_str().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default() -} - -fn get_description(yaml: &YamlOwned) -> String { - let Some(desc) = yaml.as_mapping_get("description") else { - return String::new(); - }; - if let Some(s) = desc.as_str() { - return s.to_string(); - } - if let Some(map) = desc.as_mapping() { - // Try _default first, then first value - for (k, v) in map { - if k.as_str() == Some("_default") { - if let Some(s) = v.as_str() { - return s.to_string(); - } - } - } - for (_, v) in map { - if let Some(s) = v.as_str() { - return s.to_string(); - } - } - } - if let Some(seq) = desc.as_sequence() { - return seq - .iter() - .filter_map(|v: &YamlOwned| v.as_str()) - .collect::>() - .join("; "); - } - String::new() -} - -fn parse_exec_config(yaml: &YamlOwned) -> Option { - let exec = yaml.as_mapping_get("x_exec")?; - Some(ExecConfig { - host: get_string_vec(exec, "host"), - arch: get_string_vec(exec, "arch"), - os: get_string_vec(exec, "os"), - shell: get_str(exec, "shell"), - pkgver: get_str(exec, "pkgver"), - run: get_str(exec, "run"), - }) -} - -fn parse_packages(yaml: &YamlOwned) -> Vec<(String, PackageConfig)> { - let Some(pkgs) = yaml.as_mapping_get("packages") else { - return Vec::new(); - }; - let Some(mapping) = pkgs.as_mapping() else { - return Vec::new(); - }; - mapping - .iter() - .filter_map(|(k, v)| { - let name = k.as_str()?.to_string(); - let provides = get_string_vec(v, "provides"); - Some((name, PackageConfig { provides })) - }) - .collect() -} - -impl SBuildRecipe { - /// Parse a recipe from YAML content - pub fn from_yaml(content: &str) -> Result { - let docs = YamlOwned::load_from_str(content).map_err(|e| Error::Yaml(e.to_string()))?; - let yaml = docs - .into_iter() - .next() - .ok_or_else(|| Error::Yaml("Empty YAML document".into()))?; - - Ok(Self { - disabled: get_bool(&yaml, "_disabled").unwrap_or(false), - pkg: get_str(&yaml, "pkg").unwrap_or_default(), - pkg_id: get_str(&yaml, "pkg_id").unwrap_or_default(), - pkg_type: get_str(&yaml, "pkg_type"), - pkgver: get_str(&yaml, "pkgver").or_else(|| get_str(&yaml, "version")), - remote_pkgver: get_str(&yaml, "remote_pkgver"), - category: get_string_vec(&yaml, "category"), - description: get_description(&yaml), - homepage: get_string_vec(&yaml, "homepage"), - license: get_string_vec(&yaml, "license"), - maintainer: get_string_vec(&yaml, "maintainer"), - note: get_string_vec(&yaml, "note"), - provides: get_string_vec(&yaml, "provides"), - packages: parse_packages(&yaml), - repology: get_string_vec(&yaml, "repology"), - src_url: get_string_vec(&yaml, "src_url"), - tag: get_string_vec(&yaml, "tag"), - snapshots: get_string_vec(&yaml, "snapshots"), - ghcr_pkg: get_str(&yaml, "ghcr_pkg"), - x_exec: parse_exec_config(&yaml), - }) - } - - /// Parse a recipe from a file path - pub fn from_file(path: &Path) -> Result { - let content = std::fs::read_to_string(path)?; - Self::from_yaml(&content) - } - - /// Check if this recipe supports a given architecture - pub fn supports_arch(&self, arch: &str) -> bool { - match &self.x_exec { - Some(exec) => { - // If host is specified, only check host (not arch/os) - if !exec.host.is_empty() { - return exec.host.iter().any(|h| h.eq_ignore_ascii_case(arch)); - } - - // Parse arch string like "x86_64-linux" into arch and os parts - let parts: Vec<&str> = arch.split('-').collect(); - if parts.len() >= 2 { - let target_arch = parts[0]; - let target_os = parts[1]; - - let arch_match = exec.arch.is_empty() - || exec - .arch - .iter() - .any(|a| a.eq_ignore_ascii_case(target_arch)); - - let os_match = exec.os.is_empty() - || exec.os.iter().any(|o| o.eq_ignore_ascii_case(target_os)); - - return arch_match && os_match; - } - - false - } - None => false, - } - } - - /// Get the pkgver script if available - pub fn pkgver_script(&self) -> Option<&str> { - self.x_exec.as_ref()?.pkgver.as_deref() - } - - /// Check if recipe is disabled - pub fn is_disabled(&self) -> bool { - self.disabled - } - - /// Get the GHCR package path for this recipe (simple version) - pub fn ghcr_package(&self) -> String { - self.pkg.clone() - } - - /// Extract unique package names from provides or packages field - pub fn get_provided_packages(&self) -> Vec { - // If packages field is set, use its keys as package names - if !self.packages.is_empty() { - return self.packages.iter().map(|(name, _)| name.clone()).collect(); - } - - if self.provides.is_empty() { - return vec![self.pkg.clone()]; - } - - let mut seen = std::collections::HashSet::new(); - let mut packages = Vec::new(); - - for entry in &self.provides { - // Skip binary-only entries (starting with @) - if entry.starts_with('@') { - continue; - } - - // Extract base name before any separator (:, ==, =>) - let base = entry - .split("=>") - .next() - .unwrap_or(entry) - .split("==") - .next() - .unwrap_or(entry) - .split(':') - .next() - .unwrap_or(entry) - .to_string(); - - if !base.is_empty() && seen.insert(base.clone()) { - packages.push(base); - } - } - - if packages.is_empty() { - vec![self.pkg.clone()] - } else { - packages - } - } - - /// Extract binary-only entries from provides field - pub fn get_binaries(&self) -> Vec { - let mut seen = std::collections::HashSet::new(); - let mut binaries = Vec::new(); - - for entry in &self.provides { - if let Some(bin_name) = entry.strip_prefix('@') { - let bin_name = bin_name.to_string(); - if !bin_name.is_empty() && seen.insert(bin_name.clone()) { - binaries.push(bin_name); - } - } - } - - binaries - } - - /// Get the provides list for a specific package (from the packages field) - pub fn get_package_provides(&self, pkg_name: &str) -> Option<&[String]> { - self.packages - .iter() - .find(|(name, _)| name == pkg_name) - .map(|(_, config)| config.provides.as_slice()) - } - - /// Check if this recipe uses the multi-package layout - pub fn has_packages(&self) -> bool { - !self.packages.is_empty() - } - - /// GHCR package information including path components - pub fn ghcr_packages_from_path( - &self, - recipe_path: &Path, - ghcr_owner: &str, - ) -> Vec { - let mut packages = Vec::new(); - - let pkg_family = recipe_path - .parent() - .and_then(|p| p.file_name()) - .and_then(|n| n.to_str()) - .unwrap_or(&self.pkg) - .to_string(); - - let recipe_name = recipe_path - .file_stem() - .and_then(|n| n.to_str()) - .unwrap_or("static") - .to_string(); - - let provided_packages = self.get_provided_packages(); - - for pkg_name in provided_packages { - let sanitized_pkg_name = sanitize_oci_name(&pkg_name); - let ghcr_path = if let Some(ref custom_base) = self.ghcr_pkg { - format!("{}/{}/{}", ghcr_owner, custom_base, sanitized_pkg_name) - } else { - format!( - "{}/{}/{}/{}", - ghcr_owner, pkg_family, recipe_name, sanitized_pkg_name - ) - }; - - packages.push(GhcrPackageInfo { - ghcr_path, - pkg_name, - pkg_family: pkg_family.clone(), - recipe_name: recipe_name.clone(), - }); - } - - packages - } - - /// Get the build script URL - pub fn build_script_url(&self) -> String { - format!( - "https://raw.githubusercontent.com/pkgforge/soarpkgs/refs/heads/main/binaries/{}/", - self.pkg - ) - } -} - -/// Scan a directory for SBUILD recipes -pub fn scan_recipes(dir: &Path) -> Result> { - let pattern = dir.join("**/*.yaml"); - let pattern_str = pattern.to_string_lossy(); - - let mut recipes = Vec::new(); - - for entry in glob::glob(&pattern_str)? { - match entry { - Ok(path) => { - if !path - .file_name() - .map(|n| n.to_string_lossy().contains(".yaml")) - .unwrap_or(false) - { - continue; - } - - match SBuildRecipe::from_file(&path) { - Ok(recipe) => recipes.push((path, recipe)), - Err(e) => { - log::warn!("Failed to parse recipe {:?}: {}", path, e); - } - } - } - Err(e) => { - log::warn!("Glob error: {}", e); - } - } - } - - Ok(recipes) -} - -/// Filter recipes by architecture -pub fn filter_by_arch( - recipes: Vec<(std::path::PathBuf, SBuildRecipe)>, - arch: &str, -) -> Vec<(std::path::PathBuf, SBuildRecipe)> { - recipes - .into_iter() - .filter(|(_, recipe)| recipe.supports_arch(arch)) - .collect() -} - -/// Filter out disabled recipes -pub fn filter_enabled( - recipes: Vec<(std::path::PathBuf, SBuildRecipe)>, -) -> Vec<(std::path::PathBuf, SBuildRecipe)> { - recipes - .into_iter() - .filter(|(_, recipe)| !recipe.is_disabled()) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_minimal_recipe() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -description: A test package -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - assert_eq!(recipe.pkg, "test"); - assert_eq!(recipe.pkg_id, "example.com.test"); - } - - #[test] - fn test_parse_with_version() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -version: "1.2.3" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - assert_eq!(recipe.pkgver, Some("1.2.3".to_string())); - } - - #[test] - fn test_supports_arch() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -x_exec: - host: - - "x86_64-linux" - - "aarch64-linux" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - assert!(recipe.supports_arch("x86_64-linux")); - assert!(recipe.supports_arch("aarch64-linux")); - assert!(!recipe.supports_arch("riscv64-linux")); - } - - #[test] - fn test_ghcr_packages_from_path() { - let yaml = r#" -pkg: batcat -pkg_id: github.com.sharkdp.bat -provides: - - "bat==batcat" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("binaries/bat/static.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].ghcr_path, "pkgforge/bat/static/bat"); - assert_eq!(packages[0].pkg_name, "bat"); - assert_eq!(packages[0].recipe_name, "static"); - } - - #[test] - fn test_ghcr_packages_multiple_binaries() { - let yaml = r#" -pkg: myapp -pkg_id: example.com.myapp -provides: - - "app1" - - "app2" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("binaries/myapp/static.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 2); - assert_eq!(packages[0].ghcr_path, "pkgforge/myapp/static/app1"); - assert_eq!(packages[1].ghcr_path, "pkgforge/myapp/static/app2"); - } - - #[test] - fn test_ghcr_packages_with_symlinks_dedup() { - let yaml = r#" -pkg: busybox -pkg_id: example.com.busybox -provides: - - "busybox==whoami" - - "busybox==ls" - - "busybox:bbox" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("binaries/busybox/static.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].ghcr_path, "pkgforge/busybox/static/busybox"); - assert_eq!(packages[0].pkg_name, "busybox"); - } - - #[test] - fn test_ghcr_packages_pkgcache() { - let yaml = r#" -pkg: 0ad -pkg_id: io.github.0ad -provides: - - "0ad" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("packages/0ad/appimage.0ad-matters.stable.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 1); - assert_eq!( - packages[0].ghcr_path, - "pkgforge/0ad/appimage.0ad-matters.stable/0ad" - ); - assert_eq!(packages[0].pkg_name, "0ad"); - assert_eq!(packages[0].recipe_name, "appimage.0ad-matters.stable"); - } - - #[test] - fn test_get_provided_packages() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -provides: - - "prog-a" - - "prog-b:alias" - - "prog-b==symlink" - - "prog-c=>renamed" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let packages = recipe.get_provided_packages(); - - assert_eq!(packages.len(), 3); - assert!(packages.contains(&"prog-a".to_string())); - assert!(packages.contains(&"prog-b".to_string())); - assert!(packages.contains(&"prog-c".to_string())); - } - - #[test] - fn test_ghcr_packages_custom_ghcr_pkg() { - let yaml = r#" -pkg: myapp -pkg_id: example.com.myapp -ghcr_pkg: "custom-cache/custom-pkg" -provides: - - "app1" - - "app2" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("binaries/myapp/static.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 2); - assert_eq!( - packages[0].ghcr_path, - "pkgforge/custom-cache/custom-pkg/app1" - ); - assert_eq!( - packages[1].ghcr_path, - "pkgforge/custom-cache/custom-pkg/app2" - ); - } - - #[test] - fn test_sanitize_oci_name() { - assert_eq!(sanitize_oci_name("hello"), "hello"); - assert_eq!(sanitize_oci_name("Hello"), "hello"); - assert_eq!(sanitize_oci_name("c++filt"), "cppfilt"); - assert_eq!(sanitize_oci_name("g++"), "gpp"); - assert_eq!(sanitize_oci_name("clang++"), "clangpp"); - assert_eq!(sanitize_oci_name("foo@bar"), "foo-bar"); - assert_eq!(sanitize_oci_name("foo#bar"), "foo-bar"); - assert_eq!(sanitize_oci_name("ld.gold"), "ld.gold"); - assert_eq!(sanitize_oci_name("my_app"), "my_app"); - } - - #[test] - fn test_ghcr_packages_with_special_chars() { - let yaml = r#" -pkg: binutils -pkg_id: gnu.org.binutils -provides: - - "c++filt" - - "ld.gold" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("binaries/binutils/static.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 2); - assert_eq!(packages[0].ghcr_path, "pkgforge/binutils/static/cppfilt"); - assert_eq!(packages[0].pkg_name, "c++filt"); - assert_eq!(packages[1].ghcr_path, "pkgforge/binutils/static/ld.gold"); - assert_eq!(packages[1].pkg_name, "ld.gold"); - } - - #[test] - fn test_get_provided_packages_excludes_binaries() { - let yaml = r#" -pkg: yazi -pkg_id: github.com.sxyazi.yazi -provides: - - "yazi" - - "@ya" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let packages = recipe.get_provided_packages(); - - assert_eq!(packages.len(), 1); - assert_eq!(packages[0], "yazi"); - } - - #[test] - fn test_get_provided_packages_mixed() { - let yaml = r#" -pkg: myapp -pkg_id: example.com.myapp -provides: - - "app1" - - "@bin1" - - "app2:alias" - - "@bin2" - - "app3=>rename" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let packages = recipe.get_provided_packages(); - - assert_eq!(packages.len(), 3); - assert!(packages.contains(&"app1".to_string())); - assert!(packages.contains(&"app2".to_string())); - assert!(packages.contains(&"app3".to_string())); - } - - #[test] - fn test_get_binaries() { - let yaml = r#" -pkg: yazi -pkg_id: github.com.sxyazi.yazi -provides: - - "yazi" - - "@ya" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let binaries = recipe.get_binaries(); - - assert_eq!(binaries.len(), 1); - assert_eq!(binaries[0], "ya"); - } - - #[test] - fn test_get_binaries_multiple() { - let yaml = r#" -pkg: myapp -pkg_id: example.com.myapp -provides: - - "app1" - - "@bin1" - - "app2" - - "@bin2" - - "@bin3" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let binaries = recipe.get_binaries(); - - assert_eq!(binaries.len(), 3); - assert!(binaries.contains(&"bin1".to_string())); - assert!(binaries.contains(&"bin2".to_string())); - assert!(binaries.contains(&"bin3".to_string())); - } - - #[test] - fn test_get_provided_packages_no_provides() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let packages = recipe.get_provided_packages(); - - assert_eq!(packages.len(), 1); - assert_eq!(packages[0], "test"); - } - - #[test] - fn test_get_binaries_empty() { - let yaml = r#" -pkg: test -pkg_id: example.com.test -provides: - - "app1" - - "app2" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let binaries = recipe.get_binaries(); - - assert!(binaries.is_empty()); - } - - #[test] - fn test_ghcr_packages_excludes_binaries() { - let yaml = r#" -pkg: yazi -pkg_id: github.com.sxyazi.yazi -provides: - - "yazi" - - "@ya" -"#; - let recipe = SBuildRecipe::from_yaml(yaml).unwrap(); - let path = Path::new("binaries/yazi/static.yaml"); - let packages = recipe.ghcr_packages_from_path(path, "pkgforge"); - - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].ghcr_path, "pkgforge/yazi/static/yazi"); - assert_eq!(packages[0].pkg_name, "yazi"); - } -} diff --git a/sbuild-meta/src/registry.rs b/sbuild-meta/src/registry.rs deleted file mode 100644 index eb3758f..0000000 --- a/sbuild-meta/src/registry.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! GHCR/OCI Registry client -//! -//! Provides functionality to interact with GitHub Container Registry -//! for fetching manifests, tags, and package metadata. - -use std::cmp::Ordering; - -use reqwest::header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION}; -use serde::Deserialize; - -use crate::{Error, Result}; - -const GHCR_API_BASE: &str = "https://ghcr.io/v2"; - -/// Tag list response from registry -#[derive(Debug, Deserialize)] -pub struct TagList { - pub name: String, - pub tags: Vec, -} - -/// OCI registry client -#[derive(Clone)] -pub struct RegistryClient { - client: reqwest::Client, -} - -impl RegistryClient { - /// Create a new registry client - pub fn new() -> Self { - Self { - client: reqwest::Client::builder() - .user_agent("sbuild-meta/0.1.0") - .build() - .expect("Failed to create HTTP client"), - } - } - - /// Build headers for registry requests - /// Uses anonymous bearer token (QQ== = base64 of "A") for public repos - fn build_headers() -> HeaderMap { - let mut headers = HeaderMap::new(); - headers.insert(AUTHORIZATION, HeaderValue::from_static("Bearer QQ==")); - headers.insert( - ACCEPT, - HeaderValue::from_static( - "application/vnd.docker.distribution.manifest.v2+json, \ - application/vnd.docker.distribution.manifest.list.v2+json, \ - application/vnd.oci.image.manifest.v1+json, \ - application/vnd.oci.image.index.v1+json, \ - application/vnd.oci.artifact.manifest.v1+json", - ), - ); - headers - } - - /// List tags for a repository - pub async fn list_tags(&self, repository: &str) -> Result { - let url = format!("{}/{}/tags/list", GHCR_API_BASE, repository); - - let response = self - .client - .get(&url) - .headers(Self::build_headers()) - .send() - .await?; - - if !response.status().is_success() { - return Err(Error::Registry(format!( - "Failed to list tags for {}: {}", - repository, - response.status() - ))); - } - - response.json().await.map_err(Error::Http) - } - - /// Filter tags for a specific architecture (case-insensitive) - pub fn filter_tags_by_arch<'a>(tags: &'a [String], arch: &str) -> Vec<&'a String> { - let arch_lower = arch.to_lowercase(); - tags.iter() - .filter(|tag| { - let tag_lower = tag.to_lowercase(); - // Filter out srcbuild tags and match architecture - !tag_lower.contains("srcbuild") && tag_lower.contains(&arch_lower) - }) - .collect() - } - - /// Get the latest tag for an architecture using version-aware comparison - pub fn get_latest_arch_tag<'a>(tags: &'a [String], arch: &str) -> Option<&'a String> { - Self::filter_tags_by_arch(tags, arch) - .into_iter() - .filter(|t| !t.starts_with("latest")) - .max_by(|a, b| version_compare_tags(a, b, arch)) - } - - /// Fetch manifest for a specific tag - pub async fn fetch_manifest(&self, repository: &str, tag: &str) -> Result { - let url = format!("{}/{}/manifests/{}", GHCR_API_BASE, repository, tag); - - let response = self - .client - .get(&url) - .headers(Self::build_headers()) - .send() - .await?; - - if response.status().as_u16() == 404 { - return Err(Error::ManifestNotFound(format!("{}:{}", repository, tag))); - } - - if !response.status().is_success() { - return Err(Error::Registry(format!( - "Failed to fetch manifest for {}:{}: {}", - repository, - tag, - response.status() - ))); - } - - response.text().await.map_err(Error::Http) - } - - /// Fetch manifest as parsed JSON - pub async fn fetch_manifest_json( - &self, - repository: &str, - tag: &str, - ) -> Result { - let manifest_str = self.fetch_manifest(repository, tag).await?; - serde_json::from_str(&manifest_str).map_err(Error::Json) - } - - /// Check if a package exists in the registry - pub async fn package_exists(&self, repository: &str) -> bool { - match self.list_tags(repository).await { - Ok(tags) => !tags.tags.is_empty(), - Err(_) => false, - } - } - - /// Get download URL for a package - pub fn get_download_url(repository: &str, tag: &str, filename: &str) -> String { - format!( - "https://api.ghcr.pkgforge.dev/{}?tag={}&download={}", - repository, tag, filename - ) - } - - /// Get GHCR web URL for a package - pub fn get_ghcr_url(repository: &str) -> String { - format!( - "https://github.com/pkgforge/{}/pkgs/container/{}", - repository.split('/').next().unwrap_or("bincache"), - repository.split('/').last().unwrap_or(repository) - ) - } -} - -impl Default for RegistryClient { - fn default() -> Self { - Self::new() - } -} - -/// Extract the version portion from a tag by stripping the `-{arch}` suffix. -/// e.g. "v1.2.3-x86_64-linux" -> "v1.2.3", "2026.2.23-aarch64-linux" -> "2026.2.23" -fn extract_version_from_tag<'a>(tag: &'a str, arch: &str) -> &'a str { - let arch_suffix = format!("-{}", arch); - tag.strip_suffix(&arch_suffix) - .or_else(|| { - // case-insensitive fallback - let lower = tag.to_lowercase(); - let suffix_lower = arch_suffix.to_lowercase(); - if lower.ends_with(&suffix_lower) { - Some(&tag[..tag.len() - arch_suffix.len()]) - } else { - None - } - }) - .unwrap_or(tag) -} - -/// Parse a version string into a semver::Version, handling common non-semver formats. -/// Strips leading 'v'/'V', handles calver (2026.2.23), release suffixes (-r1), etc. -fn parse_version_lenient(version: &str) -> Option { - let v = version - .strip_prefix('v') - .or(version.strip_prefix('V')) - .unwrap_or(version); - - // Check for -rN release revision suffix first (e.g. "0.16.1-r2") - // These should sort HIGHER than the base version, but semver treats - // pre-release as LOWER, so we handle them specially. - let (base_str, revision) = extract_release_revision(v); - - // Split on first '-' to separate version from extra info (dates, hashes, etc.) - let (numeric_base, extra) = match base_str.find('-') { - Some(idx) => (&base_str[..idx], Some(&base_str[idx + 1..])), - None => (base_str, None), - }; - - // Split base into numeric parts - let parts: Vec = numeric_base - .split('.') - .filter_map(|p| p.parse().ok()) - .collect(); - - if parts.is_empty() { - return None; - } - - let major = parts[0]; - let minor = if parts.len() > 1 { parts[1] } else { 0 }; - let mut patch = if parts.len() > 2 { parts[2] } else { 0 }; - - // Bump patch by revision so r2 > r1 > base - if let Some(rev) = revision { - patch += rev; - } - - let mut ver = semver::Version::new(major, minor, patch); - - // Any extra info (dates, hashes) goes into pre-release so it sorts LOWER - // than the clean version. - if let Some(extra_str) = extra { - let normalized: String = extra_str - .chars() - .map(|c| { - if c.is_alphanumeric() || c == '.' { - c - } else { - '.' - } - }) - .collect(); - if let Ok(pre) = semver::Prerelease::new(&normalized) { - ver.pre = pre; - } - } - - Some(ver) -} - -/// Extract a trailing -rN release revision from a version string. -/// Returns (base_without_rN, Some(N)) or (original, None). -fn extract_release_revision(v: &str) -> (&str, Option) { - if let Some(idx) = v.rfind("-r") { - let after = &v[idx + 2..]; - if let Ok(rev) = after.parse::() { - return (&v[..idx], Some(rev)); - } - } - (v, None) -} - -/// Compare two tags by their version, using semver-aware sorting. -fn version_compare_tags(a: &str, b: &str, arch: &str) -> Ordering { - let ver_a = extract_version_from_tag(a, arch); - let ver_b = extract_version_from_tag(b, arch); - - match (parse_version_lenient(ver_a), parse_version_lenient(ver_b)) { - (Some(va), Some(vb)) => va.cmp(&vb), - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => a.cmp(b), // fallback to lexicographic - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_filter_tags_by_arch() { - let tags = vec![ - "latest".to_string(), - "srcbuild-20241227".to_string(), - "v1.0.0-x86_64-linux".to_string(), - "v1.0.0-aarch64-linux".to_string(), - "v1.1.0-x86_64-linux".to_string(), - ]; - - let x86_tags = RegistryClient::filter_tags_by_arch(&tags, "x86_64-linux"); - assert_eq!(x86_tags.len(), 2); - - let arm_tags = RegistryClient::filter_tags_by_arch(&tags, "aarch64-linux"); - assert_eq!(arm_tags.len(), 1); - } - - #[test] - fn test_get_latest_arch_tag() { - let tags = vec![ - "v1.0.0-x86_64-linux".to_string(), - "v1.1.0-x86_64-linux".to_string(), - "v1.0.5-x86_64-linux".to_string(), - ]; - - let latest = RegistryClient::get_latest_arch_tag(&tags, "x86_64-linux"); - assert_eq!(latest, Some(&"v1.1.0-x86_64-linux".to_string())); - } - - #[test] - fn test_get_latest_calver() { - let tags = vec![ - "2026.1.8-x86_64-linux".to_string(), - "2026.1.12-x86_64-linux".to_string(), - "2026.2.9-x86_64-linux".to_string(), - "2026.2.17-x86_64-linux".to_string(), - "2026.2.23-x86_64-linux".to_string(), - ]; - - let latest = RegistryClient::get_latest_arch_tag(&tags, "x86_64-linux"); - assert_eq!(latest, Some(&"2026.2.23-x86_64-linux".to_string())); - } - - #[test] - fn test_get_latest_with_old_date_tags() { - let tags = vec![ - "v0.16.1-2026-01-01_1767256526-x86_64-linux".to_string(), - "0.16.1-v0.16.1-2026-01-22_1769071133-x86_64-linux".to_string(), - "0.16.1-x86_64-linux".to_string(), - "0.16.1-r1-x86_64-linux".to_string(), - "0.16.1-r2-x86_64-linux".to_string(), - ]; - - let latest = RegistryClient::get_latest_arch_tag(&tags, "x86_64-linux"); - assert_eq!(latest, Some(&"0.16.1-r2-x86_64-linux".to_string())); - } - - #[test] - fn test_get_latest_release_revisions() { - let tags = vec![ - "1.0.0-x86_64-linux".to_string(), - "1.0.0-r1-x86_64-linux".to_string(), - "1.0.0-r2-x86_64-linux".to_string(), - ]; - - let latest = RegistryClient::get_latest_arch_tag(&tags, "x86_64-linux"); - assert_eq!(latest, Some(&"1.0.0-r2-x86_64-linux".to_string())); - } - - #[test] - fn test_version_compare_v_prefix_vs_no_prefix() { - let tags = vec![ - "v0.16.1-x86_64-linux".to_string(), - "0.16.2-x86_64-linux".to_string(), - ]; - - let latest = RegistryClient::get_latest_arch_tag(&tags, "x86_64-linux"); - assert_eq!(latest, Some(&"0.16.2-x86_64-linux".to_string())); - } - - #[test] - fn test_two_component_version() { - let tags = vec![ - "1.5-x86_64-linux".to_string(), - "1.12-x86_64-linux".to_string(), - ]; - - let latest = RegistryClient::get_latest_arch_tag(&tags, "x86_64-linux"); - assert_eq!(latest, Some(&"1.12-x86_64-linux".to_string())); - } - - #[test] - fn test_download_url() { - let url = RegistryClient::get_download_url( - "pkgforge/bincache/bat", - "v0.24.0-x86_64-linux", - "bat", - ); - assert!(url.contains("api.ghcr.pkgforge.dev")); - assert!(url.contains("tag=v0.24.0-x86_64-linux")); - } -} diff --git a/sbuild/Cargo.toml b/sbuild/Cargo.toml deleted file mode 100644 index edbbb10..0000000 --- a/sbuild/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "sbuild" -version = "0.2.0" -license.workspace = true -edition.workspace = true -description.workspace = true -repository.workspace = true -keywords.workspace = true - -[dependencies] -blake3.workspace = true -chrono = { workspace = true, features = ["now"] } -clap.workspace = true -colored.workspace = true -env_logger.workspace = true -futures.workspace = true -glob.workspace = true -goblin.workspace = true -log.workspace = true -memmap2.workspace = true -onelf-format.workspace = true -reqwest = { workspace = true, features = ["blocking", "http2", "rustls", "stream"] } -sbuild-cache.workspace = true -sbuild-linter.workspace = true -sbuild-meta.workspace = true -serde.workspace = true -serde_json = { workspace = true, features = ["indexmap"] } -saphyr.workspace = true -sha2.workspace = true -squishy.workspace = true -tempfile.workspace = true -thiserror.workspace = true -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process", "time"] } -which.workspace = true -zstd.workspace = true diff --git a/sbuild/src/builder.rs b/sbuild/src/builder.rs deleted file mode 100644 index 8c02335..0000000 --- a/sbuild/src/builder.rs +++ /dev/null @@ -1,969 +0,0 @@ -use std::{ - collections::HashMap, - env::{ - self, - consts::{ARCH, OS}, - }, - fs, - io::{BufRead, BufReader}, - os::unix::fs::{symlink, PermissionsExt}, - path::{Path, PathBuf}, - process::{Child, Command, Stdio}, - sync::{self, Arc}, - thread, - time::Duration, -}; - -use sbuild_linter::{build_config::BuildConfig, logger::TaskLogger, BuildAsset, Linter}; -use squishy::appimage::{get_offset, AppImage, AppImageEntryKind, FilesystemType}; - -use crate::{ - cleanup::Finalize, - constant::{ - APPIMAGE_MAGIC_BYTES, ELF_MAGIC_BYTES, FLATIMAGE_MAGIC_BYTES, PNG_MAGIC_BYTES, - SVG_MAGIC_BYTES, XML_MAGIC_BYTES, - }, - onelf::OnelfPackage, - types::{OutputStream, PackageType, SoarEnv}, - utils::{ - calc_magic_bytes, download, expand_env_vars, is_onelf, is_static_elf, pack_appimage, - self_extract_appimage, temp_file, - }, -}; - -pub struct BuildContext { - pkg: String, - pkg_id: String, - pkg_type: Option, - sbuild_pkg: String, - outdir: PathBuf, - tmpdir: PathBuf, - remote_pkgver: String, - pkgver: String, - recipe_dir: Option, -} - -impl BuildContext { - fn new>( - build_config: &BuildConfig, - cache_path: P, - remote_pkgver: String, - pkgver: String, - outdir: Option, - recipe_dir: Option, - ) -> Self { - let sbuild_pkg = build_config - .pkg_type - .as_ref() - .map(|t| format!("{}.{}", build_config.pkg, t)) - .unwrap_or(build_config.pkg.clone()); - - let outdir = outdir - .map(|dir| { - let path = Path::new(&dir); - if path.is_absolute() { - // Always add pkg_id subdirectory - path.join(&build_config.pkg_id) - } else { - let current_dir = env::current_dir().expect("Failed to get current directory"); - current_dir.join(dir).join(&build_config.pkg_id) - } - }) - .unwrap_or_else(|| { - cache_path - .as_ref() - .join("sbuild") - .join(&build_config.pkg_id) - }); - let tmpdir = outdir.join("SBUILD_TEMP"); - - Self { - pkg: build_config.pkg.clone(), - pkg_id: build_config.pkg_id.clone(), - pkg_type: build_config.pkg_type.clone(), - sbuild_pkg, - outdir, - tmpdir, - remote_pkgver, - pkgver, - recipe_dir, - } - } - - fn env_vars(&self, soar_bin: &str) -> Vec<(String, String)> { - let paths = env::var("PATH").unwrap_or_default(); - - let inherit_keys = [ - "DEBIAN_FRONTEND", - "EGET_TIMEOUT", - "GIT_ASKPASS", - "GIT_TERMINAL_PROMPT", - "GITHUB_TOKEN", - "GH_TOKEN", - "GITLAB_TOKEN", - "GL_TOKEN", - "HF_TOKEN", - "HOST_TRIPLET", - "NIXPKGS_ALLOW_BROKEN", - "NIXPKGS_ALLOW_UNFREE", - "NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM", - "SYSTMP", - "TERM", - "USER_AGENT", - ]; - - let get_env_var = - |key: &str| -> (String, Option) { (key.to_string(), env::var(key).ok()) }; - - let existing_envs: Vec<(String, Option)> = - inherit_keys.iter().map(|key| get_env_var(key)).collect(); - - let arch = ARCH.to_string(); - let arch_alt = match ARCH { - "x86_64" => "amd64", - "aarch64" => "arm64", - _ => ARCH, - } - .to_string(); - - let paths = format!("{}:{}", soar_bin, paths); - let mut vars: Vec<(String, String)> = [ - ("pkg", self.pkg.clone()), - ("pkg_id", self.pkg_id.clone()), - ("pkg_type", self.pkg_type.clone().unwrap_or_default()), - ("sbuild_pkg", self.sbuild_pkg.clone()), - ("sbuild_pkgver", self.pkgver.clone()), - ("sbuild_outdir", self.outdir.to_string_lossy().to_string()), - ("sbuild_tmpdir", self.tmpdir.to_string_lossy().to_string()), - ("pkg_ver", self.remote_pkgver.clone()), - ("pkgver", self.pkgver.clone()), - ("remote_pkgver", self.remote_pkgver.clone()), - ("arch", arch), - ("arch_alt", arch_alt), - ] - .into_iter() - .flat_map(|(key, value)| { - let value = match key { - "sbuild_outdir" | "sbuild_tmpdir" => value, - _ => value.replace(|c: char| c.is_whitespace(), ""), - }; - vec![ - (key.to_string(), value.clone()), - (key.to_uppercase(), value), - ] - }) - .chain(std::iter::once(("PATH".to_string(), paths))) - .collect(); - - vars.extend( - existing_envs - .into_iter() - .filter_map(|(key, value)| value.map(|val| (key, val))), - ); - vars - } -} - -pub struct Builder { - logger: TaskLogger, - soar_env: SoarEnv, - external: bool, - desktop: HashMap, - icon: HashMap, - appstream: HashMap, - pkg_type: PackageType, - log_level: u8, - keep: bool, - timeout: Duration, -} - -impl Builder { - pub fn new( - logger: TaskLogger, - soar_env: SoarEnv, - external: bool, - log_level: u8, - keep: bool, - timeout: Duration, - ) -> Self { - Builder { - logger, - soar_env, - external, - desktop: HashMap::new(), - icon: HashMap::new(), - appstream: HashMap::new(), - pkg_type: PackageType::Unknown, - log_level, - keep, - timeout, - } - } - - pub async fn download_build_assets( - &mut self, - build_assets: &[BuildAsset], - context: &BuildContext, - ) { - let env_vars = context.env_vars(&self.soar_env.bin_path); - for asset in build_assets { - let url = expand_env_vars(&asset.url, &env_vars); - let out = expand_env_vars(&asset.out, &env_vars); - - self.logger - .info(format!("Downloading build asset from {}", url)); - - let out_path = out.to_string(); - if download(&url, &out_path).await.is_err() { - self.logger - .error(format!("Failed to download build asset from {}", url)); - std::process::exit(1); - }; - - let magic = calc_magic_bytes(&out_path, 4); - if magic == ELF_MAGIC_BYTES { - let perms = std::fs::Permissions::from_mode(0o755); - std::fs::set_permissions(&out_path, perms).unwrap(); - } - } - } - - fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), String> { - fs::create_dir_all(dst) - .map_err(|e| format!("Failed to create directory {}: {}", dst.display(), e))?; - for entry in fs::read_dir(src) - .map_err(|e| format!("Failed to read directory {}: {}", src.display(), e))? - { - let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; - let src_path = entry.path(); - let dst_path = dst.join(entry.file_name()); - if src_path.is_dir() { - Self::copy_dir_recursive(&src_path, &dst_path)?; - } else { - fs::copy(&src_path, &dst_path).map_err(|e| { - format!( - "Failed to copy {} to {}: {}", - src_path.display(), - dst_path.display(), - e - ) - })?; - } - } - Ok(()) - } - - fn prepare_resources( - &mut self, - _build_config: &BuildConfig, - context: &BuildContext, - ) -> Result<(), String> { - let recipe_dir = match &context.recipe_dir { - Some(dir) => dir, - None => return Ok(()), - }; - - let files_dir = recipe_dir.join("files"); - if !files_dir.is_dir() { - return Ok(()); - } - - self.logger - .info(format!("Copying recipe files from {}", files_dir.display())); - - for entry in fs::read_dir(&files_dir) - .map_err(|e| format!("Failed to read files directory: {}", e))? - { - let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; - let src_path = entry.path(); - let dst_path = context.outdir.join(entry.file_name()); - - if src_path.is_dir() { - Self::copy_dir_recursive(&src_path, &dst_path)?; - } else { - fs::copy(&src_path, &dst_path).map_err(|e| { - format!( - "Failed to copy {} to {}: {}", - src_path.display(), - dst_path.display(), - e - ) - })?; - } - } - - Ok(()) - } - - fn setup_output_handlers(&self) -> (sync::mpsc::Sender, thread::JoinHandle<()>) { - let (tx, rx) = sync::mpsc::channel(); - let logger = Arc::new(self.logger.clone()); - - let output_handle = thread::spawn(move || { - while let Ok(output) = rx.recv() { - match output { - OutputStream::Stdout(msg) => { - logger.info(&msg); - } - OutputStream::Stderr(msg) => { - logger.custom_error(&msg); - } - } - } - }); - - (tx, output_handle) - } - - fn setup_cmd_logging(&self, child: &mut Child) { - let stdout = child.stdout.take().unwrap(); - let stderr = child.stderr.take().unwrap(); - - let (tx, output_handle) = self.setup_output_handlers(); - let tx_stderr = tx.clone(); - - let stdout_handle = std::thread::spawn(move || { - let reader = BufReader::new(stdout); - reader.lines().for_each(|line| { - if let Ok(line) = line { - let _ = tx.send(OutputStream::Stdout(line)); - } - }) - }); - - let stderr_handle = std::thread::spawn(move || { - let reader = BufReader::new(stderr); - reader.lines().for_each(|line| { - if let Ok(line) = line { - let _ = tx_stderr.send(OutputStream::Stderr(line)); - } - }) - }); - - stdout_handle.join().unwrap(); - stderr_handle.join().unwrap(); - output_handle.join().unwrap(); - } - - pub async fn exec( - &mut self, - context: &BuildContext, - build_config: BuildConfig, - exec_file: Option, - ) -> bool { - env::set_current_dir(&context.outdir).unwrap(); - - fs::create_dir_all(&context.tmpdir).unwrap(); - - let is_container = build_config.x_exec.container.is_some(); - - if self.external && !is_container { - if let Some(build_utils) = build_config.build_util.clone() { - let mut child = Command::new("soar") - .env_clear() - .envs(context.env_vars(&self.soar_env.bin_path)) - .args(["add".to_string()].iter().chain(build_utils.iter())) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .unwrap(); - self.setup_cmd_logging(&mut child); - let status = child.wait().unwrap(); - if !status.success() { - self.logger.error("Failed to install build utils"); - return false; - } - }; - } - - if let Err(err) = self.prepare_resources(&build_config, context) { - self.logger.warn(&err); - } - - if let Some(ref build_assets) = build_config.build_asset { - self.download_build_assets(build_assets, context).await; - } - - if let Some(ref exec_file) = exec_file { - let mut child = if let Some(ref container) = build_config.x_exec.container { - let image = if container.contains(':') { - container.clone() - } else { - format!("{}:{}", container, ARCH) - }; - - let env_vars = context.env_vars(&self.soar_env.bin_path); - let mut cmd = Command::new("docker"); - cmd.args(["run", "--rm", "--privileged", "--net=host", "--pull=always"]); - - for (key, value) in &env_vars { - if key == "PATH" { - continue; - } - let val = match key.as_str() { - "sbuild_outdir" | "SBUILD_OUTDIR" => "/sbuild".to_string(), - "sbuild_tmpdir" | "SBUILD_TMPDIR" => "/sbuild/SBUILD_TEMP".to_string(), - _ => value.clone(), - }; - cmd.arg("-e").arg(format!("{}={}", key, val)); - } - - cmd.args(["-v", &format!("{}:/sbuild:rw", context.outdir.display())]); - cmd.args(["-v", &format!("{}:/exec_script:ro", exec_file)]); - cmd.args(["-w", "/sbuild"]); - cmd.arg(&image); - cmd.arg("/exec_script"); - - cmd.env_clear() - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .unwrap() - } else { - Command::new(exec_file) - .env_clear() - .envs(context.env_vars(&self.soar_env.bin_path)) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .unwrap() - }; - - self.setup_cmd_logging(&mut child); - - let timeout = self.timeout; - let child_pid = child.id(); - let (kill_tx, kill_rx) = sync::mpsc::channel::<()>(); - - let timeout_handle = thread::spawn(move || { - if kill_rx.recv_timeout(timeout).is_err() { - let _ = Command::new("kill") - .arg("-9") - .arg(child_pid.to_string()) - .output(); - } - }); - - let success = match child.wait() { - Ok(status) => { - let _ = kill_tx.send(()); - let _ = timeout_handle.join(); - status.success() - } - Err(e) => { - let _ = kill_tx.send(()); - let _ = timeout_handle.join(); - self.logger.error(format!("Build process error: {}", e)); - false - } - }; - - let _ = fs::remove_file(exec_file); - - if !success { - return false; - } - } - - if let Some(entrypoint) = build_config - .x_exec - .entrypoint - .as_ref() - .map(|e| e.trim_start_matches('/').to_string()) - { - let entry_path = Path::new(&entrypoint); - if entry_path.exists() { - symlink(entrypoint, build_config.pkg.clone()).unwrap(); - } else { - self.logger.error(format!( - "Entrypoint {} should exist in {} but doesn't.", - entrypoint, - context.outdir.display() - )); - return false; - } - } - - self.handle_provides(&context, &build_config); - - let mut finalize = Finalize::new( - &context.outdir, - build_config, - self.pkg_type.clone(), - self.keep, - ); - if let Err(e) = finalize.update().await { - self.logger - .error(format!("Failed to finalize build: {}", e)); - return false; - } - true - } - - pub async fn build( - &mut self, - file_path: &str, - outdir: Option, - timeout: Duration, - skip_existing: bool, - ) -> Option { - let logger = self.logger.clone(); - let linter = Linter::new(logger.clone(), timeout); - - let pwd = env::current_dir().unwrap(); - let mut result: Option = None; - - let validated_file = format!("{}.validated", file_path); - let version_file = format!("{}.pkgver", file_path); - - if let Some(build_config) = linter.lint(file_path, false, false, true) { - logger.info(format!("{}", fs::read_to_string(&validated_file).unwrap())); - if build_config._disabled { - logger.error(format!("{} -> Disabled package. Skipping...", file_path)); - } else { - let version = fs::read_to_string(&version_file).ok(); - - if version.is_none() { - return None; - } - - let version = version.unwrap(); - let x_exec = &build_config.x_exec; - let pkg_id = &build_config.pkg_id; - - let lines: Vec<&str> = version.lines().collect(); - let pkgver = lines[0].trim().to_string(); - let remote_pkgver = if lines.len() > 1 { - lines[1].trim().to_string() - } else { - pkgver.clone() - }; - - let recipe_dir = Path::new(file_path) - .canonicalize() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); - - let context = BuildContext::new( - &build_config, - &self.soar_env.cache_path, - remote_pkgver, - pkgver.to_string(), - outdir, - recipe_dir, - ); - - if skip_existing && context.outdir.exists() { - logger.warn(format!( - "Skipping build for {} (output directory already exists: {})", - file_path, - context.outdir.display() - )); - return Some(context.outdir); - } - - let _ = fs::remove_dir_all(&context.outdir); - fs::create_dir_all(&context.outdir).unwrap(); - let final_version_file = - format!("{}/{}.version", context.outdir.display(), context.pkg); - let final_validated_file = format!("{}/SBUILD", context.outdir.display()); - fs::copy(&version_file, &final_version_file).unwrap(); - fs::copy(&validated_file, &final_validated_file).unwrap(); - - let log_path = context.outdir.join("BUILD.log"); - logger.move_log_file(log_path).unwrap(); - - if let Some(ref arch) = x_exec.arch { - if !arch - .iter() - .any(|a| a.eq_ignore_ascii_case(ARCH.to_string().as_str())) - { - logger.error(format!("Unsupported architecture. Aborting...")); - return None; - } - } - - if let Some(ref arch) = x_exec.os { - if !arch - .iter() - .any(|o| o.eq_ignore_ascii_case(OS.to_string().as_str())) - { - logger.error(format!("Unsupported OS. Aborting...")); - return None; - } - } - - if let Some(ref host) = x_exec.host { - let current_host = format!("{ARCH}-{OS}"); - if !host - .iter() - .any(|h| h.eq_ignore_ascii_case(current_host.as_str())) - { - logger.error(format!("Unsupported HOST. Aborting...")); - return None; - } - } - - let exec_file = x_exec.run.as_ref().map(|run| { - let setx = match self.log_level { - 2 => "set -x", - 3 => "set -xv", - _ => "", - }; - // In a container the script runs as root, so subdirs the - // recipe creates under the mounted /sbuild (e.g. - // packages//) aren't writable by the host user, which - // blocks later host-side steps (icon/desktop extraction, - // signing). Relax permissions so the host user can write - // regardless of ownership. chmod (not chown) is used - // deliberately: under rootless docker/podman the container - // root maps to the host user via a subuid range, so - // chown-ing to a host uid would map it back out of reach; - // chmod works the same under both rootful and rootless - // runtimes. Preserve the script's real exit status. - let trailer = if build_config.x_exec.container.is_some() { - "\n__sbuild_rc=$?\nchmod -R a+rwX /sbuild 2>/dev/null || true\nexit $__sbuild_rc" - } else { - "" - }; - let script = - format!("#!/usr/bin/env {}\n{}\n{}{}", x_exec.shell, setx, run, trailer); - let tmp = temp_file(pkg_id, &script); - tmp.to_string_lossy().to_string() - }); - - let success = self.exec(&context, build_config, exec_file).await; - if success { - logger.success(format!( - "Successfully built the package at {}", - context.outdir.display() - )); - result = Some(context.outdir.clone()); - } else { - logger.success(&format!("Failed to build the package: {}", context.pkg)); - } - } - } - - env::set_current_dir(pwd).unwrap(); - - let _ = fs::remove_file(validated_file); - let _ = fs::remove_file(version_file); - result - } - - pub fn handle_provides(&mut self, context: &BuildContext, build_config: &BuildConfig) { - let pkg_name = &build_config.pkg; - let pkg_type = &build_config.pkg_type; - - // Collect all provides: from packages field or global provides - let provides = if let Some(ref packages) = build_config.packages { - packages - .iter() - .flat_map(|(pkg, config)| { - config - .provides - .iter() - .map(move |p| (Some(pkg.as_str()), p.clone())) - }) - .collect::>() - } else if build_config.x_exec.entrypoint.is_some() { - vec![(None, pkg_name.clone())] - } else { - build_config - .provides - .clone() - .unwrap_or_else(|| vec![pkg_name.clone()]) - .into_iter() - .map(|p| (None, p)) - .collect() - }; - - let mut exists_any = false; - - for (parent_pkg, provide) in provides { - let cmd = provide - .split_once(|c| c == ':' || c == '=') - .map(|(p1, _)| p1.to_string()) - .unwrap_or_else(|| provide.to_string()); - // Strip @ prefix for binary-only entries - let cmd = cmd.strip_prefix('@').unwrap_or(&cmd); - - // Check in packages// first, then root outdir - let provide_path = if let Some(parent) = parent_pkg { - let pkg_path = Path::new("packages").join(parent).join(cmd); - if pkg_path.exists() { - pkg_path - } else { - PathBuf::from(cmd) - } - } else { - PathBuf::from(cmd) - }; - - if !provide_path.exists() { - self.logger - .warn(format!("Provide '{}' does not exist.", provide)); - continue; - } - - exists_any = true; - - let magic_bytes = calc_magic_bytes(&provide_path, 12); - - if magic_bytes[8..] == APPIMAGE_MAGIC_BYTES { - // Determine filter based on current pkg_type or recipe pkg_type - let filter = match (self.pkg_type.clone(), pkg_type.as_deref()) { - (PackageType::NixAppImage, _) | (_, Some("nixappimage")) => { - self.pkg_type = PackageType::NixAppImage; - Some(pkg_name.as_str()) - } - (PackageType::Unknown, _) => { - self.pkg_type = PackageType::AppImage; - None - } - _ => None, - }; - - let offset = get_offset(&provide_path).unwrap(); - - let Ok(mut appimage) = AppImage::new(filter, &provide_path, None) else { - self.logger.warn(format!( - "Failed to read {} as AppImage. Trying self-extract approach.", - provide_path.display() - )); - - self_extract_appimage( - &cmd, - "*.desktop".to_string(), - &format!("{}.desktop", cmd), - ); - self_extract_appimage(&cmd, ".DirIcon".to_string(), ".DirIcon"); - - self.rename_icon(".DirIcon", context, &provide, &cmd); - - continue; - }; - - if appimage.filesystem_type() == FilesystemType::SquashFS - && !is_static_elf(&provide_path) - { - self.logger.info(format!( - "{} -> Dynamic SquashFS AppImage. Attempting to convert it to static.", - &provide_path.display() - )); - let tmp_path = "SBUILD_TEMP/squashfs_tmp/"; - let file_path = &provide_path.to_string_lossy().to_string(); - let env_vars = context.env_vars(&self.soar_env.bin_path); - - let Ok(usqfs) = which::which("unsquashfs") else { - self.logger - .warn("unsquashfs not found. Skipping conversion."); - continue; - }; - - let mut child = Command::new(usqfs) - .env_clear() - .envs(env_vars.clone()) - .args([ - "-offset", - &offset.to_string(), - "-force", - "-dest", - tmp_path, - file_path, - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .unwrap(); - - let _ = child.wait().unwrap(); - if !Path::new(tmp_path).exists() { - self.logger.warn("Failed to unpack appimage"); - } - if pack_appimage(env_vars, tmp_path, &file_path, &self.logger) { - self.logger.info(format!( - "{} -> Successfully converted to static AppImage.", - &provide_path.display() - )); - }; - } - if self.icon.get(&provide).is_none() { - if let Some(entry) = appimage.find_icon() { - if let AppImageEntryKind::File = entry.kind { - let dest = format!("{}.DirIcon", cmd); - let _ = appimage.write_entry(&entry, &dest); - self.logger.info(&format!( - "Extracted {} to {}", - entry.path.display(), - dest - )); - - self.rename_icon(dest, context, &provide, &cmd); - } - } - } - if self.desktop.get(&provide).is_none() { - if let Some(entry) = appimage.find_desktop() { - if let AppImageEntryKind::File = entry.kind { - let dest = format!("{}.desktop", cmd); - let _ = appimage.write_entry(&entry, &dest); - self.logger.info(&format!( - "Extracted {} to {}", - entry.path.display(), - dest - )); - self.desktop.insert(provide.clone(), true); - } - }; - } - if self.appstream.get(&provide).is_none() { - if let Some(entry) = appimage.find_appstream() { - if let AppImageEntryKind::File = entry.kind { - let file_name = if entry - .path - .file_name() - .unwrap() - .to_string_lossy() - .contains("appdata") - { - "appdata" - } else { - "metainfo" - }; - let dest = format!("{}.{}.xml", cmd, file_name); - let _ = appimage.write_entry(&entry, &dest); - self.logger.info(&format!( - "Extracted {} to {}", - entry.path.display(), - dest - )); - self.appstream.insert(provide.clone(), true); - } - }; - } - } else if magic_bytes[4..8] == FLATIMAGE_MAGIC_BYTES { - // Only auto-detect if pkg_type is not already set - if self.pkg_type == PackageType::Unknown { - self.pkg_type = PackageType::FlatImage; - } - } else if is_onelf(&provide_path) { - // onelf packs a directory into a self-extracting ELF, so its - // leading magic is ELF; it's identified by a trailing footer. - // Check before the plain-ELF branch so it isn't mistaken for a - // static binary. Only auto-detect if pkg_type is not already set. - if self.pkg_type == PackageType::Unknown { - self.pkg_type = PackageType::Onelf; - } - - // Write extracted assets next to the binary so sub-package - // provides (packages//) land in their own dir - // rather than the root outdir. - let dest_dir = provide_path.parent().unwrap_or_else(|| Path::new("")); - - match OnelfPackage::open(&provide_path) { - Ok(mut pkg) => { - if self.icon.get(&provide).is_none() { - let dest = dest_dir.join(format!("{}.DirIcon", cmd)); - match pkg.extract_icon(cmd, &dest) { - Ok(Some(())) => { - self.logger - .info(&format!("Extracted icon to {}", dest.display())); - self.rename_icon(dest, context, &provide, cmd); - } - Ok(None) => {} - Err(e) => self.logger.warn(format!( - "Failed to extract icon from {}: {}", - provide_path.display(), - e - )), - } - } - if self.desktop.get(&provide).is_none() { - let dest = dest_dir.join(format!("{}.desktop", cmd)); - match pkg.extract_desktop(cmd, &dest) { - Ok(Some(())) => { - self.logger - .info(&format!("Extracted desktop to {}", dest.display())); - self.desktop.insert(provide.clone(), true); - } - Ok(None) => {} - Err(e) => self.logger.warn(format!( - "Failed to extract desktop from {}: {}", - provide_path.display(), - e - )), - } - } - } - Err(e) => { - self.logger.warn(format!( - "Failed to read {} as onelf: {}", - provide_path.display(), - e - )); - } - } - } else if magic_bytes[..4] == ELF_MAGIC_BYTES { - // Only auto-detect if pkg_type is not already set - if self.pkg_type == PackageType::Unknown { - self.pkg_type = if is_static_elf(&provide_path) { - PackageType::Static - } else { - PackageType::Dynamic - }; - } - }; - } - - if !exists_any { - self.logger.error("None of the provides exist. Aborting."); - std::process::exit(1); - } - } - - fn rename_icon>( - &mut self, - file_path: P, - context: &BuildContext, - provide: &str, - cmd: &str, - ) { - let file_path = file_path.as_ref(); - if !file_path.exists() { - return; - } - let magic_bytes = calc_magic_bytes(&file_path, 8); - if let Some(extension) = if magic_bytes == PNG_MAGIC_BYTES { - Some("png") - } else if magic_bytes[..4] == SVG_MAGIC_BYTES || magic_bytes[..5] == XML_MAGIC_BYTES { - Some("svg") - } else { - None - } { - // Keep the renamed icon in the same directory as the source so - // sub-package icons don't get hoisted to the root outdir. - let dir = file_path.parent().unwrap_or_else(|| Path::new("")); - let final_path = dir.join(format!("{}.{}", cmd, extension)); - fs::rename(&file_path, &final_path).unwrap(); - self.logger.info(&format!( - "Renamed {} to {}", - file_path.display(), - final_path.display() - )); - self.icon.insert(provide.to_string(), true); - } else { - let tmp_path = context.tmpdir.join(&file_path); - fs::rename(&file_path, &tmp_path).unwrap(); - self.logger.warn(&format!( - "Unsupported icon. Moved to {}", - tmp_path.display() - )); - }; - } -} diff --git a/sbuild/src/checksum.rs b/sbuild/src/checksum.rs deleted file mode 100644 index 2e5fd34..0000000 --- a/sbuild/src/checksum.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Checksum generation utilities -//! -//! Provides functions to compute BLAKE3 and SHA256 checksums for files. - -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::Path; - -use blake3::Hasher as Blake3Hasher; -use sha2::{Digest, Sha256}; - -/// Compute BLAKE3 hash of a file -pub fn b3sum>(path: P) -> std::io::Result { - let file = File::open(path)?; - let mut reader = BufReader::new(file); - let mut hasher = Blake3Hasher::new(); - - let mut buffer = [0u8; 65536]; // 64KB buffer - loop { - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - - Ok(hasher.finalize().to_hex().to_string()) -} - -/// Compute SHA256 hash of a file -pub fn sha256sum>(path: P) -> std::io::Result { - let file = File::open(path)?; - let mut reader = BufReader::new(file); - let mut hasher = Sha256::new(); - - let mut buffer = [0u8; 65536]; // 64KB buffer - loop { - let bytes_read = reader.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - - let result = hasher.finalize(); - Ok(format!("{:x}", result)) -} - -/// Compute both BLAKE3 and SHA256 checksums -pub fn compute_checksums>(path: P) -> std::io::Result { - let path = path.as_ref(); - Ok(Checksums { - b3sum: b3sum(path)?, - sha256: sha256sum(path)?, - }) -} - -/// Container for file checksums -#[derive(Debug, Clone)] -pub struct Checksums { - pub b3sum: String, - pub sha256: String, -} - -impl Checksums { - /// Write checksums to files alongside the original file - pub fn write_to_files>(&self, base_path: P) -> std::io::Result<()> { - let base = base_path.as_ref(); - let filename = base.file_name().unwrap_or_default().to_string_lossy(); - - // Write b3sum file - let b3sum_path = base.with_extension(format!( - "{}.b3sum", - base.extension().unwrap_or_default().to_string_lossy() - )); - std::fs::write(&b3sum_path, format!("{} {}\n", self.b3sum, filename))?; - - // Write sha256sum file - let sha256_path = base.with_extension(format!( - "{}.sha256", - base.extension().unwrap_or_default().to_string_lossy() - )); - std::fs::write(&sha256_path, format!("{} {}\n", self.sha256, filename))?; - - Ok(()) - } -} - -/// Generate CHECKSUM file with all files in a directory -pub fn generate_checksum_file>(dir: P) -> std::io::Result { - let dir = dir.as_ref(); - let mut lines = Vec::new(); - - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_file() { - let filename = path.file_name().unwrap_or_default().to_string_lossy(); - // Skip existing checksum files and the CHECKSUM file itself - if filename.ends_with(".b3sum") - || filename.ends_with(".sha256") - || filename == "CHECKSUM" - { - continue; - } - - let b3 = b3sum(&path)?; - let sha = sha256sum(&path)?; - lines.push(format!("BLAKE3: {} {}", b3, filename)); - lines.push(format!("SHA256: {} {}", sha, filename)); - } - } - - lines.sort(); - let content = lines.join("\n"); - - // Write CHECKSUM file - let checksum_path = dir.join("CHECKSUM"); - std::fs::write(&checksum_path, &content)?; - - Ok(content) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::Write; - use tempfile::NamedTempFile; - - #[test] - fn test_b3sum() { - let mut file = NamedTempFile::new().unwrap(); - file.write_all(b"hello world").unwrap(); - file.flush().unwrap(); - - let hash = b3sum(file.path()).unwrap(); - // Known BLAKE3 hash of "hello world" - assert_eq!( - hash, - "d74981efa70a0c880b8d8c1985d075dbcbf679b99a5f9914e5aaf96b831a9e24" - ); - } - - #[test] - fn test_sha256sum() { - let mut file = NamedTempFile::new().unwrap(); - file.write_all(b"hello world").unwrap(); - file.flush().unwrap(); - - let hash = sha256sum(file.path()).unwrap(); - // Known SHA256 hash of "hello world" - assert_eq!( - hash, - "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" - ); - } -} diff --git a/sbuild/src/cleanup.rs b/sbuild/src/cleanup.rs deleted file mode 100644 index 3593299..0000000 --- a/sbuild/src/cleanup.rs +++ /dev/null @@ -1,158 +0,0 @@ -use std::{ - fs::{self, File}, - io::{self, Write}, - path::{Path, PathBuf}, -}; - -use sbuild_linter::build_config::BuildConfig; - -use crate::{ - constant::{MIN_DESKTOP_SIZE, MIN_ICON_SIZE, XML_MAGIC_BYTES}, - types::PackageType, - utils::{calc_magic_bytes, download}, -}; - -pub struct Finalize { - dir_path: PathBuf, - build_config: BuildConfig, - pkg_type: PackageType, - fallback_icon: Option, - keep: bool, -} - -impl Finalize { - pub fn new>( - dir_path: P, - build_config: BuildConfig, - pkg_type: PackageType, - keep: bool, - ) -> Self { - Self { - dir_path: dir_path.as_ref().to_path_buf(), - build_config, - pkg_type, - fallback_icon: None, - keep, - } - } - - pub async fn update(&mut self) -> std::io::Result<()> { - if !self.keep { - self.cleanup_temp()?; - } - self.validate_files().await?; - // Note: CHECKSUM generation is handled by post_build_processing in main.rs - Ok(()) - } - - async fn validate_files(&mut self) -> io::Result<()> { - // onelf is a portable single-binary format like static/dynamic: any - // icon/desktop is extracted from its bundled `.onelf/` metadata during - // the build. Don't fabricate fallback assets when none were bundled. - if matches!( - self.pkg_type, - PackageType::Static | PackageType::Dynamic | PackageType::Onelf - ) { - return Ok(()); - }; - let build_config = &self.build_config; - let pkg_name = &build_config.pkg; - - let provides = build_config.provides.clone(); - let provides = if build_config.x_exec.entrypoint.is_some() { - vec![pkg_name.clone()] - } else { - provides.unwrap_or_else(|| vec![pkg_name.clone()]) - }; - - for provide in provides { - let cmd = provide - .split_once(|c| c == ':' || c == '=') - .map(|(p1, _)| p1.to_string()) - .unwrap_or_else(|| provide.to_string()); - // Strip @ prefix for binary-only entries - let cmd = cmd.strip_prefix('@').unwrap_or(&cmd); - - self.validate_icon(&cmd).await?; - self.validate_appstream(&cmd)?; - self.validate_desktop(&cmd)?; - } - Ok(()) - } - - async fn validate_icon(&mut self, cmd: &str) -> io::Result<()> { - let png_path = self.dir_path.join(format!("{}.png", cmd)); - let svg_path = self.dir_path.join(format!("{}.svg", cmd)); - - let icon_valid = match (png_path.exists(), svg_path.exists()) { - (true, _) => self.check_file_size(&png_path, MIN_ICON_SIZE)?, - (_, true) => self.check_file_size(&svg_path, MIN_ICON_SIZE)?, - _ => false, - }; - - if !icon_valid { - if let Some(ref fallback_icon) = self.fallback_icon { - fs::copy(fallback_icon, png_path)?; - } else { - let url = "https://raw.githubusercontent.com/pkgforge/soarpkgs/main/assets/pkg.png"; - download(&url, &png_path).await.unwrap(); - self.fallback_icon = Some(png_path); - } - } - - Ok(()) - } - - fn validate_appstream(&self, cmd: &str) -> io::Result<()> { - let appdata_path = self.dir_path.join(format!("{}.appdata.xml", cmd)); - let metainfo_path = self.dir_path.join(format!("{}.metainfo.xml", cmd)); - - for path in [metainfo_path, appdata_path].iter() { - if path.exists() { - let magic_bytes = calc_magic_bytes(path, 5); - if magic_bytes == XML_MAGIC_BYTES { - return Ok(()); - } - } - } - - Ok(()) - } - - fn validate_desktop(&self, cmd: &str) -> io::Result<()> { - let desktop_path = self.dir_path.join(format!("{}.desktop", cmd)); - - if !desktop_path.exists() || !self.check_file_size(&desktop_path, MIN_DESKTOP_SIZE)? { - let desktop_content = self.generate_desktop_content(cmd); - let mut file = File::create(&desktop_path)?; - file.write_all(desktop_content.as_bytes())?; - } - - Ok(()) - } - - fn check_file_size(&self, path: &Path, min_size: u64) -> io::Result { - Ok(fs::metadata(path)?.len() >= min_size) - } - - fn cleanup_temp(&self) -> std::io::Result<()> { - let temp_dir = self.dir_path.join("SBUILD_TEMP"); - if temp_dir.exists() { - fs::remove_dir_all(temp_dir)?; - } - Ok(()) - } - - fn generate_desktop_content(&self, cmd: impl Into) -> String { - format!( - r#"[Desktop Entry] -Type=Application -Name={0} -Exec={0} -Icon={0} -Categories=Utility; -"#, - cmd.into() - ) - } -} diff --git a/sbuild/src/commands/build.rs b/sbuild/src/commands/build.rs deleted file mode 100644 index 61cd732..0000000 --- a/sbuild/src/commands/build.rs +++ /dev/null @@ -1,1100 +0,0 @@ -use std::{ - env, fs, - path::{Path, PathBuf}, - sync::{ - self, - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, - }, - thread, - time::{Duration, Instant}, -}; - -use clap::Parser; -use colored::Colorize; -use log::{error, info, warn, LevelFilter}; -use sbuild::{ - builder::Builder, - checksum, fetch_recipe, - ghcr::{sanitize_oci_tag, GhcrClient, PackageAnnotations}, - read_recipe_metadata, - signing::Signer, - types::SoarEnv, - update_json_metadata, -}; -use sbuild_linter::logger::{LogManager, LogMessage}; -use sbuild_meta::sanitize_oci_name; - -#[derive(Parser)] -#[command(about = "Build packages from SBUILD recipes")] -pub struct BuildArgs { - #[arg(required = true)] - pub recipes: Vec, - - #[arg(short, long)] - pub outdir: Option, - - #[arg(short, long)] - pub keep: bool, - - #[arg(long, default_value = "3600")] - pub timeout: u64, - - #[arg(long, default_value = "30")] - pub timeout_linter: u64, - - #[arg(long, value_enum, default_value = "info")] - pub log_level: LogLevel, - - #[arg(long)] - pub ci: bool, - - #[arg(long)] - pub force: bool, - - #[arg(long)] - pub skip_existing: bool, - - #[arg(long, env = "GITHUB_TOKEN")] - pub github_token: Option, - - #[arg(long, env = "GHCR_TOKEN")] - pub ghcr_token: Option, - - #[arg(long)] - pub ghcr_repo: Option, - - #[arg(long)] - pub push: bool, - - #[arg(long)] - pub dry_run: bool, - - #[arg(long)] - pub sign: bool, - - #[arg(long, env = "MINISIGN_KEY")] - pub minisign_key: Option, - - #[arg(long, env = "MINISIGN_PASSWORD")] - pub minisign_password: Option, - - #[arg(long, default_value = "true")] - pub checksums: bool, - - #[arg(long)] - pub cache: Option, -} - -#[derive(Debug, Clone, Copy, clap::ValueEnum, Default)] -pub enum LogLevel { - #[default] - Info, - Verbose, - Debug, -} - -impl From for u8 { - fn from(level: LogLevel) -> u8 { - match level { - LogLevel::Info => 1, - LogLevel::Verbose => 2, - LogLevel::Debug => 3, - } - } -} - -impl From for LevelFilter { - fn from(level: LogLevel) -> LevelFilter { - match level { - LogLevel::Debug | LogLevel::Verbose => LevelFilter::Debug, - LogLevel::Info => LevelFilter::Info, - } - } -} - -pub async fn run(args: BuildArgs, soar_env: Option) -> Result<(), String> { - init_logging(args.ci, args.log_level); - - println!( - "{} v{}", - "sbuild".bright_cyan().bold(), - env!("CARGO_PKG_VERSION") - ); - - let soar_env = soar_env.unwrap_or_default(); - - let now = Instant::now(); - let success = Arc::new(AtomicUsize::new(0)); - let fail = Arc::new(AtomicUsize::new(0)); - // A build can succeed while its GHCR push fails; track that separately so - // the build count stays accurate but the process still exits non-zero. - let post_build_failed = Arc::new(AtomicBool::new(false)); - - let (tx, rx) = sync::mpsc::channel(); - let log_manager = LogManager::new(tx.clone()); - - let logger_handle = thread::spawn(move || { - let check = "✔".bright_green().bold(); - let cross = "✗".bright_red().bold(); - let warning = "⚠".bright_yellow().bold(); - - while let Ok(log) = rx.recv() { - match log { - LogMessage::Info(msg) => println!("{}", msg), - LogMessage::Error(msg) => eprintln!("[{}] {}", cross, msg), - LogMessage::Warn(msg) => eprintln!("[{}] {}", warning, msg), - LogMessage::Success(msg) => println!("[{}] {}", check, msg), - LogMessage::CustomError(msg) => eprintln!("{}", msg), - LogMessage::Done => break, - } - } - }); - - for recipe_input in &args.recipes { - let (recipe_path, recipe_url) = - if recipe_input.starts_with("http://") || recipe_input.starts_with("https://") { - match fetch_recipe(recipe_input).await { - Ok(content) => { - let temp_path = - std::env::temp_dir().join(format!("sbuild-{}.yaml", uuid_simple())); - if let Err(e) = std::fs::write(&temp_path, &content) { - error!("Failed to write temp recipe: {}", e); - fail.fetch_add(1, Ordering::SeqCst); - continue; - } - ( - temp_path.to_string_lossy().to_string(), - Some(recipe_input.as_str()), - ) - } - Err(e) => { - error!("Failed to fetch recipe {}: {}", recipe_input, e); - fail.fetch_add(1, Ordering::SeqCst); - continue; - } - } - } else { - (recipe_input.clone(), None) - }; - - let named_temp_file = tempfile::Builder::new() - .prefix("sbuild-log-") - .rand_bytes(8) - .tempfile() - .expect("Failed to create temp file"); - let tmp_file_path = named_temp_file.path().to_path_buf(); - let logger = log_manager.create_logger(Some(tmp_file_path)); - - let now_time = chrono::Utc::now(); - logger.write_to_file(format!( - "sbuild v{} [{}]", - env!("CARGO_PKG_VERSION"), - now_time.format("%A, %B %d, %Y %H:%M:%S UTC") - )); - - let mut builder = Builder::new( - logger.clone(), - soar_env.clone(), - true, - args.log_level.into(), - args.keep, - Duration::from_secs(args.timeout), - ); - - info!("Building: {}", recipe_input); - - let outdir_str = args - .outdir - .as_ref() - .map(|p| p.to_string_lossy().to_string()); - - if let Some(build_outdir) = builder - .build( - &recipe_path, - outdir_str.clone(), - Duration::from_secs(args.timeout_linter), - args.skip_existing, - ) - .await - { - success.fetch_add(1, Ordering::SeqCst); - - if args.ci { - write_github_env("SBUILD_SUCCESSFUL", "YES"); - } - - let pkg_name = build_outdir - .file_name() - .and_then(|n| n.to_str()) - .map(|s| s.to_string()); - if let Err(e) = - post_build_processing(&build_outdir, &args, recipe_url, pkg_name.as_deref()).await - { - error!("Post-build processing failed: {}", e); - post_build_failed.store(true, Ordering::SeqCst); - if args.ci { - write_github_env("GHA_BUILD_FAILED", "YES"); - } - } - } else { - fail.fetch_add(1, Ordering::SeqCst); - - if args.ci { - write_github_env("SBUILD_SUCCESSFUL", "NO"); - write_github_env("GHA_BUILD_FAILED", "YES"); - } - } - } - - log_manager.done(); - logger_handle.join().unwrap(); - - println!(); - let success_count = success.load(Ordering::SeqCst); - let fail_count = fail.load(Ordering::SeqCst); - let total = success_count + fail_count; - - println!( - "[{}] {} of {} packages built successfully", - "+".bright_blue().bold(), - success_count, - total, - ); - - if fail_count > 0 { - println!( - "[{}] {} packages failed", - "-".bright_red().bold(), - fail_count - ); - } - - println!("[{}] Completed in {:.2?}", "⏱".bright_blue(), now.elapsed()); - - if args.ci { - write_github_output("success_count", &success_count.to_string()); - write_github_output("fail_count", &fail_count.to_string()); - } - - let push_failed = post_build_failed.load(Ordering::SeqCst); - if push_failed { - println!( - "[{}] Post-build processing (e.g. GHCR push) failed", - "-".bright_red().bold() - ); - } - - if fail_count > 0 || push_failed { - std::process::exit(1); - } - - Ok(()) -} - -fn init_logging(_ci_mode: bool, log_level: LogLevel) { - env_logger::Builder::new() - .filter_level(log_level.into()) - .format_target(false) - .format_timestamp(None) - .init(); -} - -fn write_github_env(key: &str, value: &str) { - if let Ok(env_file) = env::var("GITHUB_ENV") { - if let Ok(mut file) = std::fs::OpenOptions::new().append(true).open(&env_file) { - use std::io::Write; - writeln!(file, "{}={}", key, value).ok(); - } - } -} - -fn write_github_output(key: &str, value: &str) { - if let Ok(output_file) = env::var("GITHUB_OUTPUT") { - if let Ok(mut file) = std::fs::OpenOptions::new().append(true).open(&output_file) { - use std::io::Write; - writeln!(file, "{}={}", key, value).ok(); - } - } -} - -fn uuid_simple() -> String { - use std::time::{SystemTime, UNIX_EPOCH}; - let duration = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default(); - format!("{:x}{:x}", duration.as_secs(), duration.subsec_nanos()) -} - -/// The binary file name a `provides` entry refers to: the part before any -/// `:`, `==`, or `=>` separator, with a leading `@` stripped. For example -/// `x_gui==x` -> `x_gui`, `@x` -> `x`. -fn provide_binary_name(entry: &str) -> String { - let entry = entry.strip_prefix('@').unwrap_or(entry); - entry - .split("=>") - .next() - .unwrap_or(entry) - .split("==") - .next() - .unwrap_or(entry) - .split(':') - .next() - .unwrap_or(entry) - .to_string() -} - -fn sign_file(signer: &Signer, file_path: &Path) -> Option { - match signer.sign(file_path) { - Ok(_) => { - let sig_path = PathBuf::from(format!("{}.sig", file_path.display())); - if sig_path.exists() { - info!("Signed: {}", file_path.display()); - Some(sig_path) - } else { - warn!("Signature file not created for: {}", file_path.display()); - None - } - } - Err(e) => { - warn!("Failed to sign {}: {}", file_path.display(), e); - None - } - } -} - -async fn post_build_processing( - outdir: &Path, - cli: &BuildArgs, - recipe_url: Option<&str>, - pkg_name: Option<&str>, -) -> Result<(), String> { - use sbuild::parse_ghcr_path; - - if cli.checksums { - info!("Generating checksums..."); - match checksum::generate_checksum_file(outdir) { - Ok(_) => info!("Checksums generated"), - Err(e) => warn!("Failed to generate checksums: {}", e), - } - } - - let signer = if cli.sign { - if let Some(ref key) = cli.minisign_key { - if let Err(e) = Signer::check_minisign() { - return Err(format!("Signing failed: {}", e)); - } - - let s = if Path::new(key).exists() { - Signer::with_key_file(key) - } else { - Signer::with_key_data(key.clone()) - } - .with_password(cli.minisign_password.clone()); - - Some(s) - } else { - warn!("--sign specified but no --minisign-key provided"); - None - } - } else { - None - }; - - if cli.push { - if let (Some(ref token), Some(ref base_repo)) = (&cli.ghcr_token, &cli.ghcr_repo) { - if cli.dry_run { - info!("[DRY-RUN] Simulating GHCR push..."); - } else { - info!("Pushing to GHCR..."); - } - - if !cli.dry_run { - if let Err(e) = GhcrClient::check_oras() { - return Err(format!("GHCR push failed: {}", e)); - } - } - - let client = if !cli.dry_run { - let c = GhcrClient::new(token.clone()); - if let Err(e) = c.login() { - return Err(format!("GHCR login failed: {}", e)); - } - Some(c) - } else { - None - }; - - let (base_version, remote_version) = { - let version_content = std::fs::read_dir(outdir).ok().and_then(|entries| { - entries - .filter_map(|e| e.ok()) - .find(|e| { - e.path() - .extension() - .map(|ext| ext == "version") - .unwrap_or(false) - }) - .and_then(|e| std::fs::read_to_string(e.path()).ok()) - }); - - match version_content { - Some(content) => { - let lines: Vec<&str> = content.lines().collect(); - let base = lines.first().unwrap_or(&"").trim().to_string(); - let base = if base.is_empty() { - "latest".to_string() - } else { - base - }; - let remote = if lines.len() > 1 { - let r = lines[1].trim().to_string(); - if r.is_empty() || r == base { - None - } else { - Some(r) - } - } else { - None - }; - (base, remote) - } - None => ("latest".to_string(), None), - } - }; - - let arch = format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS); - - // Fetch revision and snapshots from cache - let (version, revision, db_snapshots) = { - let meta = read_recipe_metadata(outdir); - let cache_pkg_id = meta - .as_ref() - .map(|m| m.pkg_id.as_str()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| pkg_name.unwrap_or("unknown")); - let host = arch.to_lowercase(); - - let (rev_result, snapshots_result) = - if let Ok(uri) = std::env::var("SBUILD_CACHE_URI") { - if !uri.is_empty() { - match sbuild_cache::MongoDatabase::connect(&uri).await { - Ok(mongo_db) => { - let rev = mongo_db - .get_revision( - cache_pkg_id, - &host, - &base_version, - remote_version.as_deref(), - None, - ) - .await - .ok(); - let snapshots = - mongo_db.get_snapshots(cache_pkg_id, &host).await.ok(); - (rev, snapshots) - } - Err(e) => { - warn!("Failed to connect to MongoDB cache: {}", e); - (None, None) - } - } - } else { - (None, None) - } - } else if let Some(ref cache_path) = cli.cache { - match sbuild_cache::CacheDatabase::open(cache_path) { - Ok(cache_db) => { - let rev = cache_db - .get_revision( - cache_pkg_id, - &host, - &base_version, - remote_version.as_deref(), - None, - ) - .ok(); - let snapshots = cache_db.get_snapshots(cache_pkg_id, &host).ok(); - (rev, snapshots) - } - Err(e) => { - warn!("Failed to open build cache: {}", e); - (None, None) - } - } - } else { - (None, None) - }; - - let versioned = match rev_result { - Some(rev) if rev > 0 => { - let versioned = format!("{}-r{}", base_version, rev); - info!( - "Revision {}: version {} -> {}", - rev, base_version, versioned - ); - (versioned, rev) - } - _ => (base_version.clone(), 0), - }; - - (versioned.0, versioned.1, snapshots_result) - }; - - let tag = format!("{}-{}", sanitize_oci_tag(&version), arch.to_lowercase()); - - let (pkg_family, recipe_name) = - recipe_url.and_then(parse_ghcr_path).unwrap_or_else(|| { - let default = pkg_name.unwrap_or("unknown").to_string(); - (default.clone(), default) - }); - - let metadata = read_recipe_metadata(outdir); - - let mut push_success = true; - let mut pushed_urls = Vec::new(); - - let packages_dir = outdir.join("packages"); - - if packages_dir.is_dir() { - info!("Found packages/ directory, using explicit package structure"); - - let shared_files: Vec = std::fs::read_dir(outdir) - .map_err(|e| format!("Failed to read output directory: {}", e))? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| p.is_file()) - .collect(); - - // Use recipe packages field if available, otherwise discover from directories - let package_names: Vec = if let Some(ref meta) = metadata { - if meta.has_packages() { - meta.get_provided_packages() - } else { - std::fs::read_dir(&packages_dir) - .map_err(|e| format!("Failed to read packages directory: {}", e))? - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_dir()) - .filter_map(|e| e.path().file_name()?.to_str().map(|s| s.to_string())) - .collect() - } - } else { - std::fs::read_dir(&packages_dir) - .map_err(|e| format!("Failed to read packages directory: {}", e))? - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_dir()) - .filter_map(|e| e.path().file_name()?.to_str().map(|s| s.to_string())) - .collect() - }; - - if package_names.is_empty() { - warn!("packages/ directory is empty"); - return Ok(()); - } - - for pkg_name_dir in &package_names { - let pkg_dir = packages_dir.join(pkg_name_dir); - - let sanitized_pkg_name = sanitize_oci_name(pkg_name_dir); - let owner = base_repo.split('/').next().unwrap_or(base_repo); - let full_repo = if let Some(ref custom_base) = - metadata.as_ref().and_then(|m| m.ghcr_pkg.as_ref()) - { - format!("{}/{}/{}", owner, custom_base, sanitized_pkg_name) - } else { - format!( - "{}/{}/{}/{}", - base_repo, pkg_family, recipe_name, sanitized_pkg_name - ) - }; - info!("Pushing package {} to {}", pkg_name_dir, full_repo); - - // Collect files from the package directory - let mut files_to_push: Vec = if pkg_dir.is_dir() { - std::fs::read_dir(&pkg_dir) - .map_err(|e| format!("Failed to read package directory: {}", e))? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| p.is_file()) - .collect() - } else { - Vec::new() - }; - - // Also check for binaries in the root outdir (per-package provides) - if let Some(provides) = metadata - .as_ref() - .and_then(|m| m.get_package_provides(pkg_name_dir)) - { - for provide in provides { - let root_path = outdir.join(provide); - if root_path.exists() - && root_path.is_file() - && !files_to_push.contains(&root_path) - { - files_to_push.push(root_path); - } - } - } - - files_to_push.extend(shared_files.clone()); - - // The main binary (used for checksum/size annotations) is - // the package's first non-@ provide: '@'-prefixed entries - // are extra bundled binaries, not the primary one. The - // binary file is the part before any :/==/=> separator, so - // it need not be named after the package. Fall back to the - // package name when there are no provides to go on. - let main_binary_name = metadata - .as_ref() - .and_then(|m| m.get_package_provides(pkg_name_dir)) - .and_then(|provides| { - provides - .iter() - .find(|p| !p.starts_with('@')) - .map(|p| provide_binary_name(p)) - }); - let main_binary = [main_binary_name.as_deref(), Some(pkg_name_dir.as_str())] - .into_iter() - .flatten() - .find_map(|name| { - files_to_push - .iter() - .find(|f| { - f.file_name().and_then(|n| n.to_str()) == Some(name) - }) - .cloned() - }); - - // All non-shared files in the package are binaries to sign - let binaries_to_sign: Vec = files_to_push - .iter() - .filter(|f| !shared_files.contains(f)) - .cloned() - .collect(); - - if let Some(ref s) = signer { - for binary_path in &binaries_to_sign { - if let Some(sig_path) = sign_file(s, binary_path) { - files_to_push.push(sig_path); - } - } - } - - let (bsum, shasum, binary_size) = if let Some(ref binary_path) = main_binary { - let size = fs::metadata(binary_path).ok().map(|m| m.len()); - ( - checksum::b3sum(binary_path).ok(), - checksum::sha256sum(binary_path).ok(), - size, - ) - } else { - (None, None, None) - }; - - let checksum_bsum = { - let checksum_path = pkg_dir.join("CHECKSUM"); - if checksum_path.exists() { - checksum::b3sum(&checksum_path).ok() - } else { - None - } - }; - - let ghcr_total_size: u64 = files_to_push - .iter() - .filter_map(|f| fs::metadata(f).ok().map(|m| m.len())) - .sum(); - - let pkg_provides = metadata - .as_ref() - .and_then(|m| m.get_package_provides(pkg_name_dir)); - - let json_path = pkg_dir.join(format!("{}.json", pkg_name_dir)); - if json_path.exists() { - if let Err(e) = update_json_metadata( - &json_path, - pkg_name_dir, - pkg_name_dir, - &full_repo, - &tag, - bsum.as_deref(), - shasum.as_deref(), - checksum_bsum.as_deref(), - binary_size, - Some(ghcr_total_size), - pkg_provides, - db_snapshots.as_deref(), - ) { - warn!("Failed to update JSON metadata: {}", e); - } - } - - let annotations = PackageAnnotations { - pkg: pkg_name_dir.to_string(), - pkg_id: metadata - .as_ref() - .map(|m| m.pkg_id.clone()) - .unwrap_or_default(), - pkg_type: metadata.as_ref().and_then(|m| m.pkg_type.clone()), - version: version.clone(), - description: metadata - .as_ref() - .map(|m| m.description.clone()) - .filter(|s| !s.is_empty()), - homepage: metadata.as_ref().and_then(|m| m.homepage.first().cloned()), - license: metadata - .as_ref() - .map(|m| m.license.join(", ")) - .filter(|s| !s.is_empty()), - build_date: chrono::Utc::now().to_rfc3339(), - build_id: env::var("GITHUB_RUN_ID").ok(), - build_gha: env::var("GITHUB_RUN_ID").ok().map(|id| { - format!( - "https://github.com/{}/actions/runs/{}", - env::var("GITHUB_REPOSITORY").unwrap_or_default(), - id - ) - }), - build_script: recipe_url.map(|s| s.to_string()), - bsum, - shasum, - checksum_bsum, - }; - - if cli.dry_run { - let target = format!("ghcr.io/{}:{}", full_repo, tag); - info!( - "[DRY-RUN] Would push {} files to {}", - files_to_push.len(), - target - ); - for f in &files_to_push { - let name = f.file_name().and_then(|n| n.to_str()).unwrap_or("?"); - let size = fs::metadata(f).ok().map(|m| m.len()).unwrap_or(0); - info!(" - {} ({} bytes)", name, size); - } - pushed_urls.push(target); - } else { - match client.as_ref().unwrap().push( - &files_to_push, - &full_repo, - &tag, - &annotations, - ) { - Ok(target) => { - info!("Pushed {} to {}", pkg_name_dir, target); - pushed_urls.push(target); - } - Err(e) => { - error!("Failed to push {}: {}", pkg_name_dir, e); - push_success = false; - } - } - } - } - } else { - let all_files: Vec = std::fs::read_dir(outdir) - .map_err(|e| format!("Failed to read output directory: {}", e))? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .filter(|p| p.is_file() && !p.is_symlink()) - .collect(); - - if all_files.is_empty() { - warn!("No files found to push"); - return Ok(()); - } - - let default_pkg_name = pkg_name.unwrap_or(&pkg_family).to_string(); - let packages_to_push: Vec = metadata - .as_ref() - .map(|m| m.get_provided_packages()) - .unwrap_or_else(|| vec![default_pkg_name.clone()]); - - if packages_to_push.len() > 1 - || packages_to_push - .first() - .map_or(true, |p| p != &default_pkg_name) - { - info!("Using packages from provides: {:?}", packages_to_push); - } - - for pkg_name_item in &packages_to_push { - let sanitized_pkg_name = sanitize_oci_name(pkg_name_item); - let owner = base_repo.split('/').next().unwrap_or(base_repo); - let full_repo = if let Some(ref custom_base) = - metadata.as_ref().and_then(|m| m.ghcr_pkg.as_ref()) - { - format!("{}/{}/{}", owner, custom_base, sanitized_pkg_name) - } else { - format!( - "{}/{}/{}/{}", - base_repo, pkg_family, recipe_name, sanitized_pkg_name - ) - }; - info!("Pushing {} to {}", pkg_name_item, full_repo); - - let pkg_provides: Vec = metadata - .as_ref() - .and_then(|m| m.get_package_provides(pkg_name_item)) - .map(|p| p.to_vec()) - .unwrap_or_else(|| vec![pkg_name_item.clone()]); - - let main_binary_name = pkg_provides - .iter() - .find(|p| !p.starts_with('@')) - .map(|p| { - p.split("=>") - .next() - .unwrap_or(p) - .split("==") - .next() - .unwrap_or(p) - .split(':') - .next() - .unwrap_or(p) - .to_string() - }) - .unwrap_or_else(|| pkg_name_item.clone()); - - let main_binary_path = all_files - .iter() - .find(|p| p.file_name().and_then(|n| n.to_str()) == Some(&main_binary_name)) - .cloned(); - - let mut files_to_push = all_files.clone(); - - let mut binaries_to_sign: Vec = Vec::new(); - if let Some(ref bin_path) = main_binary_path { - binaries_to_sign.push(bin_path.clone()); - } - - for provide in &pkg_provides { - if provide.starts_with('@') { - let bin_name = provide.strip_prefix('@').unwrap(); - let bin_path = outdir.join(bin_name); - if bin_path.exists() && !binaries_to_sign.contains(&bin_path) { - binaries_to_sign.push(bin_path); - } - } - } - - if let Some(ref s) = signer { - for bin_path in &binaries_to_sign { - if let Some(sig_path) = sign_file(s, bin_path) { - files_to_push.push(sig_path); - } - } - } - - let (bsum, shasum, binary_size) = if let Some(ref bin_path) = main_binary_path { - ( - checksum::b3sum(bin_path).ok(), - checksum::sha256sum(bin_path).ok(), - fs::metadata(bin_path).ok().map(|m| m.len()), - ) - } else { - (None, None, None) - }; - - let checksum_bsum = { - let checksum_path = outdir.join("CHECKSUM"); - if checksum_path.exists() { - checksum::b3sum(&checksum_path).ok() - } else { - None - } - }; - - let ghcr_total_size: u64 = files_to_push - .iter() - .filter_map(|f| fs::metadata(f).ok().map(|m| m.len())) - .sum(); - - let json_path = outdir.join(format!("{}.json", main_binary_name)); - let meta_pkg_name = metadata - .as_ref() - .map(|m| m.pkg.as_str()) - .unwrap_or(pkg_name_item); - if json_path.exists() { - if let Err(e) = update_json_metadata( - &json_path, - meta_pkg_name, - &main_binary_name, - &full_repo, - &tag, - bsum.as_deref(), - shasum.as_deref(), - checksum_bsum.as_deref(), - binary_size, - Some(ghcr_total_size), - Some(&pkg_provides), - db_snapshots.as_deref(), - ) { - warn!("Failed to update JSON metadata: {}", e); - } - } - - let annotations = PackageAnnotations { - pkg: main_binary_name.clone(), - pkg_id: metadata - .as_ref() - .map(|m| m.pkg_id.clone()) - .unwrap_or_default(), - pkg_type: metadata.as_ref().and_then(|m| m.pkg_type.clone()), - version: version.clone(), - description: metadata - .as_ref() - .map(|m| m.description.clone()) - .filter(|s| !s.is_empty()), - homepage: metadata.as_ref().and_then(|m| m.homepage.first().cloned()), - license: metadata - .as_ref() - .map(|m| m.license.join(", ")) - .filter(|s| !s.is_empty()), - build_date: chrono::Utc::now().to_rfc3339(), - build_id: env::var("GITHUB_RUN_ID").ok(), - build_gha: env::var("GITHUB_RUN_ID").ok().map(|id| { - format!( - "https://github.com/{}/actions/runs/{}", - env::var("GITHUB_REPOSITORY").unwrap_or_default(), - id - ) - }), - build_script: recipe_url.map(|s| s.to_string()), - bsum, - shasum, - checksum_bsum, - }; - - if cli.dry_run { - let target = format!("ghcr.io/{}:{}", full_repo, tag); - info!( - "[DRY-RUN] Would push {} files to {}", - files_to_push.len(), - target - ); - for f in &files_to_push { - let name = f.file_name().and_then(|n| n.to_str()).unwrap_or("?"); - let size = fs::metadata(f).ok().map(|m| m.len()).unwrap_or(0); - info!(" - {} ({} bytes)", name, size); - } - pushed_urls.push(target); - } else { - match client.as_ref().unwrap().push( - &files_to_push, - &full_repo, - &tag, - &annotations, - ) { - Ok(target) => { - info!("Pushed {} to {}", pkg_name_item, target); - pushed_urls.push(target); - } - Err(e) => { - error!("Failed to push {}: {}", pkg_name_item, e); - push_success = false; - } - } - } - } - } - - if cli.ci { - if push_success && !pushed_urls.is_empty() { - write_github_env("GHCRPKG_URL", &pushed_urls.join(",")); - write_github_env("PUSH_SUCCESSFUL", "YES"); - } else { - write_github_env("PUSH_SUCCESSFUL", "NO"); - } - } - - if !cli.dry_run && push_success && !pushed_urls.is_empty() { - let meta = read_recipe_metadata(outdir); - let cache_pkg_id = meta - .as_ref() - .map(|m| m.pkg_id.as_str()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| pkg_name.unwrap_or("unknown")); - let host = arch.to_lowercase(); - let build_id = env::var("GITHUB_RUN_ID").ok(); - let cache_pkg_name = meta - .as_ref() - .map(|m| m.pkg.as_str()) - .filter(|s| !s.is_empty()) - .unwrap_or(cache_pkg_id); - let build_log_url = env::var("GITHUB_RUN_ID").ok().map(|id| { - format!( - "https://github.com/{}/actions/runs/{}", - env::var("GITHUB_REPOSITORY").unwrap_or_default(), - id - ) - }); - - if let Ok(uri) = std::env::var("SBUILD_CACHE_URI") { - if !uri.is_empty() { - match sbuild_cache::MongoDatabase::connect(&uri).await { - Ok(mongo_db) => { - let _ = mongo_db - .get_or_create_package(cache_pkg_id, cache_pkg_name, &host) - .await; - if let Err(e) = mongo_db - .update_build_result( - cache_pkg_id, - &host, - &version, - sbuild_cache::BuildStatus::Success, - build_id.as_deref(), - Some(&tag), - None, - Some(&base_version), - remote_version.as_deref(), - revision, - None, - None, - build_log_url.as_deref(), - ) - .await - { - warn!("Failed to update MongoDB cache: {}", e); - } else { - info!("Updated MongoDB cache for {} on {}", cache_pkg_id, host); - } - } - Err(e) => warn!("Failed to connect to MongoDB cache: {}", e), - } - } - } else if let Some(ref cache_path) = cli.cache { - if let Ok(cache_db) = sbuild_cache::CacheDatabase::open(cache_path) { - let _ = cache_db.get_or_create_package(cache_pkg_id, cache_pkg_name, &host); - if let Err(e) = cache_db.update_build_result( - cache_pkg_id, - &host, - &version, - sbuild_cache::BuildStatus::Success, - build_id.as_deref(), - Some(&tag), - None, - Some(&base_version), - remote_version.as_deref(), - revision, - ) { - warn!("Failed to update build cache: {}", e); - } else { - info!("Updated build cache for {} on {}", cache_pkg_id, host); - } - } - } - } - - if !push_success { - return Err("One or more GHCR pushes failed".to_string()); - } - } else { - warn!("--push specified but --ghcr-token or --ghcr-repo not provided"); - } - } - - Ok(()) -} diff --git a/sbuild/src/commands/cache.rs b/sbuild/src/commands/cache.rs deleted file mode 100644 index b502ca0..0000000 --- a/sbuild/src/commands/cache.rs +++ /dev/null @@ -1,871 +0,0 @@ -use clap::{Parser, Subcommand, ValueEnum}; -use std::path::PathBuf; - -use sbuild_cache::{BuildStatus, CacheDatabase, MongoDatabase, Result}; - -#[derive(Parser)] -#[command(about = "Build cache management for SBUILD packages")] -pub struct CacheArgs { - #[arg(short, long, default_value = "build_cache.sdb")] - cache: PathBuf, - - #[command(subcommand)] - command: CacheCommands, -} - -#[derive(Clone, ValueEnum)] -enum StatusFilter { - Success, - Failed, - Pending, - Skipped, - Outdated, - All, -} - -#[derive(Clone, ValueEnum)] -enum ReportFormat { - Markdown, - Html, - Json, -} - -#[derive(Subcommand)] -enum CacheCommands { - Init, - - Update { - #[arg(short, long)] - package: String, - - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(short, long)] - version: String, - - #[arg(short, long)] - status: String, - - #[arg(short, long)] - build_id: Option, - - #[arg(short, long)] - tag: Option, - - #[arg(long)] - hash: Option, - }, - - MarkOutdated { - #[arg(short, long)] - package: String, - - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(short, long)] - upstream_version: String, - }, - - Stats { - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(long)] - json: bool, - }, - - List { - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(short, long, value_enum, default_value = "all")] - status: StatusFilter, - - #[arg(long)] - json: bool, - - #[arg(short, long)] - limit: Option, - }, - - NeedsRebuild { - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(long)] - json: bool, - }, - - Report { - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(short, long, value_enum, default_value = "markdown")] - format: ReportFormat, - - #[arg(short, long)] - output: Option, - - #[arg(long, default_value = "20")] - history_limit: i64, - }, - - Recent { - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(short, long, default_value = "20")] - limit: i64, - - #[arg(long)] - json: bool, - }, - - Prune { - #[arg(short, long, default_value = "10")] - keep: i64, - }, - - Get { - #[arg(short, long)] - package: String, - - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(long)] - json: bool, - }, - - GhSummary { - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(short, long, default_value = "Build Status")] - title: String, - }, - - /// Export MongoDB cache to SQLite file - Export { - #[arg(short, long, default_value = "build_cache.sdb")] - output: PathBuf, - }, - - /// List snapshots for a package - Snapshots { - #[arg(short, long)] - package: String, - - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - - #[arg(long)] - json: bool, - }, - - /// Migrate snapshots from recipe files to MongoDB - MigrateSnapshots { - /// Recipe file path (YAML) - #[arg(short, long)] - recipe: PathBuf, - - /// Package ID (optional, will be extracted from recipe if not provided) - #[arg(short, long)] - package: Option, - - #[arg(short = 'H', long, default_value = "x86_64-linux")] - host: String, - }, -} - -/// Get the cache URI from environment variable -fn get_cache_uri() -> Option { - std::env::var("SBUILD_CACHE_URI") - .ok() - .filter(|s| !s.is_empty()) -} - -pub async fn run(args: CacheArgs) -> Result<()> { - match args.command { - CacheCommands::Init => { - if let Some(uri) = get_cache_uri() { - let _db = MongoDatabase::connect(&uri).await?; - println!("Connected to MongoDB cache"); - } else { - let db = CacheDatabase::open(&args.cache)?; - println!("Initialized cache database at {:?}", args.cache); - let stats = db.get_stats("x86_64-linux")?; - println!("Total packages: {}", stats.total_packages); - } - Ok(()) - } - CacheCommands::Update { - package, - host, - version, - status, - build_id, - tag, - hash, - } => { - let build_status = BuildStatus::from_str(&status) - .ok_or_else(|| sbuild_cache::Error::InvalidStatus(status.clone()))?; - let pkg_name = package.rsplit('.').next().unwrap_or(&package); - - if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - db.get_or_create_package(&package, pkg_name, &host).await?; - db.update_build_result( - &package, - &host, - &version, - build_status, - build_id.as_deref(), - tag.as_deref(), - hash.as_deref(), - None, - None, - 0, - None, - None, - None, - ) - .await?; - } else { - let db = CacheDatabase::open(&args.cache)?; - db.get_or_create_package(&package, pkg_name, &host)?; - db.update_build_result( - &package, - &host, - &version, - build_status, - build_id.as_deref(), - tag.as_deref(), - hash.as_deref(), - None, - None, - 0, - )?; - if build_status == BuildStatus::Success { - db.clear_failure(&package, &host)?; - } - } - - println!( - "Updated {} on {} to version {} ({})", - package, host, version, status - ); - Ok(()) - } - CacheCommands::MarkOutdated { - package, - host, - upstream_version, - } => { - if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - db.mark_outdated(&package, &host, &upstream_version).await?; - } else { - let db = CacheDatabase::open(&args.cache)?; - db.mark_outdated(&package, &host, &upstream_version)?; - } - println!( - "Marked {} as outdated (upstream: {})", - package, upstream_version - ); - Ok(()) - } - CacheCommands::Stats { host, json } => { - let stats = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - db.get_stats(&host).await? - } else { - let db = CacheDatabase::open(&args.cache)?; - db.get_stats(&host)? - }; - - if json { - println!("{}", serde_json::to_string_pretty(&stats)?); - } else { - println!("Build Statistics for {}", host); - println!("========================"); - println!("Total packages: {}", stats.total_packages); - println!("Successful: {}", stats.successful); - println!("Failed: {}", stats.failed); - println!("Pending: {}", stats.pending); - println!("Outdated: {}", stats.outdated); - } - Ok(()) - } - CacheCommands::NeedsRebuild { host, json } => { - let packages = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - db.get_packages_needing_rebuild(&host).await? - } else { - let db = CacheDatabase::open(&args.cache)?; - db.get_packages_needing_rebuild(&host)? - }; - - if json { - println!("{}", serde_json::to_string_pretty(&packages)?); - } else { - println!("Packages needing rebuild on {}:", host); - println!(); - for pkg in &packages { - let status = pkg - .last_build_status - .map(|s| s.to_string()) - .unwrap_or_else(|| "never built".to_string()); - let version = pkg.current_version.as_deref().unwrap_or("unknown"); - println!(" {} (v{}) - {}", pkg.pkg_name, version, status); - if pkg.is_outdated { - if let Some(ref upstream) = pkg.upstream_version { - println!(" -> upstream: {}", upstream); - } - } - } - println!(); - println!("Total: {} packages", packages.len()); - } - Ok(()) - } - CacheCommands::Prune { keep } => { - // Prune only applies to SQLite (MongoDB handles this via $slice) - let db = CacheDatabase::open(&args.cache)?; - let deleted = db.prune_history(keep)?; - println!("Pruned {} old build history entries", deleted); - Ok(()) - } - CacheCommands::Get { - package, - host, - json, - } => { - let packages = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - if let Some(pkg) = db.get_package(&package, &host).await? { - vec![pkg] - } else { - db.find_packages_by_name(&package, &host).await? - } - } else { - let db = CacheDatabase::open(&args.cache)?; - if let Some(pkg) = db.get_package(&package, &host)? { - vec![pkg] - } else { - db.find_packages_by_name(&package, &host)? - } - }; - - if packages.is_empty() { - eprintln!("Package not found: {} on {}", package, host); - std::process::exit(1); - } - - if json { - if packages.len() == 1 { - println!("{}", serde_json::to_string_pretty(&packages[0])?); - } else { - println!("{}", serde_json::to_string_pretty(&packages)?); - } - } else { - for (i, pkg) in packages.iter().enumerate() { - if i > 0 { - println!("{}", "-".repeat(40)); - } - println!("Package: {}", pkg.pkg_name); - println!("ID: {}", pkg.pkg_id); - println!("Host: {}", pkg.host_triplet); - println!( - "Version: {}", - pkg.current_version.as_deref().unwrap_or("unknown") - ); - if let Some(ref base) = pkg.base_version { - println!("Base Version: {}", base); - println!("Revision: {}", pkg.revision); - } - println!( - "Status: {}", - pkg.last_build_status - .map(|s| s.to_string()) - .unwrap_or_else(|| "never built".to_string()) - ); - println!("Outdated: {}", pkg.is_outdated); - if let Some(ref hash) = pkg.recipe_hash { - println!("Recipe Hash: {}", hash); - } - if let Some(ref tag) = pkg.ghcr_tag { - println!("GHCR Tag: {}", tag); - } - } - } - Ok(()) - } - CacheCommands::List { - host, - status, - json, - limit, - } => { - let (status_filter, include_outdated) = match status { - StatusFilter::Success => (Some(BuildStatus::Success), false), - StatusFilter::Failed => (Some(BuildStatus::Failed), false), - StatusFilter::Pending => (Some(BuildStatus::Pending), false), - StatusFilter::Skipped => (Some(BuildStatus::Skipped), false), - StatusFilter::Outdated => (None, true), - StatusFilter::All => (None, false), - }; - - let mut packages = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - db.list_packages(&host, status_filter, include_outdated) - .await? - } else { - let db = CacheDatabase::open(&args.cache)?; - db.list_packages(&host, status_filter, include_outdated)? - }; - - if let Some(limit) = limit { - packages.truncate(limit); - } - - if json { - println!("{}", serde_json::to_string_pretty(&packages)?); - } else { - let status_icon = |s: Option| match s { - Some(BuildStatus::Success) => "✓", - Some(BuildStatus::Failed) => "✗", - Some(BuildStatus::Pending) => "○", - Some(BuildStatus::Skipped) => "⊘", - None => "?", - }; - - println!("Packages on {} ({} total):", host, packages.len()); - println!(); - println!( - "{:<3} {:<30} {:<15} {:<10}", - "", "Package", "Version", "Status" - ); - println!("{}", "-".repeat(60)); - - for pkg in &packages { - let version = pkg.current_version.as_deref().unwrap_or("-"); - let icon = status_icon(pkg.last_build_status); - let outdated = if pkg.is_outdated { " [outdated]" } else { "" }; - println!( - "{:<3} {:<30} {:<15} {:<10}{}", - icon, - pkg.pkg_name, - version, - pkg.last_build_status - .map(|s| s.to_string()) - .unwrap_or_else(|| "never".to_string()), - outdated - ); - } - } - Ok(()) - } - CacheCommands::Recent { host, limit, json } => { - let builds = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - db.get_recent_builds(&host, limit).await? - } else { - let db = CacheDatabase::open(&args.cache)?; - db.get_recent_builds(&host, limit)? - }; - - if json { - let output: Vec<_> = builds - .iter() - .map(|(pkg, hist)| { - serde_json::json!({ - "package": pkg.pkg_name, - "version": hist.version, - "status": hist.build_status.to_string(), - "build_id": hist.build_id, - "build_date": hist.build_date.to_rfc3339(), - }) - }) - .collect(); - println!("{}", serde_json::to_string_pretty(&output)?); - } else { - println!("Recent builds on {}:", host); - println!(); - println!( - "{:<3} {:<25} {:<12} {:<10} {:<20}", - "", "Package", "Version", "Status", "Date" - ); - println!("{}", "-".repeat(75)); - - for (pkg, hist) in &builds { - let icon = match hist.build_status { - BuildStatus::Success => "✓", - BuildStatus::Failed => "✗", - BuildStatus::Pending => "○", - BuildStatus::Skipped => "⊘", - }; - let date = hist.build_date.format("%Y-%m-%d %H:%M"); - println!( - "{:<3} {:<25} {:<12} {:<10} {:<20}", - icon, pkg.pkg_name, hist.version, hist.build_status, date - ); - } - } - Ok(()) - } - CacheCommands::Report { - host, - format, - output, - history_limit, - } => { - let (stats, failed, outdated, recent) = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - let stats = db.get_stats(&host).await?; - let failed = db - .list_packages(&host, Some(BuildStatus::Failed), false) - .await?; - let outdated = db.list_packages(&host, None, true).await?; - let recent = db.get_recent_builds(&host, history_limit).await?; - (stats, failed, outdated, recent) - } else { - let db = CacheDatabase::open(&args.cache)?; - let stats = db.get_stats(&host)?; - let failed = db.list_packages(&host, Some(BuildStatus::Failed), false)?; - let outdated = db.list_packages(&host, None, true)?; - let recent = db.get_recent_builds(&host, history_limit)?; - (stats, failed, outdated, recent) - }; - - let report = match format { - ReportFormat::Json => serde_json::to_string_pretty(&serde_json::json!({ - "host": host, - "stats": stats, - "failed_packages": failed, - "outdated_packages": outdated, - "recent_builds": recent.iter().map(|(p, h)| { - serde_json::json!({ - "package": p.pkg_name, - "version": h.version, - "status": h.build_status.to_string(), - "date": h.build_date.to_rfc3339(), - }) - }).collect::>(), - }))?, - ReportFormat::Markdown => { - generate_markdown_report(&host, &stats, &failed, &outdated, &recent) - } - ReportFormat::Html => { - generate_html_report(&host, &stats, &failed, &outdated, &recent) - } - }; - - if let Some(path) = output { - std::fs::write(&path, &report)?; - println!("Report written to {:?}", path); - } else { - println!("{}", report); - } - Ok(()) - } - CacheCommands::GhSummary { host, title } => { - let (stats, failed) = if let Some(uri) = get_cache_uri() { - let db = MongoDatabase::connect(&uri).await?; - let stats = db.get_stats(&host).await?; - let failed = db - .list_packages(&host, Some(BuildStatus::Failed), false) - .await?; - (stats, failed) - } else { - let db = CacheDatabase::open(&args.cache)?; - let stats = db.get_stats(&host)?; - let failed = db.list_packages(&host, Some(BuildStatus::Failed), false)?; - (stats, failed) - }; - - let success_rate = if stats.total_packages > 0 { - (stats.successful as f64 / stats.total_packages as f64) * 100.0 - } else { - 0.0 - }; - - let mut summary = String::new(); - summary.push_str(&format!("## {} ({})\n\n", title, host)); - summary.push_str("| ✅ Success | ❌ Failed | ⏳ Pending | 📊 Total | 🎯 Rate |\n"); - summary.push_str("|---|---|---|---|---|\n"); - summary.push_str(&format!( - "| {} | {} | {} | {} | {:.1}% |\n\n", - stats.successful, stats.failed, stats.pending, stats.total_packages, success_rate - )); - - if !failed.is_empty() { - summary.push_str("### ❌ Failed Packages\n\n"); - summary.push_str("

Show failed packages\n\n"); - summary.push_str("| Package | Version |\n"); - summary.push_str("|---------|--------|\n"); - for pkg in failed.iter().take(50) { - let version = pkg.current_version.as_deref().unwrap_or("-"); - summary.push_str(&format!("| {} | {} |\n", pkg.pkg_name, version)); - } - if failed.len() > 50 { - summary.push_str(&format!("\n*...and {} more*\n", failed.len() - 50)); - } - summary.push_str("\n
\n"); - } - - if let Ok(summary_path) = std::env::var("GITHUB_STEP_SUMMARY") { - use std::io::Write; - let mut file = std::fs::OpenOptions::new() - .append(true) - .create(true) - .open(&summary_path)?; - writeln!(file, "{}", summary)?; - println!("Summary written to GITHUB_STEP_SUMMARY"); - } else { - println!("{}", summary); - } - Ok(()) - } - CacheCommands::Export { output } => { - let uri = get_cache_uri().ok_or_else(|| { - sbuild_cache::Error::Other( - "SBUILD_CACHE_URI environment variable is required for export".to_string(), - ) - })?; - let mongo = MongoDatabase::connect(&uri).await?; - sbuild_cache::export::export_to_sqlite(&mongo, &output).await?; - println!("Exported MongoDB cache to {:?}", output); - Ok(()) - } - CacheCommands::Snapshots { - package, - host, - json, - } => { - let uri = get_cache_uri().ok_or_else(|| { - sbuild_cache::Error::Other( - "SBUILD_CACHE_URI environment variable is required".to_string(), - ) - })?; - let mongo = MongoDatabase::connect(&uri).await?; - let snapshots = mongo.get_snapshots(&package, &host).await?; - - if json { - println!("{}", serde_json::to_string_pretty(&snapshots)?); - } else { - if snapshots.is_empty() { - println!("No snapshots found for {} on {}", package, host); - } else { - println!("Snapshots for {} on {}:", package, host); - for snapshot in &snapshots { - println!(" - {}", snapshot); - } - println!("\nTotal: {} snapshot(s)", snapshots.len()); - } - } - Ok(()) - } - CacheCommands::MigrateSnapshots { - recipe, - package, - host, - } => { - let uri = get_cache_uri().ok_or_else(|| { - sbuild_cache::Error::Other( - "SBUILD_CACHE_URI environment variable is required".to_string(), - ) - })?; - - // Parse recipe file using sbuild-meta - let recipe_content = std::fs::read_to_string(&recipe) - .map_err(|e| sbuild_cache::Error::Other(format!("Failed to read recipe: {}", e)))?; - let parsed = sbuild_meta::SBuildRecipe::from_yaml(&recipe_content).map_err(|e| { - sbuild_cache::Error::Other(format!("Failed to parse recipe: {}", e)) - })?; - - // Use provided package ID or extract from recipe - let pkg_id = package.unwrap_or_else(|| { - if parsed.pkg_id.is_empty() { - parsed.pkg.clone() - } else { - parsed.pkg_id.clone() - } - }); - - let pkg_name = parsed.pkg.clone(); - let recipe_snapshots = parsed.snapshots.clone(); - - if recipe_snapshots.is_empty() { - println!("No snapshots found in recipe file"); - return Ok(()); - } - - let mongo = MongoDatabase::connect(&uri).await?; - - // Ensure package exists - mongo - .get_or_create_package(&pkg_id, &pkg_name, &host) - .await?; - - // Get existing snapshots from DB - let existing = mongo.get_snapshots(&pkg_id, &host).await?; - let existing_set: std::collections::HashSet<_> = existing.iter().cloned().collect(); - - // Add new snapshots - let mut added = 0; - for snapshot in &recipe_snapshots { - if !existing_set.contains(snapshot) { - mongo.add_snapshot(&pkg_id, &host, snapshot).await?; - added += 1; - } - } - - println!("Migrated snapshots for {} on {}:", pkg_id, host); - println!(" Recipe snapshots: {}", recipe_snapshots.len()); - println!(" Already in DB: {}", recipe_snapshots.len() - added); - println!(" Newly added: {}", added); - - Ok(()) - } - } -} - -fn generate_markdown_report( - host: &str, - stats: &sbuild_cache::BuildStats, - failed: &[sbuild_cache::PackageRecord], - outdated: &[sbuild_cache::PackageRecord], - recent: &[(sbuild_cache::PackageRecord, sbuild_cache::BuildHistoryEntry)], -) -> String { - let mut md = String::new(); - md.push_str(&format!("# Build Report: {}\n\n", host)); - md.push_str(&format!( - "Generated: {}\n\n", - chrono::Utc::now().format("%Y-%m-%d %H:%M UTC") - )); - - md.push_str("## Summary\n\n"); - md.push_str("| Metric | Count |\n"); - md.push_str("|--------|-------|\n"); - md.push_str(&format!("| Total Packages | {} |\n", stats.total_packages)); - md.push_str(&format!("| Successful | {} |\n", stats.successful)); - md.push_str(&format!("| Failed | {} |\n", stats.failed)); - md.push_str(&format!("| Pending | {} |\n", stats.pending)); - md.push_str(&format!("| Outdated | {} |\n\n", stats.outdated)); - - if stats.total_packages > 0 { - let success_rate = (stats.successful as f64 / stats.total_packages as f64) * 100.0; - md.push_str(&format!("**Success Rate: {:.1}%**\n\n", success_rate)); - } - - if !failed.is_empty() { - md.push_str("## Failed Packages\n\n"); - md.push_str("| Package | Version | Last Build |\n"); - md.push_str("|---------|---------|------------|\n"); - for pkg in failed.iter().take(20) { - let version = pkg.current_version.as_deref().unwrap_or("-"); - let date = pkg - .last_build_date - .map(|d| d.format("%Y-%m-%d").to_string()) - .unwrap_or_else(|| "-".to_string()); - md.push_str(&format!("| {} | {} | {} |\n", pkg.pkg_name, version, date)); - } - if failed.len() > 20 { - md.push_str(&format!("\n*...and {} more*\n", failed.len() - 20)); - } - md.push('\n'); - } - - if !outdated.is_empty() { - md.push_str("## Outdated Packages\n\n"); - md.push_str("| Package | Current | Upstream |\n"); - md.push_str("|---------|---------|----------|\n"); - for pkg in outdated.iter().take(20) { - let current = pkg.current_version.as_deref().unwrap_or("-"); - let upstream = pkg.upstream_version.as_deref().unwrap_or("-"); - md.push_str(&format!( - "| {} | {} | {} |\n", - pkg.pkg_name, current, upstream - )); - } - if outdated.len() > 20 { - md.push_str(&format!("\n*...and {} more*\n", outdated.len() - 20)); - } - md.push('\n'); - } - - if !recent.is_empty() { - md.push_str("## Recent Builds\n\n"); - md.push_str("| Status | Package | Version | Date |\n"); - md.push_str("|--------|---------|---------|------|\n"); - for (pkg, hist) in recent { - let icon = match hist.build_status { - BuildStatus::Success => "✅", - BuildStatus::Failed => "❌", - BuildStatus::Pending => "⏳", - BuildStatus::Skipped => "⏭️", - }; - let date = hist.build_date.format("%Y-%m-%d %H:%M"); - md.push_str(&format!( - "| {} | {} | {} | {} |\n", - icon, pkg.pkg_name, hist.version, date - )); - } - md.push('\n'); - } - - md -} - -fn generate_html_report( - host: &str, - stats: &sbuild_cache::BuildStats, - _failed: &[sbuild_cache::PackageRecord], - _outdated: &[sbuild_cache::PackageRecord], - _recent: &[(sbuild_cache::PackageRecord, sbuild_cache::BuildHistoryEntry)], -) -> String { - let success_rate = if stats.total_packages > 0 { - (stats.successful as f64 / stats.total_packages as f64) * 100.0 - } else { - 0.0 - }; - - format!( - r#" - - - Build Report: {host} - - - -

Build Report: {host}

-

Generated: {timestamp}

-
-
{total}
Total
-
{success}
Success
-
{fail}
Failed
-
{pending}
Pending
-
-

Success Rate: {success_rate:.1}%

- -"#, - host = host, - timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M UTC"), - total = stats.total_packages, - success = stats.successful, - fail = stats.failed, - pending = stats.pending, - success_rate = success_rate, - ) -} diff --git a/sbuild/src/commands/info.rs b/sbuild/src/commands/info.rs deleted file mode 100644 index 9f2b71c..0000000 --- a/sbuild/src/commands/info.rs +++ /dev/null @@ -1,188 +0,0 @@ -use clap::{Parser, ValueEnum}; -use colored::Colorize; -use saphyr::{LoadableYamlNode, YamlOwned}; -use sbuild::fetch_recipe; - -#[derive(Parser)] -#[command(about = "Get information about an SBUILD recipe")] -pub struct InfoArgs { - #[arg(required = true)] - pub recipe: String, - - #[arg(long)] - pub check_host: Option, - - #[arg(long, value_enum, default_value = "text")] - pub format: OutputFormat, - - #[arg(long)] - pub field: Option, -} - -#[derive(Debug, Clone, Copy, ValueEnum, Default)] -pub enum OutputFormat { - #[default] - Text, - Json, -} - -fn yaml_to_json(yaml: &YamlOwned) -> serde_json::Value { - if let Some(s) = yaml.as_str() { - serde_json::Value::String(s.to_string()) - } else if let Some(b) = yaml.as_bool() { - serde_json::Value::Bool(b) - } else if let Some(i) = yaml.as_integer() { - serde_json::json!(i) - } else if let Some(f) = yaml.as_floating_point() { - serde_json::json!(f) - } else if yaml.is_null() { - serde_json::Value::Null - } else if let Some(seq) = yaml.as_sequence() { - serde_json::Value::Array(seq.iter().map(yaml_to_json).collect()) - } else if let Some(map) = yaml.as_mapping() { - let obj: serde_json::Map = map - .iter() - .filter_map(|(k, v)| { - let key = k.as_str().map(|s| s.to_string())?; - Some((key, yaml_to_json(v))) - }) - .collect(); - serde_json::Value::Object(obj) - } else { - serde_json::Value::Null - } -} - -fn yaml_value_to_string(yaml: &YamlOwned) -> String { - if let Some(s) = yaml.as_str() { - s.to_string() - } else if let Some(b) = yaml.as_bool() { - b.to_string() - } else if let Some(i) = yaml.as_integer() { - i.to_string() - } else if let Some(f) = yaml.as_floating_point() { - f.to_string() - } else { - serde_json::to_string_pretty(&yaml_to_json(yaml)).unwrap_or_default() - } -} - -fn get_str(yaml: &YamlOwned, key: &str) -> Option { - yaml.as_mapping_get(key) - .and_then(|v: &YamlOwned| v.as_str()) - .map(|s| s.to_string()) -} - -fn get_hosts(yaml: &YamlOwned) -> Option> { - let x_exec = yaml.as_mapping_get("x_exec")?; - let host = x_exec.as_mapping_get("host")?; - let seq = host.as_sequence()?; - Some( - seq.iter() - .filter_map(|h: &YamlOwned| h.as_str().map(|s| s.to_string())) - .collect(), - ) -} - -pub async fn run(args: InfoArgs) -> Result<(), String> { - let content = if args.recipe.starts_with("http://") || args.recipe.starts_with("https://") { - fetch_recipe(&args.recipe).await? - } else { - std::fs::read_to_string(&args.recipe) - .map_err(|e| format!("Failed to read recipe: {}", e))? - }; - - let docs = - YamlOwned::load_from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?; - let yaml = docs - .into_iter() - .next() - .ok_or_else(|| "Empty YAML document".to_string())?; - - if let Some(ref check_host) = args.check_host { - if let Some(host_list) = get_hosts(&yaml) { - let is_supported = host_list.iter().any(|h| h.eq_ignore_ascii_case(check_host)); - - if !is_supported { - let supported: Vec<&str> = host_list.iter().map(|s| s.as_str()).collect(); - eprintln!("Recipe does not support host: {}", check_host); - eprintln!("Supported hosts: {:?}", supported); - std::process::exit(1); - } - - println!("Host {} is supported", check_host); - return Ok(()); - } else { - println!("Host {} is supported (no restrictions)", check_host); - return Ok(()); - } - } - - if let Some(ref field) = args.field { - let value = match field.as_str() { - "pkg" => get_str(&yaml, "pkg"), - "pkg_id" => get_str(&yaml, "pkg_id"), - "pkg_name" => get_str(&yaml, "pkg_name"), - "pkg_type" => get_str(&yaml, "pkg_type"), - "description" => get_str(&yaml, "description"), - "version" => get_str(&yaml, "version"), - "hosts" => get_hosts(&yaml).map(|h| h.join(",")), - _ => yaml - .as_mapping_get(field) - .map(|v: &YamlOwned| yaml_value_to_string(v)), - }; - - match value { - Some(v) => { - println!("{}", v); - Ok(()) - } - None => { - eprintln!("Field '{}' not found", field); - std::process::exit(1); - } - } - } else { - match args.format { - OutputFormat::Json => { - let json = serde_json::to_string_pretty(&yaml_to_json(&yaml)) - .map_err(|e| format!("Failed to convert to JSON: {}", e))?; - println!("{}", json); - } - OutputFormat::Text => { - println!( - "{}: {}", - "pkg".bright_cyan(), - get_str(&yaml, "pkg").as_deref().unwrap_or("N/A") - ); - println!( - "{}: {}", - "pkg_id".bright_cyan(), - get_str(&yaml, "pkg_id").as_deref().unwrap_or("N/A") - ); - println!( - "{}: {}", - "pkg_name".bright_cyan(), - get_str(&yaml, "pkg_name").as_deref().unwrap_or("N/A") - ); - println!( - "{}: {}", - "pkg_type".bright_cyan(), - get_str(&yaml, "pkg_type").as_deref().unwrap_or("N/A") - ); - println!( - "{}: {}", - "description".bright_cyan(), - get_str(&yaml, "description").as_deref().unwrap_or("N/A") - ); - - if let Some(host_list) = get_hosts(&yaml) { - println!("{}: {}", "hosts".bright_cyan(), host_list.join(", ")); - } else { - println!("{}: {}", "hosts".bright_cyan(), "all (no restrictions)"); - } - } - } - Ok(()) - } -} diff --git a/sbuild/src/commands/lint.rs b/sbuild/src/commands/lint.rs deleted file mode 100644 index 881fe1e..0000000 --- a/sbuild/src/commands/lint.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::{ - collections::HashSet, - env, - fs::OpenOptions, - io::Write, - path::PathBuf, - sync::{ - self, - atomic::{AtomicUsize, Ordering}, - Arc, LazyLock, - }, - thread, - time::{Duration, Instant}, -}; - -use clap::Parser; -use colored::Colorize; -use sbuild_linter::{ - logger::{LogManager, LogMessage}, - semaphore::Semaphore, - Linter, -}; - -static CHECK_MARK: LazyLock = LazyLock::new(|| "✔".bright_green().bold()); -static CROSS_MARK: LazyLock = LazyLock::new(|| "〤".bright_red().bold()); -static WARN: LazyLock = LazyLock::new(|| "⚠️".bright_yellow().bold()); - -#[derive(Parser)] -#[command(about = "Linter for SBUILD package files")] -pub struct LintArgs { - /// Files to lint - #[arg(required = true)] - files: Vec, - - /// Enable pkgver mode - #[arg(short = 'P', long)] - pkgver: bool, - - /// Disable shellcheck - #[arg(long)] - no_shellcheck: bool, - - /// Run N jobs in parallel - #[arg(short, long, default_value = "4")] - parallel: usize, - - /// Replace the original file on success - #[arg(short, long)] - inplace: bool, - - /// File to store successful packages list - #[arg(long)] - success: Option, - - /// File to store failed packages list - #[arg(long)] - fail: Option, - - /// Timeout duration in seconds - #[arg(long, default_value = "30")] - timeout: u64, -} - -pub fn run(args: LintArgs) -> Result<(), String> { - let files: HashSet = args.files.iter().cloned().collect(); - - if files.is_empty() { - return Err("No files specified".to_string()); - } - - if !args.no_shellcheck && which::which("shellcheck").is_err() { - return Err("shellcheck not found. Please install.".to_string()); - } - - println!("sbuild lint v{}", env!("CARGO_PKG_VERSION")); - - let now = Instant::now(); - let success = Arc::new(AtomicUsize::new(0)); - let fail = Arc::new(AtomicUsize::new(0)); - - let (tx, rx) = sync::mpsc::channel(); - let log_manager = LogManager::new(tx.clone()); - - let fail_store = if let Some(ref fail_path) = args.fail { - let f = OpenOptions::new() - .create(true) - .append(true) - .open(fail_path) - .map_err(|e| e.to_string())?; - Some(Arc::new(f)) - } else { - None - }; - - let success_store = if let Some(ref success_path) = args.success { - let f = OpenOptions::new() - .create(true) - .append(true) - .open(success_path) - .map_err(|e| e.to_string())?; - Some(Arc::new(f)) - } else { - None - }; - - let parallel = args.parallel; - let file_count = files.len(); - let logger_handle = thread::spawn(move || { - let show_all = parallel == 1 || file_count == 1; - while let Ok(log) = rx.recv() { - match log { - LogMessage::Info(msg) if show_all => println!("{}", msg), - LogMessage::Success(msg) if show_all => println!("[{}] {}", &*CHECK_MARK, msg), - LogMessage::Error(msg) => eprintln!("[{}] {}", &*CROSS_MARK, msg), - LogMessage::Warn(msg) => eprintln!("[{}] {}", &*WARN, msg), - LogMessage::CustomError(msg) => eprintln!("{}", msg), - LogMessage::Done => break, - _ => {} - } - } - }); - - let semaphore = Arc::new(Semaphore::new(args.parallel)); - let mut handles = Vec::new(); - - for file_path in &files { - let file_path = file_path.clone(); - let semaphore = Arc::clone(&semaphore); - let success = Arc::clone(&success); - let logger = log_manager.create_logger::(None); - let fail = Arc::clone(&fail); - let success_store = success_store.clone(); - let fail_store = fail_store.clone(); - let inplace = args.inplace; - let no_shellcheck = args.no_shellcheck; - let pkgver = args.pkgver; - let timeout = args.timeout; - - semaphore.acquire(); - let handle = thread::spawn(move || { - let linter = Linter::new(logger, Duration::from_secs(timeout)); - if linter - .lint(&file_path, inplace, no_shellcheck, pkgver) - .is_some() - { - if let Some(mut success_store) = success_store { - let fp = format!("{}\n", file_path); - let _ = success_store.write_all(fp.as_bytes()); - } - success.fetch_add(1, Ordering::SeqCst); - } else { - if let Some(mut fail_store) = fail_store { - let fp = format!("{}\n", file_path); - let _ = fail_store.write_all(fp.as_bytes()); - } - fail.fetch_add(1, Ordering::SeqCst); - } - semaphore.release(); - }); - - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - - log_manager.done(); - logger_handle.join().unwrap(); - - println!(); - println!( - "[{}] {} files validated successfully", - "+".bright_blue().bold(), - success.load(Ordering::SeqCst), - ); - println!( - "[{}] {} files failed to pass validation", - "+".bright_blue().bold(), - fail.load(Ordering::SeqCst), - ); - let total_evaluated = fail.load(Ordering::SeqCst) + success.load(Ordering::SeqCst); - println!( - "[{}] Evaluated {}/{} file(s) in {:#?}", - "+".bright_blue().bold(), - total_evaluated, - files.len(), - now.elapsed() - ); - - if fail.load(Ordering::SeqCst) > 0 { - return Err(format!( - "{} file(s) failed validation", - fail.load(Ordering::SeqCst) - )); - } - - Ok(()) -} diff --git a/sbuild/src/commands/meta.rs b/sbuild/src/commands/meta.rs deleted file mode 100644 index b841552..0000000 --- a/sbuild/src/commands/meta.rs +++ /dev/null @@ -1,638 +0,0 @@ -use std::path::PathBuf; - -use clap::{Parser, Subcommand}; -use log::{debug, info, warn}; -use sbuild_cache::MongoDatabase; -use sbuild_meta::{ - hash::{compute_recipe_hash, compute_recipe_hash_excluding_version}, - manifest::OciManifest, - metadata::PackageMetadata, - recipe::{filter_by_arch, filter_enabled, scan_recipes, SBuildRecipe}, - registry::RegistryClient, - Error, Result, -}; - -#[derive(Parser)] -#[command(about = "Metadata generator for SBUILD packages")] -pub struct MetaArgs { - #[command(subcommand)] - command: MetaCommands, -} - -#[derive(Subcommand)] -enum MetaCommands { - Generate { - #[arg(short, long)] - arch: String, - - #[arg(short, long, num_args = 1..)] - recipes: Vec, - - #[arg(short, long)] - output: Option, - - #[arg(short, long)] - cache: Option, - - #[arg(short, long, default_value = "4")] - parallel: usize, - - #[arg(long, env = "GITHUB_TOKEN")] - github_token: Option, - - #[arg(long, default_value = "pkgforge")] - ghcr_owner: String, - }, - - ShouldRebuild { - #[arg(short, long)] - recipe: PathBuf, - - #[arg(short, long)] - cache: Option, - - #[arg(short, long)] - force: bool, - }, - - CheckUpdates { - #[arg(short, long, num_args = 1..)] - recipes: Vec, - - #[arg(short, long)] - cache: Option, - - #[arg(short, long)] - output: PathBuf, - - #[arg(short, long, default_value = "10")] - parallel: usize, - - #[arg(long, default_value = "30")] - timeout: u64, - }, - - Inspect { - recipe: PathBuf, - - #[arg(short, long, default_value = "x86_64-linux")] - arch: String, - - #[arg(long, default_value = "pkgforge")] - ghcr_owner: String, - - /// Fetch live manifest data from GHCR - #[arg(long)] - live: bool, - }, - - Hash { - recipe: PathBuf, - - #[arg(long)] - exclude_version: bool, - }, - - FetchManifest { - #[arg(short, long)] - repository: String, - - #[arg(short, long)] - tag: Option, - - #[arg(short, long, default_value = "x86_64-linux")] - arch: String, - - #[arg(long, env = "GITHUB_TOKEN")] - github_token: Option, - }, -} - -pub async fn run(args: MetaArgs) -> Result<()> { - setup_logging(); - - match args.command { - MetaCommands::Generate { - arch, - recipes, - output, - cache, - parallel, - github_token, - ghcr_owner, - } => { - cmd_generate( - arch, - recipes, - output, - cache, - parallel, - github_token, - ghcr_owner, - ) - .await - } - - MetaCommands::ShouldRebuild { - recipe, - cache, - force, - } => cmd_should_rebuild(recipe, cache, force).await, - - MetaCommands::CheckUpdates { - recipes, - cache, - output, - parallel, - timeout, - } => cmd_check_updates(recipes, cache, output, parallel, timeout).await, - - MetaCommands::Inspect { - recipe, - arch, - ghcr_owner, - live, - } => cmd_inspect(recipe, arch, ghcr_owner, live).await, - - MetaCommands::Hash { - recipe, - exclude_version, - } => cmd_hash(recipe, exclude_version), - - MetaCommands::FetchManifest { - repository, - tag, - arch, - github_token, - } => cmd_fetch_manifest(repository, tag, arch, github_token).await, - } -} - -fn setup_logging() { - env_logger::Builder::new() - .filter_level(log::LevelFilter::Info) - .format_target(false) - .format_timestamp(None) - .init(); -} - -async fn cmd_generate( - arch: String, - recipe_dirs: Vec, - output: Option, - _cache: Option, - _parallel: usize, - github_token: Option, - ghcr_owner: String, -) -> Result<()> { - let arch = arch.to_lowercase(); - info!("Generating metadata for {}", arch); - - let _ = github_token; - let client = RegistryClient::new(); - - // Connect to MongoDB for snapshots (optional) - let mongo_db = if let Ok(uri) = std::env::var("SBUILD_CACHE_URI") { - if !uri.is_empty() { - match MongoDatabase::connect(&uri).await { - Ok(db) => { - info!("Connected to MongoDB cache for snapshots"); - Some(db) - } - Err(e) => { - warn!("Failed to connect to MongoDB cache: {}", e); - None - } - } - } else { - None - } - } else { - None - }; - - let mut all_recipes = Vec::new(); - for dir in &recipe_dirs { - info!("Scanning recipes in {:?}", dir); - let recipes = scan_recipes(dir)?; - all_recipes.extend(recipes); - } - - info!("Found {} total recipes", all_recipes.len()); - - let recipes = filter_enabled(filter_by_arch(all_recipes, &arch)); - info!("After filtering: {} recipes for {}", recipes.len(), arch); - - let mut metadata: Vec = Vec::new(); - - for (path, recipe) in recipes { - let ghcr_packages = recipe.ghcr_packages_from_path(&path, &ghcr_owner); - - if ghcr_packages.is_empty() { - warn!("No GHCR packages found for {:?}", path); - continue; - } - - for ghcr_info in &ghcr_packages { - info!( - "Processing: {} -> {} ({:?})", - recipe.pkg, ghcr_info.pkg_name, path - ); - - let mut pkg_metadata = PackageMetadata::from_recipe(&recipe); - - pkg_metadata.pkg_name = ghcr_info.pkg_name.clone(); - pkg_metadata.pkg_family = Some(ghcr_info.pkg_family.clone()); - - let pkg_type = ghcr_info.recipe_name.split('.').next().unwrap_or("static"); - pkg_metadata.pkg_type = Some(pkg_type.to_string()); - pkg_metadata.pkg = format!("{}.{}", ghcr_info.pkg_name, pkg_type); - - pkg_metadata.ghcr_url = Some(ghcr_info.ghcr_url()); - pkg_metadata.pkg_webpage = Some(ghcr_info.pkg_webpage(&arch)); - - if let Some(pkg_provides) = recipe.get_package_provides(&ghcr_info.pkg_name) { - pkg_metadata.provides = Some(pkg_provides.to_vec()); - } - - let recipe_path_str = path.to_string_lossy(); - pkg_metadata.build_script = Some(format!( - "https://github.com/pkgforge/soarpkgs/blob/main/{}", - recipe_path_str - )); - - match client.list_tags(&ghcr_info.ghcr_path).await { - Ok(tag_list) => { - if let Some(tag) = RegistryClient::get_latest_arch_tag(&tag_list.tags, &arch) { - match client.fetch_manifest(&ghcr_info.ghcr_path, tag).await { - Ok(manifest_str) => { - if let Ok(manifest) = OciManifest::from_json(&manifest_str) { - pkg_metadata.enrich_from_manifest( - &manifest, - &ghcr_info.ghcr_path, - &arch, - ); - } - } - Err(e) => { - warn!( - "Failed to fetch manifest for {}: {}", - ghcr_info.ghcr_path, e - ); - } - } - } else { - debug!("No matching tag for {} on {}", arch, ghcr_info.ghcr_path); - } - } - Err(e) => { - warn!("Failed to list tags for {}: {}", ghcr_info.ghcr_path, e); - } - } - - pkg_metadata.parse_note_flags(); - - // Fetch snapshots from MongoDB cache if available - if let Some(ref db) = &mongo_db { - if let Ok(db_snapshots) = db.get_snapshots(&pkg_metadata.pkg_id, &arch).await { - if !db_snapshots.is_empty() { - pkg_metadata.merge_snapshots(&db_snapshots); - debug!( - "Merged {} snapshots from cache for {}", - db_snapshots.len(), - pkg_metadata.pkg_id - ); - } - } - } - - if pkg_metadata.is_valid() { - metadata.push(pkg_metadata); - } else { - debug!( - "Skipping {}: not in GHCR or invalid metadata", - ghcr_info.ghcr_path - ); - } - } - } - - if metadata.is_empty() { - info!("No packages to write"); - return Ok(()); - } - - metadata.sort_by(|a, b| a.pkg.cmp(&b.pkg)); - - let output_path = output.unwrap_or_else(|| PathBuf::from(format!("{}.json", arch))); - - if let Some(parent) = output_path.parent() { - std::fs::create_dir_all(parent)?; - } - - let json = serde_json::to_string_pretty(&metadata)?; - std::fs::write(&output_path, json)?; - - info!( - "Generated metadata for {} packages -> {:?}", - metadata.len(), - output_path - ); - - Ok(()) -} - -async fn cmd_should_rebuild( - recipe_path: PathBuf, - _cache: Option, - force: bool, -) -> Result<()> { - if force { - info!("Force rebuild requested"); - std::process::exit(0); - } - - let recipe = SBuildRecipe::from_file(&recipe_path)?; - - if recipe.is_disabled() { - info!("Recipe is disabled, skipping"); - std::process::exit(1); - } - - if recipe.pkgver.is_none() { - info!("No version field in recipe, should rebuild (new package)"); - std::process::exit(0); - } - - info!("No rebuild needed"); - std::process::exit(1); -} - -async fn cmd_check_updates( - recipe_dirs: Vec, - _cache: Option, - output: PathBuf, - _parallel: usize, - timeout: u64, -) -> Result<()> { - info!("Checking for upstream updates (timeout: {}s)", timeout); - - let mut all_recipes = Vec::new(); - for dir in &recipe_dirs { - let recipes = scan_recipes(dir)?; - all_recipes.extend(recipes); - } - - let enabled_recipes = filter_enabled(all_recipes); - info!("Found {} enabled recipes", enabled_recipes.len()); - - #[derive(serde::Serialize)] - struct UpdateInfo { - pkg: String, - pkg_id: String, - recipe_path: String, - current_version: String, - upstream_version: String, - upstream_remote_version: Option, - } - - let mut updates: Vec = Vec::new(); - - for (path, recipe) in enabled_recipes { - let current_pkgver = match &recipe.pkgver { - Some(v) => v.clone(), - None => continue, - }; - - let current_remote_version = match &recipe.remote_pkgver { - Some(v) => v.clone(), - None => current_pkgver.clone(), - }; - - let pkgver_script = match recipe.pkgver_script() { - Some(s) => s, - None => continue, - }; - - info!( - "Checking {} (current: {})", - recipe.pkg, current_remote_version - ); - - match execute_pkgver(pkgver_script, timeout).await { - Ok((upstream_version, upstream_remote_version)) => { - let upstream_version = upstream_version.trim().to_string(); - let upstream_for_comparison = upstream_remote_version - .as_ref() - .map(|s| s.trim().to_string()) - .unwrap_or_else(|| upstream_version.clone()); - - if upstream_for_comparison != current_remote_version { - info!( - " Update available: {} -> {}", - current_remote_version, upstream_for_comparison - ); - updates.push(UpdateInfo { - pkg: recipe.pkg.clone(), - pkg_id: recipe.pkg_id.clone(), - recipe_path: path.to_string_lossy().to_string(), - current_version: current_pkgver, - upstream_version, - upstream_remote_version, - }); - } - } - Err(e) => { - warn!(" Failed to check {}: {}", recipe.pkg, e); - } - } - } - - let json = serde_json::to_string_pretty(&updates)?; - std::fs::write(&output, json)?; - - info!("Found {} updates -> {:?}", updates.len(), output); - Ok(()) -} - -async fn execute_pkgver(script: &str, timeout_secs: u64) -> Result<(String, Option)> { - use tokio::process::Command; - use tokio::time::{timeout, Duration}; - - let result = timeout( - Duration::from_secs(timeout_secs), - Command::new("bash").arg("-c").arg(script).output(), - ) - .await; - - match result { - Ok(Ok(output)) => { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout).to_string(); - let lines: Vec<&str> = stdout.lines().collect(); - - if lines.is_empty() { - Ok((String::new(), None)) - } else { - let pkgver = lines[0].trim().to_string(); - let remote_pkgver = if lines.len() >= 2 { - Some(lines[1].trim().to_string()) - } else { - None - }; - Ok((pkgver, remote_pkgver)) - } - } else { - Err(Error::PkgverFailed( - String::from_utf8_lossy(&output.stderr).to_string(), - )) - } - } - Ok(Err(e)) => Err(Error::PkgverFailed(e.to_string())), - Err(_) => Err(Error::PkgverFailed("Timeout".to_string())), - } -} - -async fn cmd_inspect( - recipe_path: PathBuf, - arch: String, - ghcr_owner: String, - live: bool, -) -> Result<()> { - let arch = arch.to_lowercase(); - let recipe = SBuildRecipe::from_file(&recipe_path)?; - let ghcr_packages = recipe.ghcr_packages_from_path(&recipe_path, &ghcr_owner); - - if ghcr_packages.is_empty() { - info!("No GHCR packages found for {:?}", recipe_path); - return Ok(()); - } - - let client = if live { - Some(RegistryClient::new()) - } else { - None - }; - - let mut all_metadata = Vec::new(); - - for ghcr_info in &ghcr_packages { - let mut pkg_metadata = PackageMetadata::from_recipe(&recipe); - - pkg_metadata.pkg_name = ghcr_info.pkg_name.clone(); - pkg_metadata.pkg_family = Some(ghcr_info.pkg_family.clone()); - - let pkg_type = ghcr_info.recipe_name.split('.').next().unwrap_or("static"); - pkg_metadata.pkg_type = Some(pkg_type.to_string()); - pkg_metadata.pkg = format!("{}.{}", ghcr_info.pkg_name, pkg_type); - - pkg_metadata.ghcr_url = Some(ghcr_info.ghcr_url()); - pkg_metadata.pkg_webpage = Some(ghcr_info.pkg_webpage(&arch)); - - if let Some(pkg_provides) = recipe.get_package_provides(&ghcr_info.pkg_name) { - pkg_metadata.provides = Some(pkg_provides.to_vec()); - } - - let recipe_path_str = recipe_path.to_string_lossy(); - pkg_metadata.build_script = Some(format!( - "https://github.com/pkgforge/soarpkgs/blob/main/{}", - recipe_path_str - )); - - if let Some(ref client) = client { - match client.list_tags(&ghcr_info.ghcr_path).await { - Ok(tag_list) => { - if let Some(tag) = RegistryClient::get_latest_arch_tag(&tag_list.tags, &arch) { - match client.fetch_manifest(&ghcr_info.ghcr_path, tag).await { - Ok(manifest_str) => { - if let Ok(manifest) = OciManifest::from_json(&manifest_str) { - pkg_metadata.enrich_from_manifest( - &manifest, - &ghcr_info.ghcr_path, - &arch, - ); - } - } - Err(e) => { - warn!( - "Failed to fetch manifest for {}: {}", - ghcr_info.ghcr_path, e - ); - } - } - } - } - Err(e) => { - warn!("Failed to list tags for {}: {}", ghcr_info.ghcr_path, e); - } - } - } - - pkg_metadata.parse_note_flags(); - all_metadata.push(pkg_metadata); - } - - let json = serde_json::to_string_pretty(&all_metadata)?; - println!("{}", json); - Ok(()) -} - -fn cmd_hash(recipe_path: PathBuf, exclude_version: bool) -> Result<()> { - let content = std::fs::read_to_string(&recipe_path)?; - - let hash = if exclude_version { - compute_recipe_hash_excluding_version(&content) - } else { - compute_recipe_hash(&content) - }; - - println!("{}", hash); - Ok(()) -} - -async fn cmd_fetch_manifest( - repository: String, - tag: Option, - arch: String, - _github_token: Option, -) -> Result<()> { - let arch = arch.to_lowercase(); - let client = RegistryClient::new(); - - let tag = match tag { - Some(t) => t, - None => { - info!("Fetching tag list for {}", repository); - let tag_list = client.list_tags(&repository).await?; - debug!("Found {} tags: {:?}", tag_list.tags.len(), tag_list.tags); - RegistryClient::get_latest_arch_tag(&tag_list.tags, &arch) - .ok_or_else(|| Error::ManifestNotFound(format!("No tag found for {}", arch)))? - .clone() - } - }; - - info!("Fetching manifest for {}:{}", repository, tag); - let manifest_str = client.fetch_manifest(&repository, &tag).await?; - let manifest = OciManifest::from_json(&manifest_str)?; - - println!("Repository: {}", repository); - println!("Tag: {}", tag); - println!("Schema Version: {}", manifest.schema_version); - println!("Total Size: {}", manifest.total_size_human()); - println!("Files: {:?}", manifest.filenames()); - - if let Some(ghcr_pkg) = manifest.ghcr_pkg() { - println!("GHCR Package: {}", ghcr_pkg); - } - - if let Some(build_id) = manifest.build_id() { - println!("Build ID: {}", build_id); - } - - if let Ok(Some(pkg_json)) = manifest.get_package_json() { - println!("\nEmbedded Package JSON:"); - println!("{}", serde_json::to_string_pretty(&pkg_json)?); - } - - Ok(()) -} diff --git a/sbuild/src/commands/mod.rs b/sbuild/src/commands/mod.rs deleted file mode 100644 index d42b5dc..0000000 --- a/sbuild/src/commands/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod build; -pub mod cache; -pub mod info; -pub mod lint; -pub mod meta; diff --git a/sbuild/src/constant.rs b/sbuild/src/constant.rs deleted file mode 100644 index 3378d47..0000000 --- a/sbuild/src/constant.rs +++ /dev/null @@ -1,17 +0,0 @@ -pub const ELF_MAGIC_BYTES: [u8; 4] = [0x7f, 0x45, 0x4c, 0x46]; -pub const APPIMAGE_MAGIC_BYTES: [u8; 4] = [0x41, 0x49, 0x02, 0x00]; -pub const FLATIMAGE_MAGIC_BYTES: [u8; 4] = [0x46, 0x49, 0x01, 0x00]; - -// onelf packs a directory into a self-extracting ELF whose last 76 bytes are a -// fixed footer. The footer starts with "ONELF\0\x01\x00"; the file itself ends -// with "FLENONE\0". Since the file begins with ELF magic, onelf can only be told -// apart from a plain static ELF by reading this trailing footer magic. -pub const ONELF_MAGIC_BYTES: [u8; 8] = [0x4f, 0x4e, 0x45, 0x4c, 0x46, 0x00, 0x01, 0x00]; -pub const ONELF_FOOTER_SIZE: u64 = 76; - -pub const PNG_MAGIC_BYTES: [u8; 8] = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; -pub const SVG_MAGIC_BYTES: [u8; 4] = [0x3c, 0x73, 0x76, 0x67]; -pub const XML_MAGIC_BYTES: [u8; 5] = [0x3c, 0x3f, 0x78, 0x6d, 0x6c]; - -pub const MIN_ICON_SIZE: u64 = 20; -pub const MIN_DESKTOP_SIZE: u64 = 8; diff --git a/sbuild/src/ghcr.rs b/sbuild/src/ghcr.rs deleted file mode 100644 index 9a00b81..0000000 --- a/sbuild/src/ghcr.rs +++ /dev/null @@ -1,262 +0,0 @@ -//! GHCR (GitHub Container Registry) push utilities -//! -//! Handles pushing built packages to GHCR using the OCI registry API. - -use std::collections::HashMap; -use std::path::Path; -use std::process::Command; - -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum GhcrError { - #[error("oras command not found - install oras to push packages")] - OrasNotFound, - - #[error("GHCR authentication failed: {0}")] - AuthFailed(String), - - #[error("Push failed: {0}")] - PushFailed(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// GHCR package metadata for annotations -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PackageAnnotations { - pub pkg: String, - pub pkg_id: String, - pub pkg_type: Option, - pub version: String, - pub description: Option, - pub homepage: Option, - pub license: Option, - pub build_date: String, - pub build_id: Option, - pub build_gha: Option, - pub build_script: Option, - /// BLAKE3 checksum of the main binary - pub bsum: Option, - /// SHA256 checksum of the main binary - pub shasum: Option, - /// BLAKE3 checksum of the CHECKSUM file - pub checksum_bsum: Option, -} - -/// GHCR client for pushing packages -pub struct GhcrClient { - token: String, - registry: String, -} - -impl GhcrClient { - pub fn new(token: String) -> Self { - Self { - token, - registry: "ghcr.io".to_string(), - } - } - - /// Check if oras is available - pub fn check_oras() -> Result<(), GhcrError> { - if which::which("oras").is_err() { - return Err(GhcrError::OrasNotFound); - } - Ok(()) - } - - /// Login to GHCR - pub fn login(&self) -> Result<(), GhcrError> { - let output = Command::new("oras") - .args(["login", &self.registry, "-u", "token", "-p", &self.token]) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(GhcrError::AuthFailed(stderr.to_string())); - } - - Ok(()) - } - - /// Push a package to GHCR - pub fn push>( - &self, - files: &[P], - repository: &str, - tag: &str, - annotations: &PackageAnnotations, - ) -> Result { - let target = format!("{}/{}:{}", self.registry, repository, tag); - - let mut cmd = Command::new("oras"); - cmd.arg("push") - .arg("--disable-path-validation") - .arg("--config") - .arg("/dev/null:application/vnd.oci.empty.v1+json"); - - // Add standard annotations - let annotation_map = self.build_annotations(annotations); - for (key, value) in &annotation_map { - cmd.arg("--annotation").arg(format!("{}={}", key, value)); - } - - // Add target - cmd.arg(&target); - - // oras stores each pushed file under its basename relative to the - // working directory. Our file set can span multiple directories (the - // package subdir plus shared files in the root outdir like BUILD.log - // and CHECKSUM), so cd-ing to a single source directory can't see all - // of them. Stage every file into one temp dir and push from there. - let staging = tempfile::tempdir()?; - let mut filenames: Vec = Vec::new(); - let mut seen: std::collections::HashSet = std::collections::HashSet::new(); - - for file in files { - let path = file.as_ref(); - if !path.exists() { - continue; - } - let Some(filename) = path.file_name() else { - continue; - }; - let name = filename.to_string_lossy().to_string(); - // First occurrence of a basename wins; skip later duplicates so a - // shared file never clobbers a package-specific one. - if !seen.insert(name.clone()) { - continue; - } - std::fs::copy(path, staging.path().join(filename))?; - filenames.push(name); - } - - // Add filenames (relative to the staging dir) - for filename in &filenames { - cmd.arg(filename); - } - cmd.current_dir(staging.path()); - - let output = cmd.output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(GhcrError::PushFailed(stderr.to_string())); - } - - Ok(target) - } - - /// Build OCI annotations from package metadata - fn build_annotations(&self, meta: &PackageAnnotations) -> HashMap { - let mut annotations = HashMap::new(); - - // Standard OCI annotations - annotations.insert( - "org.opencontainers.image.created".to_string(), - meta.build_date.clone(), - ); - if let Some(ref desc) = meta.description { - annotations.insert( - "org.opencontainers.image.description".to_string(), - desc.clone(), - ); - } - if let Some(ref license) = meta.license { - annotations.insert( - "org.opencontainers.image.licenses".to_string(), - license.clone(), - ); - } - annotations.insert( - "org.opencontainers.image.title".to_string(), - meta.pkg.clone(), - ); - if let Some(ref url) = meta.homepage { - annotations.insert("org.opencontainers.image.url".to_string(), url.clone()); - } - annotations.insert( - "org.opencontainers.image.vendor".to_string(), - "pkgforge".to_string(), - ); - annotations.insert( - "org.opencontainers.image.version".to_string(), - meta.version.clone(), - ); - - // pkgforge-specific annotations - annotations.insert("dev.pkgforge.soar.pkg".to_string(), meta.pkg.clone()); - annotations.insert("dev.pkgforge.soar.pkg_id".to_string(), meta.pkg_id.clone()); - if let Some(ref pkg_type) = meta.pkg_type { - annotations.insert("dev.pkgforge.soar.pkg_type".to_string(), pkg_type.clone()); - } - annotations.insert( - "dev.pkgforge.soar.version".to_string(), - meta.version.clone(), - ); - annotations.insert( - "dev.pkgforge.soar.push_date".to_string(), - meta.build_date.clone(), - ); - - if let Some(ref build_id) = meta.build_id { - annotations.insert("dev.pkgforge.soar.build_id".to_string(), build_id.clone()); - } - if let Some(ref build_gha) = meta.build_gha { - annotations.insert("dev.pkgforge.soar.build_gha".to_string(), build_gha.clone()); - } - if let Some(ref build_script) = meta.build_script { - annotations.insert( - "dev.pkgforge.soar.build_script".to_string(), - build_script.clone(), - ); - } - if let Some(ref bsum) = meta.bsum { - annotations.insert("dev.pkgforge.soar.bsum".to_string(), bsum.clone()); - } - if let Some(ref shasum) = meta.shasum { - annotations.insert("dev.pkgforge.soar.shasum".to_string(), shasum.clone()); - } - if let Some(ref checksum_bsum) = meta.checksum_bsum { - annotations.insert( - "dev.pkgforge.soar.checksum_bsum".to_string(), - checksum_bsum.clone(), - ); - } - - annotations - } -} - -/// Generate GHCR repository path from recipe info -pub fn ghcr_path(pkg_family: &str, recipe_name: &str, pkg_name: &str) -> String { - format!("pkgforge/{}/{}/{}", pkg_family, recipe_name, pkg_name) -} - -/// Sanitize a string for use as an OCI tag -/// OCI tags only allow: [a-zA-Z0-9_.-] -pub fn sanitize_oci_tag(s: &str) -> String { - s.chars() - .map(|c| { - if c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-' { - c - } else { - '-' - } - }) - .collect::() - // Remove consecutive dashes - .split('-') - .filter(|s| !s.is_empty()) - .collect::>() - .join("-") -} - -/// Generate GHCR tag from version and architecture -pub fn ghcr_tag(version: &str, arch: &str) -> String { - let sanitized_version = sanitize_oci_tag(version); - format!("{}-{}", sanitized_version, arch.to_lowercase()) -} diff --git a/sbuild/src/lib.rs b/sbuild/src/lib.rs deleted file mode 100644 index 66cf581..0000000 --- a/sbuild/src/lib.rs +++ /dev/null @@ -1,176 +0,0 @@ -pub mod builder; -pub mod checksum; -pub mod cleanup; -pub mod constant; -pub mod ghcr; -pub mod onelf; -pub mod signing; -pub mod types; -pub mod utils; - -use std::path::Path; - -pub use sbuild_meta::format_size; - -pub fn parse_ghcr_path(recipe_path: &str) -> Option<(String, String)> { - let path_part = if recipe_path.contains("/binaries/") { - recipe_path.split("/binaries/").last() - } else if recipe_path.contains("/packages/") { - recipe_path.split("/packages/").last() - } else if recipe_path.starts_with("binaries/") { - recipe_path.strip_prefix("binaries/") - } else if recipe_path.starts_with("packages/") { - recipe_path.strip_prefix("packages/") - } else { - return None; - }; - - let path_part = path_part?; - - let parts: Vec<&str> = path_part.split('/').collect(); - if parts.len() < 2 { - return None; - } - - let pkg_family = parts[0].to_string(); - let filename = parts[1]; - - let recipe_name = filename - .strip_suffix(".yaml") - .or_else(|| filename.strip_suffix(".yml")) - .unwrap_or(filename) - .to_string(); - - Some((pkg_family, recipe_name)) -} - -pub fn update_json_metadata( - json_path: &Path, - pkg_name: &str, - binary_name: &str, - ghcr_repo: &str, - tag: &str, - bsum: Option<&str>, - shasum: Option<&str>, - checksum_bsum: Option<&str>, - binary_size: Option, - ghcr_total_size: Option, - provides: Option<&[String]>, - snapshots: Option<&[String]>, -) -> Result<(), String> { - let content = - std::fs::read_to_string(json_path).map_err(|e| format!("Failed to read JSON: {}", e))?; - - let mut json: serde_json::Value = - serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?; - - if let Some(obj) = json.as_object_mut() { - obj.insert("pkg_name".to_string(), serde_json::json!(pkg_name)); - - obj.insert( - "ghcr_pkg".to_string(), - serde_json::json!(format!("ghcr.io/{}:{}", ghcr_repo, tag)), - ); - - obj.insert( - "ghcr_url".to_string(), - serde_json::json!(format!("https://ghcr.io/{}", ghcr_repo)), - ); - - obj.insert( - "download_url".to_string(), - serde_json::json!(format!( - "https://api.ghcr.pkgforge.dev/{}?tag={}&download={}", - ghcr_repo, tag, binary_name - )), - ); - - if let Some(b) = bsum { - obj.insert("bsum".to_string(), serde_json::json!(b)); - } - if let Some(s) = shasum { - obj.insert("shasum".to_string(), serde_json::json!(s)); - } - if let Some(cb) = checksum_bsum { - obj.insert("checksum_bsum".to_string(), serde_json::json!(cb)); - } - - if let Some(s) = binary_size { - obj.insert("size".to_string(), serde_json::json!(format_size(s))); - obj.insert("size_raw".to_string(), serde_json::json!(s)); - } - - if let Some(s) = ghcr_total_size { - obj.insert("ghcr_size".to_string(), serde_json::json!(format_size(s))); - obj.insert("ghcr_size_raw".to_string(), serde_json::json!(s)); - } - - if let Some(p) = provides { - obj.insert("provides".to_string(), serde_json::json!(p)); - } - - // Merge snapshots: combine existing with new, dedupe - if let Some(new_snapshots) = snapshots { - let existing: Vec = obj - .get("snapshots") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(|s| s.to_string())) - .collect() - }) - .unwrap_or_default(); - - let mut combined = existing; - for s in new_snapshots { - if !combined.contains(s) { - combined.push(s.clone()); - } - } - - if !combined.is_empty() { - obj.insert("snapshots".to_string(), serde_json::json!(combined)); - } - } - } - - let updated = serde_json::to_string_pretty(&json) - .map_err(|e| format!("Failed to serialize JSON: {}", e))?; - - std::fs::write(json_path, updated).map_err(|e| format!("Failed to write JSON: {}", e))?; - - Ok(()) -} - -pub use sbuild_meta::SBuildRecipe; - -/// Read a validated SBUILD recipe from the output directory. -pub fn read_recipe_metadata(outdir: &Path) -> Option { - let sbuild_path = outdir.join("SBUILD"); - SBuildRecipe::from_file(&sbuild_path).ok() -} - -/// Fetch a recipe from a URL. -pub async fn fetch_recipe(url: &str) -> Result { - use std::time::Duration; - - let client = reqwest::Client::builder() - .timeout(Duration::from_secs(30)) - .build() - .map_err(|e| format!("Failed to create HTTP client: {}", e))?; - - let response = client - .get(url) - .send() - .await - .map_err(|e| format!("Failed to fetch recipe: {}", e))?; - - if !response.status().is_success() { - return Err(format!("HTTP error {}: {}", response.status(), url)); - } - - response - .text() - .await - .map_err(|e| format!("Failed to read response: {}", e)) -} diff --git a/sbuild/src/main.rs b/sbuild/src/main.rs deleted file mode 100644 index 9c09228..0000000 --- a/sbuild/src/main.rs +++ /dev/null @@ -1,64 +0,0 @@ -mod commands; - -use clap::{Parser, Subcommand}; -use colored::Colorize; -use sbuild::types::SoarEnv; - -#[derive(Parser)] -#[command(name = "sbuild")] -#[command(about = "Toolchain for building, linting, and managing SBUILD packages", long_about = None)] -#[command(version)] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - Build(commands::build::BuildArgs), - Info(commands::info::InfoArgs), - Cache(commands::cache::CacheArgs), - Lint(commands::lint::LintArgs), - Meta(commands::meta::MetaArgs), -} - -fn get_soar_env() -> Option { - use std::process::Command; - - let cmd = Command::new("soar").arg("env").output(); - let mut soar_env = SoarEnv::default(); - - if let Ok(cmd_output) = cmd { - if cmd_output.status.success() && cmd_output.stderr.is_empty() { - let output_str = String::from_utf8_lossy(&cmd_output.stdout); - for line in output_str.lines() { - if let Some(value) = line.strip_prefix("SOAR_CACHE=") { - soar_env.cache_path = value.to_string(); - } - if let Some(value) = line.strip_prefix("SOAR_BIN=") { - soar_env.bin_path = value.to_string(); - } - } - return Some(soar_env); - } - } - None -} - -#[tokio::main] -async fn main() { - let cli = Cli::parse(); - - let result = match cli.command { - Commands::Build(args) => commands::build::run(args, get_soar_env()).await, - Commands::Info(args) => commands::info::run(args).await, - Commands::Cache(args) => commands::cache::run(args).await.map_err(|e| e.to_string()), - Commands::Lint(args) => commands::lint::run(args), - Commands::Meta(args) => commands::meta::run(args).await.map_err(|e| e.to_string()), - }; - - if let Err(e) = result { - eprintln!("{}: {}", "Error".bright_red(), e); - std::process::exit(1); - } -} diff --git a/sbuild/src/onelf.rs b/sbuild/src/onelf.rs deleted file mode 100644 index ec991d9..0000000 --- a/sbuild/src/onelf.rs +++ /dev/null @@ -1,207 +0,0 @@ -//! Reading icons and desktop files out of onelf-packed binaries. -//! -//! An onelf file is a self-extracting ELF with a trailing 76-byte footer -//! pointing at a zstd-compressed manifest and a block-compressed payload. This -//! module reads just enough of that format (via the published `onelf-format` -//! type definitions) to pull out the bundled icon and `.desktop` file, which -//! live under the `.onelf/` metadata convention: -//! -//! - icons: `.onelf/icons/{entrypoint}.svg`, `{entrypoint}.png`, -//! `default.svg`, `default.png` -//! - desktop: `.onelf/desktop/{entrypoint}.desktop`, `default.desktop` -//! -//! See the onelf file-format reference for the on-disk layout. - -use std::{ - fs::{self, File}, - io::{self, Cursor, Read, Seek, SeekFrom}, - path::Path, -}; - -use onelf_format::{Entry, EntryKind, Footer, Manifest, FOOTER_SIZE}; - -/// A parsed onelf package, holding its footer and manifest plus the path to -/// the backing file so individual entries can be decompressed on demand. -pub struct OnelfPackage { - file: File, - footer: Footer, - manifest: Manifest, - dict: Option>, -} - -impl OnelfPackage { - /// Parse the footer and manifest of an onelf-packed binary. - pub fn open>(path: P) -> io::Result { - let mut file = File::open(path)?; - let file_size = file.metadata()?.len(); - if file_size < FOOTER_SIZE as u64 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "file too small for onelf footer", - )); - } - - file.seek(SeekFrom::End(-(FOOTER_SIZE as i64)))?; - let mut footer_buf = [0u8; FOOTER_SIZE]; - file.read_exact(&mut footer_buf)?; - let footer = Footer::from_bytes(&footer_buf)?; - - file.seek(SeekFrom::Start(footer.manifest_offset))?; - let mut compressed = vec![0u8; footer.manifest_compressed as usize]; - file.read_exact(&mut compressed)?; - let manifest_bytes = zstd::bulk::decompress(&compressed, footer.manifest_original as usize) - .map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("manifest decompression failed: {e}"), - ) - })?; - let manifest = Manifest::deserialize(&manifest_bytes)?; - - let dict = if footer.dict_size > 0 { - file.seek(SeekFrom::Start(footer.dict_offset))?; - let mut buf = vec![0u8; footer.dict_size as usize]; - file.read_exact(&mut buf)?; - Some(buf) - } else { - None - }; - - Ok(Self { - file, - footer, - manifest, - dict, - }) - } - - /// Find the bundled icon entry for any of the given entrypoint names, - /// falling back to the package defaults. Returns the entry index and the - /// file extension implied by the resolved path (`svg` or `png`). - fn resolve_icon(&self, entrypoints: &[&str]) -> Option<(usize, &'static str)> { - for ep in entrypoints { - if let Some(idx) = self.find_file(&format!(".onelf/icons/{ep}.svg")) { - return Some((idx, "svg")); - } - if let Some(idx) = self.find_file(&format!(".onelf/icons/{ep}.png")) { - return Some((idx, "png")); - } - } - if let Some(idx) = self.find_file(".onelf/icons/default.svg") { - return Some((idx, "svg")); - } - if let Some(idx) = self.find_file(".onelf/icons/default.png") { - return Some((idx, "png")); - } - None - } - - /// Find the bundled `.desktop` entry for any of the given entrypoint names, - /// falling back to the package default. - fn resolve_desktop(&self, entrypoints: &[&str]) -> Option { - for ep in entrypoints { - if let Some(idx) = self.find_file(&format!(".onelf/desktop/{ep}.desktop")) { - return Some(idx); - } - } - self.find_file(".onelf/desktop/default.desktop") - } - - /// Name of the package's default entrypoint, if any. - fn default_entrypoint(&self) -> Option<&str> { - let idx = self.manifest.header.default_entrypoint as usize; - self.manifest - .entrypoints - .get(idx) - .map(|ep| self.manifest.get_string(ep.name)) - } - - fn find_file(&self, path: &str) -> Option { - self.manifest - .entries - .iter() - .enumerate() - .filter(|(_, e)| e.kind == EntryKind::File) - .find(|(i, _)| self.manifest.entry_path(*i) == path) - .map(|(i, _)| i) - } - - /// Decompress and concatenate an entry's payload blocks into its full - /// file content. - fn read_entry(&mut self, entry: &Entry) -> io::Result> { - let mut result = Vec::new(); - for block in &entry.blocks { - self.file.seek(SeekFrom::Start( - self.footer.payload_offset + block.payload_offset, - ))?; - let mut compressed = vec![0u8; block.compressed_size as usize]; - self.file.read_exact(&mut compressed)?; - - // Stored mode: payload bytes are the file content verbatim. - if self.footer.is_stored() { - result.extend_from_slice(&compressed); - continue; - } - - let decompressed = if let Some(dict) = &self.dict { - let mut decoder = zstd::Decoder::with_dictionary(Cursor::new(&compressed), dict)?; - let mut buf = Vec::with_capacity(block.original_size as usize); - decoder.read_to_end(&mut buf)?; - buf - } else { - zstd::bulk::decompress(&compressed, block.original_size as usize).map_err(|e| { - io::Error::new( - io::ErrorKind::InvalidData, - format!("block decompression failed: {e}"), - ) - })? - }; - result.extend_from_slice(&decompressed); - } - Ok(result) - } - - /// Extract the bundled icon to `dest`, matching the icon to `cmd` (or the - /// package default). Returns the destination path on success, or `None` - /// when the package ships no icon. - pub fn extract_icon>(&mut self, cmd: &str, dest: P) -> io::Result> { - let default_ep = self.default_entrypoint().map(str::to_owned); - let mut names = vec![cmd]; - if let Some(ep) = &default_ep { - if ep != cmd { - names.push(ep); - } - } - let Some((idx, _ext)) = self.resolve_icon(&names) else { - return Ok(None); - }; - let entry = self.manifest.entries[idx].clone(); - let data = self.read_entry(&entry)?; - fs::write(dest, data)?; - Ok(Some(())) - } - - /// Extract the bundled `.desktop` file to `dest`, matching `cmd` (or the - /// package default). Returns the destination path on success, or `None` - /// when the package ships no desktop file. - pub fn extract_desktop>( - &mut self, - cmd: &str, - dest: P, - ) -> io::Result> { - let default_ep = self.default_entrypoint().map(str::to_owned); - let mut names = vec![cmd]; - if let Some(ep) = &default_ep { - if ep != cmd { - names.push(ep); - } - } - let Some(idx) = self.resolve_desktop(&names) else { - return Ok(None); - }; - let entry = self.manifest.entries[idx].clone(); - let data = self.read_entry(&entry)?; - fs::write(dest, data)?; - Ok(Some(())) - } -} diff --git a/sbuild/src/signing.rs b/sbuild/src/signing.rs deleted file mode 100644 index 93cf3ed..0000000 --- a/sbuild/src/signing.rs +++ /dev/null @@ -1,181 +0,0 @@ -//! Package signing utilities using minisign -//! -//! Provides functions to sign build artifacts with minisign. - -use std::io::Write; -use std::path::Path; -use std::process::{Command, Stdio}; - -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum SignError { - #[error("minisign not found - install minisign to sign packages")] - MinisignNotFound, - - #[error("minisign key not found or invalid")] - KeyNotFound, - - #[error("signing failed: {0}")] - SignFailed(String), - - #[error("IO error: {0}")] - Io(#[from] std::io::Error), -} - -/// Minisign signer for package artifacts -pub struct Signer { - key_path: Option, - key_data: Option, - password: Option, -} - -impl Signer { - /// Create a new signer with key file path - pub fn with_key_file>(path: P) -> Self { - Self { - key_path: Some(path.as_ref().to_string_lossy().to_string()), - key_data: None, - password: None, - } - } - - /// Create a new signer with key data (for CI environments) - pub fn with_key_data(key: String) -> Self { - Self { - key_path: None, - key_data: Some(key), - password: None, - } - } - - /// Set the password for the private key - pub fn with_password(mut self, password: Option) -> Self { - self.password = password; - self - } - - /// Check if minisign is available - pub fn check_minisign() -> Result<(), SignError> { - if which::which("minisign").is_err() { - return Err(SignError::MinisignNotFound); - } - Ok(()) - } - - /// Sign a file, creating a .sig file alongside it - pub fn sign>(&self, file: P) -> Result<(), SignError> { - let file_path = file.as_ref(); - - // Prepare key file if using key data - let temp_key = if let Some(ref key_data) = self.key_data { - let temp_path = std::env::temp_dir().join("minisign_key.tmp"); - std::fs::write(&temp_path, key_data)?; - Some(temp_path) - } else { - None - }; - - let key_path = temp_key - .as_ref() - .map(|p| p.to_string_lossy().to_string()) - .or_else(|| self.key_path.clone()) - .ok_or(SignError::KeyNotFound)?; - - let mut child = Command::new("minisign") - .args([ - "-S", // Sign - "-s", - &key_path, // Secret key - "-m", - &file_path.to_string_lossy(), // File to sign - "-x", - &format!("{}.sig", file_path.display()), // Output signature - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; - - // Write password to stdin if provided - if let Some(ref password) = self.password { - if let Some(mut stdin) = child.stdin.take() { - writeln!(stdin, "{}", password)?; - } - } - - let output = child.wait_with_output()?; - - // Clean up temp key - if let Some(temp_path) = temp_key { - std::fs::remove_file(temp_path).ok(); - } - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(SignError::SignFailed(stderr.to_string())); - } - - Ok(()) - } - - /// Sign all files in a directory (recursively) - pub fn sign_directory>(&self, dir: P) -> Result, SignError> { - let dir = dir.as_ref(); - let mut signed = Vec::new(); - - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - // Recursively sign subdirectories - signed.extend(self.sign_directory(&path)?); - } else if path.is_file() { - let filename = path.file_name().unwrap_or_default().to_string_lossy(); - - // Skip signature files and checksums - if filename.ends_with(".sig") - || filename.ends_with(".b3sum") - || filename.ends_with(".sha256") - || filename == "CHECKSUM" - { - continue; - } - - self.sign(&path)?; - signed.push(path.to_string_lossy().to_string()); - } - } - - Ok(signed) - } -} - -/// Verify a signature -pub fn verify>(file: P, pubkey: &str) -> Result { - Signer::check_minisign()?; - - let file_path = file.as_ref(); - let sig_path = format!("{}.sig", file_path.display()); - - // Write pubkey to temp file - let temp_pub = std::env::temp_dir().join("minisign_pub.tmp"); - std::fs::write(&temp_pub, pubkey)?; - - let output = Command::new("minisign") - .args([ - "-V", // Verify - "-p", - &temp_pub.to_string_lossy(), - "-m", - &file_path.to_string_lossy(), - "-x", - &sig_path, - ]) - .output()?; - - std::fs::remove_file(temp_pub).ok(); - - Ok(output.status.success()) -} diff --git a/sbuild/src/types.rs b/sbuild/src/types.rs deleted file mode 100644 index 84abb07..0000000 --- a/sbuild/src/types.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::fmt::Display; - -pub enum OutputStream { - Stdout(String), - Stderr(String), -} - -#[derive(Clone, PartialEq, Eq)] -pub enum PackageType { - Static, - Dynamic, - AppImage, - FlatImage, - NixAppImage, - Onelf, - Unknown, -} - -impl Display for PackageType { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PackageType::Static => write!(f, "static"), - PackageType::Dynamic => write!(f, "dynamic"), - PackageType::AppImage => write!(f, "appimage"), - PackageType::NixAppImage => write!(f, "nixappimage"), - PackageType::FlatImage => write!(f, "flatimage"), - PackageType::Onelf => write!(f, "onelf"), - PackageType::Unknown => write!(f, "unknown"), - } - } -} - -#[derive(Default, Clone)] -pub struct SoarEnv { - pub bin_path: String, - pub cache_path: String, -} diff --git a/sbuild/src/utils.rs b/sbuild/src/utils.rs deleted file mode 100644 index 7c439a8..0000000 --- a/sbuild/src/utils.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::{ - env, - fs::{self, File, OpenOptions}, - io::{BufReader, Read, Seek, SeekFrom, Write}, - path::{Path, PathBuf}, - process::{Command, Stdio}, - time::{SystemTime, UNIX_EPOCH}, -}; - -use futures::StreamExt; -use glob::glob; -use goblin::elf::Elf; -use memmap2::Mmap; -use reqwest::header::USER_AGENT; -use sbuild_linter::logger::TaskLogger; - -pub async fn download>(url: &str, out: P) -> Result<(), String> { - let client = reqwest::Client::new(); - let response = client - .get(url) - .header(USER_AGENT, "pkgforge/soar") - .send() - .await - .unwrap(); - - if !response.status().is_success() { - return Err(format!("Error downloading {}", url)); - } - - let output_path = out.as_ref(); - if let Some(output_dir) = output_path.parent() { - if !output_dir.exists() { - fs::create_dir_all(output_dir).unwrap(); - } - } - - let temp_path = format!("{}.part", output_path.display()); - let mut stream = response.bytes_stream(); - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(&temp_path) - .unwrap(); - - while let Some(chunk) = stream.next().await { - let chunk = chunk.unwrap(); - file.write_all(&chunk).unwrap(); - } - - fs::rename(&temp_path, output_path).unwrap(); - - Ok(()) -} - -pub fn extract_filename(url: &str) -> String { - Path::new(url) - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_else(|| { - let dt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis(); - dt.to_string() - }) -} - -pub fn temp_file(pkg_id: &str, script: &str) -> PathBuf { - let tmp_dir = env::temp_dir(); - let tmp_file_path = tmp_dir.join(format!("sbuild-{}", pkg_id)); - { - let mut tmp_file = - File::create(&tmp_file_path).expect("Failed to create temporary script file"); - tmp_file - .write_all(script.as_bytes()) - .expect("Failed to write to temporary script file"); - - use std::os::unix::fs::PermissionsExt; - let mut perms = fs::metadata(&tmp_file_path) - .expect("Failed to read file metadata") - .permissions(); - perms.set_mode(0o755); - fs::set_permissions(&tmp_file_path, perms).expect("Failed to set executable permissions"); - } - tmp_file_path -} - -pub fn calc_magic_bytes>(file_path: P, size: usize) -> Vec { - let file = File::open(file_path).unwrap(); - let mut file = BufReader::new(file); - let mut magic_bytes = vec![0u8; size]; - if file.read_exact(&mut magic_bytes).is_ok() { - file.rewind().unwrap(); - return magic_bytes; - }; - file.rewind().unwrap(); - magic_bytes -} - -pub fn pack_appimage>( - env_vars: Vec<(String, String)>, - path: P, - output_path: P, - logger: &TaskLogger, -) -> bool { - let Ok(aitool) = which::which("appimagetool") else { - logger.warn("appimagetool not found."); - return false; - }; - - let mut child = Command::new(aitool) - .env_clear() - .envs(env_vars) - .args([ - "--comp", - "zstd", - "--mksquashfs-opt", - "-root-owned", - "--mksquashfs-opt", - "-no-xattrs", - "--mksquashfs-opt", - "-noappend", - "--mksquashfs-opt", - "-b", - "--mksquashfs-opt", - "1M", - "--mksquashfs-opt", - "-mkfs-time", - "--mksquashfs-opt", - "0", - "--mksquashfs-opt", - "-Xcompression-level", - "--mksquashfs-opt", - "22", - "--no-appstream", - &path.as_ref().to_string_lossy().to_string(), - &output_path.as_ref().to_string_lossy().to_string(), - ]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .unwrap(); - - let _ = child.wait().unwrap(); - true -} - -pub fn self_extract_appimage(cmd: &str, mut pattern: String, dest: &str) { - for _ in 0..10 { - let mut child = Command::new(format!("./{}", cmd)) - .env_clear() - .args(["--appimage-extract", pattern.as_ref()]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .stdin(Stdio::null()) - .spawn() - .unwrap(); - - let result = child.wait().unwrap(); - if result.success() { - let search_pattern = format!("squashfs-root/{}", pattern); - for entry in glob(&search_pattern).unwrap().filter_map(Result::ok) { - fs::rename(&entry, dest).unwrap(); - break; - } - } - - if let Ok(link) = fs::read_link(dest) { - pattern = link - .to_string_lossy() - .into_owned() - .trim_start_matches("./") - .to_string(); - continue; - } - - break; - } -} - -/// Detect an onelf-packed binary by its trailing footer magic. -/// -/// onelf files start with an ELF runtime stub, so they cannot be distinguished -/// from a plain static ELF by leading magic bytes. Instead, the last -/// `ONELF_FOOTER_SIZE` bytes hold a fixed footer whose first 8 bytes are -/// `ONELF_MAGIC_BYTES`. -pub fn is_onelf>(file_path: P) -> bool { - use crate::constant::{ONELF_FOOTER_SIZE, ONELF_MAGIC_BYTES}; - - let Ok(mut file) = File::open(&file_path) else { - return false; - }; - let Ok(size) = file.metadata().map(|m| m.len()) else { - return false; - }; - if size < ONELF_FOOTER_SIZE { - return false; - } - if file - .seek(SeekFrom::Start(size - ONELF_FOOTER_SIZE)) - .is_err() - { - return false; - } - let mut magic = [0u8; 8]; - if file.read_exact(&mut magic).is_err() { - return false; - } - magic == ONELF_MAGIC_BYTES -} - -pub fn is_static_elf>(file_path: P) -> bool { - let file = File::open(&file_path).unwrap(); - let mmap = unsafe { Mmap::map(&file).unwrap() }; - let elf = Elf::parse(&mmap).unwrap(); - elf.interpreter.is_none() -} - -pub fn expand_env_vars(input: &str, vars: &[(String, String)]) -> String { - let mut result = input.to_string(); - for (key, value) in vars { - result = result.replace(&format!("${{{}}}", key), value); - } - result -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::constant::{ONELF_FOOTER_SIZE, ONELF_MAGIC_BYTES}; - use tempfile::NamedTempFile; - - #[test] - fn detects_onelf_footer_magic() { - let mut file = NamedTempFile::new().unwrap(); - // ELF-looking prefix so leading magic alone wouldn't distinguish it. - file.write_all(&[0x7f, 0x45, 0x4c, 0x46]).unwrap(); - // A 76-byte footer beginning with the onelf magic. - let mut footer = vec![0u8; ONELF_FOOTER_SIZE as usize]; - footer[..ONELF_MAGIC_BYTES.len()].copy_from_slice(&ONELF_MAGIC_BYTES); - file.write_all(&footer).unwrap(); - file.flush().unwrap(); - - assert!(is_onelf(file.path())); - } - - #[test] - fn plain_elf_is_not_onelf() { - let mut file = NamedTempFile::new().unwrap(); - file.write_all(&[0x7f, 0x45, 0x4c, 0x46]).unwrap(); - file.write_all(&vec![0u8; ONELF_FOOTER_SIZE as usize]) - .unwrap(); - file.flush().unwrap(); - - assert!(!is_onelf(file.path())); - } - - #[test] - fn too_small_file_is_not_onelf() { - let mut file = NamedTempFile::new().unwrap(); - file.write_all(&ONELF_MAGIC_BYTES).unwrap(); - file.flush().unwrap(); - - assert!(!is_onelf(file.path())); - } -} diff --git a/sbuild/tests/onelf_extract.rs b/sbuild/tests/onelf_extract.rs deleted file mode 100644 index 4f820ab..0000000 --- a/sbuild/tests/onelf_extract.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! End-to-end extraction test against a real onelf package. -//! -//! Ignored by default; run with the path to a packed `.onelf` file: -//! -//! ```sh -//! ONELF_TEST_FILE=/path/to/myapp.onelf cargo test -p sbuild --test onelf_extract -- --ignored --nocapture -//! ``` - -use std::env; -use std::path::Path; - -use sbuild::onelf::OnelfPackage; - -#[test] -#[ignore] -fn extracts_icon_and_desktop() { - let path = env::var("ONELF_TEST_FILE").expect("set ONELF_TEST_FILE"); - let cmd = env::var("ONELF_TEST_CMD").unwrap_or_else(|_| "myapp".to_string()); - let dir = tempfile::tempdir().unwrap(); - - let mut pkg = OnelfPackage::open(&path).expect("open onelf"); - - let icon_dest = dir.path().join("out.icon"); - let got = pkg.extract_icon(&cmd, &icon_dest).expect("extract icon"); - assert!(got.is_some(), "expected an icon to be extracted"); - let icon = std::fs::read(&icon_dest).unwrap(); - assert!(!icon.is_empty(), "icon should not be empty"); - eprintln!("icon: {} bytes", icon.len()); - - let desktop_dest = dir.path().join("out.desktop"); - let got = pkg - .extract_desktop(&cmd, &desktop_dest) - .expect("extract desktop"); - assert!(got.is_some(), "expected a desktop file to be extracted"); - let desktop = std::fs::read_to_string(&desktop_dest).unwrap(); - assert!( - desktop.contains("[Desktop Entry]"), - "desktop file should be a real .desktop, got: {desktop}" - ); - eprintln!("desktop:\n{desktop}"); -} - -/// Mirrors the builder's dest-dir computation: for a sub-package binary at -/// `packages//`, extracted assets must land in that same dir, -/// not the root. -#[test] -#[ignore] -fn extracts_next_to_subpackage_binary() { - let path = env::var("ONELF_TEST_FILE").expect("set ONELF_TEST_FILE"); - let cmd = env::var("ONELF_TEST_CMD").unwrap_or_else(|_| "amdgpu_top".to_string()); - let provide_path = Path::new(&path); - let dest_dir = provide_path.parent().unwrap_or_else(|| Path::new("")); - - let mut pkg = OnelfPackage::open(provide_path).expect("open onelf"); - - let desktop_dest = dest_dir.join(format!("{cmd}.desktop")); - let got = pkg - .extract_desktop(&cmd, &desktop_dest) - .expect("extract desktop"); - assert!(got.is_some(), "expected a desktop file"); - assert!( - desktop_dest.exists(), - "desktop should be written beside the binary at {}", - desktop_dest.display() - ); - eprintln!("desktop written to: {}", desktop_dest.display()); -} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..783e99b --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod port; diff --git a/src/commands/port.rs b/src/commands/port.rs new file mode 100644 index 0000000..9fa0074 --- /dev/null +++ b/src/commands/port.rs @@ -0,0 +1,301 @@ +use std::path::PathBuf; + +use clap::Subcommand; +use colored::Colorize; + +use futures::StreamExt; +use sbuild::port::{audit, hashfill, meta, new, resolve, tree, validate}; + +#[derive(Subcommand)] +pub enum PortCommands { + /// Check the tree; every pinned URL must have a hash beside it + Validate { + #[arg(default_value = ".")] + root: PathBuf, + }, + /// Scaffold a new package + New { + /// Package name + name: String, + /// Upstream repository, e.g. pkgforge-dev/Foo-AppImage + repo: String, + #[arg(long, default_value = "appimage")] + r#type: String, + #[arg(long)] + description: Option, + #[arg(long)] + maintainer: Option, + /// Upstream tags carry a build suffix like 1.2.3@2026-01-01_1234 + #[arg(long)] + tag_suffix_strip: bool, + #[arg(long, default_value = ".")] + root: PathBuf, + }, + /// Pin the current upstream version of every package + Resolve { + #[arg(default_value = ".")] + root: PathBuf, + /// Restrict to these packages + packages: Vec, + #[arg(long, env = "GITHUB_TOKEN", default_value = "")] + github_token: String, + }, + /// Hash any pinned artifact the forge API had no digest for + Hashfill { + #[arg(default_value = ".")] + root: PathBuf, + /// Concurrent downloads + #[arg(long, default_value = "12")] + jobs: usize, + }, + /// Download each archive and check every install path resolves in it + Audit { + #[arg(default_value = ".")] + root: PathBuf, + /// Restrict to these packages + packages: Vec, + /// Hosts to audit; repeat the flag for more than one + #[arg(long, default_values_t = [String::from("x86_64-linux")])] + host: Vec, + #[arg(long, default_value = "10")] + jobs: usize, + }, + /// Generate the soar metadata index for one host + Meta { + #[arg(default_value = ".")] + root: PathBuf, + #[arg(short, long)] + arch: String, + #[arg(short, long)] + output: PathBuf, + }, +} + +pub async fn run(command: PortCommands) -> Result<(), String> { + match command { + PortCommands::Validate { root } => { + let r = validate::run(&root); + println!("checked {} pinned artifacts across {} packages", r.checked, r.packages); + for w in &r.warnings { + println!(" {} {w}", "warn".yellow()); + } + for e in &r.errors { + println!(" {} {e}", "ERROR".red()); + } + println!("\n{} errors, {} warnings", r.errors.len(), r.warnings.len()); + if r.errors.is_empty() { + Ok(()) + } else { + Err(format!("{} validation errors", r.errors.len())) + } + } + PortCommands::New { name, repo, r#type, description, maintainer, tag_suffix_strip, root } => { + let scaffold = new::Scaffold { + name: &name, + repo: &repo, + kind: new::Kind::parse(&r#type)?, + description: description.as_deref(), + maintainer: maintainer.as_deref(), + tag_suffix_strip, + }; + let path = new::write(&root, &scaffold)?; + println!("wrote {}", path.display()); + println!("next: sbuild port resolve {} {name}", root.display()); + Ok(()) + } + PortCommands::Resolve { root, packages, github_token } => { + let client = reqwest::Client::builder() + .user_agent("sbuild-port") + .build() + .map_err(|e| e.to_string())?; + let (pkgs, errs) = tree::load(&root); + for e in &errs { + eprintln!(" {} {e}", "warn".yellow()); + } + let (mut ok, mut partial, mut failed, mut skipped) = (0, 0, 0, 0); + for p in &pkgs { + let name = p.pkg.family().to_string(); + if !packages.is_empty() && !packages.contains(&name) { + continue; + } + if p.pkg.pkg.disabled || p.pkg.update.is_none() || p.pkg.source.is_none() { + skipped += 1; + continue; + } + match resolve::resolve(&client, &p.pkg, &github_token).await { + Ok(mut r) => { + let dest = p.dir.join(format!("{}-{}.toml", name, r.version)); + if let Ok(prev) = std::fs::read_to_string(&dest) { + if let Ok(v) = toml::from_str(&prev) { + resolve::carry_forward(&mut r, &v); + } + } + std::fs::write(&dest, resolve::render(&r)).map_err(|e| e.to_string())?; + if r.missing.is_empty() { + ok += 1; + } else { + partial += 1; + } + } + Err(e) => { + failed += 1; + eprintln!(" {} {name}: {e}", "FAIL".red()); + } + } + } + println!("fully pinned {ok} | partial {partial} | skipped {skipped} | failed {failed}"); + Ok(()) + } + PortCommands::Hashfill { root, jobs } => { + let client = reqwest::Client::builder() + .user_agent("sbuild-port") + .build() + .map_err(|e| e.to_string())?; + let gaps = hashfill::gaps(&root); + if gaps.is_empty() { + println!("no artifact hashes missing"); + } + let total = gaps.len(); + if total > 0 { + println!("hashing {total} artifacts with {jobs} concurrent downloads ..."); + } + let mut by_file: std::collections::BTreeMap> = + Default::default(); + let mut failed = 0; + let mut done = 0; + + // Artifacts are streamed and discarded, so concurrency costs + // bandwidth but not memory or disk. + let mut stream = futures::stream::iter(gaps.iter().map(|g| { + let client = client.clone(); + async move { (g, hashfill::digests(&client, &g.url).await) } + })) + .buffer_unordered(jobs); + + // Merge as each artifact lands rather than at the end. A full + // run is tens of gigabytes, and hashfill skips anything that + // already has blake3, so writing incrementally makes it resumable. + while let Some((g, res)) = stream.next().await { + done += 1; + match res { + Ok((b3, sha, n)) => { + let mut one = std::collections::BTreeMap::new(); + one.insert(g.host.clone(), (b3, sha, n)); + if let Err(e) = hashfill::merge(&g.path, &one) { + failed += 1; + eprintln!(" {} {}: {e}", "WRITE".red(), g.path.display()); + } else { + by_file.insert(g.path.clone(), Default::default()); + } + } + Err(e) => { + failed += 1; + eprintln!(" {} {}: {e}", "FAIL".red(), g.url); + } + } + if done % 25 == 0 { + eprintln!(" ... {done}/{total} ({failed} failed)"); + } + } + if total > 0 { + println!("filled {} hashes across {} files ({failed} failed)", + total - failed, by_file.len()); + } + + // Side files are pinned per version too: most point at a branch, + // so their content can change with no version bump. + let egaps = hashfill::extra_gaps(&root); + if !egaps.is_empty() { + println!("hashing {} side files ...", egaps.len()); + let mut per_file: std::collections::BTreeMap> = + Default::default(); + let mut efailed = 0; + let mut estream = futures::stream::iter(egaps.iter().map(|g| { + let client = client.clone(); + async move { + let res = match &g.local { + Some(p) => hashfill::digests_local(p), + None => hashfill::digests(&client, &g.url).await, + }; + (g, res) + } + })) + .buffer_unordered(jobs); + let mut edone = 0; + while let Some((g, res)) = estream.next().await { + edone += 1; + match res { + Ok((b3, sha, _)) => per_file + .entry(g.path.clone()) + .or_default() + .push((g.url.clone(), g.to.clone(), b3, sha)), + Err(e) => { + efailed += 1; + eprintln!(" {} {}: {e}", "FAIL".red(), g.url); + } + } + if edone % 50 == 0 { + eprintln!(" ... {edone}/{}", egaps.len()); + } + } + for (path, items) in &per_file { + hashfill::merge_extras(path, items)?; + } + println!("pinned {} side files across {} versions ({efailed} failed)", + egaps.len() - efailed, per_file.len()); + } + Ok(()) + } + PortCommands::Audit { root, packages, host, jobs } => { + let findings = audit::run(&root, &host, &packages, jobs).await?; + let mut ok = 0; + let mut unlistable = Vec::new(); + let mut problems = Vec::new(); + for f in &findings { + match &f.outcome { + audit::Outcome::Ok => ok += 1, + audit::Outcome::Unlistable => unlistable.push(f), + _ => problems.push(f), + } + } + println!("\n{ok}/{} verified against real archive contents", findings.len()); + if !unlistable.is_empty() { + // Single-file compression carries no member list, so these + // are unchecked rather than wrong. + println!("\n{} unlistable (single-file compression):", unlistable.len()); + for f in &unlistable { + println!(" {} ({})", f.package, f.host); + } + } + for f in &problems { + match &f.outcome { + audit::Outcome::Missing(m) => { + println!(" {} {} ({})", "MISSING".red(), f.package, f.host); + for x in m { + println!(" no archive member matches: {x}"); + } + } + audit::Outcome::Failed(e) => { + println!(" {} {} ({}): {e}", "FAILED".red(), f.package, f.host) + } + _ => {} + } + } + if problems.is_empty() { + Ok(()) + } else { + Err(format!("{} packages have unresolvable install paths", problems.len())) + } + } + PortCommands::Meta { root, arch, output } => { + let (entries, errors) = meta::generate(&root, &arch); + for e in &errors { + eprintln!(" {} {e}", "warn".yellow()); + } + let json = serde_json::to_string_pretty(&entries).map_err(|e| e.to_string())?; + std::fs::write(&output, json).map_err(|e| e.to_string())?; + println!("wrote {} entries -> {}", entries.len(), output.display()); + Ok(()) + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4d43eeb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +//! Tooling for the soarpkgs package format. +//! +//! Packages are declarative and hash-pinned: a client resolves one by parsing +//! alone, so the hash can live in the repository rather than being measured +//! after the artifact is built. + +pub mod port; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6d35f1c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +mod commands; + +use clap::{Parser, Subcommand}; +use colored::Colorize; + +#[derive(Parser)] +#[command(name = "sbuild")] +#[command(about = "Tooling for the soarpkgs package format", long_about = None)] +#[command(version)] +struct Cli { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + #[command(flatten)] + Port(commands::port::PortCommands), +} + +#[tokio::main] +async fn main() { + let cli = Cli::parse(); + + let result = match cli.command { + Commands::Port(args) => commands::port::run(args).await, + }; + + if let Err(e) = result { + eprintln!("{}: {}", "Error".bright_red(), e); + std::process::exit(1); + } +} diff --git a/src/port/audit.rs b/src/port/audit.rs new file mode 100644 index 0000000..699e22e --- /dev/null +++ b/src/port/audit.rs @@ -0,0 +1,198 @@ +//! Verify that every install-map path resolves inside its real archive. +//! +//! Static checks cannot catch a path that simply is not in the tarball, which +//! is the failure that ships a broken package: the artifact downloads, its +//! hash verifies, and the file the map points at does not exist. The only way +//! to know is to fetch the archive and list it. +//! +//! Listing shells out to `tar` and `unzip` rather than linking a decompressor +//! for every format upstreams use. This is a maintainer tool, never something +//! a client runs. + +use std::{path::Path, process::Command}; + +use futures::StreamExt; + +use crate::port::{resolve::expand, tree}; + +/// What the audit concluded about one package on one host. +#[derive(Debug)] +pub enum Outcome { + /// Every install path matched at least one archive member. + Ok, + /// Paths that matched nothing. These ship a broken package. + Missing(Vec), + /// Single-file compression (`.gz`, `.bz2`) has no member list, so the + /// install path cannot be checked this way. + Unlistable, + Failed(String), +} + +#[derive(Debug)] +pub struct Finding { + pub package: String, + pub host: String, + pub outcome: Outcome, +} + +impl Finding { + pub fn is_problem(&self) -> bool { + matches!(self.outcome, Outcome::Missing(_) | Outcome::Failed(_)) + } +} + +/// One unit of work: a package's artifact for a single host. +struct Job { + package: String, + host: String, + url: String, + version: String, + arch: String, + install: Vec, +} + +fn list_members(path: &Path) -> Option> { + // GNU tar auto-detects gz/xz/bz2/zst, so try it first and fall back to + // zip; `file` mislabels some zips (OOXML), so never trust it to choose. + for (cmd, args) in [("tar", vec!["tf"]), ("unzip", vec!["-Z1"])] { + let out = Command::new(cmd) + .args(&args) + .arg(path) + .output() + .ok()?; + if out.status.success() { + let members: Vec = String::from_utf8_lossy(&out.stdout) + .lines() + .filter(|l| !l.trim().is_empty()) + .map(|l| l.to_string()) + .collect(); + if !members.is_empty() { + return Some(members); + } + } + } + None +} + +fn matches(pattern: &str, members: &[String]) -> bool { + let pat = glob::Pattern::new(pattern).ok(); + members.iter().any(|m| { + m == pattern + || m.trim_end_matches('/') == pattern + || m.ends_with(&format!("/{pattern}")) + || pat.as_ref().is_some_and(|p| p.matches(m)) + }) +} + +async fn audit_one(client: &reqwest::Client, job: Job) -> Finding { + let mut tmp = match tempfile::NamedTempFile::new() { + Ok(t) => t, + Err(e) => { + return Finding { + package: job.package, + host: job.host, + outcome: Outcome::Failed(e.to_string()), + } + } + }; + + let fetched = async { + use std::io::Write; + let resp = client + .get(&job.url) + .header("User-Agent", "sbuild-audit") + .send() + .await + .map_err(|e| e.to_string())?; + if !resp.status().is_success() { + return Err(format!("http {}", resp.status().as_u16())); + } + let mut stream = resp.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| e.to_string())?; + tmp.as_file_mut().write_all(&chunk).map_err(|e| e.to_string())?; + } + tmp.as_file_mut().flush().map_err(|e| e.to_string()) + } + .await; + + if let Err(e) = fetched { + return Finding { package: job.package, host: job.host, outcome: Outcome::Failed(e) }; + } + + let outcome = match list_members(tmp.path()) { + None => Outcome::Unlistable, + Some(members) => { + let missing: Vec = job + .install + .iter() + .map(|k| expand(k, &job.version, &job.arch, &job.package)) + .filter(|p| !matches(p, &members)) + .collect(); + if missing.is_empty() { Outcome::Ok } else { Outcome::Missing(missing) } + } + }; + Finding { package: job.package, host: job.host, outcome } +} + +/// Audit every package with an install map, optionally restricted by name. +pub async fn run( + root: &Path, + hosts: &[String], + only: &[String], + jobs: usize, +) -> Result, String> { + let (packages, _) = tree::load(root); + let mut work = Vec::new(); + + for p in &packages { + let name = p.pkg.family().to_string(); + if p.pkg.pkg.disabled || (!only.is_empty() && !only.contains(&name)) { + continue; + } + let Some(src) = &p.pkg.source else { continue }; + // An install map is the only thing worth auditing. Whether the + // artifact is an archive is decided by listing it, not by a field: + // soar sniffs magic bytes, so nothing else needs to be told. + if src.install.is_empty() { + continue; + } + let Some(v) = p.versions.last() else { continue }; + for host in hosts { + let Some(url) = v.url.get(host) else { continue }; + let raw = host.split('-').next().unwrap_or(host); + work.push(Job { + package: name.clone(), + host: host.clone(), + url: url.clone(), + version: v.version.clone(), + arch: p.pkg.arch.get(raw).cloned().unwrap_or_else(|| raw.to_string()), + install: src.install.keys().cloned().collect(), + }); + } + } + + let client = reqwest::Client::builder() + .user_agent("sbuild-audit") + .build() + .map_err(|e| e.to_string())?; + + println!("auditing {} artifacts with {jobs} concurrent downloads ...", work.len()); + let total = work.len(); + let mut findings = Vec::new(); + let mut stream = futures::stream::iter( + work.into_iter().map(|j| audit_one(&client, j)), + ) + .buffer_unordered(jobs); + + let mut done = 0; + while let Some(f) = stream.next().await { + done += 1; + findings.push(f); + if done % 25 == 0 { + eprintln!(" ... {done}/{total}"); + } + } + findings.sort_by(|a, b| a.package.cmp(&b.package).then(a.host.cmp(&b.host))); + Ok(findings) +} diff --git a/src/port/hashfill.rs b/src/port/hashfill.rs new file mode 100644 index 0000000..f0d710c --- /dev/null +++ b/src/port/hashfill.rs @@ -0,0 +1,223 @@ +//! Hash artifacts the forge API could not vouch for. +//! +//! Releases predating GitHub's asset `digest` field, and non-GitHub sources +//! such as GitLab generic packages, have no reported hash. Those artifacts are +//! streamed once and hashed locally. Nothing is written to disk: a pinned URL +//! without a hash is the one state the tree must never be left in. + +use std::{collections::BTreeMap, fs, path::Path}; + +use futures::StreamExt; +use sha2::{Digest, Sha256}; + +/// A side file declared in pkg.toml that this version has not pinned yet. +pub struct ExtraGap { + pub path: std::path::PathBuf, + /// URL the client will fetch it from. + pub url: String, + pub to: String, + /// Set when the file is vendored here: hash it from disk instead of + /// fetching a copy of something we already have. + pub local: Option, +} + +/// Side files whose content is not yet pinned for a given version. +/// +/// Nearly all of these point at a branch rather than a tag, so the content can +/// change with no version bump. Pinning per version means a changed licence +/// shows up as a diff on the next bump rather than silently. +pub fn extra_gaps(root: &Path) -> Vec { + let (packages, _) = crate::port::tree::load(root); + let mut out = Vec::new(); + for p in packages { + if p.pkg.pkg.disabled || p.pkg.extra.is_empty() { + continue; + } + let mut paths: Vec<_> = fs::read_dir(&p.dir) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|q| { + q.extension().is_some_and(|x| x == "toml") + && q.file_name().is_some_and(|f| f != "pkg.toml") + }) + .collect(); + paths.sort(); + for (v, path) in p.versions.iter().zip(paths) { + for e in &p.pkg.extra { + let pinned = v + .extra + .iter() + .any(|x| x.to == e.to && x.blake3.is_some()); + if pinned { + continue; + } + // A vendored licence resolves to this repository rather than + // upstream, which is the point: some hosts rate-limit and some + // upstreams are gone. + let (url, local) = match (&e.url, &e.license) { + (Some(u), _) => (u.replace("${version}", &v.version), None), + (None, Some(spdx)) => ( + format!( + "https://raw.githubusercontent.com/pkgforge/soarpkgs/main/licenses/{spdx}.txt" + ), + Some(root.join("licenses").join(format!("{spdx}.txt"))), + ), + _ => continue, + }; + out.push(ExtraGap { path: path.clone(), url, to: e.to.clone(), local }); + } + } + } + out +} + +/// A version file with at least one unhashed URL. +pub struct Gap { + pub path: std::path::PathBuf, + pub host: String, + pub url: String, +} + +/// Find every pinned URL lacking a blake3 hash. +/// +/// blake3 is the digest soar verifies against, and it cannot be obtained from +/// a forge API, so any entry without one has to be downloaded. +pub fn gaps(root: &Path) -> Vec { + let (packages, _) = crate::port::tree::load(root); + let mut out = Vec::new(); + for p in packages { + let mut paths: Vec<_> = fs::read_dir(&p.dir) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|q| { + q.extension().is_some_and(|x| x == "toml") + && q.file_name().is_some_and(|f| f != "pkg.toml") + }) + .collect(); + paths.sort(); + for (v, path) in p.versions.iter().zip(paths) { + for (host, url) in &v.url { + if !v.blake3.contains_key(host) { + out.push(Gap { path: path.clone(), host: host.clone(), url: url.clone() }); + } + } + } + } + out +} + +/// Hash a file already on disk, for content vendored in this repository. +pub fn digests_local(path: &Path) -> Result<(String, String, u64), String> { + let data = fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + let mut sha = Sha256::new(); + sha.update(&data); + let sha_hex: String = sha.finalize().iter().map(|b| format!("{b:02x}")).collect(); + Ok(( + blake3::hash(&data).to_hex().to_string(), + sha_hex, + data.len() as u64, + )) +} + +/// Stream a URL once, returning (blake3, sha256, bytes). The body is +/// discarded, so memory and disk stay flat regardless of artifact size. +pub async fn digests(client: &reqwest::Client, url: &str) -> Result<(String, String, u64), String> { + let resp = client + .get(url) + .header("User-Agent", "sbuild-port") + .send() + .await + .map_err(|e| e.to_string())?; + if !resp.status().is_success() { + return Err(format!("http {}", resp.status().as_u16())); + } + let mut sha = Sha256::new(); + let mut b3 = blake3::Hasher::new(); + let mut n = 0u64; + let mut stream = resp.bytes_stream(); + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| e.to_string())?; + n += chunk.len() as u64; + sha.update(&chunk); + b3.update(&chunk); + } + let sha_hex: String = sha.finalize().iter().map(|b| format!("{b:02x}")).collect(); + Ok((b3.finalize().to_hex().to_string(), sha_hex, n)) +} + +/// Append resolved side files to a version file. +pub fn merge_extras( + path: &Path, + new: &[(String, String, String, String)], +) -> Result<(), String> { + let raw = fs::read_to_string(path).map_err(|e| e.to_string())?; + let mut s = raw.trim_end().to_string(); + for (url, to, b3, sha) in new { + s.push_str(&format!( + "\n\n[[extra]]\nurl = {url:?}\nto = {to:?}\nblake3 = {b3:?}\nsha256 = {sha:?}" + )); + } + s.push('\n'); + fs::write(path, s).map_err(|e| e.to_string()) +} + +/// Merge freshly computed hashes and sizes into a version file, preserving +/// everything above the digest tables. +pub fn merge(path: &Path, new: &BTreeMap) -> Result<(), String> { + let raw = fs::read_to_string(path).map_err(|e| e.to_string())?; + let v: crate::port::model::VersionToml = + toml::from_str(&raw).map_err(|e| e.to_string())?; + + let mut b3s = v.blake3.clone(); + let mut shas = v.sha256.clone(); + let mut sizes = v.size.clone(); + for (host, (b3, sha, size)) in new { + b3s.insert(host.clone(), b3.clone()); + shas.insert(host.clone(), sha.clone()); + sizes.insert(host.clone(), *size); + } + + let head = raw + .split("\n[blake3]") + .next() + .unwrap_or(&raw) + .split("\n[hash]") + .next() + .unwrap_or(&raw) + .split("\n[sha256]") + .next() + .unwrap_or(&raw) + .split("\n[size]") + .next() + .unwrap_or(&raw) + .trim_end() + .to_string(); + + let mut s = head; + if !b3s.is_empty() { + let w = b3s.keys().map(|k| k.len()).max().unwrap_or(0); + s.push_str("\n\n[blake3]\n"); + for (h, val) in &b3s { + s.push_str(&format!("{:, + pub pkg_type: Option, + pub description: Option, + pub version: String, + pub download_url: String, + /// Bytes. Clients format this for display; shipping a pre-formatted + /// string as well just meant two fields that could disagree. + pub size: Option, + pub src_url: Vec, + pub homepage: Vec, + pub license: Vec, + pub maintainer: Vec, + pub note: Vec, + pub category: Vec, + pub provides: Vec, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub repology: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub shasum: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bsum: Option, + /// Where each executable lives inside the artifact. Only emitted when a + /// file has to be renamed or is not at the archive root, since soar + /// otherwise finds it by package name. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub binaries: Vec, + /// Side files to install alongside the artifact, each pinned. Typically a + /// licence the artifact itself does not carry. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub extra: Vec, +} + +/// A pinned side file as published in the index. +#[derive(Debug, Clone, Serialize)] +pub struct ExtraFile { + pub url: String, + pub to: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub blake3: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sha256: Option, +} + +/// One executable inside the artifact, as published in the index. +#[derive(Debug, Clone, Serialize)] +pub struct Binary { + pub source: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub link_as: Option, +} + +/// Drop a leading archive-root component. +/// +/// Install paths are written against the archive as published, but soar +/// promotes a single top-level directory away before locating binaries. A +/// path with no directory component is already at the root. +fn strip_archive_root(path: &str) -> String { + match path.split_once('/') { + Some((_, rest)) if !rest.is_empty() => rest.to_string(), + _ => path.to_string(), + } +} + +/// Expand the two template variables an install path may carry. +fn expand_arch(s: &str, version: &str, arch: &str) -> String { + s.replace("${version}", version).replace("${arch}", arch) +} + +/// Rebuild the user-facing note list from the structured fields. +/// +/// Notes are presentation, so they are derived here rather than stored once +/// per package in the tree. +fn render_notes(p: &PkgToml, src: &str) -> Vec { + let explicit = &p.pkg.note; + let is_prov = |n: &String| n.starts_with("Official binary from") || n.starts_with("Fetched from"); + + // A package may carry its own provenance wording; it wins over the + // derived line and keeps the leading position. + let mut out: Vec = explicit.iter().filter(|n| is_prov(n)).cloned().collect(); + if out.is_empty() { + out.push(if p.pkg.kind.as_deref() == Some("appimage") { + format!("Fetched from Pre Built Community Created AppImage. Check/Report @ {src}") + } else { + format!("Official binary from {src}") + }); + } + + if p.pkg.portable { + let suffix = if p.pkg.kind.as_deref() == Some("appimage") { + "Works on AnyLinux" + } else { + "Portable Static Binary" + }; + out.push(format!("[PORTABLE] ({suffix})")); + } else { + out.push(match &p.pkg.portable_reason { + Some(why) => format!("[NOT PORTABLE] {why}"), + None => "[NOT PORTABLE]".to_string(), + }); + } + + out.extend(explicit.iter().filter(|n| !is_prov(n)).cloned()); + out +} + +/// Distinct produced binaries for a package. +/// +/// `bin=>alias` collapses to the left-hand name, `@name` marks a secondary +/// symlink rather than its own package, and a `[subpackage]` block overrides +/// both. +fn units(p: &PkgToml, provides: &[String]) -> Vec<(String, Vec)> { + if !p.subpackage.is_empty() { + return p + .subpackage + .iter() + .map(|(k, sub)| { + let pr = if sub.provides.is_empty() { vec![k.clone()] } else { sub.provides.clone() }; + (k.clone(), pr) + }) + .collect(); + } + + let mut names: Vec = Vec::new(); + for pr in provides { + if pr.starts_with('@') { + continue; + } + let base = pr.split("==").next().unwrap_or(pr); + let base = base.split("=>").next().unwrap_or(base).trim().to_string(); + if !names.contains(&base) { + names.push(base); + } + } + if names.is_empty() { + names.push(p.pkg.name.clone()); + } + names.into_iter().map(|n| (n, provides.to_vec())).collect() +} + +/// Generate every index entry for `host`. +pub fn generate(root: &Path, host: &str) -> (Vec, Vec) { + let (packages, errors) = tree::load(root); + let mut out = Vec::new(); + + for pkg in &packages { + let p = &pkg.pkg; + if p.pkg.disabled || !p.host.supported.iter().any(|h| h == host) { + continue; + } + let fam = p.pkg.family.clone(); + let srcs = p.src_urls(); + let src0 = srcs.first().cloned().unwrap_or_default(); + + for v in &pkg.versions { + let Some(url) = v.url.get(host) else { continue }; + + // The version file may override pkg.toml; these are the fields + // that realistically change between releases. + let provides = v.provides.clone().unwrap_or_else(|| p.pkg.provides.clone()); + let note_src = v.note.clone(); + + let size = v.size.get(host).copied(); + // soar's index has a bsum column only, so the blake3 digest is + // what actually reaches a client. sha256 rides along for + // cross-checking against upstream checksum files. + let bsum = v.blake3.get(host).cloned(); + let shasum = v.sha256.get(host).cloned(); + + // No ghcr_* fields are emitted: soar prefers an OCI source over + // download_url and requires a per-artifact signature there, which + // the upstream-plus-hash model does not produce. `ghcr` stays in + // pkg.toml because it defines the package identity namespace. + let arch_for_host = { + let raw = host.split('-').next().unwrap_or(host); + p.arch.get(raw).cloned().unwrap_or_else(|| raw.to_string()) + }; + // Licences and docs are not executables, and a file already at + // the archive root under its own name needs no mapping. + let binaries: Vec = p + .source + .as_ref() + .map(|src| { + src.install + .iter() + .filter(|(from, to)| { + let base = from.rsplit('/').next().unwrap_or(from); + // An entry is worth publishing when the file has + // to be renamed, or when it is nested rather than + // at the artifact root: soar looks at the root by + // package name and would not find bin/. + let nested = from.trim_start_matches("*/").contains('/'); + (base != *to || nested) + && !to.eq_ignore_ascii_case("LICENSE") + && *from != "*" + }) + .map(|(from, to)| Binary { + // The index is generated per host, so templates + // are expanded here rather than shipped for the + // client to resolve. + // The archive root is promoted away before + // binaries are resolved, so publish the path + // relative to what remains. + source: strip_archive_root(&expand_arch( + from, + &v.version, + &arch_for_host, + )), + // Strip soar's provides markers; link_as is a + // plain filename. + link_as: Some( + to.split("==").next().unwrap_or(to) + .split("=>").next().unwrap_or(to) + .trim().to_string(), + ), + }) + .collect() + }) + .unwrap_or_default(); + + // Only pinned side files are published: an unhashed fetch would + // be an unverified download, which is the thing this format exists + // to avoid. + let extras: Vec = v + .extra + .iter() + .filter(|e| e.blake3.is_some() || e.sha256.is_some()) + .map(|e| ExtraFile { + url: e.url.clone(), + to: e.to.clone(), + blake3: e.blake3.clone(), + sha256: e.sha256.clone(), + }) + .collect(); + + for (name, prov) in units(p, &provides) { + let mut note = render_notes(p, &src0); + if let Some(n) = ¬e_src { + note = n.clone(); + } + out.push(Entry { + pkg_name: name.clone(), + pkg_family: fam.clone(), + pkg_type: p.pkg.kind.clone(), + description: p.pkg.description.clone(), + version: v.version.clone(), + download_url: url.clone(), + size, + src_url: srcs.clone(), + homepage: p.pkg.homepage.clone(), + license: p.pkg.license.clone(), + maintainer: p.pkg.maintainer.clone(), + note, + category: p.pkg.category.clone(), + provides: prov, + repology: p.pkg.repology.clone(), + shasum: shasum.clone(), + bsum: bsum.clone(), + binaries: binaries.clone(), + extra: extras.clone(), + }); + } + } + } + + out.sort_by(|a, b| a.pkg_name.cmp(&b.pkg_name)); + (out, errors) +} diff --git a/src/port/mod.rs b/src/port/mod.rs new file mode 100644 index 0000000..1a6fe47 --- /dev/null +++ b/src/port/mod.rs @@ -0,0 +1,11 @@ +//! The declarative port format: parsing, validation, resolution and index +//! generation. + +pub mod audit; +pub mod hashfill; +pub mod meta; +pub mod new; +pub mod model; +pub mod resolve; +pub mod tree; +pub mod validate; diff --git a/src/port/model.rs b/src/port/model.rs new file mode 100644 index 0000000..8dcb7e8 --- /dev/null +++ b/src/port/model.rs @@ -0,0 +1,210 @@ +//! Data model for the declarative port format. +//! +//! A package is described by two kinds of file: +//! +//! * `pkg.toml` holds identity, metadata and the update policy, stable across +//! releases. +//! * `-.toml` is one per pinned version, holding the resolved +//! per-host URL, hash and size. +//! +//! Nothing here is executable. A client can resolve a package by parsing +//! alone, which is what allows the hash to live in the repository rather than +//! being measured after the fact. + +use std::collections::BTreeMap; + +use serde::Deserialize; + +/// A parsed `pkg.toml`. +#[derive(Debug, Deserialize)] +pub struct PkgToml { + pub pkg: Pkg, + #[serde(default)] + pub host: Host, + /// Maps a host architecture onto the name upstream uses for it. + #[serde(default)] + pub arch: BTreeMap, + pub update: Option, + pub source: Option, + #[serde(default)] + pub extra: Vec, + /// Present only when one recipe yields several independently named + /// packages; each becomes its own index entry. + #[serde(default)] + pub subpackage: BTreeMap, +} + +#[derive(Debug, Deserialize)] +pub struct Pkg { + pub name: String, + #[serde(rename = "type")] + pub kind: Option, + pub description: Option, + /// Directory name, which is not always `name` (packages/fd-find holds fd). + pub family: Option, + /// Release channel; `stable` unless stated. + pub channel: Option, + #[serde(default)] + pub homepage: Vec, + /// Upstream repository. Derived from the update policy when omitted. + #[serde(default)] + pub src: Vec, + #[serde(default)] + pub license: Vec, + #[serde(default)] + pub maintainer: Vec, + #[serde(default)] + pub category: Vec, + #[serde(default)] + pub repology: Vec, + #[serde(default)] + pub provides: Vec, + /// Only notes that cannot be derived from the structured fields. + #[serde(default)] + pub note: Vec, + #[serde(default = "yes")] + pub portable: bool, + #[serde(rename = "portable-reason")] + pub portable_reason: Option, + #[serde(default)] + pub disabled: bool, + #[serde(rename = "disabled-reason")] + pub disabled_reason: Option, +} + +fn yes() -> bool { + true +} + +#[derive(Debug, Default, Deserialize)] +pub struct Host { + #[serde(default)] + pub supported: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Update { + pub strategy: String, + pub repo: String, + #[serde(rename = "strip-prefix")] + pub strip_prefix: Option, + #[serde(rename = "tag-suffix")] + pub tag_suffix: Option, + /// Only consider releases whose tag starts with this. Needed when one + /// repository publishes releases for several packages. + #[serde(rename = "tag-prefix")] + pub tag_prefix: Option, + pub pattern: Option, +} + +/// How the bot locates the artifact for a release. Never used at install time. +#[derive(Debug, Deserialize)] +pub struct Source { + pub url: Option, + pub github: Option, + pub tag: Option, + pub glob: Option, + #[serde(default)] + pub r#match: Vec, + #[serde(default)] + pub exclude: Vec, + /// Archive path to output path. + #[serde(default)] + pub install: BTreeMap, +} + +/// A single template, or one URL per host when upstream filenames differ +/// irreconcilably between architectures. +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum UrlSpec { + Template(String), + PerHost(BTreeMap), +} + +/// A side file the artifact does not ship, typically a licence. +/// +/// In `pkg.toml` this is a template the bot resolves. Nearly all of these +/// point at a branch rather than a tag, so the content can change without any +/// version bump; the resolved copy and its hash therefore live in the version +/// file and are refreshed whenever a version is pinned. +#[derive(Debug, Deserialize)] +pub struct Extra { + /// Fetched from upstream. + pub url: Option, + /// Or taken from this repository's `licenses/` directory by SPDX id. + /// + /// Only valid for licences whose text is verbatim by requirement (the GPL + /// family). MIT, BSD and friends embed a per-project copyright line, so + /// only the project's own file will do. + pub license: Option, + pub to: String, +} + +/// A resolved side file: the URL actually fetched and what it hashed to. +#[derive(Debug, Clone, Deserialize)] +pub struct PinnedExtra { + pub url: String, + pub to: String, + #[serde(default)] + pub blake3: Option, + #[serde(default)] + pub sha256: Option, +} + +#[derive(Debug, Deserialize)] +pub struct SubPackage { + #[serde(default)] + pub provides: Vec, +} + +/// A parsed `-.toml`. +#[derive(Debug, Deserialize)] +pub struct VersionToml { + pub version: String, + #[serde(default)] + pub url: BTreeMap, + /// blake3 digests per host. This is what soar verifies downloads + /// against, and it can only be obtained by fetching the artifact. + #[serde(default)] + pub blake3: BTreeMap, + /// sha256 digests per host. Reported by forge APIs without downloading, + /// so it doubles as a cross-check against a release's checksums file. + #[serde(default)] + pub sha256: BTreeMap, + #[serde(default)] + pub size: BTreeMap, + /// Any `pkg.toml` field may be overridden per version; these are the ones + /// that realistically change between releases. + #[serde(default)] + pub note: Option>, + #[serde(default)] + pub provides: Option>, + /// Side files resolved and hashed for this version. + #[serde(default)] + pub extra: Vec, +} + +impl PkgToml { + /// Directory name, falling back to the package name. + pub fn family(&self) -> &str { + self.pkg.family.as_deref().unwrap_or(&self.pkg.name) + } + + pub fn channel(&self) -> &str { + self.pkg.channel.as_deref().unwrap_or("stable") + } + + /// Upstream repository, derived from the update policy when not stated. + pub fn src_urls(&self) -> Vec { + if !self.pkg.src.is_empty() { + return self.pkg.src.clone(); + } + match &self.update { + Some(u) if u.strategy.starts_with("github") => { + vec![format!("https://github.com/{}", u.repo)] + } + _ => Vec::new(), + } + } +} diff --git a/src/port/new.rs b/src/port/new.rs new file mode 100644 index 0000000..a21b70a --- /dev/null +++ b/src/port/new.rs @@ -0,0 +1,101 @@ +//! Scaffolding for a new package. +//! +//! Writes a `pkg.toml` with the update policy and source selector filled in. +//! No version is pinned here: run `port resolve` afterwards, which is what +//! records the URL and hash. + +use std::{fs, path::Path}; + +/// What kind of artifact the upstream release publishes. +#[derive(Clone, Copy, PartialEq)] +pub enum Kind { + AppImage, + Static, +} + +impl Kind { + pub fn parse(s: &str) -> Result { + match s { + "appimage" => Ok(Kind::AppImage), + "static" => Ok(Kind::Static), + other => Err(format!("unknown type {other:?}, expected appimage or static")), + } + } + + fn as_str(self) -> &'static str { + match self { + Kind::AppImage => "appimage", + Kind::Static => "static", + } + } +} + +pub struct Scaffold<'a> { + pub name: &'a str, + pub repo: &'a str, + pub kind: Kind, + pub description: Option<&'a str>, + pub maintainer: Option<&'a str>, + /// Upstream tags carrying a build suffix, e.g. `1.2.3@2026-01-01_1234`. + pub tag_suffix_strip: bool, +} + +fn arr(items: &[String]) -> String { + let inner: Vec = items.iter().map(|s| format!("{s:?}")).collect(); + format!("[{}]", inner.join(", ")) +} + +/// Render a `pkg.toml`. Fields a human must still fill are left as TODO so +/// validation and review catch them rather than them being silently wrong. +pub fn render(s: &Scaffold) -> String { + let owner = s.repo.split('/').next().unwrap_or("upstream"); + let mut out = String::from("[pkg]\n"); + out.push_str(&format!("name = {:?}\n", s.name)); + out.push_str(&format!("id = {:?}\n", format!("{owner}.{}", s.name))); + out.push_str(&format!("type = {:?}\n", s.kind.as_str())); + out.push_str(&format!( + "description = {:?}\n", + s.description.unwrap_or("TODO: one-line description") + )); + out.push_str(&format!( + "homepage = {}\n", + arr(&[format!("https://github.com/{}", s.repo)]) + )); + out.push_str("license = [\"TODO\"]\n"); + out.push_str(&format!( + "maintainer = {}\n", + arr(&[s.maintainer.unwrap_or("TODO ").to_string()]) + )); + out.push_str("category = [\"TODO\"]\n"); + out.push_str("tag = [\"TODO\"]\n"); + out.push_str("repology = [\"TODO\"]\n"); + out.push_str(&format!("provides = {}\n", arr(&[s.name.to_string()]))); + + out.push_str("\n[host]\nsupported = [\"x86_64-linux\", \"aarch64-linux\"]\n"); + + out.push_str("\n[update]\nstrategy = \"github-releases\"\n"); + out.push_str(&format!("repo = {:?}\n", s.repo)); + if s.tag_suffix_strip { + out.push_str("tag-suffix = \"strip\"\n"); + } + + out.push_str("\n[source]\n"); + out.push_str(&format!("github = {:?}\n", s.repo)); + match s.kind { + Kind::AppImage => out.push_str("glob = \"*${arch}*.appimage\"\n"), + Kind::Static => out.push_str("glob = \"TODO: asset glob, e.g. *${arch}*-linux-musl.tar.gz\"\n"), + } + out +} + +/// Write the scaffold to `/packages//pkg.toml`. +pub fn write(root: &Path, s: &Scaffold) -> Result { + let dir = root.join("packages").join(s.name); + if dir.join("pkg.toml").exists() { + return Err(format!("{} already exists", dir.join("pkg.toml").display())); + } + fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let path = dir.join("pkg.toml"); + fs::write(&path, render(s)).map_err(|e| e.to_string())?; + Ok(path) +} diff --git a/src/port/resolve.rs b/src/port/resolve.rs new file mode 100644 index 0000000..a03a010 --- /dev/null +++ b/src/port/resolve.rs @@ -0,0 +1,349 @@ +//! Version and artifact resolution: the only part of the pipeline that needs +//! the network. +//! +//! Given a package's update policy, find the current upstream version, select +//! the matching release asset per host, and record the resolved URL, hash and +//! size. The result is written as a pinned version file, after which nothing +//! else in the system has to trust upstream again. + +use indexmap::IndexMap; + +use serde::Deserialize; + +use crate::port::model::{PkgToml, Source, UrlSpec, Update}; + +const API: &str = "https://api.github.com"; + +/// A release asset as reported by the forge API. +#[derive(Debug, Clone, Deserialize)] +pub struct Asset { + pub name: String, + pub browser_download_url: String, + pub size: Option, + /// GitHub reports `sha256:...` for assets uploaded since the field + /// existed. Older releases have none and must be hashed by download. + pub digest: Option, +} + +#[derive(Debug, Deserialize)] +struct Release { + tag_name: Option, + #[serde(default)] + assets: Vec, +} + +#[derive(Debug, Deserialize)] +struct Tag { + name: String, +} + +/// What resolution produced for one package. +pub struct Resolved { + pub version: String, + /// Insertion-ordered to follow `host.supported`, so re-resolving an + /// unchanged package produces an unchanged file. + pub urls: IndexMap, + /// sha256 only: the forge reports it, blake3 needs a download. + pub hashes: IndexMap, + /// blake3 is never resolved here; it is carried forward from an existing + /// pin so re-resolving cannot discard what hashfill downloaded. + pub blake3: IndexMap, + pub sizes: IndexMap, + /// Hosts that could not be resolved, with a reason. + pub missing: Vec, +} + +/// Substitute the three template variables. Deliberately not an expression +/// language: anything more complex belongs in an explicit per-host table. +pub fn expand(tpl: &str, version: &str, arch: &str, name: &str) -> String { + tpl.replace("${version}", version) + .replace("${arch}", arch) + .replace("${name}", name) +} + +/// Strip the decoration upstreams put around a version in their tags. +pub fn clean_version(tag: &str, u: &Update) -> String { + let mut v = tag.to_string(); + if u.tag_suffix.as_deref() == Some("strip") { + v = v.split('@').next().unwrap_or(&v).to_string(); + } + if let Some(p) = &u.tag_prefix { + if let Some(rest) = v.strip_prefix(p.as_str()) { + v = rest.to_string(); + } + } + if let Some(p) = &u.strip_prefix { + if let Some(rest) = v.strip_prefix(p.as_str()) { + v = rest.to_string(); + } + } + // A bare leading `v` only counts when a digit follows it. + let bytes = v.as_bytes(); + if bytes.first() == Some(&b'v') && bytes.get(1).is_some_and(|c| c.is_ascii_digit()) { + v = v.trim_start_matches('v').to_string(); + } + v +} + +fn is_sidecar(name: &str) -> bool { + let n = name.to_lowercase(); + [".sig", ".asc", ".zsync", ".txt", ".pem", ".sbom", ".json", ".intoto", ".jsonl"] + .iter() + .any(|e| n.ends_with(e)) + || n.rsplit('.').next().is_some_and(|e| { + e.starts_with("sha") && e[3..].chars().all(|c| c.is_ascii_digit() || c == 's' || c == 'u' || c == 'm') + }) +} + +/// Choose the artifact for one host from a release's assets. +pub fn pick_asset<'a>( + assets: &'a [Asset], + src: &Source, + version: &str, + arch: &str, + name: &str, +) -> Option<&'a Asset> { + let mut cands: Vec<&Asset> = assets.iter().collect(); + + if let Some(g) = &src.glob { + let pat = expand(g, version, arch, name).to_lowercase(); + let pattern = glob::Pattern::new(&pat).ok()?; + cands.retain(|a| pattern.matches(&a.name.to_lowercase())); + } else if !src.r#match.is_empty() { + for tok in &src.r#match { + let t = expand(tok, version, arch, name).to_lowercase(); + cands.retain(|a| a.name.to_lowercase().contains(&t)); + } + } + for tok in &src.exclude { + let t = expand(tok, version, arch, name).to_lowercase(); + cands.retain(|a| !a.name.to_lowercase().contains(&t)); + } + cands.retain(|a| !is_sidecar(&a.name)); + + // Shortest name wins; upstreams decorate variants, not the plain artifact. + cands.sort_by(|a, b| a.name.len().cmp(&b.name.len()).then(a.name.cmp(&b.name))); + cands.into_iter().next() +} + +async fn get_json Deserialize<'de>>( + client: &reqwest::Client, + url: &str, + token: &str, +) -> Result { + let mut req = client.get(url).header("Accept", "application/vnd.github+json"); + if !token.is_empty() && url.starts_with(API) { + req = req.bearer_auth(token); + } + let resp = req.send().await.map_err(|e| e.to_string())?; + if !resp.status().is_success() { + return Err(format!("api {}", resp.status().as_u16())); + } + resp.json::().await.map_err(|e| e.to_string()) +} + +/// Resolve the current version and per-host artifacts for one package. +pub async fn resolve( + client: &reqwest::Client, + pkg: &PkgToml, + token: &str, +) -> Result { + let upd = pkg.update.as_ref().ok_or("no update policy")?; + let src = pkg.source.as_ref().ok_or("no source")?; + let name = &pkg.pkg.name; + + let mut assets: Vec = Vec::new(); + let tag = match upd.strategy.as_str() { + "html-regex" => { + let body = client + .get(&upd.repo) + .header("User-Agent", "sbuild-port") + .send() + .await + .map_err(|e| e.to_string())? + .text() + .await + .map_err(|e| e.to_string())?; + let pat = upd.pattern.as_deref().ok_or("html-regex needs a pattern")?; + let re = regex::Regex::new(pat).map_err(|e| e.to_string())?; + re.captures(&body) + .and_then(|c| c.get(1)) + .map(|m| m.as_str().to_string()) + .ok_or("pattern matched nothing")? + } + "gitlab-tags" => { + let url = format!( + "https://gitlab.com/api/v4/projects/{}/repository/tags?per_page=5&order_by=version", + upd.repo + ); + let tags: Vec = get_json(client, &url, token).await?; + tags.first().map(|t| t.name.clone()).ok_or("no tags")? + } + "github-tags" => { + let url = format!("{API}/repos/{}/tags", upd.repo); + let tags: Vec = get_json(client, &url, token).await?; + tags.first().map(|t| t.name.clone()).ok_or("no tags")? + } + _ => { + // A repository that publishes for several packages has no single + // "latest", so filter by tag prefix when one is given. + let rel: Release = if let Some(prefix) = &upd.tag_prefix { + let url = format!("{API}/repos/{}/releases?per_page=100", upd.repo); + let rels: Vec = get_json(client, &url, token).await?; + rels.into_iter() + .find(|r| { + r.tag_name.as_deref().is_some_and(|t| t.starts_with(prefix.as_str())) + }) + .ok_or("no release matching tag-prefix")? + } else { + let url = format!("{API}/repos/{}/releases/latest", upd.repo); + get_json(client, &url, token).await? + }; + assets = rel.assets; + rel.tag_name.ok_or("no tag_name")? + } + }; + + let version = clean_version(&tag, upd); + if version.is_empty() { + return Err("empty version".into()); + } + + let hosts = if pkg.host.supported.is_empty() { + vec!["x86_64-linux".to_string()] + } else { + pkg.host.supported.clone() + }; + + let mut out = Resolved { + version: version.clone(), + urls: IndexMap::new(), + hashes: IndexMap::new(), + blake3: IndexMap::new(), + sizes: IndexMap::new(), + missing: Vec::new(), + }; + + for host in hosts { + let raw = host.split('-').next().unwrap_or(&host).to_string(); + let arch = pkg.arch.get(&raw).cloned().unwrap_or(raw); + + let (url, asset) = match &src.url { + Some(UrlSpec::PerHost(m)) => { + let Some(tpl) = m.get(&host) else { + out.missing.push(format!("{host}:no-url")); + continue; + }; + let u = expand(tpl, &version, &arch, name); + let a = assets.iter().find(|a| a.browser_download_url == u); + (Some(u), a) + } + Some(UrlSpec::Template(tpl)) => { + let u = expand(tpl, &version, &arch, name); + let base = u.rsplit('/').next().unwrap_or_default().to_string(); + let a = assets + .iter() + .find(|a| a.browser_download_url == u || a.name == base); + (Some(u), a) + } + None => { + let a = pick_asset(&assets, src, &version, &arch, name); + (a.map(|a| a.browser_download_url.clone()), a) + } + }; + + // Always take the URL from the asset that was actually matched. + // Emitting an expanded template while hashing a differently-matched + // asset can pin a correct hash against a URL that 404s. + let url = match asset { + Some(a) => Some(a.browser_download_url.clone()), + None => url, + }; + let Some(url) = url else { + out.missing.push(format!("{host}:no-asset")); + continue; + }; + if url.contains("${") { + out.missing.push(format!("{host}:unexpanded")); + continue; + } + + out.urls.insert(host.clone(), url); + if let Some(a) = asset { + if let Some(s) = a.size { + out.sizes.insert(host.clone(), s); + } + match &a.digest { + Some(d) => { + let bare = d.strip_prefix("sha256:").unwrap_or(d); + out.hashes.insert(host.clone(), bare.to_string()); + } + None => out.missing.push(format!("{host}:no-digest")), + } + } + } + + if out.urls.is_empty() { + return Err("no asset matched any host".into()); + } + Ok(out) +} + +/// Carry hashes forward from an existing pin. +/// +/// Forges that report no digest (GitLab, older GitHub releases) would +/// otherwise lose the hash that `hashfill` computed. Only reuse a hash when +/// the URL is byte-identical, so a changed artifact can never keep a stale one. +pub fn carry_forward(r: &mut Resolved, existing: &crate::port::model::VersionToml) { + if existing.version != r.version { + return; + } + for (host, url) in &r.urls { + if r.hashes.contains_key(host) && r.blake3.contains_key(host) { + continue; + } + if existing.url.get(host).map(|u| u == url).unwrap_or(false) { + if let Some(h) = existing.sha256.get(host) { + r.hashes.insert(host.clone(), h.clone()); + } + if let Some(b) = existing.blake3.get(host) { + r.blake3.insert(host.clone(), b.clone()); + } + if let Some(s) = existing.size.get(host) { + r.sizes.insert(host.clone(), *s); + } + } + } +} + +/// Render a pinned version file. Keys are padded so diffs stay readable. +pub fn render(r: &Resolved) -> String { + let mut s = format!("version = {:?}\n", r.version); + let w = r.urls.keys().map(|k| k.len()).max().unwrap_or(0); + s.push_str("\n[url]\n"); + for (h, u) in &r.urls { + s.push_str(&format!("{:, +} + +/// Read every package under `/packages`, sorted by directory name. +/// +/// A malformed package is reported rather than aborting the scan, so one bad +/// file cannot hide the state of the rest of the tree. +pub fn load(root: &Path) -> (Vec, Vec) { + let mut packages = Vec::new(); + let mut errors = Vec::new(); + + let mut dirs: Vec = match fs::read_dir(root.join("packages")) { + Ok(rd) => rd.filter_map(|e| e.ok()).map(|e| e.path()).filter(|p| p.is_dir()).collect(), + Err(e) => { + errors.push(format!("{}: {e}", root.join("packages").display())); + return (packages, errors); + } + }; + dirs.sort(); + + for dir in dirs { + let name = dir.file_name().unwrap_or_default().to_string_lossy().to_string(); + let pkg_path = dir.join("pkg.toml"); + let raw = match fs::read_to_string(&pkg_path) { + Ok(s) => s, + Err(_) => { + errors.push(format!("{name}: no pkg.toml")); + continue; + } + }; + let pkg: PkgToml = match toml::from_str(&raw) { + Ok(p) => p, + Err(e) => { + errors.push(format!("{name}: pkg.toml unparseable: {e}")); + continue; + } + }; + + let mut version_paths: Vec = fs::read_dir(&dir) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + p.extension().is_some_and(|x| x == "toml") + && p.file_name().is_some_and(|f| f != "pkg.toml") + }) + .collect(); + version_paths.sort(); + + let mut versions = Vec::new(); + for vp in version_paths { + let vname = vp.file_name().unwrap_or_default().to_string_lossy().to_string(); + match fs::read_to_string(&vp).map_err(|e| e.to_string()).and_then(|s| { + toml::from_str::(&s).map_err(|e| e.to_string()) + }) { + Ok(v) => versions.push(v), + Err(e) => errors.push(format!("{vname}: unparseable: {e}")), + } + } + packages.push(Package { dir, pkg, versions }); + } + + (packages, errors) +} diff --git a/src/port/validate.rs b/src/port/validate.rs new file mode 100644 index 0000000..9011ffe --- /dev/null +++ b/src/port/validate.rs @@ -0,0 +1,80 @@ +//! Structural validation of a ports tree, intended as a CI gate. +//! +//! The load-bearing rule is that every pinned URL has a hash beside it in the +//! same file. A version file without one is worse than no file at all: it +//! looks pinned while trusting whatever the server happens to return. + +use std::path::Path; + +use crate::port::tree; + +pub struct Report { + pub checked: usize, + pub packages: usize, + pub errors: Vec, + pub warnings: Vec, +} + +pub fn run(root: &Path) -> Report { + let (packages, mut errors) = tree::load(root); + let mut warnings = Vec::new(); + let mut checked = 0usize; + + for p in &packages { + let name = p.dir.file_name().unwrap_or_default().to_string_lossy().to_string(); + + if p.pkg.pkg.name.is_empty() { + errors.push(format!("{name}: pkg.name missing")); + } + if p.pkg.pkg.description.as_deref().unwrap_or("").is_empty() { + errors.push(format!("{name}: pkg.description missing")); + } + + if p.pkg.pkg.disabled { + if p.pkg.pkg.disabled_reason.is_none() { + warnings.push(format!("{name}: disabled without a reason")); + } + continue; + } + + if p.pkg.host.supported.is_empty() { + errors.push(format!("{name}: no supported hosts")); + } + if p.versions.is_empty() { + errors.push(format!("{name}: no version file")); + continue; + } + + for v in &p.versions { + let tag = format!("{name}-{}", v.version); + if v.version.is_empty() { + errors.push(format!("{name}: version file with no version")); + } + if v.url.is_empty() { + errors.push(format!("{tag}: no url")); + } + for (host, url) in &v.url { + checked += 1; + if url.contains("${") { + errors.push(format!("{tag}: unexpanded template in {host} url")); + } + let b3 = v.blake3.get(host); + let sha = v.sha256.get(host); + if b3.is_none() && sha.is_none() { + errors.push(format!("{tag}: {host} pinned with NO HASH")); + } else if b3.is_none() { + // soar's index carries a bsum column only, so an entry + // without blake3 reaches the client unverifiable. + warnings.push(format!("{tag}: {host} has no blake3; soar cannot verify it")); + } + } + for host in v.blake3.keys().chain(v.sha256.keys()) { + if !v.url.contains_key(host) { + errors.push(format!("{tag}: hash for {host} with no url")); + } + } + } + } + + Report { checked, packages: packages.len(), errors, warnings } +}