use std::{ env, fs, io::Write, path::{Path, PathBuf}, process::{self, Command, Stdio}, }; // WASI logic lifted from https://github.com/bytecodealliance/javy/blob/61616e1507d2bf896f46dc8d72687273438b58b2/crates/quickjs-wasm-sys/build.rs#L18 const WASI_SDK_VERSION_MAJOR: usize = 20; const WASI_SDK_VERSION_MINOR: usize = 0; fn download_wasi_sdk() -> PathBuf { let mut wasi_sdk_dir: PathBuf = env::var("OUT_DIR").unwrap().into(); wasi_sdk_dir.push("wasi-sdk"); fs::create_dir_all(&wasi_sdk_dir).unwrap(); let major_version = WASI_SDK_VERSION_MAJOR; let minor_version = WASI_SDK_VERSION_MINOR; let mut archive_path = wasi_sdk_dir.clone(); archive_path.push(format!("wasi-sdk-{major_version}-{minor_version}.tar.gz")); println!("SDK tar: {archive_path:?}"); // Download archive if necessary if !archive_path.try_exists().unwrap() { let file_suffix = match (env::consts::OS, env::consts::ARCH) { ("linux", "x86") | ("linux", "x86_64") => "linux", ("macos", "x86") | ("macos", "x86_64") | ("macos", "aarch64") => "macos", ("windows", "x86") => "mingw-x86", ("windows", "x86_64") => "mingw", other => panic!("Unsupported platform tuple {:?}", other), }; let uri = format!("https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-{major_version}/wasi-sdk-{major_version}.{minor_version}-{file_suffix}.tar.gz"); println!("Downloading WASI SDK archive from {uri} to {archive_path:?}"); let output = process::Command::new("curl") .args([ "--location", "-o", archive_path.to_string_lossy().as_ref(), uri.as_ref(), ]) .output() .unwrap(); println!("curl output: {}", String::from_utf8_lossy(&output.stdout)); println!("curl err: {}", String::from_utf8_lossy(&output.stderr)); if !output.status.success() { panic!( "curl WASI SDK failed: {}", String::from_utf8_lossy(&output.stderr) ); } } let mut test_binary = wasi_sdk_dir.clone(); test_binary.extend(["bin", "wasm-ld"]); // Extract archive if necessary if !test_binary.try_exists().unwrap() { println!("Extracting WASI SDK archive {archive_path:?}"); let output = process::Command::new("tar") .args([ "-zxf", archive_path.to_string_lossy().as_ref(), "--strip-components", "1", ]) .current_dir(&wasi_sdk_dir) .output() .unwrap(); if !output.status.success() { panic!( "Unpacking WASI SDK failed: {}", String::from_utf8_lossy(&output.stderr) ); } } wasi_sdk_dir } fn get_wasi_sdk_path() -> PathBuf { std::env::var_os("WASI_SDK") .map(PathBuf::from) .unwrap_or_else(download_wasi_sdk) } fn main() { #[cfg(feature = "logging")] pretty_env_logger::init(); let features = [ "bindgen", "update-bindings", "dump-bytecode", "dump-gc", "dump-gc-free", "dump-free", "dump-leaks", "dump-mem", "dump-objects", "dump-atoms", "dump-shapes", "dump-module-resolve", "dump-promise", "dump-read-object", ]; println!("cargo:rerun-if-changed=build.rs"); for feature in &features { println!("cargo:rerun-if-env-changed={}", feature_to_cargo(feature)); } let src_dir = Path::new("quickjs"); let patches_dir = Path::new("patches"); let out_dir = env::var("OUT_DIR").expect("No OUT_DIR env var is set by cargo"); let out_dir = Path::new(&out_dir); let header_files = [ "libbf.h", "libregexp-opcode.h", "libregexp.h", "libunicode-table.h", "libunicode.h", "list.h", "quickjs-atom.h", "quickjs-opcode.h", "quickjs.h", "cutils.h", ]; let source_files = [ "libregexp.c", "libunicode.c", "cutils.c", "quickjs.c", "libbf.c", ]; let mut patch_files = vec![ "error_column_number.patch", "get_function_proto.patch", "check_stack_overflow.patch", "infinity_handling.patch", ]; let version = fs::read_to_string(src_dir.join("VERSION")).expect("failed to read quickjs VERSION file"); let version = format!("\"{}\"", version.trim()); let mut defines = vec![ ("_GNU_SOURCE".into(), None), ("CONFIG_VERSION".into(), Some(version.as_str())), ("CONFIG_BIGNUM".into(), None), ]; if env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" && env::var("CARGO_CFG_TARGET_ENV").unwrap() == "msvc" { patch_files.push("basic_msvc_compat.patch"); } for feature in &features { if feature.starts_with("dump-") && env::var(feature_to_cargo(feature)).is_ok() { defines.push((feature_to_define(feature), None)); } } if env::var("CARGO_CFG_TARGET_OS").unwrap() == "wasi" { // pretend we're emscripten - there are already ifdefs that match // also, wasi doesn't ahve FE_DOWNWARD or FE_UPWARD defines.push(("EMSCRIPTEN".into(), Some("1"))); defines.push(("FE_DOWNWARD".into(), Some("0"))); defines.push(("FE_UPWARD".into(), Some("0"))); } for file in source_files.iter().chain(header_files.iter()) { fs::copy(src_dir.join(file), out_dir.join(file)) .expect("Unable to copy source; try 'git submodule update --init'"); } fs::copy("quickjs.bind.h", out_dir.join("quickjs.bind.h")).expect("Unable to copy source"); // applying patches for file in &patch_files { patch(out_dir, patches_dir.join(file)); } let mut add_cflags = vec![]; if env::var("CARGO_CFG_TARGET_OS").unwrap() == "wasi" { let wasi_sdk_path = get_wasi_sdk_path(); if !wasi_sdk_path.try_exists().unwrap() { panic!( "wasi-sdk not installed in specified path of {}", wasi_sdk_path.display() ); } env::set_var("CC", wasi_sdk_path.join("bin/clang").to_str().unwrap()); env::set_var("AR", wasi_sdk_path.join("bin/ar").to_str().unwrap()); let sysroot = format!( "--sysroot={}", wasi_sdk_path.join("share/wasi-sysroot").display() ); env::set_var("CFLAGS", &sysroot); add_cflags.push(sysroot); } // generating bindings bindgen( out_dir, out_dir.join("quickjs.bind.h"), &defines, add_cflags, ); let mut builder = cc::Build::new(); builder .extra_warnings(false) .flag_if_supported("-Wno-implicit-const-int-float-conversion") //.flag("-Wno-array-bounds") //.flag("-Wno-format-truncation") ; for (name, value) in &defines { builder.define(name, *value); } for src in &source_files { builder.file(out_dir.join(src)); } builder.compile("libquickjs.a"); } fn feature_to_cargo(name: impl AsRef) -> String { format!("CARGO_FEATURE_{}", feature_to_define(name)) } fn feature_to_define(name: impl AsRef) -> String { name.as_ref().to_uppercase().replace('-', "_") } fn patch, P: AsRef>(out_dir: D, patch: P) { let mut child = Command::new("patch") .args(["-p1", "-f"]) .stdin(Stdio::piped()) .current_dir(out_dir) .spawn() .expect("Unable to execute patch, you may need to install it: {}"); println!("Applying patch {}", patch.as_ref().display()); { let patch = fs::read(patch).expect("Unable to read patch"); let stdin = child.stdin.as_mut().unwrap(); stdin.write_all(&patch).expect("Unable to apply patch"); } child.wait_with_output().expect("Unable to apply patch"); } #[cfg(not(feature = "bindgen"))] fn bindgen<'a, D, H, X, K, V>(out_dir: D, _header_file: H, _defines: X, _add_cflags: Vec) where D: AsRef, H: AsRef, X: IntoIterator)>, K: AsRef + 'a, V: AsRef + 'a, { let target = env::var("TARGET").unwrap(); if !Path::new("./") .join("src") .join("bindings") .join(format!("{}.rs", target)) .canonicalize() .map(|x| x.exists()) .unwrap_or(false) { println!( "cargo:warning=rquickjs probably doesn't ship bindings for platform `{}`. try the `bindgen` feature instead.", target ); } let bindings_file = out_dir.as_ref().join("bindings.rs"); fs::write( bindings_file, format!( r#"macro_rules! bindings_env {{ ("TARGET") => {{ "{target}" }}; }}"# ), ) .unwrap(); } #[cfg(feature = "bindgen")] fn bindgen<'a, D, H, X, K, V>(out_dir: D, header_file: H, defines: X, mut add_cflags: Vec) where D: AsRef, H: AsRef, X: IntoIterator)>, K: AsRef + 'a, V: AsRef + 'a, { let target = env::var("TARGET").unwrap(); let out_dir = out_dir.as_ref(); let header_file = header_file.as_ref(); let mut cflags = vec![format!("--target={}", target)]; cflags.append(&mut add_cflags); //format!("-I{}", out_dir.parent().display()), for (name, value) in defines { cflags.push(if let Some(value) = value { format!("-D{}={}", name.as_ref(), value.as_ref()) } else { format!("-D{}", name.as_ref()) }); } println!("Bindings for target: {}", target); let mut builder = bindgen_rs::Builder::default() .detect_include_paths(true) .clang_arg("-xc") .clang_arg("-v") .clang_args(cflags) .size_t_is_usize(false) .header(header_file.display().to_string()) .allowlist_type("JS.*") .allowlist_function("js.*") .allowlist_function("JS.*") .allowlist_function("__JS.*") .allowlist_var("JS.*") .opaque_type("FILE") .blocklist_type("FILE") .blocklist_function("JS_DumpMemoryUsage"); if env::var("CARGO_CFG_TARGET_OS").unwrap() == "wasi" { builder = builder.clang_arg("-fvisibility=default"); } let bindings = builder.generate().expect("Unable to generate bindings"); let bindings_file = out_dir.join("bindings.rs"); bindings .write_to_file(&bindings_file) .expect("Couldn't write bindings"); // Special case to support bundled bindings if env::var("CARGO_FEATURE_UPDATE_BINDINGS").is_ok() { let dest_dir = Path::new("src").join("bindings"); fs::create_dir_all(&dest_dir).unwrap(); let dest_file = format!("{}.rs", target); fs::copy(&bindings_file, dest_dir.join(dest_file)).unwrap(); } }