[build.zig]: a concrete approach to build for web

This commit is contained in:
haxsam 2025-08-31 16:16:49 +02:00 committed by Nikolas
parent df70c5c952
commit b44a355a81
7 changed files with 211 additions and 238 deletions

View File

@ -55,9 +55,9 @@ want to run an example, say `basic_window` run `zig build basic_window`
### Using raylib-zig's template
* Execute `project_setup.sh project_name`, this will create a folder with the name specified
* You can copy that folder anywhere you want and edit the source
* Run `zig build run` at any time to test your project
- Execute `project_setup.sh project_name`, this will create a folder with the name specified
- You can copy that folder anywhere you want and edit the source
- Run `zig build run` at any time to test your project
### In an existing project (e.g. created with `zig init`)
@ -88,8 +88,8 @@ exe.root_module.addImport("raylib", raylib);
exe.root_module.addImport("raygui", raygui);
```
If you additionally want to support Web as a platform with emscripten, you will need to use `emcc.zig` by importing
raylib-zig's build script with `const rlz = @import("raylib_zig");` and then accessing its functions with `rlz.emcc`.
If you additionally want to support Web as a platform with emscripten, you will need to use `emsdk` by importing
raylib-zig's build script with `const rlz = @import("raylib_zig");` and then accessing like described here [Exporting for web](https://github.com/raylib-zig/raylib-zig?tab=readme-ov-file#exporting-for-web).
Refer to raylib-zig's project template on how to use them.
### Passing build options
@ -118,16 +118,50 @@ raylib_artifact.root_module.addCMacro("SUPPORT_FILEFORMAT_JPG", "");
## Exporting for web
To export your project for the web, first install emsdk.
Once emsdk is installed, set it up by running
To export your project for the web, first add emsdk to your dependencies.
Its also possible to use a local emsdk folder.
`emsdk install latest`
`zig fetch --save git+https://github.com/emscripten-core/emsdk#4.0.9`
Find the folder where it's installed and run
Add this to your build method to build for the web
`zig build -Dtarget=wasm32-emscripten --sysroot [path to emsdk]/upstream/emscripten`
```zig
if (target.query.os_tag == .emscripten) {
const emsdk = rlz.emsdk;
const wasm = b.addLibrary(.{
.name = <your_project_name>,
.root_module = exe_mod,
});
once that is finished, the exported project should be located at `zig-out/htmlout`
const install_dir: std.Build.InstallDir = .{ .custom = "web" };
const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{ .optimize = optimize });
const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{ .optimize = optimize });
const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
.optimize = optimize,
.flags = emcc_flags,
.settings = emcc_settings,
.install_dir = install_dir,
});
b.getInstallStep().dependOn(emcc_step);
const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
emrun_step.dependOn(emcc_step);
run_step.dependOn(emrun_step);
}
```
then you can run
`zig build -Dtarget=wasm32-emscripten`
once that is finished, the exported project should be located at `zig-out/web`
### When is the binding updated?
@ -136,6 +170,6 @@ implementation stuff should be updatable with some hacks on your side.
### What needs to be done?
+ _(Done)_ Set up a proper package build and a build script for the examples
+ Port all the examples
+ Member functions/initialisers
- _(Done)_ Set up a proper package build and a build script for the examples
- Port all the examples
- Member functions/initialisers

View File

