use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let target = env::var("TARGET").unwrap_or_default();
let is_wasm_target = target.contains("wasm32");
if env::var("CARGO_FEATURE_EMBED_WASM").is_ok() && !is_wasm_target {
let wasm_opt_path = find_wasm_opt();
build_wasm(&out_dir, wasm_opt_path.as_ref());
build_loader(&out_dir);
}
println!("cargo:rustc-env=SOREX_OUT_DIR={out_dir}");
if is_wasm_target {
println!("cargo:rustc-env=SOREX_SKIP_LOADER=1");
}
}
fn find_wasm_opt() -> Option<PathBuf> {
if let Ok(output) = Command::new("which").arg("wasm-opt").output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
}
let homebrew_paths = [
"/opt/homebrew/bin/wasm-opt",
"/usr/local/bin/wasm-opt",
];
for path in homebrew_paths {
if Path::new(path).exists() {
return Some(PathBuf::from(path));
}
}
println!("cargo:warning=wasm-opt not found, skipping WASM optimization");
None
}
fn optimize_wasm(wasm_path: &Path, wasm_opt_path: &Path) {
let size_before = fs::metadata(wasm_path).map(|m| m.len()).unwrap_or(0);
let optimized_path = wasm_path.with_extension("wasm.optimized");
let status = Command::new(wasm_opt_path)
.args(["-O4", "--enable-bulk-memory", "--enable-nontrapping-float-to-int"])
.arg(wasm_path)
.arg("-o")
.arg(&optimized_path)
.status();
match status {
Ok(s) if s.success() => {
if let Err(e) = fs::rename(&optimized_path, wasm_path) {
println!("cargo:warning=Failed to replace WASM: {e}");
} else {
let size_after = fs::metadata(wasm_path).map(|m| m.len()).unwrap_or(0);
println!(
"cargo:warning=WASM optimized: {} -> {} bytes",
size_before, size_after
);
}
}
Ok(_) => {
println!("cargo:warning=wasm-opt failed");
let _ = fs::remove_file(&optimized_path);
}
Err(e) => {
println!("cargo:warning=wasm-opt error: {e}");
let _ = fs::remove_file(&optimized_path);
}
}
}
fn build_wasm(out_dir: &str, wasm_opt_path: Option<&PathBuf>) {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let pkg_dir = Path::new(&manifest_dir).join("target/pkg");
let wasm_dest = Path::new(out_dir).join("sorex_bg.wasm");
let wasm_sources = [
"src/lib.rs",
"src/search.rs",
"src/types.rs",
"src/scoring.rs",
"src/levenshtein.rs",
"src/levenshtein_dfa.rs",
"src/wasm.rs",
];
for src in &wasm_sources {
println!("cargo:rerun-if-changed={}", src);
}
let wasm_src = pkg_dir.join("sorex_bg.wasm");
let needs_rebuild = if !wasm_src.exists() {
println!("cargo:warning=WASM not found, building...");
true
} else {
let wasm_mtime = fs::metadata(&wasm_src)
.and_then(|m| m.modified())
.ok();
let any_source_newer = wasm_sources.iter().any(|src| {
let src_path = Path::new(&manifest_dir).join(src);
if let (Some(wasm_time), Ok(src_meta)) = (wasm_mtime, fs::metadata(&src_path)) {
if let Ok(src_time) = src_meta.modified() {
if src_time > wasm_time {
println!("cargo:warning=Source {} is newer than WASM, rebuilding...", src);
return true;
}
}
}
false
});
any_source_newer
};
if needs_rebuild {
println!("cargo:warning=Building WASM module with wasm-pack...");
let wasm_target_dir = Path::new(out_dir).join("wasm-target");
let temp_build_dir = Path::new(out_dir).join("wasm-build");
let _ = fs::remove_dir_all(&temp_build_dir);
fs::create_dir_all(&temp_build_dir).ok();
let cargo_config_dir = temp_build_dir.join(".cargo");
fs::create_dir_all(&cargo_config_dir).ok();
fs::write(
cargo_config_dir.join("config.toml"),
r#"[target.wasm32-unknown-unknown]
rustflags = [
"-Ctarget-feature=+simd128,+atomics,+bulk-memory",
"-Clink-arg=--shared-memory",
"-Clink-arg=--max-memory=1073741824",
"-Clink-arg=--import-memory",
"-Clink-arg=--export=__wasm_init_tls",
"-Clink-arg=--export=__tls_size",
"-Clink-arg=--export=__tls_align",
"-Clink-arg=--export=__tls_base",
]
[unstable]
build-std = ["panic_abort", "std"]
"#,
)
.expect("Failed to create .cargo/config.toml");
let cargo_toml_path = Path::new(&manifest_dir).join("Cargo.toml");
let original_cargo_toml = fs::read_to_string(&cargo_toml_path)
.expect("Failed to read Cargo.toml");
let mut temp_cargo_toml = original_cargo_toml.clone();
if let Some(ws_start) = temp_cargo_toml.find("[workspace]") {
if let Some(next_section) = temp_cargo_toml[ws_start + 11..].find("\n[") {
temp_cargo_toml = format!(
"{}{}",
&temp_cargo_toml[..ws_start],
&temp_cargo_toml[ws_start + 11 + next_section + 1..]
);
}
}
temp_cargo_toml = temp_cargo_toml.replace(
"serde = { workspace = true }",
r#"serde = { version = "1.0", features = ["derive"] }"#,
);
temp_cargo_toml = temp_cargo_toml.replace(
r#"sorex-lean-macros = { version = "1.0.0", path = "macros", optional = true }"#,
r#"sorex-lean-macros = { version = "1.0.0", optional = true }"#,
);
while let Some(bench_start) = temp_cargo_toml.find("[[bench]]") {
let section_end = temp_cargo_toml[bench_start + 9..]
.find("\n[")
.map(|i| bench_start + 9 + i)
.unwrap_or(temp_cargo_toml.len());
temp_cargo_toml = format!(
"{}{}",
&temp_cargo_toml[..bench_start],
&temp_cargo_toml[section_end..]
);
}
if temp_cargo_toml.contains(r#"crate-type = ["rlib"]"#) {
temp_cargo_toml = temp_cargo_toml.replace(
r#"crate-type = ["rlib"]"#,
r#"crate-type = ["cdylib", "rlib"]"#,
);
}
temp_cargo_toml.push_str("\n[workspace]\n");
fs::write(temp_build_dir.join("Cargo.toml"), &temp_cargo_toml)
.expect("Failed to write temp Cargo.toml");
let src_link = temp_build_dir.join("src");
if !src_link.exists() {
#[cfg(unix)]
std::os::unix::fs::symlink(Path::new(&manifest_dir).join("src"), &src_link).ok();
#[cfg(windows)]
std::os::windows::fs::symlink_dir(Path::new(&manifest_dir).join("src"), &src_link).ok();
}
fs::write(temp_build_dir.join("build.rs"), "fn main() {}").ok();
let mut cmd = Command::new("wasm-pack");
cmd.args([
"build",
"--target",
"web",
"--release",
"--out-dir",
])
.arg(pkg_dir.to_str().unwrap()) .args(["--no-default-features", "--features", "wasm,serde_json"])
.current_dir(&temp_build_dir)
.env("CARGO_TARGET_DIR", &wasm_target_dir);
for (key, _) in env::vars() {
if key.starts_with("CARGO") && key != "CARGO_TARGET_DIR" {
cmd.env_remove(&key);
}
}
let status = cmd
.status()
.expect("Failed to run wasm-pack. Install it with: cargo install wasm-pack");
if !status.success() {
panic!("wasm-pack build failed");
}
if let Some(wasm_opt) = wasm_opt_path {
optimize_wasm(&wasm_src, wasm_opt);
}
}
fs::copy(&wasm_src, &wasm_dest).expect("Failed to copy WASM to OUT_DIR");
}
fn build_loader(out_dir: &str) {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let tools_dir = Path::new(&manifest_dir).join("tools");
let build_script = tools_dir.join("build.ts");
let loader_ts = tools_dir.join("loader.ts");
let pkg_js = Path::new(&manifest_dir).join("target/pkg/sorex.js");
let loader_output = Path::new(&manifest_dir).join("target/loader/sorex.js");
let loader_dest = Path::new(out_dir).join("sorex.js");
println!("cargo:rerun-if-changed=tools/build.ts");
println!("cargo:rerun-if-changed=tools/loader.ts");
println!("cargo:rerun-if-changed=target/pkg/sorex.js");
let needs_rebuild = if !loader_output.exists() {
println!("cargo:warning=Loader not found, building...");
true
} else {
let loader_mtime = fs::metadata(&loader_output)
.and_then(|m| m.modified())
.ok();
let pkg_mtime = fs::metadata(&pkg_js)
.and_then(|m| m.modified())
.ok();
let build_mtime = fs::metadata(&build_script)
.and_then(|m| m.modified())
.ok();
let loader_ts_mtime = fs::metadata(&loader_ts)
.and_then(|m| m.modified())
.ok();
match (loader_mtime, pkg_mtime, build_mtime, loader_ts_mtime) {
(Some(loader), Some(pkg), Some(build), Some(ts)) => {
if pkg > loader {
println!("cargo:warning=target/pkg/sorex.js is newer than loader, rebuilding...");
true
} else if build > loader {
println!("cargo:warning=tools/build.ts is newer than loader, rebuilding...");
true
} else if ts > loader {
println!("cargo:warning=tools/loader.ts is newer than loader, rebuilding...");
true
} else {
false
}
}
_ => true,
}
};
if needs_rebuild {
println!("cargo:warning=Building JavaScript loader with deno...");
let status = Command::new("deno")
.args(["task", "build"])
.current_dir(&tools_dir)
.status()
.expect("Failed to run deno. Install it from: https://deno.land");
if !status.success() {
panic!("deno task build failed");
}
}
fs::copy(&loader_output, &loader_dest).expect("Failed to copy loader JS to OUT_DIR");
}