tree-sitter/xtask/src/build_wasm.rs
Will Lillis d0e0e0322c
fix(xtask): properly trim emscripten version at the source (#4402)
* fix(xtask): properly trim emscripten version at the source

* fix(loader): trim emscripten version in `cargo:rustc-env=` print
2025-04-27 20:59:04 -07:00

258 lines
7.6 KiB
Rust

use std::{
collections::HashSet,
ffi::{OsStr, OsString},
fmt::Write,
fs,
path::PathBuf,
process::Command,
time::Duration,
};
use anyhow::{anyhow, Result};
use notify::{
event::{AccessKind, AccessMode},
EventKind, RecursiveMode,
};
use notify_debouncer_full::new_debouncer;
use crate::{bail_on_err, watch_wasm, BuildWasm, EMSCRIPTEN_TAG};
#[derive(PartialEq, Eq)]
enum EmccSource {
Native,
Docker,
Podman,
}
const EXPORTED_RUNTIME_METHODS: [&str; 8] = [
"AsciiToString",
"stringToUTF8",
"UTF8ToString",
"lengthBytesUTF8",
"stringToUTF16",
"loadWebAssemblyModule",
"getValue",
"setValue",
];
pub fn run_wasm(args: &BuildWasm) -> Result<()> {
let mut emscripten_flags = vec!["-O3", "--minify", "0"];
if args.debug {
emscripten_flags.extend(["-s", "ASSERTIONS=1", "-s", "SAFE_HEAP=1", "-O0", "-g"]);
}
if args.verbose {
emscripten_flags.extend(["-s", "VERBOSE=1", "-v"]);
}
let emcc_name = if cfg!(windows) { "emcc.bat" } else { "emcc" };
// Order of preference: emscripten > docker > podman > error
let source = if !args.docker && Command::new(emcc_name).output().is_ok() {
EmccSource::Native
} else if Command::new("docker")
.output()
.is_ok_and(|out| out.status.success())
{
EmccSource::Docker
} else if Command::new("podman")
.arg("--version")
.output()
.is_ok_and(|out| out.status.success())
{
EmccSource::Podman
} else {
return Err(anyhow!(
"You must have either emcc, docker, or podman on your PATH to run this command"
));
};
let mut command = match source {
EmccSource::Native => Command::new(emcc_name),
EmccSource::Docker | EmccSource::Podman => {
let mut command = match source {
EmccSource::Docker => Command::new("docker"),
EmccSource::Podman => Command::new("podman"),
_ => unreachable!(),
};
command.args(["run", "--rm"]);
// Mount the root directory as a volume, which is the repo root
let mut volume_string = OsString::from(std::env::current_dir().unwrap());
volume_string.push(":/src:Z");
command.args([OsStr::new("--volume"), &volume_string]);
// In case `docker` is an alias to `podman`, ensure that podman
// mounts the current directory as writable by the container
// user which has the same uid as the host user. Setting the
// podman-specific variable is more reliable than attempting to
// detect whether `docker` is an alias for `podman`.
// see https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode
command.env("PODMAN_USERNS", "keep-id");
// Get the current user id so that files created in the docker container will have
// the same owner.
#[cfg(unix)]
{
#[link(name = "c")]
extern "C" {
fn getuid() -> u32;
}
// don't need to set user for podman since PODMAN_USERNS=keep-id is already set
if source == EmccSource::Docker {
let user_id = unsafe { getuid() };
command.args(["--user", &user_id.to_string()]);
}
};
// Run `emcc` in a container using the `emscripten-slim` image
command.args([EMSCRIPTEN_TAG, "emcc"]);
command
}
};
fs::create_dir_all("target/scratch").unwrap();
let exported_functions = format!(
"{}{}",
fs::read_to_string("lib/src/wasm/stdlib-symbols.txt")?,
fs::read_to_string("lib/binding_web/lib/exports.txt")?
)
.replace('"', "")
.lines()
.fold(String::new(), |mut output, line| {
let _ = write!(output, "_{line}");
output
})
.trim_end_matches(',')
.to_string();
let exported_functions = format!("EXPORTED_FUNCTIONS={exported_functions}");
let exported_runtime_methods = format!(
"EXPORTED_RUNTIME_METHODS={}",
EXPORTED_RUNTIME_METHODS.join(",")
);
// Clean up old files from prior runs
for file in [
"web-tree-sitter.mjs",
"web-tree-sitter.cjs",
"web-tree-sitter.wasm",
"web-tree-sitter.wasm.map",
] {
fs::remove_file(PathBuf::from("lib/binding_web/lib").join(file)).ok();
}
if !args.cjs {
emscripten_flags.extend(["-s", "EXPORT_ES6=1"]);
}
#[rustfmt::skip]
emscripten_flags.extend([
"-gsource-map",
"--source-map-base", ".",
"-fno-exceptions",
"-std=c11",
"-s", "WASM=1",
"-s", "MODULARIZE=1",
"-s", "INITIAL_MEMORY=33554432",
"-s", "ALLOW_MEMORY_GROWTH=1",
"-s", "SUPPORT_BIG_ENDIAN=1",
"-s", "MAIN_MODULE=2",
"-s", "FILESYSTEM=0",
"-s", "NODEJS_CATCH_EXIT=0",
"-s", "NODEJS_CATCH_REJECTION=0",
"-s", &exported_functions,
"-s", &exported_runtime_methods,
"-D", "fprintf(...)=",
"-D", "NDEBUG=",
"-D", "_POSIX_C_SOURCE=200112L",
"-D", "_DEFAULT_SOURCE=",
"-I", "lib/src",
"-I", "lib/include",
"--js-library", "lib/binding_web/lib/imports.js",
"--pre-js", "lib/binding_web/lib/prefix.js",
"-o", if args.cjs { "lib/binding_web/lib/web-tree-sitter.cjs" } else { "lib/binding_web/lib/web-tree-sitter.mjs" },
"lib/src/lib.c",
"lib/binding_web/lib/tree-sitter.c",
]);
if args.emit_tsd {
emscripten_flags.extend(["--emit-tsd", "web-tree-sitter.d.ts"]);
}
let command = command.args(&emscripten_flags);
if args.watch {
watch_wasm!(|| build_wasm(command));
} else {
build_wasm(command)?;
}
Ok(())
}
fn build_wasm(cmd: &mut Command) -> Result<()> {
bail_on_err(
&cmd.spawn()?.wait_with_output()?,
"Failed to compile the Tree-sitter WASM library",
)?;
Ok(())
}
pub fn run_wasm_stdlib() -> Result<()> {
let export_flags = include_str!("../../lib/src/wasm/stdlib-symbols.txt")
.lines()
.map(|line| format!("-Wl,--export={}", &line[1..line.len() - 2]))
.collect::<Vec<String>>();
let mut command = Command::new("docker");
let output = command
.args([
"run",
"--rm",
"-v",
format!("{}:/src", std::env::current_dir().unwrap().display()).as_str(),
"-w",
"/src",
"ghcr.io/webassembly/wasi-sdk",
"/opt/wasi-sdk/bin/clang",
"-o",
"stdlib.wasm",
"-Os",
"-fPIC",
"-Wl,--no-entry",
"-Wl,--stack-first",
"-Wl,-z",
"-Wl,stack-size=65536",
"-Wl,--import-undefined",
"-Wl,--import-memory",
"-Wl,--import-table",
"-Wl,--strip-debug",
"-Wl,--export=reset_heap",
"-Wl,--export=__wasm_call_ctors",
"-Wl,--export=__stack_pointer",
])
.args(export_flags)
.arg("lib/src/wasm/stdlib.c")
.output()?;
bail_on_err(&output, "Failed to compile the Tree-sitter WASM stdlib")?;
let xxd = Command::new("xxd")
.args(["-C", "-i", "stdlib.wasm"])
.output()?;
bail_on_err(
&xxd,
"Failed to run xxd on the compiled Tree-sitter WASM stdlib",
)?;
fs::write("lib/src/wasm/wasm-stdlib.h", xxd.stdout)?;
fs::rename("stdlib.wasm", "target/stdlib.wasm")?;
Ok(())
}