From 23050adbd311ee74045262fc606efa63f3d9c120 Mon Sep 17 00:00:00 2001 From: Gerald Pinder Date: Fri, 11 Oct 2024 19:37:28 -0400 Subject: [PATCH] feat: Add validation command --- .gitignore | 2 + Cargo.lock | 607 ++++++++++++++---- Cargo.toml | 10 +- bacon.toml | 9 +- .../test-repo/recipes/akmods.yml | 2 + .../test-repo/recipes/flatpaks.yml | 2 + .../test-repo/recipes/recipe-arm64.yml | 2 +- process/Cargo.toml | 1 + recipe/src/lib.rs | 17 + recipe/src/module.rs | 13 +- recipe/src/module_ext.rs | 50 +- recipe/src/recipe.rs | 9 - recipe/src/stage.rs | 13 +- recipe/src/stages_ext.rs | 50 +- src/bin/bluebuild.rs | 4 + src/commands.rs | 5 + src/commands/generate.rs | 9 + src/commands/validate.rs | 399 ++++++++++++ 18 files changed, 1000 insertions(+), 204 deletions(-) create mode 100644 src/commands/validate.rs diff --git a/.gitignore b/.gitignore index 1ad22d54..61ae6973 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .vscode/ result* .direnv/ +.arg +.secret cosign.key !test-files/keys/cosign.key diff --git a/Cargo.lock b/Cargo.lock index 0a8a1fea..ff5420a4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,7 @@ dependencies = [ "cfg-if", "getrandom", "once_cell", + "serde", "version_check", "zerocopy", ] @@ -264,6 +265,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -328,6 +344,7 @@ dependencies = [ "colored", "fuzzy-matcher", "indicatif", + "jsonschema", "log", "miette", "oci-distribution", @@ -335,6 +352,7 @@ dependencies = [ "os_info", "rayon", "requestty", + "reqwest 0.12.8", "rusty-hook", "serde", "serde_json", @@ -369,6 +387,7 @@ dependencies = [ "once_cell", "os_pipe", "rand 0.8.5", + "reqwest 0.12.8", "rstest", "semver", "serde", @@ -458,6 +477,12 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "borrow-or-share" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32" + [[package]] name = "bstr" version = "1.10.0" @@ -474,6 +499,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "byteorder" version = "1.5.0" @@ -537,8 +568,6 @@ version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -701,12 +730,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const_fn" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d" - [[package]] name = "const_format" version = "0.2.33" @@ -1011,6 +1034,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "docker_credential" version = "1.3.1" @@ -1094,6 +1128,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + [[package]] name = "encode_unicode" version = "0.3.6" @@ -1135,6 +1178,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fancy-regex" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -1179,6 +1233,17 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fluent-uri" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7bd399b64ddd63a83cf40512c96007dafe9ac26cfc8c89c820a247c6f7d2376" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1207,6 +1272,16 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "fraction" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "fsio" version = "0.1.3" @@ -1362,19 +1437,6 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -[[package]] -name = "git2" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" -dependencies = [ - "bitflags 2.6.0", - "libc", - "libgit2-sys", - "log", - "url", -] - [[package]] name = "glob" version = "0.3.1" @@ -1663,7 +1725,6 @@ dependencies = [ "hyper 1.4.1", "hyper-util", "rustls 0.23.14", - "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1713,6 +1774,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1729,6 +1908,18 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd69211b9b519e98303c015e21a007e293db403b6c85b9b124e133d25e242cdd" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1884,15 +2075,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.72" @@ -1933,6 +2115,35 @@ dependencies = [ "utf8-decode", ] +[[package]] +name = "jsonschema" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0762f81b67c46ac403497420b7f4ae457f6cdbef499d24f101bf4a63c35ddea1" +dependencies = [ + "ahash 0.8.11", + "anyhow", + "base64 0.22.1", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "getrandom", + "idna 1.0.2", + "itoa", + "num-cmp", + "once_cell", + "percent-encoding", + "referencing", + "regex", + "regex-syntax", + "reqwest 0.12.8", + "serde", + "serde_json", + "url", + "uuid-simd", +] + [[package]] name = "jwt" version = "0.16.0" @@ -2065,18 +2276,6 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" -[[package]] -name = "libgit2-sys" -version = "0.16.2+1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libm" version = "0.2.8" @@ -2093,18 +2292,6 @@ dependencies = [ "libc", ] -[[package]] -name = "libz-sys" -version = "1.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d16453e800a8cf6dd2fc3eb4bc99b786a9b90c663b8559a5b1a041bf89e472" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linked-hash-map" version = "0.5.6" @@ -2117,6 +2304,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" + [[package]] name = "lock_api" version = "0.4.12" @@ -2329,6 +2522,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -2346,6 +2563,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -2372,6 +2604,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2591,12 +2834,6 @@ dependencies = [ "url", ] -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - [[package]] name = "option-ext" version = "0.2.0" @@ -2633,6 +2870,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + [[package]] name = "owo-colors" version = "4.1.0" @@ -3209,6 +3452,39 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "referencing" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43fda277062a68f002ee482f8319d2fcc84be5ff11669cacd74db7ca01f7f36a" +dependencies = [ + "ahash 0.8.11", + "fluent-uri", + "once_cell", + "percent-encoding", + "serde_json", +] + [[package]] name = "regex" version = "1.11.0" @@ -3345,6 +3621,7 @@ checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http 1.1.0", @@ -3363,7 +3640,6 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.14", - "rustls-native-certs", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -3562,19 +3838,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" -dependencies = [ - "openssl-probe", - "rustls-pemfile 2.2.0", - "rustls-pki-types", - "schannel", - "security-framework", -] - [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3668,15 +3931,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" -dependencies = [ - "windows-sys 0.59.0", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3719,29 +3973,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.23" @@ -3919,10 +4150,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5c5c8276991763b44ede03efaf966eaa0412fafbf299e6380704678ca3b997" dependencies = [ "const_format", - "git2", "is_debug", "time", - "tzdb", ] [[package]] @@ -4228,6 +4457,17 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "syntect" version = "5.2.0" @@ -4411,6 +4651,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -4617,35 +4867,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "tz-rs" -version = "0.6.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33851b15c848fad2cf4b105c6bb66eb9512b6f6c44a4b13f57c53c73c707e2b4" -dependencies = [ - "const_fn", -] - -[[package]] -name = "tzdb" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b580f6b365fa89f5767cdb619a55d534d04a4e14c2d7e5b9a31e94598687fb1" -dependencies = [ - "iana-time-zone", - "tz-rs", - "tzdb_data", -] - -[[package]] -name = "tzdb_data" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "654c1ec546942ce0594e8d220e6b8e3899e0a0a8fe70ddd54d32a376dfefe3f8" -dependencies = [ - "tz-rs", -] - [[package]] name = "unicase" version = "2.7.0" @@ -4744,7 +4965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -4765,12 +4986,24 @@ dependencies = [ "log", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8-decode" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -4787,10 +5020,15 @@ dependencies = [ ] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "uuid-simd" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] [[package]] name = "version_check" @@ -4798,6 +5036,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "walkdir" version = "2.5.0" @@ -5274,6 +5518,18 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ab703352da6a72f35c39a533526393725640575bb211f61987a2748323ad956" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "x509-cert" version = "0.2.5" @@ -5297,6 +5553,30 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -5318,6 +5598,27 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "zerofrom" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" @@ -5338,3 +5639,25 @@ dependencies = [ "quote", "syn 2.0.79", ] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] diff --git a/Cargo.toml b/Cargo.toml index ecac5147..aa6d771a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,8 @@ colored = "2" indexmap = { version = "2", features = ["serde"] } indicatif = { version = "0.17", features = ["improved_unicode"] } log = "0.4" -oci-distribution = { version = "0.11.0", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots"] } +oci-distribution = { version = "0.11", default-features = false } +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } miette = "7" rstest = "0.18" serde = { version = "1", features = ["derive"] } @@ -65,11 +66,12 @@ blue-build-process-management = { version = "=0.8.20", path = "./process" } clap-verbosity-flag = "2" clap_complete = "4" fuzzy-matcher = "0.3" +jsonschema = { version = "0.22.3", optional = true } open = "5" os_info = "3" rayon = { version = "1.10.0", optional = true } requestty = { version = "0.5", features = ["macros", "termion"] } -shadow-rs = "0.26" +shadow-rs = { version = "0.26", default-features = false } urlencoding = "2" cached.workspace = true @@ -79,6 +81,7 @@ indicatif.workspace = true log.workspace = true miette = { workspace = true, features = ["fancy"] } oci-distribution.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true serde_yaml.workspace = true @@ -95,12 +98,13 @@ iso = [] switch = [] sigstore = ["blue-build-process-management/sigstore"] login = [] +validate = ["jsonschema", "rayon"] [dev-dependencies] rusty-hook = "0.11" [build-dependencies] -shadow-rs = "0.26" +shadow-rs = { version = "0.26", default-features = false } [lints] workspace = true diff --git a/bacon.toml b/bacon.toml index c2b2ae81..b5c835ac 100644 --- a/bacon.toml +++ b/bacon.toml @@ -89,6 +89,9 @@ watch = ["src", "process", "recipe", "template", "utils", "Cargo.toml", "build.r # should go in your personal global prefs.toml file instead. [keybindings] # alt-m = "job:my-job" -c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target -i = "job:install-all" -t = "job:test-all" +c = "job:clippy" +shift-c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target +i = "job:install" +shift-i = "job:install-all" +t = "job:test" +shift-t = "job:test-all" diff --git a/integration-tests/test-repo/recipes/akmods.yml b/integration-tests/test-repo/recipes/akmods.yml index 10e11054..33382992 100644 --- a/integration-tests/test-repo/recipes/akmods.yml +++ b/integration-tests/test-repo/recipes/akmods.yml @@ -1,3 +1,5 @@ +--- +# yaml-language-server: $schema=https://schema.blue-build.org/module-list-v1.json modules: # Tests installing rpms from a combo image stage - type: akmods diff --git a/integration-tests/test-repo/recipes/flatpaks.yml b/integration-tests/test-repo/recipes/flatpaks.yml index e32cc8b4..39de41ce 100644 --- a/integration-tests/test-repo/recipes/flatpaks.yml +++ b/integration-tests/test-repo/recipes/flatpaks.yml @@ -1,3 +1,5 @@ +--- +# yaml-language-server: $schema=https://schema.blue-build.org/module-v1.json type: default-flatpaks notify: true system: diff --git a/integration-tests/test-repo/recipes/recipe-arm64.yml b/integration-tests/test-repo/recipes/recipe-arm64.yml index 66539261..5b5d9015 100644 --- a/integration-tests/test-repo/recipes/recipe-arm64.yml +++ b/integration-tests/test-repo/recipes/recipe-arm64.yml @@ -4,7 +4,7 @@ base-image: quay.io/fedora/fedora-silverblue image-version: 40 alt_tags: - arm64 -stages: +stages: [] modules: - from-file: flatpaks.yml diff --git a/process/Cargo.toml b/process/Cargo.toml index b722c02f..1d73b641 100644 --- a/process/Cargo.toml +++ b/process/Cargo.toml @@ -36,6 +36,7 @@ indexmap.workspace = true log.workspace = true miette.workspace = true oci-distribution.workspace = true +reqwest.workspace = true serde.workspace = true serde_json.workspace = true tempdir.workspace = true diff --git a/recipe/src/lib.rs b/recipe/src/lib.rs index 4d041191..b74671d4 100644 --- a/recipe/src/lib.rs +++ b/recipe/src/lib.rs @@ -5,9 +5,26 @@ pub mod recipe; pub mod stage; pub mod stages_ext; +use std::path::Path; + +use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH}; +use log::warn; + pub use akmods_info::*; pub use module::*; pub use module_ext::*; pub use recipe::*; pub use stage::*; pub use stages_ext::*; + +pub(crate) fn base_recipe_path() -> &'static Path { + let legacy_path = Path::new(CONFIG_PATH); + let recipe_path = Path::new(RECIPE_PATH); + + if recipe_path.exists() && recipe_path.is_dir() { + recipe_path + } else { + warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}"); + legacy_path + } +} diff --git a/recipe/src/module.rs b/recipe/src/module.rs index ddfb390b..ee894334 100644 --- a/recipe/src/module.rs +++ b/recipe/src/module.rs @@ -9,7 +9,7 @@ use miette::{bail, Result}; use serde::{Deserialize, Serialize}; use serde_yaml::Value; -use crate::{AkmodsInfo, ModuleExt}; +use crate::{base_recipe_path, AkmodsInfo, ModuleExt}; #[derive(Serialize, Deserialize, Debug, Clone, Builder, Default)] pub struct ModuleRequiredFields<'a> { @@ -164,7 +164,7 @@ pub struct Module<'a> { pub from_file: Option>, } -impl<'a> Module<'a> { +impl Module<'_> { /// Get's any child modules. /// /// # Errors @@ -202,7 +202,7 @@ impl<'a> Module<'a> { traversed_files.push(file_name.clone()); Self::get_modules( - &ModuleExt::parse(&file_name)?.modules, + &ModuleExt::try_from(&file_name)?.modules, Some(traversed_files), )? } @@ -224,6 +224,13 @@ impl<'a> Module<'a> { Ok(found_modules) } + #[must_use] + pub fn get_from_file_path(&self) -> Option { + self.from_file + .as_ref() + .map(|path| base_recipe_path().join(&**path)) + } + #[must_use] pub fn example() -> Self { Self::builder() diff --git a/recipe/src/module_ext.rs b/recipe/src/module_ext.rs index 2e45dab6..9d219b15 100644 --- a/recipe/src/module_ext.rs +++ b/recipe/src/module_ext.rs @@ -1,12 +1,15 @@ -use std::{collections::HashSet, fs, path::Path}; +use std::{ + collections::HashSet, + fs, + path::{Path, PathBuf}, +}; -use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH}; use bon::Builder; -use log::{trace, warn}; -use miette::{Context, IntoDiagnostic, Result}; +use log::trace; +use miette::{Context, IntoDiagnostic, Report, Result}; use serde::{Deserialize, Serialize}; -use crate::{AkmodsInfo, Module}; +use crate::{base_recipe_path, AkmodsInfo, Module}; #[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)] pub struct ModuleExt<'a> { @@ -15,21 +18,28 @@ pub struct ModuleExt<'a> { } impl ModuleExt<'_> { - /// Parse a module file returning a [`ModuleExt`] - /// - /// # Errors - /// Can return an `anyhow` Error if the file cannot be read or deserialized - /// into a [`ModuleExt`] - pub fn parse(file_name: &Path) -> Result { - let legacy_path = Path::new(CONFIG_PATH); - let recipe_path = Path::new(RECIPE_PATH); + #[must_use] + pub fn get_from_file_paths(&self) -> Vec { + self.modules + .iter() + .filter_map(Module::get_from_file_path) + .collect() + } +} - let file_path = if recipe_path.exists() && recipe_path.is_dir() { - recipe_path.join(file_name) - } else { - warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}"); - legacy_path.join(file_name) - }; +impl TryFrom<&PathBuf> for ModuleExt<'_> { + type Error = Report; + + fn try_from(value: &PathBuf) -> std::result::Result { + Self::try_from(value.as_path()) + } +} + +impl TryFrom<&Path> for ModuleExt<'_> { + type Error = Report; + + fn try_from(file_name: &Path) -> Result { + let file_path = base_recipe_path().join(file_name); let file = fs::read_to_string(&file_path) .into_diagnostic() @@ -45,7 +55,9 @@ impl ModuleExt<'_> { Ok, ) } +} +impl ModuleExt<'_> { #[must_use] pub fn get_akmods_info_list(&self, os_version: &u64) -> Vec { trace!("get_akmods_image_list({self:#?}, {os_version})"); diff --git a/recipe/src/recipe.rs b/recipe/src/recipe.rs index 4fe54ee5..122d75d3 100644 --- a/recipe/src/recipe.rs +++ b/recipe/src/recipe.rs @@ -1,12 +1,10 @@ use std::{borrow::Cow, fs, path::Path}; use bon::Builder; -use indexmap::IndexMap; use log::{debug, trace}; use miette::{Context, IntoDiagnostic, Result}; use oci_distribution::Reference; use serde::{Deserialize, Serialize}; -use serde_yaml::Value; use crate::{Module, ModuleExt, StagesExt}; @@ -69,13 +67,6 @@ pub struct Recipe<'a> { /// This holds the list of modules to be run on the image. #[serde(flatten)] pub modules_ext: ModuleExt<'a>, - - /// Extra data that the user might have added. This is - /// done in case we serialize the data to a yaml file - /// so that we retain any unused information. - #[serde(flatten)] - #[builder(into)] - pub extra: IndexMap, } impl<'a> Recipe<'a> { diff --git a/recipe/src/stage.rs b/recipe/src/stage.rs index dff8875b..0b4bb6b4 100644 --- a/recipe/src/stage.rs +++ b/recipe/src/stage.rs @@ -6,7 +6,7 @@ use colored::Colorize; use miette::{bail, Result}; use serde::{Deserialize, Serialize}; -use crate::{Module, ModuleExt, StagesExt}; +use crate::{base_recipe_path, Module, ModuleExt, StagesExt}; /// Contains the required fields for a stage. #[derive(Serialize, Deserialize, Debug, Clone, Builder)] @@ -86,7 +86,7 @@ pub struct Stage<'a> { pub from_file: Option>, } -impl<'a> Stage<'a> { +impl Stage<'_> { /// Get's any child stages. /// /// # Errors @@ -119,7 +119,7 @@ impl<'a> Stage<'a> { let mut tf = traversed_files.clone(); tf.push(file_name.clone()); - Self::get_stages(&StagesExt::parse(&file_name)?.stages, Some(tf))? + Self::get_stages(&StagesExt::try_from(&file_name)?.stages, Some(tf))? } _ => { let from_example = Stage::builder().from_file("path/to/stage.yml").build(); @@ -139,6 +139,13 @@ impl<'a> Stage<'a> { Ok(found_stages) } + #[must_use] + pub fn get_from_file_path(&self) -> Option { + self.from_file + .as_ref() + .map(|path| base_recipe_path().join(&**path)) + } + #[must_use] pub fn example() -> Self { Stage::builder() diff --git a/recipe/src/stages_ext.rs b/recipe/src/stages_ext.rs index ae8ebd7a..eb396756 100644 --- a/recipe/src/stages_ext.rs +++ b/recipe/src/stages_ext.rs @@ -1,12 +1,13 @@ -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, +}; -use blue_build_utils::constants::{CONFIG_PATH, RECIPE_PATH}; use bon::Builder; -use log::warn; -use miette::{Context, IntoDiagnostic, Result}; +use miette::{Context, IntoDiagnostic, Report, Result}; use serde::{Deserialize, Serialize}; -use crate::{Module, Stage}; +use crate::{base_recipe_path, Module, Stage}; #[derive(Default, Serialize, Clone, Deserialize, Debug, Builder)] pub struct StagesExt<'a> { @@ -14,22 +15,29 @@ pub struct StagesExt<'a> { pub stages: Vec>, } -impl<'a> StagesExt<'a> { - /// Parse a module file returning a [`StagesExt`] - /// - /// # Errors - /// Can return an `anyhow` Error if the file cannot be read or deserialized - /// into a [`StagesExt`] - pub fn parse(file_name: &Path) -> Result { - let legacy_path = Path::new(CONFIG_PATH); - let recipe_path = Path::new(RECIPE_PATH); - - let file_path = if recipe_path.exists() && recipe_path.is_dir() { - recipe_path.join(file_name) - } else { - warn!("Use of {CONFIG_PATH} for recipes is deprecated, please move your recipe files into {RECIPE_PATH}"); - legacy_path.join(file_name) - }; +impl StagesExt<'_> { + #[must_use] + pub fn get_from_file_paths(&self) -> Vec { + self.stages + .iter() + .filter_map(Stage::get_from_file_path) + .collect() + } +} + +impl TryFrom<&PathBuf> for StagesExt<'_> { + type Error = Report; + + fn try_from(value: &PathBuf) -> Result { + Self::try_from(value.as_path()) + } +} + +impl TryFrom<&Path> for StagesExt<'_> { + type Error = Report; + + fn try_from(file_name: &Path) -> Result { + let file_path = base_recipe_path().join(file_name); let file = fs::read_to_string(&file_path) .into_diagnostic() diff --git a/src/bin/bluebuild.rs b/src/bin/bluebuild.rs index 06776492..879af089 100644 --- a/src/bin/bluebuild.rs +++ b/src/bin/bluebuild.rs @@ -12,6 +12,7 @@ fn main() { ("hyper::proto", LevelFilter::Off), ("hyper_util", LevelFilter::Off), ("oci_distribution", LevelFilter::Off), + ("reqwest", LevelFilter::Off), ]) .log_out_dir(args.log_out.clone()) .init(); @@ -42,6 +43,9 @@ fn main() { #[cfg(feature = "iso")] CommandArgs::GenerateIso(mut command) => command.run(), + #[cfg(feature = "validate")] + CommandArgs::Validate(mut command) => command.run(), + CommandArgs::BugReport(mut command) => command.run(), CommandArgs::Completions(mut command) => command.run(), diff --git a/src/commands.rs b/src/commands.rs index 5c6c2b97..d31e1082 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -15,6 +15,8 @@ pub mod generate; pub mod generate_iso; #[cfg(feature = "login")] pub mod login; +#[cfg(feature = "validate")] +pub mod validate; // #[cfg(feature = "init")] // pub mod init; #[cfg(not(feature = "switch"))] @@ -115,6 +117,9 @@ pub enum CommandArgs { #[cfg(feature = "login")] Login(login::LoginCommand), + #[cfg(feature = "validate")] + Validate(Box), + // /// Initialize a new Ublue Starting Point repo // #[cfg(feature = "init")] // Init(init::InitCommand), diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 95736771..16bb6753 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -18,6 +18,8 @@ use clap::{crate_version, Args}; use log::{debug, info, trace, warn}; use miette::{IntoDiagnostic, Result}; +#[cfg(feature = "validate")] +use crate::commands::validate::ValidateCommand; use crate::shadow; use super::BlueBuildCommand; @@ -99,6 +101,13 @@ impl GenerateCommand { legacy_path.join(RECIPE_FILE) } }); + + #[cfg(feature = "validate")] + ValidateCommand::builder() + .recipe(recipe_path.clone()) + .build() + .try_run()?; + let registry = if let (Some(registry), Some(registry_namespace)) = (&self.registry, &self.registry_namespace) { diff --git a/src/commands/validate.rs b/src/commands/validate.rs new file mode 100644 index 00000000..9ee64e6f --- /dev/null +++ b/src/commands/validate.rs @@ -0,0 +1,399 @@ +use std::{ + fs::OpenOptions, + io::{BufReader, Read}, + path::{Path, PathBuf}, +}; + +use blue_build_recipe::{ModuleExt, Recipe, StagesExt}; +use blue_build_utils::traits::AsRefCollector; +use bon::Builder; +use cached::proc_macro::cached; +use clap::Args; +use colored::Colorize; +use jsonschema::{Retrieve, Uri, ValidationError, Validator}; +use log::{debug, info, trace}; +use miette::{bail, miette, Context, IntoDiagnostic, Report}; +use rayon::prelude::*; +use serde_json::Value; + +use super::BlueBuildCommand; + +const BASE_SCHEMA_URL: &str = "https://schema.blue-build.org"; +const RECIPE_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/recipe-v1.json"; +const STAGE_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/stage-v1.json"; +const STAGE_LIST_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/stage-list-v1.json"; +const MODULE_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/module-v1.json"; +const MODULE_LIST_V1_SCHEMA_URL: &str = "https://schema.blue-build.org/module-list-v1.json"; + +#[derive(Debug, Args, Builder)] +pub struct ValidateCommand { + pub recipe: PathBuf, + + #[clap(skip)] + recipe_validator: Option, + + #[clap(skip)] + stage_validator: Option, + + #[clap(skip)] + stage_list_validator: Option, + + #[clap(skip)] + module_validator: Option, + + #[clap(skip)] + module_list_validator: Option, +} + +impl BlueBuildCommand for ValidateCommand { + fn try_run(&mut self) -> miette::Result<()> { + let recipe_path_display = self.recipe.display().to_string().bold().italic(); + + if !self.recipe.is_file() { + bail!("File {recipe_path_display} must exist"); + } + + self.setup_validators()?; + + if let Err(errors) = self.validate_recipe() { + bail!( + "Recipe {recipe_path_display} failed to validate:\n{}", + errors.into_iter().fold(String::new(), |mut full, err| { + full.push_str(&format!("{err:?}")); + full + }) + ); + } + info!("Recipe {recipe_path_display} is valid"); + + Ok(()) + } +} + +impl ValidateCommand { + fn setup_validators(&mut self) -> Result<(), Report> { + self.recipe_validator = Some(build_validator(RECIPE_V1_SCHEMA_URL)?); + self.stage_validator = Some(build_validator(STAGE_V1_SCHEMA_URL)?); + self.stage_list_validator = Some(build_validator(STAGE_LIST_V1_SCHEMA_URL)?); + self.module_validator = Some(build_validator(MODULE_V1_SCHEMA_URL)?); + self.module_list_validator = Some(build_validator(MODULE_LIST_V1_SCHEMA_URL)?); + Ok(()) + } + + fn validate_stage_file<'a>( + &self, + path: &'a Path, + mut traversed_files: Vec<&'a Path>, + ) -> Result<(), Vec> { + let path_display = path.display().to_string().bold().italic(); + + if traversed_files.contains(&path) { + return Err(vec![miette!( + "{} File {path_display} has already been parsed:\n{traversed_files:?}", + "Circular dependency detected!".bright_red(), + )]); + } + traversed_files.push(path); + + let stage_str = read_file(path).map_err(err_vec)?; + let stage: Value = serde_yaml::from_str(&stage_str) + .into_diagnostic() + .with_context(|| format!("Failed to deserialize stage {path_display}")) + .map_err(err_vec)?; + trace!("{path_display}:\n{stage}"); + + self.stage_validator + .as_ref() + .unwrap() + .validate(&stage) + .map_err(validate_err(path)) + .or_else(|mut e1| { + self.stage_list_validator + .as_ref() + .unwrap() + .validate(&stage) + .map_err(validate_err(path)) + .map_err(|e2| { + e1.extend(e2); + e1 + }) + .and_then(|()| { + debug!("{path_display} is a multi stage file"); + + let stages: StagesExt = serde_yaml::from_str(&stage_str) + .into_diagnostic() + .map_err(err_vec)?; + + let errors = stages + .get_from_file_paths() + .par_iter() + .map(|stage_path| { + debug!( + "Found 'from-file' reference in {path_display} going to {}", + stage_path.display().to_string().italic().bold() + ); + + self.validate_stage_file( + stage_path, + traversed_files.collect_as_ref_vec(), + ) + }) + .filter_map(Result::err) + .flatten() + .collect::>(); + + if !errors.is_empty() { + return Err(errors); + } + + Ok(()) + }) + })?; + + Ok(()) + } + + fn validate_module_file<'a>( + &self, + path: &'a Path, + mut traversed_files: Vec<&'a Path>, + ) -> Result<(), Vec> { + let path_display = path.display().to_string().bold().italic(); + debug!("Validating module file {path_display}"); + + if traversed_files.contains(&path) { + return Err(vec![miette!( + "{} File {path_display} has already been parsed:\n{traversed_files:?}", + "Circular dependency detected!".bright_red(), + )]); + } + traversed_files.push(path); + + let module_str = read_file(path).map_err(err_vec)?; + let module: Value = serde_yaml::from_str(&module_str) + .into_diagnostic() + .with_context(|| format!("Failed to deserialize module {path_display}")) + .map_err(err_vec)?; + trace!("{path_display}:\n{module}"); + + self.module_validator + .as_ref() + .unwrap() + .validate(&module) + .map_err(validate_err(path)) + .or_else(|mut e1| { + self.module_list_validator + .as_ref() + .unwrap() + .validate(&module) + .map_err(validate_err(path)) + .map_err(|e2| { + e1.extend(e2); + e1 + }) + .and_then(|()| { + debug!("{path_display} is a multi module file"); + + let modules: ModuleExt = serde_yaml::from_str(&module_str) + .into_diagnostic() + .map_err(err_vec)?; + + let errors = modules + .get_from_file_paths() + .par_iter() + .map(|module_path| { + debug!( + "Found 'from-file' reference in {path_display} going to {}", + module_path.display().to_string().italic().bold() + ); + + self.validate_module_file( + module_path, + traversed_files.collect_as_ref_vec(), + ) + }) + .filter_map(Result::err) + .flatten() + .collect::>(); + + if !errors.is_empty() { + return Err(errors); + } + + Ok(()) + }) + })?; + + Ok(()) + } + + fn validate_recipe(&self) -> Result<(), Vec> { + let recipe_path_display = self.recipe.display().to_string().bold().italic(); + debug!("Validating recipe {recipe_path_display}"); + + let recipe_str = read_file(&self.recipe).map_err(err_vec)?; + let recipe: Value = serde_yaml::from_str(&recipe_str) + .into_diagnostic() + .with_context(|| format!("Failed to deserialize recipe {recipe_path_display}")) + .map_err(err_vec)?; + trace!("{recipe_path_display}:\n{recipe}"); + + self.recipe_validator + .as_ref() + .unwrap() + .validate(&recipe) + .map_err(validate_err(&self.recipe))?; + let recipe: Recipe = serde_yaml::from_str(&recipe_str) + .into_diagnostic() + .with_context(|| format!("Unable to convert Value to Recipe for {recipe_path_display}")) + .map_err(err_vec)?; + + let mut errors: Vec = Vec::new(); + if let Some(stages) = &recipe.stages_ext { + debug!("Validating stages for recipe {recipe_path_display}"); + + errors.extend( + stages + .get_from_file_paths() + .par_iter() + .map(|stage_path| { + debug!( + "Found 'from-file' reference in {recipe_path_display} going to {}", + stage_path.display().to_string().italic().bold() + ); + self.validate_stage_file(stage_path, vec![]) + }) + .filter_map(Result::err) + .flatten() + .collect::>(), + ); + } + + debug!("Validating modules for recipe {recipe_path_display}"); + errors.extend( + recipe + .modules_ext + .get_from_file_paths() + .par_iter() + .map(|module_path| { + debug!( + "Found 'from-file' reference in {recipe_path_display} going to {}", + module_path.display().to_string().italic().bold() + ); + self.validate_module_file(module_path, vec![]) + }) + .filter_map(Result::err) + .flatten() + .collect::>(), + ); + if errors.is_empty() { + Ok(()) + } else { + Err(errors) + } + } +} + +fn err_vec(err: Report) -> Vec { + vec![err] +} + +fn build_validator(url: &str) -> Result { + let recipe_schema = reqwest::blocking::get(url) + .into_diagnostic() + .with_context(|| format!("Failed to get schema at {url}"))? + .json() + .into_diagnostic() + .with_context(|| format!("Failed to get json for schema {url}"))?; + jsonschema::options() + .with_retriever(ModuleSchemaRetriever) + .build(&recipe_schema) + .into_diagnostic() +} + +fn read_file(path: &Path) -> Result { + let mut recipe = String::new(); + BufReader::new( + OpenOptions::new() + .read(true) + .open(path) + .into_diagnostic() + .with_context(|| { + format!( + "Unable to open {}", + path.display().to_string().italic().bold() + ) + })?, + ) + .read_to_string(&mut recipe) + .into_diagnostic()?; + Ok(recipe) +} + +fn validate_err<'a, 'b, I>(path: &'b Path) -> impl Fn(I) -> Vec + 'b +where + I: Iterator>, +{ + |errors: I| { + errors + .map(|err| { + let instance = err.instance; + let dot_path = err + .instance_path + .into_iter() + .map(|p| match p { + jsonschema::paths::PathChunk::Property(prop) => format!(".{prop}"), + jsonschema::paths::PathChunk::Index(ind) => format!("[{ind}]"), + jsonschema::paths::PathChunk::Keyword(key) => key.to_string(), + }) + .collect::(); + + miette!( + "- Invalid value at path '{}' in file '{}':\n{}", + dot_path.bold().bright_yellow(), + path.display().to_string().italic().bold(), + serde_yaml::to_string(&*instance).unwrap_or_else(|_| instance.to_string()) + ) + }) + .collect() + } +} + +struct ModuleSchemaRetriever; + +impl Retrieve for ModuleSchemaRetriever { + fn retrieve( + &self, + uri: &Uri<&str>, + ) -> Result> { + Ok(cache_retrieve(uri)?) + } +} + +#[cached( + result = true, + key = "String", + sync_writes = true, + convert = r#"{ format!("{uri}") }"# +)] +fn cache_retrieve(uri: &Uri<&str>) -> miette::Result { + let scheme = uri.scheme(); + let path = uri.path(); + + let uri = match scheme.as_str() { + "json-schema" => { + format!("{BASE_SCHEMA_URL}{path}") + } + "https" => uri.to_string(), + scheme => bail!("Unknown scheme {scheme}"), + }; + + debug!("Retrieving schema from {}", uri.bold().italic()); + reqwest::blocking::get(&uri) + .into_diagnostic() + .with_context(|| format!("Failed to retrieve schema from {uri}"))? + .json() + .into_diagnostic() + .with_context(|| format!("Failed to parse json from {uri}")) + .inspect(|value| trace!("{}:\n{value}", uri.bold().italic())) +}