aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNikolay Govorov <me@govorov.online>2026-07-12 04:11:34 +0100
committerNikolay Govorov <me@govorov.online>2026-07-12 04:11:34 +0100
commit2a56ee9d2c402c55d3efb8491d1dab755c0505a4 (patch)
tree87988837bc155baa0e8c036bfee66548e1cf25dc
parent666ba6b7058c7fbecf4501b3468d9896e3c30d94 (diff)
downloadtar
tar.gz
tar.bz2
tar.lz
tar.xz
tar.zst
zip
Basic OCI workflow: image import, pull/push, and run.
Diffstat
-rw-r--r--.gitignore1+1 −0
-rw-r--r--Cargo.lock2216+2170 −46
-rw-r--r--Cargo.toml1+1 −0
-rw-r--r--crates/hule-image/Cargo.toml17+17 −0
-rw-r--r--crates/hule-image/src/lib.rs793+793 −0
-rw-r--r--crates/hule/Cargo.toml7+6 −1
-rw-r--r--crates/hule/src/main.rs719+570 −149
-rwxr-xr-ximages/alpine/genimg65+33 −32
-rwxr-xr-ximages/debian/genimg66+33 −33
-rwxr-xr-ximages/fedora/genimg58+26 −32
-rwxr-xr-ximages/freebsd/genimg46+21 −25
-rwxr-xr-ximages/ubuntu/genimg55+26 −29
12 files changed, 3697 insertions, 347 deletions
diff --git a/.gitignore b/.gitignore
index 9e0e89b..4af8e00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ __pycache__
# Built machine images and manifests
*.hmi
*.hmm
+/images/*/*/*/config.json
diff --git a/Cargo.lock b/Cargo.lock
index 2c164ec..ec39167 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,115 +3,2239 @@
version = 4
[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "aws-lc-rs"
+version = "1.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad"
+dependencies = [
+ "aws-lc-sys",
+ "untrusted 0.7.1",
+ "zeroize",
+]
+
+[[package]]
+name = "aws-lc-sys"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444"
+dependencies = [
+ "cc",
+ "cmake",
+ "dunce",
+ "fs_extra",
+ "pkg-config",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
+
+[[package]]
+name = "cc"
+version = "1.2.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "const_format"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e"
+dependencies = [
+ "const_format_proc_macros",
+ "konst",
+]
+
+[[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 = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "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",
+]
+
+[[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",
+]
+
+[[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",
+]
+
+[[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",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.1",
+ "const-oid",
+ "crypto-common 0.2.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[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 = "fs_extra"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
+
+[[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",
+]
+
+[[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.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi",
+ "rand_core 0.10.1",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getset"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cf442baaabe4213ce7d1239afc26c039180b6456da2cededa316ae2c8a77a77"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[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 = "http"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
+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 = "hule"
version = "0.1.0"
dependencies = [
- "hule-oci",
- "hule-vmm",
+ "futures-util",
+ "hule-image",
+ "hule-oci",
+ "hule-vmm",
+ "oci-client",
+ "serde_json",
+ "sha2 0.10.9",
+ "tokio",
+ "zstd",
+]
+
+[[package]]
+name = "hule-image"
+version = "0.1.0"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "hule-oci"
+version = "0.1.0"
+
+[[package]]
+name = "hule-vmm"
+version = "0.1.0"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "hyper"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "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.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
+dependencies = [
+ "cfg-if",
+ "combine",
+ "jni-macros",
+ "jni-sys",
+ "log",
+ "simd_cesu8",
+ "thiserror",
+ "walkdir",
+ "windows-link",
+]
+
+[[package]]
+name = "jni-macros"
+version = "0.22.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3"
+dependencies = [
+ "getrandom 0.4.3",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "jsonwebtoken"
+version = "10.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
+dependencies = [
+ "aws-lc-rs",
+ "base64",
+ "getrandom 0.2.17",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "signature",
+ "zeroize",
+]
+
+[[package]]
+name = "konst"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb"
+dependencies = [
+ "konst_macro_rules",
+]
+
+[[package]]
+name = "konst_macro_rules"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "log"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "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 = "oci-client"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5261a7fb43d9c53b8e63e6d5e86860719dad253d015d022066c72d585125aed8"
+dependencies = [
+ "bytes",
+ "chrono",
+ "futures-util",
+ "hex",
+ "http",
+ "http-auth",
+ "jsonwebtoken",
+ "lazy_static",
+ "oci-spec",
+ "olpc-cjson",
+ "regex",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "unicase",
+]
+
+[[package]]
+name = "oci-spec"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8445a2631507cec628a15fdd6154b54a3ab3f20ed4fe9d73a3b8b7a4e1ba03a"
+dependencies = [
+ "const_format",
+ "derive_builder",
+ "getset",
+ "regex",
+ "serde",
+ "serde_json",
+ "strum",
+ "strum_macros",
+ "thiserror",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[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.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[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 = "quinn"
+version = "0.11.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560"
+dependencies = [
+ "aws-lc-rs",
+ "bytes",
+ "getrandom 0.4.3",
+ "lru-slab",
+ "rand",
+ "rand_pcg",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
+dependencies = [
+ "chacha20",
+ "getrandom 0.4.3",
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
+
+[[package]]
+name = "rand_pcg"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a"
+dependencies = [
+ "rand_core 0.10.1",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "reqwest"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted 0.9.0",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f"
+dependencies = [
+ "aws-lc-rs",
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "aws-lc-rs",
+ "ring",
+ "rustls-pki-types",
+ "untrusted 0.9.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
+
+[[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.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_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 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "simd_cesu8"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[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",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.118"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
+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.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "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.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[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",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[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.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[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",
- "serde_json",
]
[[package]]
-name = "hule-oci"
-version = "0.1.0"
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
-name = "hule-vmm"
-version = "0.1.0"
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
-name = "itoa"
-version = "1.0.18"
+name = "walkdir"
+version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
[[package]]
-name = "memchr"
-version = "2.8.3"
+name = "want"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
[[package]]
-name = "proc-macro2"
-version = "1.0.106"
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.126"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
dependencies = [
"unicode-ident",
]
[[package]]
-name = "quote"
-version = "1.0.46"
+name = "wasm-streams"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
+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 = "webpki-root-certs"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[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",
]
[[package]]
-name = "serde"
-version = "1.0.228"
+name = "windows-interface"
+version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
- "serde_core",
- "serde_derive",
+ "proc-macro2",
+ "quote",
+ "syn",
]
[[package]]
-name = "serde_core"
-version = "1.0.228"
+name = "windows-link"
+version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "serde_derive",
+ "windows-link",
]
[[package]]
-name = "serde_derive"
-version = "1.0.228"
+name = "windows-strings"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[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",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[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_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[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_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [
"proc-macro2",
"quote",
"syn",
+ "synstructure",
]
[[package]]
-name = "serde_json"
-version = "1.0.150"
+name = "zerofrom"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
- "itoa",
- "memchr",
- "serde",
- "serde_core",
- "zmij",
+ "zerofrom-derive",
]
[[package]]
-name = "syn"
-version = "2.0.118"
+name = "zerofrom-derive"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [
"proc-macro2",
"quote",
- "unicode-ident",
+ "syn",
+ "synstructure",
]
[[package]]
-name = "unicode-ident"
-version = "1.0.24"
+name = "zeroize"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 251b05f..ef31bac 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,6 +17,7 @@ repository = "https://github.com/dimidiumlabs/hule"
# local dependencies
hule-oci = { path = "crates/hule-oci" }
hule-vmm = { path = "crates/hule-vmm" }
+hule-image = { path = "crates/hule-image" }
# external dependencies
serde = { version = "1", features = ["derive"] }
diff --git a/crates/hule-image/Cargo.toml b/crates/hule-image/Cargo.toml
new file mode 100644
--- /dev/null
+++ b/crates/hule-image/Cargo.toml
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2026 Nikolay Govorov
+# SPDX-License-Identifier: Apache-2.0
+
+[package]
+name = "hule-image"
+description = "Types and validation for Hule machine images"
+
+publish.workspace = true
+edition.workspace = true
+version.workspace = true
+license.workspace = true
+authors.workspace = true
+repository.workspace = true
+
+[dependencies]
+serde.workspace = true
+serde_json.workspace = true
diff --git a/crates/hule-image/src/lib.rs b/crates/hule-image/src/lib.rs
new file mode 100644
--- /dev/null
+++ b/crates/hule-image/src/lib.rs
@@ -0,0 +1,793 @@
+// SPDX-FileCopyrightText: 2026 Nikolay Govorov
+// SPDX-License-Identifier: Apache-2.0
+
+//! Types and validation for a Hule machine image `config.json`.
+//!
+//! This crate deliberately has no filesystem, registry, or VMM concerns. It
+//! validates the declared image contract; callers decide how referenced files
+//! are stored and executed.
+//!
+//! # Schema evolution
+//!
+//! Bump [`SCHEMA_VERSION`] only for breaking changes. Within a version:
+//! [`Boot`]/[`Access`] are menus, so they tolerate unrecognized entries via
+//! `Unknown`; [`Architecture`]/[`DiskFormat`] are single mandatory values
+//! with no fallback, so they stay closed enums; `system.os` and digest
+//! algorithms stay plain strings. No `deny_unknown_fields`; `rename_all =
+//! "camelCase"` on every struct.
+
+use std::collections::HashSet;
+use std::fmt;
+use std::path::{Component, Path};
+
+use serde::{Deserialize, Serialize};
+
+pub const SCHEMA_VERSION: u8 = 1;
+pub const KIND: &str = "MachineImage";
+
+pub const BOOT_LINUX_DIRECT: &str = "linux/direct";
+pub const BOOT_FIRMWARE_DISK_BIOS: &str = "firmware-disk/bios";
+pub const BOOT_FIRMWARE_DISK_UEFI: &str = "firmware-disk/uefi";
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum DiskFormat {
+ Qcow2,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(transparent)]
+pub struct Digest(pub String);
+
+impl fmt::Display for Digest {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(&self.0)
+ }
+}
+
+impl Digest {
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ let Some(hex) = self.0.strip_prefix("sha256:") else {
+ validation.error(format!("{field} must use sha256"));
+ return;
+ };
+ if hex.len() != 64 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
+ validation.error(format!("{field} must contain 64 hexadecimal digits"));
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ResourceRange {
+ pub minimum: u64,
+ pub default: u64,
+}
+
+impl ResourceRange {
+ fn validate_into(self, field: &str, validation: &mut Validation) {
+ if self.minimum == 0 {
+ validation.error(format!("{field}.minimum must be greater than zero"));
+ }
+ if self.default < self.minimum {
+ validation.error(format!("{field}.default must be at least minimum"));
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FileRef {
+ pub path: String,
+ pub digest: Digest,
+}
+
+impl FileRef {
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ validation.path(&self.path, &format!("{field}.path"));
+ self.digest
+ .validate_into(&format!("{field}.digest"), validation);
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BootBios {
+ pub protocol: String,
+ pub disk: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BootUefi {
+ pub protocol: String,
+ pub disk: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BootLinux {
+ pub protocol: String,
+ pub disk: String,
+ pub kernel: FileRef,
+ pub initrd: FileRef,
+ pub cmdline: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(untagged)]
+pub enum Boot {
+ Bios(BootBios),
+ Uefi(BootUefi),
+ Linux(BootLinux),
+ Unknown(serde_json::Value),
+}
+
+impl<'de> Deserialize<'de> for Boot {
+ fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ use serde::de::Error;
+
+ let value = serde_json::Value::deserialize(deserializer)?;
+ let protocol = value.get("protocol").and_then(|p| p.as_str()).unwrap_or("");
+
+ #[derive(Deserialize)]
+ struct Raw {
+ disk: String,
+ #[serde(default)]
+ kernel: Option<FileRef>,
+ #[serde(default)]
+ initrd: Option<FileRef>,
+ #[serde(default)]
+ cmdline: Option<String>,
+ }
+
+ match protocol {
+ BOOT_FIRMWARE_DISK_BIOS => {
+ let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?;
+ if raw.kernel.is_some() || raw.initrd.is_some() || raw.cmdline.is_some() {
+ return Err(Error::custom(
+ "firmware-disk boot must not contain kernel, initrd, or cmdline",
+ ));
+ }
+ Ok(Self::Bios(BootBios {
+ protocol: BOOT_FIRMWARE_DISK_BIOS.into(),
+ disk: raw.disk,
+ }))
+ }
+ BOOT_FIRMWARE_DISK_UEFI => {
+ let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?;
+ if raw.kernel.is_some() || raw.initrd.is_some() || raw.cmdline.is_some() {
+ return Err(Error::custom(
+ "firmware-disk boot must not contain kernel, initrd, or cmdline",
+ ));
+ }
+ Ok(Self::Uefi(BootUefi {
+ protocol: BOOT_FIRMWARE_DISK_UEFI.into(),
+ disk: raw.disk,
+ }))
+ }
+ BOOT_LINUX_DIRECT => {
+ let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?;
+ Ok(Self::Linux(BootLinux {
+ protocol: BOOT_LINUX_DIRECT.into(),
+ disk: raw.disk,
+ kernel: raw.kernel.ok_or_else(|| Error::missing_field("kernel"))?,
+ initrd: raw.initrd.ok_or_else(|| Error::missing_field("initrd"))?,
+ cmdline: raw.cmdline.ok_or_else(|| Error::missing_field("cmdline"))?,
+ }))
+ }
+ _ => Ok(Self::Unknown(value)),
+ }
+ }
+}
+
+impl Boot {
+ pub fn protocol(&self) -> &str {
+ match self {
+ Self::Bios(v) => &v.protocol,
+ Self::Uefi(v) => &v.protocol,
+ Self::Linux(v) => &v.protocol,
+ Self::Unknown(v) => v.get("protocol").and_then(|p| p.as_str()).unwrap_or(""),
+ }
+ }
+
+ pub fn disk(&self) -> &str {
+ match self {
+ Self::Bios(v) => &v.disk,
+ Self::Uefi(v) => &v.disk,
+ Self::Linux(v) => &v.disk,
+ Self::Unknown(_) => "",
+ }
+ }
+
+ /// False only for `Unknown`.
+ pub fn is_recognized(&self) -> bool {
+ !matches!(self, Self::Unknown(_))
+ }
+
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ match self {
+ Self::Bios(value) => {
+ if value.protocol != BOOT_FIRMWARE_DISK_BIOS {
+ validation.error(format!(
+ "{field} BIOS protocol must be '{BOOT_FIRMWARE_DISK_BIOS}'"
+ ));
+ }
+ validation.required(&value.disk, &format!("{field}.disk"));
+ }
+ Self::Uefi(value) => {
+ if value.protocol != BOOT_FIRMWARE_DISK_UEFI {
+ validation.error(format!(
+ "{field} UEFI protocol must be '{BOOT_FIRMWARE_DISK_UEFI}'"
+ ));
+ }
+ validation.required(&value.disk, &format!("{field}.disk"));
+ }
+ Self::Linux(value) => {
+ if value.protocol != BOOT_LINUX_DIRECT {
+ validation.error(format!(
+ "{field} direct Linux protocol must be '{BOOT_LINUX_DIRECT}'"
+ ));
+ }
+ validation.required(&value.disk, &format!("{field}.disk"));
+ value
+ .kernel
+ .validate_into(&format!("{field}.kernel"), validation);
+ value
+ .initrd
+ .validate_into(&format!("{field}.initrd"), validation);
+ validation.required(&value.cmdline, &format!("{field}.cmdline"));
+ }
+ // Nothing to check: unrecognized entries are tolerated, not inspected.
+ Self::Unknown(_) => {}
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Architecture {
+ Amd64,
+ Arm64,
+ Loong64,
+ Ppc64le,
+ Riscv64,
+ S390x,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct System {
+ pub os: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option<String>,
+ pub version: String,
+ pub architecture: Architecture,
+}
+
+impl System {
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ validation.required(&self.os, &format!("{field}.os"));
+ if let Some(name) = &self.name {
+ validation.required(name, &format!("{field}.name"));
+ }
+ validation.required(&self.version, &format!("{field}.version"));
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Disk {
+ pub id: String,
+ pub format: DiskFormat,
+ pub path: String,
+ pub digest: Digest,
+ pub virt_size: u64,
+ pub disk_size: u64,
+}
+
+impl Disk {
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ validation.required(&self.id, &format!("{field}.id"));
+ validation.path(&self.path, &format!("{field}.path"));
+ self.digest
+ .validate_into(&format!("{field}.digest"), validation);
+ if self.virt_size == 0 {
+ validation.error(format!("{field}.virtSize must be greater than zero"));
+ }
+ if self.disk_size == 0 {
+ validation.error(format!("{field}.diskSize must be greater than zero"));
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
+#[serde(untagged, rename_all = "camelCase")]
+pub enum Access {
+ Ssh {
+ #[serde(rename = "type")]
+ kind: String,
+ port: u16,
+ user: String,
+ auth: String,
+ },
+ /// Unrecognized access surface, preserved verbatim.
+ Unknown(serde_json::Value),
+}
+
+impl<'de> Deserialize<'de> for Access {
+ fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+ use serde::de::Error;
+
+ let value = serde_json::Value::deserialize(deserializer)?;
+ let kind = value.get("type").and_then(|t| t.as_str()).unwrap_or("");
+
+ match kind {
+ "ssh" => {
+ #[derive(Deserialize)]
+ struct Raw {
+ port: u16,
+ user: String,
+ auth: String,
+ }
+ let raw: Raw = serde_json::from_value(value).map_err(Error::custom)?;
+ Ok(Self::Ssh {
+ kind: "ssh".into(),
+ port: raw.port,
+ user: raw.user,
+ auth: raw.auth,
+ })
+ }
+ _ => Ok(Self::Unknown(value)),
+ }
+ }
+}
+
+impl Access {
+ /// False only for `Unknown`.
+ pub fn is_recognized(&self) -> bool {
+ !matches!(self, Self::Unknown(_))
+ }
+
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ match self {
+ Self::Ssh { port, user, auth, .. } => {
+ if *port == 0 {
+ validation.error(format!("{field}.port must be greater than zero"));
+ }
+ validation.required(user, &format!("{field}.user"));
+ validation.required(auth, &format!("{field}.auth"));
+ }
+ Self::Unknown(_) => {}
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NetworkDhcp {}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct NetworkStatic {
+ pub address: String,
+ pub gateway: String,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(tag = "mode", rename_all = "lowercase")]
+pub enum Network {
+ Dhcp(NetworkDhcp),
+ Static(NetworkStatic),
+}
+
+impl Network {
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ if let Self::Static(network) = self {
+ validation.required(&network.address, &format!("{field}.address"));
+ validation.required(&network.gateway, &format!("{field}.gateway"));
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Machine {
+ pub cpu: ResourceRange,
+ pub ram: ResourceRange,
+ pub boot: Vec<Boot>,
+ pub access: Vec<Access>,
+ pub network: Network,
+}
+
+impl Machine {
+ fn validate_into(&self, field: &str, validation: &mut Validation) {
+ self.cpu.validate_into(&format!("{field}.cpu"), validation);
+ self.ram.validate_into(&format!("{field}.ram"), validation);
+ if !self.boot.iter().any(Boot::is_recognized) {
+ validation.error(format!(
+ "{field}.boot must contain at least one supported boot protocol"
+ ));
+ }
+ for (index, boot) in self.boot.iter().enumerate() {
+ boot.validate_into(&format!("{field}.boot[{index}]"), validation);
+ }
+ if !self.access.iter().any(Access::is_recognized) {
+ validation.error(format!(
+ "{field}.access must contain at least one supported access method \
+ (a running machine must be reachable by something, if only to shut it down)"
+ ));
+ }
+ for (index, access) in self.access.iter().enumerate() {
+ access.validate_into(&format!("{field}.access[{index}]"), validation);
+ }
+ self.network
+ .validate_into(&format!("{field}.network"), validation);
+ }
+}
+
+#[derive(Default)]
+struct Validation {
+ issues: Vec<String>,
+}
+
+impl Validation {
+ fn error(&mut self, message: impl Into<String>) {
+ self.issues.push(message.into());
+ }
+
+ fn required(&mut self, value: &str, field: &str) {
+ if value.trim().is_empty() {
+ self.error(format!("{field} must not be empty"));
+ }
+ }
+
+ fn path(&mut self, value: &str, field: &str) {
+ let path = Path::new(value);
+ if value.is_empty()
+ || path.is_absolute()
+ || path
+ .components()
+ .any(|component| !matches!(component, Component::Normal(_)))
+ {
+ self.error(format!("{field} must be a relative normalized path"));
+ }
+ }
+
+ fn finish(self) -> Result<(), ValidationError> {
+ if self.issues.is_empty() {
+ Ok(())
+ } else {
+ Err(ValidationError {
+ issues: self.issues,
+ })
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct ValidationError {
+ issues: Vec<String>,
+}
+
+impl ValidationError {
+ fn single(message: impl Into<String>) -> Self {
+ Self {
+ issues: vec![message.into()],
+ }
+ }
+
+ pub fn issues(&self) -> &[String] {
+ &self.issues
+ }
+}
+
+impl fmt::Display for ValidationError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "invalid Hule image configuration: {}",
+ self.issues.join("; ")
+ )
+ }
+}
+
+impl std::error::Error for ValidationError {}
+
+#[derive(Debug)]
+pub enum ParseError {
+ Json(serde_json::Error),
+ Validation(ValidationError),
+}
+
+impl fmt::Display for ParseError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Json(error) => write!(f, "invalid JSON: {error}"),
+ Self::Validation(error) => error.fmt(f),
+ }
+ }
+}
+
+impl std::error::Error for ParseError {}
+
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MachineImage {
+ pub schema_version: u8,
+ pub kind: String,
+ pub system: System,
+ pub machine: Machine,
+ pub disks: Vec<Disk>,
+}
+
+impl MachineImage {
+ pub fn from_json(data: &[u8]) -> Result<Self, ParseError> {
+ #[derive(Deserialize)]
+ #[serde(rename_all = "camelCase")]
+ struct Header {
+ schema_version: u8,
+ #[serde(default)]
+ kind: Option<String>,
+ }
+
+ let header: Header = serde_json::from_slice(data).map_err(ParseError::Json)?;
+ if header.schema_version != SCHEMA_VERSION {
+ return Err(ParseError::Validation(ValidationError::single(format!(
+ "unsupported schemaVersion {}",
+ header.schema_version
+ ))));
+ }
+ if header.kind.as_deref() != Some(KIND) {
+ return Err(ParseError::Validation(ValidationError::single(format!(
+ "kind must be '{KIND}'"
+ ))));
+ }
+
+ let image: Self = serde_json::from_slice(data).map_err(ParseError::Json)?;
+ image.validate().map_err(ParseError::Validation)?;
+ Ok(image)
+ }
+
+ pub fn validate(&self) -> Result<(), ValidationError> {
+ if self.schema_version != SCHEMA_VERSION {
+ return Err(ValidationError::single(format!(
+ "unsupported schemaVersion {}",
+ self.schema_version
+ )));
+ }
+ if self.kind != KIND {
+ return Err(ValidationError::single(format!("kind must be '{KIND}'")));
+ }
+
+ let mut validation = Validation::default();
+ self.system.validate_into("system", &mut validation);
+ self.machine.validate_into("machine", &mut validation);
+
+ if self.disks.is_empty() {
+ validation.error("disks must not be empty");
+ }
+ let mut disk_ids = HashSet::new();
+ let mut paths = HashSet::new();
+ for (index, disk) in self.disks.iter().enumerate() {
+ let at = format!("disks[{index}]");
+ disk.validate_into(&at, &mut validation);
+ if !disk_ids.insert(disk.id.as_str()) {
+ validation.error(format!("duplicate disk id '{}'", disk.id));
+ }
+ if !paths.insert(disk.path.as_str()) {
+ validation.error(format!("duplicate image path '{}'", disk.path));
+ }
+ }
+
+ let mut boot_protocols = HashSet::new();
+ for (index, boot) in self.machine.boot.iter().enumerate() {
+ if !boot.is_recognized() {
+ // Tolerated, not cross-referenced: we don't know what its
+ // fields (e.g. "disk") even mean.
+ continue;
+ }
+ if !boot_protocols.insert(boot.protocol()) {
+ validation.error(format!("duplicate boot protocol '{}'", boot.protocol()));
+ }
+ if !disk_ids.contains(boot.disk()) {
+ validation.error(format!(
+ "machine.boot[{index}] references unknown disk '{}'",
+ boot.disk()
+ ));
+ }
+ if let Boot::Linux(linux) = boot {
+ if self.system.os != "linux" {
+ validation.error(format!(
+ "machine.boot[{index}] protocol '{BOOT_LINUX_DIRECT}' requires system.os 'linux'"
+ ));
+ }
+ for path in [&linux.kernel.path, &linux.initrd.path] {
+ if !paths.insert(path) {
+ validation.error(format!("duplicate image path '{path}'"));
+ }
+ }
+ }
+ }
+
+ validation.finish()
+ }
+
+ pub fn referenced_paths(&self) -> impl Iterator<Item = &str> {
+ self.disks.iter().map(|d| d.path.as_str()).chain(
+ self.machine
+ .boot
+ .iter()
+ .flat_map(|boot| match boot {
+ Boot::Bios(_) | Boot::Uefi(_) | Boot::Unknown(_) => [None, None],
+ Boot::Linux(v) => [Some(v.kernel.path.as_str()), Some(v.initrd.path.as_str())],
+ })
+ .flatten(),
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ fn valid_json() -> Vec<u8> {
+ br#"{"schemaVersion":1,"kind":"MachineImage","system":{"os":"linux","name":"alpine","version":"3.24","architecture":"amd64"},"machine":{"cpu":{"minimum":1,"default":2},"ram":{"minimum":268435456,"default":1073741824},"boot":[{"protocol":"firmware-disk/bios","disk":"root"}],"access":[{"type":"ssh","port":22,"user":"build","auth":"empty-password"}],"network":{"mode":"static","address":"10.0.2.15/24","gateway":"10.0.2.2"}},"disks":[{"id":"root","format":"qcow2","path":"root.hmi","digest":"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","virtSize":1024,"diskSize":512}]}"#.to_vec()
+ }
+
+ #[test]
+ fn parses_and_round_trips_valid_config() {
+ let image = MachineImage::from_json(&valid_json()).unwrap();
+ let encoded = serde_json::to_vec(&image).unwrap();
+ assert_eq!(MachineImage::from_json(&encoded).unwrap(), image);
+ }
+
+ #[test]
+ fn rejects_unknown_disk_and_unsafe_path() {
+ let mut image = MachineImage::from_json(&valid_json()).unwrap();
+ image.disks[0].path = "../root.hmi".into();
+ match &mut image.machine.boot[0] {
+ Boot::Bios(v) => v.disk = "missing".into(),
+ Boot::Uefi(_) | Boot::Linux(_) | Boot::Unknown(_) => unreachable!(),
+ }
+ let error = image.validate().unwrap_err();
+ assert!(
+ error
+ .issues()
+ .iter()
+ .any(|v| v.contains("relative normalized path"))
+ );
+ assert!(error.issues().iter().any(|v| v.contains("unknown disk")));
+ }
+
+ #[test]
+ fn rejects_invalid_digest_and_resource_range() {
+ let mut image = MachineImage::from_json(&valid_json()).unwrap();
+ image.disks[0].digest = Digest("md5:no".into());
+ image.machine.cpu.default = 0;
+ let error = image.validate().unwrap_err();
+ assert_eq!(error.issues().len(), 2);
+ }
+
+ #[test]
+ fn system_name_is_optional_but_not_empty() {
+ let mut image = MachineImage::from_json(&valid_json()).unwrap();
+ image.system.name = None;
+ let encoded = serde_json::to_value(&image).unwrap();
+ assert!(encoded["system"].get("name").is_none());
+ image.validate().unwrap();
+
+ image.system.name = Some(String::new());
+ assert!(image.validate().is_err());
+ }
+
+ #[test]
+ fn parses_uefi_firmware_disk_boot() {
+ let mut image = MachineImage::from_json(&valid_json()).unwrap();
+ image.machine.boot[0] = Boot::Uefi(BootUefi {
+ protocol: BOOT_FIRMWARE_DISK_UEFI.into(),
+ disk: "root".into(),
+ });
+
+ let encoded = serde_json::to_vec(&image).unwrap();
+ let decoded = MachineImage::from_json(&encoded).unwrap();
+ assert_eq!(decoded.machine.boot[0].protocol(), BOOT_FIRMWARE_DISK_UEFI);
+ }
+
+ #[test]
+ fn rejects_config_whose_only_boot_entry_is_unrecognized() {
+ // An unrecognized boot protocol must not fail *parsing* -- see
+ // `mixed_boot_list_tolerates_unrecognized_entries` below for why --
+ // but a config with *no* recognized entry at all is unusable and
+ // must fail *validation*.
+ let json = String::from_utf8(valid_json())
+ .unwrap()
+ .replace(BOOT_FIRMWARE_DISK_BIOS, "firmware-disk/unknown");
+ let error = MachineImage::from_json(json.as_bytes()).unwrap_err();
+ assert!(
+ error
+ .to_string()
+ .contains("must contain at least one supported boot protocol")
+ );
+ }
+
+ #[test]
+ fn mixed_boot_list_tolerates_unrecognized_entries() {
+ // A newer image may list a protocol this version doesn't know about
+ // alongside one it does; parsing and validation must both succeed,
+ // and the unrecognized entry must round-trip losslessly.
+ let json = String::from_utf8(valid_json()).unwrap().replace(
+ r#"[{"protocol":"firmware-disk/bios","disk":"root"}]"#,
+ r#"[{"protocol":"firmware-disk/bios","disk":"root"},{"protocol":"vsock/direct","disk":"root","futureField":42}]"#,
+ );
+ let image = MachineImage::from_json(json.as_bytes()).unwrap();
+ assert_eq!(image.machine.boot.len(), 2);
+ assert!(image.machine.boot[0].is_recognized());
+ assert!(!image.machine.boot[1].is_recognized());
+
+ let encoded = serde_json::to_vec(&image).unwrap();
+ assert_eq!(MachineImage::from_json(&encoded).unwrap(), image);
+ }
+
+ #[test]
+ fn rejects_config_whose_only_access_entry_is_unrecognized() {
+ let json = String::from_utf8(valid_json()).unwrap().replace(
+ r#"{"type":"ssh","port":22,"user":"build","auth":"empty-password"}"#,
+ r#"{"type":"vsock-agent","channel":9000}"#,
+ );
+ let error = MachineImage::from_json(json.as_bytes()).unwrap_err();
+ assert!(
+ error
+ .to_string()
+ .contains("must contain at least one supported access method")
+ );
+ }
+
+ #[test]
+ fn parses_dhcp_network_without_static_parameters() {
+ let json = String::from_utf8(valid_json()).unwrap().replace(
+ r#"{"mode":"static","address":"10.0.2.15/24","gateway":"10.0.2.2"}"#,
+ r#"{"mode":"dhcp"}"#,
+ );
+ let image = MachineImage::from_json(json.as_bytes()).unwrap();
+ assert!(matches!(image.machine.network, Network::Dhcp(_)));
+ }
+
+ #[test]
+ fn rejects_unknown_schema_before_parsing_schema_fields() {
+ let error = MachineImage::from_json(br#"{"schemaVersion":2}"#).unwrap_err();
+ let ParseError::Validation(error) = error else {
+ panic!("expected validation error")
+ };
+ assert_eq!(error.issues(), ["unsupported schemaVersion 2"]);
+ }
+
+ #[test]
+ fn rejects_unknown_kind_before_parsing_kind_fields() {
+ let error = MachineImage::from_json(br#"{"schemaVersion":1,"kind":"Other"}"#).unwrap_err();
+ let ParseError::Validation(error) = error else {
+ panic!("expected validation error")
+ };
+ assert_eq!(error.issues(), ["kind must be 'MachineImage'"]);
+ }
+
+ #[test]
+ fn rejects_unknown_architecture() {
+ let json = String::from_utf8(valid_json())
+ .unwrap()
+ .replace("\"architecture\":\"amd64\"", "\"architecture\":\"mips\"");
+ let error = MachineImage::from_json(json.as_bytes()).unwrap_err();
+ assert!(error.to_string().contains("unknown variant `mips`"));
+ }
+
+ #[test]
+ fn parses_additional_oci_architectures() {
+ for architecture in ["loong64", "riscv64", "s390x"] {
+ let json = String::from_utf8(valid_json()).unwrap().replace(
+ "\"architecture\":\"amd64\"",
+ &format!("\"architecture\":\"{architecture}\""),
+ );
+ MachineImage::from_json(json.as_bytes()).unwrap();
+ }
+ }
+}
diff --git a/crates/hule/Cargo.toml b/crates/hule/Cargo.toml
index 8a5dfa7..41a87ef 100644
--- a/crates/hule/Cargo.toml
+++ b/crates/hule/Cargo.toml
@@ -15,5 +15,10 @@ repository.workspace = true
[dependencies]
hule-oci.workspace = true
hule-vmm.workspace = true
-serde.workspace = true
+hule-image.workspace = true
serde_json.workspace = true
+oci-client = "0.17"
+tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs"] }
+futures-util = "0.3"
+zstd = "0.13"
+sha2 = "0.10"
diff --git a/crates/hule/src/main.rs b/crates/hule/src/main.rs
index ba3a72e..fdff3a8 100644
--- a/crates/hule/src/main.rs
+++ b/crates/hule/src/main.rs
@@ -1,198 +1,549 @@
// SPDX-FileCopyrightText: 2026 Nikolay Govorov
// SPDX-License-Identifier: Apache-2.0
-//! Manifest-driven VM harness
+//! Manifest-driven VM harness and OCI image lifecycle.
//!
-//! Reads a Hule Machine Manifest (`image.hmm`) from an image directory and
+//! Reads a Hule machine configuration (`config.json`) from an image directory and
//! boots the guest with qemu according to the boot protocol it declares. No
//! per-distro logic lives here -- everything comes from the manifest. See
//! docs/boot-protocol.md.
+//!
+//! Images live in a local content-addressed store (`$HOME/.hule/store`, a
+//! plain OCI Image Layout) and move in and out of OCI registries via
+//! `import`/`push`/`pull`; `run` resolves a reference against that store
+//! instead of taking a raw directory.
+use std::collections::{BTreeMap, BTreeSet};
+use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, exit};
-use serde::Deserialize;
+use hule_image::{Access, Architecture, BOOT_FIRMWARE_DISK_BIOS, Boot, MachineImage};
+use oci_client::annotations::{ORG_OPENCONTAINERS_IMAGE_REF_NAME, ORG_OPENCONTAINERS_IMAGE_TITLE};
+use oci_client::client::{ClientConfig, ClientProtocol, Config, ImageLayer};
+use oci_client::manifest::{
+ ImageIndexEntry, OCI_IMAGE_MEDIA_TYPE, OciDescriptor, OciImageIndex, OciImageManifest,
+};
+use oci_client::secrets::RegistryAuth;
+use oci_client::{Client, Reference};
+
+type R<T> = std::result::Result<T, String>;
-#[derive(Deserialize)]
-struct MinDefault {
- minimum: u64,
- default: u64,
+fn die(msg: impl AsRef<str>) -> ! {
+ eprintln!("hule: {}", msg.as_ref());
+ exit(1);
}
-#[derive(Deserialize)]
-struct System {
- family: String,
- distro: String,
- release: String,
+// ---- local OCI-layout store -------------------------------------------
+
+const CHUNK_SIZE: usize = 256 * 1024 * 1024;
+const HULE_CHUNK_MEDIA_TYPE: &str = "application/vnd.hule.disk.chunk.v1";
+const HULE_CONFIG_MEDIA_TYPE: &str = "application/vnd.hule.machine.config.v1+json";
+const ANNOTATION_CHUNK_OFFSET: &str = "io.hule.chunk.offset";
+const ANNOTATION_CHUNK_LENGTH: &str = "io.hule.chunk.length";
+
+fn store_root() -> PathBuf {
+ let home = std::env::var("HOME").unwrap_or_else(|_| die("HOME is not set"));
+ PathBuf::from(home).join(".hule").join("store")
}
-#[derive(Deserialize)]
-struct Machine {
- arch: String,
- cpu: MinDefault,
- ram: MinDefault,
+fn blobs_dir() -> PathBuf {
+ store_root().join("blobs").join("sha256")
}
-#[derive(Deserialize)]
-struct Disk {
- id: String,
- path: String,
+fn materialized_dir(manifest_digest: &str) -> PathBuf {
+ store_root()
+ .join("materialized")
+ .join(strip_sha256(manifest_digest))
}
-#[derive(Deserialize)]
-struct FileRef {
- path: String,
- checksum: String,
+fn index_path() -> PathBuf {
+ store_root().join("index.json")
}
-/// A boot protocol entry, discriminated by its `id`. `firmware-disk/*` boots a
-/// self-contained disk; `linux/*` is direct-kernel and therefore carries the
-/// kernel, initrd and cmdline (validated at parse time, not left optional).
-enum Boot {
- FirmwareDisk {
- id: String,
- disk: String,
- },
- Linux {
- id: String,
- disk: String,
- kernel: FileRef,
- initrd: FileRef,
- cmdline: String,
- },
+fn strip_sha256(digest: &str) -> &str {
+ digest.strip_prefix("sha256:").unwrap_or(digest)
}
-impl Boot {
- fn id(&self) -> &str {
- match self {
- Boot::FirmwareDisk { id, .. } | Boot::Linux { id, .. } => id,
- }
+fn sha256_hex(data: &[u8]) -> String {
+ use sha2::{Digest, Sha256};
+ let mut hasher = Sha256::new();
+ hasher.update(data);
+ format!("sha256:{:x}", hasher.finalize())
+}
+
+fn ensure_store() -> R<()> {
+ std::fs::create_dir_all(blobs_dir()).map_err(|e| e.to_string())?;
+ std::fs::create_dir_all(store_root().join("materialized")).map_err(|e| e.to_string())?;
+ let layout = store_root().join("oci-layout");
+ if !layout.exists() {
+ std::fs::write(&layout, br#"{"imageLayoutVersion":"1.0.0"}"#).map_err(|e| e.to_string())?;
+ }
+ Ok(())
+}
+
+/// Writes raw bytes as a content-addressed blob, no-op if already present.
+fn write_blob(data: &[u8]) -> R<String> {
+ let digest = sha256_hex(data);
+ let path = blobs_dir().join(strip_sha256(&digest));
+ if !path.exists() {
+ let tmp = path.with_extension("tmp");
+ std::fs::write(&tmp, data).map_err(|e| e.to_string())?;
+ std::fs::rename(&tmp, &path).map_err(|e| e.to_string())?;
+ }
+ Ok(digest)
+}
+
+fn read_blob(digest: &str) -> R<Vec<u8>> {
+ std::fs::read(blobs_dir().join(strip_sha256(digest))).map_err(|e| e.to_string())
+}
+
+fn read_index() -> R<OciImageIndex> {
+ let path = index_path();
+ if !path.exists() {
+ return Ok(OciImageIndex {
+ schema_version: 2,
+ media_type: None,
+ manifests: vec![],
+ artifact_type: None,
+ annotations: None,
+ });
+ }
+ let data = std::fs::read(&path).map_err(|e| e.to_string())?;
+ serde_json::from_slice(&data).map_err(|e| e.to_string())
+}
+
+fn write_index(index: &OciImageIndex) -> R<()> {
+ let data = serde_json::to_vec_pretty(index).map_err(|e| e.to_string())?;
+ std::fs::write(index_path(), data).map_err(|e| e.to_string())
+}
+
+fn index_lookup(index: &OciImageIndex, reference: &str) -> Option<String> {
+ index
+ .manifests
+ .iter()
+ .find(|m| {
+ m.annotations
+ .as_ref()
+ .and_then(|a| a.get(ORG_OPENCONTAINERS_IMAGE_REF_NAME))
+ .map(String::as_str)
+ == Some(reference)
+ })
+ .map(|m| m.digest.clone())
+}
+
+fn index_set(index: &mut OciImageIndex, reference: &str, digest: &str, size: u64) {
+ index.manifests.retain(|m| {
+ m.annotations
+ .as_ref()
+ .and_then(|a| a.get(ORG_OPENCONTAINERS_IMAGE_REF_NAME))
+ .map(String::as_str)
+ != Some(reference)
+ });
+ let mut annotations = BTreeMap::new();
+ annotations.insert(
+ ORG_OPENCONTAINERS_IMAGE_REF_NAME.to_string(),
+ reference.to_string(),
+ );
+ index.manifests.push(ImageIndexEntry {
+ media_type: OCI_IMAGE_MEDIA_TYPE.to_string(),
+ digest: digest.to_string(),
+ size: size as i64,
+ platform: None,
+ annotations: Some(annotations),
+ artifact_type: None,
+ });
+}
+
+fn hardlink_or_copy(src: &Path, dest: &Path) -> R<()> {
+ if std::fs::hard_link(src, dest).is_err() {
+ std::fs::copy(src, dest).map_err(|e| e.to_string())?;
}
+ Ok(())
+}
- fn disk(&self) -> &str {
- match self {
- Boot::FirmwareDisk { disk, .. } | Boot::Linux { disk, .. } => disk,
+fn write_at(file: &std::fs::File, offset: u64, data: &[u8]) -> R<()> {
+ use std::os::unix::fs::FileExt;
+ let mut written = 0usize;
+ while written < data.len() {
+ let n = file
+ .write_at(&data[written..], offset + written as u64)
+ .map_err(|e| e.to_string())?;
+ if n == 0 {
+ return Err("write_at wrote 0 bytes".to_string());
}
+ written += n;
}
+ Ok(())
}
-impl<'de> Deserialize<'de> for Boot {
- fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
- use serde::de::Error;
-
- #[derive(Deserialize)]
- struct Raw {
- id: String,
- disk: String,
- #[serde(default)]
- kernel: Option<FileRef>,
- #[serde(default)]
- initrd: Option<FileRef>,
- #[serde(default)]
- cmdline: Option<String>,
+/// Splits `path` into fixed-size chunks, compresses each independently
+/// (own zstd frame, no state shared between chunks -- see the design notes
+/// in the project plan on why this must not be done the other way around),
+/// and writes each as its own blob. Returns one descriptor per chunk, all
+/// sharing a title annotation plus a chunk offset/length in terms of the
+/// *uncompressed* file so pull can reassemble it.
+fn write_file_chunks(path: &Path) -> R<Vec<OciDescriptor>> {
+ let title = path
+ .file_name()
+ .ok_or_else(|| format!("{} has no file name", path.display()))?
+ .to_string_lossy()
+ .to_string();
+ let mut file = std::fs::File::open(path).map_err(|e| e.to_string())?;
+ let mut descriptors = Vec::new();
+ let mut offset: u64 = 0;
+ loop {
+ let mut buf = Vec::with_capacity(CHUNK_SIZE);
+ (&mut file)
+ .take(CHUNK_SIZE as u64)
+ .read_to_end(&mut buf)
+ .map_err(|e| e.to_string())?;
+ if buf.is_empty() {
+ break;
}
+ let length = buf.len() as u64;
+ let compressed = zstd::stream::encode_all(&buf[..], 0).map_err(|e| e.to_string())?;
+ let digest = write_blob(&compressed)?;
- let r = Raw::deserialize(d)?;
- if r.id.starts_with("linux/") {
- Ok(Boot::Linux {
- kernel: r.kernel.ok_or_else(|| Error::missing_field("kernel"))?,
- initrd: r.initrd.ok_or_else(|| Error::missing_field("initrd"))?,
- cmdline: r.cmdline.ok_or_else(|| Error::missing_field("cmdline"))?,
- id: r.id,
- disk: r.disk,
- })
- } else if r.id.starts_with("firmware-disk/") {
- Ok(Boot::FirmwareDisk {
- id: r.id,
- disk: r.disk,
- })
- } else {
- Err(Error::custom(format!("unknown boot protocol id '{}'", r.id)))
+ let mut annotations = BTreeMap::new();
+ annotations.insert(ORG_OPENCONTAINERS_IMAGE_TITLE.to_string(), title.clone());
+ annotations.insert(ANNOTATION_CHUNK_OFFSET.to_string(), offset.to_string());
+ annotations.insert(ANNOTATION_CHUNK_LENGTH.to_string(), length.to_string());
+
+ descriptors.push(OciDescriptor {
+ media_type: HULE_CHUNK_MEDIA_TYPE.to_string(),
+ digest,
+ size: compressed.len() as i64,
+ urls: None,
+ annotations: Some(annotations),
+ artifact_type: None,
+ });
+
+ offset += length;
+ if length < CHUNK_SIZE as u64 {
+ break;
}
}
+ if descriptors.is_empty() {
+ return Err(format!("{} is empty", path.display()));
+ }
+ Ok(descriptors)
}
-/// A guest access surface, discriminated by `type`.
-#[derive(Deserialize)]
-#[serde(tag = "type", rename_all = "lowercase")]
-enum Access {
- Ssh { port: u16 },
+fn make_client(reference: &Reference) -> Client {
+ let registry = reference.resolve_registry();
+ let host = registry.split(':').next().unwrap_or(registry);
+ let protocol = if host == "localhost" || host == "127.0.0.1" {
+ ClientProtocol::Http
+ } else {
+ ClientProtocol::Https
+ };
+ Client::new(ClientConfig {
+ protocol,
+ ..Default::default()
+ })
}
-#[derive(Deserialize)]
-struct Network {
- mode: String,
- address: String,
- gateway: String,
+fn parse_reference(s: &str) -> R<Reference> {
+ s.parse()
+ .map_err(|e| format!("invalid reference '{s}': {e}"))
}
-#[derive(Deserialize)]
-struct Manifest {
- schemaVersion: u8,
- kind: String,
+// ---- import / push / pull / run ---------------------------------------
+
+async fn cmd_import(path: &Path, reference: Option<&str>) -> R<()> {
+ ensure_store()?;
+ let config_path = path.join("config.json");
+ if !config_path.is_file() {
+ return Err(format!("{} does not contain config.json", path.display()));
+ }
+ let config_bytes = std::fs::read(&config_path).map_err(|e| e.to_string())?;
+ MachineImage::from_json(&config_bytes).map_err(|e| e.to_string())?;
+
+ let mut entries: Vec<_> = std::fs::read_dir(path)
+ .map_err(|e| e.to_string())?
+ .collect::<std::result::Result<Vec<_>, _>>()
+ .map_err(|e| e.to_string())?;
+ entries.sort_by_key(|e| e.file_name());
- system: System,
- machine: Machine,
+ let mut layers = Vec::new();
+ let mut sources: Vec<(String, PathBuf)> = Vec::new();
+ for entry in entries {
+ if !entry.file_type().map_err(|e| e.to_string())?.is_file() {
+ continue;
+ }
+ let name = entry.file_name().to_string_lossy().to_string();
+ if name == "config.json" {
+ continue;
+ }
+ let file_path = entry.path();
+ layers.extend(write_file_chunks(&file_path)?);
+ sources.push((name, file_path));
+ }
- boot: Vec<Boot>,
- disks: Vec<Disk>,
- access: Vec<Access>,
- network: Network,
+ let config_digest = write_blob(&config_bytes)?;
+ let config = OciDescriptor {
+ media_type: HULE_CONFIG_MEDIA_TYPE.to_string(),
+ digest: config_digest,
+ size: config_bytes.len() as i64,
+ urls: None,
+ annotations: None,
+ artifact_type: None,
+ };
+
+ let manifest = OciImageManifest {
+ schema_version: 2,
+ media_type: Some(OCI_IMAGE_MEDIA_TYPE.to_string()),
+ config,
+ layers,
+ subject: None,
+ artifact_type: None,
+ annotations: None,
+ };
+ let manifest_bytes = serde_json::to_vec(&manifest).map_err(|e| e.to_string())?;
+ let manifest_digest = write_blob(&manifest_bytes)?;
+
+ let materialized = materialized_dir(&manifest_digest);
+ std::fs::create_dir_all(&materialized).map_err(|e| e.to_string())?;
+ for (title, source) in &sources {
+ let dest = materialized.join(title);
+ if !dest.exists() {
+ hardlink_or_copy(source, &dest)?;
+ }
+ }
+
+ if let Some(reference) = reference {
+ let mut index = read_index()?;
+ index_set(
+ &mut index,
+ reference,
+ &manifest_digest,
+ manifest_bytes.len() as u64,
+ );
+ write_index(&index)?;
+ eprintln!(
+ "hule: imported {} as {reference} ({manifest_digest})",
+ path.display()
+ );
+ } else {
+ eprintln!(
+ "hule: imported {} ({manifest_digest}, untagged)",
+ path.display()
+ );
+ }
+ Ok(())
}
-fn die(msg: impl AsRef<str>) -> ! {
- eprintln!("hule: {}", msg.as_ref());
- exit(1);
+async fn cmd_push(reference_str: &str) -> R<()> {
+ ensure_store()?;
+ let index = read_index()?;
+ let manifest_digest = index_lookup(&index, reference_str)
+ .ok_or_else(|| format!("no local image tagged '{reference_str}'"))?;
+ let manifest_bytes = read_blob(&manifest_digest)?;
+ let manifest: OciImageManifest =
+ serde_json::from_slice(&manifest_bytes).map_err(|e| e.to_string())?;
+
+ let reference = parse_reference(reference_str)?;
+ let client = make_client(&reference);
+ let auth = RegistryAuth::Anonymous;
+
+ let mut layers = Vec::new();
+ for descriptor in &manifest.layers {
+ let data = read_blob(&descriptor.digest)?;
+ layers.push(ImageLayer::new(
+ data,
+ descriptor.media_type.clone(),
+ descriptor.annotations.clone(),
+ ));
+ }
+ let config_bytes = read_blob(&manifest.config.digest)?;
+ let config = Config::new(
+ config_bytes,
+ manifest.config.media_type.clone(),
+ manifest.config.annotations.clone(),
+ );
+
+ client
+ .push(&reference, &layers, config, &auth, Some(manifest))
+ .await
+ .map_err(|e| e.to_string())?;
+
+ eprintln!("hule: pushed {reference_str}");
+ Ok(())
}
-fn main() {
- let args: Vec<String> = std::env::args().collect();
- // hule boot <image-dir> [port]
- if args.len() < 3 || args[1] != "boot" {
- eprintln!("usage: {} boot <image-dir> [port]", args[0]);
- exit(2);
+async fn cmd_pull(reference_str: &str) -> R<()> {
+ ensure_store()?;
+ let reference = parse_reference(reference_str)?;
+ let client = make_client(&reference);
+ let auth = RegistryAuth::Anonymous;
+
+ let image_data = client
+ .pull(&reference, &auth, vec![HULE_CHUNK_MEDIA_TYPE])
+ .await
+ .map_err(|e| e.to_string())?;
+ let manifest = image_data
+ .manifest
+ .ok_or("registry returned no image manifest")?;
+
+ // `client.pull()` fetches layers via `buffer_unordered`, so `image_data.layers` is in
+ // completion order, NOT `manifest.layers` order -- do not zip the two. Each `ImageLayer`
+ // already carries its own annotations, so it's self-describing without cross-referencing.
+ for layer in &image_data.layers {
+ write_blob(&layer.data)?;
}
- let dir = PathBuf::from(&args[2]);
- let port: u16 = args.get(3).map_or(8022, |s| {
- s.parse().unwrap_or_else(|_| die(format!("invalid port '{s}'")))
- });
+ write_blob(&image_data.config.data)?;
+
+ let manifest_bytes = serde_json::to_vec(&manifest).map_err(|e| e.to_string())?;
+ let manifest_digest = write_blob(&manifest_bytes)?;
+
+ let materialized = materialized_dir(&manifest_digest);
+ std::fs::create_dir_all(&materialized).map_err(|e| e.to_string())?;
+
+ let mut by_title: BTreeMap<String, Vec<(u64, &[u8])>> = BTreeMap::new();
+ for layer in &image_data.layers {
+ let annotations = layer
+ .annotations
+ .as_ref()
+ .ok_or("chunk layer missing annotations")?;
+ let title = annotations
+ .get(ORG_OPENCONTAINERS_IMAGE_TITLE)
+ .ok_or("chunk layer missing title annotation")?;
+ let offset: u64 = annotations
+ .get(ANNOTATION_CHUNK_OFFSET)
+ .ok_or("chunk layer missing offset annotation")?
+ .parse()
+ .map_err(|_| "invalid chunk offset annotation".to_string())?;
+ by_title
+ .entry(title.clone())
+ .or_default()
+ .push((offset, &layer.data[..]));
+ }
+
+ for (title, mut chunks) in by_title {
+ chunks.sort_by_key(|(offset, _)| *offset);
+ let file = std::fs::File::create(materialized.join(&title)).map_err(|e| e.to_string())?;
+ for (offset, compressed) in chunks {
+ let decompressed = zstd::stream::decode_all(compressed).map_err(|e| e.to_string())?;
+ write_at(&file, offset, &decompressed)?;
+ }
+ }
+
+ let mut index = read_index()?;
+ index_set(
+ &mut index,
+ reference_str,
+ &manifest_digest,
+ manifest_bytes.len() as u64,
+ );
+ write_index(&index)?;
+
+ eprintln!("hule: pulled {reference_str} ({manifest_digest})");
+ Ok(())
+}
+
+async fn cmd_run(reference_str: &str, port: u16) -> R<()> {
+ ensure_store()?;
+ let mut index = read_index()?;
+ let manifest_digest = match index_lookup(&index, reference_str) {
+ Some(d) => d,
+ None => {
+ eprintln!("hule: {reference_str} not found locally, pulling...");
+ cmd_pull(reference_str).await?;
+ index = read_index()?;
+ index_lookup(&index, reference_str)
+ .ok_or_else(|| format!("pull of '{reference_str}' did not produce a local tag"))?
+ }
+ };
+
+ let manifest_bytes = read_blob(&manifest_digest)?;
+ let manifest: OciImageManifest =
+ serde_json::from_slice(&manifest_bytes).map_err(|e| e.to_string())?;
+ let config_bytes = read_blob(&manifest.config.digest)?;
- let manifest_path = dir.join("image.hmm");
+ let scratch = std::env::temp_dir().join(format!("hule-run-{}", std::process::id()));
+ std::fs::create_dir_all(&scratch).map_err(|e| e.to_string())?;
+ std::fs::write(scratch.join("config.json"), &config_bytes).map_err(|e| e.to_string())?;
+
+ let materialized = materialized_dir(&manifest_digest);
+ let mut titles: BTreeSet<String> = BTreeSet::new();
+ for descriptor in &manifest.layers {
+ if let Some(title) = descriptor
+ .annotations
+ .as_ref()
+ .and_then(|a| a.get(ORG_OPENCONTAINERS_IMAGE_TITLE))
+ {
+ titles.insert(title.clone());
+ }
+ }
+ for title in titles {
+ let dest = scratch.join(&title);
+ if !dest.exists() {
+ hardlink_or_copy(&materialized.join(&title), &dest)?;
+ }
+ }
+
+ launch_qemu(&scratch, port);
+}
+
+// ---- qemu launch (unchanged logic, factored out of the old boot-only main) --
+
+/// Reads `dir/config.json` and execs qemu according to its boot protocol.
+/// Never returns.
+fn launch_qemu(dir: &Path, port: u16) -> ! {
+ let manifest_path = dir.join("config.json");
let data = std::fs::read_to_string(&manifest_path)
.unwrap_or_else(|e| die(format!("cannot read {}: {e}", manifest_path.display())));
- let m: Manifest = serde_json::from_str(&data)
- .unwrap_or_else(|e| die(format!("invalid manifest {}: {e}", manifest_path.display())));
+ let m = MachineImage::from_json(data.as_bytes()).unwrap_or_else(|e| {
+ die(format!(
+ "invalid machine configuration {}: {e}",
+ manifest_path.display()
+ ))
+ });
// Negotiation: qemu handles every protocol we emit. Prefer direct-kernel
// (skips firmware+bootloader), else firmware-disk. Manifest order is not
// significant.
let proto = m
+ .machine
.boot
.iter()
- .find(|b| matches!(b, Boot::Linux { .. }))
- .or_else(|| m.boot.iter().find(|b| matches!(b, Boot::FirmwareDisk { .. })))
+ .find(|b| matches!(b, Boot::Linux(_)))
+ .or_else(|| {
+ m.machine
+ .boot
+ .iter()
+ .find(|b| matches!(b, Boot::Bios(_)) && b.protocol() == BOOT_FIRMWARE_DISK_BIOS)
+ })
.unwrap_or_else(|| die("no control-supported boot protocol in manifest"));
let disk = m
.disks
.iter()
.find(|d| d.id == proto.disk())
- .unwrap_or_else(|| die(format!("boot entry references unknown disk '{}'", proto.disk())));
+ .unwrap_or_else(|| {
+ die(format!(
+ "boot entry references unknown disk '{}'",
+ proto.disk()
+ ))
+ });
let disk_path = dir.join(&disk.path);
let ssh_port = m
+ .machine
.access
.iter()
.find_map(|a| match a {
- Access::Ssh { port } => Some(*port),
+ Access::Ssh { port, .. } => Some(*port),
+ Access::Unknown(_) => None,
})
.unwrap_or(22);
- let qa = qemu_arch(&m.machine.arch);
+ let qa = qemu_arch(&m.system.architecture);
let mem_mib = m.machine.ram.default / (1024 * 1024);
let mut cmd = Command::new(format!("qemu-system-{qa}"));
- cmd.args(cpu_opts(&m.machine.arch));
+ cmd.args(cpu_opts(&m.system.architecture));
cmd.args([
"-pidfile".into(),
format!("/tmp/qemu-{port}.id"),
@@ -214,39 +565,42 @@ fn main() {
// Ephemeral: snapshot=on discards guest writes.
match proto {
- Boot::Linux {
- kernel,
- initrd,
- cmdline,
- ..
- } => {
+ Boot::Linux(boot) => {
// Direct kernel boot: sidecar kernel/initrd + canonical cmdline.
cmd.args([
"-drive".into(),
- format!("file={},media=disk,snapshot=on,id=root,if=none", disk_path.display()),
+ format!(
+ "file={},media=disk,snapshot=on,id=root,if=none",
+ disk_path.display()
+ ),
"-device".into(),
"virtio-blk-pci,drive=root".into(),
"-kernel".into(),
- dir.join(&kernel.path).display().to_string(),
+ dir.join(&boot.kernel.path).display().to_string(),
"-initrd".into(),
- dir.join(&initrd.path).display().to_string(),
+ dir.join(&boot.initrd.path).display().to_string(),
"-append".into(),
- cmdline.clone(),
+ boot.cmdline.clone(),
]);
}
- Boot::FirmwareDisk { .. } => {
+ Boot::Bios(_) => {
// Self-bootable disk, qemu's firmware boots it.
cmd.args([
"-drive".into(),
- format!("file={},media=disk,snapshot=on,if=virtio", disk_path.display()),
+ format!(
+ "file={},media=disk,snapshot=on,if=virtio",
+ disk_path.display()
+ ),
]);
}
+ Boot::Uefi(_) => die("UEFI boot is not supported by the QEMU backend yet"),
+ Boot::Unknown(_) => die("unreachable: negotiation never selects an unrecognized boot protocol"),
}
eprintln!(
"hule: booting {} via {} (ssh: localhost:{port})",
dir.display(),
- proto.id()
+ proto.protocol()
);
let status = cmd
.status()
@@ -255,34 +609,101 @@ fn main() {
}
/// Hule machine arch -> qemu-system-<arch> suffix.
-fn qemu_arch(arch: &str) -> &'static str {
+fn qemu_arch(arch: &Architecture) -> &'static str {
match arch {
- "amd64" => "x86_64",
- "arm64" => "aarch64",
- "ppc64el" => "ppc64le",
- other => die(format!("unsupported arch '{other}'")),
+ Architecture::Amd64 => "x86_64",
+ Architecture::Arm64 => "aarch64",
+ Architecture::Loong64 => "loongarch64",
+ Architecture::Ppc64le => "ppc64le",
+ Architecture::Riscv64 => "riscv64",
+ Architecture::S390x => "s390x",
}
}
/// Machine/accel flags for an arch (KVM when the host matches, else TCG).
-fn cpu_opts(arch: &str) -> Vec<String> {
- let qa = qemu_arch(arch);
- let kvm = std::env::consts::ARCH == qa && Path::new("/dev/kvm").exists();
+fn cpu_opts(arch: &Architecture) -> Vec<String> {
+ let host_matches = match arch {
+ Architecture::Amd64 => std::env::consts::ARCH == "x86_64",
+ Architecture::Arm64 => std::env::consts::ARCH == "aarch64",
+ Architecture::Loong64 => std::env::consts::ARCH == "loongarch64",
+ Architecture::Ppc64le => {
+ std::env::consts::ARCH == "powerpc64" && cfg!(target_endian = "little")
+ }
+ Architecture::Riscv64 => std::env::consts::ARCH == "riscv64",
+ Architecture::S390x => std::env::consts::ARCH == "s390x",
+ };
+ let kvm = host_matches && Path::new("/dev/kvm").exists();
let mut v: Vec<String> = Vec::new();
- match qa {
- "aarch64" => v.extend(["-M".into(), "virt".into()]),
- "ppc64le" => v.extend(["-machine".into(), "pseries".into()]),
+ match arch {
+ Architecture::Arm64 | Architecture::Riscv64 => v.extend(["-M".into(), "virt".into()]),
+ Architecture::Ppc64le => v.extend(["-machine".into(), "pseries".into()]),
_ => {}
}
if kvm {
v.extend(["-cpu".into(), "host".into(), "-enable-kvm".into()]);
} else {
- match qa {
- "aarch64" => v.extend(["-cpu".into(), "cortex-a53".into()]),
- "x86_64" => v.extend(["-cpu".into(), "qemu64".into()]),
- "ppc64le" => v.extend(["-cpu".into(), "power9".into()]),
- _ => {}
+ match arch {
+ Architecture::Amd64 => v.extend(["-cpu".into(), "qemu64".into()]),
+ Architecture::Arm64 => v.extend(["-cpu".into(), "cortex-a53".into()]),
+ Architecture::Loong64 => v.extend(["-cpu".into(), "la464".into()]),
+ Architecture::Ppc64le => v.extend(["-cpu".into(), "power9".into()]),
+ Architecture::Riscv64 => v.extend(["-cpu".into(), "rv64".into()]),
+ Architecture::S390x => v.extend(["-cpu".into(), "max".into()]),
}
}
v
}
+
+// ---- CLI dispatch -------------------------------------------------------
+
+#[tokio::main]
+async fn main() {
+ let args: Vec<String> = std::env::args().collect();
+ let usage = |prog: &str| -> ! {
+ eprintln!("usage:");
+ eprintln!(" {prog} import <image-dir> [ref]");
+ eprintln!(" {prog} push <ref>");
+ eprintln!(" {prog} pull <ref>");
+ eprintln!(" {prog} run <ref> [port]");
+ exit(2);
+ };
+
+ if args.len() < 2 {
+ usage(&args[0]);
+ }
+
+ match args[1].as_str() {
+ "import" => {
+ if args.len() < 3 {
+ usage(&args[0]);
+ }
+ let reference = args.get(3).map(String::as_str);
+ cmd_import(Path::new(&args[2]), reference)
+ .await
+ .unwrap_or_else(|e| die(e));
+ }
+ "push" => {
+ if args.len() < 3 {
+ usage(&args[0]);
+ }
+ cmd_push(&args[2]).await.unwrap_or_else(|e| die(e));
+ }
+ "pull" => {
+ if args.len() < 3 {
+ usage(&args[0]);
+ }
+ cmd_pull(&args[2]).await.unwrap_or_else(|e| die(e));
+ }
+ "run" => {
+ if args.len() < 3 {
+ usage(&args[0]);
+ }
+ let port: u16 = args.get(3).map_or(8022, |s| {
+ s.parse()
+ .unwrap_or_else(|_| die(format!("invalid port '{s}'")))
+ });
+ cmd_run(&args[2], port).await.unwrap_or_else(|e| die(e));
+ }
+ _ => usage(&args[0]),
+ }
+}
diff --git a/images/alpine/genimg b/images/alpine/genimg
index ef2aaf1..3bf3e2d 100755
--- a/images/alpine/genimg
+++ b/images/alpine/genimg
@@ -7,7 +7,7 @@ self=$(dirname "$(readlink -f "$0")")
cd "$self"
version=${1:-}
-arch="${2:-x86_64}"
+arch="${2:-amd64}"
linux="${linux:-linux-lts}"
case $version in
@@ -23,6 +23,15 @@ edge)
;;
esac
+case $arch in
+amd64) apk_arch=x86_64 ;;
+arm64) apk_arch=aarch64 ;;
+*)
+ echo "unsupported architecture $arch" >&2
+ exit 1
+ ;;
+esac
+
out="$version/$arch"
cleanup() {
@@ -52,7 +61,7 @@ for i in $(seq 1 5); do
done
trap cleanup EXIT
-if [ "$arch" == "x86_64" ] || [ "$arch" == "i686" ]; then
+if [ "$arch" = "amd64" ]; then
dd if=/usr/share/syslinux/mbr.bin of=/dev/nbd0 bs=1 count=440
fi
sfdisk --no-reread /dev/nbd0 <<EOF
@@ -76,7 +85,7 @@ apk add -U \
-X http://dl-cdn.alpinelinux.org/alpine/$release/main/ \
-X http://dl-cdn.alpinelinux.org/alpine/$release/community/ \
--allow-untrusted \
- --arch="$arch" \
+ --arch="$apk_arch" \
--root=/mnt \
--initdb \
acct alpine-base alpine-conf alpine-sdk linux-firmware-none $linux \
@@ -190,33 +199,38 @@ sync
cleanup
trap : EXIT
-# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b ---------------
-case "$arch" in
-x86_64) march=amd64 ;;
-aarch64) march=arm64 ;;
-*) march=$arch ;;
-esac
+# --- Hule machine configuration (`config.json`) ------------------------------
disk_sha=$(sha256sum "$out/root.hmi" | cut -d' ' -f1)
disk_bytes=$(stat -c%s "$out/root.hmi")
virtual_bytes=$((size_gib * 1024 * 1024 * 1024))
mem_min=$((256 * 1024 * 1024))
mem_def=$((1024 * 1024 * 1024))
-cat >"$out/image.hmm" <<EOF
+cat >"$out/config.json" <<EOF
{
"schemaVersion": 1,
"kind": "MachineImage",
"system": {
- "family": "linux",
- "distro": "alpine",
- "release": "$version"
+ "os": "linux",
+ "name": "alpine",
+ "version": "$version",
+ "architecture": "$arch"
},
"machine": {
- "arch": "$march",
"cpu": { "minimum": 1, "default": 2 },
- "ram": { "minimum": $mem_min, "default": $mem_def }
+ "ram": { "minimum": $mem_min, "default": $mem_def },
+ "boot": [
+ {
+ "protocol": "firmware-disk/bios",
+ "disk": "root"
+ }
+ ],
+ "access": [
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ ],
+ "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
"disks": [
@@ -224,23 +238,10 @@ cat >"$out/image.hmm" <<EOF
"id": "root",
"format": "qcow2",
"path": "root.hmi",
- "checksum": "sha256:$disk_sha",
- "virtualSize": $virtual_bytes,
- "physicalSize": $disk_bytes
+ "digest": "sha256:$disk_sha",
+ "virtSize": $virtual_bytes,
+ "diskSize": $disk_bytes
}
- ],
-
- "boot": [
- {
- "id": "firmware-disk/bios",
- "disk": "root"
- }
- ],
-
- "access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
- ],
-
- "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
+ ]
}
EOF
diff --git a/images/debian/genimg b/images/debian/genimg
index eafe118..116fab2 100755
--- a/images/debian/genimg
+++ b/images/debian/genimg
@@ -19,16 +19,19 @@ esac
case $arch in
amd64)
+ darch=amd64
iface=ens3
qarch=x86_64
kpkg=linux-image-amd64
;;
arm64)
+ darch=arm64
iface=enp0s1
qarch=aarch64
kpkg=linux-image-arm64
;;
-ppc64el)
+ppc64le)
+ darch=ppc64el
iface=enp0s0
qarch=ppc64le
kpkg=linux-image-powerpc64le
@@ -88,9 +91,9 @@ mkdir /mnt/boot
mount /dev/nbd0p1 /mnt/boot
if [ "$arch" = "amd64" ]; then
- debootstrap --include=gnupg2 --arch=$arch $release /mnt
+ debootstrap --include=gnupg2 --arch=$darch $release /mnt
else
- ./qemu-debootstrap --include=gnupg2 --arch=$arch $release /mnt
+ ./qemu-debootstrap --include=gnupg2 --arch=$darch $release /mnt
fi
mount --bind /dev /mnt/dev
@@ -190,7 +193,7 @@ arm64)
cp /mnt/boot/vmlinuz-* "$out/vmlinuz"
cp /mnt/boot/initrd.img-* "$out/initrd"
;;
-ppc64el)
+ppc64le)
cp /mnt/boot/vmlinux-* "$out/vmlinux"
cp /mnt/boot/initrd.img-* "$out/initrd"
;;
@@ -202,7 +205,7 @@ sync
cleanup
trap : EXIT
-# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b ---------------
+# --- Hule machine configuration (`config.json`) ------------------------------
disk_sha=$(sha256sum "$out/root.hmi" | cut -d' ' -f1)
disk_bytes=$(stat -c%s "$out/root.hmi")
virtual_bytes=$((size_gib * 1024 * 1024 * 1024))
@@ -215,7 +218,7 @@ amd64)
protocols=$(
cat <<JSON
{
- "id": "firmware-disk/bios",
+ "protocol": "firmware-disk/bios",
"disk": "root"
}
JSON
@@ -227,25 +230,25 @@ arm64)
protocols=$(
cat <<JSON
{
- "id": "linux/arm64-image",
+ "protocol": "linux/direct",
"disk": "root",
- "kernel": { "path": "vmlinuz", "checksum": "sha256:$kernel_sha" },
- "initrd": { "path": "initrd", "checksum": "sha256:$initrd_sha" },
+ "kernel": { "path": "vmlinuz", "digest": "sha256:$kernel_sha" },
+ "initrd": { "path": "initrd", "digest": "sha256:$initrd_sha" },
"cmdline": "$cmdline"
}
JSON
)
;;
-ppc64el)
+ppc64le)
kernel_sha=$(sha256sum "$out/vmlinux" | cut -d' ' -f1)
initrd_sha=$(sha256sum "$out/initrd" | cut -d' ' -f1)
protocols=$(
cat <<JSON
{
- "id": "linux/ppc64le-elf",
+ "protocol": "linux/direct",
"disk": "root",
- "kernel": { "path": "vmlinux", "checksum": "sha256:$kernel_sha" },
- "initrd": { "path": "initrd", "checksum": "sha256:$initrd_sha" },
+ "kernel": { "path": "vmlinux", "digest": "sha256:$kernel_sha" },
+ "initrd": { "path": "initrd", "digest": "sha256:$initrd_sha" },
"cmdline": "$cmdline"
}
JSON
@@ -253,21 +256,28 @@ JSON
;;
esac
-cat >"$out/image.hmm" <<EOF
+cat >"$out/config.json" <<EOF
{
"schemaVersion": 1,
"kind": "MachineImage",
"system": {
- "family": "linux",
- "distro": "debian",
- "release": "$release"
+ "os": "linux",
+ "name": "debian",
+ "version": "$release",
+ "architecture": "$arch"
},
"machine": {
- "arch": "$arch",
"cpu": { "minimum": 1, "default": 2 },
- "ram": { "minimum": $mem_min, "default": $mem_def }
+ "ram": { "minimum": $mem_min, "default": $mem_def },
+ "boot": [
+$protocols
+ ],
+ "access": [
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ ],
+ "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
"disks": [
@@ -275,20 +285,10 @@ cat >"$out/image.hmm" <<EOF
"id": "root",
"format": "qcow2",
"path": "root.hmi",
- "checksum": "sha256:$disk_sha",
- "virtualSize": $virtual_bytes,
- "physicalSize": $disk_bytes
+ "digest": "sha256:$disk_sha",
+ "virtSize": $virtual_bytes,
+ "diskSize": $disk_bytes
}
- ],
-
- "boot": [
-$protocols
- ],
-
- "access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
- ],
-
- "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
+ ]
}
EOF
diff --git a/images/fedora/genimg b/images/fedora/genimg
index 74f1e33..35c2695 100755
--- a/images/fedora/genimg
+++ b/images/fedora/genimg
@@ -10,7 +10,7 @@ self=$(dirname "$(readlink -f "$0")")
cd "$self"
release=${1:-}
-arch=${2:-x86_64}
+arch=${2:-amd64}
case $release in
42 | 43 | 44) ;;
@@ -21,7 +21,8 @@ case $release in
esac
case $arch in
-x86_64)
+amd64)
+ fedora_arch=x86_64
iface=ens3
;;
*)
@@ -148,7 +149,7 @@ run_root dnf clean all
cat >/mnt/etc/dracut.conf.d/virtio-blk.conf <<EOF
add_drivers="virtio-blk"
EOF
-kernel_version=$(ls /mnt/boot | grep "vmlinuz.*.$arch" | cut -d- -f2-)
+kernel_version=$(ls /mnt/boot | grep "vmlinuz.*.$fedora_arch" | cut -d- -f2-)
run_root dracut --force --kver "$kernel_version"
run_root grub2-install --target=i386-pc $NBD_DEVICE
@@ -181,34 +182,40 @@ sync
cleanup
trap : EXIT
-# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b ---------------
-# x86_64 only, self-booting via grub2 (BIOS). No boot.cmdline: grub owns it and
+# --- Hule machine configuration (`config.json`) ------------------------------
+# amd64 only, self-booting via grub2 (BIOS). No boot.cmdline: grub owns it and
# firmware-disk boot takes no cmdline from the VMM.
-case "$arch" in
-x86_64) march=amd64 ;;
-*) march=$arch ;;
-esac
disk_sha=$(sha256sum "$out/root.hmi" | cut -d' ' -f1)
disk_bytes=$(stat -c%s "$out/root.hmi")
virtual_bytes=$((size_gib * 1024 * 1024 * 1024))
mem_min=$((256 * 1024 * 1024))
mem_def=$((1024 * 1024 * 1024))
-cat >"$out/image.hmm" <<EOF
+cat >"$out/config.json" <<EOF
{
"schemaVersion": 1,
"kind": "MachineImage",
"system": {
- "family": "linux",
- "distro": "fedora",
- "release": "$release"
+ "os": "linux",
+ "name": "fedora",
+ "version": "$release",
+ "architecture": "$arch"
},
"machine": {
- "arch": "$march",
"cpu": { "minimum": 1, "default": 2 },
- "ram": { "minimum": $mem_min, "default": $mem_def }
+ "ram": { "minimum": $mem_min, "default": $mem_def },
+ "boot": [
+ {
+ "protocol": "firmware-disk/bios",
+ "disk": "root"
+ }
+ ],
+ "access": [
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ ],
+ "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
"disks": [
@@ -216,23 +223,10 @@ cat >"$out/image.hmm" <<EOF
"id": "root",
"format": "qcow2",
"path": "root.hmi",
- "checksum": "sha256:$disk_sha",
- "virtualSize": $virtual_bytes,
- "physicalSize": $disk_bytes
- }
- ],
-
- "boot": [
- {
- "id": "firmware-disk/bios",
- "disk": "root"
+ "digest": "sha256:$disk_sha",
+ "virtSize": $virtual_bytes,
+ "diskSize": $disk_bytes
}
- ],
-
- "access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
- ],
-
- "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
+ ]
}
EOF
diff --git a/images/freebsd/genimg b/images/freebsd/genimg
index 8b23ed0..e360436 100755
--- a/images/freebsd/genimg
+++ b/images/freebsd/genimg
@@ -198,7 +198,7 @@ mkimg -s gpt \
cleanup
trap : EXIT
-# --- Hule Machine Manifest (.hmm) -- docs/boot-protocol.md 10b ---------------
+# --- Hule machine configuration (`config.json`) ------------------------------
# GPT + freebsd-boot (gptboot/pmbr), booted by the VMM's BIOS firmware. No
# boot.cmdline (the FreeBSD loader owns boot; firmware-disk takes none from the
# VMM). FreeBSD host tools: sha256(1) and stat -f, not GNU coreutils.
@@ -208,21 +208,30 @@ virtual_bytes=$((size_gib * 1024 * 1024 * 1024))
mem_min=$((256 * 1024 * 1024))
mem_def=$((1024 * 1024 * 1024))
-cat >"$out/image.hmm" <<HMM
+cat >"$out/config.json" <<CONFIG
{
"schemaVersion": 1,
"kind": "MachineImage",
"system": {
- "family": "freebsd",
- "distro": "freebsd",
- "release": "$version"
+ "os": "freebsd",
+ "version": "$version",
+ "architecture": "$arch"
},
"machine": {
- "arch": "$arch",
"cpu": { "minimum": 1, "default": 2 },
- "ram": { "minimum": $mem_min, "default": $mem_def }
+ "ram": { "minimum": $mem_min, "default": $mem_def },
+ "boot": [
+ {
+ "protocol": "firmware-disk/bios",
+ "disk": "root"
+ }
+ ],
+ "access": [
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ ],
+ "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
},
"disks": [
@@ -230,23 +239,10 @@ cat >"$out/image.hmm" <<HMM
"id": "root",
"format": "qcow2",
"path": "root.hmi",
- "checksum": "sha256:$disk_sha",
- "virtualSize": $virtual_bytes,
- "physicalSize": $disk_bytes
+ "digest": "sha256:$disk_sha",
+ "virtSize": $virtual_bytes,
+ "diskSize": $disk_bytes
}
- ],
-
- "boot": [
- {
- "id": "firmware-disk/bios",
- "disk": "root"
- }
- ],
-
- "access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
- ],
-
- "network": { "mode": "static", "address": "10.0.2.15/24", "gateway": "10.0.2.2" }
+ ]
}
-HMM
+CONFIG
diff --git a/images/ubuntu/genimg b/images/ubuntu/genimg
index c503b05..114438c 100755
--- a/images/ubuntu/genimg
+++ b/images/ubuntu/genimg
@@ -209,7 +209,7 @@ sync
cleanup
trap : EXIT
-# --- Hule Machine Manifest (.hmm) -------------------------------------------
+# --- Hule machine configuration (`config.json`) ------------------------------
# JSON description of the image: the .hmi disk(s), the boot protocols this image
# can honestly be booted with, and the surfaces it exposes. The declaration is a
# promise (see docs/boot-protocol.md); only list what this build produces.
@@ -228,7 +228,7 @@ amd64)
protocols=$(
cat <<JSON
{
- "id": "firmware-disk/bios",
+ "protocol": "firmware-disk/bios",
"disk": "root"
}
JSON
@@ -242,10 +242,10 @@ JSON
protocols=$(
cat <<JSON
{
- "id": "linux/arm64-image",
+ "protocol": "linux/direct",
"disk": "root",
- "kernel": { "path": "vmlinuz", "checksum": "sha256:$kernel_sha" },
- "initrd": { "path": "initrd", "checksum": "sha256:$initrd_sha" },
+ "kernel": { "path": "vmlinuz", "digest": "sha256:$kernel_sha" },
+ "initrd": { "path": "initrd", "digest": "sha256:$initrd_sha" },
"cmdline": "$cmdline"
}
JSON
@@ -253,21 +253,32 @@ JSON
;;
esac
-cat >"$out/image.hmm" <<EOF
+cat >"$out/config.json" <<EOF
{
"schemaVersion": 1,
"kind": "MachineImage",
"system": {
- "family": "linux",
- "distro": "ubuntu",
- "release": "$release"
+ "os": "linux",
+ "name": "ubuntu",
+ "version": "$release",
+ "architecture": "$arch"
},
"machine": {
- "arch": "$arch",
"cpu": { "minimum": 1, "default": 2 },
- "ram": { "minimum": $mem_min, "default": $mem_def }
+ "ram": { "minimum": $mem_min, "default": $mem_def },
+ "boot": [
+$protocols
+ ],
+ "access": [
+ { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
+ ],
+ "network": {
+ "mode": "$net_mode",
+ "address": "$net_address",
+ "gateway": "$net_gateway"
+ }
},
"disks": [
@@ -275,24 +286,10 @@ cat >"$out/image.hmm" <<EOF
"id": "root",
"format": "qcow2",
"path": "root.hmi",
- "checksum": "sha256:$disk_sha",
- "virtualSize": $virtual_bytes,
- "physicalSize": $disk_bytes
+ "digest": "sha256:$disk_sha",
+ "virtSize": $virtual_bytes,
+ "diskSize": $disk_bytes
}
- ],
-
- "boot": [
-$protocols
- ],
-
- "access": [
- { "type": "ssh", "port": 22, "user": "build", "auth": "empty-password" }
- ],
-
- "network": {
- "mode": "$net_mode",
- "address": "$net_address",
- "gateway": "$net_gateway"
- }
+ ]
}
EOF