tree-sitter/crates/xtask/src/main.rs
ObserverOfTime 88e0b4cea4 docs: change WASM/wasm to Wasm
That is the official capitalisation.
2025-08-21 09:56:32 +03:00

370 lines
11 KiB
Rust

mod benchmark;
mod build_wasm;
mod bump;
mod check_wasm_exports;
mod clippy;
mod fetch;
mod generate;
mod test;
mod upgrade_emscripten;
mod upgrade_wasmtime;
use std::path::Path;
use anstyle::{AnsiColor, Color, Style};
use anyhow::Result;
use clap::{crate_authors, Args, Command, FromArgMatches as _, Subcommand};
use git2::{Oid, Repository};
use semver::Version;
#[derive(Subcommand)]
#[command(about="Run various tasks", author=crate_authors!("\n"), styles=get_styles())]
enum Commands {
/// Runs `cargo benchmark` with some optional environment variables set.
Benchmark(Benchmark),
/// Compile the Tree-sitter Wasm library. This will create two files in the
/// `lib/binding_web` directory: `web-tree-sitter.js` and `web-tree-sitter.wasm`.
BuildWasm(BuildWasm),
/// Compile the Tree-sitter Wasm standard library.
BuildWasmStdlib,
/// Bumps the version of the workspace.
BumpVersion(BumpVersion),
/// Checks that Wasm exports are synced.
CheckWasmExports(CheckWasmExports),
/// Runs `cargo clippy`.
Clippy(Clippy),
/// Fetches emscripten.
FetchEmscripten,
/// Fetches the fixtures for testing tree-sitter.
FetchFixtures,
/// Generate the Rust bindings from the C library.
GenerateBindings,
/// Generates the fixtures for testing tree-sitter.
GenerateFixtures(GenerateFixtures),
/// Generate the list of exports from Tree-sitter Wasm files.
GenerateWasmExports,
/// Run the test suite
Test(Test),
/// Run the Wasm test suite
TestWasm,
/// Upgrade the wasmtime dependency.
UpgradeWasmtime(UpgradeWasmtime),
/// Upgrade the emscripten file.
UpgradeEmscripten,
}
#[derive(Args)]
struct Benchmark {
/// The language to run the benchmarks for.
#[arg(long, short)]
language: Option<String>,
/// The example file to run the benchmarks for.
#[arg(long, short)]
example_file_name: Option<String>,
/// The number of times to parse each sample (default is 5).
#[arg(long, short, default_value = "5")]
repetition_count: u32,
/// Whether to run the benchmarks in debug mode.
#[arg(long, short = 'g')]
debug: bool,
}
#[derive(Args)]
struct BuildWasm {
/// Compile the library more quickly, with fewer optimizations
/// and more runtime assertions.
#[arg(long, short = '0')]
debug: bool,
/// Run emscripten using docker, even if \`emcc\` is installed.
/// By default, \`emcc\` will be run directly when available.
#[arg(long, short)]
docker: bool,
/// Run emscripten with verbose output.
#[arg(long, short)]
verbose: bool,
/// Rebuild when relevant files are changed.
#[arg(long, short)]
watch: bool,
/// Emit TypeScript type definitions for the generated bindings,
/// requires `tsc` to be available.
#[arg(long, short)]
emit_tsd: bool,
/// Generate `CommonJS` modules instead of ES modules.
#[arg(long, short, env = "CJS")]
cjs: bool,
}
#[derive(Args)]
struct BumpVersion {
/// The version to bump to.
#[arg(long, short)]
version: Option<Version>,
}
#[derive(Args)]
struct CheckWasmExports {
/// Recheck when relevant files are changed.
#[arg(long, short)]
watch: bool,
}
#[derive(Args)]
struct Clippy {
/// Automatically apply lint suggestions (`clippy --fix`).
#[arg(long, short)]
fix: bool,
/// The package to run Clippy against (`cargo -p <PACKAGE> clippy`).
#[arg(long, short)]
package: Option<String>,
}
#[derive(Args)]
struct GenerateFixtures {
/// Generates the parser to Wasm
#[arg(long, short)]
wasm: bool,
}
#[derive(Args)]
struct Test {
/// Compile C code with the Clang address sanitizer.
#[arg(long, short)]
address_sanitizer: bool,
/// Run only the corpus tests for the given language.
#[arg(long, short)]
language: Option<String>,
/// Run only the corpus tests whose name contain the given string.
#[arg(long, short)]
example: Option<String>,
/// Run the given number of iterations of randomized tests (default 10).
#[arg(long, short)]
iterations: Option<u32>,
/// Set the seed used to control random behavior.
#[arg(long, short)]
seed: Option<usize>,
/// Print parsing log to stderr.
#[arg(long, short)]
debug: bool,
/// Generate an SVG graph of parsing logs.
#[arg(long, short = 'D')]
debug_graph: bool,
/// Run the tests with a debugger.
#[arg(short)]
g: bool,
#[arg(trailing_var_arg = true)]
args: Vec<String>,
/// Don't capture the output
#[arg(long)]
nocapture: bool,
/// Enable the Wasm tests.
#[arg(long, short)]
wasm: bool,
}
#[derive(Args)]
struct UpgradeWasmtime {
/// The version to upgrade to.
#[arg(long, short)]
version: Version,
}
const BUILD_VERSION: &str = env!("CARGO_PKG_VERSION");
const BUILD_SHA: Option<&str> = option_env!("BUILD_SHA");
const EMSCRIPTEN_VERSION: &str = include_str!("../../loader/emscripten-version").trim_ascii();
const EMSCRIPTEN_TAG: &str = concat!(
"docker.io/emscripten/emsdk:",
include_str!("../../loader/emscripten-version")
)
.trim_ascii();
fn main() {
let result = run();
if let Err(err) = &result {
// Ignore BrokenPipe errors
if let Some(error) = err.downcast_ref::<std::io::Error>() {
if error.kind() == std::io::ErrorKind::BrokenPipe {
return;
}
}
if !err.to_string().is_empty() {
eprintln!("{err:?}");
}
std::process::exit(1);
}
}
fn run() -> Result<()> {
let version = BUILD_SHA.map_or_else(
|| BUILD_VERSION.to_string(),
|build_sha| format!("{BUILD_VERSION} ({build_sha})"),
);
let version: &'static str = Box::leak(version.into_boxed_str());
let cli = Command::new("xtask")
.help_template(
"\
{before-help}{name} {version}
{author-with-newline}{about-with-newline}
{usage-heading} {usage}
{all-args}{after-help}
",
)
.version(version)
.subcommand_required(true)
.arg_required_else_help(true)
.disable_help_subcommand(true)
.disable_colored_help(false);
let command = Commands::from_arg_matches(&Commands::augment_subcommands(cli).get_matches())?;
match command {
Commands::Benchmark(benchmark_options) => benchmark::run(&benchmark_options)?,
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_options) => check_wasm_exports::run(&check_options)?,
Commands::Clippy(clippy_options) => clippy::run(&clippy_options)?,
Commands::FetchEmscripten => fetch::run_emscripten()?,
Commands::FetchFixtures => fetch::run_fixtures()?,
Commands::GenerateBindings => generate::run_bindings()?,
Commands::GenerateFixtures(generate_fixtures_options) => {
generate::run_fixtures(&generate_fixtures_options)?;
}
Commands::GenerateWasmExports => generate::run_wasm_exports()?,
Commands::Test(test_options) => test::run(&test_options)?,
Commands::TestWasm => test::run_wasm()?,
Commands::UpgradeWasmtime(upgrade_wasmtime_options) => {
upgrade_wasmtime::run(&upgrade_wasmtime_options)?;
}
Commands::UpgradeEmscripten => upgrade_emscripten::run()?,
}
Ok(())
}
fn root_dir() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
}
fn bail_on_err(output: &std::process::Output, prefix: &str) -> Result<()> {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("{prefix}:\n{stderr}");
}
Ok(())
}
#[must_use]
const fn get_styles() -> clap::builder::Styles {
clap::builder::Styles::styled()
.usage(
Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
)
.header(
Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
)
.literal(Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))))
.invalid(
Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Red))),
)
.error(
Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Red))),
)
.valid(
Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Green))),
)
.placeholder(Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))))
}
pub fn create_commit(repo: &Repository, msg: &str, paths: &[&str]) -> Result<Oid> {
let mut index = repo.index()?;
for path in paths {
index.add_path(Path::new(path))?;
}
index.write()?;
let tree_id = index.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let signature = repo.signature()?;
let parent_commit = repo.revparse_single("HEAD")?.peel_to_commit()?;
Ok(repo.commit(
Some("HEAD"),
&signature,
&signature,
msg,
&tree,
&[&parent_commit],
)?)
}
#[macro_export]
macro_rules! watch_wasm {
($watch_fn:expr) => {
if let Err(e) = $watch_fn() {
eprintln!("{e}");
} else {
println!("Build succeeded");
}
let watch_files = [
"lib/tree-sitter.c",
"lib/exports.txt",
"lib/imports.js",
"lib/prefix.js",
]
.iter()
.map(PathBuf::from)
.collect::<HashSet<PathBuf>>();
let (tx, rx) = std::sync::mpsc::channel();
let mut debouncer = new_debouncer(Duration::from_secs(1), None, tx)?;
debouncer.watch("lib/binding_web", RecursiveMode::NonRecursive)?;
for result in rx {
match result {
Ok(events) => {
for event in events {
if event.kind == EventKind::Access(AccessKind::Close(AccessMode::Write))
&& event
.paths
.iter()
.filter_map(|p| p.file_name())
.any(|p| watch_files.contains(&PathBuf::from(p)))
{
if let Err(e) = $watch_fn() {
eprintln!("{e}");
} else {
println!("Build succeeded");
}
}
}
}
Err(errors) => {
return Err(anyhow!(
"{}",
errors
.into_iter()
.map(|e| e.to_string())
.collect::<Vec<_>>()
.join("\n")
));
}
}
}
};
}