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 } +}