sdlparser-scrap/build.zig

347 lines
15 KiB
Zig

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 c_import_path = b.option([]const u8, "cImportPath", "Path used by generated files when importing c.zig");
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}));
}
if (c_import_path) |path| {
regenerate.addArg(b.fmt("--c-import={s}", .{path}));
}
// 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(&regenerate.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(&regenerateJson.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);
}