sokoban-v2/build.zig

488 lines
17 KiB
Zig

const std = @import("std");
const Build = std.Build;
const sokol = @import("sokol");
const game_name = "sokoban";
const itchio_target = "SlimeBOOS/sokoban";
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" };
const run_step = b.step("run", "Run game");
const artifact = try buildProject(ctx, target, optimize, .{
.imgui = b.option(bool, "imgui", "ImGui integration"),
.assets_from_filesystem = b.option(bool, "assets-filesystem", "Allow reading assets from filesystem"),
.tracy = b.option(bool, "tracy", "Tracy integration")
});
switch (artifact) {
.native => |exe| {
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.addPassthruArgs();
run_step.dependOn(&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);
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_step.dependOn(&emrun_step.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 package_suffix = getPackageSuffix(target);
const package_name = b.fmt("{s}_{s}.zip", .{game_name, package_suffix});
const package = artifact.createPackage(ctx, package_name);
const package_step = b.step("package", "Pack artifacts into package");
package_step.dependOn(&b.addInstallFileWithDir(package, pkg_install_dir, package_name).step);
const deploy_step = b.step("deploy", "Deploy package to itch.io");
const deploy_run = butlerPush(b, package, package_suffix);
deploy_step.dependOn(&deploy_run.step);
}
{
const deploy_all_step = b.step("deploy-all", "Deploy windows,linux and web to itch.io");
inline for (.{
b.graph.host, // TODO: Add proper target for linux. Encountered problems with zig complaining about dynamic libraries.
b.resolveTargetQuery(.{
.os_tag = .windows,
.cpu_arch = .x86_64
}),
b.resolveTargetQuery(.{
.os_tag = .emscripten,
.cpu_arch = .wasm32
}),
}) |deploy_target| {
const deploy_artifact = try buildProject(ctx, deploy_target, .ReleaseSmall, .{});
const package_suffix = getPackageSuffix(deploy_target);
const package_name = b.fmt("{s}_{s}.zip", .{game_name, package_suffix});
const package = deploy_artifact.createPackage(ctx, package_name);
const deploy_run = butlerPush(b, package, package_suffix);
deploy_all_step.dependOn(&deploy_run.step);
}
}
}
fn butlerPush(b: *Build, package: Build.LazyPath, channel: []const u8) *Build.Step.Run {
const deploy_run = b.addSystemCommand(&.{"butler", "push"});
deploy_run.addFileArg(package);
deploy_run.addArg(b.fmt("{s}:{s}", .{itchio_target, channel}));
deploy_run.has_side_effects = true;
return deploy_run;
}
const ProjectResult = union(enum) {
native: *std.Build.Step.Compile,
wasm: *std.Build.Step.InstallDir,
pub fn createPackage(self: ProjectResult, ctx: Context, output_name: []const u8) Build.LazyPath {
switch (self) {
.native => |exe| {
return buildZip(ctx, output_name, &.{
.{ .file = exe.getEmittedBin() }
});
},
.wasm => |install_dir| {
return buildZip(ctx, output_name, &.{
.{ .directory = install_dir.options.source_dir }
});
}
}
}
};
fn getPackageSuffix(target: Build.ResolvedTarget) []const u8 {
return switch(target.result.os.tag) {
.emscripten => "web",
.linux => "linux",
.windows => "windows",
else => @panic("Unsupported target")
};
}
const bytes_per_kib = 1024;
const bytes_per_mib = bytes_per_kib * 1024;
const ProjectOptions = struct {
stack_size: usize = 4 * bytes_per_mib,
imgui: ?bool = null,
assets_from_filesystem: ?bool = null,
tracy: ?bool = null,
};
fn buildProject(ctx: Context, target: Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, opts: ProjectOptions) !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 = opts.imgui orelse is_debug;
var assets_from_filesystem = false;
if (!is_wasm) {
assets_from_filesystem = opts.assets_from_filesystem orelse is_debug;
}
var has_tracy = false;
if (!is_wasm) {
has_tracy = opts.tracy 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);
}
if (is_debug and !is_wasm) {
build_options.addOption(usize, "stack_size_limit", opts.stack_size);
}
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 = b.createModule(.{
.root_source_file = try sokol.shdc.compile(b, .{
.shdc_dep = dep_shdc,
.input = "src/platform/shader.glsl",
.output = "src/platform/shader.zig",
.slang = slang
})
});
mod_shader.addImport("sokol", mod_sokol);
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) {
exe_mod.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 = "index",
.root_module = exe_mod
});
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
patchWasmIncludeDirs(
exe_mod,
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
&(dep_sokol.artifact("sokol_clib").step)
);
var extra_args: std.ArrayList([]const u8) = .empty;
try extra_args.append(b.allocator, "-sALLOW_MEMORY_GROWTH");
try extra_args.append(b.allocator, "-sENVIRONMENT=web");
try extra_args.append(b.allocator, b.fmt("-sSTACK_SIZE={}", .{opts.stack_size}));
if (is_debug) {
try extra_args.append(b.allocator, "-sASSERTIONS=2");
try extra_args.append(b.allocator, "-sSTACK_OVERFLOW_CHECK=2");
try extra_args.append(b.allocator, "-gsource-map");
try extra_args.append(b.allocator, "-g2");
}
const link_step = try sokol.emLinkStep(b, .{
.lib_main = main_lib,
.target = target,
.optimize = optimize,
.emsdk = dep_emsdk,
.use_webgl2 = true,
.use_emmalloc = true,
.use_filesystem = true,
.shell_file_path = b.path("src/platform/shell.html"),
.extra_args = extra_args.items
});
return ProjectResult{
.wasm = link_step
};
} 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 ZipEntry = union(enum) {
directory: Build.LazyPath,
file: Build.LazyPath
};
fn buildZip(ctx: Context, output_name: []const u8, entries: []const ZipEntry) Build.LazyPath {
const b = ctx.b;
var create_zip_run = b.addRunArtifact(ctx.create_zip);
for (entries) |entry| {
switch (entry) {
.directory => |lp| create_zip_run.addDirectoryArg(lp),
.file => |lp| create_zip_run.addFileArg(lp),
}
}
return create_zip_run.addOutputFileArg(output_name);
}
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);
}
}