const builtin = @import("builtin"); const std = @import("std"); const ImportList = std.ArrayList(struct { name: []const u8, module: *std.Build.Module }); const Project = struct { target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, imports: ImportList, pub fn find_demos(self: Project, step: *std.Build.Step) void { const absolute_path = step.owner.path("demos/").getPath(step.owner); var dir = std.fs.openDirAbsolute(absolute_path, .{.iterate = true}) catch |open_error| { std.log.warn("failed to open demos directory at {s} with error {s}, skipping...", .{ absolute_path, @errorName(open_error), }); return; }; defer { dir.close(); } var entries = dir.walk(step.owner.allocator) catch @panic("OOM"); defer { entries.deinit(); } while (entries.next() catch @panic("I/O failure")) |entry| { if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".zig")) { continue; } const source_path = step.owner.pathJoin(&.{"demos", entry.basename}); var path_buffer = [_:0]u8{0} ** 255; const demo = step.owner.addExecutable(.{ .name = std.fmt.bufPrint(&path_buffer, "{s}.out", .{std.fs.path.stem(entry.basename)}) catch { @panic("a demo file name is too long"); }, .root_source_file = step.owner.path(source_path), .target = self.target, .optimize = self.optimize, }); for (self.imports.items) |import| { demo.root_module.addImport(import.name, import.module); } step.dependOn(&step.owner.addInstallArtifact(demo, .{ .dest_dir = .{ .override = .{.custom = "../demos"}, }, }).step); } } pub fn find_tests(self: Project, step: *std.Build.Step) void { for (self.imports.items) |import| { step.dependOn(&step.owner.addRunArtifact(step.owner.addTest(.{ .root_source_file = import.module.root_source_file.?, .target = self.target, .optimize = self.optimize, })).step); } } pub fn add_module(self: *Project, b: *std.Build, comptime name: []const u8, options: std.Build.Module.CreateOptions) !*std.Build.Module { const cwd = std.fs.cwd(); var corrected_options = options; if (corrected_options.root_source_file == null) { corrected_options.root_source_file = b.path("src/" ++ name ++ "/" ++ name ++ ".zig"); } if (corrected_options.target == null) { corrected_options.target = self.target; } if (corrected_options.optimize == null) { corrected_options.optimize = self.optimize; } const module = b.addModule(name, corrected_options); try self.imports.append(.{ .name = name, .module = module }); // TODO: Probably want to make it search the same path as any explicit root_source_path override for shaders. const shaders_path = "src/" ++ name ++ "/shaders/"; var shaders_dir = cwd.openDir(shaders_path, .{.iterate = true}) catch |open_error| { return switch (open_error) { error.FileNotFound, error.NotDir => module, else => open_error, }; }; defer { shaders_dir.close(); } var shaders_entries = try shaders_dir.walk(b.allocator); defer { shaders_entries.deinit(); } while (try shaders_entries.next()) |entry| { if (entry.kind != .file) { continue; } const is_shader_file = std.mem.endsWith(u8, entry.path, ".frag") or std.mem.endsWith(u8, entry.path, ".vert"); if (!is_shader_file) { continue; } var binary_buffer = [_:0]u8{0} ** 255; const binary_name = try std.fmt.bufPrint(&binary_buffer, "{s}.spv", .{entry.path}); const glslang_validator_args = [_][]const u8{ "glslangValidator", "-V", b.pathJoin(&.{shaders_path, entry.path}), "-o", b.pathJoin(&.{shaders_path, binary_name}), }; shaders_dir.access(binary_name, .{.mode = .read_only}) catch { const output = b.run(&glslang_validator_args); std.log.info("{s}", .{output[0 .. output.len - 1]}); continue; }; if ((try shaders_dir.statFile(entry.path)).mtime > (try shaders_dir.statFile(binary_name)).mtime) { const output = b.run(&glslang_validator_args); std.log.info("{s}", .{output[0 .. output.len - 1]}); continue; } } return module; } }; pub fn build(b: *std.Build) !void { var project = Project{ .imports = ImportList.init(b.allocator), .target = b.standardTargetOptions(.{}), .optimize = b.standardOptimizeOption(.{}), }; const sokol_dependency = b.dependency("sokol", .{ .target = project.target, .optimize = project.optimize, }); const ona_module = try project.add_module(b, "ona", .{}); const hid_module = try project.add_module(b, "hid", .{ .imports = &.{ .{ .name = "ona", .module = ona_module, }, }, }); const gfx_module = try project.add_module(b, "gfx", .{ .imports = &.{ .{ .name = "sokol", .module = sokol_dependency.module("sokol"), }, .{ .name = "ona", .module = ona_module, }, .{ .name = "hid", .module = hid_module, }, }, }); gfx_module.addIncludePath(b.path("ext/")); gfx_module.linkLibrary(spirv_cross: { const dir = "ext/spirv-cross/"; const sources = [_][]const u8{ "spirv_cross.cpp", "spirv_parser.cpp", "spirv_cross_parsed_ir.cpp", "spirv_cfg.cpp", "spirv_glsl.cpp", "spirv_msl.cpp", "spirv_hlsl.cpp", "spirv_reflect.cpp", "spirv_cross_util.cpp", "spirv_cross_c.cpp", }; const lib = b.addStaticLibrary(.{ .name = "spirvcross", .target = project.target, .optimize = project.optimize, }); switch (lib.rootModuleTarget().abi) { .msvc => lib.linkLibC(), else => lib.linkLibCpp(), } inline for (sources) |src| { lib.addCSourceFile(.{ .file = b.path(dir ++ src), .flags = &.{ "-fstrict-aliasing", "-DSPIRV_CROSS_C_API_GLSL", "-DSPIRV_CROSS_C_API_HLSL", "-DSPIRV_CROSS_C_API_MSL", }, }); } break: spirv_cross lib; }); gfx_module.linkSystemLibrary("SDL2", .{ .needed = true, .preferred_link_mode = .dynamic, }); gfx_module.link_libc = true; _ = try project.add_module(b, "gui", .{ .imports = &.{ .{ .name = "ona", .module = ona_module, }, .{ .name = "gfx", .module = gfx_module, }, }, }); project.find_demos(b.step("demos", "Build demos")); project.find_tests(b.step("tests", "Build and run tests")); }