const std = @import("std"); fn resolveHeaderRoot(allocator: std.mem.Allocator, source_dir: []const u8) ![]const u8 { const include_sdl3 = try std.fs.path.join(allocator, &.{ source_dir, "include", "SDL3" }); defer allocator.free(include_sdl3); if (std.fs.cwd().access(include_sdl3, .{})) { return try allocator.dupe(u8, "include/SDL3"); } else |_| {} const sdl3_dir = try std.fs.path.join(allocator, &.{ source_dir, "SDL3" }); defer allocator.free(sdl3_dir); if (std.fs.cwd().access(sdl3_dir, .{})) { return try allocator.dupe(u8, "SDL3"); } else |_| {} return try allocator.dupe(u8, "."); } fn addFetchSdlStep(b: *std.Build) *std.Build.Step { const default_sdl_url = "git@github.com:castholm/SDL.git"; const official_sdl_url = "git@github.com:libsdl-org/SDL.git"; const sdl_url_option = b.option([]const u8, "sdl-url", "URL to fetch SDL3 from (use 'official' for libsdl-org/SDL)") orelse default_sdl_url; const sdl_url = if (std.mem.eql(u8, sdl_url_option, "official") or b.option(bool, "official", "use the official sdl path of git@github.com:libsdl-org/SDL.git (same as setting -Dsdl-url=official)") != null) official_sdl_url else sdl_url_option; const clean_sdl = b.option(bool, "clean", "Delete sdl3/ directory before fetching") orelse false; const sdl_checkout = b.option([]const u8, "ref", "the git ref to check out after fetching sdl, this can be tag, commit, branch..."); const fetch_step = b.step("fetch-sdl", "Fetch SDL3 source from git"); const sdl_exists = if (std.fs.cwd().access("sdl3", .{})) true else |_| false; var end_step: *std.Build.Step = undefined; if (clean_sdl) { const remove_dir = b.addRemoveDirTree(b.path("sdl3")); const fetch_sdl = b.addSystemCommand(&.{ "git", "clone", sdl_url, "sdl3" }); fetch_sdl.step.dependOn(&remove_dir.step); end_step = &fetch_sdl.step; // fetch_step.dependOn(&fetch_sdl.step); } else if (sdl_exists) { const echo_msg = b.addSystemCommand(&.{ "echo", "sdl already fetched" }); end_step = &echo_msg.step; // fetch_step.dependOn(&echo_msg.step); } else { const fetch_sdl = b.addSystemCommand(&.{ "git", "clone", sdl_url, "sdl3" }); end_step = &fetch_sdl.step; } const checkout_step = if (sdl_checkout) |ref| &b.addSystemCommand(&.{ "git", "-C", "sdl3", "checkout", ref }).step else end_step; if (sdl_checkout != null) { checkout_step.dependOn(end_step); } fetch_step.dependOn(checkout_step); return fetch_step; } const ArchiveStep = struct { step: std.Build.Step, base_dir: ?[]const u8, pub fn create(b: *std.Build, base_dir: ?[]const u8) *std.Build.Step { const self = b.allocator.create(ArchiveStep) catch @panic("OOM"); self.* = .{ .step = std.Build.Step.init(.{ .id = .custom, .name = "archive generate outputs", .owner = b, .makeFn = make, }), .base_dir = base_dir, }; return &self.step; } fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void { _ = options; const self: *ArchiveStep = @fieldParentPtr("step", step); const cwd = std.fs.cwd(); const json_path = if (self.base_dir) |base_dir| try std.fs.path.join(step.owner.allocator, &.{ base_dir, "json" }) else try step.owner.allocator.dupe(u8, "json"); defer step.owner.allocator.free(json_path); const api_path = if (self.base_dir) |base_dir| try std.fs.path.join(step.owner.allocator, &.{ base_dir, "api" }) else try step.owner.allocator.dupe(u8, "api"); defer step.owner.allocator.free(api_path); const archive_root = if (self.base_dir) |base_dir| try std.fs.path.join(step.owner.allocator, &.{ base_dir, "archive", "generate" }) else try step.owner.allocator.dupe(u8, "archive/generate"); defer step.owner.allocator.free(archive_root); const json_exists = if (cwd.access(json_path, .{})) true else |_| false; const api_exists = if (cwd.access(api_path, .{})) true else |_| false; if (!json_exists and !api_exists) return; const timestamp = std.time.timestamp(); const timestamp_dir = try std.fmt.allocPrint(step.owner.allocator, "{d}", .{timestamp}); defer step.owner.allocator.free(timestamp_dir); const archive_path = try std.fs.path.join(step.owner.allocator, &.{ archive_root, timestamp_dir }); defer step.owner.allocator.free(archive_path); try cwd.makePath(archive_path); if (json_exists) { const json_dest = try std.fs.path.join(step.owner.allocator, &.{ archive_path, "json" }); defer step.owner.allocator.free(json_dest); try cwd.rename(json_path, json_dest); } if (api_exists) { const api_dest = try std.fs.path.join(step.owner.allocator, &.{ archive_path, "api" }); defer step.owner.allocator.free(api_dest); try cwd.rename(api_path, api_dest); } } }; const MakeDirStep = struct { step: std.Build.Step, dir_path: []const u8, pub fn create(b: *std.Build, dir_path: []const u8) *std.Build.Step { const self = b.allocator.create(MakeDirStep) catch @panic("OOM"); self.* = .{ .step = std.Build.Step.init(.{ .id = .custom, .name = b.fmt("mkdir {s}", .{dir_path}), .owner = b, .makeFn = make, }), .dir_path = dir_path, }; return &self.step; } fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void { _ = options; const self: *MakeDirStep = @fieldParentPtr("step", step); try std.fs.cwd().makePath(self.dir_path); } }; pub fn generateApi(b: *std.Build, parser_exe: *std.Build.Step.Compile, fetch_sdl_step: *std.Build.Step) void { const source_dir = b.option([]const u8, "sourceDir", "Parse SDL headers from an existing local directory instead of fetching git"); const output_dir = b.option([]const u8, "outputDir", "Directory where generated api/ and json/ folders should be written"); const basedir = b.option([]const u8, "basedir", "Working directory for the parser to execute in"); const effective_output_dir = output_dir orelse source_dir; const header_root_suffix = if (source_dir) |dir| resolveHeaderRoot(b.allocator, dir) catch @panic("OOM") else null; // Archive existing json/ and api/ directories before regenerating const archive_step = ArchiveStep.create(b, effective_output_dir); const generation_root = if (source_dir != null) archive_step else fetch_sdl_step; if (source_dir == null) { fetch_sdl_step.dependOn(archive_step); } // Write a build marker file after the source selection step to enable caching const timestamp = std.time.timestamp(); const wf = b.addWriteFiles(); const marker_file = wf.add(".buildmarker", b.fmt("{d}", .{timestamp})); _ = marker_file; wf.step.dependOn(generation_root); var path_prep_step: *std.Build.Step = &wf.step; if (effective_output_dir) |dir| { const output_dir_step = MakeDirStep.create(b, dir); output_dir_step.dependOn(path_prep_step); path_prep_step = output_dir_step; } if (basedir) |dir| { const basedir_step = MakeDirStep.create(b, dir); basedir_step.dependOn(path_prep_step); path_prep_step = basedir_step; } // All public SDL3 API headers (53 total) // Skipped: assert, thread, hidapi, mutex, tray (not core APIs or problematic) const headers_to_generate = [_]struct { header: []const u8, output: []const u8 }{ // .{ .header = "SDL_asyncio.h", .output = "asyncio" }, // .{ .header = "SDL_atomic.h", .output = "atomic" }, .{ .header = "SDL_audio.h", .output = "audio" }, .{ .header = "SDL_blendmode.h", .output = "blendmode" }, .{ .header = "SDL_camera.h", .output = "camera" }, .{ .header = "SDL_clipboard.h", .output = "clipboard" }, // .{ .header = "SDL_cpuinfo.h", .output = "cpuinfo" }, .{ .header = "SDL_dialog.h", .output = "dialog" }, .{ .header = "SDL_endian.h", .output = "endian" }, .{ .header = "SDL_error.h", .output = "error" }, .{ .header = "SDL_events.h", .output = "events" }, .{ .header = "SDL_filesystem.h", .output = "filesystem" }, .{ .header = "SDL_gamepad.h", .output = "gamepad" }, .{ .header = "SDL_gpu.h", .output = "gpu" }, // .{ .header = "SDL_guid.h", .output = "guid" }, .{ .header = "SDL_haptic.h", .output = "haptic" }, // .{ .header = "SDL_hidapi.h", .output = "hidapi" }, // Skipped: not core API .{ .header = "SDL_hints.h", .output = "hints" }, .{ .header = "SDL_init.h", .output = "init" }, // .{ .header = "SDL_iostream.h", .output = "iostream" }, // Skipped: complex I/O API .{ .header = "SDL_joystick.h", .output = "joystick" }, // .{ .header = "SDL_keyboard.h", .output = "keyboard" }, .{ .header = "SDL_keycode.h", .output = "keycode" }, .{ .header = "SDL_loadso.h", .output = "loadso" }, // .{ .header = "SDL_locale.h", .output = "locale" }, // .{ .header = "SDL_log.h", .output = "log" }, .{ .header = "SDL_messagebox.h", .output = "messagebox" }, // .{ .header = "SDL_metal.h", .output = "metal" }, .{ .header = "SDL_misc.h", .output = "misc" }, .{ .header = "SDL_mouse.h", .output = "mouse" }, // .{ .header = "SDL_mutex.h", .output = "mutex" }, // Skipped: not core API // .{ .header = "SDL_opengl.h", .output = "opengl" }, // .{ .header = "SDL_pen.h", .output = "pen" }, .{ .header = "SDL_pixels.h", .output = "pixels" }, // .{ .header = "SDL_power.h", .output = "power" }, // .{ .header = "SDL_process.h", .output = "process" }, .{ .header = "SDL_properties.h", .output = "properties" }, .{ .header = "SDL_rect.h", .output = "rect" }, .{ .header = "SDL_render.h", .output = "render" }, // .{ .header = "SDL_scancode.h", .output = "scancode" }, .{ .header = "SDL_sensor.h", .output = "sensor" }, .{ .header = "SDL_storage.h", .output = "storage" }, .{ .header = "SDL_surface.h", .output = "surface" }, .{ .header = "SDL_system.h", .output = "system" }, // .{ .header = "SDL_thread.h", .output = "thread" }, // Skipped: not core API .{ .header = "SDL_time.h", .output = "time" }, .{ .header = "SDL_timer.h", .output = "timer" }, .{ .header = "SDL_touch.h", .output = "touch" }, // .{ .header = "SDL_tray.h", .output = "tray" }, // Skipped: not core API .{ .header = "SDL_version.h", .output = "version" }, .{ .header = "SDL_video.h", .output = "video" }, // .{ .header = "SDL_vulkan.h", .output = "vulkan" }, // Skipped: Vulkan interop }; const regenerate_step = b.step("generate", "Regenerate bindings from SDL headers"); const header_root = if (source_dir) |dir| b.fmt("{s}/{s}", .{ dir, header_root_suffix.? }) else "sdl3/include/SDL3"; const output_root = if (effective_output_dir) |dir| b.fmt("{s}/api", .{dir}) else "api"; const json_root = if (effective_output_dir) |dir| b.fmt("{s}/json", .{dir}) else "json"; const timestamp_arg = b.fmt("--timestamp={d}", .{timestamp}); for (headers_to_generate) |header_info| { const regenerate = b.addRunArtifact(parser_exe); regenerate.addArg(b.fmt("{s}/{s}", .{ header_root, header_info.header })); regenerate.addArg(b.fmt("--output={s}/{s}.zig", .{ output_root, header_info.output })); regenerate.addArg(timestamp_arg); if (basedir) |dir| { regenerate.addArg(b.fmt("--basedir={s}", .{dir})); } // regenerate.addArg(b.fmt("--output=api/{s}.zig --mocks=mocks/{s}.c", .{ header_info.output, header_info.output })); regenerate.step.dependOn(path_prep_step); regenerate_step.dependOn(®enerate.step); const regenerateJson = b.addRunArtifact(parser_exe); regenerateJson.addArg(b.fmt("{s}/{s}", .{ header_root, header_info.header })); regenerateJson.addArg(b.fmt("--generate-json={s}/{s}.json", .{ json_root, header_info.output })); regenerateJson.addArg(timestamp_arg); if (basedir) |dir| { regenerateJson.addArg(b.fmt("--basedir={s}", .{dir})); } regenerateJson.step.dependOn(path_prep_step); regenerate_step.dependOn(®enerateJson.step); } } pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const fetch_sdl_step = addFetchSdlStep(b); // Parser executable const parser_exe = b.addExecutable(.{ .name = "sdl-parser", .root_module = b.createModule(.{ .root_source_file = b.path("src/parser.zig"), .target = target, .optimize = optimize, }), }); b.installArtifact(parser_exe); generateApi(b, parser_exe, fetch_sdl_step); // Run command const run_cmd = b.addRunArtifact(parser_exe); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } const run_step = b.step("run", "Run the SDL3 header parser"); run_step.dependOn(&run_cmd.step); // Test mocks generation target const test_mocks_cmd = b.addRunArtifact(parser_exe); test_mocks_cmd.step.dependOn(b.getInstallStep()); const test_header_path = b.path("test_small.h"); const test_output = b.path("zig-out/test_small.zig"); const test_mocks = b.path("zig-out/test_small_mock.c"); test_mocks_cmd.addArg(test_header_path.getPath(b)); test_mocks_cmd.addArg(b.fmt("--output={s}", .{test_output.getPath(b)})); test_mocks_cmd.addArg(b.fmt("--mocks={s}", .{test_mocks.getPath(b)})); const test_mocks_step = b.step("test-mocks", "Test mock generation with test_small.h"); test_mocks_step.dependOn(&test_mocks_cmd.step); // Tests const parser_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/parser.zig"), .target = target, .optimize = optimize, }), }); const run_tests = b.addRunArtifact(parser_tests); const test_step = b.step("test", "Run parser tests"); test_step.dependOn(&run_tests.step); }