From f36ed57f70b957ad2a2f4506d6d5fdf6858a595c Mon Sep 17 00:00:00 2001 From: kuni <4cecorporation@gmail.com> Date: Mon, 17 Aug 2026 08:42:58 +0900 Subject: [PATCH] c-api: install headers from build.rs without requiring cmake The build script previously shelled out to `cmake -P cmake/install-headers.cmake` to produce the C API headers in OUT_DIR. cmake was only being used as a scripting engine there: it substitutes the #cmakedefine lines in conf.h.in and copies the .h/.hh files. This made cmake a build requirement for every crate that transitively depends on wasmtime-c-api-impl (e.g. anything using tree-sitter's `wasm` feature), even though nothing is compiled with it. Reimplement the header install directly in build.rs with std only: - conf.h is generated from conf.h.in by turning each `#cmakedefine WASMTIME_FEATURE_X` line into `#define ...` or `/* #undef ... */` based on the corresponding CARGO_FEATURE_* env var, with CRLF newlines to match cmake's NEWLINE_STYLE CRLF. The feature list is read from the template itself, so build.rs no longer needs its own copy of WASMTIME_FEATURE_LIST. - headers are copied recursively, matching file(INSTALL ... FILES_MATCHING REGEX "\.hh?$"). The cmake scripts are untouched and still used by the standalone CMake build; build.rs simply no longer invokes cmake. Verified that the OUT_DIR include tree is byte-for-byte identical to the cmake-generated one (all-features-off and a cranelift/gc-drc/wasi/ wat set), and that `cargo check -p wasmtime-c-api-impl` succeeds with cmake removed from PATH. --- crates/c-api/build.rs | 113 +++++++++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/crates/c-api/build.rs b/crates/c-api/build.rs index a5ca742ea0d1..bfa03ab70f13 100644 --- a/crates/c-api/build.rs +++ b/crates/c-api/build.rs @@ -1,55 +1,76 @@ use std::env; -use std::process::Command; - -// WASMTIME_FEATURE_LIST -const FEATURES: &[&str] = &[ - "ASYNC", - "PROFILING", - "CACHE", - "PARALLEL_COMPILATION", - "WASI", - "WASI_HTTP", - "LOGGING", - "DISABLE_LOGGING", - "COREDUMP", - "ADDR2LINE", - "DEMANGLE", - "THREADS", - "GC", - "GC_DRC", - "GC_NULL", - "CRANELIFT", - "WINCH", - "DEBUG_BUILTINS", - "WAT", - "POOLING_ALLOCATOR", - "COMPONENT_MODEL", - "COMPONENT_MODEL_ASYNC", - "PULLEY", - "ALL_ARCH", -]; -// ... if you add a line above this be sure to change the other locations -// marked WASMTIME_FEATURE_LIST +use std::fs; +use std::path::{Path, PathBuf}; fn main() { - println!("cargo:rerun-if-changed=cmake/features.cmake"); - println!("cargo:rerun-if-changed=cmake/install-headers.cmake"); println!("cargo:rerun-if-changed=include"); - let out_dir = std::env::var("OUT_DIR").unwrap(); - let mut cmake = Command::new("cmake"); - cmake.arg("-DWASMTIME_DISABLE_ALL_FEATURES=ON"); - cmake.arg(format!("-DCMAKE_INSTALL_PREFIX={out_dir}")); - for f in FEATURES { - if env::var_os(format!("CARGO_FEATURE_{f}")).is_some() { - cmake.arg(format!("-DWASMTIME_FEATURE_{f}=ON")); - } - } + let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + let include_src = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()).join("include"); + let dst = out_dir.join("include"); - cmake.arg("-P").arg("cmake/install-headers.cmake"); + generate_conf_header(&include_src, &dst); + copy_headers(&include_src, &dst); + + println!("cargo:include={}", dst.display()); +} - let status = cmake.status().expect("failed to spawn `cmake`"); - assert!(status.success()); +/// Generates `wasmtime/conf.h` from `wasmtime/conf.h.in`, mirroring what +/// `cmake/install-headers.cmake` does for standalone CMake builds: each +/// `#cmakedefine WASMTIME_FEATURE_X` line becomes `#define WASMTIME_FEATURE_X` +/// when the corresponding Cargo feature is enabled and `/* #undef ... */` +/// otherwise. The feature list is read from the template itself, so this +/// function needs no copy of the WASMTIME_FEATURE_LIST. +/// +/// Note the CRLF line endings, matching cmake's `NEWLINE_STYLE CRLF` so that +/// the generated header is byte-identical either way. +fn generate_conf_header(include_src: &Path, dst: &Path) { + let template_path = include_src.join("wasmtime").join("conf.h.in"); + let template = fs::read_to_string(&template_path) + .unwrap_or_else(|e| panic!("failed to read {template_path:?}: {e}")); + let mut conf = String::new(); + for line in template.lines() { + if let Some(rest) = line.strip_prefix("#cmakedefine ") { + let var = rest.split_whitespace().next().unwrap(); + let feature = var + .strip_prefix("WASMTIME_FEATURE_") + .unwrap_or_else(|| panic!("unexpected #cmakedefine {var} in conf.h.in")); + if env::var_os(format!("CARGO_FEATURE_{feature}")).is_some() { + conf.push_str("#define "); + conf.push_str(var); + } else { + conf.push_str("/* #undef "); + conf.push_str(var); + conf.push_str(" */"); + } + } else { + conf.push_str(line); + } + conf.push_str("\r\n"); + } + let conf_dir = dst.join("wasmtime"); + fs::create_dir_all(&conf_dir).unwrap(); + fs::write(conf_dir.join("conf.h"), conf).unwrap(); +} - println!("cargo:include={out_dir}/include"); +/// Copies all `.h`/`.hh` files under `include/` into `$OUT_DIR/include`, +/// preserving the directory structure (the equivalent of cmake's +/// `file(INSTALL ... FILES_MATCHING REGEX "\.hh?$")`). +fn copy_headers(src: &Path, dst: &Path) { + for entry in fs::read_dir(src).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + copy_headers(&path, &dst.join(entry.file_name())); + } else { + let is_header = path + .extension() + .map(|e| e == "h" || e == "hh") + .unwrap_or(false); + if is_header { + fs::create_dir_all(dst).unwrap(); + fs::copy(&path, dst.join(entry.file_name())).unwrap(); + } + } + } }