use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
struct WasiSdk {
clang: PathBuf,
clangxx: PathBuf,
sysroot: PathBuf,
}
fn find_wasi_sdk() -> Option<WasiSdk> {
if std::env::var_os("WASI_SDK_PATH").is_none()
&& let Some(b) = afterburner_wasi::wasi_sdk_bundle::resolve()
{
return Some(WasiSdk {
clang: b.clang,
clangxx: b.clangxx,
sysroot: b.sysroot,
});
}
if let Ok(root) = std::env::var("WASI_SDK_PATH")
&& let Some(sdk) = wasi_sdk_from_root(Path::new(&root))
{
return Some(sdk);
}
if let Ok(sysroot) = std::env::var("WASI_SYSROOT") {
let sysroot = PathBuf::from(sysroot);
if sysroot.is_dir() {
let clang = std::env::var("CC").map(PathBuf::from).unwrap_or_else(|_| {
std::env::var("CLANG")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("clang"))
});
let clangxx = std::env::var("CXX")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("clang++"));
return Some(WasiSdk {
clang,
clangxx,
sysroot,
});
}
}
let mut roots: Vec<PathBuf> = vec![
PathBuf::from("/opt/wasi-sdk"),
PathBuf::from("/usr/local/wasi-sdk"),
PathBuf::from("/usr/lib/wasi-sdk"),
];
if let Some(home) = std::env::var_os("HOME") {
roots.push(Path::new(&home).join("wasi-sdk"));
roots.push(Path::new(&home).join(".wasi-sdk"));
}
roots.into_iter().find_map(|r| wasi_sdk_from_root(&r))
}
fn wasi_sdk_from_root(root: &Path) -> Option<WasiSdk> {
let clang = root.join("bin/clang");
let clangxx = root.join("bin/clang++");
let sysroot = ["share/wasi-sysroot", "wasi-sysroot"]
.iter()
.map(|s| root.join(s))
.find(|p| p.is_dir())?;
if clang.is_file() {
Some(WasiSdk {
clang,
clangxx,
sysroot,
})
} else {
None
}
}
fn wasi_sdk_missing_error(lang: &str) -> anyhow::Error {
anyhow::anyhow!("C/C++ compilation is not available in this build of burn ({lang})")
}
fn collect_sources(pkg_dir: &Path, exts: &[&str]) -> Result<Vec<PathBuf>> {
let root = {
let s = pkg_dir.join("source");
if s.is_dir() { s } else { pkg_dir.to_path_buf() }
};
let mut out = Vec::new();
collect_sources_into(&root, exts, &mut out)?;
out.sort();
Ok(out)
}
fn collect_sources_into(dir: &Path, exts: &[&str], out: &mut Vec<PathBuf>) -> Result<()> {
let entries =
std::fs::read_dir(dir).with_context(|| format!("reading sources in {}", dir.display()))?;
for entry in entries {
let entry = entry?;
let path = entry.path();
let ft = entry.file_type()?;
if ft.is_dir() {
collect_sources_into(&path, exts, out)?;
} else if ft.is_file()
&& path
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.is_some_and(|e| exts.contains(&e.as_str()))
{
out.push(path);
}
}
Ok(())
}
fn try_build_system(
pkg_dir: &Path,
sdk: &WasiSdk,
is_cpp: bool,
cpp_exceptions: bool,
) -> Result<Option<Vec<u8>>> {
let has_make = ["Makefile", "makefile", "GNUmakefile"]
.iter()
.any(|f| pkg_dir.join(f).is_file());
let has_cmake = pkg_dir.join("CMakeLists.txt").is_file();
if !has_make && !has_cmake {
return Ok(None);
}
let cc = sdk.clang.to_string_lossy().into_owned();
let cxx = sdk.clangxx.to_string_lossy().into_owned();
let sysroot = sdk.sysroot.to_string_lossy().into_owned();
let target_flags = format!("--target=wasm32-wasip1 --sysroot={sysroot}");
let (cxx_flags, ld_flags) = if cpp_exceptions {
(
format!("{target_flags} -fwasm-exceptions"),
format!("{target_flags} -lunwind"),
)
} else {
(
format!("{target_flags} -fno-exceptions"),
target_flags.clone(),
)
};
let run_make = |program: &str| -> Result<std::process::ExitStatus> {
std::process::Command::new(program)
.current_dir(pkg_dir)
.env("WASI_SDK_PATH", sdk_root_of(sdk))
.env("WASI_SYSROOT", &sysroot)
.env("CC", &cc)
.env("CXX", &cxx)
.env("CFLAGS", &target_flags)
.env("CXXFLAGS", &cxx_flags)
.env("LDFLAGS", &ld_flags)
.status()
.with_context(|| format!("spawning `{program}`"))
};
if has_make {
let status = run_make("make")?;
if !status.success() {
anyhow::bail!(
"`make` exited with code {} building {}",
status.code().unwrap_or(-1),
pkg_dir.display()
);
}
} else {
let build_dir = pkg_dir.join("build");
std::fs::create_dir_all(&build_dir).ok();
let compiler_flag = if is_cpp {
format!("-DCMAKE_CXX_COMPILER={cxx}")
} else {
format!("-DCMAKE_C_COMPILER={cc}")
};
let configure = std::process::Command::new("cmake")
.current_dir(pkg_dir)
.args([
"-S",
".",
"-B",
"build",
"-DCMAKE_SYSTEM_NAME=WASI",
"-DCMAKE_SYSTEM_VERSION=1",
"-DCMAKE_SYSTEM_PROCESSOR=wasm32",
])
.arg(format!("-DCMAKE_SYSROOT={sysroot}"))
.arg(format!("-DCMAKE_C_FLAGS={target_flags}"))
.arg(format!("-DCMAKE_CXX_FLAGS={cxx_flags}"))
.arg(format!("-DCMAKE_EXE_LINKER_FLAGS={ld_flags}"))
.arg(&compiler_flag)
.status()
.with_context(|| "spawning `cmake` (configure)")?;
if !configure.success() {
anyhow::bail!(
"`cmake` configure exited with code {}",
configure.code().unwrap_or(-1)
);
}
let build = std::process::Command::new("cmake")
.current_dir(pkg_dir)
.args(["--build", "build"])
.status()
.with_context(|| "spawning `cmake` (build)")?;
if !build.success() {
anyhow::bail!(
"`cmake --build` exited with code {}",
build.code().unwrap_or(-1)
);
}
}
let mut wasms = Vec::new();
collect_sources_into(pkg_dir, &["wasm"], &mut wasms).ok();
let newest = wasms
.into_iter()
.filter_map(|p| {
let m = std::fs::metadata(&p).ok()?.modified().ok()?;
Some((m, p))
})
.max_by_key(|(m, _)| *m)
.map(|(_, p)| p);
match newest {
Some(p) => {
Ok(Some(std::fs::read(&p).with_context(|| {
format!("reading built wasm {}", p.display())
})?))
}
None => anyhow::bail!(
"the build system in {} ran but produced no .wasm output",
pkg_dir.display()
),
}
}
fn sdk_root_of(sdk: &WasiSdk) -> PathBuf {
sdk.clang
.parent() .and_then(|p| p.parent()) .map(Path::to_path_buf)
.unwrap_or_else(|| sdk.clang.clone())
}
fn find_wasm_opt() -> Option<PathBuf> {
if std::process::Command::new("wasm-opt")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
{
return Some(PathBuf::from("wasm-opt"));
}
if let Some(home) = std::env::var_os("HOME") {
let p = Path::new(&home).join("emsdk/upstream/bin/wasm-opt");
if p.exists() {
return Some(p);
}
}
None
}
fn translate_to_exnref(wasm_opt: &Path, wasm: &Path) -> Result<()> {
const FLAGS: &[&str] = &[
"--translate-to-exnref",
"--enable-exception-handling",
"--enable-reference-types",
"--enable-bulk-memory",
"--enable-simd",
"--enable-sign-ext",
"--enable-nontrapping-float-to-int",
"--enable-mutable-globals",
"--enable-multivalue",
];
let out = wasm.with_extension("exnref.wasm");
let status = std::process::Command::new(wasm_opt)
.args(FLAGS)
.arg(wasm)
.arg("-o")
.arg(&out)
.status()
.with_context(|| {
format!("spawning wasm-opt {wasm_opt:?} for the C++ exnref translation")
})?;
if !status.success() {
let _ = std::fs::remove_file(&out);
anyhow::bail!(
"exnref translation (wasm-opt) exited with code {}",
status.code().unwrap_or(-1)
);
}
std::fs::rename(&out, wasm)
.with_context(|| format!("replacing {} with its exnref form", wasm.display()))?;
Ok(())
}
pub(super) fn compile_c(pkg_dir: &Path, entry: &str) -> Result<Vec<u8>> {
compile_c_family(pkg_dir, entry, false)
}
pub(super) fn compile_cpp(pkg_dir: &Path, entry: &str) -> Result<Vec<u8>> {
compile_c_family(pkg_dir, entry, true)
}
fn compile_c_family(pkg_dir: &Path, entry: &str, is_cpp: bool) -> Result<Vec<u8>> {
let lang = if is_cpp { "C++" } else { "C" };
let entry_path = pkg_dir.join(entry);
if !entry_path.exists() {
anyhow::bail!(
"{lang} entry file {:?} does not exist in {}",
entry,
pkg_dir.display()
);
}
let Some(sdk) = find_wasi_sdk() else {
return Err(wasi_sdk_missing_error(lang));
};
let wasm_opt = if is_cpp { find_wasm_opt() } else { None };
let cpp_exceptions = is_cpp && wasm_opt.is_some();
if let Some(mut bytes) = try_build_system(pkg_dir, &sdk, is_cpp, cpp_exceptions)? {
if let Some(ref wasm_opt) = wasm_opt {
let tmp = std::env::temp_dir().join(format!("burn-cpp-bs-{}.wasm", std::process::id()));
std::fs::write(&tmp, &bytes)
.with_context(|| format!("staging build-system wasm {}", tmp.display()))?;
let translated = translate_to_exnref(wasm_opt, &tmp).and_then(|()| {
std::fs::read(&tmp).with_context(|| format!("reading {}", tmp.display()))
});
let _ = std::fs::remove_file(&tmp);
bytes = translated?;
}
return Ok(bytes);
}
let exts: &[&str] = if is_cpp {
&["cpp", "cxx", "cc"]
} else {
&["c"]
};
let sources = if pkg_dir.join("source").is_dir() {
let found = collect_sources(pkg_dir, exts)?;
if found.is_empty() {
anyhow::bail!(
"no {lang} sources (*.{}) found under {}/source",
exts.join(", *."),
pkg_dir.display()
);
}
found
} else {
vec![entry_path.clone()]
};
let driver = if is_cpp { &sdk.clangxx } else { &sdk.clang };
let wasm_out = std::env::temp_dir().join(format!(
"burn-{}-{}.wasm",
if is_cpp { "cpp" } else { "c" },
std::process::id()
));
let sysroot_arg = format!("--sysroot={}", sdk.sysroot.display());
let mut cmd = std::process::Command::new(driver);
cmd.arg("--target=wasm32-wasip1")
.arg(&sysroot_arg)
.arg("-O2");
if is_cpp {
cmd.arg(if cpp_exceptions {
"-fwasm-exceptions"
} else {
"-fno-exceptions"
});
}
cmd.arg("-I")
.arg(pkg_dir)
.arg("-I")
.arg(pkg_dir.join("source"));
for src in &sources {
cmd.arg(src);
}
if cpp_exceptions {
cmd.arg("-lunwind");
}
cmd.arg("-o").arg(&wasm_out).current_dir(pkg_dir);
let status = cmd.status().map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
wasi_sdk_missing_error(lang)
} else {
anyhow::anyhow!("spawning the {lang} compiler: {e}")
}
})?;
if !status.success() {
let _ = std::fs::remove_file(&wasm_out);
anyhow::bail!(
"{lang} compile exited with code {} ({} source file{} from {})",
status.code().unwrap_or(-1),
sources.len(),
if sources.len() == 1 { "" } else { "s" },
pkg_dir.display()
);
}
if let Some(ref wasm_opt) = wasm_opt {
translate_to_exnref(wasm_opt, &wasm_out)?;
}
let bytes = std::fs::read(&wasm_out)
.with_context(|| format!("reading {lang} WASM {}", wasm_out.display()))?;
let _ = std::fs::remove_file(&wasm_out);
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn collect_sources_finds_all_c_recursively_excluding_headers() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("source");
std::fs::create_dir_all(src.join("util")).unwrap();
std::fs::write(src.join("main.c"), b"int main(){return 0;}").unwrap();
std::fs::write(src.join("util/helper.c"), b"int h(){return 1;}").unwrap();
std::fs::write(src.join("util/helper.h"), b"int h(void);").unwrap();
std::fs::write(src.join("ignore.cpp"), b"int x(){return 2;}").unwrap();
let found = collect_sources(dir.path(), &["c"]).unwrap();
let names: Vec<_> = found
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert!(names.contains(&"main.c".to_string()), "got {names:?}");
assert!(names.contains(&"helper.c".to_string()), "got {names:?}");
assert!(!names.contains(&"helper.h".to_string()), "headers excluded");
assert!(!names.contains(&"ignore.cpp".to_string()), "cpp excluded");
assert_eq!(found.len(), 2);
}
#[test]
fn collect_sources_finds_cpp_variants() {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("source");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("a.cpp"), b"").unwrap();
std::fs::write(src.join("b.cxx"), b"").unwrap();
std::fs::write(src.join("c.cc"), b"").unwrap();
let found = collect_sources(dir.path(), &["cpp", "cxx", "cc"]).unwrap();
assert_eq!(found.len(), 3, "all three c++ extensions: {found:?}");
}
#[test]
fn c_compile_missing_entry_errors_clearly() {
let dir = tempfile::tempdir().unwrap();
let err = compile_c(dir.path(), "source/main.c").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("does not exist"),
"missing entry must say so: {msg}"
);
}
#[test]
fn cpp_compile_missing_entry_errors_clearly() {
let dir = tempfile::tempdir().unwrap();
let err = compile_cpp(dir.path(), "source/main.cpp").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("does not exist"),
"missing entry must say so: {msg}"
);
}
#[test]
fn c_compile_without_toolchain_is_honest_not_silent() {
if find_wasi_sdk().is_some() {
return;
}
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("source");
std::fs::create_dir_all(&src).unwrap();
std::fs::write(src.join("main.c"), b"int main(void){return 0;}").unwrap();
let err = compile_c(dir.path(), "source/main.c").unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("C/C++ compilation is not available"),
"must be an honest toolchain-missing error: {msg}"
);
assert!(
!msg.contains("wasi-sdk") && !msg.contains("WASI_SDK_PATH"),
"the user-facing error must not name the internal toolchain: {msg}"
);
}
}