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, b: *std.Build) !void { const demos = b.step("demos", "Build demos"); var dir = try std.fs.cwd().openDir("demos/", .{ .iterate = true, }); defer { dir.close(); } var entries = try dir.walk(b.allocator); defer { entries.deinit(); } while (try entries.next()) |entry| { if (entry.kind != .file or !std.mem.endsWith(u8, entry.path, ".zig")) { continue; } const source_path = try sub_path(.{"demos", entry.basename}); var path_buffer = [_:0]u8{0} ** 255; const demo = b.addExecutable(.{ .name = try std.fmt.bufPrint(&path_buffer, "{s}.out", .{std.fs.path.stem(entry.basename)}), .root_source_file = b.path(source_path.bytes()), .target = self.target, .optimize = self.optimize, }); for (self.imports.items) |import| { demo.root_module.addImport(import.name, import.module); } demos.dependOn(&b.addInstallArtifact(demo, .{ .dest_dir = .{ .override = .{ .custom = "../demos/", }, }, }).step); } } pub fn find_tests(self: Project, b: *std.Build) !void { const tests = b.step("tests", "Build and run tests"); for (self.imports.items) |import| { tests.dependOn(&b.addRunArtifact(b.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 full_source_path = try sub_path(.{shaders_path, entry.path}); const full_binary_path = try sub_path(.{shaders_path, binary_name}); const glslang_validator_args = [_][]const u8{ "glslangValidator", "-V", full_source_path.bytes(), "-o", full_binary_path.bytes(), }; 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; } }; const SubPath = struct { buffer: [max]u8 = [_]u8{0} ** max, unused: u8 = max, pub const max = 255; pub fn append(self: *SubPath, component: []const u8) !void { const used = max - self.unused; if (used != 0 and self.buffer[used - 1] != '/') { if (component.len > self.unused) { return error.PathTooBig; } @memcpy(self.buffer[used .. (used + component.len)], component); self.unused -= @intCast(component.len); } else { const required_len = component.len + 1; if (required_len > self.unused) { return error.PathTooBig; } @memcpy(self.buffer[used .. (used + component.len)], component); self.buffer[component.len] = '/'; self.unused -= @intCast(required_len); } } pub fn bytes(self: *const SubPath) [:0]const u8 { return @ptrCast(self.buffer[0 .. max - self.unused]); } }; 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.find_tests(b); try project.find_demos(b); } fn sub_path(components: anytype) !SubPath { var path = comptime try std.BoundedArray(u8, SubPath.max).init(0); const Components = @TypeOf(components); switch (@typeInfo(Components)) { .Struct => |@"struct"| { if (!@"struct".is_tuple) { @compileError("`components` must be a tuple"); } const last_component_index = components.len - 1; inline for (components, 0 .. components.len) |component, i| { path.appendSlice(component) catch { return error.PathTooBig; }; if (i < last_component_index and !std.mem.endsWith(u8, component, "/")) { path.append('/') catch { return error.PathTooBig; }; } } }, else => { @compileError("`components` cannot be a " ++ @typeName(Components)); } } return .{ .unused = SubPath.max - path.len, .buffer = path.buffer, }; }