const std = @import("std"); const Build = std.Build; const sokol = @import("sokol"); const game_name = "sokol_template_v2"; pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const dep_stb = b.dependency("stb", .{}); const dep_miniz = b.dependency("miniz", .{ .target = b.graph.host, }); const ctx = Context.init(b, dep_stb.module("stb_image"), dep_miniz.module("miniz")); const pkg_install_dir: Build.InstallDir = .{ .custom = "pkg" }; var run_cmd_step: *std.Build.Step = undefined; switch (try buildProject(ctx, target, optimize)) { .native => |exe| { b.installArtifact(exe); const run_cmd = b.addRunArtifact(exe); run_cmd.addPassthruArgs(); run_cmd_step = &run_cmd.step; { const exe_tests = b.addTest(.{ .root_module = exe.root_module, }); const run_exe_tests = b.addRunArtifact(exe_tests); const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_exe_tests.step); } }, .wasm => |install_dir| { b.getInstallStep().dependOn(&install_dir.step); var create_zip_run = b.addRunArtifact(ctx.create_zip); create_zip_run.addDirectoryArg(install_dir.options.source_dir); const wasm_zip = create_zip_run.addOutputFileArg("html.zip"); var html_install_file = b.addInstallFileWithDir(wasm_zip, pkg_install_dir, b.fmt("{s}_html.zip", .{game_name})); b.getInstallStep().dependOn(&html_install_file.step); const dep_sokol = b.dependency("sokol", .{}); const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk }); emrun_step.step.dependOn(&install_dir.step); run_cmd_step = &emrun_step.step; // add an emsdk install step const emsdk_install_step = sokol.emSdkInstallStep(b, dep_sokol.builder.dependency("emsdk", .{}), .{}); b.step("install-emsdk", "Install Emscripten SDK in zig-pkg").dependOn(emsdk_install_step); } } const run_step = b.step("run", "Run game"); run_step.dependOn(run_cmd_step); } const ProjectResult = union(enum) { native: *std.Build.Step.Compile, wasm: *std.Build.Step.InstallDir, }; fn buildProject(ctx: Context, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) !ProjectResult { const b = ctx.b; const assets_dir = b.path("assets"); const asset_files = [_][]const u8{ "sokoban/sokoban_tilesheet.png", "sokoban/sokoban_tilesheet@2.png", "roboto-font/Roboto-Regular.ttf", "tiled/map.tmx", "tiled/tileset.tsx", "impact-sounds/footstep_concrete_000.ogg" }; const is_wasm = target.result.cpu.arch.isWasm(); const is_debug = optimize == .debug; const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse is_debug; var assets_from_filesystem = false; if (!is_wasm) { assets_from_filesystem = b.option(bool, "assets-filesystem", "Allow reading assets from filesystem") orelse is_debug; } var has_tracy = false; if (!is_wasm) { has_tracy = b.option(bool, "tracy", "Tracy integration") orelse is_debug; } const win32_has_console = null; const win32_icon_png: ?Build.LazyPath = b.path("assets/icon.png"); const exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); const dep_tiled = b.dependency("tiled", .{}); exe_mod.addImport("tiled", dep_tiled.module("tiled")); const dep_stb = b.dependency("stb", .{}); exe_mod.addImport("stb_image", dep_stb.module("stb_image")); exe_mod.addImport("stb_truetype", dep_stb.module("stb_truetype")); var dep_sokol: *Build.Dependency = undefined; const platform_mod = b.createModule(.{ .root_source_file = b.path("src/platform/root.zig") }); exe_mod.addImport("platform", platform_mod); { const build_options = b.addOptions(); build_options.addOption(bool, "has_imgui", has_imgui); if (assets_from_filesystem) { build_options.addOptionPathDirectory("assets_dir", assets_dir); } platform_mod.addOptions("build_options", build_options); platform_mod.addImport("embedded_assets", try createAssetsModule(b, assets_dir, &asset_files)); const math_mod = b.createModule(.{ .root_source_file = b.path("src/platform/math.zig") }); platform_mod.addImport("math", math_mod); dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, .with_sokol_imgui = has_imgui }); const mod_sokol = dep_sokol.module("sokol"); platform_mod.linkLibrary(dep_sokol.artifact("sokol_clib")); platform_mod.addImport("sokol", mod_sokol); const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); var slang = sokol.shdc.Slang{}; if (is_wasm) { slang.wgsl = true; slang.glsl300es = true; } else { slang.glsl410 = true; slang.hlsl4 = true; slang.metal_macos = true; slang.glsl300es = true; } const mod_shader = try sokol.shdc.createModule(b, "shader", mod_sokol, .{ .shdc_dep = dep_shdc, .input = "src/platform/shader.glsl", .output = "src/platform/shader.zig", .slang = slang }); mod_shader.addImport("math", math_mod); platform_mod.addImport("shader", mod_shader); if (has_imgui) { if (b.lazyDependency("cimgui", .{ .target = target, .optimize = optimize, })) |dep_cimgui| { const cimgui = b.lazyImport(@This(), "cimgui").?; const cimgui_conf = cimgui.getConfig(true); platform_mod.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name)); dep_sokol.artifact("sokol_clib").root_module.addIncludePath(dep_cimgui.path(cimgui_conf.include_dir)); } } platform_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); platform_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack")); const dep_tracy = b.dependency("tracy", .{ .target = target, .optimize = optimize, .tracy_enable = has_tracy, .tracy_only_localhost = true }); if (has_tracy) { platform_mod.linkLibrary(dep_tracy.artifact("tracy")); } platform_mod.addImport("tracy", dep_tracy.module("tracy")); } if (is_wasm) { return ProjectResult{ .wasm = buildWasm(b, .{ .name = "index", .root_module = exe_mod, .dep_sokol = dep_sokol, }) }; } else { const exe = b.addExecutable(.{ .name = game_name, .root_module = exe_mod, }); if (target.result.os.tag == .windows) { const has_console = win32_has_console orelse is_debug; exe.subsystem = if (has_console) .console else .windows; if (win32_icon_png) |icon_png| { buildWin32ResourceFile(ctx, exe.root_module, .{ .icon_png = icon_png }); } } return ProjectResult{ .native = exe }; } } fn createAssetsModule( b: *std.Build, assets_dir: Build.LazyPath, asset_files: []const []const u8 ) !*std.Build.Module { const write_files = b.addWriteFiles(); var mod_contents: std.ArrayList(u8) = .empty; defer mod_contents.deinit(b.allocator); try mod_contents.appendSlice(b.allocator, "pub const files = .{\n"); const mod = b.createModule(.{}); for (asset_files) |asset_file| { mod.addAnonymousImport(asset_file, .{ .root_source_file = assets_dir.path(b, asset_file) }); try mod_contents.appendSlice(b.allocator, " .{ "); try mod_contents.appendSlice(b.allocator, ".name = \""); try mod_contents.appendSlice(b.allocator, asset_file); try mod_contents.appendSlice(b.allocator, "\""); try mod_contents.appendSlice(b.allocator, ", "); try mod_contents.appendSlice(b.allocator, ".contents = @embedFile(\""); try mod_contents.appendSlice(b.allocator, asset_file); try mod_contents.appendSlice(b.allocator, "\")"); try mod_contents.appendSlice(b.allocator, " },\n"); } try mod_contents.appendSlice(b.allocator, "};\n"); mod.root_source_file = write_files.add("assets.zig", mod_contents.items); return mod; } const Context = struct { b: *std.Build, png_to_icon: *Build.Step.Compile, create_win32_resource: *Build.Step.Compile, create_zip: *Build.Step.Compile, pub fn init(b: *std.Build, stb_image: *Build.Module, miniz: *Build.Module) Context { return Context{ .b = b, .png_to_icon = b.addExecutable(.{ .name = "png-to-icon", .root_module = b.createModule(.{ .target = b.graph.host, .root_source_file = b.path("scripts/png-to-icon.zig"), .imports = &.{ .{ .name = "stb_image", .module = stb_image } } }) }), .create_win32_resource = b.addExecutable(.{ .name = "create-win32-resource", .root_module = b.createModule(.{ .target = b.graph.host, .root_source_file = b.path("scripts/create-win32-resource.zig"), }) }), .create_zip = b.addExecutable(.{ .name = "create-zip", .root_module = b.createModule(.{ .target = b.graph.host, .root_source_file = b.path("scripts/create-zip.zig"), .imports = &.{ .{ .name = "miniz", .module = miniz } } }) }) }; } }; const Win32ResourceFileOptions = struct { icon_png: Build.LazyPath }; fn buildWin32ResourceFile(ctx: Context, mod: *Build.Module, opts: Win32ResourceFileOptions) void { const b = ctx.b; const png_to_icon_step = b.addRunArtifact(ctx.png_to_icon); png_to_icon_step.addFileArg(opts.icon_png); const icon_file = png_to_icon_step.addOutputFileArg("icon.ico"); const resource_file_step = b.addRunArtifact(ctx.create_win32_resource); resource_file_step.addFileArg(icon_file); const resource_file = resource_file_step.addOutputFileArg("resource-file.rc"); mod.addWin32ResourceFile(.{ .file = resource_file }); } const BuildWasmOptions = struct { name: []const u8, root_module: *std.Build.Module, dep_sokol: *std.Build.Dependency, }; fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step.InstallDir { opts.root_module.sanitize_c = .off; // build the main file into a library, this is because the WASM 'exe' // needs to be linked in a separate build step with the Emscripten linker const main_lib = b.addLibrary(.{ .name = opts.name, .root_module = opts.root_module, }); const dep_sokol = opts.dep_sokol; const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); // TODO: // const emsdk_install_step = sokol.emSdkInstallStep(b, dep_emsdk, .{}); // main_lib.step.dependOn(emsdk_install_step); patchWasmIncludeDirs( opts.root_module, dep_emsdk.path("upstream/emscripten/cache/sysroot/include"), &(dep_sokol.artifact("sokol_clib").step) ); const link_step = sokol.emLinkStep(b, .{ .lib_main = main_lib, .target = opts.root_module.resolved_target.?, .optimize = opts.root_module.optimize.?, .emsdk = dep_emsdk, .use_webgl2 = true, .use_emmalloc = true, .use_filesystem = true, .shell_file_path = b.path("src/platform/shell.html"), .extra_args = &.{ "-sALLOW_MEMORY_GROWTH", "-sENVIRONMENT=web" } }) catch unreachable; return link_step; } fn patchWasmIncludeDirs( module: *std.Build.Module, path: std.Build.LazyPath, depend_step: *std.Build.Step ) void { if (module.link_libc != null and module.link_libc.?) { // need to inject the Emscripten system header include path into // the cimgui C library otherwise the C/C++ code won't find // C stdlib headers module.addSystemIncludePath(path); } for (module.import_table.values()) |imported_module| { patchWasmIncludeDirs(imported_module, path, depend_step); } for (module.link_objects.items) |link_object| { if (link_object != .other_step) { continue; } const lib = link_object.other_step; if (&lib.step == depend_step) { continue; } if (lib.root_module.link_libc != null and lib.root_module.link_libc.?) { // need to inject the Emscripten system header include path into // the cimgui C library otherwise the C/C++ code won't find // C stdlib headers lib.root_module.addSystemIncludePath(path); // all C libraries need to depend on the sokol library, when building for // WASM this makes sure that the Emscripten SDK has been setup before // C compilation is attempted (since the sokol C library depends on the // Emscripten SDK setup step) lib.step.dependOn(depend_step); } patchWasmIncludeDirs(lib.root_module, path, depend_step); } }