From fe93878e8383f8182751d55db20c3946e6d86703 Mon Sep 17 00:00:00 2001 From: barathrm Date: Sun, 23 Nov 2025 12:28:17 +0100 Subject: [PATCH] Refactor for improved build.zig integration Only use LazyPaths to improve caching and rebuild triggering. To achieve this, don't use addSystemCommand. Instead we have to build small zig programs to act as runArtifacts which allow us to rely on dynamic input and output paths only. Tell me if you know a better way! Install SDKs into project-specific cache directory which should allow different projects using different emsdk versions more easily on the same build host. Recursively add a build target's dependencies to the emcc command. --- build.zig | 171 ++++++++++++++++++++-------------------- src/emsdk_installer.zig | 106 +++++++++++++++++++++++++ src/passthrough.zig | 37 +++++++++ 3 files changed, 228 insertions(+), 86 deletions(-) create mode 100644 src/emsdk_installer.zig create mode 100644 src/passthrough.zig diff --git a/build.zig b/build.zig index eb20733..b7fe173 100644 --- a/build.zig +++ b/build.zig @@ -1,29 +1,34 @@ const builtin = @import("builtin"); const std = @import("std"); -pub const emsdk_ver_major = "4"; -pub const emsdk_ver_minor = "0"; -pub const emsdk_ver_tiny = "3"; -pub const emsdk_version = emsdk_ver_major ++ "." ++ emsdk_ver_minor ++ "." ++ emsdk_ver_tiny; - pub fn build(b: *std.Build) void { _ = b.addModule("root", .{ .root_source_file = b.path("src/zemscripten.zig") }); + const passthrough = b.addExecutable( + .{ + .name = "passthrough", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/passthrough.zig"), + .target = b.graph.host, + .optimize = .ReleaseFast, + }), + }, + ); + b.installArtifact(passthrough); } -pub fn emccPath(b: *std.Build) []const u8 { - return std.fs.path.join(b.allocator, &.{ - b.dependency("emsdk", .{}).path("").getPath(b), +pub fn emccPath(b: *std.Build, emsdk: Emsdk) std.Build.LazyPath { + return emsdk.emsdk_dir.path(b, b.pathJoin(&.{ "upstream", "emscripten", switch (builtin.target.os.tag) { .windows => "emcc.bat", else => "emcc", }, - }) catch unreachable; + })); } -pub fn emrunPath(b: *std.Build) []const u8 { - return std.fs.path.join(b.allocator, &.{ +pub fn emrunPath(b: *std.Build, emsdk: Emsdk) std.Build.LazyPath { + return emsdk.emsdk_dir.path(b, b.pathJoin(&.{ b.dependency("emsdk", .{}).path("").getPath(b), "upstream", "emscripten", @@ -31,76 +36,59 @@ pub fn emrunPath(b: *std.Build) []const u8 { .windows => "emrun.bat", else => "emrun", }, - }) catch unreachable; + })); } -pub fn htmlPath(b: *std.Build) []const u8 { - return std.fs.path.join(b.allocator, &.{ +pub fn htmlPath(b: *std.Build, emsdk: Emsdk) std.Build.LazyPath { + return emsdk.emsdk_dir.path(b, b.pathJoin(&.{ b.dependency("emsdk", .{}).path("").getPath(b), "upstream", "emscripten", "src", "shell.html", - }) catch unreachable; + })); } -pub fn activateEmsdkStep(b: *std.Build) *std.Build.Step { - const emsdk_script_path = std.fs.path.join(b.allocator, &.{ - b.dependency("emsdk", .{}).path("").getPath(b), - switch (builtin.target.os.tag) { - .windows => "emsdk.bat", - else => "emsdk", - }, - }) catch unreachable; - - var emsdk_install = b.addSystemCommand(&.{ emsdk_script_path, "install", emsdk_version }); +const ActivateEmsdkOptions = struct { + emsdk: *std.Build.Dependency, + zemscripten: *std.Build.Dependency, + sdk_version: ?[]const u8 = null, +}; - switch (builtin.target.os.tag) { - .linux, .macos => { - emsdk_install.step.dependOn(&b.addSystemCommand(&.{ "chmod", "+x", emsdk_script_path }).step); - }, - .windows => { - emsdk_install.step.dependOn(&b.addSystemCommand(&.{ "takeown", "/f", emsdk_script_path }).step); - }, - else => {}, - } +const Emsdk = struct { + zemscripten: *std.Build.Dependency, + step: *std.Build.Step, + emsdk_dir: std.Build.LazyPath, +}; - var emsdk_activate = b.addSystemCommand(&.{ emsdk_script_path, "activate", emsdk_version }); - emsdk_activate.step.dependOn(&emsdk_install.step); - - const step = b.allocator.create(std.Build.Step) catch unreachable; - step.* = std.Build.Step.init(.{ - .id = .custom, - .name = "Activate EMSDK", - .owner = b, - .makeFn = &struct { - fn make(_: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void {} - }.make, +pub fn activateEmsdkStep( + b: *std.Build, + options: ActivateEmsdkOptions, +) Emsdk { + const version = options.sdk_version orelse "4.0.19"; + + const emsdk_installer = b.addExecutable(.{ + .name = "invoker", + .root_module = b.createModule(.{ + .target = b.graph.host, + .optimize = .ReleaseSafe, + .root_source_file = options.zemscripten.path("src/emsdk_installer.zig"), + }), }); - switch (builtin.target.os.tag) { - .linux, .macos => { - const chmod_emcc = b.addSystemCommand(&.{ "chmod", "+x", emccPath(b) }); - chmod_emcc.step.dependOn(&emsdk_activate.step); - step.dependOn(&chmod_emcc.step); - - const chmod_emrun = b.addSystemCommand(&.{ "chmod", "+x", emrunPath(b) }); - chmod_emrun.step.dependOn(&emsdk_activate.step); - step.dependOn(&chmod_emrun.step); - }, - .windows => { - const takeown_emcc = b.addSystemCommand(&.{ "takeown", "/f", emccPath(b) }); - takeown_emcc.step.dependOn(&emsdk_activate.step); - step.dependOn(&takeown_emcc.step); - - const takeown_emrun = b.addSystemCommand(&.{ "takeown", "/f", emrunPath(b) }); - takeown_emrun.step.dependOn(&emsdk_activate.step); - step.dependOn(&takeown_emrun.step); - }, - else => {}, - } + const emsdk_script_path = options.emsdk.path("").join(b.allocator, switch (builtin.target.os.tag) { + .windows => "emsdk.bat", + else => "emsdk", + }) catch unreachable; - return step; + const activate_and_install_step = b.addRunArtifact(emsdk_installer); + activate_and_install_step.addFileArg(emsdk_script_path); + activate_and_install_step.addArg(version); + return .{ + .step = &activate_and_install_step.step, + .emsdk_dir = activate_and_install_step.addOutputDirectoryArg("emsdk"), + .zemscripten = options.zemscripten, + }; } pub const EmccFlags = std.StringHashMap(void); @@ -163,7 +151,6 @@ pub fn emccDefaultSettings(allocator: std.mem.Allocator, options: EmccDefaultSet }, else => {}, } - settings.put("USE_OFFSET_CONVERTER", "1") catch unreachable; settings.put("MALLOC", @tagName(options.emsdk_allocator)) catch unreachable; return settings; } @@ -186,12 +173,36 @@ pub const StepOptions = struct { install_dir: std.Build.InstallDir, }; +fn addAllLinkedArtifacts(b: *std.Build, emcc: *std.Build.Step.Run, root_module: *std.Build.Module) void { + for (root_module.getGraph().modules) |module| { + for (module.link_objects.items) |link_object| { + switch (link_object) { + .other_step => |compile_step| { + switch (compile_step.kind) { + .lib => { + emcc.addArtifactArg(compile_step); + addAllLinkedArtifacts(b, emcc, compile_step.root_module); + }, + else => {}, + } + }, + .static_path => |static_path| { + emcc.addFileArg(static_path); + }, + else => {}, + } + } + } +} + pub fn emccStep( b: *std.Build, + emsdk: Emsdk, wasm: *std.Build.Step.Compile, options: StepOptions, ) *std.Build.Step { - var emcc = b.addSystemCommand(&.{emccPath(b)}); + var emcc = b.addRunArtifact(emsdk.zemscripten.artifact("passthrough")); + emcc.addFileArg(emccPath(b, emsdk)); var iterFlags = options.flags.iterator(); while (iterFlags.next()) |kvp| { @@ -209,21 +220,7 @@ pub fn emccStep( emcc.addArtifactArg(wasm); { - for (wasm.root_module.getGraph().modules) |module| { - for (module.link_objects.items) |link_object| { - switch (link_object) { - .other_step => |compile_step| { - switch (compile_step.kind) { - .lib => { - emcc.addArtifactArg(compile_step); - }, - else => {}, - } - }, - else => {}, - } - } - } + addAllLinkedArtifacts(b, emcc, wasm.root_module); } emcc.addArg("-o"); @@ -287,13 +284,15 @@ pub fn emccStep( pub fn emrunStep( b: *std.Build, + emsdk: Emsdk, html_path: []const u8, extra_args: []const []const u8, ) *std.Build.Step { - var emrun = b.addSystemCommand(&.{emrunPath(b)}); + var emrun = b.addRunArtifact(emsdk.zemscripten.artifact("passthrough")); + emrun.addFileArg(emrunPath(b, emsdk)); + emrun.addArgs(extra_args); emrun.addArg(html_path); - // emrun.addArg("--"); return &emrun.step; } diff --git a/src/emsdk_installer.zig b/src/emsdk_installer.zig new file mode 100644 index 0000000..71a23b7 --- /dev/null +++ b/src/emsdk_installer.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +fn chmod(allocator: std.mem.Allocator, path: []const u8) !void { + const argv: []const []const u8 = switch (builtin.target.os.tag) { + .linux, .macos => &.{ "chmod", "+x", path }, + .windows => &.{ "takeown", "/f", path }, + else => return, + }; + + var child = std.process.Child.init( + argv, + allocator, + ); + if (try child.spawnAndWait() != .Exited) + return error.FailedChmod; +} + +fn do_action( + allocator: std.mem.Allocator, + emsdk_path: []const u8, + action: []const u8, + version: []const u8, +) !void { + var child = std.process.Child.init( + &.{ + emsdk_path, + action, + version, + }, + allocator, + ); + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + try child.spawn(); + + return switch (try child.wait()) { + .Exited => {}, + else => |term| { + std.log.err( + "running emsdk installer failed for action {s}: {}", + .{ action, term }, + ); + return error.InstallerFailed; + }, + }; +} + +pub fn main() !void { + var allocator_strategy = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = allocator_strategy.allocator(); + defer _ = allocator_strategy.deinit(); + + const argv = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, argv); + + if (argv.len != 4) { + std.log.err( + "usage: {s} ", + .{argv[0]}, + ); + return error.InvalidArguments; + } + + const emsdk_script_path_src = argv[1]; + const version = argv[2]; + const install_path = argv[3]; + + const emsdk_dir_src = std.fs.path.dirname(emsdk_script_path_src) orelse unreachable; + + // Create copy of SDK installer in a dedicate output dir + // FIXME Replace with zig implementation of cp -r + { + const emsdk_dir_contents_src = try std.fmt.allocPrint(allocator, "{s}/.", .{emsdk_dir_src}); + var child = std.process.Child.init( + &.{ + "cp", + "-r", + emsdk_dir_contents_src, + install_path, + }, + allocator, + ); + switch (try child.spawnAndWait()) { + .Exited => {}, + else => |term| { + std.log.err( + "chmod failed with {} ", + .{term}, + ); + return error.InstallerFailed; + }, + } + } + + const emsdk_script_path_dst = try std.fmt.allocPrint( + allocator, + "{s}/emsdk", + .{install_path}, + ); + + try chmod(allocator, emsdk_script_path_dst); + try do_action(allocator, emsdk_script_path_dst, "install", version); + try do_action(allocator, emsdk_script_path_dst, "activate", version); +} diff --git a/src/passthrough.zig b/src/passthrough.zig new file mode 100644 index 0000000..2c271be --- /dev/null +++ b/src/passthrough.zig @@ -0,0 +1,37 @@ +const std = @import("std"); + +pub fn main() !u8 { + var allocator_strategy = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = allocator_strategy.allocator(); + defer _ = allocator_strategy.deinit(); + + const argv = try std.process.argsAlloc(allocator); + defer std.process.argsFree(allocator, argv); + + if (argv.len < 2) { + std.log.err("need at least one argument", .{}); + return error.InvalidArgument; + } + + var child = std.process.Child.init( + argv[1..], + allocator, + ); + + child.stderr_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stdin_behavior = .Inherit; + + switch (try child.spawnAndWait()) { + .Exited => |ret| { + return ret; + }, + else => |term| { + std.log.err( + "{s} failed with {} ", + .{ argv[1], term }, + ); + return error.InstallerFailed; + }, + } +}