From 48f8db1236dcbc8ac0431ccdd237e896b9bd823b Mon Sep 17 00:00:00 2001 From: Till Wegmueller Date: Sun, 15 Feb 2026 15:30:22 +0100 Subject: [PATCH] Initial implementation of refraction-forger Standalone workspace with 4 crates for building optimized OS images and publishing to OCI registries: - spec-parser: KDL image spec parsing with include resolution and profile-based conditional filtering - forge-oci: OCI image creation (tar layers, manifests, Image Layout) and registry push via oci-client - forge-engine: Build pipeline with Phase 1 (rootfs assembly via native package managers with -R) and Phase 2 (QCOW2/OCI/artifact targets), plus dyn-compatible ToolRunner trait for external tool execution - forger: CLI binary with build, validate, inspect, push, and targets commands Ported KDL specs and overlay files from the vm-manager prototype. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + Cargo.lock | 2916 +++++++++++++++++ Cargo.toml | 58 + crates/forge-engine/Cargo.toml | 22 + crates/forge-engine/src/error.rs | 123 + crates/forge-engine/src/lib.rs | 89 + .../forge-engine/src/phase1/customizations.rs | 63 + crates/forge-engine/src/phase1/mod.rs | 94 + crates/forge-engine/src/phase1/overlays.rs | 323 ++ crates/forge-engine/src/phase1/packages.rs | 25 + crates/forge-engine/src/phase1/staging.rs | 47 + crates/forge-engine/src/phase1/variants.rs | 18 + crates/forge-engine/src/phase2/artifact.rs | 67 + crates/forge-engine/src/phase2/mod.rs | 41 + crates/forge-engine/src/phase2/oci.rs | 52 + crates/forge-engine/src/phase2/qcow2.rs | 150 + crates/forge-engine/src/tools/bootloader.rs | 78 + crates/forge-engine/src/tools/devfsadm.rs | 18 + crates/forge-engine/src/tools/loopback.rs | 56 + crates/forge-engine/src/tools/mod.rs | 70 + crates/forge-engine/src/tools/pkg.rs | 93 + crates/forge-engine/src/tools/qemu_img.rs | 40 + crates/forge-engine/src/tools/zfs.rs | 54 + crates/forge-engine/src/tools/zpool.rs | 56 + crates/forge-oci/Cargo.toml | 21 + crates/forge-oci/src/layout.rs | 113 + crates/forge-oci/src/lib.rs | 4 + crates/forge-oci/src/manifest.rs | 136 + crates/forge-oci/src/registry.rs | 121 + crates/forge-oci/src/tar_layer.rs | 126 + crates/forger/Cargo.toml | 19 + crates/forger/src/commands/build.rs | 58 + crates/forger/src/commands/inspect.rs | 132 + crates/forger/src/commands/mod.rs | 5 + crates/forger/src/commands/push.rs | 126 + crates/forger/src/commands/targets.rs | 36 + crates/forger/src/commands/validate.rs | 25 + crates/forger/src/main.rs | 120 + crates/spec-parser/Cargo.toml | 14 + crates/spec-parser/src/lib.rs | 152 + crates/spec-parser/src/profile.rs | 107 + crates/spec-parser/src/resolve.rs | 322 ++ crates/spec-parser/src/schema.rs | 297 ++ images/common.kdl | 20 + images/devfs.kdl | 23 + images/files/boot_console.ttya | 4 + images/files/default_init.utc | 4 + images/files/etc/hosts | 5 + images/files/etc/nodename | 1 + images/files/etc/omnios_sshd_config | 122 + images/files/etc/resolv.conf | 2 + images/files/etc/sshd_config | 77 + images/files/etc/system | 2 + images/files/omniosce-ca.cert.pem | 0 images/files/ttydefs.115200 | 62 + images/omnios-bloody-base.kdl | 47 + images/omnios-bloody-disk.kdl | 70 + 57 files changed, 6927 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/forge-engine/Cargo.toml create mode 100644 crates/forge-engine/src/error.rs create mode 100644 crates/forge-engine/src/lib.rs create mode 100644 crates/forge-engine/src/phase1/customizations.rs create mode 100644 crates/forge-engine/src/phase1/mod.rs create mode 100644 crates/forge-engine/src/phase1/overlays.rs create mode 100644 crates/forge-engine/src/phase1/packages.rs create mode 100644 crates/forge-engine/src/phase1/staging.rs create mode 100644 crates/forge-engine/src/phase1/variants.rs create mode 100644 crates/forge-engine/src/phase2/artifact.rs create mode 100644 crates/forge-engine/src/phase2/mod.rs create mode 100644 crates/forge-engine/src/phase2/oci.rs create mode 100644 crates/forge-engine/src/phase2/qcow2.rs create mode 100644 crates/forge-engine/src/tools/bootloader.rs create mode 100644 crates/forge-engine/src/tools/devfsadm.rs create mode 100644 crates/forge-engine/src/tools/loopback.rs create mode 100644 crates/forge-engine/src/tools/mod.rs create mode 100644 crates/forge-engine/src/tools/pkg.rs create mode 100644 crates/forge-engine/src/tools/qemu_img.rs create mode 100644 crates/forge-engine/src/tools/zfs.rs create mode 100644 crates/forge-engine/src/tools/zpool.rs create mode 100644 crates/forge-oci/Cargo.toml create mode 100644 crates/forge-oci/src/layout.rs create mode 100644 crates/forge-oci/src/lib.rs create mode 100644 crates/forge-oci/src/manifest.rs create mode 100644 crates/forge-oci/src/registry.rs create mode 100644 crates/forge-oci/src/tar_layer.rs create mode 100644 crates/forger/Cargo.toml create mode 100644 crates/forger/src/commands/build.rs create mode 100644 crates/forger/src/commands/inspect.rs create mode 100644 crates/forger/src/commands/mod.rs create mode 100644 crates/forger/src/commands/push.rs create mode 100644 crates/forger/src/commands/targets.rs create mode 100644 crates/forger/src/commands/validate.rs create mode 100644 crates/forger/src/main.rs create mode 100644 crates/spec-parser/Cargo.toml create mode 100644 crates/spec-parser/src/lib.rs create mode 100644 crates/spec-parser/src/profile.rs create mode 100644 crates/spec-parser/src/resolve.rs create mode 100644 crates/spec-parser/src/schema.rs create mode 100644 images/common.kdl create mode 100644 images/devfs.kdl create mode 100644 images/files/boot_console.ttya create mode 100644 images/files/default_init.utc create mode 100644 images/files/etc/hosts create mode 100644 images/files/etc/nodename create mode 100644 images/files/etc/omnios_sshd_config create mode 100644 images/files/etc/resolv.conf create mode 100644 images/files/etc/sshd_config create mode 100644 images/files/etc/system create mode 100644 images/files/omniosce-ca.cert.pem create mode 100644 images/files/ttydefs.115200 create mode 100644 images/omnios-bloody-base.kdl create mode 100644 images/omnios-bloody-disk.kdl diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..755734a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2916 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytesize" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chumsky" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "clap" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.115", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.115", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[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.115", +] + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "forge-engine" +version = "0.1.0" +dependencies = [ + "bytesize", + "flate2", + "forge-oci", + "hex", + "miette 7.6.0", + "serde", + "serde_json", + "sha2", + "spec-parser", + "tar", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "walkdir", +] + +[[package]] +name = "forge-oci" +version = "0.1.0" +dependencies = [ + "bytes", + "flate2", + "hex", + "miette 7.6.0", + "oci-client", + "oci-spec", + "serde", + "serde_json", + "sha2", + "tar", + "thiserror 2.0.18", + "tokio", + "tracing", + "walkdir", +] + +[[package]] +name = "forger" +version = "0.1.0" +dependencies = [ + "clap", + "forge-engine", + "forge-oci", + "indicatif", + "miette 7.6.0", + "serde", + "serde_json", + "spec-parser", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-auth" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "150fa4a9462ef926824cf4519c84ed652ca8f4fbae34cb8af045b5cbcaf98822" +dependencies = [ + "memchr", +] + +[[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.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.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 = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[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 = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "knuffel" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04bee6ddc6071011314b1ce4f7705fef6c009401dba4fd22cb0009db6a177413" +dependencies = [ + "base64 0.21.7", + "chumsky", + "knuffel-derive", + "miette 5.10.0", + "thiserror 1.0.69", + "unicode-width 0.1.14", +] + +[[package]] +name = "knuffel-derive" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91977f56c49cfb961e3d840e2e7c6e4a56bde7283898cf606861f1421348283d" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libredox" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "miette" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +dependencies = [ + "miette-derive 5.10.0", + "once_cell", + "thiserror 1.0.69", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive 7.6.0", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5d26952a508f321b4d3d2e80e78fc2603eaefcdf0c30783867f19586518bdc" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "oci-client" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b74df13319e08bc386d333d3dc289c774c88cc543cae31f5347db07b5ec2172" +dependencies = [ + "bytes", + "chrono", + "futures-util", + "http", + "http-auth", + "jwt", + "lazy_static", + "oci-spec", + "olpc-cjson", + "regex", + "reqwest", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tracing", + "unicase", +] + +[[package]] +name = "oci-spec" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3da52b83ce3258fbf29f66ac784b279453c2ac3c22c5805371b921ede0d308" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror 2.0.18", +] + +[[package]] +name = "olpc-cjson" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696183c9b5fe81a7715d074fd632e8bd46f4ccc0231a3ed7fc580a80de5f7083" +dependencies = [ + "serde", + "serde_json", + "unicode-normalization", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[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 = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.115", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +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 = "redox_syscall" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustix" +version = "1.1.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-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "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 = "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_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "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 = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "spec-parser" +version = "0.1.0" +dependencies = [ + "knuffel", + "miette 7.6.0", + "serde", + "tempfile", + "thiserror 2.0.18", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.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 = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen 0.46.0", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.115", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.115", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.115", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..298cd3d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,58 @@ +[workspace] +resolver = "3" +members = [ + "crates/spec-parser", + "crates/forge-oci", + "crates/forge-engine", + "crates/forger", +] + +[workspace.package] +edition = "2024" +rust-version = "1.85" + +[workspace.dependencies] +# Parsing +knuffel = "3.2" + +# Error handling & diagnostics +miette = { version = "7", features = ["fancy"] } +thiserror = "2" + +# CLI +clap = { version = "4.5", features = ["derive", "env"] } + +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Async runtime +tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "process", "time"] } + +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +# OCI +oci-spec = "0.8" +oci-client = "0.15" + +# Crypto & encoding +sha2 = "0.10" +hex = "0.4" + +# IO +bytes = "1" +tar = "0.4" +flate2 = "1" +walkdir = "2" +tempfile = "3" + +# Misc +bytesize = "2" +indicatif = "0.17" + +# Internal crates +spec-parser = { path = "crates/spec-parser" } +forge-oci = { path = "crates/forge-oci" } +forge-engine = { path = "crates/forge-engine" } diff --git a/crates/forge-engine/Cargo.toml b/crates/forge-engine/Cargo.toml new file mode 100644 index 0000000..139ccba --- /dev/null +++ b/crates/forge-engine/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "forge-engine" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +spec-parser = { workspace = true } +forge-oci = { workspace = true } +miette = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tempfile = { workspace = true } +walkdir = { workspace = true } +bytesize = { workspace = true } +tar = { workspace = true } +flate2 = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } diff --git a/crates/forge-engine/src/error.rs b/crates/forge-engine/src/error.rs new file mode 100644 index 0000000..5f42853 --- /dev/null +++ b/crates/forge-engine/src/error.rs @@ -0,0 +1,123 @@ +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug, Error, Diagnostic)] +pub enum ForgeError { + #[error("Staging directory setup failed")] + #[diagnostic( + help("Ensure sufficient disk space and write permissions for temporary directories"), + code(forge::staging_failed) + )] + StagingSetup(#[source] std::io::Error), + + #[error("Base tarball extraction failed: {path}")] + #[diagnostic( + help("Verify the base tarball exists and is a valid tar.gz or tar archive"), + code(forge::base_extract_failed) + )] + BaseExtract { + path: String, + #[source] + source: std::io::Error, + }, + + #[error("Package manager operation failed: {operation}")] + #[diagnostic( + help("Check that the package manager is available on the build host and the repositories are reachable.\nCommand: {command}"), + code(forge::pkg_failed) + )] + PackageManager { + operation: String, + command: String, + detail: String, + }, + + #[error("Tool execution failed: `{tool} {args}`")] + #[diagnostic( + help("Ensure '{tool}' is installed and available in PATH.\nStderr: {stderr}"), + code(forge::tool_failed) + )] + ToolExecution { + tool: String, + args: String, + stderr: String, + #[source] + source: std::io::Error, + }, + + #[error("Tool returned non-zero exit code: `{tool} {args}` (exit code {exit_code})")] + #[diagnostic( + help("The command failed. Check stderr output below for details.\nStderr: {stderr}"), + code(forge::tool_exit_code) + )] + ToolNonZero { + tool: String, + args: String, + exit_code: i32, + stderr: String, + }, + + #[error("Overlay application failed: {action}")] + #[diagnostic( + help("Check that the source file exists and the destination path is valid.\n{detail}"), + code(forge::overlay_failed) + )] + Overlay { + action: String, + detail: String, + #[source] + source: std::io::Error, + }, + + #[error("Overlay source file not found: {path}")] + #[diagnostic( + help("Ensure the file exists relative to the images/files/ directory"), + code(forge::overlay_source_missing) + )] + OverlaySourceMissing { path: String }, + + #[error("Customization failed: {operation}")] + #[diagnostic( + help("{detail}"), + code(forge::customization_failed) + )] + Customization { + operation: String, + detail: String, + }, + + #[error("QCOW2 image creation failed: {step}")] + #[diagnostic( + help("{detail}"), + code(forge::qcow2_failed) + )] + Qcow2Build { step: String, detail: String }, + + #[error("OCI image creation failed")] + #[diagnostic(code(forge::oci_failed))] + OciBuild(String), + + #[error("Disk size not specified for qcow2 target")] + #[diagnostic( + help("Add a `disk_size \"2000M\"` child node to your qcow2 target block"), + code(forge::missing_disk_size) + )] + MissingDiskSize, + + #[error("Invalid disk size: {value}")] + #[diagnostic( + help("Use a value like \"2000M\" or \"20G\""), + code(forge::invalid_disk_size) + )] + InvalidDiskSize { value: String }, + + #[error("No target named '{name}' found in spec")] + #[diagnostic( + help("Available targets: {available}. Use `forger targets` to list them."), + code(forge::target_not_found) + )] + TargetNotFound { name: String, available: String }, + + #[error("IO error")] + Io(#[from] std::io::Error), +} diff --git a/crates/forge-engine/src/lib.rs b/crates/forge-engine/src/lib.rs new file mode 100644 index 0000000..fb008a7 --- /dev/null +++ b/crates/forge-engine/src/lib.rs @@ -0,0 +1,89 @@ +pub mod error; +pub mod phase1; +pub mod phase2; +pub mod tools; + +use std::path::Path; + +use error::ForgeError; +use spec_parser::schema::{ImageSpec, Target, TargetKind}; +use tools::ToolRunner; +use tracing::info; + +/// Context for running a build. +pub struct BuildContext<'a> { + /// The resolved and profile-filtered image spec. + pub spec: &'a ImageSpec, + /// Directory containing overlay source files (images/files/). + pub files_dir: &'a Path, + /// Output directory for build artifacts. + pub output_dir: &'a Path, + /// Tool runner for executing external commands. + pub runner: &'a dyn ToolRunner, +} + +impl<'a> BuildContext<'a> { + /// Build a specific target by name, or all targets if name is None. + pub async fn build(&self, target_name: Option<&str>) -> Result<(), ForgeError> { + let targets = self.select_targets(target_name)?; + + std::fs::create_dir_all(self.output_dir)?; + + for target in targets { + info!(target = %target.name, kind = %target.kind, "Building target"); + + // Phase 1: Assemble rootfs + let phase1_result = + phase1::execute(self.spec, self.files_dir, self.runner).await?; + + // Phase 2: Produce target artifact + phase2::execute( + target, + &phase1_result.staging_root, + self.files_dir, + self.output_dir, + self.runner, + ) + .await?; + + info!(target = %target.name, "Target built successfully"); + } + + Ok(()) + } + + fn select_targets(&self, target_name: Option<&str>) -> Result, ForgeError> { + match target_name { + Some(name) => { + let target = self + .spec + .targets + .iter() + .find(|t| t.name == name) + .ok_or_else(|| { + let available = self + .spec + .targets + .iter() + .map(|t| t.name.as_str()) + .collect::>() + .join(", "); + ForgeError::TargetNotFound { + name: name.to_string(), + available, + } + })?; + Ok(vec![target]) + } + None => Ok(self.spec.targets.iter().collect()), + } + } +} + +/// List available targets from a spec. +pub fn list_targets(spec: &ImageSpec) -> Vec<(&str, &TargetKind)> { + spec.targets + .iter() + .map(|t| (t.name.as_str(), &t.kind)) + .collect() +} diff --git a/crates/forge-engine/src/phase1/customizations.rs b/crates/forge-engine/src/phase1/customizations.rs new file mode 100644 index 0000000..151ad06 --- /dev/null +++ b/crates/forge-engine/src/phase1/customizations.rs @@ -0,0 +1,63 @@ +use std::path::Path; + +use spec_parser::schema::Customization; +use tracing::info; + +use crate::error::ForgeError; + +/// Apply customizations (user/group creation) by editing files in the staging root. +pub fn apply(customization: &Customization, staging_root: &Path) -> Result<(), ForgeError> { + for user in &customization.users { + create_user(&user.name, staging_root)?; + } + Ok(()) +} + +/// Create a user by appending entries to passwd, shadow, and group files in +/// the staging root. This does not use `useradd` since we're operating on a +/// staged filesystem, not the running system. +fn create_user(username: &str, staging_root: &Path) -> Result<(), ForgeError> { + info!(username, "Creating user in staging root"); + + let etc_dir = staging_root.join("etc"); + std::fs::create_dir_all(&etc_dir).map_err(|e| ForgeError::Customization { + operation: format!("create /etc directory for user {username}"), + detail: e.to_string(), + })?; + + // Append to /etc/passwd + let passwd_path = etc_dir.join("passwd"); + let passwd_entry = format!("{username}:x:1000:1000::/home/{username}:/bin/sh\n"); + append_or_create(&passwd_path, &passwd_entry).map_err(|e| ForgeError::Customization { + operation: format!("add user {username} to /etc/passwd"), + detail: e.to_string(), + })?; + + // Append to /etc/shadow + let shadow_path = etc_dir.join("shadow"); + let shadow_entry = format!("{username}:*LK*:::::::\n"); + append_or_create(&shadow_path, &shadow_entry).map_err(|e| ForgeError::Customization { + operation: format!("add user {username} to /etc/shadow"), + detail: e.to_string(), + })?; + + // Append to /etc/group + let group_path = etc_dir.join("group"); + let group_entry = format!("{username}::1000:\n"); + append_or_create(&group_path, &group_entry).map_err(|e| ForgeError::Customization { + operation: format!("add group {username} to /etc/group"), + detail: e.to_string(), + })?; + + Ok(()) +} + +fn append_or_create(path: &Path, content: &str) -> Result<(), std::io::Error> { + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path)?; + file.write_all(content.as_bytes())?; + Ok(()) +} diff --git a/crates/forge-engine/src/phase1/mod.rs b/crates/forge-engine/src/phase1/mod.rs new file mode 100644 index 0000000..edff668 --- /dev/null +++ b/crates/forge-engine/src/phase1/mod.rs @@ -0,0 +1,94 @@ +pub mod customizations; +pub mod overlays; +pub mod packages; +pub mod staging; +pub mod variants; + +use std::path::{Path, PathBuf}; + +use spec_parser::schema::ImageSpec; +use tracing::info; + +use crate::error::ForgeError; +use crate::tools::ToolRunner; + +/// Result of Phase 1: a populated staging directory ready for Phase 2. +pub struct Phase1Result { + /// Path to the staging root containing the assembled rootfs. + pub staging_root: PathBuf, + /// The tempdir handle -- dropping it cleans up the staging dir. + pub _staging_dir: tempfile::TempDir, +} + +/// Execute Phase 1: assemble a rootfs in a staging directory from the spec. +/// +/// Steps: +/// 1. Create staging directory +/// 2. Extract base tarball (if specified) +/// 3. Apply IPS variants +/// 4. Configure package repositories and install packages +/// 5. Apply customizations (users, groups) +/// 6. Apply overlays (files, dirs, symlinks, shadow, devfsadm) +pub async fn execute( + spec: &ImageSpec, + files_dir: &Path, + runner: &dyn ToolRunner, +) -> Result { + info!(name = %spec.metadata.name, "Starting Phase 1: rootfs assembly"); + + // 1. Create staging directory + let (staging_dir, staging_root) = staging::create_staging()?; + let root = staging_root.to_str().unwrap(); + info!(root, "Staging directory created"); + + // 2. Extract base tarball + if let Some(ref base) = spec.base { + staging::extract_base_tarball(base, &staging_root)?; + } + + // 3. Create IPS image and configure publishers + crate::tools::pkg::image_create(runner, root).await?; + + for publisher in &spec.repositories.publishers { + crate::tools::pkg::set_publisher(runner, root, &publisher.name, &publisher.origin).await?; + } + + // 4. Apply variants + if let Some(ref vars) = spec.variants { + variants::apply_variants(runner, root, vars).await?; + } + + // 5. Approve CA certificates + if let Some(ref certs) = spec.certificates { + for ca in &certs.ca { + let certfile_path = files_dir.join(&ca.certfile); + let certfile_str = certfile_path.to_str().unwrap_or(&ca.certfile); + crate::tools::pkg::approve_ca_cert(runner, root, &ca.publisher, certfile_str).await?; + } + } + + // 6. Set incorporation + if let Some(ref incorporation) = spec.incorporation { + crate::tools::pkg::set_incorporation(runner, root, incorporation).await?; + } + + // 7. Install packages + packages::install_all(runner, root, &spec.packages).await?; + + // 8. Apply customizations + for customization in &spec.customizations { + customizations::apply(customization, &staging_root)?; + } + + // 9. Apply overlays + for overlay_block in &spec.overlays { + overlays::apply_overlays(&overlay_block.actions, &staging_root, files_dir, runner).await?; + } + + info!("Phase 1 complete: rootfs assembled"); + + Ok(Phase1Result { + staging_root, + _staging_dir: staging_dir, + }) +} diff --git a/crates/forge-engine/src/phase1/overlays.rs b/crates/forge-engine/src/phase1/overlays.rs new file mode 100644 index 0000000..63a9ebd --- /dev/null +++ b/crates/forge-engine/src/phase1/overlays.rs @@ -0,0 +1,323 @@ +use std::path::Path; + +use spec_parser::schema::OverlayAction; +use tracing::info; + +use crate::error::ForgeError; +use crate::tools::ToolRunner; + +/// Apply a list of overlay actions to the staging root. +pub async fn apply_overlays( + actions: &[OverlayAction], + staging_root: &Path, + files_dir: &Path, + runner: &dyn ToolRunner, +) -> Result<(), ForgeError> { + for action in actions { + apply_action(action, staging_root, files_dir, runner).await?; + } + Ok(()) +} + +async fn apply_action( + action: &OverlayAction, + staging_root: &Path, + files_dir: &Path, + runner: &dyn ToolRunner, +) -> Result<(), ForgeError> { + match action { + OverlayAction::File(file_overlay) => { + let dest = staging_root.join( + file_overlay + .destination + .strip_prefix('/') + .unwrap_or(&file_overlay.destination), + ); + + // Ensure parent directory exists + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent).map_err(|e| ForgeError::Overlay { + action: format!("create parent dir for {}", file_overlay.destination), + detail: parent.display().to_string(), + source: e, + })?; + } + + if let Some(ref source) = file_overlay.source { + // Copy source file to destination + let src_path = files_dir.join(source); + if !src_path.exists() { + return Err(ForgeError::OverlaySourceMissing { + path: src_path.display().to_string(), + }); + } + info!( + source = %src_path.display(), + destination = %dest.display(), + "Copying overlay file" + ); + std::fs::copy(&src_path, &dest).map_err(|e| ForgeError::Overlay { + action: format!("copy {} -> {}", source, file_overlay.destination), + detail: String::new(), + source: e, + })?; + } else { + // Create empty file + info!(destination = %dest.display(), "Creating empty overlay file"); + std::fs::write(&dest, b"").map_err(|e| ForgeError::Overlay { + action: format!("create empty file {}", file_overlay.destination), + detail: String::new(), + source: e, + })?; + } + + // Set permissions if specified + #[cfg(unix)] + if let Some(ref mode) = file_overlay.mode { + use std::os::unix::fs::PermissionsExt; + if let Ok(mode_val) = u32::from_str_radix(mode, 8) { + std::fs::set_permissions(&dest, std::fs::Permissions::from_mode(mode_val)) + .map_err(|e| ForgeError::Overlay { + action: format!("set mode {} on {}", mode, file_overlay.destination), + detail: String::new(), + source: e, + })?; + } + } + } + + OverlayAction::Devfsadm(_) => { + let root_str = staging_root.to_str().unwrap_or("."); + crate::tools::devfsadm::run_devfsadm(runner, root_str).await?; + } + + OverlayAction::EnsureDir(ensure_dir) => { + let dir_path = staging_root.join( + ensure_dir + .path + .strip_prefix('/') + .unwrap_or(&ensure_dir.path), + ); + info!(path = %dir_path.display(), "Ensuring directory exists"); + std::fs::create_dir_all(&dir_path).map_err(|e| ForgeError::Overlay { + action: format!("ensure directory {}", ensure_dir.path), + detail: String::new(), + source: e, + })?; + + #[cfg(unix)] + if let Some(ref mode) = ensure_dir.mode { + use std::os::unix::fs::PermissionsExt; + if let Ok(mode_val) = u32::from_str_radix(mode, 8) { + std::fs::set_permissions( + &dir_path, + std::fs::Permissions::from_mode(mode_val), + ) + .map_err(|e| ForgeError::Overlay { + action: format!("set mode {} on {}", mode, ensure_dir.path), + detail: String::new(), + source: e, + })?; + } + } + } + + OverlayAction::RemoveFiles(remove) => { + if let Some(ref file) = remove.file { + let path = staging_root.join(file.strip_prefix('/').unwrap_or(file)); + if path.exists() { + info!(path = %path.display(), "Removing file"); + std::fs::remove_file(&path).map_err(|e| ForgeError::Overlay { + action: format!("remove file {file}"), + detail: String::new(), + source: e, + })?; + } + } + if let Some(ref dir) = remove.dir { + let path = staging_root.join(dir.strip_prefix('/').unwrap_or(dir)); + if path.exists() { + info!(path = %path.display(), "Removing directory contents"); + // Remove directory contents but not the directory itself + for entry in std::fs::read_dir(&path).map_err(|e| ForgeError::Overlay { + action: format!("read directory {dir}"), + detail: String::new(), + source: e, + })? { + let entry = entry.map_err(|e| ForgeError::Overlay { + action: format!("read entry in {dir}"), + detail: String::new(), + source: e, + })?; + let entry_path = entry.path(); + if entry_path.is_dir() { + std::fs::remove_dir_all(&entry_path).map_err(|e| { + ForgeError::Overlay { + action: format!( + "remove dir {}", + entry_path.display() + ), + detail: String::new(), + source: e, + } + })?; + } else { + std::fs::remove_file(&entry_path).map_err(|e| { + ForgeError::Overlay { + action: format!( + "remove file {}", + entry_path.display() + ), + detail: String::new(), + source: e, + } + })?; + } + } + } + } + if let Some(ref pattern) = remove.pattern { + info!(pattern, "Removing files matching pattern"); + // Simple glob-like pattern matching: only supports trailing * + let base = staging_root.join( + pattern + .trim_end_matches('*') + .strip_prefix('/') + .unwrap_or(pattern.trim_end_matches('*')), + ); + if let Some(parent) = base.parent() { + if parent.exists() { + for entry in std::fs::read_dir(parent).map_err(|e| { + ForgeError::Overlay { + action: format!("read directory for pattern {pattern}"), + detail: String::new(), + source: e, + } + })? { + let entry = entry.map_err(|e| ForgeError::Overlay { + action: format!("read entry for pattern {pattern}"), + detail: String::new(), + source: e, + })?; + let name = entry.file_name().to_string_lossy().to_string(); + let base_name = base + .file_name() + .map(|n| n.to_string_lossy().to_string()) + .unwrap_or_default(); + if name.starts_with(&base_name) { + let entry_path = entry.path(); + if entry_path.is_file() { + std::fs::remove_file(&entry_path).ok(); + } + } + } + } + } + } + } + + OverlayAction::EnsureSymlink(symlink) => { + let link_path = staging_root.join( + symlink + .path + .strip_prefix('/') + .unwrap_or(&symlink.path), + ); + + // Ensure parent directory exists + if let Some(parent) = link_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| ForgeError::Overlay { + action: format!("create parent dir for symlink {}", symlink.path), + detail: String::new(), + source: e, + })?; + } + + // Remove existing file/symlink if present + if link_path.exists() || link_path.symlink_metadata().is_ok() { + std::fs::remove_file(&link_path).ok(); + } + + info!( + link = %link_path.display(), + target = %symlink.target, + "Creating symlink" + ); + + #[cfg(unix)] + std::os::unix::fs::symlink(&symlink.target, &link_path).map_err(|e| { + ForgeError::Overlay { + action: format!("create symlink {} -> {}", symlink.path, symlink.target), + detail: String::new(), + source: e, + } + })?; + + #[cfg(not(unix))] + return Err(ForgeError::Overlay { + action: format!("create symlink {} -> {}", symlink.path, symlink.target), + detail: "Symlinks are only supported on Unix platforms".to_string(), + source: std::io::Error::new(std::io::ErrorKind::Unsupported, "not unix"), + }); + } + + OverlayAction::Shadow(shadow) => { + let shadow_path = staging_root.join("etc/shadow"); + if shadow_path.exists() { + info!(username = %shadow.username, "Updating shadow password"); + let content = std::fs::read_to_string(&shadow_path).map_err(|e| { + ForgeError::Overlay { + action: format!("read /etc/shadow for user {}", shadow.username), + detail: String::new(), + source: e, + } + })?; + + let mut found = false; + let new_content: String = content + .lines() + .map(|line| { + let parts: Vec<&str> = line.splitn(9, ':').collect(); + if parts.len() >= 2 && parts[0] == shadow.username { + found = true; + let mut new_parts = parts.clone(); + new_parts[1] = &shadow.password; + new_parts.join(":") + } else { + line.to_string() + } + }) + .collect::>() + .join("\n"); + + let final_content = if found { + new_content + } else { + format!("{new_content}\n{}:{}:::::::\n", shadow.username, shadow.password) + }; + + std::fs::write(&shadow_path, final_content).map_err(|e| ForgeError::Overlay { + action: format!("write /etc/shadow for user {}", shadow.username), + detail: String::new(), + source: e, + })?; + } else { + // Create shadow file with this entry + let content = format!("{}:{}:::::::\n", shadow.username, shadow.password); + let etc_dir = staging_root.join("etc"); + std::fs::create_dir_all(&etc_dir).map_err(|e| ForgeError::Overlay { + action: "create /etc for shadow".to_string(), + detail: String::new(), + source: e, + })?; + std::fs::write(&shadow_path, content).map_err(|e| ForgeError::Overlay { + action: format!("create /etc/shadow for user {}", shadow.username), + detail: String::new(), + source: e, + })?; + } + } + } + + Ok(()) +} diff --git a/crates/forge-engine/src/phase1/packages.rs b/crates/forge-engine/src/phase1/packages.rs new file mode 100644 index 0000000..9afef28 --- /dev/null +++ b/crates/forge-engine/src/phase1/packages.rs @@ -0,0 +1,25 @@ +use spec_parser::schema::PackageList; +use tracing::info; + +use crate::error::ForgeError; +use crate::tools::ToolRunner; + +/// Install all package lists into the staging root via `pkg -R`. +pub async fn install_all( + runner: &dyn ToolRunner, + root: &str, + package_lists: &[PackageList], +) -> Result<(), ForgeError> { + let all_packages: Vec = package_lists + .iter() + .flat_map(|pl| pl.packages.iter().map(|p| p.name.clone())) + .collect(); + + if all_packages.is_empty() { + info!("No packages to install"); + return Ok(()); + } + + info!(count = all_packages.len(), "Installing packages"); + crate::tools::pkg::install(runner, root, &all_packages).await +} diff --git a/crates/forge-engine/src/phase1/staging.rs b/crates/forge-engine/src/phase1/staging.rs new file mode 100644 index 0000000..2b5eca7 --- /dev/null +++ b/crates/forge-engine/src/phase1/staging.rs @@ -0,0 +1,47 @@ +use std::path::PathBuf; + +use tracing::info; + +use crate::error::ForgeError; + +/// Create a temporary staging directory for rootfs assembly. +pub fn create_staging() -> Result<(tempfile::TempDir, PathBuf), ForgeError> { + let staging_dir = tempfile::TempDir::new().map_err(ForgeError::StagingSetup)?; + let staging_root = staging_dir.path().to_path_buf(); + Ok((staging_dir, staging_root)) +} + +/// Extract a base tarball into the staging directory. +pub fn extract_base_tarball(tarball_path: &str, staging_root: &PathBuf) -> Result<(), ForgeError> { + info!(tarball_path, "Extracting base tarball"); + + let file = + std::fs::File::open(tarball_path).map_err(|e| ForgeError::BaseExtract { + path: tarball_path.to_string(), + source: e, + })?; + + // Try gzip first, fall back to plain tar + let reader = std::io::BufReader::new(file); + if tarball_path.ends_with(".gz") || tarball_path.ends_with(".tgz") { + let decoder = flate2::read::GzDecoder::new(reader); + let mut archive = tar::Archive::new(decoder); + archive + .unpack(staging_root) + .map_err(|e| ForgeError::BaseExtract { + path: tarball_path.to_string(), + source: e, + })?; + } else { + let mut archive = tar::Archive::new(reader); + archive + .unpack(staging_root) + .map_err(|e| ForgeError::BaseExtract { + path: tarball_path.to_string(), + source: e, + })?; + } + + info!("Base tarball extracted"); + Ok(()) +} diff --git a/crates/forge-engine/src/phase1/variants.rs b/crates/forge-engine/src/phase1/variants.rs new file mode 100644 index 0000000..6465f0b --- /dev/null +++ b/crates/forge-engine/src/phase1/variants.rs @@ -0,0 +1,18 @@ +use spec_parser::schema::Variants; +use tracing::info; + +use crate::error::ForgeError; +use crate::tools::ToolRunner; + +/// Apply IPS variants via `pkg -R change-variant`. +pub async fn apply_variants( + runner: &dyn ToolRunner, + root: &str, + variants: &Variants, +) -> Result<(), ForgeError> { + for var in &variants.vars { + info!(name = %var.name, value = %var.value, "Applying IPS variant"); + crate::tools::pkg::change_variant(runner, root, &var.name, &var.value).await?; + } + Ok(()) +} diff --git a/crates/forge-engine/src/phase2/artifact.rs b/crates/forge-engine/src/phase2/artifact.rs new file mode 100644 index 0000000..5480d28 --- /dev/null +++ b/crates/forge-engine/src/phase2/artifact.rs @@ -0,0 +1,67 @@ +use std::path::Path; + +use flate2::write::GzEncoder; +use flate2::Compression; +use spec_parser::schema::Target; +use tracing::info; +use walkdir::WalkDir; + +use crate::error::ForgeError; + +/// Build a tarball artifact from the staged rootfs. +pub fn build_artifact( + target: &Target, + staging_root: &Path, + output_dir: &Path, + _files_dir: &Path, +) -> Result<(), ForgeError> { + let output_path = output_dir.join(format!("{}.tar.gz", target.name)); + info!(path = %output_path.display(), "Creating artifact tarball"); + + let file = std::fs::File::create(&output_path)?; + let encoder = GzEncoder::new(file, Compression::default()); + let mut tar = tar::Builder::new(encoder); + + for entry in WalkDir::new(staging_root).follow_links(false) { + let entry = entry.map_err(|e| ForgeError::Qcow2Build { + step: "artifact_walk".to_string(), + detail: e.to_string(), + })?; + + let full_path = entry.path(); + let rel_path = full_path + .strip_prefix(staging_root) + .unwrap_or(full_path); + + if rel_path.as_os_str().is_empty() { + continue; + } + + if full_path.is_file() { + tar.append_path_with_name(full_path, rel_path) + .map_err(|e| ForgeError::Qcow2Build { + step: "artifact_tar".to_string(), + detail: e.to_string(), + })?; + } else if full_path.is_dir() { + tar.append_dir(rel_path, full_path) + .map_err(|e| ForgeError::Qcow2Build { + step: "artifact_tar_dir".to_string(), + detail: e.to_string(), + })?; + } + } + + let encoder = tar.into_inner().map_err(|e| ForgeError::Qcow2Build { + step: "artifact_tar_finish".to_string(), + detail: e.to_string(), + })?; + + encoder.finish().map_err(|e| ForgeError::Qcow2Build { + step: "artifact_gz_finish".to_string(), + detail: e.to_string(), + })?; + + info!(path = %output_path.display(), "Artifact tarball created"); + Ok(()) +} diff --git a/crates/forge-engine/src/phase2/mod.rs b/crates/forge-engine/src/phase2/mod.rs new file mode 100644 index 0000000..6803ca5 --- /dev/null +++ b/crates/forge-engine/src/phase2/mod.rs @@ -0,0 +1,41 @@ +pub mod artifact; +pub mod oci; +pub mod qcow2; + +use std::path::Path; + +use spec_parser::schema::{Target, TargetKind}; +use tracing::info; + +use crate::error::ForgeError; +use crate::tools::ToolRunner; + +/// Execute Phase 2: produce the target artifact from the staged rootfs. +pub async fn execute( + target: &Target, + staging_root: &Path, + files_dir: &Path, + output_dir: &Path, + runner: &dyn ToolRunner, +) -> Result<(), ForgeError> { + info!( + target = %target.name, + kind = %target.kind, + "Starting Phase 2: target production" + ); + + match target.kind { + TargetKind::Oci => { + oci::build_oci(target, staging_root, output_dir)?; + } + TargetKind::Qcow2 => { + qcow2::build_qcow2(target, staging_root, output_dir, runner).await?; + } + TargetKind::Artifact => { + artifact::build_artifact(target, staging_root, output_dir, files_dir)?; + } + } + + info!(target = %target.name, "Phase 2 complete"); + Ok(()) +} diff --git a/crates/forge-engine/src/phase2/oci.rs b/crates/forge-engine/src/phase2/oci.rs new file mode 100644 index 0000000..0f8f5c7 --- /dev/null +++ b/crates/forge-engine/src/phase2/oci.rs @@ -0,0 +1,52 @@ +use std::path::Path; + +use spec_parser::schema::Target; +use tracing::info; + +use crate::error::ForgeError; + +/// Build an OCI container image from the staged rootfs. +pub fn build_oci( + target: &Target, + staging_root: &Path, + output_dir: &Path, +) -> Result<(), ForgeError> { + info!("Building OCI container image"); + + // Create the tar.gz layer from staging + let layer = + forge_oci::tar_layer::create_layer(staging_root).map_err(|e| ForgeError::OciBuild(e.to_string()))?; + + info!( + digest = %layer.digest, + size = layer.data.len(), + "Layer created" + ); + + // Build image options from target spec + let mut options = forge_oci::manifest::ImageOptions::default(); + + if let Some(ref ep) = target.entrypoint { + options.entrypoint = Some(vec![ep.command.clone()]); + } + + if let Some(ref env) = target.environment { + options.env = env + .vars + .iter() + .map(|v| format!("{}={}", v.key, v.value)) + .collect(); + } + + // Build manifest and config + let (config_json, manifest_json) = forge_oci::manifest::build_manifest(&[layer.clone()], &options) + .map_err(|e| ForgeError::OciBuild(e.to_string()))?; + + // Write OCI Image Layout + let oci_output = output_dir.join(format!("{}-oci", target.name)); + forge_oci::layout::write_oci_layout(&oci_output, &[layer], &config_json, &manifest_json) + .map_err(|e| ForgeError::OciBuild(e.to_string()))?; + + info!(path = %oci_output.display(), "OCI Image Layout written"); + Ok(()) +} diff --git a/crates/forge-engine/src/phase2/qcow2.rs b/crates/forge-engine/src/phase2/qcow2.rs new file mode 100644 index 0000000..b7def6a --- /dev/null +++ b/crates/forge-engine/src/phase2/qcow2.rs @@ -0,0 +1,150 @@ +use std::path::Path; + +use spec_parser::schema::Target; +use tracing::info; + +use crate::error::ForgeError; +use crate::tools::ToolRunner; + +/// Build a QCOW2 VM image from the staged rootfs. +/// +/// Pipeline: +/// 1. Create raw disk image of specified size +/// 2. Attach loopback device +/// 3. Create ZFS pool with spec properties +/// 4. Create boot environment structure (rpool/ROOT/be-1) +/// 5. Copy staging rootfs into mounted BE +/// 6. Install bootloader via chroot +/// 7. Set bootfs property +/// 8. Export pool, detach loopback +/// 9. Convert raw -> qcow2 +pub async fn build_qcow2( + target: &Target, + staging_root: &Path, + output_dir: &Path, + runner: &dyn ToolRunner, +) -> Result<(), ForgeError> { + let disk_size = target + .disk_size + .as_deref() + .ok_or(ForgeError::MissingDiskSize)?; + + let bootloader_type = target.bootloader.as_deref().unwrap_or("uefi"); + + let raw_path = output_dir.join(format!("{}.raw", target.name)); + let qcow2_path = output_dir.join(format!("{}.qcow2", target.name)); + let raw_str = raw_path.to_str().unwrap(); + let qcow2_str = qcow2_path.to_str().unwrap(); + + // Collect pool properties + let pool_props: Vec<(&str, &str)> = target + .pool + .as_ref() + .map(|p| { + p.properties + .iter() + .map(|prop| (prop.name.as_str(), prop.value.as_str())) + .collect() + }) + .unwrap_or_default(); + + let pool_name = "rpool"; + let be_dataset = format!("{pool_name}/ROOT/be-1"); + + info!(disk_size, "Step 1: Creating raw disk image"); + crate::tools::qemu_img::create_raw(runner, raw_str, disk_size).await?; + + info!("Step 2: Attaching loopback device"); + let device = crate::tools::loopback::attach(runner, raw_str).await?; + + // Wrap the rest in a closure-like structure so we can clean up on error + let result = async { + info!(device = %device, "Step 3: Creating ZFS pool"); + crate::tools::zpool::create(runner, pool_name, &device, &pool_props).await?; + + info!("Step 4: Creating boot environment structure"); + crate::tools::zfs::create( + runner, + &format!("{pool_name}/ROOT"), + &[("canmount", "off"), ("mountpoint", "legacy")], + ) + .await?; + + let staging_str = staging_root.to_str().unwrap_or("."); + crate::tools::zfs::create( + runner, + &be_dataset, + &[("canmount", "noauto"), ("mountpoint", staging_str)], + ) + .await?; + + crate::tools::zfs::mount(runner, &be_dataset).await?; + + info!("Step 5: Copying staging rootfs into boot environment"); + copy_rootfs(staging_root, staging_root)?; + + info!("Step 6: Installing bootloader"); + crate::tools::bootloader::install(runner, staging_str, pool_name, bootloader_type).await?; + + info!("Step 7: Setting bootfs property"); + crate::tools::zpool::set(runner, pool_name, "bootfs", &be_dataset).await?; + + info!("Step 8: Exporting ZFS pool"); + crate::tools::zfs::unmount(runner, &be_dataset).await?; + crate::tools::zpool::export(runner, pool_name).await?; + + Ok::<(), ForgeError>(()) + } + .await; + + // Always try to detach loopback, even on error + info!("Detaching loopback device"); + let detach_result = crate::tools::loopback::detach(runner, &device).await; + + // Return the original error if there was one + result?; + detach_result?; + + info!("Step 9: Converting raw -> qcow2"); + crate::tools::qemu_img::convert_to_qcow2(runner, raw_str, qcow2_str).await?; + + // Clean up raw file + std::fs::remove_file(&raw_path).ok(); + + info!(path = %qcow2_path.display(), "QCOW2 image created"); + Ok(()) +} + +/// Copy the staging rootfs into the mounted BE. +/// Since the BE is mounted at the staging root mountpoint, we use a recursive +/// copy approach for files that need relocation. +fn copy_rootfs(src: &Path, dest: &Path) -> Result<(), ForgeError> { + // In the actual build, the ZFS dataset is mounted at the staging_root path, + // so the files are already in place after package installation. This function + // handles the case where we need to copy from a temp staging dir into the + // mounted ZFS dataset. + if src == dest { + return Ok(()); + } + + for entry in walkdir::WalkDir::new(src).follow_links(false) { + let entry = entry.map_err(|e| ForgeError::Qcow2Build { + step: "copy_rootfs".to_string(), + detail: e.to_string(), + })?; + + let rel = entry.path().strip_prefix(src).unwrap_or(entry.path()); + let target = dest.join(rel); + + if entry.path().is_dir() { + std::fs::create_dir_all(&target)?; + } else if entry.path().is_file() { + if let Some(parent) = target.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::copy(entry.path(), &target)?; + } + } + + Ok(()) +} diff --git a/crates/forge-engine/src/tools/bootloader.rs b/crates/forge-engine/src/tools/bootloader.rs new file mode 100644 index 0000000..9879819 --- /dev/null +++ b/crates/forge-engine/src/tools/bootloader.rs @@ -0,0 +1,78 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Install the bootloader into the staging root. +/// +/// For illumos: uses `bootadm` to install the boot archive and UEFI loader. +/// For Linux: uses `grub-install` with chroot. +#[cfg(target_os = "illumos")] +pub async fn install( + runner: &dyn ToolRunner, + staging_root: &str, + pool_name: &str, + bootloader_type: &str, +) -> Result<(), ForgeError> { + info!(staging_root, pool_name, bootloader_type, "Installing bootloader (illumos)"); + + // Install the boot archive + runner + .run("bootadm", &["update-archive", "-R", staging_root]) + .await?; + + if bootloader_type == "uefi" { + // Install the UEFI bootloader + runner + .run( + "bootadm", + &["install-bootloader", "-M", "-f", "-P", pool_name, "-R", staging_root], + ) + .await?; + } + + Ok(()) +} + +#[cfg(target_os = "linux")] +pub async fn install( + runner: &dyn ToolRunner, + staging_root: &str, + _pool_name: &str, + bootloader_type: &str, +) -> Result<(), ForgeError> { + info!(staging_root, bootloader_type, "Installing bootloader (Linux)"); + + match bootloader_type { + "grub" | "uefi" => { + runner + .run( + "chroot", + &[staging_root, "grub-install", "--target=x86_64-efi"], + ) + .await?; + } + other => { + return Err(ForgeError::Qcow2Build { + step: "bootloader_install".to_string(), + detail: format!("Unsupported bootloader type: {other}"), + }); + } + } + + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "illumos")))] +pub async fn install( + _runner: &dyn ToolRunner, + _staging_root: &str, + _pool_name: &str, + bootloader_type: &str, +) -> Result<(), ForgeError> { + Err(ForgeError::Qcow2Build { + step: "bootloader_install".to_string(), + detail: format!( + "Bootloader installation is not supported on this platform (type: {bootloader_type})" + ), + }) +} diff --git a/crates/forge-engine/src/tools/devfsadm.rs b/crates/forge-engine/src/tools/devfsadm.rs new file mode 100644 index 0000000..4188113 --- /dev/null +++ b/crates/forge-engine/src/tools/devfsadm.rs @@ -0,0 +1,18 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Run devfsadm in the staging root to populate /dev. +/// This is illumos-specific and is a no-op on other platforms. +#[cfg(target_os = "illumos")] +pub async fn run_devfsadm(runner: &dyn ToolRunner, root: &str) -> Result<(), ForgeError> { + info!(root, "Running devfsadm"); + runner.run("devfsadm", &["-r", root]).await?; + Ok(()) +} + +#[cfg(not(target_os = "illumos"))] +pub async fn run_devfsadm(_runner: &dyn ToolRunner, root: &str) -> Result<(), ForgeError> { + info!(root, "Skipping devfsadm (not on illumos)"); + Ok(()) +} diff --git a/crates/forge-engine/src/tools/loopback.rs b/crates/forge-engine/src/tools/loopback.rs new file mode 100644 index 0000000..b2b6d89 --- /dev/null +++ b/crates/forge-engine/src/tools/loopback.rs @@ -0,0 +1,56 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Attach a file to a loopback device and return the device path. +#[cfg(target_os = "linux")] +pub async fn attach(runner: &dyn ToolRunner, file_path: &str) -> Result { + info!(file_path, "Attaching loopback device (Linux)"); + let output = runner + .run("losetup", &["--find", "--show", file_path]) + .await?; + Ok(output.stdout.trim().to_string()) +} + +/// Detach a loopback device. +#[cfg(target_os = "linux")] +pub async fn detach(runner: &dyn ToolRunner, device: &str) -> Result<(), ForgeError> { + info!(device, "Detaching loopback device (Linux)"); + runner.run("losetup", &["--detach", device]).await?; + Ok(()) +} + +/// Attach a file to a loopback device and return the device path. +#[cfg(target_os = "illumos")] +pub async fn attach(runner: &dyn ToolRunner, file_path: &str) -> Result { + info!(file_path, "Attaching loopback device (illumos)"); + let output = runner.run("lofiadm", &["-a", file_path]).await?; + Ok(output.stdout.trim().to_string()) +} + +/// Detach a loopback device. +#[cfg(target_os = "illumos")] +pub async fn detach(runner: &dyn ToolRunner, device: &str) -> Result<(), ForgeError> { + info!(device, "Detaching loopback device (illumos)"); + runner.run("lofiadm", &["-d", device]).await?; + Ok(()) +} + +// Stub for unsupported platforms (compile-time guard) +#[cfg(not(any(target_os = "linux", target_os = "illumos")))] +pub async fn attach(_runner: &dyn ToolRunner, file_path: &str) -> Result { + Err(ForgeError::Qcow2Build { + step: "loopback_attach".to_string(), + detail: format!("Loopback devices are not supported on this platform (file: {file_path})"), + }) +} + +#[cfg(not(any(target_os = "linux", target_os = "illumos")))] +pub async fn detach(_runner: &dyn ToolRunner, device: &str) -> Result<(), ForgeError> { + Err(ForgeError::Qcow2Build { + step: "loopback_detach".to_string(), + detail: format!( + "Loopback devices are not supported on this platform (device: {device})" + ), + }) +} diff --git a/crates/forge-engine/src/tools/mod.rs b/crates/forge-engine/src/tools/mod.rs new file mode 100644 index 0000000..41dd028 --- /dev/null +++ b/crates/forge-engine/src/tools/mod.rs @@ -0,0 +1,70 @@ +pub mod bootloader; +pub mod devfsadm; +pub mod loopback; +pub mod pkg; +pub mod qemu_img; +pub mod zfs; +pub mod zpool; + +use std::future::Future; +use std::pin::Pin; + +use crate::error::ForgeError; + +/// Output from a tool execution. +#[derive(Debug)] +pub struct ToolOutput { + pub stdout: String, + pub stderr: String, + pub exit_code: i32, +} + +/// Trait for executing external tools. Allows mocking in tests. +pub trait ToolRunner: Send + Sync { + fn run<'a>( + &'a self, + program: &'a str, + args: &'a [&'a str], + ) -> Pin> + Send + 'a>>; +} + +/// Real tool runner that uses `tokio::process::Command`. +pub struct SystemToolRunner; + +impl ToolRunner for SystemToolRunner { + fn run<'a>( + &'a self, + program: &'a str, + args: &'a [&'a str], + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let output = tokio::process::Command::new(program) + .args(args) + .output() + .await + .map_err(|e| ForgeError::ToolExecution { + tool: program.to_string(), + args: args.join(" "), + stderr: String::new(), + source: e, + })?; + + let result = ToolOutput { + stdout: String::from_utf8_lossy(&output.stdout).to_string(), + stderr: String::from_utf8_lossy(&output.stderr).to_string(), + exit_code: output.status.code().unwrap_or(-1), + }; + + if !output.status.success() { + return Err(ForgeError::ToolNonZero { + tool: program.to_string(), + args: args.join(" "), + exit_code: result.exit_code, + stderr: result.stderr, + }); + } + + Ok(result) + }) + } +} diff --git a/crates/forge-engine/src/tools/pkg.rs b/crates/forge-engine/src/tools/pkg.rs new file mode 100644 index 0000000..b56a4fc --- /dev/null +++ b/crates/forge-engine/src/tools/pkg.rs @@ -0,0 +1,93 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Create a new IPS image at the given root path. +pub async fn image_create(runner: &dyn ToolRunner, root: &str) -> Result<(), ForgeError> { + info!(root, "Creating IPS image"); + runner.run("pkg", &["image-create", "-F", "-p", root]).await?; + Ok(()) +} + +/// Set a publisher in the IPS image at the given root. +pub async fn set_publisher( + runner: &dyn ToolRunner, + root: &str, + name: &str, + origin: &str, +) -> Result<(), ForgeError> { + info!(root, name, origin, "Setting publisher"); + runner + .run("pkg", &["-R", root, "set-publisher", "-O", origin, name]) + .await?; + Ok(()) +} + +/// Install packages into the IPS image at the given root. +pub async fn install( + runner: &dyn ToolRunner, + root: &str, + packages: &[String], +) -> Result<(), ForgeError> { + if packages.is_empty() { + return Ok(()); + } + info!(root, count = packages.len(), "Installing packages"); + let mut args = vec!["-R", root, "install", "--accept"]; + let pkg_refs: Vec<&str> = packages.iter().map(|s| s.as_str()).collect(); + args.extend(pkg_refs); + runner.run("pkg", &args).await?; + Ok(()) +} + +/// Change an IPS variant in the image at the given root. +pub async fn change_variant( + runner: &dyn ToolRunner, + root: &str, + name: &str, + value: &str, +) -> Result<(), ForgeError> { + info!(root, name, value, "Changing variant"); + let variant_arg = format!("{name}={value}"); + runner + .run("pkg", &["-R", root, "change-variant", &variant_arg]) + .await?; + Ok(()) +} + +/// Approve a CA certificate for a publisher in the IPS image. +pub async fn approve_ca_cert( + runner: &dyn ToolRunner, + root: &str, + publisher: &str, + certfile: &str, +) -> Result<(), ForgeError> { + info!(root, publisher, certfile, "Approving CA certificate"); + runner + .run( + "pkg", + &[ + "-R", + root, + "set-publisher", + "--approve-ca-cert", + certfile, + publisher, + ], + ) + .await?; + Ok(()) +} + +/// Set the incorporation package. +pub async fn set_incorporation( + runner: &dyn ToolRunner, + root: &str, + incorporation: &str, +) -> Result<(), ForgeError> { + info!(root, incorporation, "Setting incorporation"); + runner + .run("pkg", &["-R", root, "install", "--accept", incorporation]) + .await?; + Ok(()) +} diff --git a/crates/forge-engine/src/tools/qemu_img.rs b/crates/forge-engine/src/tools/qemu_img.rs new file mode 100644 index 0000000..bbe7753 --- /dev/null +++ b/crates/forge-engine/src/tools/qemu_img.rs @@ -0,0 +1,40 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Create a raw disk image of the given size. +pub async fn create_raw( + runner: &dyn ToolRunner, + path: &str, + size: &str, +) -> Result<(), ForgeError> { + info!(path, size, "Creating raw disk image"); + runner + .run("qemu-img", &["create", "-f", "raw", path, size]) + .await?; + Ok(()) +} + +/// Convert a raw image to qcow2 format. +pub async fn convert_to_qcow2( + runner: &dyn ToolRunner, + input: &str, + output: &str, +) -> Result<(), ForgeError> { + info!(input, output, "Converting raw image to qcow2"); + runner + .run( + "qemu-img", + &["convert", "-f", "raw", "-O", "qcow2", input, output], + ) + .await?; + Ok(()) +} + +/// Get info about a disk image (JSON output). +pub async fn info(runner: &dyn ToolRunner, path: &str) -> Result { + let output = runner + .run("qemu-img", &["info", "--output=json", path]) + .await?; + Ok(output.stdout) +} diff --git a/crates/forge-engine/src/tools/zfs.rs b/crates/forge-engine/src/tools/zfs.rs new file mode 100644 index 0000000..fd374aa --- /dev/null +++ b/crates/forge-engine/src/tools/zfs.rs @@ -0,0 +1,54 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Create a ZFS dataset. +pub async fn create( + runner: &dyn ToolRunner, + dataset: &str, + properties: &[(&str, &str)], +) -> Result<(), ForgeError> { + info!(dataset, "Creating ZFS dataset"); + let mut args = vec!["create"]; + + let prop_strings: Vec = properties + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect(); + for prop in &prop_strings { + args.push("-o"); + args.push(prop); + } + + // Create parent datasets if needed + args.push("-p"); + args.push(dataset); + + runner.run("zfs", &args).await?; + Ok(()) +} + +/// Mount a ZFS dataset at a given mountpoint. +pub async fn mount(runner: &dyn ToolRunner, dataset: &str) -> Result<(), ForgeError> { + info!(dataset, "Mounting ZFS dataset"); + runner.run("zfs", &["mount", dataset]).await?; + Ok(()) +} + +/// Set a property on a ZFS dataset. +pub async fn set( + runner: &dyn ToolRunner, + dataset: &str, + property: &str, + value: &str, +) -> Result<(), ForgeError> { + let prop_val = format!("{property}={value}"); + runner.run("zfs", &["set", &prop_val, dataset]).await?; + Ok(()) +} + +/// Unmount a ZFS dataset. +pub async fn unmount(runner: &dyn ToolRunner, dataset: &str) -> Result<(), ForgeError> { + runner.run("zfs", &["unmount", dataset]).await?; + Ok(()) +} diff --git a/crates/forge-engine/src/tools/zpool.rs b/crates/forge-engine/src/tools/zpool.rs new file mode 100644 index 0000000..99b78eb --- /dev/null +++ b/crates/forge-engine/src/tools/zpool.rs @@ -0,0 +1,56 @@ +use crate::error::ForgeError; +use crate::tools::ToolRunner; +use tracing::info; + +/// Create a ZFS pool on the given device. +pub async fn create( + runner: &dyn ToolRunner, + pool_name: &str, + device: &str, + properties: &[(&str, &str)], +) -> Result<(), ForgeError> { + info!(pool_name, device, "Creating ZFS pool"); + let mut args = vec!["create"]; + + // Add -o property=value for each pool property + let prop_strings: Vec = properties + .iter() + .map(|(k, v)| format!("{k}={v}")) + .collect(); + for prop in &prop_strings { + args.push("-o"); + args.push(prop); + } + + args.push(pool_name); + args.push(device); + + runner.run("zpool", &args).await?; + Ok(()) +} + +/// Export a ZFS pool. +pub async fn export(runner: &dyn ToolRunner, pool_name: &str) -> Result<(), ForgeError> { + info!(pool_name, "Exporting ZFS pool"); + runner.run("zpool", &["export", pool_name]).await?; + Ok(()) +} + +/// Destroy a ZFS pool (force). +pub async fn destroy(runner: &dyn ToolRunner, pool_name: &str) -> Result<(), ForgeError> { + info!(pool_name, "Destroying ZFS pool"); + runner.run("zpool", &["destroy", "-f", pool_name]).await?; + Ok(()) +} + +/// Set a property on a ZFS pool. +pub async fn set( + runner: &dyn ToolRunner, + pool_name: &str, + property: &str, + value: &str, +) -> Result<(), ForgeError> { + let prop_val = format!("{property}={value}"); + runner.run("zpool", &["set", &prop_val, pool_name]).await?; + Ok(()) +} diff --git a/crates/forge-oci/Cargo.toml b/crates/forge-oci/Cargo.toml new file mode 100644 index 0000000..2efae69 --- /dev/null +++ b/crates/forge-oci/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "forge-oci" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +miette = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +oci-spec = { workspace = true } +oci-client = { workspace = true } +sha2 = { workspace = true } +hex = { workspace = true } +bytes = { workspace = true } +tar = { workspace = true } +flate2 = { workspace = true } +walkdir = { workspace = true } diff --git a/crates/forge-oci/src/layout.rs b/crates/forge-oci/src/layout.rs new file mode 100644 index 0000000..84e4d74 --- /dev/null +++ b/crates/forge-oci/src/layout.rs @@ -0,0 +1,113 @@ +use std::path::Path; + +use miette::Diagnostic; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use crate::tar_layer::LayerBlob; + +#[derive(Debug, Error, Diagnostic)] +pub enum LayoutError { + #[error("Failed to create OCI layout directory: {path}")] + #[diagnostic(help("Ensure the parent directory exists and is writable"))] + CreateDir { + path: String, + #[source] + source: std::io::Error, + }, + + #[error("Failed to write OCI layout file: {path}")] + #[diagnostic(help("Check disk space and permissions"))] + WriteFile { + path: String, + #[source] + source: std::io::Error, + }, + + #[error("Failed to build OCI manifest")] + ManifestError(#[from] crate::manifest::ManifestError), + + #[error("Failed to serialize OCI layout JSON")] + Serialize(#[from] serde_json::Error), +} + +/// Write an OCI Image Layout directory at `output_dir`. +/// +/// Structure: +/// ```text +/// output_dir/ +/// oci-layout +/// index.json +/// blobs/ +/// sha256/ +/// +/// ... +/// +/// ``` +pub fn write_oci_layout( + output_dir: &Path, + layers: &[LayerBlob], + config_json: &[u8], + manifest_json: &[u8], +) -> Result<(), LayoutError> { + let blobs_dir = output_dir.join("blobs").join("sha256"); + std::fs::create_dir_all(&blobs_dir).map_err(|e| LayoutError::CreateDir { + path: blobs_dir.display().to_string(), + source: e, + })?; + + // Write oci-layout + let oci_layout = serde_json::json!({ + "imageLayoutVersion": "1.0.0" + }); + write_file( + &output_dir.join("oci-layout"), + serde_json::to_vec_pretty(&oci_layout)?.as_slice(), + )?; + + // Write layer blobs + for layer in layers { + let digest_hex = layer + .digest + .strip_prefix("sha256:") + .unwrap_or(&layer.digest); + write_file(&blobs_dir.join(digest_hex), &layer.data)?; + } + + // Write config blob + let mut config_hasher = Sha256::new(); + config_hasher.update(config_json); + let config_digest_hex = hex::encode(config_hasher.finalize()); + write_file(&blobs_dir.join(&config_digest_hex), config_json)?; + + // Write manifest blob + let mut manifest_hasher = Sha256::new(); + manifest_hasher.update(manifest_json); + let manifest_digest_hex = hex::encode(manifest_hasher.finalize()); + write_file(&blobs_dir.join(&manifest_digest_hex), manifest_json)?; + + // Write index.json + let index = serde_json::json!({ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": format!("sha256:{manifest_digest_hex}"), + "size": manifest_json.len() + } + ] + }); + write_file( + &output_dir.join("index.json"), + serde_json::to_vec_pretty(&index)?.as_slice(), + )?; + + Ok(()) +} + +fn write_file(path: &Path, data: &[u8]) -> Result<(), LayoutError> { + std::fs::write(path, data).map_err(|e| LayoutError::WriteFile { + path: path.display().to_string(), + source: e, + }) +} diff --git a/crates/forge-oci/src/lib.rs b/crates/forge-oci/src/lib.rs new file mode 100644 index 0000000..149dc12 --- /dev/null +++ b/crates/forge-oci/src/lib.rs @@ -0,0 +1,4 @@ +pub mod layout; +pub mod manifest; +pub mod registry; +pub mod tar_layer; diff --git a/crates/forge-oci/src/manifest.rs b/crates/forge-oci/src/manifest.rs new file mode 100644 index 0000000..c420ea7 --- /dev/null +++ b/crates/forge-oci/src/manifest.rs @@ -0,0 +1,136 @@ +use miette::Diagnostic; +use oci_spec::image::{ + ConfigBuilder, DescriptorBuilder, ImageConfigurationBuilder, ImageManifestBuilder, + MediaType, RootFsBuilder, Sha256Digest, +}; +use sha2::{Digest, Sha256}; +use thiserror::Error; + +use crate::tar_layer::LayerBlob; + +#[derive(Debug, Error, Diagnostic)] +pub enum ManifestError { + #[error("Failed to build OCI image configuration")] + #[diagnostic(help("This is likely a bug in the manifest builder"))] + ConfigBuild(String), + + #[error("Failed to build OCI image manifest")] + #[diagnostic(help("This is likely a bug in the manifest builder"))] + ManifestBuild(String), + + #[error("Failed to serialize OCI manifest to JSON")] + Serialize(#[source] serde_json::Error), +} + +/// Options for building an OCI image configuration. +pub struct ImageOptions { + pub os: String, + pub architecture: String, + pub entrypoint: Option>, + pub env: Vec, +} + +impl Default for ImageOptions { + fn default() -> Self { + Self { + os: "solaris".to_string(), + architecture: "amd64".to_string(), + entrypoint: None, + env: Vec::new(), + } + } +} + +/// Build the OCI image configuration JSON and image manifest from a set of layers. +/// +/// Returns `(config_json, manifest_json)`. +pub fn build_manifest( + layers: &[LayerBlob], + options: &ImageOptions, +) -> Result<(Vec, Vec), ManifestError> { + // Build the diff_ids for the rootfs (uncompressed layer digests aren't tracked here, + // so we use the compressed digest -- in a full implementation you'd track both) + let diff_ids: Vec = layers.iter().map(|l| l.digest.clone()).collect(); + + let rootfs = RootFsBuilder::default() + .typ("layers") + .diff_ids(diff_ids) + .build() + .map_err(|e| ManifestError::ConfigBuild(e.to_string()))?; + + let mut config_builder = ImageConfigurationBuilder::default() + .os(options.os.as_str()) + .architecture(options.architecture.as_str()) + .rootfs(rootfs); + + // Build a config block with optional entrypoint/env + let mut inner_config_builder = ConfigBuilder::default(); + if let Some(ref ep) = options.entrypoint { + inner_config_builder = inner_config_builder.entrypoint(ep.clone()); + } + if !options.env.is_empty() { + inner_config_builder = inner_config_builder.env(options.env.clone()); + } + let inner_config = inner_config_builder + .build() + .map_err(|e| ManifestError::ConfigBuild(e.to_string()))?; + config_builder = config_builder.config(inner_config); + + let image_config = config_builder + .build() + .map_err(|e| ManifestError::ConfigBuild(e.to_string()))?; + + let config_json = + serde_json::to_vec_pretty(&image_config).map_err(ManifestError::Serialize)?; + + let mut config_hasher = Sha256::new(); + config_hasher.update(&config_json); + let config_digest = format!("sha256:{}", hex::encode(config_hasher.finalize())); + + let config_sha_digest: Sha256Digest = config_digest + .strip_prefix("sha256:") + .unwrap_or(&config_digest) + .parse() + .map_err(|e: oci_spec::OciSpecError| ManifestError::ConfigBuild(e.to_string()))?; + + let config_descriptor = DescriptorBuilder::default() + .media_type(MediaType::ImageConfig) + .size(config_json.len() as u64) + .digest(config_sha_digest) + .build() + .map_err(|e| ManifestError::ConfigBuild(e.to_string()))?; + + // Build layer descriptors + let layer_descriptors: Vec<_> = layers + .iter() + .map(|layer| { + let layer_sha: Sha256Digest = layer + .digest + .strip_prefix("sha256:") + .unwrap_or(&layer.digest) + .parse() + .map_err(|e: oci_spec::OciSpecError| { + ManifestError::ManifestBuild(e.to_string()) + })?; + + DescriptorBuilder::default() + .media_type(MediaType::ImageLayerGzip) + .size(layer.data.len() as u64) + .digest(layer_sha) + .build() + .map_err(|e| ManifestError::ManifestBuild(e.to_string())) + }) + .collect::>()?; + + let manifest = ImageManifestBuilder::default() + .schema_version(2u32) + .media_type(MediaType::ImageManifest) + .config(config_descriptor) + .layers(layer_descriptors) + .build() + .map_err(|e| ManifestError::ManifestBuild(e.to_string()))?; + + let manifest_json = serde_json::to_vec_pretty(&manifest).map_err(ManifestError::Serialize)?; + + Ok((config_json, manifest_json)) +} diff --git a/crates/forge-oci/src/registry.rs b/crates/forge-oci/src/registry.rs new file mode 100644 index 0000000..f15b728 --- /dev/null +++ b/crates/forge-oci/src/registry.rs @@ -0,0 +1,121 @@ +use miette::Diagnostic; +use oci_client::client::{ClientConfig, ClientProtocol, Config, ImageLayer}; +use oci_client::manifest; +use oci_client::secrets::RegistryAuth; +use oci_client::{Client, Reference}; +use thiserror::Error; +use tracing::info; + +use crate::tar_layer::LayerBlob; + +#[derive(Debug, Error, Diagnostic)] +pub enum RegistryError { + #[error("Invalid image reference: {reference}")] + #[diagnostic(help( + "Use the format /:, e.g. ghcr.io/org/image:v1" + ))] + InvalidReference { + reference: String, + #[source] + source: oci_client::ParseError, + }, + + #[error("Failed to push image to registry")] + #[diagnostic(help("Check registry URL, credentials, and network connectivity"))] + PushFailed(String), + + #[error("Failed to read auth file: {path}")] + #[diagnostic(help("Ensure the auth file exists and contains valid JSON"))] + AuthFileRead { + path: String, + #[source] + source: std::io::Error, + }, +} + +/// Authentication configuration for registry operations. +pub enum AuthConfig { + Anonymous, + Basic { username: String, password: String }, + Bearer { token: String }, +} + +impl AuthConfig { + fn to_registry_auth(&self) -> RegistryAuth { + match self { + AuthConfig::Anonymous => RegistryAuth::Anonymous, + AuthConfig::Basic { username, password } => { + RegistryAuth::Basic(username.clone(), password.clone()) + } + AuthConfig::Bearer { token } => RegistryAuth::Bearer(token.clone()), + } + } +} + +/// Push an OCI image (layers + config + manifest) to a registry. +pub async fn push_image( + reference_str: &str, + layers: Vec, + config_json: Vec, + auth: &AuthConfig, + insecure_registries: &[String], +) -> Result { + let reference: Reference = reference_str + .parse() + .map_err(|e| RegistryError::InvalidReference { + reference: reference_str.to_string(), + source: e, + })?; + + let client_config = ClientConfig { + protocol: if insecure_registries.is_empty() { + ClientProtocol::Https + } else { + ClientProtocol::HttpsExcept(insecure_registries.to_vec()) + }, + ..Default::default() + }; + + let client = Client::new(client_config); + let registry_auth = auth.to_registry_auth(); + + let oci_layers: Vec = layers + .into_iter() + .map(|layer| { + ImageLayer::new( + layer.data, + manifest::IMAGE_LAYER_GZIP_MEDIA_TYPE.to_string(), + None, + ) + }) + .collect(); + + let config = Config::new( + config_json, + manifest::IMAGE_CONFIG_MEDIA_TYPE.to_string(), + None, + ); + + let image_manifest = + oci_client::manifest::OciImageManifest::build(&oci_layers, &config, None); + + info!(reference = %reference, "Pushing image to registry"); + + let response = client + .push( + &reference, + &oci_layers, + config, + ®istry_auth, + Some(image_manifest), + ) + .await + .map_err(|e| RegistryError::PushFailed(e.to_string()))?; + + info!( + manifest_url = %response.manifest_url, + "Image pushed successfully" + ); + + Ok(response.manifest_url) +} diff --git a/crates/forge-oci/src/tar_layer.rs b/crates/forge-oci/src/tar_layer.rs new file mode 100644 index 0000000..1515a3e --- /dev/null +++ b/crates/forge-oci/src/tar_layer.rs @@ -0,0 +1,126 @@ +use std::path::Path; + +use flate2::write::GzEncoder; +use flate2::Compression; +use miette::Diagnostic; +use sha2::{Digest, Sha256}; +use thiserror::Error; +use walkdir::WalkDir; + +#[derive(Debug, Error, Diagnostic)] +pub enum TarLayerError { + #[error("Failed to walk staging directory: {path}")] + #[diagnostic(help("Ensure the staging directory exists and is readable"))] + WalkDir { + path: String, + #[source] + source: walkdir::Error, + }, + + #[error("Failed to create tar archive")] + #[diagnostic(help("Check disk space and permissions"))] + TarCreate(#[source] std::io::Error), + + #[error("Failed to read file for tar: {path}")] + ReadFile { + path: String, + #[source] + source: std::io::Error, + }, +} + +/// Result of creating a tar.gz layer from a staging directory. +#[derive(Clone)] +pub struct LayerBlob { + /// Compressed tar.gz data + pub data: Vec, + /// SHA-256 digest of the compressed data (hex-encoded) + pub digest: String, + /// Uncompressed size in bytes + pub uncompressed_size: u64, +} + +/// Create a tar.gz layer from a staging directory. All paths in the tar are +/// relative to the staging root. +pub fn create_layer(staging_dir: &Path) -> Result { + let mut uncompressed_size: u64 = 0; + let buf = Vec::new(); + let encoder = GzEncoder::new(buf, Compression::default()); + let mut tar = tar::Builder::new(encoder); + + for entry in WalkDir::new(staging_dir).follow_links(false) { + let entry = entry.map_err(|e| TarLayerError::WalkDir { + path: staging_dir.display().to_string(), + source: e, + })?; + + let full_path = entry.path(); + let rel_path = full_path + .strip_prefix(staging_dir) + .unwrap_or(full_path); + + // Skip the root directory itself + if rel_path.as_os_str().is_empty() { + continue; + } + + let metadata = entry.metadata().map_err(|e| TarLayerError::WalkDir { + path: full_path.display().to_string(), + source: e, + })?; + + if metadata.is_file() { + uncompressed_size += metadata.len(); + let mut header = tar::Header::new_gnu(); + header.set_size(metadata.len()); + header.set_mode(0o644); + header.set_cksum(); + + let file_data = + std::fs::read(full_path).map_err(|e| TarLayerError::ReadFile { + path: full_path.display().to_string(), + source: e, + })?; + + tar.append_data(&mut header, rel_path, file_data.as_slice()) + .map_err(TarLayerError::TarCreate)?; + } else if metadata.is_dir() { + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Directory); + header.set_size(0); + header.set_mode(0o755); + header.set_cksum(); + + tar.append_data(&mut header, rel_path, std::io::empty()) + .map_err(TarLayerError::TarCreate)?; + } else if metadata.is_symlink() { + let link_target = std::fs::read_link(full_path).map_err(|e| { + TarLayerError::ReadFile { + path: full_path.display().to_string(), + source: e, + } + })?; + + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Symlink); + header.set_size(0); + header.set_cksum(); + + tar.append_link(&mut header, rel_path, &link_target) + .map_err(TarLayerError::TarCreate)?; + } + } + + let encoder = tar.into_inner().map_err(TarLayerError::TarCreate)?; + let compressed = encoder.finish().map_err(TarLayerError::TarCreate)?; + + let mut hasher = Sha256::new(); + hasher.update(&compressed); + let digest = format!("sha256:{}", hex::encode(hasher.finalize())); + + Ok(LayerBlob { + data: compressed, + digest, + uncompressed_size, + }) +} diff --git a/crates/forger/Cargo.toml b/crates/forger/Cargo.toml new file mode 100644 index 0000000..6a561d6 --- /dev/null +++ b/crates/forger/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "forger" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +spec-parser = { workspace = true } +forge-oci = { workspace = true } +forge-engine = { workspace = true } +clap = { workspace = true } +miette = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +indicatif = { workspace = true } diff --git a/crates/forger/src/commands/build.rs b/crates/forger/src/commands/build.rs new file mode 100644 index 0000000..522e18d --- /dev/null +++ b/crates/forger/src/commands/build.rs @@ -0,0 +1,58 @@ +use std::path::PathBuf; + +use forge_engine::tools::SystemToolRunner; +use forge_engine::BuildContext; +use miette::{Context, IntoDiagnostic}; +use tracing::info; + +/// Build an image from a spec file. +pub async fn run( + spec_path: &PathBuf, + target: Option<&str>, + profiles: &[String], + output_dir: &PathBuf, +) -> miette::Result<()> { + let kdl_content = std::fs::read_to_string(spec_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read spec file: {}", spec_path.display()))?; + + let spec = spec_parser::parse(&kdl_content) + .map_err(miette::Report::new) + .wrap_err("Failed to parse spec")?; + + let spec_dir = spec_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); + + let resolved = spec_parser::resolve::resolve(spec, spec_dir) + .map_err(miette::Report::new) + .wrap_err("Failed to resolve includes")?; + + let filtered = spec_parser::profile::apply_profiles(resolved, profiles); + + // Determine files directory (images/files/ relative to spec) + let files_dir = spec_dir.join("files"); + + let runner = SystemToolRunner; + + let ctx = BuildContext { + spec: &filtered, + files_dir: &files_dir, + output_dir, + runner: &runner, + }; + + info!( + spec = %spec_path.display(), + output = %output_dir.display(), + "Starting build" + ); + + ctx.build(target) + .await + .map_err(miette::Report::new) + .wrap_err("Build failed")?; + + println!("Build complete. Output: {}", output_dir.display()); + Ok(()) +} diff --git a/crates/forger/src/commands/inspect.rs b/crates/forger/src/commands/inspect.rs new file mode 100644 index 0000000..a630c0e --- /dev/null +++ b/crates/forger/src/commands/inspect.rs @@ -0,0 +1,132 @@ +use std::path::PathBuf; + +use miette::{Context, IntoDiagnostic}; + +/// Inspect a spec file: parse, resolve includes, apply profiles, and print the result. +pub fn run(spec_path: &PathBuf, profiles: &[String]) -> miette::Result<()> { + let kdl_content = std::fs::read_to_string(spec_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read spec file: {}", spec_path.display()))?; + + let spec = spec_parser::parse(&kdl_content) + .map_err(miette::Report::new) + .wrap_err("Failed to parse spec")?; + + let spec_dir = spec_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); + + let resolved = spec_parser::resolve::resolve(spec, spec_dir) + .map_err(miette::Report::new) + .wrap_err("Failed to resolve includes")?; + + let filtered = spec_parser::profile::apply_profiles(resolved, profiles); + + println!("Image: {} v{}", filtered.metadata.name, filtered.metadata.version); + + if let Some(ref desc) = filtered.metadata.description { + println!("Description: {desc}"); + } + + println!("\nRepositories:"); + for pub_entry in &filtered.repositories.publishers { + println!(" {} -> {}", pub_entry.name, pub_entry.origin); + } + + if let Some(ref inc) = filtered.incorporation { + println!("\nIncorporation: {inc}"); + } + + if let Some(ref variants) = filtered.variants { + println!("\nVariants:"); + for var in &variants.vars { + println!(" {} = {}", var.name, var.value); + } + } + + if let Some(ref certs) = filtered.certificates { + println!("\nCertificates:"); + for ca in &certs.ca { + println!(" CA: publisher={} certfile={}", ca.publisher, ca.certfile); + } + } + + let total_packages: usize = filtered.packages.iter().map(|pl| pl.packages.len()).sum(); + println!("\nPackages ({total_packages} total):"); + for pl in &filtered.packages { + if let Some(ref cond) = pl.r#if { + println!(" [if={cond}]"); + } + for pkg in &pl.packages { + println!(" {}", pkg.name); + } + } + + if !filtered.customizations.is_empty() { + println!("\nCustomizations:"); + for c in &filtered.customizations { + if let Some(ref cond) = c.r#if { + println!(" [if={cond}]"); + } + for user in &c.users { + println!(" user: {}", user.name); + } + } + } + + let total_overlays: usize = filtered.overlays.iter().map(|o| o.actions.len()).sum(); + println!("\nOverlays ({total_overlays} actions):"); + for overlay_block in &filtered.overlays { + if let Some(ref cond) = overlay_block.r#if { + println!(" [if={cond}]"); + } + for action in &overlay_block.actions { + match action { + spec_parser::schema::OverlayAction::File(f) => { + println!( + " file: {} <- {}", + f.destination, + f.source.as_deref().unwrap_or("(empty)") + ); + } + spec_parser::schema::OverlayAction::Devfsadm(_) => { + println!(" devfsadm"); + } + spec_parser::schema::OverlayAction::EnsureDir(d) => { + println!(" ensure-dir: {}", d.path); + } + spec_parser::schema::OverlayAction::RemoveFiles(r) => { + let target = r + .file + .as_deref() + .or(r.dir.as_deref()) + .or(r.pattern.as_deref()) + .unwrap_or("?"); + println!(" remove: {target}"); + } + spec_parser::schema::OverlayAction::EnsureSymlink(s) => { + println!(" symlink: {} -> {}", s.path, s.target); + } + spec_parser::schema::OverlayAction::Shadow(s) => { + println!(" shadow: {} (hash set)", s.username); + } + } + } + } + + if !filtered.targets.is_empty() { + println!("\nTargets:"); + for target in &filtered.targets { + print!(" {} ({})", target.name, target.kind); + if let Some(ref size) = target.disk_size { + print!(" disk_size={size}"); + } + if let Some(ref bl) = target.bootloader { + print!(" bootloader={bl}"); + } + println!(); + } + } + + Ok(()) +} diff --git a/crates/forger/src/commands/mod.rs b/crates/forger/src/commands/mod.rs new file mode 100644 index 0000000..dc2aea1 --- /dev/null +++ b/crates/forger/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod build; +pub mod inspect; +pub mod push; +pub mod targets; +pub mod validate; diff --git a/crates/forger/src/commands/push.rs b/crates/forger/src/commands/push.rs new file mode 100644 index 0000000..72be071 --- /dev/null +++ b/crates/forger/src/commands/push.rs @@ -0,0 +1,126 @@ +use std::path::PathBuf; + +use miette::{Context, IntoDiagnostic}; +use tracing::info; + +/// Push an OCI Image Layout to a registry. +pub async fn run( + image_dir: &PathBuf, + reference: &str, + auth_file: Option<&PathBuf>, +) -> miette::Result<()> { + // Read the OCI Image Layout index.json + let index_path = image_dir.join("index.json"); + let index_content = std::fs::read_to_string(&index_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read OCI index: {}", index_path.display()))?; + + let index: serde_json::Value = + serde_json::from_str(&index_content).into_diagnostic()?; + + let manifests = index["manifests"] + .as_array() + .ok_or_else(|| miette::miette!("Invalid OCI index: missing manifests array"))?; + + if manifests.is_empty() { + return Err(miette::miette!("OCI index contains no manifests")); + } + + // Read the manifest + let manifest_digest = manifests[0]["digest"] + .as_str() + .ok_or_else(|| miette::miette!("Invalid manifest entry: missing digest"))?; + + let digest_hex = manifest_digest + .strip_prefix("sha256:") + .ok_or_else(|| miette::miette!("Unsupported digest algorithm: {manifest_digest}"))?; + + let manifest_path = image_dir.join("blobs/sha256").join(digest_hex); + let manifest_json: serde_json::Value = serde_json::from_str( + &std::fs::read_to_string(&manifest_path) + .into_diagnostic() + .wrap_err("Failed to read manifest blob")?, + ) + .into_diagnostic()?; + + // Read config blob + let config_digest = manifest_json["config"]["digest"] + .as_str() + .ok_or_else(|| miette::miette!("Missing config digest in manifest"))?; + let config_hex = config_digest.strip_prefix("sha256:").unwrap_or(config_digest); + let config_json = std::fs::read(image_dir.join("blobs/sha256").join(config_hex)) + .into_diagnostic() + .wrap_err("Failed to read config blob")?; + + // Read layer blobs + let layers_json = manifest_json["layers"] + .as_array() + .ok_or_else(|| miette::miette!("Missing layers in manifest"))?; + + let mut layers = Vec::new(); + for layer_desc in layers_json { + let layer_digest = layer_desc["digest"] + .as_str() + .ok_or_else(|| miette::miette!("Missing layer digest"))?; + let layer_hex = layer_digest + .strip_prefix("sha256:") + .unwrap_or(layer_digest); + + let layer_data = std::fs::read(image_dir.join("blobs/sha256").join(layer_hex)) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read layer blob: {layer_digest}"))?; + + layers.push(forge_oci::tar_layer::LayerBlob { + data: layer_data, + digest: layer_digest.to_string(), + uncompressed_size: 0, // Not tracked in layout + }); + } + + // Determine auth + let auth = if let Some(auth_path) = auth_file { + let auth_content = std::fs::read_to_string(auth_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read auth file: {}", auth_path.display()))?; + + let auth_json: serde_json::Value = + serde_json::from_str(&auth_content).into_diagnostic()?; + + if let Some(token) = auth_json["token"].as_str() { + forge_oci::registry::AuthConfig::Bearer { + token: token.to_string(), + } + } else if let (Some(user), Some(pass)) = ( + auth_json["username"].as_str(), + auth_json["password"].as_str(), + ) { + forge_oci::registry::AuthConfig::Basic { + username: user.to_string(), + password: pass.to_string(), + } + } else { + forge_oci::registry::AuthConfig::Anonymous + } + } else { + forge_oci::registry::AuthConfig::Anonymous + }; + + // Determine if we need insecure registries (localhost) + let insecure = if reference.starts_with("localhost") || reference.starts_with("127.0.0.1") { + let host_port = reference.split('/').next().unwrap_or(""); + vec![host_port.to_string()] + } else { + vec![] + }; + + info!(reference, "Pushing OCI image to registry"); + + let manifest_url = + forge_oci::registry::push_image(reference, layers, config_json, &auth, &insecure) + .await + .map_err(miette::Report::new) + .wrap_err("Push failed")?; + + println!("Pushed: {manifest_url}"); + Ok(()) +} diff --git a/crates/forger/src/commands/targets.rs b/crates/forger/src/commands/targets.rs new file mode 100644 index 0000000..60716c3 --- /dev/null +++ b/crates/forger/src/commands/targets.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use miette::{Context, IntoDiagnostic}; + +/// List available targets from a spec file. +pub fn run(spec_path: &PathBuf) -> miette::Result<()> { + let kdl_content = std::fs::read_to_string(spec_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read spec file: {}", spec_path.display()))?; + + let spec = spec_parser::parse(&kdl_content) + .map_err(miette::Report::new) + .wrap_err("Failed to parse spec")?; + + let spec_dir = spec_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); + + let resolved = spec_parser::resolve::resolve(spec, spec_dir) + .map_err(miette::Report::new) + .wrap_err("Failed to resolve includes")?; + + let targets = forge_engine::list_targets(&resolved); + + if targets.is_empty() { + println!("No targets defined in spec."); + return Ok(()); + } + + println!("Available targets:"); + for (name, kind) in targets { + println!(" {name} ({kind})"); + } + + Ok(()) +} diff --git a/crates/forger/src/commands/validate.rs b/crates/forger/src/commands/validate.rs new file mode 100644 index 0000000..f518823 --- /dev/null +++ b/crates/forger/src/commands/validate.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; + +use miette::{Context, IntoDiagnostic}; + +/// Validate a spec file by parsing it and resolving all includes. +pub fn run(spec_path: &PathBuf) -> miette::Result<()> { + let kdl_content = std::fs::read_to_string(spec_path) + .into_diagnostic() + .wrap_err_with(|| format!("Failed to read spec file: {}", spec_path.display()))?; + + let spec = spec_parser::parse(&kdl_content) + .map_err(miette::Report::new) + .wrap_err("Failed to parse spec")?; + + let spec_dir = spec_path + .parent() + .unwrap_or_else(|| std::path::Path::new(".")); + + let _resolved = spec_parser::resolve::resolve(spec, spec_dir) + .map_err(miette::Report::new) + .wrap_err("Failed to resolve includes")?; + + println!("Spec is valid: {}", spec_path.display()); + Ok(()) +} diff --git a/crates/forger/src/main.rs b/crates/forger/src/main.rs new file mode 100644 index 0000000..a1f5f36 --- /dev/null +++ b/crates/forger/src/main.rs @@ -0,0 +1,120 @@ +mod commands; + +use std::path::PathBuf; + +use clap::{Parser, Subcommand}; +use miette::Result; +use tracing_subscriber::EnvFilter; + +#[derive(Parser, Debug)] +#[command( + name = "forger", + version, + about = "Build optimized OS images and publish to OCI registries" +)] +struct Args { + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand, Debug)] +enum Commands { + /// Build an image from a spec file + Build { + /// Path to the spec file + #[arg(short, long)] + spec: PathBuf, + + /// Target name to build (builds all if omitted) + #[arg(short, long)] + target: Option, + + /// Active profiles for conditional blocks + #[arg(short, long)] + profile: Vec, + + /// Output directory for build artifacts + #[arg(short, long, default_value = "./output")] + output_dir: PathBuf, + }, + + /// Validate a spec file (parse + resolve includes) + Validate { + /// Path to the spec file + #[arg(short, long)] + spec: PathBuf, + }, + + /// Inspect a resolved spec (parse + resolve + apply profiles) + Inspect { + /// Path to the spec file + #[arg(short, long)] + spec: PathBuf, + + /// Active profiles for conditional blocks + #[arg(short, long)] + profile: Vec, + }, + + /// Push an OCI Image Layout to a registry + Push { + /// Path to the OCI Image Layout directory + #[arg(short, long)] + image: PathBuf, + + /// Registry reference (e.g., ghcr.io/org/image:tag) + #[arg(short, long)] + reference: String, + + /// Path to auth file (JSON with username/password or token) + #[arg(short, long)] + auth_file: Option, + }, + + /// List available targets from a spec file + Targets { + /// Path to the spec file + #[arg(short, long)] + spec: PathBuf, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")), + ) + .init(); + + let args = Args::parse(); + + match args.command { + Commands::Build { + spec, + target, + profile, + output_dir, + } => { + commands::build::run(&spec, target.as_deref(), &profile, &output_dir).await?; + } + Commands::Validate { spec } => { + commands::validate::run(&spec)?; + } + Commands::Inspect { spec, profile } => { + commands::inspect::run(&spec, &profile)?; + } + Commands::Push { + image, + reference, + auth_file, + } => { + commands::push::run(&image, &reference, auth_file.as_ref()).await?; + } + Commands::Targets { spec } => { + commands::targets::run(&spec)?; + } + } + + Ok(()) +} diff --git a/crates/spec-parser/Cargo.toml b/crates/spec-parser/Cargo.toml new file mode 100644 index 0000000..8bcbb0a --- /dev/null +++ b/crates/spec-parser/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "spec-parser" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +knuffel = { workspace = true } +miette = { workspace = true } +thiserror = { workspace = true } +serde = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } diff --git a/crates/spec-parser/src/lib.rs b/crates/spec-parser/src/lib.rs new file mode 100644 index 0000000..76de7e3 --- /dev/null +++ b/crates/spec-parser/src/lib.rs @@ -0,0 +1,152 @@ +pub mod profile; +pub mod resolve; +pub mod schema; + +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug, Error, Diagnostic)] +pub enum ParseError { + #[error("Failed to parse KDL spec")] + #[diagnostic( + help("Check the KDL syntax in your spec file"), + code(spec_parser::kdl_parse) + )] + KdlError { + detail: String, + }, +} + +impl From for ParseError { + fn from(err: knuffel::Error) -> Self { + ParseError::KdlError { + detail: err.to_string(), + } + } +} + +pub fn parse(kdl: &str) -> Result { + knuffel::parse("image.kdl", kdl).map_err(ParseError::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_example() { + let kdl = r#" + metadata name="my-image" version="1.0.0" description="A test image" + + base "path/to/base.tar.gz" + build-host "path/to/build-vm.qcow2" + + repositories { + publisher name="test-pub" origin="http://pkg.test.com" + } + + incorporation "pkg:/test/incorporation" + + packages { + package "system/kernel" + } + + packages if="desktop" { + package "desktop/gnome" + } + + customization { + user "admin" + } + + overlays { + file source="local/file" destination="/remote/file" + } + + target "vm" kind="qcow2" { + disk-size "20G" + bootloader "grub" + } + + target "container" kind="oci" { + entrypoint command="/bin/sh" + environment { + set "PATH" "/bin:/usr/bin" + } + } + "#; + + let spec = parse(kdl).expect("Failed to parse KDL"); + assert_eq!(spec.metadata.name, "my-image"); + assert_eq!(spec.base, Some("path/to/base.tar.gz".to_string())); + assert_eq!( + spec.build_host, + Some("path/to/build-vm.qcow2".to_string()) + ); + assert_eq!(spec.repositories.publishers.len(), 1); + assert_eq!(spec.packages.len(), 2); + assert_eq!(spec.targets.len(), 2); + + let vm_target = &spec.targets[0]; + assert_eq!(vm_target.name, "vm"); + assert_eq!(vm_target.kind, schema::TargetKind::Qcow2); + assert_eq!(vm_target.disk_size, Some("20G".to_string())); + assert_eq!(vm_target.bootloader, Some("grub".to_string())); + + let container_target = &spec.targets[1]; + assert_eq!(container_target.name, "container"); + assert_eq!(container_target.kind, schema::TargetKind::Oci); + assert_eq!( + container_target.entrypoint.as_ref().unwrap().command, + "/bin/sh" + ); + } + + #[test] + fn test_parse_variants_and_certificates() { + let kdl = r#" + metadata name="test" version="0.1.0" + repositories { + publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/" + } + variants { + set name="opensolaris.zone" value="global" + } + certificates { + ca publisher="omnios" certfile="omniosce-ca.cert.pem" + } + "#; + + let spec = parse(kdl).expect("Failed to parse KDL"); + let variants = spec.variants.unwrap(); + assert_eq!(variants.vars.len(), 1); + assert_eq!(variants.vars[0].name, "opensolaris.zone"); + assert_eq!(variants.vars[0].value, "global"); + + let certs = spec.certificates.unwrap(); + assert_eq!(certs.ca.len(), 1); + assert_eq!(certs.ca[0].publisher, "omnios"); + } + + #[test] + fn test_parse_pool_properties() { + let kdl = r#" + metadata name="test" version="0.1.0" + repositories {} + target "disk" kind="qcow2" { + disk-size "2000M" + bootloader "uefi" + pool { + property name="ashift" value="12" + } + } + "#; + + let spec = parse(kdl).expect("Failed to parse KDL"); + let target = &spec.targets[0]; + let pool = target.pool.as_ref().unwrap(); + assert_eq!(pool.properties.len(), 1); + assert_eq!(pool.properties[0].name, "ashift"); + assert_eq!(pool.properties[0].value, "12"); + } +} diff --git a/crates/spec-parser/src/profile.rs b/crates/spec-parser/src/profile.rs new file mode 100644 index 0000000..893bec2 --- /dev/null +++ b/crates/spec-parser/src/profile.rs @@ -0,0 +1,107 @@ +use crate::schema::ImageSpec; + +/// Filter an `ImageSpec` so that only blocks matching the active profiles are +/// retained. Blocks with no `if` condition are always included. Blocks whose +/// `if` value matches one of the active profile names are included. All others +/// are removed. +pub fn apply_profiles(mut spec: ImageSpec, active_profiles: &[String]) -> ImageSpec { + spec.packages + .retain(|p| profile_matches(p.r#if.as_deref(), active_profiles)); + + spec.customizations + .retain(|c| profile_matches(c.r#if.as_deref(), active_profiles)); + + spec.overlays + .retain(|o| profile_matches(o.r#if.as_deref(), active_profiles)); + + spec +} + +fn profile_matches(condition: Option<&str>, active_profiles: &[String]) -> bool { + match condition { + None => true, + Some(name) => active_profiles.iter().any(|p| p == name), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse; + + #[test] + fn test_unconditional_blocks_always_included() { + let kdl = r#" + metadata name="test" version="1.0.0" + repositories { + publisher name="main" origin="http://example.com" + } + packages { + package "always/included" + } + packages if="desktop" { + package "desktop/only" + } + "#; + + let spec = parse(kdl).unwrap(); + let filtered = apply_profiles(spec, &[]); + + assert_eq!(filtered.packages.len(), 1); + assert_eq!(filtered.packages[0].packages[0].name, "always/included"); + } + + #[test] + fn test_matching_profile_included() { + let kdl = r#" + metadata name="test" version="1.0.0" + repositories { + publisher name="main" origin="http://example.com" + } + packages { + package "always/included" + } + packages if="desktop" { + package "desktop/only" + } + packages if="server" { + package "server/only" + } + "#; + + let spec = parse(kdl).unwrap(); + let filtered = apply_profiles(spec, &["desktop".to_string()]); + + assert_eq!(filtered.packages.len(), 2); + assert_eq!(filtered.packages[0].packages[0].name, "always/included"); + assert_eq!(filtered.packages[1].packages[0].name, "desktop/only"); + } + + #[test] + fn test_overlays_and_customizations_filtered() { + let kdl = r#" + metadata name="test" version="1.0.0" + repositories { + publisher name="main" origin="http://example.com" + } + overlays { + ensure-dir "/always" owner="root" group="root" mode="755" + } + overlays if="dev" { + ensure-dir "/dev-only" owner="root" group="root" mode="755" + } + customization { + user "admin" + } + customization if="dev" { + user "developer" + } + "#; + + let spec = parse(kdl).unwrap(); + let filtered = apply_profiles(spec, &[]); + + assert_eq!(filtered.overlays.len(), 1); + assert_eq!(filtered.customizations.len(), 1); + } +} diff --git a/crates/spec-parser/src/resolve.rs b/crates/spec-parser/src/resolve.rs new file mode 100644 index 0000000..029142a --- /dev/null +++ b/crates/spec-parser/src/resolve.rs @@ -0,0 +1,322 @@ +use std::collections::HashSet; +use std::path::{Path, PathBuf}; + +use miette::Diagnostic; +use thiserror::Error; + +use crate::schema::ImageSpec; + +#[derive(Debug, Error, Diagnostic)] +pub enum ResolveError { + #[error("Failed to read spec file: {path}")] + #[diagnostic(help("Ensure the file exists and is readable"))] + ReadFile { + path: String, + #[source] + source: std::io::Error, + }, + + #[error("Failed to parse included spec: {path}")] + #[diagnostic(help("Check the KDL syntax in the included file"))] + ParseInclude { + path: String, + #[source] + source: crate::ParseError, + }, + + #[error("Circular include detected: {path}")] + #[diagnostic( + help("The include chain forms a cycle. Remove the circular reference."), + code(spec_parser::circular_include) + )] + CircularInclude { path: String }, + + #[error("Failed to resolve base spec: {path}")] + #[diagnostic(help("Ensure the base spec path is correct and the file exists"))] + ResolveBase { + path: String, + #[source] + source: Box, + }, +} + +/// Resolve all includes and base references in an `ImageSpec`, producing a +/// fully merged spec. The `spec_dir` is the directory containing the root spec +/// file, used to resolve relative paths. +pub fn resolve(spec: ImageSpec, spec_dir: &Path) -> Result { + let mut visited = HashSet::new(); + resolve_inner(spec, spec_dir, &mut visited) +} + +fn resolve_inner( + mut spec: ImageSpec, + spec_dir: &Path, + visited: &mut HashSet, +) -> Result { + // Resolve base spec first (base is the "parent" we inherit from) + if let Some(base_path) = spec.base.take() { + let base_abs = resolve_path(spec_dir, &base_path); + let canonical = base_abs + .canonicalize() + .map_err(|e| ResolveError::ReadFile { + path: base_abs.display().to_string(), + source: e, + })?; + + if !visited.insert(canonical.clone()) { + return Err(ResolveError::CircularInclude { + path: canonical.display().to_string(), + }); + } + + let base_content = + std::fs::read_to_string(&canonical).map_err(|e| ResolveError::ReadFile { + path: canonical.display().to_string(), + source: e, + })?; + + let base_spec = crate::parse(&base_content).map_err(|e| ResolveError::ParseInclude { + path: canonical.display().to_string(), + source: e, + })?; + + let base_dir = canonical.parent().unwrap_or(spec_dir); + let resolved_base = resolve_inner(base_spec, base_dir, visited).map_err(|e| { + ResolveError::ResolveBase { + path: canonical.display().to_string(), + source: Box::new(e), + } + })?; + + spec = merge_base(resolved_base, spec); + } + + // Resolve includes (siblings that contribute packages/overlays/customizations) + let includes = std::mem::take(&mut spec.includes); + for include in includes { + let inc_abs = resolve_path(spec_dir, &include.path); + let canonical = inc_abs + .canonicalize() + .map_err(|e| ResolveError::ReadFile { + path: inc_abs.display().to_string(), + source: e, + })?; + + if !visited.insert(canonical.clone()) { + return Err(ResolveError::CircularInclude { + path: canonical.display().to_string(), + }); + } + + let inc_content = + std::fs::read_to_string(&canonical).map_err(|e| ResolveError::ReadFile { + path: canonical.display().to_string(), + source: e, + })?; + + let inc_spec = crate::parse(&inc_content).map_err(|e| ResolveError::ParseInclude { + path: canonical.display().to_string(), + source: e, + })?; + + let inc_dir = canonical.parent().unwrap_or(spec_dir); + let resolved_inc = resolve_inner(inc_spec, inc_dir, visited)?; + + merge_include(&mut spec, resolved_inc); + } + + Ok(spec) +} + +/// Merge a base (parent) spec with the child. The child's values take +/// precedence; the base provides defaults. +fn merge_base(mut base: ImageSpec, child: ImageSpec) -> ImageSpec { + // Metadata comes from the child + base.metadata = child.metadata; + + // build_host: child overrides + if child.build_host.is_some() { + base.build_host = child.build_host; + } + + // repositories: merge publishers from child into base (child publishers appended) + for pub_entry in child.repositories.publishers { + if !base + .repositories + .publishers + .iter() + .any(|p| p.name == pub_entry.name) + { + base.repositories.publishers.push(pub_entry); + } + } + + // incorporation: child overrides + if child.incorporation.is_some() { + base.incorporation = child.incorporation; + } + + // variants: merge + if let Some(child_variants) = child.variants { + if let Some(ref mut base_variants) = base.variants { + for var in child_variants.vars { + if let Some(existing) = base_variants.vars.iter_mut().find(|v| v.name == var.name) { + existing.value = var.value; + } else { + base_variants.vars.push(var); + } + } + } else { + base.variants = Some(child_variants); + } + } + + // certificates: merge + if let Some(child_certs) = child.certificates { + if let Some(ref mut base_certs) = base.certificates { + base_certs.ca.extend(child_certs.ca); + } else { + base.certificates = Some(child_certs); + } + } + + // packages, customizations, overlays: child appended after base + base.packages.extend(child.packages); + base.customizations.extend(child.customizations); + base.overlays.extend(child.overlays); + + // includes: already resolved, don't carry forward + base.includes = Vec::new(); + + // targets: child's targets replace base entirely + if !child.targets.is_empty() { + base.targets = child.targets; + } + + base +} + +/// Merge an included spec into the current spec. Includes contribute +/// packages, customizations, and overlays but not metadata/targets. +fn merge_include(spec: &mut ImageSpec, included: ImageSpec) { + spec.packages.extend(included.packages); + spec.customizations.extend(included.customizations); + spec.overlays.extend(included.overlays); +} + +fn resolve_path(base_dir: &Path, relative: &str) -> PathBuf { + let path = Path::new(relative); + if path.is_absolute() { + path.to_path_buf() + } else { + base_dir.join(path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[test] + fn test_resolve_with_include() { + let tmp = TempDir::new().unwrap(); + + let included_kdl = r#" + metadata name="included" version="0.0.1" + repositories { + publisher name="extra" origin="http://extra.example.com" + } + packages { + package "extra/pkg" + } + overlays { + ensure-dir "/extra/dir" owner="root" group="root" mode="755" + } + "#; + fs::write(tmp.path().join("included.kdl"), included_kdl).unwrap(); + + let root_kdl = r#" + metadata name="root" version="1.0.0" + repositories { + publisher name="main" origin="http://main.example.com" + } + include "included.kdl" + packages { + package "main/pkg" + } + "#; + + let spec = crate::parse(root_kdl).unwrap(); + let resolved = resolve(spec, tmp.path()).unwrap(); + + assert_eq!(resolved.metadata.name, "root"); + assert_eq!(resolved.packages.len(), 2); + assert_eq!(resolved.overlays.len(), 1); + } + + #[test] + fn test_resolve_with_base() { + let tmp = TempDir::new().unwrap(); + + let base_kdl = r#" + metadata name="base" version="0.0.1" + repositories { + publisher name="core" origin="http://core.example.com" + } + packages { + package "base/pkg" + } + "#; + fs::write(tmp.path().join("base.kdl"), base_kdl).unwrap(); + + let child_kdl = r#" + metadata name="child" version="1.0.0" + base "base.kdl" + repositories { + publisher name="extra" origin="http://extra.example.com" + } + packages { + package "child/pkg" + } + target "vm" kind="qcow2" { + disk-size "10G" + } + "#; + + let spec = crate::parse(child_kdl).unwrap(); + let resolved = resolve(spec, tmp.path()).unwrap(); + + assert_eq!(resolved.metadata.name, "child"); + assert_eq!(resolved.repositories.publishers.len(), 2); + assert_eq!(resolved.packages.len(), 2); + assert_eq!(resolved.packages[0].packages[0].name, "base/pkg"); + assert_eq!(resolved.packages[1].packages[0].name, "child/pkg"); + assert_eq!(resolved.targets.len(), 1); + } + + #[test] + fn test_circular_include_detected() { + let tmp = TempDir::new().unwrap(); + + let a_kdl = r#" + metadata name="a" version="0.0.1" + repositories {} + include "b.kdl" + "#; + let b_kdl = r#" + metadata name="b" version="0.0.1" + repositories {} + include "a.kdl" + "#; + fs::write(tmp.path().join("a.kdl"), a_kdl).unwrap(); + fs::write(tmp.path().join("b.kdl"), b_kdl).unwrap(); + + let spec = crate::parse(a_kdl).unwrap(); + let result = resolve(spec, tmp.path()); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err, ResolveError::CircularInclude { .. })); + } +} diff --git a/crates/spec-parser/src/schema.rs b/crates/spec-parser/src/schema.rs new file mode 100644 index 0000000..8d0df66 --- /dev/null +++ b/crates/spec-parser/src/schema.rs @@ -0,0 +1,297 @@ +use knuffel::Decode; + +#[derive(Debug, Decode)] +pub struct ImageSpec { + #[knuffel(child)] + pub metadata: Metadata, + + #[knuffel(child, unwrap(argument))] + pub base: Option, + + #[knuffel(child, unwrap(argument))] + pub build_host: Option, + + #[knuffel(child)] + pub repositories: Repositories, + + #[knuffel(child, unwrap(argument))] + pub incorporation: Option, + + #[knuffel(child)] + pub variants: Option, + + #[knuffel(child)] + pub certificates: Option, + + #[knuffel(children(name = "packages"))] + pub packages: Vec, + + #[knuffel(children(name = "customization"))] + pub customizations: Vec, + + #[knuffel(children(name = "overlays"))] + pub overlays: Vec, + + #[knuffel(children(name = "include"))] + pub includes: Vec, + + #[knuffel(children(name = "target"))] + pub targets: Vec, +} + +#[derive(Debug, Decode)] +pub struct Metadata { + #[knuffel(property)] + pub name: String, + #[knuffel(property)] + pub version: String, + #[knuffel(property)] + pub description: Option, +} + +#[derive(Debug, Decode)] +pub struct Repositories { + #[knuffel(children(name = "publisher"))] + pub publishers: Vec, +} + +#[derive(Debug, Decode)] +pub struct Publisher { + #[knuffel(property)] + pub name: String, + #[knuffel(property)] + pub origin: String, +} + +#[derive(Debug, Decode)] +pub struct PackageList { + #[knuffel(property)] + pub r#if: Option, + + #[knuffel(children(name = "package"))] + pub packages: Vec, +} + +#[derive(Debug, Decode)] +pub struct Package { + #[knuffel(argument)] + pub name: String, +} + +#[derive(Debug, Decode)] +pub struct Customization { + #[knuffel(property)] + pub r#if: Option, + + #[knuffel(children(name = "user"))] + pub users: Vec, +} + +#[derive(Debug, Decode)] +pub struct User { + #[knuffel(argument)] + pub name: String, +} + +#[derive(Debug, Decode)] +pub struct Overlays { + #[knuffel(property)] + pub r#if: Option, + + #[knuffel(children)] + pub actions: Vec, +} + +#[derive(Debug, Decode)] +pub enum OverlayAction { + File(FileOverlay), + Devfsadm(Devfsadm), + EnsureDir(EnsureDir), + RemoveFiles(RemoveFiles), + EnsureSymlink(EnsureSymlink), + Shadow(ShadowOverlay), +} + +#[derive(Debug, Decode)] +pub struct FileOverlay { + #[knuffel(property)] + pub destination: String, + + #[knuffel(property)] + pub source: Option, + + #[knuffel(property)] + pub owner: Option, + #[knuffel(property)] + pub group: Option, + #[knuffel(property)] + pub mode: Option, +} + +#[derive(Debug, Decode)] +pub struct Devfsadm {} + +#[derive(Debug, Decode)] +pub struct EnsureDir { + #[knuffel(argument)] + pub path: String, + #[knuffel(property)] + pub owner: Option, + #[knuffel(property)] + pub group: Option, + #[knuffel(property)] + pub mode: Option, +} + +#[derive(Debug, Decode)] +pub struct RemoveFiles { + #[knuffel(property)] + pub file: Option, + #[knuffel(property)] + pub dir: Option, + #[knuffel(property)] + pub pattern: Option, +} + +#[derive(Debug, Decode)] +pub struct EnsureSymlink { + #[knuffel(argument)] + pub path: String, + #[knuffel(property)] + pub target: String, + #[knuffel(property)] + pub owner: Option, + #[knuffel(property)] + pub group: Option, +} + +#[derive(Debug, Decode)] +pub struct ShadowOverlay { + #[knuffel(property)] + pub username: String, + #[knuffel(property)] + pub password: String, +} + +#[derive(Debug, Decode)] +pub struct Target { + #[knuffel(argument)] + pub name: String, + + #[knuffel(property, str)] + pub kind: TargetKind, + + #[knuffel(child, unwrap(argument))] + pub disk_size: Option, + + #[knuffel(child, unwrap(argument))] + pub bootloader: Option, + + #[knuffel(child)] + pub entrypoint: Option, + + #[knuffel(child)] + pub environment: Option, + + #[knuffel(child)] + pub pool: Option, +} + +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub enum TargetKind { + Qcow2, + Oci, + #[default] + Artifact, +} + +impl std::str::FromStr for TargetKind { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_ascii_lowercase().as_str() { + "qcow2" | "qcow" => Ok(TargetKind::Qcow2), + "oci" => Ok(TargetKind::Oci), + "artifact" | "tar" => Ok(TargetKind::Artifact), + other => Err(format!("invalid target kind: {other}")), + } + } +} + +impl std::fmt::Display for TargetKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TargetKind::Qcow2 => write!(f, "qcow2"), + TargetKind::Oci => write!(f, "oci"), + TargetKind::Artifact => write!(f, "artifact"), + } + } +} + +#[derive(Debug, Decode)] +pub struct Entrypoint { + #[knuffel(property)] + pub command: String, +} + +#[derive(Debug, Decode)] +pub struct Environment { + #[knuffel(children(name = "set"))] + pub vars: Vec, +} + +#[derive(Debug, Decode)] +pub struct EnvVar { + #[knuffel(argument)] + pub key: String, + #[knuffel(argument)] + pub value: String, +} + +#[derive(Debug, Decode)] +pub struct Pool { + #[knuffel(children(name = "property"))] + pub properties: Vec, +} + +#[derive(Debug, Decode)] +pub struct PoolProperty { + #[knuffel(property)] + pub name: String, + #[knuffel(property)] + pub value: String, +} + +#[derive(Debug, Decode)] +pub struct Variants { + #[knuffel(children(name = "set"))] + pub vars: Vec, +} + +#[derive(Debug, Decode)] +pub struct VariantPair { + #[knuffel(property)] + pub name: String, + #[knuffel(property)] + pub value: String, +} + +#[derive(Debug, Decode)] +pub struct Certificates { + #[knuffel(children(name = "ca"))] + pub ca: Vec, +} + +#[derive(Debug, Decode)] +pub struct CaCertificate { + #[knuffel(property)] + pub publisher: String, + #[knuffel(property)] + pub certfile: String, +} + +#[derive(Debug, Decode)] +pub struct Include { + #[knuffel(argument)] + pub path: String, +} diff --git a/images/common.kdl b/images/common.kdl new file mode 100644 index 0000000..09a5539 --- /dev/null +++ b/images/common.kdl @@ -0,0 +1,20 @@ +// SMF profiles and basic network/name service configuration ported from image-builder JSON +overlays { + // SMF default profiles + ensure-symlink "/etc/svc/profile/generic.xml" target="generic_limited_net.xml" owner="root" group="root" + ensure-symlink "/etc/svc/profile/inetd_services.xml" target="inetd_generic.xml" owner="root" group="root" + ensure-symlink "/etc/svc/profile/platform.xml" target="platform_none.xml" owner="root" group="root" + + // Name service profile + ensure-symlink "/etc/svc/profile/name_service.xml" target="ns_dns.xml" owner="root" group="root" + + // nsswitch: use the dns profile file; symlink to keep parity with imagesrc copy + ensure-symlink "/etc/nsswitch.conf" target="nsswitch.dns" owner="root" group="root" + + // Network basics and identity + file destination="/etc/inet/hosts" source="etc/hosts" owner="root" group="root" mode="644" + file destination="/etc/nodename" source="etc/nodename" owner="root" group="root" mode="644" + + // Empty resolv.conf; can be populated by DHCP or later config + file destination="/etc/resolv.conf" owner="root" group="root" mode="644" +} diff --git a/images/devfs.kdl b/images/devfs.kdl new file mode 100644 index 0000000..97fdf6f --- /dev/null +++ b/images/devfs.kdl @@ -0,0 +1,23 @@ +overlays { + devfsadm + ensure-dir "/dev/cfg" owner="root" group="root" mode="755" + ensure-dir "/dev/dsk" owner="root" group="sys" mode="755" + ensure-dir "/dev/rdsk" owner="root" group="sys" mode="755" + ensure-dir "/dev/usb" owner="root" group="root" mode="755" + + remove-files dir="/dev/cfg" + remove-files dir="/dev/dsk" + remove-files dir="/dev/rdsk" + remove-files dir="/dev/usb" + + // Re-create dirs + ensure-dir "/dev/cfg" owner="root" group="root" mode="755" + ensure-dir "/dev/dsk" owner="root" group="sys" mode="755" + ensure-dir "/dev/rdsk" owner="root" group="sys" mode="755" + ensure-dir "/dev/usb" owner="root" group="root" mode="755" + + ensure-symlink "/dev/msglog" target="../devices/pseudo/sysmsg@0:msglog" owner="root" group="root" + + // Empty file (implied by missing source) + file destination="/reconfigure" owner="root" group="root" mode="644" +} diff --git a/images/files/boot_console.ttya b/images/files/boot_console.ttya new file mode 100644 index 0000000..19a1d5f --- /dev/null +++ b/images/files/boot_console.ttya @@ -0,0 +1,4 @@ +autoboot_delay="8" +console="ttya" +os_console="ttya" +ttya-mode="115200,8,n,1,-" \ No newline at end of file diff --git a/images/files/default_init.utc b/images/files/default_init.utc new file mode 100644 index 0000000..7cc14c2 --- /dev/null +++ b/images/files/default_init.utc @@ -0,0 +1,4 @@ +# OmniOS default /etc/default/init file (UTC timezone) +# Upstream reference: omnios-image-builder/templates/files/default_init.utc +CMASK=022 +TZ=UTC diff --git a/images/files/etc/hosts b/images/files/etc/hosts new file mode 100644 index 0000000..6677bd2 --- /dev/null +++ b/images/files/etc/hosts @@ -0,0 +1,5 @@ +# +# Internet host table +# +::1 unknown unknown.local localhost loghost +127.0.0.1 unknown unknown.local localhost loghost \ No newline at end of file diff --git a/images/files/etc/nodename b/images/files/etc/nodename new file mode 100644 index 0000000..3546645 --- /dev/null +++ b/images/files/etc/nodename @@ -0,0 +1 @@ +unknown diff --git a/images/files/etc/omnios_sshd_config b/images/files/etc/omnios_sshd_config new file mode 100644 index 0000000..b8c8a92 --- /dev/null +++ b/images/files/etc/omnios_sshd_config @@ -0,0 +1,122 @@ +# $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $ + +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# This sshd was compiled with PATH=/usr/ccs/bin:/usr/bin:/bin:/usr/sbin:/sbin + +# The strategy used for options in the default sshd_config shipped with +# OpenSSH is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_ecdsa_key +#HostKey /etc/ssh/ssh_host_ed25519_key + +# Ciphers and keying +#RekeyLimit default none + +# Logging +#SyslogFacility AUTH +#LogLevel INFO + +# Use the client's locale/language settings +#AcceptEnv LANG LC_ALL LC_CTYPE LC_COLLATE LC_TIME LC_NUMERIC +#AcceptEnv LC_MONETARY LC_MESSAGES + +# Authentication: + +#LoginGraceTime 2m +PermitRootLogin without-password +#StrictModes yes +#MaxAuthTries 6 +#MaxSessions 10 + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile .ssh/authorized_keys + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts +#HostbasedAuthentication no +# Change to yes if you don't trust ~/.ssh/known_hosts for +# HostbasedAuthentication +#IgnoreUserKnownHosts no +# Don't read the user's ~/.rhosts and ~/.shosts files +#IgnoreRhosts yes + +# To disable tunneled clear text passwords, change to no here! +#PasswordAuthentication yes +#PermitEmptyPasswords no + +# Change to no to disable s/key passwords +#ChallengeResponseAuthentication yes + +# Kerberos options +#KerberosAuthentication no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes +#KerberosGetAFSToken no + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes +#GSSAPIStrictAcceptorCheck yes +#GSSAPIKeyExchange no + +# Set this to 'yes' to enable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the ChallengeResponseAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via ChallengeResponseAuthentication may bypass +# the setting of "PermitRootLogin without-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and ChallengeResponseAuthentication to 'no'. +#UsePAM no + +#AllowAgentForwarding yes +#AllowTcpForwarding yes +#GatewayPorts no +#X11Forwarding no +#X11DisplayOffset 10 +#X11UseLocalhost yes +#PermitTTY yes +PrintMotd no +#PrintLastLog yes +#TCPKeepAlive yes +#PermitUserEnvironment no +#Compression delayed +#ClientAliveInterval 0 +#ClientAliveCountMax 3 +#UseDNS no +#PidFile /var/run/sshd.pid +#MaxStartups 10:30:100 +#PermitTunnel no +#ChrootDirectory none +#VersionAddendum none + +# no default banner path +#Banner none + +# override default of no subsystems +Subsystem sftp /usr/libexec/amd64/sftp-server + +# Example of overriding settings on a per-user basis +#Match User anoncvs +# X11Forwarding no +# AllowTcpForwarding no +# PermitTTY no +# ForceCommand cvs server \ No newline at end of file diff --git a/images/files/etc/resolv.conf b/images/files/etc/resolv.conf new file mode 100644 index 0000000..bb27186 --- /dev/null +++ b/images/files/etc/resolv.conf @@ -0,0 +1,2 @@ +nameserver 1.1.1.1 +nameserver 8.8.8.8 diff --git a/images/files/etc/sshd_config b/images/files/etc/sshd_config new file mode 100644 index 0000000..0ec5a80 --- /dev/null +++ b/images/files/etc/sshd_config @@ -0,0 +1,77 @@ +# +# Configuration file for sshd(1m) (see also sshd_config(4)) +# + +Protocol 2 +Port 22 + +# If port forwarding is enabled (default), specify if the server can bind to +# INADDR_ANY. +# This allows the local port forwarding to work when connections are received +# from any remote host. +GatewayPorts no + +# X11 tunneling options +X11Forwarding yes +X11DisplayOffset 10 +X11UseLocalhost yes + +# The maximum number of concurrent unauthenticated connections to sshd. +# start:rate:full see sshd(1) for more information. +# The default is 10 unauthenticated clients. +#MaxStartups 10:30:60 + +# Banner to be printed before authentication starts. +#Banner /etc/issue + +# Should sshd print the /etc/motd file and check for mail. +# On Solaris it is assumed that the login shell will do these (eg /etc/profile). +PrintMotd no + +# KeepAlive specifies whether keep alive messages are sent to the client. +# See sshd(1) for detailed description of what this means. +# Note that the client may also be sending keep alive messages to the server. +KeepAlive yes + +# Syslog facility and level +SyslogFacility auth +LogLevel info + +# +# Authentication configuration +# + +# Host private key files +# Must be on a local disk and readable only by the root user (root:sys 600). +# HostKey /etc/ssh/ssh_host_rsa_key +# HostKey /etc/ssh/ssh_host_dsa_key + +# Ensure secure permissions on users .ssh directory. +StrictModes yes + +# Length of time in seconds before a client that hasn't completed +# authentication is disconnected. +# Default is 600 seconds. 0 means no time limit. +LoginGraceTime 600 + +# Maximum number of retries for authentication +MaxAuthTries 6 + +# Are logins to accounts with empty passwords allowed. +# If PermitEmptyPasswords is no, pass PAM_DISALLOW_NULL_AUTHTOK +# to pam_authenticate(3PAM). +PermitEmptyPasswords no + +# To disable tunneled clear text passwords, change PasswordAuthentication to no. +# You probably also need to disable ChallengeResponseAuthentication. +PasswordAuthentication yes + +# Change to no to disable s/key passwords +#ChallengeResponseAuthentication yes + +PermitRootLogin without-password + +# sftp subsystem +Subsystem sftp internal-sftp + +IgnoreRhosts yes \ No newline at end of file diff --git a/images/files/etc/system b/images/files/etc/system new file mode 100644 index 0000000..741dd4d --- /dev/null +++ b/images/files/etc/system @@ -0,0 +1,2 @@ +set zfs:zfs_arc_max = 0x40000000 +set noexec_user_stack = 1 diff --git a/images/files/omniosce-ca.cert.pem b/images/files/omniosce-ca.cert.pem new file mode 100644 index 0000000..e69de29 diff --git a/images/files/ttydefs.115200 b/images/files/ttydefs.115200 new file mode 100644 index 0000000..bddc3a9 --- /dev/null +++ b/images/files/ttydefs.115200 @@ -0,0 +1,62 @@ +# VERSION=1 +460800:460800 hupcl:460800 hupcl::307200 +307200:307200 hupcl:307200 hupcl::230400 +230400:230400 hupcl:230400 hupcl::153600 +153600:153600 hupcl:153600 hupcl::115200 +115200:115200 hupcl:115200 hupcl::76800 +76800:76800 hupcl:76800 hupcl::57600 +57600:57600 hupcl:57600 hupcl::38400 +38400:38400 hupcl:38400 hupcl::19200 +19200:19200 hupcl:19200 hupcl::9600 +9600:9600 hupcl:9600 hupcl::4800 +4800:4800 hupcl:4800 hupcl::2400 +2400:2400 hupcl:2400 hupcl::1200 +1200:1200 hupcl:1200 hupcl::300 +300:300 hupcl:300 hupcl::460800 + +460800E:460800 hupcl evenp:460800 evenp::307200 +307200E:307200 hupcl evenp:307200 evenp::230400 +230400E:230400 hupcl evenp:230400 evenp::153600 +153600E:153600 hupcl evenp:153600 evenp::115200 +115200E:115200 hupcl evenp:115200 evenp::76800 +76800E:76800 hupcl evenp:76800 evenp::57600 +57600E:57600 hupcl evenp:57600 evenp::38400 +38400E:38400 hupcl evenp:38400 evenp::19200 +19200E:19200 hupcl evenp:19200 evenp::9600 +9600E:9600 hupcl evenp:9600 evenp::4800 +4800E:4800 hupcl evenp:4800 evenp::2400 +2400E:2400 hupcl evenp:2400 evenp::1200 +1200E:1200 hupcl evenp:1200 evenp::300 +300E:300 hupcl evenp:300 evenp::19200 + +auto:hupcl:sane hupcl:A:9600 + +console:115200 hupcl opost onlcr:115200::console +console1:1200 hupcl opost onlcr:1200::console2 +console2:300 hupcl opost onlcr:300::console3 +console3:2400 hupcl opost onlcr:2400::console4 +console4:4800 hupcl opost onlcr:4800::console5 +console5:19200 hupcl opost onlcr:19200::console + +contty:9600 hupcl opost onlcr:9600 sane::contty1 +contty1:1200 hupcl opost onlcr:1200 sane::contty2 +contty2:300 hupcl opost onlcr:300 sane::contty3 +contty3:2400 hupcl opost onlcr:2400 sane::contty4 +contty4:4800 hupcl opost onlcr:4800 sane::contty5 +contty5:19200 hupcl opost onlcr:19200 sane::contty + + +4800H:4800:4800 sane hupcl::9600H +9600H:9600:9600 sane hupcl::19200H +19200H:19200:19200 sane hupcl::38400H +38400H:38400:38400 sane hupcl::2400H +2400H:2400:2400 sane hupcl::1200H +1200H:1200:1200 sane hupcl::300H +300H:300:300 sane hupcl::4800H + +conttyH:9600 opost onlcr:9600 hupcl sane::contty1H +contty1H:1200 opost onlcr:1200 hupcl sane::contty2H +contty2H:300 opost onlcr:300 hupcl sane::contty3H +contty3H:2400 opost onlcr:2400 hupcl sane::contty4H +contty4H:4800 opost onlcr:4800 hupcl sane::contty5H +contty5H:19200 opost onlcr:19200 hupcl sane::conttyH \ No newline at end of file diff --git a/images/omnios-bloody-base.kdl b/images/omnios-bloody-base.kdl new file mode 100644 index 0000000..4881a0f --- /dev/null +++ b/images/omnios-bloody-base.kdl @@ -0,0 +1,47 @@ +// OmniOS bloody base configuration (ported from image-builder JSON) + +metadata name="omnios-bloody-base" version="0.0.1" description="OmniOS bloody: core + extra publishers; base incorporation 'entire'" + +repositories { + // Core publisher + publisher name="omnios" origin="https://pkg.omnios.org/bloody/core/" + + // Extra publisher (enable via features in consumers if applicable) + publisher name="extra.omnios" origin="https://pkg.omnios.org/bloody/extra/" +} + +// Prefer the standard OmniOS incorporation umbrella +incorporation "entire" + +// Approve IPS CA certs used for mTLS when contacting publishers +certificates { + ca publisher="omnios" certfile="omniosce-ca.cert.pem" +} + +// IPS variants to set inside the target image +variants { + // OmniOS global zone + set name="opensolaris.zone" value="global" +} + +// Packages from the artifact phase JSON (finalization steps like pkg_purge_history +// and seed_smf are intentionally omitted here) +packages { + package "/editor/vim" + package "/network/rsync" + package "/system/library/gcc-runtime" + package "/system/library/g++-runtime" + package "/network/ftp" + package "/network/openssh-server" + package "/network/telnet" + package "/service/network/ntpsec" + package "/web/curl" + package "/web/wget" + package "/system/management/mdata-client" +} + +// Build-only tools +packages if="build" { + package "/developer/build-essential" + package "/developer/omnios-build-tools" +} \ No newline at end of file diff --git a/images/omnios-bloody-disk.kdl b/images/omnios-bloody-disk.kdl new file mode 100644 index 0000000..603e4ee --- /dev/null +++ b/images/omnios-bloody-disk.kdl @@ -0,0 +1,70 @@ +// OmniOS bloody final disk image target (ported from image-builder JSON) +// Source JSON reference (abridged): +// { +// "pool": { "name": "rpool", "ashift": 12, "uefi": true, "size": 2000 }, +// "steps": [ +// { "t": "create_be" }, +// { "t": "unpack_tar", "name": "omnios-stable-r${release}.tar" }, +// { "t": "include", "name": "devfs" }, +// { "t": "assemble_files", "dir": "/etc/versions", "output": "/etc/versions/build", "prefix": "build." }, +// { "t": "make_bootable" }, +// { "t": "include", "name": "common" }, +// { "t": "ensure_file", "file": "/boot/conf.d/console", "src": "boot_console.${console}", "owner": "root", "group": "root", "mode": "644" }, +// { "t": "ensure_file", "file": "/etc/ttydefs", "src": "ttydefs.115200", "owner": "root", "group": "sys", "mode": "644" }, +// { "t": "ensure_file", "file": "/etc/default/init", "src": "default_init.utc", "owner": "root", "group": "root", "mode": "644" }, +// { "t": "shadow", "username": "root", "password": "$5$kr1VgdIt$OUiUAyZCDogH/uaxH71rMeQxvpDEY2yX.x0ZQRnmeb9" }, +// { "t": "include", "name": "finalise" } +// ] +// } +// Notes: +// - The refraction KDL schema doesn’t model the step DSL directly; we express the +// relevant parts via includes, overlays, and target settings. +// - UEFI + disk size are mapped to the target stanza. Pool properties are now +// modeled via a pool { property name=".." value=".." } block under target. +// - Finalizers are implicit per the toolchain and don’t need to be listed. + +metadata name="omnios-bloody-disk" version="0.0.1" description="OmniOS bloody: bootable qcow2 target from base spec" + +// Derive from the base publisher/variant/cert configuration and base packages +base "images/omnios-bloody-base.kdl" + +// Device filesystem overlays and common system files +include "images/devfs.kdl" +include "images/common.kdl" + +packages { + package "/system/management/cloud-init" + package "/driver/crypto/viorand" + package "/driver/network/vioif" + package "/driver/storage/vio9p" + package "/driver/storage/vioblk" + package "/driver/storage/vioscsi" +} + +// Files that the original JSON ensured +overlays { + // Console configuration (115200 by default) + file destination="/boot/conf.d/console" source="boot_console.115200" owner="root" group="root" mode="644" + + // TTY speed definitions + file destination="/etc/ttydefs" source="ttydefs.115200" owner="root" group="sys" mode="644" + + // Init defaults (UTC timezone) + file destination="/etc/default/init" source="default_init.utc" owner="root" group="root" mode="644" + + // Fallback local login: set a default root password in /etc/shadow + // This is used only if cloud-init (or metadata) fails to provision credentials + shadow username="root" password="$5$kr1VgdIt$OUiUAyZCDogH/uaxH71rMeQxvpDEY2yX.x0ZQRnmeb9" +} + +// Bootable qcow2 disk image; size is 2000 MB; use UEFI bootloader +target "qcow2" kind="qcow2" { + disk-size "2000M" + bootloader "uefi" + + // ZFS pool properties for the created rpool + pool { + // Match original JSON: ashift=12 + property name="ashift" value="12" + } +}