@ -3,8 +3,7 @@
const std = @import("std");
const this = @This();
const rl = @import("raylib");
pub const emcc = @import("emcc.zig");
pub const emsdk = rl.emsdk;
pub const Options = rl.Options;
pub const OpenglVersion = rl.OpenglVersion;
@ -27,7 +26,7 @@ fn getRaylib(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.buil
.rtext = options.rtext,
.rtextures = options.rtextures,
.platform = options.platform,
.shared = options.shared,
.linkage = options.linkage,
.linux_display_backend = options.linux_display_backend,
.opengl_version = options.opengl_version,
.android_api_version = options.android_api_version,
@ -390,33 +389,54 @@ pub fn build(b: *std.Build) !void {
const examples_step = b.step("examples", "Builds all the examples");
for (examples) |ex| {
const mod = b.createModule(.{
.root_source_file = b.path(ex.path),
.target = target,
.optimize = optimize,
});
if (target.query.os_tag == .emscripten) {
const exe_lib = try emcc.compileForEmscripten(b, ex.name, ex.path, target, optimize);
exe_lib.root_module.addImport("raylib", raylib);
exe_lib.root_module.addImport("raygui", raygui);
const wasm = b.addLibrary(.{
.name = ex.name,
.root_module = mod,
});
wasm.root_module.addImport("raylib", raylib);
wasm.root_module.addImport("raygui", raygui);
wasm.linkLibrary(raylib_artifact);
// Note that raylib itself isn't actually added to the exe_lib
// output file, so it also needs to be linked with emscripten.
exe_lib.linkLibrary(raylib_artifact);
const link_step = try emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact });
link_step.addArg("--emrun");
link_step.addArg("--embed-file");
link_step.addArg("resources/");
const install_dir: std.Build.InstallDir = .{ .custom = "web" };
const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{
.optimize = optimize,
.asyncify = !std.mem.endsWith(u8, ex.name, "web"),
});
const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{
.optimize = optimize,
});
const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
.optimize = optimize,
.flags = emcc_flags,
.settings = emcc_settings,
.shell_file_path = emsdk.shell(b),
.install_dir = install_dir,
.embed_paths = &.{.{ .src_path = "resources/" }},
});
const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
emrun_step.dependOn(emcc_step);
const run_step = try emcc.emscriptenRunStep(b);
run_step.step.dependOn(&link_step.step);
const run_option = b.step(ex.name, ex.desc);
run_option.dependOn(&run_step.step);
examples_step.dependOn(&exe_lib.step);
run_option.dependOn(emrun_step);
examples_step.dependOn(emcc_step);
} else {
const exe = b.addExecutable(.{
.name = ex.name,
.root_module = b.createModule(.{
.root_source_file = b.path(ex.path),
.target = target,
.optimize = optimize,
}),
.root_module = mod,
});
exe.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib);

View File

