build: bundle the Leptos UI during Cargo builds

This commit is contained in:
Steve Biedermann 2026-10-07 21:09:43 +02:00
parent ef47209c79
commit 2daf8f30f2
6 changed files with 307 additions and 75 deletions

268
Cargo.lock generated
View File

@ -350,6 +350,31 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "crossbeam-deque"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
[[package]]
name = "crypto-common"
version = "0.1.7"
@ -452,12 +477,24 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "find-msvc-tools"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
@ -605,6 +642,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
dependencies = [
"fallible-iterator",
"indexmap",
"stable_deref_trait",
]
[[package]]
name = "gloo-net"
version = "0.6.0"
@ -657,12 +705,29 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e2ac29387b1aa07a1e448f7bb4f35b500787971e965b02842b900afa5c8f6f"
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash",
"serde",
"serde_core",
]
[[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 = "html-escape"
version = "0.2.15"
@ -702,12 +767,6 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range-header"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
[[package]]
name = "httparse"
version = "1.10.1"
@ -850,6 +909,15 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
dependencies = [
"rayon",
]
[[package]]
name = "idna"
version = "1.1.0"
@ -871,6 +939,25 @@ dependencies = [
"icu_properties",
]
[[package]]
name = "include_dir"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
dependencies = [
"include_dir_macros",
]
[[package]]
name = "include_dir_macros"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "indexmap"
version = "2.14.2"
@ -878,7 +965,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855"
dependencies = [
"equivalent",
"hashbrown",
"hashbrown 0.17.1",
"serde",
"serde_core",
]
[[package]]
@ -934,6 +1023,18 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
[[package]]
name = "leb128"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c83bff1d572d6b9aeef67ddfc8448e4a3737909cb28e81f97c791b9018703e52"
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "leptos"
version = "0.8.22"
@ -1144,16 +1245,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "1.2.4"
@ -1498,6 +1589,26 @@ dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rayon"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "reactive_graph"
version = "0.2.15"
@ -1687,6 +1798,12 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "rustc-demangle"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb"
[[package]]
name = "rustc-hash"
version = "2.1.3"
@ -1930,9 +2047,10 @@ name = "sim"
version = "0.1.0"
dependencies = [
"axum",
"include_dir",
"rune",
"tokio",
"tower-http",
"wasm-bindgen-cli-support",
]
[[package]]
@ -2170,19 +2288,6 @@ dependencies = [
"tungstenite",
]
[[package]]
name = "tokio-util"
version = "0.7.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52"
dependencies = [
"bytes",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "1.1.6+spec-1.1.0"
@ -2238,31 +2343,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags",
"bytes",
"futures-core",
"futures-util",
"http",
"http-body",
"http-body-util",
"http-range-header",
"httpdate",
"mime",
"mime_guess",
"percent-encoding",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@ -2389,12 +2469,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "unicase"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f"
[[package]]
name = "unicode-ident"
version = "1.0.26"
@ -2470,6 +2544,35 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "walrus"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e124668d1bce62ae1d2183c1e85da2b08aa53f36baa3b975427fa6f0ec2fb1e8"
dependencies = [
"anyhow",
"gimli",
"id-arena",
"leb128",
"log",
"rayon",
"walrus-macro",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "walrus-macro"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a9b0525d7ea6e5f906aca581a172e5c91b4c595290dfa8ad4a2bc9ffef33b44"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@ -2498,6 +2601,24 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-cli-support"
version = "0.2.129"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0724352b96c7779dcfd3f580b9e7e29beac8f4229c9a27cd2ea53e04f1cc65a"
dependencies = [
"anyhow",
"base64",
"leb128",
"log",
"rustc-demangle",
"serde",
"serde_json",
"walrus",
"wasm-bindgen-shared",
"wasmparser",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.79"
@ -2541,6 +2662,16 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.245.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-streams"
version = "0.5.0"
@ -2576,6 +2707,19 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "wasmparser"
version = "0.245.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e"
dependencies = [
"bitflags",
"hashbrown 0.16.1",
"indexmap",
"semver",
"serde",
]
[[package]]
name = "web-sys"
version = "0.3.106"

View File

@ -10,4 +10,7 @@ members = ["ui"]
rune = "0.14.2"
axum = { version = "0.8", features = ["ws"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "sync", "time"] }
tower-http = { version = "0.6", features = ["fs"] }
include_dir = "0.7"
[build-dependencies]
wasm-bindgen-cli-support = "=0.2.129"

76
build.rs Normal file
View File

@ -0,0 +1,76 @@
// Builds the Leptos UI (ui/) to wasm and generates the JS bindings, so a plain `cargo run` works.
// The result lands in OUT_DIR/ui-pkg and is embedded into the binary by src/main.rs.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
fn main() {
for p in ["ui/src", "ui/js", "ui/style.css", "ui/Cargo.toml"] {
println!("cargo:rerun-if-changed={p}");
}
let out = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
let pkg = out.join("ui-pkg");
let release = env::var("PROFILE").is_ok_and(|p| p == "release");
ensure_wasm_target();
// A separate target dir avoids deadlocking on the outer build's lock.
let target_dir = out.join("ui-target");
let mut cargo = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
cargo
.args([
"build",
"--manifest-path",
"ui/Cargo.toml",
"--target",
"wasm32-unknown-unknown",
])
.env("CARGO_TARGET_DIR", &target_dir)
// Host rustflags (e.g. -fuse-ld=lld) break the wasm link.
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("RUSTFLAGS");
if release {
cargo.arg("--release");
}
let status = cargo
.status()
.expect("failed to run cargo for the ui crate");
if !status.success() {
panic!(
"building ui/ failed (see the cargo output above); the wasm32-unknown-unknown target must be installed: rustup target add wasm32-unknown-unknown"
);
}
let profile = if release { "release" } else { "debug" };
let wasm = target_dir
.join("wasm32-unknown-unknown")
.join(profile)
.join("ui.wasm");
let _ = fs::remove_dir_all(&pkg);
wasm_bindgen_cli_support::Bindgen::new()
.input_path(&wasm)
.web(true)
.expect("bindgen target")
.out_name("ui")
.typescript(false)
.generate(&pkg)
.expect("wasm-bindgen failed");
}
fn ensure_wasm_target() {
let rustc = env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
let sysroot = Command::new(rustc)
.args(["--print", "sysroot"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| PathBuf::from(s.trim()));
let installed = sysroot
.as_deref()
.is_some_and(|s: &Path| s.join("lib/rustlib/wasm32-unknown-unknown").exists());
if !installed {
let _ = Command::new("rustup")
.args(["target", "add", "wasm32-unknown-unknown"])
.status();
}
}

View File

@ -2,23 +2,26 @@ mod world;
use axum::Router;
use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::extract::{DefaultBodyLimit, Request, State};
use axum::extract::{DefaultBodyLimit, Path, Request, State};
use axum::http::{StatusCode, header};
use axum::middleware::{self, Next};
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use include_dir::{Dir, include_dir};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use tokio::sync::{mpsc, oneshot, watch};
use tower_http::services::ServeDir;
use world::{DEFAULT_SCRIPT, World};
// The UI bundle that build.rs produced from ui/.
static UI: Dir = include_dir!("$OUT_DIR/ui-pkg");
// Minimal shell; the whole UI is the Leptos app in ui/ compiled to wasm.
const INDEX: &str = r#"<!doctype html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>World Simulation</title></head>
<body style="background:#111;color:#ddd;font:13px monospace"><script type="module">
try { const m = await import('/pkg/ui.js'); await m.default(); }
catch (e) { document.body.textContent = 'Failed to load the UI (did you run ui/build.sh?): ' + e; }
try { const m = await import('/pkg/ui.js'); await m.default({ module_or_path: '/pkg/ui_bg.wasm' }); }
catch (e) { document.body.textContent = 'Failed to load the UI: ' + e; }
</script></body></html>"#;
enum Msg {
@ -95,6 +98,18 @@ async fn index() -> Html<&'static str> {
Html(INDEX)
}
async fn pkg(Path(path): Path<String>) -> Response {
let Some(file) = UI.get_file(&path) else {
return StatusCode::NOT_FOUND.into_response();
};
let ctype = match path.rsplit('.').next() {
Some("js") => "text/javascript",
Some("wasm") => "application/wasm",
_ => "application/octet-stream",
};
([(header::CONTENT_TYPE, ctype)], file.contents()).into_response()
}
async fn terrain(State(app): State<App>) -> impl IntoResponse {
json(app.terrain.lock().unwrap().clone())
}
@ -164,7 +179,6 @@ async fn main() {
.and_then(|s| s.parse().ok())
.unwrap_or(42u64);
let addr = std::env::var("SIM_ADDR").unwrap_or_else(|_| "127.0.0.1:7878".into());
let ui_dir = std::env::var("SIM_UI_DIR").unwrap_or_else(|_| "ui/pkg".into());
let terrain_buf = Arc::new(Mutex::new(String::new()));
let script = Arc::new(Mutex::new(DEFAULT_SCRIPT.to_string()));
@ -188,7 +202,7 @@ async fn main() {
.route("/api/script", get(get_script).post(post_script))
.route("/api/edit", post(post_edit))
.route("/ws", get(ws))
.nest_service("/pkg", ServeDir::new(ui_dir))
.route("/pkg/{*path}", get(pkg))
.layer(DefaultBodyLimit::max(65536))
.layer(middleware::from_fn(same_origin))
.with_state(app);

View File

@ -8,7 +8,7 @@ crate-type = ["cdylib"]
[dependencies]
leptos = { version = "0.8", features = ["csr"] }
wasm-bindgen = "0.2"
wasm-bindgen = "=0.2.129"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = ["CanvasRenderingContext2d", "HtmlCanvasElement", "HtmlElement", "MouseEvent", "WebSocket", "MessageEvent", "Location", "Element", "DomRect", "HtmlDivElement", "Window", "Storage"] }

View File

@ -1,5 +0,0 @@
#!/bin/sh
# Builds the Leptos UI into ui/pkg, which the sim server serves at /pkg/.
set -e
cd "$(dirname "$0")"
wasm-pack build --target web --out-dir pkg --no-typescript "$@"