const std = @import("std"); const sokol = @import("sokol"); pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const assets_dir = "assets"; const asset_files = [_][]const u8{ "sokoban/sokoban_tilesheet.png", "roboto-font/Roboto-Regular.ttf", "tiled/map.tmx", "tiled/tileset.tsx" }; const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir }); 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; const 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 exe_mod = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, }); const build_options = b.addOptions(); build_options.addOption(bool, "has_imgui", has_imgui); build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null); exe_mod.addOptions("build_options", build_options); exe_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files)); const math_mod = b.createModule(.{ .root_source_file = b.path("src/math.zig") }); exe_mod.addImport("math", math_mod); const dep_tiled = b.dependency("tiled", .{}); exe_mod.addImport("tiled", dep_tiled.module("tiled")); const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, .with_sokol_imgui = has_imgui }); const mod_sokol = dep_sokol.module("sokol"); exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib")); exe_mod.addImport("sokol", mod_sokol); const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); var slang = sokol.shdc.Slang{}; if (is_wasm) { slang.wgsl = 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/shader.glsl", .output = "src/shader.zig", .slang = slang }); mod_shader.addImport("math", math_mod); exe_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); exe_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)); } } const dep_stb = b.dependency("stb", .{}); exe_mod.addImport("stb_image", dep_stb.module("stb_image")); exe_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); exe_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack")); exe_mod.addImport("stb_truetype", dep_stb.module("stb_truetype")); const dep_tracy = b.dependency("tracy", .{ .target = target, .optimize = optimize, .tracy_enable = has_tracy, .tracy_only_localhost = true }); if (has_tracy) { exe_mod.linkLibrary(dep_tracy.artifact("tracy")); } exe_mod.addImport("tracy", dep_tracy.module("tracy")); var run_cmd_step: *std.Build.Step = undefined; if (is_wasm) { if (true) { @panic("TODO: test in 0.17"); } const build_step = buildWasm(b, .{ .name = "index", .root_module = exe_mod, .dep_sokol = dep_sokol, }); b.getInstallStep().dependOn(build_step); const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk }); emrun_step.step.dependOn(build_step); run_cmd_step = &emrun_step.step; // TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload } else { const exe = buildNative(b, .{ .name = "sokol_template_v2", .root_module = exe_mod }); b.installArtifact(exe); { const run_cmd = b.addRunArtifact(exe); if (b.args) |args| { run_cmd.addArgs(args); } run_cmd_step = &run_cmd.step; } { const exe_tests = b.addTest(.{ .root_module = exe_mod, }); const run_exe_tests = b.addRunArtifact(exe_tests); const test_step = b.step("test", "Run tests"); test_step.dependOn(&run_exe_tests.step); } } const run_step = b.step("run", "Run game"); run_step.dependOn(run_cmd_step); } fn createEmbeddedAssets( b: *std.Build, assets_dir: []const u8, asset_files: []const []const u8 ) !*std.Build.Module { const write_files = b.addWriteFiles(); var embedded_assets: std.ArrayList(u8) = .empty; defer embedded_assets.deinit(b.allocator); try embedded_assets.appendSlice(b.allocator, "pub const files = .{\n"); const mod_embedded_assets = b.createModule(.{}); for (asset_files) |asset_file| { mod_embedded_assets.addAnonymousImport(asset_file, .{ .root_source_file = b.path(b.pathJoin(&.{ assets_dir, asset_file })) }); try embedded_assets.appendSlice(b.allocator, " .{ "); try embedded_assets.appendSlice(b.allocator, ".name = \""); try embedded_assets.appendSlice(b.allocator, asset_file); try embedded_assets.appendSlice(b.allocator, "\""); try embedded_assets.appendSlice(b.allocator, ", "); try embedded_assets.appendSlice(b.allocator, ".contents = @embedFile(\""); try embedded_assets.appendSlice(b.allocator, asset_file); try embedded_assets.appendSlice(b.allocator, "\")"); try embedded_assets.appendSlice(b.allocator, " },\n"); } try embedded_assets.appendSlice(b.allocator, "};\n"); mod_embedded_assets.root_source_file = write_files.add("embedded_assets.zig", embedded_assets.items); return mod_embedded_assets; } const BuildNativeOptions = struct { name: []const u8, root_module: *std.Build.Module, win32_has_console: ?bool = null, win32_png_icon: ?std.Build.LazyPath = null }; fn buildNative( b: *std.Build, opts: BuildNativeOptions, ) *std.Build.Step.Compile { const exe = b.addExecutable(.{ .name = opts.name, .root_module = opts.root_module, }); const target = opts.root_module.resolved_target.?; if (target.result.os.tag == .windows) { const optimize = opts.root_module.optimize.?; const has_console = opts.win32_has_console orelse (optimize == .Debug); exe.subsystem = if (has_console) .Console else .Windows; if (opts.win32_png_icon) |win32_png_icon| { const png_to_icon_tool = buildPngToIconTool(b, b.graph.host, .ReleaseSafe); const png_to_icon_step = b.addRunArtifact(png_to_icon_tool); png_to_icon_step.addFileArg(win32_png_icon); const icon_file = png_to_icon_step.addOutputFileArg("icon.ico"); const add_icon_step = AddExecutableIcon.init(exe, icon_file); exe.step.dependOn(&add_icon_step.step); } } return exe; } fn buildPngToIconTool( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) *std.Build.Step.Compile { const mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("tools/png-to-icon.zig"), }); const dep_stb = b.dependency("stb", .{}); mod.addImport("stb_image", dep_stb.module("stb_image")); return b.addExecutable(.{ .name = "png-to-icon", .root_module = mod, }); } const AddExecutableIcon = struct { obj: *std.Build.Step.Compile, step: std.Build.Step, icon_file: std.Build.LazyPath, resource_file: std.Build.LazyPath, fn init(obj: *std.Build.Step.Compile, icon_file: std.Build.LazyPath) *AddExecutableIcon { const b = obj.step.owner; const self = b.allocator.create(AddExecutableIcon) catch @panic("OOM"); self.obj = obj; self.step = std.Build.Step.init(.{ .id = .custom, .name = "add executable icon", .owner = b, .makeFn = make }); self.icon_file = icon_file; icon_file.addStepDependencies(&self.step); const write_files = b.addWriteFiles(); self.resource_file = write_files.add("resource-file.rc", ""); self.step.dependOn(&write_files.step); self.obj.root_module.addWin32ResourceFile(.{ .file = self.resource_file, }); return self; } fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void { const b = step.owner; const self: *AddExecutableIcon = @fieldParentPtr("step", step); const relative_icon_path = try std.fs.path.relative( b.allocator, "", null, self.resource_file.dirname().getPath(b), self.icon_file.getPath(b) ); std.mem.replaceScalar(u8, relative_icon_path, '\\', '/'); { const cwd = std.Io.Dir.cwd(); const resource_file = try cwd.createFile(b.graph.io, self.resource_file.getPath(b), .{ }); defer resource_file.close(b.graph.io); var buff: [4*4096]u8 = undefined; var file_writer = resource_file.writer(b.graph.io, &buff); const w = &file_writer.interface; try w.writeAll("IDI_ICON ICON \""); try w.writeAll(relative_icon_path); try w.writeAll("\""); } } }; 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 { 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 = false, .shell_file_path = b.path("src/engine/shell.html"), }) catch unreachable; return &link_step.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); } }