@ -4,13 +4,17 @@
.fingerprint = 0xc4cfa8c610114f28,
.dependencies = .{
.raylib = .{
.url = "git+https://github.com/raysan5/raylib?ref=master#f83c5cb6e1b3462c9b723c0ab3b479951aa48458",
.hash = "raylib-5.5.0-whq8uBlJxwRAvOHHNQIi8WS0QTbQJcdx7FbjSSOnPn6n",
.url = "git+https://github.com/raysan5/raylib#82d65e110a2caba862b31a3f1c11ca7a8ba60e3b",
.hash = "raylib-5.6.0-dev-whq8uHRjyARJNFHTJ8fBtYU71IrnB-rTHJwuasjUnRWt",
},
.raygui = .{
.url = "git+https://github.com/raysan5/raygui#6530ee136b3c5af86c5640151f07837a604308ec",
.hash = "N-V-__8AAOQabwCjOjMI2uUTw4Njc0tAUOO6Lw2kCydLbvVG",
},
.emsdk = .{
.url = "git+https://github.com/emscripten-core/emsdk#4.0.9",
.hash = "N-V-__8AAJl1DwBezhYo_VE6f53mPVm00R-Fk28NPW7P14EQ",
},
},
.minimum_zig_version = "0.15.1",
.paths = .{

126
emcc.zig
View File

@ -1,126 +0,0 @@
// raylib-zig (c) Nikolas Wipper 2020-2024
const std = @import("std");
const builtin = @import("builtin");
const emccOutputDir = "zig-out" ++ std.fs.path.sep_str ++ "htmlout" ++ std.fs.path.sep_str;
const emccOutputFile = "index.html";
pub fn emscriptenRunStep(b: *std.Build) !*std.Build.Step.Run {
// If compiling on windows , use emrun.bat.
const emrunExe = switch (builtin.os.tag) {
.windows => "emrun.bat",
else => "emrun",
};
var emrun_run_arg = try b.allocator.alloc(u8, b.sysroot.?.len + emrunExe.len + 1);
defer b.allocator.free(emrun_run_arg);
if (b.sysroot == null) {
emrun_run_arg = try std.fmt.bufPrint(emrun_run_arg, "{s}", .{emrunExe});
} else {
emrun_run_arg = try std.fmt.bufPrint(emrun_run_arg, "{s}" ++ std.fs.path.sep_str ++ "{s}", .{ b.sysroot.?, emrunExe });
}
const run_cmd = b.addSystemCommand(&[_][]const u8{ emrun_run_arg, emccOutputDir ++ emccOutputFile });
return run_cmd;
}
// Creates the static library to build a project for Emscripten.
pub fn compileForEmscripten(
b: *std.Build,
name: []const u8,
root_source_file: []const u8,
target: std.Build.ResolvedTarget,
optimize: std.builtin.OptimizeMode,
) !*std.Build.Step.Compile {
// TODO: It might be a good idea to create a custom compile step, that does
// both the compile to static library and the link with emcc by overidding
// the make function of the step. However it might also be a bad idea since
// it messes with the build system itself.
// The project is built as a library and linked later.
const lib = b.addLibrary(.{
.name = name,
.linkage = .static,
.root_module = b.createModule(.{
.root_source_file = b.path(root_source_file),
.target = target,
.optimize = optimize,
}),
});
const emscripten_headers = try std.fs.path.join(b.allocator, &.{ b.sysroot.?, "cache", "sysroot", "include" });
defer b.allocator.free(emscripten_headers);
lib.addIncludePath(.{ .cwd_relative = emscripten_headers });
return lib;
}
// Links a set of items together using emscripten.
//
// Will accept objects and static libraries as items to link. As for files to
// include, it is recomended to have a single resources directory and just pass
// the entire directory instead of passing every file individually. The entire
// path given will be the path to read the file within the program. So, if
// "resources/image.png" is passed, your program will use "resources/image.png"
// as the path to load the file.
//
// TODO: Test if shared libraries are accepted, I don't remember if emcc can
// link a shared library with a project or not.
// TODO: Add a parameter that allows a custom output directory.
pub fn linkWithEmscripten(
b: *std.Build,
itemsToLink: []const *std.Build.Step.Compile,
) !*std.Build.Step.Run {
const emccExe = switch (builtin.os.tag) {
.windows => "emcc.bat",
else => "emcc",
};
var emcc_run_arg = try b.allocator.alloc(u8, b.sysroot.?.len + emccExe.len + 1);
defer b.allocator.free(emcc_run_arg);
if (b.sysroot == null) {
emcc_run_arg = try std.fmt.bufPrint(emcc_run_arg, "{s}", .{emccExe});
} else {
emcc_run_arg = try std.fmt.bufPrint(
emcc_run_arg,
"{s}" ++ std.fs.path.sep_str ++ "{s}",
.{ b.sysroot.?, emccExe },
);
}
// Create the output directory because emcc can't do it.
const mkdir_command = switch (builtin.os.tag) {
.windows => b.addSystemCommand(&.{ "cmd.exe", "/c", "if", "not", "exist", emccOutputDir, "mkdir", emccOutputDir }),
else => b.addSystemCommand(&.{ "mkdir", "-p", emccOutputDir }),
};
// Actually link everything together.
const emcc_command = b.addSystemCommand(&[_][]const u8{emcc_run_arg});
for (itemsToLink) |item| {
emcc_command.addFileArg(item.getEmittedBin());
emcc_command.step.dependOn(&item.step);
}
// This puts the file in zig-out/htmlout/index.html.
emcc_command.step.dependOn(&mkdir_command.step);
emcc_command.addArgs(&[_][]const u8{
"-o",
emccOutputDir ++ emccOutputFile,
"-sUSE_OFFSET_CONVERTER",
"-sFULL-ES3=1",
"-sUSE_GLFW=3",
"-sASYNCIFY",
"-O3",
"-fsanitize=undefined",
});
return emcc_command;
}
// TODO: See if zig's standard library already has somehing like this.
fn lastIndexOf(string: []const u8, character: u8) usize {
// Interestingly, Zig has no nice way of iterating a slice backwards.
for (0..string.len) |i| {
const index = string.len - i - 1;
if (string[index] == character) return index;
}
return string.len - 1;
}

View File

@ -1,11 +1,10 @@
// A raylib-zig port of https://github.com/raysan5/raylib/blob/master/examples/core/core_basic_window_web.c
const std = @import("std");
const rl = @import("raylib");
const builtin = @import("builtin");
const c = if (builtin.os.tag == .emscripten) @cImport({
@cInclude("emscripten/emscripten.h");
});
const emscripten = std.os.emscripten;
//----------------------------------------------------------------------------------
// Global Variables Definition
@ -24,7 +23,7 @@ pub fn main() anyerror!void {
defer rl.closeWindow(); // Close window and OpenGL context
if (builtin.os.tag == .emscripten) {
c.emscripten_set_main_loop(@ptrCast(&updateDrawFrame), 0, 1);
emscripten.emscripten_set_main_loop(@ptrCast(&updateDrawFrame), 0, 1);
} else {
rl.setTargetFPS(60); // Set our game to run at 60 frames-per-second

View File

@ -24,55 +24,78 @@ pub fn build(b: *std.Build) !void {
.target = target,
.optimize = optimize,
});
const raylib = raylib_dep.module("raylib");
const raylib_artifact = raylib_dep.artifact("raylib");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("raylib", raylib);
exe_mod.linkLibrary(raylib_artifact);
const run_step = b.step("run", "Run the app");
//web exports are completely separate
if (target.query.os_tag == .emscripten) {
const exe_lib = try rlz.emcc.compileForEmscripten(b, "$PROJECT_NAME", "src/main.zig", target, optimize);
const emsdk = rlz.emsdk;
const wasm = b.addLibrary(.{
.name = "$PROJECT_NAME",
.root_module = exe_mod,
});
exe_lib.linkLibrary(raylib_artifact);
exe_lib.root_module.addImport("raylib", raylib);
// Note that raylib itself is not actually added to the exe_lib output file, so it also needs to be linked with emscripten.
const link_step = try rlz.emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact });
//this lets your program access files like "resources/my-image.png":
link_step.addArg("--emrun");
link_step.addArg("--embed-file");
link_step.addArg("resources/");
b.getInstallStep().dependOn(&link_step.step);
const run_step = try rlz.emcc.emscriptenRunStep(b);
run_step.step.dependOn(&link_step.step);
const run_option = b.step("run", "Run $PROJECT_NAME");
run_option.dependOn(&run_step.step);
return;
}
const exe = b.addExecutable(.{
.name = "'$PROJECT_NAME'",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
const install_dir: std.Build.InstallDir = .{ .custom = "web" };
const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{
.optimize = optimize,
.target = target
}),
});
});
const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{
.optimize = optimize,
});
exe.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib);
const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
.optimize = optimize,
.flags = emcc_flags,
.settings = emcc_settings,
.shell_file_path = emsdk.shell(raylib_dep.builder),
.embed_paths = &.{
.{
.src_path = b.pathJoin(&.{ module_subpath, "resources" }),
.virtual_path = "resources",
},
},
.install_dir = install_dir,
});
b.getInstallStep().dependOn(emcc_step);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run $PROJECT_NAME");
run_step.dependOn(&run_cmd.step);
const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
b.installArtifact(exe);
emrun_step.dependOn(emcc_step);
run_step.dependOn(emrun_step);
} else {
const exe = b.addExecutable(.{
.name = "$PROJECT_NAME",
.root_module = exe_mod,
});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_step.dependOn(&run_cmd.step);
}
}
"@
New-Item -Name "build.zig" -ItemType "file" -Value $BUILD_DOT_ZIG -Force
zig fetch --save git+https://github.com/raylib-zig/raylib-zig#devel
zig fetch --save git+https://github.com/Not-Nik/raylib-zig#devel
zig fetch --save git+https://github.com/emscripten-core/emsdk#4.0.9
New-Item -Name "resources" -ItemType "directory"
New-Item -Name "resources/placeholder.txt" -ItemType "file" -Value "" -Force

