diff --git a/Cargo.lock b/Cargo.lock index 4dc9c7fa1..24f1b0d51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3843,6 +3843,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "hyperion-platform" +version = "0.1.0" +dependencies = [ + "libc", +] + [[package]] name = "hyperion-proto" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index a0e018cdc..25a096896 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ 'crates/hyperion-packet-macros', 'crates/hyperion-palette', 'crates/hyperion-permission', + 'crates/hyperion-platform', 'crates/hyperion-proto', 'crates/hyperion-proxy', 'crates/hyperion-proxy-module', @@ -195,6 +196,9 @@ path = 'crates/hyperion-palette' [workspace.dependencies.hyperion-permission] path = 'crates/hyperion-permission' +[workspace.dependencies.hyperion-platform] +path = 'crates/hyperion-platform' + [workspace.dependencies.hyperion-proto] path = 'crates/hyperion-proto' diff --git a/bare-metal/Cargo.lock b/bare-metal/Cargo.lock new file mode 100644 index 000000000..27c459ad1 --- /dev/null +++ b/bare-metal/Cargo.lock @@ -0,0 +1,1302 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.5.0", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bevy_ecs" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7709fbd22f81fb681534cd913c41e1cd18b17143368743281195d7f024b61aea" +dependencies = [ + "async-channel", + "bevy_ecs_macros", + "bevy_ptr", + "bevy_tasks", + "bevy_utils", + "downcast-rs", + "event-listener", + "fixedbitset", + "rustc-hash", + "serde", + "thiserror", + "thread_local", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8843aa489f159f25cdcd9fee75cd7d221a7098a71eaa72cb2d6b40ac4e3f1ba" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e566640c6b6dced73d2006c764c2cffebe1a82be4809486c4a5d7b4b50efed4d" +dependencies = [ + "proc-macro2", + "quote", + "rustc-hash", + "syn 2.0.119", + "toml_edit", +] + +[[package]] +name = "bevy_ptr" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77ec20c8fafcdc196508ef5ccb4f0400a8d193cb61f7b14a36ed9a25ad423cf" + +[[package]] +name = "bevy_tasks" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fefa7fe0da8923525f7500e274f1bd60dbd79918a25cf7d0dfa0a6ba15c1cf" +dependencies = [ + "async-channel", + "async-executor", + "async-task", + "concurrent-queue", + "futures-lite 1.13.0", + "wasm-bindgen-futures", +] + +[[package]] +name = "bevy_utils" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7915222f4a08ccc782e08d10b751b42e5f9d786e697d0cb3fd09333cb7e8b6ea" +dependencies = [ + "ahash", + "bevy_utils_proc_macros", + "getrandom 0.2.17", + "hashbrown 0.14.5", + "instant", + "nonmax", + "petgraph", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "bevy_utils_proc_macros" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aafecc952b6b8eb1a93c12590bd867d25df2f4ae1033a01dfdfc3c35ebccfff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bitfield-struct" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2869c63ccf4f8bf0d485070b880e60e097fb7aeea80ee82a0a94a957e372a0b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.5.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[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.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "glam" +version = "0.29.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit" +version = "0.13.2" +source = "git+https://github.com/hermit-os/hermit-rs?tag=hermit-0.13.2#0e68850e7b848656f8704b6c1fdc3a09685cb4de" +dependencies = [ + "cc", + "home", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "hyperion-platform" +version = "0.1.0" +dependencies = [ + "libc", +] + +[[package]] +name = "hyperion-unikernel" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "hermit", + "hyperion-platform", + "valence_bytes", + "valence_protocol", +] + +[[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 = "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 = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[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 = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[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.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[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 = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[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_derive", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valence_build_utils" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "anyhow", + "proc-macro2", + "syn 2.0.119", +] + +[[package]] +name = "valence_bytes" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "bytes", +] + +[[package]] +name = "valence_generated" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "anyhow", + "heck", + "proc-macro2", + "quote", + "serde", + "serde_json", + "uuid", + "valence_build_utils", + "valence_ident", + "valence_math", +] + +[[package]] +name = "valence_ident" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "bytes", + "serde", + "thiserror", + "valence_bytes", + "valence_ident_macros", +] + +[[package]] +name = "valence_ident_macros" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "valence_math" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "glam", +] + +[[package]] +name = "valence_nbt" +version = "0.8.0" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "byteorder", + "cesu8", + "uuid", +] + +[[package]] +name = "valence_protocol" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "anyhow", + "base64", + "bevy_ecs", + "bitfield-struct", + "byteorder", + "bytes", + "derive_more", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "uuid", + "valence_bytes", + "valence_generated", + "valence_ident", + "valence_math", + "valence_nbt", + "valence_protocol_macros", + "valence_text", +] + +[[package]] +name = "valence_protocol_macros" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "valence_text" +version = "0.2.0-alpha.1+mc.1.20.1" +source = "git+https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb6669b64c5dc2366eb3d074b293def046" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "uuid", + "valence_ident", + "valence_nbt", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[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 2.0.119", + "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 = "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 = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[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 = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[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 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[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 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/bare-metal/Cargo.toml b/bare-metal/Cargo.toml new file mode 100644 index 000000000..6d5806c8e --- /dev/null +++ b/bare-metal/Cargo.toml @@ -0,0 +1,27 @@ +# A workspace of its own, on purpose. +# +# The unikernel binary needs the Hermit kernel as a git dependency and a +# different nightly from the one the main workspace pins, and neither belongs in +# the lockfile every normal build resolves. Keeping it separate means a plain +# `cargo build` at the repo root neither sees nor fetches any of it. + +[workspace] +members = ["hyperion-unikernel"] +resolver = "2" + +[workspace.package] +edition = "2024" +license = "Apache-2.0" +publish = false +repository = "https://github.com/andrewgazelka/hyperion" +version = "0.1.0" + +[workspace.lints.clippy] +complexity = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } + +missing_errors_doc = "allow" diff --git a/bare-metal/hyperion-unikernel/Cargo.toml b/bare-metal/hyperion-unikernel/Cargo.toml new file mode 100644 index 000000000..8312cc4c0 --- /dev/null +++ b/bare-metal/hyperion-unikernel/Cargo.toml @@ -0,0 +1,32 @@ +[package] +edition.workspace = true +license.workspace = true +name = "hyperion-unikernel" +publish.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +anyhow = "1.0.98" +bytes = "1.8.0" +hyperion-platform = { path = "../../crates/hyperion-platform" } + +[dependencies.valence_bytes] +branch = "feat-bytes" +git = "https://github.com/TestingPlant/valence" + +[dependencies.valence_protocol] +branch = "feat-bytes" +default-features = false +git = "https://github.com/TestingPlant/valence" + +# Linking the kernel is what turns this from an application into a machine +# image. On a hosted OS the same source builds as an ordinary binary. +[target.'cfg(target_os = "hermit")'.dependencies.hermit] +default-features = false +features = ["acpi", "dhcpv4", "pci", "pci-ids", "smp", "tcp", "virtio-net"] +git = "https://github.com/hermit-os/hermit-rs" +tag = "hermit-0.13.2" + +[lints] +workspace = true diff --git a/bare-metal/hyperion-unikernel/src/main.rs b/bare-metal/hyperion-unikernel/src/main.rs new file mode 100644 index 000000000..dfe9e2a34 --- /dev/null +++ b/bare-metal/hyperion-unikernel/src/main.rs @@ -0,0 +1,203 @@ +//! Hyperion's protocol layer, on a machine with no operating system. +//! +//! This answers a Minecraft client's server-list ping using the same +//! `valence_protocol` codec the real server uses, over TCP, on whatever +//! platform it was built for. On Linux that is an ordinary process. On +//! `x86_64-unknown-hermit` it is a unikernel image: the binary *is* the kernel, +//! it boots under QEMU, brings up virtio-net, takes a DHCP lease, and listens. +//! +//! It is a demonstration, not a server. What it proves is narrow and specific: +//! that the wire protocol, the codec and the platform seam all work with no OS +//! underneath, which is the part of hyperion nobody had established. It does +//! not log anyone in — see `docs/bare-metal.md` for why the rest of the server +//! does not build yet. + +// Linking the kernel is a side effect of the dependency existing; nothing calls +// into it directly. +#[cfg(target_os = "hermit")] +use hermit as _; + +use std::{ + io::{Read, Write}, + net::{SocketAddr, TcpStream}, +}; + +use anyhow::{Context, bail, ensure}; +use hyperion_platform::{CAPABILITIES, clock, net, parallelism}; +use valence_bytes::CowUtf8Bytes; +use valence_protocol::{ + DecodeBytes, Encode, MAX_PACKET_SIZE, PROTOCOL_VERSION, Packet, PacketEncoder, + bytes::Bytes, + packets::{ + handshaking::{HandshakeC2s, handshake_c2s::HandshakeNextState}, + status::{QueryPingC2s, QueryPongS2c, QueryRequestC2s, QueryResponseS2c}, + }, +}; + +/// Where to listen unless `HYPERION_PORT` says otherwise. +/// +/// The environment is the only configuration channel that works on both +/// platforms: a unikernel has no config file to read, but Hermit does populate +/// `std::env` from the boot command line. +const DEFAULT_PORT: u16 = 25565; + +fn port() -> u16 { + std::env::var("HYPERION_PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_PORT) +} + +fn main() -> anyhow::Result<()> { + let started = clock::monotonic(); + + println!("[hyperion] platform: {}", hyperion_platform::NAME); + println!("[hyperion] capabilities: {CAPABILITIES:?}"); + println!("[hyperion] parallelism: {}", parallelism::available()); + match clock::wall_clock() { + Some(t) => println!("[hyperion] wall clock: {t:?}"), + None => println!("[hyperion] wall clock: unavailable on this platform"), + } + + let addr = SocketAddr::from(([0, 0, 0, 0], port())); + let listener = net::bind_tcp(addr)?; + println!( + "[hyperion] listening on {addr} after {elapsed:?}", + elapsed = started.elapsed() + ); + + for stream in listener.incoming() { + let stream = stream.context("accept failed")?; + let peer = stream.peer_addr().ok(); + // One connection at a time. A status ping is three packets and + // concurrency is not the thing under test here. + if let Err(e) = serve(stream) { + println!("[hyperion] {peer:?}: {e:#}"); + } + } + + Ok(()) +} + +/// Handshake, status, ping. The whole server-list exchange. +fn serve(mut stream: TcpStream) -> anyhow::Result<()> { + println!("[hyperion] accepted {peer:?}", peer = stream.peer_addr()); + + let handshake: HandshakeC2s<'_> = read_packet(&mut stream)?; + println!( + "[hyperion] handshake: protocol={protocol} host={host} port={port} next={next:?}", + protocol = handshake.protocol_version.0, + host = handshake.server_address.0.as_str(), + port = handshake.server_port, + next = handshake.next_state + ); + + ensure!( + handshake.next_state == HandshakeNextState::Status, + "only the status handshake is implemented on this platform" + ); + + let _: QueryRequestC2s = read_packet(&mut stream)?; + + let json = status_json(); + write_packet( + &mut stream, + &QueryResponseS2c { + json: CowUtf8Bytes::Borrowed(&json), + }, + )?; + println!("[hyperion] sent status"); + + // A client measures latency from this round trip. Echoing the payload + // unchanged is the whole protocol. + let ping: QueryPingC2s = read_packet(&mut stream)?; + write_packet( + &mut stream, + &QueryPongS2c { + payload: ping.payload, + }, + )?; + println!("[hyperion] ponged {payload:#x}", payload = ping.payload); + + Ok(()) +} + +/// The server-list entry a client renders. +fn status_json() -> String { + format!( + r#"{{"version":{{"name":"hyperion/{name}","protocol":{PROTOCOL_VERSION}}},"players":{{"max":10000,"online":0,"sample":[]}},"description":{{"text":"hyperion on {name}, no operating system"}}}}"#, + name = hyperion_platform::NAME + ) +} + +/// Read one length-prefixed frame and decode it as `P`. +/// +/// `valence_protocol` keeps its framing decoder private and hyperion's own +/// lives in a crate that does not build for this target yet, so the framing is +/// open-coded here. It is only correct because compression is off: with a +/// threshold set, the body carries a second length that this does not read. +fn read_packet

(stream: &mut TcpStream) -> anyhow::Result

+where + P: Packet + DecodeBytes, +{ + let len = read_var_int(stream).context("packet length")?; + ensure!( + (0..=MAX_PACKET_SIZE).contains(&len), + "packet length {len} out of bounds" + ); + + let mut body = vec![0u8; usize::try_from(len).expect("length is non-negative")]; + stream.read_exact(&mut body).context("packet body")?; + let mut body = Bytes::from(body); + + let id = read_var_int(&mut body.as_ref()).context("packet id")?; + let id_len = var_int_len(id); + let _ = body.split_to(id_len); + + ensure!( + id == P::ID, + "expected {name} (id {expected}), got id {id}", + name = P::NAME, + expected = P::ID + ); + + let packet = P::decode_bytes(&mut body).context("decode failed")?; + ensure!( + body.is_empty(), + "{name}: {left} trailing bytes", + name = P::NAME, + left = body.len() + ); + Ok(packet) +} + +/// Read a Minecraft `VarInt`: seven bits per byte, little end first, high bit +/// meaning "another byte follows", five bytes maximum. +fn read_var_int(r: &mut impl Read) -> anyhow::Result { + let mut value = 0i32; + for shift in 0..5 { + let mut byte = [0u8; 1]; + r.read_exact(&mut byte).context("short read")?; + value |= i32::from(byte[0] & 0x7F) << (shift * 7); + if byte[0] & 0x80 == 0 { + return Ok(value); + } + } + bail!("`VarInt` longer than five bytes") +} + +/// How many bytes the wire form of `value` occupies. +const fn var_int_len(value: i32) -> usize { + let bits = 32 - (value.cast_unsigned() | 1).leading_zeros(); + (bits as usize).div_ceil(7) +} + +fn write_packet

(stream: &mut TcpStream, packet: &P) -> anyhow::Result<()> +where + P: Packet + Encode, +{ + let mut encoder = PacketEncoder::new(); + encoder.append_packet(packet)?; + stream.write_all(&encoder.take()).context("write failed")?; + stream.flush().context("flush failed") +} diff --git a/bare-metal/rust-toolchain.toml b/bare-metal/rust-toolchain.toml new file mode 100644 index 000000000..12513607a --- /dev/null +++ b/bare-metal/rust-toolchain.toml @@ -0,0 +1,11 @@ +# Pinned to the Hermit kernel's own nightly, and it has to match. +# +# The kernel is compiled by a nested cargo invocation and linked into the +# application, so application `std` and kernel implement two halves of the same +# `hermit-abi`. Building the application on the main workspace's +# nightly-2025-02-22 produces an image that boots, prints, and then fails every +# socket call with an uncategorised error, because that std was built against +# hermit-abi 0.4 while kernel 0.13.2 answers 0.5. See docs/bare-metal.md. +[toolchain] +channel = "nightly-2026-07-01" +components = ["rust-src", "llvm-tools", "rustfmt", "clippy"] diff --git a/crates/hyperion-platform/Cargo.toml b/crates/hyperion-platform/Cargo.toml new file mode 100644 index 000000000..8ead9697d --- /dev/null +++ b/crates/hyperion-platform/Cargo.toml @@ -0,0 +1,26 @@ +# Package fields are spelled out rather than inherited, and the lint table is +# local, because this crate is a member of two workspaces: the main one, and +# bare-metal/, which builds it for a target the main lockfile does not know +# about. `x.workspace = true` only resolves against one root. +[package] +authors = ["Andrew Gazelka "] +edition = "2024" +license = "Apache-2.0" +name = "hyperion-platform" +publish = false +readme = "README.md" +repository = "https://github.com/andrewgazelka/hyperion" +version = "0.1.0" + +[dependencies] + +[target.'cfg(unix)'.dependencies] +libc = "0.2.172" + +[lints.clippy] +complexity = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } diff --git a/crates/hyperion-platform/README.md b/crates/hyperion-platform/README.md new file mode 100644 index 000000000..ebcac7c64 --- /dev/null +++ b/crates/hyperion-platform/README.md @@ -0,0 +1,22 @@ +# hyperion-platform + +The set of operating-system services hyperion needs, behind one narrow module +surface, so that a target without an OS is a backend rather than a fork. + +Five things differ between a hosted OS and a unikernel, and only these five: + +| module | hosted (Linux, macOS) | unikernel (Hermit) | +| ------------- | --------------------------- | --------------------------------- | +| `limits` | `setrlimit(RLIMIT_NOFILE)` | no such limit; reports the cap | +| `clock` | monotonic + wall clock | monotonic; wall clock may be absent | +| `storage` | the filesystem | RAM, seeded by the image | +| `net` | `std::net` + `AF_UNIX` | `std::net` over virtio-net only | +| `parallelism` | `available_parallelism` | vCPUs handed over at boot | + +Everything else hyperion does is arithmetic, and needs no seam. + +The backend is picked by `cfg(target_os)` at compile time. Hosted is the +default and the only one a normal build ever sees, so adding a third platform +means writing a `backend/*.rs` and one `cfg` arm, not editing call sites. + +See `docs/bare-metal.md` for what actually builds on the unikernel today. diff --git a/crates/hyperion-platform/src/backend.rs b/crates/hyperion-platform/src/backend.rs new file mode 100644 index 000000000..1333b6bd1 --- /dev/null +++ b/crates/hyperion-platform/src/backend.rs @@ -0,0 +1,15 @@ +//! Backend selection. +//! +//! One `cfg` arm per platform, and nothing else in the crate branches on the +//! target. Adding a platform is: write a module, add an arm here, add a row to +//! the table in the README. + +#[cfg(target_os = "hermit")] +mod unikernel; +#[cfg(target_os = "hermit")] +pub use unikernel::*; + +#[cfg(not(target_os = "hermit"))] +mod hosted; +#[cfg(not(target_os = "hermit"))] +pub use hosted::*; diff --git a/crates/hyperion-platform/src/backend/hosted.rs b/crates/hyperion-platform/src/backend/hosted.rs new file mode 100644 index 000000000..2add1d60a --- /dev/null +++ b/crates/hyperion-platform/src/backend/hosted.rs @@ -0,0 +1,95 @@ +//! Linux, macOS, and anything else with an operating system under it. +//! +//! This is the default backend and the only one a normal build compiles. Every +//! function here is the behaviour hyperion had before the seam existed. + +use std::{ + fs, io, + num::NonZeroUsize, + path::Path, + sync::OnceLock, + time::SystemTime, +}; + +use crate::{Capabilities, storage::Store}; + +pub const NAME: &str = crate::HOSTED; + +pub const CAPABILITIES: Capabilities = Capabilities { + persistent_storage: true, + unix_sockets: cfg!(unix), + dns: true, + trustworthy_wall_clock: true, + adjustable_file_limit: cfg!(unix), + subprocesses: true, +}; + +#[cfg(unix)] +pub fn raise_open_files(recommended_min: u64) -> io::Result { + // Initialised by getrlimit; the zeroes are never read. + let mut limits = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + + // SAFETY: `limits` is a live, correctly typed rlimit for the duration of + // the call, and RLIMIT_NOFILE is a valid resource. + if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &raw mut limits) } != 0 { + return Err(io::Error::last_os_error()); + } + + limits.rlim_cur = limits.rlim_max; + + // SAFETY: as above, and rlim_cur <= rlim_max so the request is valid. + if unsafe { libc::setrlimit(libc::RLIMIT_NOFILE, &raw const limits) } != 0 { + return Err(io::Error::last_os_error()); + } + + let _ = recommended_min; + Ok(limits.rlim_cur) +} + +#[cfg(not(unix))] +pub fn raise_open_files(recommended_min: u64) -> io::Result { + Ok(recommended_min) +} + +// The seam's signature, not this backend's: a hosted OS always has an +// answer, but the caller must still handle the platform that does not. +#[expect(clippy::unnecessary_wraps, reason = "signature is fixed by the seam")] +pub fn wall_clock() -> Option { + Some(SystemTime::now()) +} + +pub fn available_parallelism() -> NonZeroUsize { + std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN) +} + +/// The filesystem, reached through the blob-shaped [`Store`] interface. +struct FsStore; + +impl Store for FsStore { + fn read(&self, key: &Path) -> io::Result> { + fs::read(key) + } + + fn write(&self, key: &Path, bytes: &[u8]) -> io::Result<()> { + if let Some(parent) = key.parent() { + fs::create_dir_all(parent)?; + } + fs::write(key, bytes) + } + + fn exists(&self, key: &Path) -> bool { + key.exists() + } + + fn is_persistent(&self) -> bool { + true + } +} + +pub fn store() -> &'static dyn Store { + static STORE: OnceLock = OnceLock::new(); + STORE.get_or_init(|| FsStore) +} diff --git a/crates/hyperion-platform/src/backend/unikernel.rs b/crates/hyperion-platform/src/backend/unikernel.rs new file mode 100644 index 000000000..eebc330bb --- /dev/null +++ b/crates/hyperion-platform/src/backend/unikernel.rs @@ -0,0 +1,95 @@ +//! Hermit, and any future unikernel that supplies a `std`. +//! +//! Everything here is either a constant the hypervisor fixed at boot or an +//! honest refusal. Nothing pretends. + +use std::{ + collections::HashMap, + io, + num::NonZeroUsize, + path::{Path, PathBuf}, + sync::{Mutex, OnceLock}, + time::SystemTime, +}; + +use crate::{Capabilities, storage::Store}; + +pub const NAME: &str = crate::UNIKERNEL; + +pub const CAPABILITIES: Capabilities = Capabilities { + // The image's RAM is all there is, and it goes away with the VM. + persistent_storage: false, + // smoltcp speaks IP. There is no filesystem to hang a socket off anyway. + unix_sockets: false, + // Hermit resolves names only when built with its `dns` feature, and this + // crate cannot see the kernel's feature set from here. Assume not, and let + // a build that knows better say so. + dns: false, + // No RTC unless the hypervisor provides one, and QEMU's default guest boots + // believing it is the epoch. + trustworthy_wall_clock: false, + adjustable_file_limit: false, + // Hermit has no process model at all: `std::process` is the `unsupported` + // backend, so `Command::spawn` always errors. + subprocesses: false, +}; + +pub fn raise_open_files(recommended_min: u64) -> io::Result { + // There is no rlimit here. The socket table is sized when the kernel is + // built, so the honest answer is "whatever you asked for, nothing stopped + // you", and the caller's warning threshold never fires spuriously. + Ok(recommended_min) +} + +pub fn wall_clock() -> Option { + None +} + +pub fn available_parallelism() -> NonZeroUsize { + // Hermit implements this over the vCPU count the hypervisor passed at boot, + // so unlike on Linux it is exact rather than a hint. + std::thread::available_parallelism().unwrap_or(NonZeroUsize::MIN) +} + +/// RAM pretending to be a disk. +/// +/// Reads of anything the image did not ship with fail as not-found, which is +/// the same failure a hosted build gets from a missing file, so callers that +/// already handle a missing config need no unikernel-specific branch. +struct MemStore { + entries: Mutex>>, +} + +impl Store for MemStore { + fn read(&self, key: &Path) -> io::Result> { + self.entries + .lock() + .expect("store poisoned") + .get(key) + .cloned() + .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, format!("{}", key.display()))) + } + + fn write(&self, key: &Path, bytes: &[u8]) -> io::Result<()> { + self.entries + .lock() + .expect("store poisoned") + .insert(key.to_path_buf(), bytes.to_vec()); + Ok(()) + } + + fn exists(&self, key: &Path) -> bool { + self.entries.lock().expect("store poisoned").contains_key(key) + } + + fn is_persistent(&self) -> bool { + false + } +} + +pub fn store() -> &'static dyn Store { + static STORE: OnceLock = OnceLock::new(); + STORE.get_or_init(|| MemStore { + entries: Mutex::new(HashMap::new()), + }) +} diff --git a/crates/hyperion-platform/src/clock.rs b/crates/hyperion-platform/src/clock.rs new file mode 100644 index 000000000..7aabd00c9 --- /dev/null +++ b/crates/hyperion-platform/src/clock.rs @@ -0,0 +1,24 @@ +//! Time. +//! +//! Monotonic time exists everywhere. Wall-clock time does not: a unikernel +//! booted without an RTC or a hypervisor time source has no idea what year it +//! is, so [`wall_clock`] is fallible rather than silently wrong. + +use std::time::{Instant, SystemTime}; + +/// A monotonic instant. Always available. +#[must_use] +pub fn monotonic() -> Instant { + Instant::now() +} + +/// The current wall-clock time, or `None` where the platform has no trustworthy +/// source for it. +/// +/// Anything that stamps a durable record — a ban, a statistic, a log shipped +/// off the machine — should handle the `None` case rather than substituting +/// the epoch. +#[must_use] +pub fn wall_clock() -> Option { + crate::backend::wall_clock() +} diff --git a/crates/hyperion-platform/src/lib.rs b/crates/hyperion-platform/src/lib.rs new file mode 100644 index 000000000..b2c1c362b --- /dev/null +++ b/crates/hyperion-platform/src/lib.rs @@ -0,0 +1,64 @@ +//! The operating-system services hyperion needs, behind one narrow surface, so +//! that a target without an OS is a backend rather than a fork. +//! +//! This is deliberately not a `no_std` crate. The bare-metal target hyperion +//! aims at is a unikernel that supplies a real `std`, so the seam is not about +//! the absence of a standard library; it is about the handful of things `std` +//! exposes that a machine with no operating system underneath cannot honour. +//! The survey in `docs/bare-metal.md` found five, and they are the modules +//! below. Everything else hyperion does is arithmetic and needs no seam. +//! +//! The backend is chosen by `cfg(target_os)`. [`HOSTED`] is the default and the +//! only one a normal Linux or macOS build ever compiles, so adding a third +//! platform means writing a backend module and one `cfg` arm rather than +//! editing call sites. + +mod backend; + +pub mod clock; +pub mod limits; +pub mod net; +pub mod parallelism; +pub mod storage; + +/// A hosted operating system: a filesystem, a process model, and a full socket +/// API. Linux and macOS. +pub const HOSTED: &str = "hosted"; + +/// A unikernel: the application is the kernel, and there is no filesystem, no +/// process model, and no network beyond what the hypervisor hands over. +pub const UNIKERNEL: &str = "unikernel"; + +/// Which backend this build was compiled against, for logging and for tests +/// that need to skip what the platform cannot do. +pub const NAME: &str = backend::NAME; + +/// What the current platform can actually do. +/// +/// Read these rather than testing `cfg(unix)` at a call site. A call site that +/// asks "am I on Unix?" has to be revisited for every new platform; one that +/// asks "is there a filesystem?" does not. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +// A capability set is a set of yes-or-no answers. Packing them into an enum or +// a bitflag would only make call sites less readable. +#[expect(clippy::struct_excessive_bools, reason = "this is a set of flags")] +pub struct Capabilities { + /// Whether [`storage`] is backed by something that survives a reboot. + pub persistent_storage: bool, + /// Whether `AF_UNIX` sockets exist. The proxy's local transport needs them. + pub unix_sockets: bool, + /// Whether hostnames resolve. Without this, peers must be given as literal + /// addresses. + pub dns: bool, + /// Whether [`clock::wall_clock`] returns a time anyone should trust. + pub trustworthy_wall_clock: bool, + /// Whether the open-file limit is a thing that exists and can be raised. + pub adjustable_file_limit: bool, + /// Whether child processes can be spawned. Hermit has no process model at + /// all, so anything shelling out is a hard stop rather than a slow path. + pub subprocesses: bool, +} + +/// The capabilities of the platform this build targets. +pub const CAPABILITIES: Capabilities = backend::CAPABILITIES; diff --git a/crates/hyperion-platform/src/limits.rs b/crates/hyperion-platform/src/limits.rs new file mode 100644 index 000000000..d3cd53827 --- /dev/null +++ b/crates/hyperion-platform/src/limits.rs @@ -0,0 +1,18 @@ +//! Raising the open-file limit, where there is one. + +/// Raise the soft limit on open files as far as the hard limit allows, and +/// report the limit now in force. +/// +/// Ten thousand players at two file handles each is twenty thousand, and macOS +/// still defaults to a soft limit of 256, so a hosted build has to ask. A +/// unikernel has no such limit to raise: it reports the ceiling its socket +/// table was built with and does nothing. +/// +/// Callers should treat a returned value below `recommended_min` as a warning, +/// not an error, because it is one. +/// +/// # Errors +/// Whatever the platform reports when reading or setting the limit fails. +pub fn raise_open_files(recommended_min: u64) -> std::io::Result { + crate::backend::raise_open_files(recommended_min) +} diff --git a/crates/hyperion-platform/src/net.rs b/crates/hyperion-platform/src/net.rs new file mode 100644 index 000000000..991d894e3 --- /dev/null +++ b/crates/hyperion-platform/src/net.rs @@ -0,0 +1,47 @@ +//! Sockets. +//! +//! Both platforms have `std::net`, so this module does not wrap it. What it +//! does is name the two things that differ, so a call site can ask about the +//! capability instead of the operating system. +//! +//! Hermit's stack is smoltcp over virtio-net. It gives IPv4 and IPv6 TCP and +//! UDP, and nothing else: no `AF_UNIX`, and DNS only when the kernel was built +//! with its resolver. + +pub use std::net::{TcpListener, TcpStream, UdpSocket}; +use std::{io, net::SocketAddr}; + +/// Bind a TCP listener, naming the platform in the error. +/// +/// A bare `bind` failure on a unikernel is an opaque `Uncategorized`, which +/// sends people looking at their firewall rather than at the fact that the +/// guest never got an address. Say which platform failed. +/// +/// # Errors +/// Whatever the platform's `bind` reports. +pub fn bind_tcp(addr: SocketAddr) -> io::Result { + TcpListener::bind(addr).map_err(|e| { + io::Error::new( + e.kind(), + format!("{platform}: failed to bind {addr}: {e}", platform = crate::NAME), + ) + }) +} + +/// Whether `AF_UNIX` sockets exist on this platform. +/// +/// The proxy prefers a Unix socket for the server link when both ends share a +/// machine. On a unikernel there is no such thing, and no filesystem to put one +/// in, so the loopback path is the only path. +#[must_use] +pub const fn supports_unix_sockets() -> bool { + crate::CAPABILITIES.unix_sockets +} + +/// Whether hostnames resolve. +/// +/// When this is `false`, every peer address must already be a literal. +#[must_use] +pub const fn supports_dns() -> bool { + crate::CAPABILITIES.dns +} diff --git a/crates/hyperion-platform/src/parallelism.rs b/crates/hyperion-platform/src/parallelism.rs new file mode 100644 index 000000000..ae4409796 --- /dev/null +++ b/crates/hyperion-platform/src/parallelism.rs @@ -0,0 +1,35 @@ +//! How many threads to run, and how to start them. + +use std::num::NonZeroUsize; + +/// How many threads can run at once. +/// +/// On a hosted OS this is the CPU count as constrained by affinity and cgroup +/// quota. On a unikernel it is the number of vCPUs the hypervisor handed over +/// at boot, which is exact rather than a hint. +#[must_use] +pub fn available() -> NonZeroUsize { + crate::backend::available_parallelism() +} + +/// Spawn a worker thread with an explicit stack size. +/// +/// Hyperion sizes its rayon workers deliberately, and a unikernel's default +/// stack is much smaller than Linux's 8 MiB, so the size is not optional here +/// the way it is in [`std::thread::spawn`]. +/// +/// # Errors +/// If the platform will not give us another thread. +pub fn spawn_worker( + name: &str, + stack_size: usize, + f: F, +) -> std::io::Result> +where + F: FnOnce() + Send + 'static, +{ + std::thread::Builder::new() + .name(name.to_owned()) + .stack_size(stack_size) + .spawn(f) +} diff --git a/crates/hyperion-platform/src/storage.rs b/crates/hyperion-platform/src/storage.rs new file mode 100644 index 000000000..30daa7cf6 --- /dev/null +++ b/crates/hyperion-platform/src/storage.rs @@ -0,0 +1,44 @@ +//! Durable-ish bytes, keyed by path. +//! +//! Hyperion reads a TOML config, an LMDB player database, Anvil region files +//! and a downloaded asset bundle, and every one of those assumes a filesystem. +//! A unikernel has none unless the hypervisor attaches one, so this module is +//! the seam: a hosted build gets the real filesystem, and a unikernel build +//! gets RAM that starts out holding whatever the image was built with. +//! +//! The trait is intentionally blob-shaped rather than file-shaped. Nothing in +//! hyperion needs `seek` or partial writes, and offering them would mean +//! implementing a filesystem on the unikernel side. + +use std::{io, path::Path}; + +/// A place to put bytes and get them back. +pub trait Store: Send + Sync { + /// Read the whole value at `key`. + /// + /// # Errors + /// [`io::ErrorKind::NotFound`] if there is nothing at `key`. + fn read(&self, key: &Path) -> io::Result>; + + /// Write `bytes` at `key`, replacing anything already there. + /// + /// # Errors + /// Whatever the backing store reports; a read-only backend returns + /// [`io::ErrorKind::Unsupported`]. + fn write(&self, key: &Path, bytes: &[u8]) -> io::Result<()>; + + /// Whether `key` holds anything. + fn exists(&self, key: &Path) -> bool; + + /// Whether writes survive a restart. `false` means this is a cache. + fn is_persistent(&self) -> bool; +} + +/// The store for this platform. +/// +/// One process-wide instance, because the thing it stands in for — the +/// filesystem — is also one process-wide instance. +#[must_use] +pub fn store() -> &'static dyn Store { + crate::backend::store() +} diff --git a/docs/bare-metal.md b/docs/bare-metal.md new file mode 100644 index 000000000..164e8ca47 --- /dev/null +++ b/docs/bare-metal.md @@ -0,0 +1,397 @@ +# Hyperion with no operating system + +Experimental. Nothing here is on any normal build path, and none of it is +merged. + +## What this achieved: the protocol layer boots on a unikernel and answers a real client + +`nix build .#bare-metal` produces an `x86_64-unknown-hermit` machine image; +`nix run .#bare-metal-vm` boots it under QEMU, where it brings up virtio-net, +takes a DHCP lease and answers a Minecraft server-list ping decoded with the +same `valence_protocol` the real server uses. Real output is in +[Evidence](#evidence). + +What that does **not** mean: the game server does not build. Eleven of +twenty-four workspace crates compile for the target and thirteen do not, +including `hyperion` itself. The gap and its causes are in +[The blocker survey](#the-blocker-survey). + +## What to reach for: Hermit, the only live Rust unikernel with a real `std` + +The choice matters less than it looks, because only two candidates ship a +`std`, and only one of them has virtio-net drivers you can read. + +| Option | Version / date checked 2026-07-27 | What it gives | What it costs | +| --- | --- | --- | --- | +| **Hermit** (`hermit-os/kernel`, `hermit-os/hermit-rs`) | kernel **v0.13.2**, 2026-03-13; both repos pushed 2026-07-27 | A genuine `std` behind four Tier-3 triples (`x86_64`, `aarch64`, `aarch64_be`, `riscv64gc`), upstream `library/std/src/sys/pal/hermit`. virtio-net, virtio-fs, virtio-vsock, smoltcp 0.13, DHCPv4 on by default. QEMU, Firecracker and uhyve in CI. | Tier 3, so `-Z build-std`. Kernel pins its own monthly nightly. `std::process` absent entirely; `std::fs` missing rename, symlink, canonicalize and all locking. **Tokio only through two unmaintained forks.** Nothing in nixpkgs. | +| **Motor OS** (`moturus/motor-os`) | Tier-3 target `x86_64-unknown-motor` since 2025-10-17; pushed 2026-07-27 | Rust microkernel, VM-only, `std` upstream, smoltcp, ~100 ms boot, ~10 Gbps guest↔host. | No DHCP (static IPs only), Tokio only partly ported, essentially one maintainer, no security audit by its own account. | +| **Unikraft** | v0.21.0, 2026-04/05; project healthy | Mature C unikernel; runs ordinary musl Rust binaries through its binary-compatibility layer. | **Its Rust integration is dead.** `unikraft/lib-rust` last pushed 2024-01-02; the catalog's Rust examples are all pinned to Rust 1.75 (Dec 2023); the `x86_64-unikraft-linux-musl` target's own rustc docs say linking needs a KraftKit shim and point at an issue closed in 2023 with no successor. | +| **`no_std` + `virtio-drivers` + `smoltcp`** | `virtio-drivers` 0.13.0 (2026-03-03), `smoltcp` 0.13.1 (2026-04-30) | Works on stable against Tier-2 `x86_64-unknown-none`, no `build-std`, no custom JSON target. | No `std`, ever. You write the boot path, the allocator and the event loop. smoltcp was still fixing TCP panics in April 2026. | +| **Nanos/OPS, OSv** | nanos 0.1.55 (2026-04-26); OSv last tag 2022-12-20, master alive | Run unmodified Linux ELFs, so "Rust support" is `--target x86_64-unknown-linux-musl`. | The kernel is an opaque C blob; no Rust-level control of the virtio stack. | +| **Firecracker + minimal Linux** | v1.16.1, 2026-07-02 | The boring baseline, and **the only option where virtio-mem actually works** (guest driver landed 1.14.0; needs `CONFIG_VIRTIO_MEM`, Linux ≥5.16). | It is a Linux VM. Nothing bare-metal about the guest. | + +### virtio-mem does not exist in Rust guest land + +The brief assumed virtio-mem alongside virtio-net. It is not available on any +Rust unikernel. Hermit's `src/drivers/` has `console`, `fs`, `net`, `vsock` and +nothing else; the only matches for `virtio.mem` in the whole kernel tree are +three occurrences of `virtio_mem_barrier`, a memory *barrier* helper. The +`virtio-drivers` crate has no virtio-mem driver, no issue and no PR proposing +one; ballooning is an open PR (#251, 2026-06-15) and unmerged. If elastic guest +memory is a requirement, that is a from-scratch driver plus kernel allocator +work, or it is Firecracker with a Linux guest. + +### `crates.io` is a dead end for Hermit, and says so + +``` +error: This crate is no longer distributed via crates.io. Use the crate via Git instead. + --> ~/.cargo/registry/src/index.crates.io-.../hermit-0.13.0/src/lib.rs:4:1 +``` + +Both `hermit` and `hermit-kernel` on crates.io are `compile_error!` stubs. The +dependency has to be a git tag. + +## The toolchain finding that costs a day if you meet it the hard way + +**Hyperion's pinned `nightly-2025-02-22` builds a Hermit image that boots, +prints, and then fails every socket call.** It is not a networking problem and +it does not look like a toolchain problem: + +``` +[ 0.409679][0][INFO hermit ] Jumping into application +[app] hermit unikernel up + +thread 'main' panicked at src/main.rs:8:54: +bind: Kind(Uncategorized) +``` + +The kernel and the application implement two halves of one `hermit-abi`. The +application's `std` is compiled from that nightly's `rust-src`, which pulls +`hermit-abi 0.4.0`; kernel 0.13.2 answers `hermit-abi 0.5`. Boot and `println!` +happen to line up across that gap and sockets do not. Rebuilding the identical +source on `nightly-2026-07-01` — the kernel's own pin — binds first try. + +So `bare-metal/` carries its own `rust-toolchain.toml`. Two toolchains in one +repo is a cost, and the alternative is worse: moving the whole workspace onto a +2026 nightly to satisfy a target nobody ships yet. + +`nightly-2025-02-22` is also simply too old for the current `hermit` crate's +build dependencies, which is the failure you hit first: + +``` +error: rustc 1.87.0-nightly is not supported by the following package: + home@0.5.12 requires rustc 1.88 +``` + +### Unstable features relied on + +Exactly one: `-Z build-std=std,panic_abort`, plus the `rust-src` and +`llvm-tools` components. No feature gates in any source file, no custom JSON +target. `-Z build-std` is a funded 2026 Rust Project Goal with RFC 3873 +accepted, but its goal page states that using `std` with *custom* targets is +out of scope — builtin Tier-3 targets like Hermit's are the ones on the +stabilisation path, which is the right side of that line to be on. + +There is a second route worth knowing: `hermit-os/rust-std-hermit` publishes an +installable `rust-std` component tracking stable point releases (1.97.1, +2026-07-20), which removes `-Z build-std` entirely. This branch does not use it, +because it would pair a stable-built `std` with a nightly-built kernel — the +exact pairing whose ABI drift is documented above. + +## The blocker survey: 11 of 24 crates compile, and four dependencies explain the rest + +Method, so the numbers can be read correctly: + +```sh +RUSTFLAGS="--cfg tokio_unstable" cargo +nightly-2025-02-22 build \ + -Z build-std=std,panic_abort --target x86_64-unknown-hermit -p +``` + +This is a *reachability* survey run per package, so a failure is the **first** +wall, not a complete list: cargo stops at the first crate that will not build, +and anything behind it is untested. Read a "blocked by socket2" row as "gets no +further than socket2", not as "socket2 is the only problem". + +The survey ran on `nightly-2025-02-22` deliberately, to answer "what does the +tree as it stands do", not "what could it do". Since compilation does not +exercise the syscall ABI, the 2025 toolchain gives the same answer here as the +2026 one would. + +### Compiles for `x86_64-unknown-hermit` today (11) + +`hyperion-minecraft-proto`, `hyperion-nerd-font`, `hyperion-scheduled`, +`hyperion-stats`, `simd-utils`, `geometry`, `hyperion-proto`, +`hyperion-palette`, `hyperion-text`, `hyperion-crafting`, `packet-channel`. + +Two of those are more interesting than they look. `hyperion-palette` pulls +`valence_protocol` with the `compression` feature, so **the whole Minecraft +protocol codec cross-compiles unmodified** — that is what made the demo +possible. `hyperion-crafting` and `packet-channel` pull `bevy` with +`multi_threaded`, so **bevy_ecs cross-compiles too**. + +### Does not compile (13) + +| Crate | Stops at | Why | Fixable how | +| --- | --- | --- | --- | +| `bvh-region` | `wait-timeout` | `proptest` is in `[dependencies]`, not `[dev-dependencies]`; it pulls `rusty-fork` → `wait-timeout`, which has a `sys` module for unix and windows only. | Move `proptest` to `[dev-dependencies]`. This is a bug on Linux too — it ships a test framework into release builds. | +| `hyperion-utils` | `socket2`, `openssl-sys` | `reqwest` with default features on, so `default-tls` → `native-tls` → `openssl-sys`, despite `rustls-tls` also being requested. | `default-features = false` on `reqwest` removes openssl outright. `socket2` needs Hermit's fork. | +| `hyperion-command`, `hyperion-gui`, `hyperion-item`, `hyperion-genmap`, `hyperion-clap`, `hyperion-permission` | `socket2`, `openssl-sys`, `libz-ng-sys`, `ring`, `wait-timeout` | All reach these through `hyperion` or `hyperion-utils`. None has a blocker of its own. | Fix the four below and re-survey. | +| `hyperion-proxy`, `hyperion-proxy-module` | `socket2` | `tokio` with `net`. | Hermit's `socket2` and `tokio` forks. | +| `hyperion` | `socket2`, `openssl-sys`, `libz-ng-sys`, `ring`, `wait-timeout` | Everything at once, plus more behind it. | See below. | +| `bedwars` | as `hyperion`, plus `std::os::unix` | `error[E0433]: could not find 'unix' in 'os'`. | A platform seam call, which is what `hyperion-platform` is for. | + +The four dependencies that cause almost every failure: + +| Dependency | Reached via | Nature | Cost to fix | +| --- | --- | --- | --- | +| `socket2` | `tokio` (net), `reqwest` | `error: Socket2 doesn't support the compile target` | Low. `hermit-os/socket2` fork exists and is what hermit-rs patches in. | +| `openssl-sys` | `reqwest` default features | C library, needs a cross toolchain | Low, and worth doing anyway: the tree asks for `rustls-tls` and gets openssl as well. | +| `libz-ng-sys` | `flate2` with `zlib-ng` in `hyperion` | C library | Medium. Dropping to `flate2`'s pure-Rust `miniz_oxide` backend costs compression throughput on the hot egress path. | +| `ring` | rustls crypto provider | C and assembly | Medium. `aws-lc-rs` is also C; `rustls` has no pure-Rust provider in this tree. | + +### Blockers the survey never reached, from reading the manifests + +These are **unverified**. Each sits behind one of the four above, so no build +has ever gotten far enough to confirm or refute them. Listing them because they +are the ones that decide whether the server can ever boot, not because they have +been measured: + +| Dependency | Used by | Why it looks fatal | +| --- | --- | --- | +| `heed` (LMDB) | `hyperion` player DB, `hyperion-permission` | C library, `mmap`, file locking. Hermit's `std::fs` has no locking at all. | +| `memmap2` | `hyperion` Anvil region reader | `mmap` of a file; there is no file. | +| `libdeflater` | `hyperion` chunk compression | C library. | +| `tikv-jemallocator` | `hyperion`, `bedwars` | C allocator; a unikernel supplies its own. Already `cfg`'d off on Windows, so the seam exists. | +| `ndarray` with `blas` | `hyperion` | Requires a system BLAS. | +| `tracing-tracy` | `hyperion` | Profiler client over a socket. | +| `valence_anvil` | `hyperion` | Reads region files from disk. | +| `directories` | `hyperion-utils` | Asks the OS where the home directory is. | +| `tokio` | everywhere | Runs on Hermit **only** via `hermit-os/tokio`, pinned to 1.45.0, last commit 2025-05-08 and unmaintained. The tree pins tokio 1.45.0, so the versions line up today and will not stay lined up. `mio` and `polling` have genuine upstream Hermit support; tokio does not. | + +### The honest verdict + +Of the three outcomes the brief offered, this is the middle one, and nearer its +lower end than its upper. A meaningful subset — the protocol codec, the ECS, the +geometry and BVH primitives, and the wire format — compiles and one of them +demonstrably runs. The server does not, and the distance to it is not a +weekend: `heed`, `memmap2`, `libdeflater` and `ndarray+blas` are four C +libraries that a unikernel with no filesystem cannot host, so closing that gap +means replacing persistence and chunk storage, not porting them. + +The realistic next milestone is not "the server boots". It is **the proxy**, +which needs only `socket2` + `tokio` forks and has no filesystem dependency +worth the name. + +## The seam: five things, named after what differs + +`crates/hyperion-platform` is deliberately not a portability layer. Hermit +supplies a real `std`, so almost nothing needs wrapping. The survey found five +places where a hosted OS and a unikernel genuinely disagree, and the crate is +those five and nothing else: + +| Module | Hosted | Unikernel | +| --- | --- | --- | +| `limits::raise_open_files` | `setrlimit(RLIMIT_NOFILE)` | no such limit; reports the ceiling asked for | +| `clock::wall_clock` | `Some(SystemTime::now())` | `None` — no RTC unless the hypervisor gives one | +| `storage::store` | the filesystem | RAM, seeded by the image, `is_persistent() == false` | +| `net` | `std::net` plus `AF_UNIX` | `std::net` over virtio-net only | +| `parallelism::available` | CPUs, as a hint | vCPUs handed over at boot, exactly | + +Plus a `CAPABILITIES` constant, so a call site asks "is there a filesystem?" +rather than "am I on Unix?". The first question survives a new platform; the +second does not. + +The backend is one `cfg` arm in `src/backend.rs`. Adding a third platform is a +new `backend/*.rs`, that arm, and a row in the table. Hosted is the default and +is byte-for-byte the behaviour hyperion already had. + +### What the seam is not yet + +**It is not wired into `hyperion`.** Not one existing crate was changed. Wiring +it in would buy nothing today — `hyperion` is blocked on four C libraries, not +on `cfg(unix)` — and would put a diff into files three other streams are +editing. The demonstration that the seam works is `bare-metal/hyperion-unikernel`, +which uses it on both platforms and reports different, correct answers on each. + +The call sites it is *for* are already identified, and they are few: + +| File | What it does | +| --- | --- | +| `crates/hyperion/src/lib.rs:37,94` | `libc::getrlimit`/`setrlimit`, already `#[cfg(unix)]`-gated → `limits::raise_open_files` | +| `crates/hyperion/src/lib.rs:205` | `std::thread::Builder` with an explicit stack → `parallelism::spawn_worker` | +| `crates/hyperion/src/common/config.rs:80,94` | `fs::create_dir_all`, `fs::write` → `storage::store` | +| `crates/hyperion/src/storage/db.rs:24` | `fs::create_dir_all` for LMDB → `storage`, once LMDB itself is replaced | +| `crates/hyperion/src/simulation/blocks/region.rs:30,148` | `memmap2` + `File::open` → `storage`, same caveat | +| `crates/hyperion-proxy/src/main.rs:6,8,125` | `TcpListener`, `UnixListener`, `lookup_host` → `net::supports_unix_sockets`, `net::supports_dns` | +| `events/bedwars/src/lib.rs` | `std::os::unix` | + +That is the whole OS surface of the tree. It is smaller than the dependency +list suggests, which is the encouraging part of this exercise: hyperion's own +code is close to portable, and its dependencies are not. + +## Nix + +Three cargo invocations happen inside one `nix build`: + +1. the application, from `bare-metal/Cargo.lock`; +2. the standard library, because `-Z build-std` compiles `std` from source and + resolves its own lockfile out of `rust-src`; +3. the Hermit kernel, which the `hermit` crate's `build.rs` builds by shelling + out to a nested cargo in the kernel's source tree. + +The third one is why this is a single `cargo vendor --sync` producing one +vendor directory rather than three `fetchCargoVendor` calls: that build script +strips **every** `CARGO_*` and `RUST_*` variable from its environment before +running, so `$HOME/.cargo/config.toml` is the only channel that reaches it. + +Three further things the sandbox needed, each a one-line comment in +`nix/bare-metal.nix`: + +- `CARGO_NET_GIT_FETCH_WITH_CLI=true` — cargo's bundled libgit2 cannot complete + a TLS handshake in a fixed-output derivation; the `git` binary can. +- `dontFixup = true` on the vendor derivation — `patchShebangs` rewrites + vendored CI scripts to a bash store path, which both adds a store reference a + fixed-output derivation may not have and invalidates cargo's checksums. +- A `rustup` shim — the kernel's `xtask` runs `rustup target add + x86_64-unknown-none` unconditionally. The shim answers that and *fails loudly* + on anything else, so a future rustup call does not silently no-op. + +`RUSTFLAGS=""` is set on the build. The repo's `.cargo/config.toml` puts +`-Ctarget-cpu=native` in `[build] rustflags`, which applies to every target, so +any cross-compile emits host-CPU instructions and dies inside `core`: + +``` +'apple-m4' is not a recognized processor for this target (ignoring processor) +rustc-LLVM ERROR: 64-bit code requested on a subtarget that doesn't support it! +``` + +`RUSTFLAGS` overrides it, which is why `.cargo/config.toml` is untouched — no +existing build changes behaviour. + +## Evidence + +Everything below is real output, on `aarch64-darwin`, cross-compiling to +`x86_64-unknown-hermit`. + +### `nix build .#bare-metal` + +``` +$ nix build .#bare-metal --no-link -L +... +hyperion-unikernel> Compiling valence_protocol v0.2.0-alpha.1+mc.1.20.1 (https://github.com/TestingPlant/valence?branch=feat-bytes#fb792dcb) +hyperion-unikernel> Compiling hyperion-unikernel v0.1.0 (/nix/var/nix/builds/nix-50057-576481663/source/bare-metal/hyperion-unikernel) +hyperion-unikernel> Finished `release` profile [optimized] target(s) in 2m 00s +hyperion-unikernel> Running phase: installPhase +$ echo $? +0 +``` + +### `nix run .#bare-metal-vm`, then a Minecraft ping from the host + +The guest console. Note virtio-net negotiating features, DHCP, and the +platform seam reporting the unikernel's capability set rather than the hosted +one: + +``` +[ 0.286342][0][INFO pci ] Virtio network driver initialized. +[ 0.289592][0][INFO network ] Try to initialize network! +[ 0.290297][0][INFO device ] MAC address: 52-54-00-12-34-56 +[ 0.291930][0][INFO device ] ChecksumCapabilities { ipv4: Both, udp: Both, tcp: Both, icmpv4: Both, icmpv6: Both } +[ 0.292994][0][INFO device ] MTU: 1514 bytes +[ 0.302772][0][INFO network ] DHCP config acquired! +[ 0.303104][0][INFO network ] IP address: 192.168.76.9/24 +[ 0.303530][0][INFO network ] Gateway: 192.168.76.2 +[ 0.310927][0][INFO hermit ] Jumping into application +[hyperion] platform: unikernel +[hyperion] capabilities: Capabilities { persistent_storage: false, unix_sockets: false, dns: false, trustworthy_wall_clock: false, adjustable_file_limit: false, subprocesses: false } +[hyperion] parallelism: 2 +[hyperion] wall clock: unavailable on this platform +[hyperion] listening on 0.0.0.0:25565 after 13.166ms +[hyperion] accepted Ok(192.168.76.2:53071) +[hyperion] handshake: protocol=763 host=127.0.0.1 port=25599 next=Status +[hyperion] sent status +[hyperion] ponged 0xdeadbeefcafef00d +``` + +The client's side of the same exchange: + +``` +=== minecraft ping from host, 127.0.0.1:25599 === +STATUS: { + "version": { + "name": "hyperion/unikernel", + "protocol": 763 + }, + "players": { + "max": 10000, + "online": 0, + "sample": [] + }, + "description": { + "text": "hyperion on unikernel, no operating system" + } +} +PONG: 0xdeadbeefcafef00d in 3.6 ms +client rc=0 +``` + +Boot to listening socket: **13 ms**. + +### The same binary on the host + +``` +$ HYPERION_PORT=25599 ./bare-metal/target/debug/hyperion-unikernel +[hyperion] platform: hosted +[hyperion] capabilities: Capabilities { persistent_storage: true, unix_sockets: true, dns: true, trustworthy_wall_clock: true, adjustable_file_limit: true, subprocesses: true } +[hyperion] parallelism: 18 +[hyperion] wall clock: SystemTime { tv_sec: 1785138928, tv_nsec: 86423000 } +[hyperion] listening on 0.0.0.0:25599 after 229.25µs +[hyperion] accepted Ok(127.0.0.1:61665) +[hyperion] handshake: protocol=763 host=127.0.0.1 port=25599 next=Status +[hyperion] sent status +[hyperion] ponged 0xdeadbeefcafef00d +``` + +One source file, two platforms, different and correct answers from the seam. + +### Lints + +``` +$ cargo clippy -p hyperion-platform --all-targets # rc=0 +$ cd bare-metal && cargo clippy # rc=0 +``` + +## What was not verified + +Stated plainly, because these are the parts a reader cannot see for themselves. + +- **`nix build .#default` fails, and already did.** The error is + `A hash was specified for divan-0.1.17, but there is no corresponding git + dependency`. Reproduced on unmodified `313503c` in a detached worktree, so it + predates this branch. It does mean the existing nix path could not be used as + a regression check here. +- **No full `cargo build --workspace` was run** on this branch. `cargo metadata` + resolves and `hyperion-platform` builds and lints clean, but the claim "the + normal build is unchanged" rests on the diff — no existing crate's source was + touched — rather than on a measurement. +- **Linux was not tested.** Everything was built and booted on `aarch64-darwin` + cross-compiling to `x86_64-unknown-hermit`. QEMU ran without KVM, on + `-cpu Skylake-Client`. +- **Only `x86_64-unknown-hermit`.** The aarch64 and riscv64 Hermit triples were + never attempted. +- **The blocker table's second half is unmeasured.** `heed`, `memmap2`, + `libdeflater`, `ndarray+blas`, `tracing-tracy` and `valence_anvil` are + reasoned from their manifests. No build reached them. +- **No login, no gameplay, no second connection.** The demo serves the status + handshake and nothing else, one connection at a time. Nothing was measured + under load, and no packet larger than a status response has crossed the + virtio-net link. +- **Compression is off** in the demo, and the open-coded framing in + `bare-metal/hyperion-unikernel/src/main.rs` is only correct because of that. +- **The Hermit forks were not exercised.** `hermit-os/socket2` and + `hermit-os/tokio` are named as the route to the proxy on the strength of + reading hermit-rs's `[patch.crates-io]` and its example set. Neither was + built here. +- **Reproducibility of the vendor hash is untested** across cargo versions. It + is a fixed-output derivation over `cargo vendor` output, and a toolchain bump + will change it. diff --git a/flake.lock b/flake.lock index b3f043337..6eab6d279 100644 --- a/flake.lock +++ b/flake.lock @@ -19,7 +19,8 @@ "root": { "inputs": { "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" + "rust-overlay": "rust-overlay", + "rust-overlay-bare-metal": "rust-overlay-bare-metal" } }, "rust-overlay": { @@ -41,6 +42,26 @@ "repo": "rust-overlay", "type": "github" } + }, + "rust-overlay-bare-metal": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785131767, + "narHash": "sha256-VNbQv2P0zgaNh96mT4LrnX7hdXgiC5nBH+uvyrrVX7U=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c67ce00525464a710971351c183ce67acb6ca827", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 608027dd8..6b1160a3c 100644 --- a/flake.nix +++ b/flake.nix @@ -7,9 +7,16 @@ url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; }; + # The bare-metal target needs nightly-2026-07-01, which the pinned + # rust-overlay above predates. Kept as a second input so that bumping it + # cannot move the toolchain every normal build uses. + rust-overlay-bare-metal = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; - outputs = { self, nixpkgs, rust-overlay, ... }: + outputs = { self, nixpkgs, rust-overlay, rust-overlay-bare-metal, ... }: let forAllSystems = nixpkgs.lib.genAttrs [ "x86_64-linux" @@ -102,6 +109,21 @@ }; }; }; + # Experimental: hyperion's protocol layer as a unikernel image, with + # no operating system under it. Pinned to the Hermit kernel's own + # nightly, which is not the one the rest of the tree uses; see + # docs/bare-metal.md. + bareMetal = pkgs.callPackage ./nix/bare-metal.nix { + rustToolchain = + (import nixpkgs { + inherit system; + overlays = [ (import rust-overlay-bare-metal) ]; + }).rust-bin.nightly."2026-07-01".default.override { + extensions = [ "rust-src" "llvm-tools" ]; + # The kernel's loader stub is built for x86_64-unknown-none. + targets = [ "x86_64-unknown-none" ]; + }; + }; in { devShells.default = pkgs.mkShell { @@ -113,10 +135,18 @@ default = hyperion; docker-hyperion-proxy = hyperion-proxy-image; docker-bedwars = bedwars-image; + bare-metal = bareMetal.image; + bare-metal-vendor = bareMetal.image.passthru.vendor; + }; + + apps.bare-metal-vm = { + type = "app"; + program = pkgs.lib.getExe bareMetal.vm; }; }; in { + apps = forAllSystems (system: (mkSystem system).apps); devShells = forAllSystems (system: (mkSystem system).devShells); packages = forAllSystems (system: (mkSystem system).packages); }; diff --git a/nix/bare-metal.nix b/nix/bare-metal.nix new file mode 100644 index 000000000..b1d222a78 --- /dev/null +++ b/nix/bare-metal.nix @@ -0,0 +1,196 @@ +# Building a Tier-3 Rust target with a custom sysroot is exactly the kind of +# thing that works on the machine it was invented on and nowhere else, which is +# why it lives here rather than in a shell script. +# +# Three cargo invocations happen inside one build: +# +# 1. the application, from bare-metal/Cargo.lock; +# 2. the standard library, because -Z build-std compiles std from source and +# resolves its own lockfile out of rust-src; +# 3. the Hermit kernel, which the `hermit` crate's build script builds by +# shelling out to a nested cargo in the kernel's source tree. +# +# All three read one vendor directory, produced by a single `cargo vendor +# --sync`, because the third one strips every CARGO_* and RUST_* variable from +# its environment before running and can only be reached through +# $HOME/.cargo/config.toml. +{ + cacert, + fetchFromGitHub, + fetchurl, + git, + lib, + qemu, + rustToolchain, + stdenvNoCC, + writeShellApplication, + writeShellScriptBin, +}: +let + target = "x86_64-unknown-hermit"; + + # Pinned to what the `hermit` crate at tag hermit-0.13.2 carries as its kernel + # submodule. Cargo's vendoring does not follow submodules, so the kernel is + # fetched separately and handed to the build script with HERMIT_MANIFEST_DIR. + kernelSrc = fetchFromGitHub { + owner = "hermit-os"; + repo = "kernel"; + rev = "f51061476ecaa2066779c473a683d8b35b315d9b"; + hash = "sha256-k6vf89oW/69exw8qblFu6v5n/+DK/t5LC+MaMijF+Es="; + }; + + # The loader is what QEMU boots as -kernel; it unpacks the image and jumps + # into it. Taken prebuilt because building it means a fourth toolchain. + loader = fetchurl { + url = "https://github.com/hermit-os/loader/releases/download/v0.5.6/hermit-loader-x86_64"; + hash = "sha256-GF9+yEOhhISqchguynf/PgcMBBqOo18rG0FTCNZz1Uk="; + }; + + rustSrc = "${rustToolchain}/lib/rustlib/src/rust/library"; + + # The kernel's xtask runs `rustup target add x86_64-unknown-none` before + # building. There is no rustup here and the toolchain already carries that + # target, so answer yes and get out of the way. Anything else rustup is asked + # to do is a real gap, so fail loudly rather than silently succeeding. + rustupShim = writeShellScriptBin "rustup" '' + if [ "$1" = "target" ] && [ "$2" = "add" ]; then + exit 0 + fi + echo "nix/bare-metal.nix: unhandled rustup invocation: $*" >&2 + exit 1 + ''; + + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.unions [ + ../bare-metal + ../crates/hyperion-platform + ]; + }; + + # One vendor directory for all three cargo runs. `--sync` is the only way to + # get a single consistent set: vendoring each manifest separately produces + # three `config.toml` fragments that have to be merged by hand, and any + # mistake in the merge shows up as a network fetch in a sandbox. + vendor = stdenvNoCC.mkDerivation { + name = "hyperion-bare-metal-vendor"; + inherit src; + + # A fixed-output derivation has network access but nothing else from the + # host, so the certificate bundle and git have to be named explicitly. + nativeBuildInputs = [ + cacert + git + rustToolchain + ]; + + buildPhase = '' + runHook preBuild + export HOME="$TMPDIR/home" + export SSL_CERT_FILE="${cacert}/etc/ssl/certs/ca-bundle.crt" + # cargo's bundled libgit2 cannot complete a TLS handshake in the sandbox; + # the git binary can. + export CARGO_NET_GIT_FETCH_WITH_CLI=true + mkdir -p "$out" + cargo vendor --locked \ + --manifest-path bare-metal/Cargo.toml \ + --sync ${rustSrc}/Cargo.toml \ + --sync ${kernelSrc}/Cargo.toml \ + --sync ${kernelSrc}/hermit-builtins/Cargo.toml \ + --sync ${kernelSrc}/hermit-macro/Cargo.toml \ + "$out/vendor" > "$out/config.toml" + # A fixed-output derivation may not reference store paths, and cargo writes + # the absolute vendor directory into the config it emits. Leave a + # placeholder for the consumer to fill in. + substituteInPlace "$out/config.toml" --replace-fail "$out/vendor" "@vendor@" + runHook postBuild + ''; + + dontInstall = true; + # patchShebangs would rewrite vendored CI scripts to point at a bash in the + # store, which both adds a reference and invalidates cargo's checksums. + dontFixup = true; + + outputHashAlgo = "sha256"; + outputHashMode = "recursive"; + outputHash = "sha256-bCu5zrjxq78+s5dkFiYMJynosdTKHe1bKf5uWWbTzNk="; + }; + + image = stdenvNoCC.mkDerivation { + pname = "hyperion-unikernel"; + version = "0.1.0"; + inherit src; + + nativeBuildInputs = [ + rustToolchain + rustupShim + ]; + + # The repo's .cargo/config.toml adds -Ctarget-cpu=native to every build, + # which makes any cross-compile emit host-CPU instructions. RUSTFLAGS wins + # over it, so set it here rather than editing a file every other build + # reads. + RUSTFLAGS = ""; + + HERMIT_MANIFEST_DIR = kernelSrc; + + buildPhase = '' + runHook preBuild + + export HOME="$TMPDIR/home" + export CARGO_HOME="$HOME/.cargo" + mkdir -p "$CARGO_HOME" + substitute ${vendor}/config.toml "$CARGO_HOME/config.toml" \ + --replace-fail '@vendor@' '${vendor}/vendor' + + cargo build \ + --offline \ + --manifest-path bare-metal/Cargo.toml \ + --release \ + --target ${target} \ + -Z build-std=std,panic_abort + + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p "$out/share/hyperion" + cp bare-metal/target/${target}/release/hyperion-unikernel "$out/share/hyperion/image" + cp ${loader} "$out/share/hyperion/loader" + runHook postInstall + ''; + + passthru = { inherit kernelSrc loader vendor; }; + + meta = { + description = "hyperion's protocol layer as a Hermit unikernel image"; + platforms = lib.platforms.all; + }; + }; + + # Host port differs from the guest's on purpose: 25565 is usually already + # taken by a real server on a developer's machine, and forwarding onto it + # would silently talk to that instead of the VM. + vm = writeShellApplication { + name = "hyperion-bare-metal-vm"; + runtimeInputs = [ qemu ]; + text = '' + host_port="''${HYPERION_HOST_PORT:-25599}" + echo "booting hyperion unikernel; ping 127.0.0.1:$host_port as a Minecraft server" >&2 + exec qemu-system-x86_64 \ + -display none -serial stdio \ + -kernel ${image}/share/hyperion/loader \ + -initrd ${image}/share/hyperion/image \ + -smp "''${HYPERION_SMP:-2}" -m "''${HYPERION_MEM:-1024M}" \ + -cpu Skylake-Client \ + -device isa-debug-exit,iobase=0xf4,iosize=0x04 \ + -netdev "user,id=net0,hostfwd=tcp::$host_port-:25565,net=192.168.76.0/24,dhcpstart=192.168.76.9" \ + -device virtio-net-pci,netdev=net0,disable-legacy=on,packed=on,mq=on \ + "$@" + ''; + }; +in +{ + inherit image vm; +}