feat(xtask): check wasm exports

This commit is contained in:
WillLillis 2025-01-04 23:24:09 -05:00 committed by Amaan Qureshi
parent aea3a4720a
commit dcdd6ce2d2
3 changed files with 167 additions and 0 deletions

36
.github/workflows/wasm_exports.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Check WASM Exports
on:
pull_request:
paths:
- lib/include/tree_sitter/api.h
- lib/binding_web/**
push:
branches: [master]
paths:
- lib/include/tree_sitter/api.h
- lib/binding_rust/bindings.rs
- lib/CMakeLists.txt
jobs:
check-wasm-exports:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up stable Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Install wasm-objdump
run: sudo apt-get update -y && sudo apt-get install -y wabt
- name: Build C library (make)
run: make -j CFLAGS="$CFLAGS"
env:
CFLAGS: -g -Werror -Wall -Wextra -Wshadow -Wpedantic -Werror=incompatible-pointer-types
- name: Check WASM exports
run: cargo xtask check-wasm-exports

View file

@ -0,0 +1,127 @@
use std::{
collections::HashSet,
io::BufRead,
process::{Command, Stdio},
};
use anyhow::{anyhow, Result};
use crate::{bail_on_err, build_wasm::run_wasm, BuildWasm};
const EXCLUDES: [&str; 28] = [
// Unneeded because the JS side has its own way of implementing it
"ts_node_child_by_field_name",
"ts_node_edit",
// Precomputed and stored in the JS side
"ts_node_type",
"ts_node_grammar_type",
"ts_node_eq",
"ts_tree_cursor_current_field_name",
"ts_lookahead_iterator_current_symbol_name",
// Deprecated
"ts_node_child_containing_descendant",
// Not used in wasm
"ts_init",
"ts_set_allocator",
"ts_parser_set_cancellation_flag",
"ts_parser_cancellation_flag",
"ts_parser_print_dot_graphs",
"ts_tree_print_dot_graph",
"ts_parser_set_wasm_store",
"ts_parser_take_wasm_store",
"ts_parser_language",
"ts_node_language",
"ts_tree_language",
"ts_lookahead_iterator_language",
"ts_parser_logger",
"ts_parser_parse_string",
"ts_parser_parse_string_encoding",
// Query cursor is not managed by user in web bindings
"ts_query_cursor_delete",
"ts_query_cursor_timeout_micros",
"ts_query_cursor_match_limit",
"ts_query_cursor_remove_match",
"ts_query_cursor_timeout_micros",
];
pub fn run() -> Result<()> {
// Build the wasm module with debug symbols for wasm-objdump
run_wasm(&BuildWasm {
debug: true,
verbose: false,
docker: false,
})?;
let mut wasm_exports = include_str!("../../lib/binding_web/exports.txt")
.lines()
.map(|s| s.replace("_wasm", "").replace("byte", "index"))
// remove leading and trailing quotes, trailing comma
.map(|s| s[1..s.len() - 2].to_string())
.collect::<HashSet<_>>();
// Run wasm-objdump to see symbols used internally in binding.c but not exposed in any way.
let wasm_objdump = Command::new("wasm-objdump")
.args([
"--details",
"lib/binding_web/tree-sitter.wasm",
"--section",
"Name",
])
.output()
.expect("Failed to run wasm-objdump");
bail_on_err(&wasm_objdump, "Failed to run wasm-objdump")?;
wasm_exports.extend(
wasm_objdump
.stdout
.lines()
.map_while(Result::ok)
.skip_while(|line| !line.contains("- func"))
.filter_map(|line| {
if line.contains("func") {
if let Some(function) = line.split_whitespace().nth(2).map(String::from) {
let trimmed = function.trim_start_matches('<').trim_end_matches('>');
if trimmed.starts_with("ts") && !trimmed.contains("__") {
return Some(trimmed.to_string());
}
}
}
None
}),
);
let nm_child = Command::new("nm")
.arg("-W")
.arg("-U")
.arg("libtree-sitter.so")
.stdout(Stdio::piped())
.output()
.expect("Failed to run nm");
bail_on_err(&nm_child, "Failed to run nm")?;
let export_reader = nm_child
.stdout
.lines()
.map_while(Result::ok)
.filter(|line| line.contains(" T "));
let exports = export_reader
.filter_map(|line| line.split_whitespace().nth(2).map(String::from))
.filter(|symbol| !EXCLUDES.contains(&symbol.as_str()))
.collect::<HashSet<_>>();
let mut missing = exports
.iter()
.filter(|&symbol| !wasm_exports.contains(symbol))
.map(|symbol| symbol.as_str())
.collect::<Vec<_>>();
missing.sort_unstable();
if !missing.is_empty() {
Err(anyhow!(format!(
"Unmatched wasm exports:\n{}",
missing.join("\n")
)))?;
}
Ok(())
}

View file

@ -1,6 +1,7 @@
mod benchmark;
mod build_wasm;
mod bump;
mod check_wasm_exports;
mod clippy;
mod fetch;
mod generate;
@ -28,6 +29,8 @@ enum Commands {
BuildWasmStdlib,
/// Bumps the version of the workspace.
BumpVersion(BumpVersion),
/// Checks that WASM exports are synced.
CheckWasmExports,
/// Runs `cargo clippy`.
Clippy(Clippy),
/// Fetches emscripten.
@ -204,6 +207,7 @@ fn run() -> Result<()> {
Commands::BuildWasm(build_wasm_options) => build_wasm::run_wasm(&build_wasm_options)?,
Commands::BuildWasmStdlib => build_wasm::run_wasm_stdlib()?,
Commands::BumpVersion(bump_options) => bump::run(bump_options)?,
Commands::CheckWasmExports => check_wasm_exports::run()?,
Commands::Clippy(clippy_options) => clippy::run(&clippy_options)?,
Commands::FetchEmscripten => fetch::run_emscripten()?,
Commands::FetchFixtures => fetch::run_fixtures()?,