View File

@ -25,52 +25,71 @@ pub fn build(b: *std.Build) !void {
.target = target,
.optimize = optimize,
});
const raylib = raylib_dep.module("raylib");
const raylib_artifact = raylib_dep.artifact("raylib");
const exe_mod = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
exe_mod.addImport("raylib", raylib);
exe_mod.linkLibrary(raylib_artifact);
const run_step = b.step("run", "Run the app");
//web exports are completely separate
if (target.query.os_tag == .emscripten) {
const exe_lib = try rlz.emcc.compileForEmscripten(b, "'$PROJECT_NAME'", "src/main.zig", target, optimize);
const emsdk = rlz.emsdk;
const wasm = b.addLibrary(.{
.name = "'$PROJECT_NAME'",
.root_module = exe_mod,
});
exe_lib.linkLibrary(raylib_artifact);
exe_lib.root_module.addImport("raylib", raylib);
const install_dir: std.Build.InstallDir = .{ .custom = "web" };
const emcc_flags = emsdk.emccDefaultFlags(b.allocator, .{ .optimize = optimize });
const emcc_settings = emsdk.emccDefaultSettings(b.allocator, .{ .optimize = optimize });
// Note that raylib itself is not actually added to the exe_lib output file, so it also needs to be linked with emscripten.
const link_step = try rlz.emcc.linkWithEmscripten(b, &[_]*std.Build.Step.Compile{ exe_lib, raylib_artifact });
//this lets your program access files like "resources/my-image.png":
link_step.addArg("--emrun");
link_step.addArg("--embed-file");
link_step.addArg("resources/");
b.getInstallStep().dependOn(&link_step.step);
const run_step = try rlz.emcc.emscriptenRunStep(b);
run_step.step.dependOn(&link_step.step);
const run_option = b.step("run", "Run '$PROJECT_NAME'");
run_option.dependOn(&run_step.step);
return;
}
const exe = b.addExecutable(.{
.name = "'$PROJECT_NAME'",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
const emcc_step = emsdk.emccStep(b, raylib_artifact, wasm, .{
.optimize = optimize,
.target = target
}),
});
.flags = emcc_flags,
.settings = emcc_settings,
.shell_file_path = emsdk.shell(raylib_dep.builder),
.embed_paths = &.{
.{
.src_path = b.pathJoin(&.{ module_subpath, "resources" }),
.virtual_path = "resources",
},
},
.install_dir = install_dir,
});
b.getInstallStep().dependOn(emcc_step);
exe.linkLibrary(raylib_artifact);
exe.root_module.addImport("raylib", raylib);
const html_filename = try std.fmt.allocPrint(b.allocator, "{s}.html", .{wasm.name});
const emrun_step = emsdk.emrunStep(
b,
b.getInstallPath(install_dir, html_filename),
&.{},
);
const run_cmd = b.addRunArtifact(exe);
const run_step = b.step("run", "Run '$PROJECT_NAME'");
run_step.dependOn(&run_cmd.step);
emrun_step.dependOn(emcc_step);
run_step.dependOn(emrun_step);
} else {
const exe = b.addExecutable(.{
.name = "'$PROJECT_NAME'",
.root_module = exe_mod,
});
b.installArtifact(exe);
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
run_step.dependOn(&run_cmd.step);
}
}' >> build.zig
zig fetch --save git+https://github.com/raylib-zig/raylib-zig#devel
zig fetch --save git+https://github.com/Not-Nik/raylib-zig#devel
zig fetch --save git+https://github.com/emscripten-core/emsdk#4.0.9
mkdir resources
touch resources/placeholder.txt