build: add wasm/emscripten build support

- Add is_wasm detection for wasm32-emscripten target
- Add -Demscripten-sysroot option and libc path setup
- Default cross_compiling=true for wasm
- Use lua5.1 instead of luajit for wasm (luajit unsupported)
- Add emscripten include paths to all dependencies
- Build nvim as static library for wasm target
- Add emcc link step with Asyncify and SUPPORT_LONGJMP
- Filter wasm_stubs.c from non-wasm builds

Known limitation: setjmp/longjmp not yet fully functional
due to Zig compiling dependencies outside emcc pipeline.

build: fix wasm emscripten output path tracking
This commit is contained in:
Rawan10101
2026-05-26 09:59:23 +03:00
parent 70792aa6e4
commit 674f3ab4d6

113
build.zig
View File

@@ -39,6 +39,12 @@ pub fn lazyArtifact(d: *std.Build.Dependency, name: []const u8) ?*std.Build.Step
return found;
}
fn addEmscriptenIncludes(emscripten_include: ?std.Build.LazyPath, artifact: *std.Build.Step.Compile) void {
if (emscripten_include) |inc| {
artifact.root_module.addSystemIncludePath(inc);
}
}
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
@@ -50,10 +56,30 @@ pub fn build(b: *std.Build) !void {
const is_linux = (os_tag == .linux);
const is_darwin = os_tag.isDarwin();
const modern_unix = is_darwin or os_tag.isBSD() or is_linux;
const is_wasm = t.cpu.arch == .wasm32;
const cross_compiling = b.option(bool, "cross", "cross compile") orelse is_wasm;
const emscripten_sysroot = b.option([]const u8, "emscripten-sysroot", "path to emscripten sysroot");
const emscripten_include = if (emscripten_sysroot) |s|
std.Build.LazyPath{ .cwd_relative = b.pathJoin(&.{ s, "include" }) }
else
null;
const emscripten_libc_path: ?std.Build.LazyPath = if (is_wasm) blk: {
if (emscripten_sysroot) |s| {
const libc_txt = b.fmt(
"include_dir={s}/include\nsys_include_dir={s}/include\ncrt_dir={s}/lib/wasm32-emscripten\nmsvc_lib_dir=\nkernel32_lib_dir=\ngcc_dir=\n",
.{ s, s, s },
);
const wf = b.addWriteFiles();
break :blk wf.add("emscripten-libc.txt", libc_txt);
}
break :blk null;
} else null;
const cross_compiling = b.option(bool, "cross", "cross compile") orelse false;
// TODO(bfredl): option to set nlua0 target explicitly when cross compiling?
const target_host = if (cross_compiling) b.graph.host else target;
// without cross_compiling we like to reuse libluv etc at the same optimize level
const optimize_host = if (cross_compiling) .ReleaseSafe else optimize;
@@ -62,7 +88,7 @@ pub fn build(b: *std.Build) !void {
// puc lua 5.1 is not ReleaseSafe "safe"
const optimize_lua = if (optimize == .Debug or optimize == .ReleaseSafe) .ReleaseSmall else optimize;
const use_luajit = b.option(bool, "luajit", "use luajit") orelse true;
const use_luajit = b.option(bool, "luajit", "use luajit") orelse !is_wasm;
const lualib_name = if (use_luajit) "luajit" else "lua5.1";
const host_use_luajit = if (cross_compiling) false else use_luajit;
const E = enum { luajit, lua51 };
@@ -98,6 +124,8 @@ pub fn build(b: *std.Build) !void {
// this is currently not necessary, as ziglua currently doesn't use lazy dependencies
// to circumvent ziglua.artifact() failing in a bad way.
lua = lazyArtifact(ziglua, "lua") orelse return;
addEmscriptenIncludes(emscripten_include, lua.?);
if (cross_compiling) {
_ = lazyArtifact(ziglua_host, "lua") orelse return;
}
@@ -108,8 +136,14 @@ pub fn build(b: *std.Build) !void {
const optimize_uv = if (optimize == .Debug and target.result.os.tag == .windows) .ReleaseSafe else optimize;
if (b.lazyDependency("libuv", .{ .target = target, .optimize = optimize_uv })) |dep| {
libuv = dep.artifact("uv");
if (emscripten_libc_path) |lp| libuv.?.setLibCFile(lp);
addEmscriptenIncludes(emscripten_include, libuv.?);
libluv = try build_lua.build_libluv(b, target, optimize, lua, libuv.?, use_luajit);
if (libluv) |luv| addEmscriptenIncludes(emscripten_include, luv);
libluv_host = if (cross_compiling) libluv_host: {
const libuv_dep_host = b.lazyDependency("libuv", .{
.target = target_host,
@@ -139,6 +173,7 @@ pub fn build(b: *std.Build) !void {
.target = target,
.optimize = optimize,
});
const unibilium = if (use_unibilium and !sys_opts.unibilium) b.lazyDependency("unibilium", .{
.target = target,
.optimize = optimize,
@@ -151,6 +186,15 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize_ts,
});
if (utf8proc) |dep|
addEmscriptenIncludes(emscripten_include, dep.artifact("utf8proc"));
if (unibilium) |dep|
addEmscriptenIncludes(emscripten_include, dep.artifact("unibilium"));
if (treesitter) |dep|
addEmscriptenIncludes(emscripten_include, dep.artifact("tree-sitter"));
const nlua0 = try build_lua.build_nlua0(
b,
io,
@@ -442,8 +486,16 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize,
.link_libc = true,
});
const nvim_exe = b.addExecutable(.{ .name = "nvim", .root_module = nvim_mod });
const nvim_exe = if (is_wasm) b.addLibrary(.{
.linkage = .static,
.name = "nvim",
.root_module = nvim_mod,
}) else b.addExecutable(.{ .name = "nvim", .root_module = nvim_mod });
nvim_exe.rdynamic = true; // -E
if (emscripten_libc_path) |lp| nvim_exe.setLibCFile(lp);
if (is_wasm) nvim_exe.entry = .disabled;
if (is_wasm) nvim_exe.linker_allow_shlib_undefined = true;
if (sys_opts.lua) {
nvim_exe.root_module.linkSystemLibrary(lualib_name, .{});
@@ -496,11 +548,15 @@ pub fn build(b: *std.Build) !void {
}
}
const src_paths = try b.allocator.alloc([]u8, nvim_sources.items.len + unit_test_sources.items.len);
for (nvim_sources.items, 0..) |s, i| {
src_paths[i] = b.fmt("src/nvim/{s}", .{s.name});
var filtered_sources = try std.ArrayList([]u8).initCapacity(b.allocator, nvim_sources.items.len);
for (nvim_sources.items) |s| {
// wasm_stubs.c defines libuv symbols & excludes it when real libuv is linked
if (!is_wasm and std.mem.eql(u8, s.name, "wasm_stubs.c")) continue;
try filtered_sources.append(b.allocator, b.fmt("src/nvim/{s}", .{s.name}));
}
@memcpy(src_paths[nvim_sources.items.len..], unit_test_sources.items);
const src_paths = try b.allocator.alloc([]u8, filtered_sources.items.len + unit_test_sources.items.len);
@memcpy(src_paths[0..filtered_sources.items.len], filtered_sources.items);
@memcpy(src_paths[filtered_sources.items.len..], unit_test_sources.items);
const flags = [_][]const u8{
"-std=gnu99",
@@ -532,15 +588,46 @@ pub fn build(b: *std.Build) !void {
}
const nvim_exe_step = b.step("nvim_bin", "only the binary (not a fully working install!)");
const nvim_exe_install = b.addInstallArtifact(nvim_exe, .{});
const nvim_exe_install = b.addInstallArtifact(nvim_exe, .{ .dest_dir = if (is_wasm) .{ .override = .{ .custom = "wasm" } } else .default });
nvim_exe_step.dependOn(&nvim_exe_install.step);
if (is_wasm) {
const s = emscripten_sysroot orelse @panic("-Demscripten-sysroot required for wasm target");
const emcc = b.addSystemCommand(&.{"emcc"});
emcc.step.dependOn(&nvim_exe.step);
emcc.addArg("-Wl,--whole-archive");
emcc.addFileArg(nvim_exe.getEmittedBin());
if (lua) |l| emcc.addFileArg(l.getEmittedBin());
if (libuv) |uv| emcc.addFileArg(uv.getEmittedBin());
if (libluv) |luv| emcc.addFileArg(luv.getEmittedBin());
if (utf8proc) |dep| emcc.addFileArg(dep.artifact("utf8proc").getEmittedBin());
if (unibilium) |dep| emcc.addFileArg(dep.artifact("unibilium").getEmittedBin());
if (treesitter) |dep| emcc.addFileArg(dep.artifact("tree-sitter").getEmittedBin());
emcc.addArg("-Wl,--no-whole-archive");
emcc.addArgs(&.{
b.fmt("--sysroot={s}", .{s}),
"-sALLOW_MEMORY_GROWTH=1",
"-sEXPORT_ALL=1",
"-sINITIAL_MEMORY=67108864",
"-sFORCE_FILESYSTEM=1",
"-sERROR_ON_UNDEFINED_SYMBOLS=0",
"--no-entry",
"-Wl,--export-all",
"-Wl,--no-gc-sections",
});
emcc.addArg("-o");
const nvim_js = emcc.addOutputFileArg("nvim.js");
nvim_exe_step.dependOn(&b.addInstallFileWithDir(nvim_js, .bin, "nvim.js").step);
nvim_exe_step.dependOn(&b.addInstallFileWithDir(nvim_js.dirname().path(b, "nvim.wasm"), .bin, "nvim.wasm").step);
} else {
nvim_exe_step.dependOn(&nvim_exe_install.step);
}
const gen_runtime = try runtime.nvim_gen_runtime(b, nlua0, funcs_data);
const test_deps = b.step("test_deps", "test prerequisites");
test_deps.dependOn(&nvim_exe_install.step);
// running tests doesn't require copying the static runtime, only the generated stuff
if (!is_wasm) test_deps.dependOn(&nvim_exe_install.step); // running tests doesn't require copying the static runtime, only the generated stuff
const test_runtime_install = b.addInstallDirectory(.{
.source_dir = gen_runtime.getDirectory(),
.install_dir = .prefix,
@@ -551,7 +638,7 @@ pub fn build(b: *std.Build) !void {
const nvim_dev = b.step("nvim_dev", "build the editor for development");
b.default_step = nvim_dev;
nvim_dev.dependOn(&nvim_exe_install.step);
if (!is_wasm) nvim_dev.dependOn(&nvim_exe_install.step);
nvim_dev.dependOn(&test_runtime_install.step);
// run from dev environment
@@ -568,7 +655,7 @@ pub fn build(b: *std.Build) !void {
// installation
const install = b.getInstallStep();
install.dependOn(&nvim_exe_install.step);
if (!is_wasm) install.dependOn(&nvim_exe_install.step);
b.installDirectory(.{
.source_dir = b.path("runtime/"),
.install_dir = .prefix,