From 2daf8f30f29d85031e6d0068c1796f2afec534ec Mon Sep 17 00:00:00 2001 From: Steve Biedermann Date: Wed, 7 Oct 2026 21:09:43 +0200 Subject: [PATCH] build: bundle the Leptos UI during Cargo builds --- Cargo.lock | 268 ++++++++++++++++++++++++++++++++++++++------------ Cargo.toml | 5 +- build.rs | 76 ++++++++++++++ src/main.rs | 26 +++-- ui/Cargo.toml | 2 +- ui/build.sh | 5 - 6 files changed, 307 insertions(+), 75 deletions(-) create mode 100644 build.rs delete mode 100755 ui/build.sh diff --git a/Cargo.lock b/Cargo.lock index d41b4f0..da2fb87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index bfd62ed..4c78158 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..87390aa --- /dev/null +++ b/build.rs @@ -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(); + } +} diff --git a/src/main.rs b/src/main.rs index 437db66..6464fae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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#" World Simulation "#; enum Msg { @@ -95,6 +98,18 @@ async fn index() -> Html<&'static str> { Html(INDEX) } +async fn pkg(Path(path): Path) -> 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) -> 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); diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 4d8dba1..6ce087f 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -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"] } diff --git a/ui/build.sh b/ui/build.sh deleted file mode 100755 index 804a2d6..0000000 --- a/ui/build.sh +++ /dev/null @@ -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 "$@"