use std::{
env, fs,
io::Write,
path::{Path, PathBuf},
process::{self, Command, Stdio},
};
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:?}");
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"]);
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 mut defines = vec![
("_GNU_SOURCE".into(), None),
("CONFIG_VERSION".into(), Some("\"2020-01-19\"")),
("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" {
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");
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);
}
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")
;
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('-', "_")
}
fn patch<D: AsRef<Path>, P: AsRef<Path>>(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<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
);
}
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())
});
}
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");
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();
}
}