#![allow(clippy::expect_used, clippy::unwrap_used)]
use std::env;
use std::path::PathBuf;
use std::process::Command;
use wit_component::{StringEncoding, embed_component_metadata};
fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
let wasm_runtime_dir = manifest_dir.parent().unwrap().join("eryx-wasm-runtime");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
println!(
"cargo::rerun-if-changed={}",
wasm_runtime_dir.join("src").display()
);
println!(
"cargo::rerun-if-changed={}",
wasm_runtime_dir.join("clock_stubs.c").display()
);
println!(
"cargo::rerun-if-changed={}",
wasm_runtime_dir.join("Cargo.toml").display()
);
println!(
"cargo::rerun-if-changed={}",
manifest_dir.join("wit").display()
);
println!("cargo::rerun-if-env-changed=BUILD_ERYX_RUNTIME");
println!(
"cargo::rerun-if-changed={}",
manifest_dir.join("prebuilt").display()
);
println!("cargo::rerun-if-env-changed=DOCS_RS");
if env::var("DOCS_RS").is_ok() {
if env::var("CARGO_FEATURE_PREINIT").is_ok() {
std::fs::write(out_dir.join("liberyx_runtime.so.zst"), b"")
.expect("failed to write docs.rs placeholder runtime");
std::fs::write(out_dir.join("liberyx_bindings.so.zst"), b"")
.expect("failed to write docs.rs placeholder bindings");
}
return;
}
let build_requested = env::var("BUILD_ERYX_RUNTIME").is_ok();
let preinit = env::var("CARGO_FEATURE_PREINIT").is_ok();
let prebuilt_dir = manifest_dir.join("prebuilt");
let prebuilt_runtime = prebuilt_dir.join("liberyx_runtime.so.zst");
let prebuilt_bindings = prebuilt_dir.join("liberyx_bindings.so.zst");
let has_prebuilt = prebuilt_runtime.exists() && prebuilt_bindings.exists();
let out_runtime_zst = out_dir.join("liberyx_runtime.so.zst");
let out_bindings_zst = out_dir.join("liberyx_bindings.so.zst");
let has_out_artifacts = out_runtime_zst.exists() && out_bindings_zst.exists();
if build_requested {
let runtime_so = build_wasm_runtime(&wasm_runtime_dir);
build_component(&manifest_dir, &runtime_so);
} else if preinit {
if has_prebuilt {
eprintln!("Using pre-built late-linking artifacts from prebuilt/");
std::fs::copy(&prebuilt_runtime, &out_runtime_zst)
.expect("failed to copy prebuilt runtime");
std::fs::copy(&prebuilt_bindings, &out_bindings_zst)
.expect("failed to copy prebuilt bindings");
} else if has_out_artifacts {
eprintln!("Using existing late-linking artifacts from OUT_DIR");
} else {
eprintln!("Building late-linking artifacts from scratch...");
let runtime_so = build_wasm_runtime(&wasm_runtime_dir);
build_component(&manifest_dir, &runtime_so);
}
}
}
fn build_wasm_runtime(wasm_runtime_dir: &PathBuf) -> PathBuf {
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let nested_target_dir = out_dir.join("wasm-runtime-target");
std::fs::create_dir_all(&out_dir).expect("failed to create output directory");
let wasi_sdk = find_wasi_sdk().expect(
"WASI SDK not found. Install with: mise install github:WebAssembly/wasi-sdk@wasi-sdk-27",
);
let clang = wasi_sdk.join("bin/clang");
let sysroot = wasi_sdk.join("share/wasi-sysroot");
eprintln!("Building eryx-wasm-runtime...");
eprintln!(" WASI SDK: {}", wasi_sdk.display());
let mut cmd = Command::new("rustup");
cmd.current_dir(wasm_runtime_dir)
.arg("run")
.arg("nightly")
.arg("cargo")
.arg("build")
.arg("-Z")
.arg("build-std=panic_abort,std")
.arg("--target")
.arg("wasm32-wasip1")
.arg("--release");
for (key, _) in env::vars_os() {
if let Some(key_str) = key.to_str()
&& (key_str.starts_with("RUST") || key_str.starts_with("CARGO"))
{
cmd.env_remove(&key);
}
}
cmd.env("RUSTFLAGS", "-C relocation-model=pic");
cmd.env("CARGO_TARGET_DIR", &nested_target_dir);
let status = cmd
.status()
.expect("failed to run cargo build for eryx-wasm-runtime");
if !status.success() {
panic!("cargo build for eryx-wasm-runtime failed");
}
let staticlib = nested_target_dir.join("wasm32-wasip1/release/liberyx_wasm_runtime.a");
if !staticlib.exists() {
panic!("staticlib not found at {}", staticlib.display());
}
eprintln!("Compiling clock stubs...");
let clock_stubs_o = out_dir.join("clock_stubs.o");
let status = Command::new(&clang)
.arg("--target=wasm32-wasip1")
.arg(format!("--sysroot={}", sysroot.display()))
.arg("-fPIC")
.arg("-c")
.arg(wasm_runtime_dir.join("clock_stubs.c"))
.arg("-o")
.arg(&clock_stubs_o)
.status()
.expect("failed to run clang");
if !status.success() {
panic!("clang failed to compile clock_stubs.c");
}
eprintln!("Linking shared library...");
let runtime_so = out_dir.join("liberyx_runtime.so");
let status = Command::new(&clang)
.arg("--target=wasm32-wasip1")
.arg(format!("--sysroot={}", sysroot.display()))
.arg("-shared")
.arg("-Wl,--allow-undefined")
.arg("-o")
.arg(&runtime_so)
.arg("-Wl,--whole-archive")
.arg(&staticlib)
.arg("-Wl,--no-whole-archive")
.arg(&clock_stubs_o)
.status()
.expect("failed to link shared library");
if !status.success() {
panic!("clang failed to link shared library");
}
let stable_location = wasm_runtime_dir.join("target/liberyx_runtime.so");
std::fs::create_dir_all(wasm_runtime_dir.join("target")).ok();
std::fs::copy(&runtime_so, &stable_location).expect("failed to copy runtime.so");
eprintln!("Built: {}", runtime_so.display());
eprintln!("Copied to: {}", stable_location.display());
println!("cargo::rustc-env=ERYX_RUNTIME_SO={}", runtime_so.display());
let runtime_bytes = std::fs::read(&runtime_so).expect("failed to read runtime.so");
let compressed = zstd::encode_all(runtime_bytes.as_slice(), 19).expect("compress failed");
let compressed_path = out_dir.join("liberyx_runtime.so.zst");
std::fs::write(&compressed_path, &compressed).expect("failed to write compressed runtime");
eprintln!(
"Compressed runtime: {} bytes -> {} bytes",
runtime_bytes.len(),
compressed.len()
);
runtime_so
}
fn decompress_libs(manifest_dir: &std::path::Path) {
let libs_dir = manifest_dir.join("libs");
let decompressed_dir = libs_dir.join("decompressed");
std::fs::create_dir_all(&decompressed_dir).expect("failed to create decompressed dir");
let files = [
"libc.so",
"libc++.so",
"libc++abi.so",
"libpython3.14.so",
"libwasi-emulated-process-clocks.so",
"libwasi-emulated-signal.so",
"libwasi-emulated-mman.so",
"libwasi-emulated-getpid.so",
"wasi_snapshot_preview1.reactor.wasm",
];
for file in &files {
let compressed_path = libs_dir.join(format!("{}.zst", file));
let decompressed_path = decompressed_dir.join(file);
if decompressed_path.exists() {
let compressed_meta = std::fs::metadata(&compressed_path).ok();
let decompressed_meta = std::fs::metadata(&decompressed_path).ok();
if let (Some(c), Some(d)) = (compressed_meta, decompressed_meta)
&& let (Ok(c_time), Ok(d_time)) = (c.modified(), d.modified())
&& d_time >= c_time
{
continue;
}
}
eprintln!("Decompressing {}...", file);
let compressed = std::fs::read(&compressed_path)
.unwrap_or_else(|e| panic!("failed to read {}: {}", compressed_path.display(), e));
let decompressed = zstd::decode_all(compressed.as_slice())
.unwrap_or_else(|e| panic!("failed to decompress {}: {}", file, e));
std::fs::write(&decompressed_path, &decompressed)
.unwrap_or_else(|e| panic!("failed to write {}: {}", decompressed_path.display(), e));
}
}
fn build_component(manifest_dir: &std::path::Path, runtime_so: &std::path::Path) {
eprintln!("Building WASM component...");
decompress_libs(manifest_dir);
let libs_dir = manifest_dir.join("libs/decompressed");
let libc = std::fs::read(libs_dir.join("libc.so")).expect("failed to read libc.so");
let libcxx = std::fs::read(libs_dir.join("libc++.so")).expect("failed to read libc++.so");
let libcxxabi =
std::fs::read(libs_dir.join("libc++abi.so")).expect("failed to read libc++abi.so");
let wasi_clocks = std::fs::read(libs_dir.join("libwasi-emulated-process-clocks.so"))
.expect("failed to read libwasi-emulated-process-clocks.so");
let wasi_signal = std::fs::read(libs_dir.join("libwasi-emulated-signal.so"))
.expect("failed to read libwasi-emulated-signal.so");
let wasi_mman = std::fs::read(libs_dir.join("libwasi-emulated-mman.so"))
.expect("failed to read libwasi-emulated-mman.so");
let wasi_getpid = std::fs::read(libs_dir.join("libwasi-emulated-getpid.so"))
.expect("failed to read libwasi-emulated-getpid.so");
let libpython =
std::fs::read(libs_dir.join("libpython3.14.so")).expect("failed to read libpython3.14.so");
let adapter = std::fs::read(libs_dir.join("wasi_snapshot_preview1.reactor.wasm"))
.expect("failed to read wasi_snapshot_preview1.reactor.wasm");
let runtime = std::fs::read(runtime_so).expect("failed to read liberyx_runtime.so");
let wit_dir = manifest_dir.join("wit");
let mut resolve = wit_parser::Resolve::default();
let (pkg_id, _) = resolve
.push_dir(&wit_dir)
.expect("failed to parse WIT directory");
let world_id = resolve
.select_world(&[pkg_id], Some("sandbox"))
.expect("failed to select world");
let mut opts = wit_dylib::DylibOpts {
interpreter: Some("liberyx_runtime.so".to_string()),
async_: wit_dylib::AsyncFilterSet::default(),
};
let mut bindings = wit_dylib::create(&resolve, world_id, Some(&mut opts));
embed_component_metadata(&mut bindings, &resolve, world_id, StringEncoding::UTF8)
.expect("failed to embed component metadata");
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
let bindings_compressed = zstd::encode_all(bindings.as_slice(), 19).expect("compress failed");
let bindings_path = out_dir.join("liberyx_bindings.so.zst");
std::fs::write(&bindings_path, &bindings_compressed).expect("failed to write bindings");
eprintln!(
"Compressed bindings: {} bytes -> {} bytes",
bindings.len(),
bindings_compressed.len()
);
let linker = wit_component::Linker::default()
.validate(true)
.use_built_in_libdl(true)
.library("libwasi-emulated-process-clocks.so", &wasi_clocks, false)
.expect("failed to add wasi-clocks")
.library("libwasi-emulated-signal.so", &wasi_signal, false)
.expect("failed to add wasi-signal")
.library("libwasi-emulated-mman.so", &wasi_mman, false)
.expect("failed to add wasi-mman")
.library("libwasi-emulated-getpid.so", &wasi_getpid, false)
.expect("failed to add wasi-getpid")
.library("libc.so", &libc, false)
.expect("failed to add libc")
.library("libc++abi.so", &libcxxabi, false)
.expect("failed to add libc++abi")
.library("libc++.so", &libcxx, false)
.expect("failed to add libc++")
.library("libpython3.14.so", &libpython, false)
.expect("failed to add libpython")
.library("liberyx_runtime.so", &runtime, false)
.expect("failed to add eryx runtime")
.library("liberyx_bindings.so", &bindings, false)
.expect("failed to add bindings")
.adapter("wasi_snapshot_preview1", &adapter)
.expect("failed to add WASI adapter");
let component = linker.encode().expect("failed to encode component");
let component_path = manifest_dir.join("runtime.wasm");
std::fs::write(&component_path, &component).expect("failed to write runtime.wasm");
eprintln!(
"Built component: {} ({} bytes)",
component_path.display(),
component.len()
);
println!(
"cargo::rustc-env=ERYX_RUNTIME_WASM={}",
component_path.display()
);
}
fn find_wasi_sdk() -> Option<PathBuf> {
if let Ok(path) = env::var("WASI_SDK_PATH") {
let path = PathBuf::from(path);
if path.join("bin/clang").exists() {
return Some(path);
}
}
if let Ok(output) = Command::new("mise")
.args(["where", "github:WebAssembly/wasi-sdk"])
.output()
&& output.status.success()
{
let path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
if path.join("bin/clang").exists() {
return Some(path);
}
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").ok()?);
let workspace_root = manifest_dir.parent()?.parent()?;
let local_path = workspace_root.join(".wasi-sdk/wasi-sdk-27.0-x86_64-linux");
if local_path.join("bin/clang").exists() {
return Some(local_path);
}
None
}