use std::{
env, fs,
path::{Path, PathBuf},
process::{self},
};
const WASI_SDK_VERSION_MAJOR: usize = 24;
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:?}");
if !archive_path.try_exists().unwrap() {
let file_suffix = match (env::consts::OS, env::consts::ARCH) {
("linux", "x86") | ("linux", "x86_64") => "x86_64-linux",
("macos", "x86") | ("macos", "x86_64") => "x86_64-macos",
("macos", "aarch64") => "arm64-macos",
("windows", "x86") | ("windows", "x86_64") => "x86_64-windows",
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()
.expect("failed to download the WASI SDK with curl");
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"]);
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 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-c-atomics.h",
"quickjs.h",
"cutils.h",
];
let source_files = [
"libregexp.c",
"libunicode.c",
"cutils.c",
"quickjs.c",
"libbf.c",
];
let mut defines: Vec<(String, Option<&str>)> = vec![("_GNU_SOURCE".into(), None)];
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap();
let mut builder = cc::Build::new();
builder
.extra_warnings(false)
.flag_if_supported("-Wno-implicit-const-int-float-conversion")
;
let mut bindgen_cflags = vec![];
if target_os == "windows" {
if target_env == "msvc" {
env::set_var("CFLAGS", "/std:c11 /experimental:c11atomics");
} else {
env::set_var("CFLAGS", "-std=c11");
}
}
if target_os == "wasi" {
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");
if target_os == "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);
bindgen_cflags.push(sysroot);
}
bindgen(
out_dir,
out_dir.join("quickjs.bind.h"),
&defines,
bindgen_cflags,
);
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<str>) -> String {
format!("CARGO_FEATURE_{}", feature_to_define(name))
}
fn feature_to_define(name: impl AsRef<str>) -> String {
name.as_ref().to_uppercase().replace('-', "_")
}
#[cfg(not(feature = "bindgen"))]
fn bindgen<'a, D, H, X, K, V>(out_dir: D, _header_file: H, _defines: X, _add_cflags: Vec<String>)
where
D: AsRef<Path>,
H: AsRef<Path>,
X: IntoIterator<Item = &'a (K, Option<V>)>,
K: AsRef<str> + 'a,
V: AsRef<str> + '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,
env::var("BUILD_TARGET").unwrap_or("n/a".into())
);
}
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<String>)
where
D: AsRef<Path>,
H: AsRef<Path>,
X: IntoIterator<Item = &'a (K, Option<V>)>,
K: AsRef<str> + 'a,
V: AsRef<str> + '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);
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())
});
}
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");
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();
}
}