From cf8547ae4add6826ceb177af7859cdcc33dbb817 Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Sun, 8 Feb 2026 05:33:32 -0700 Subject: [PATCH] initial sync branch implementation --- .gitignore | 3 + build.zig | 164 ++++++++++++++++++++++++++++++++++++++++++ build.zig.zon | 22 ++++++ src/common.zig | 98 +++++++++++++++++++++++++ src/fetchgenerate.zig | 93 ++++++++++++++++++++++++ src/sync.zig | 38 ++++++++++ 6 files changed, 418 insertions(+) create mode 100644 .gitignore create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 src/common.zig create mode 100644 src/fetchgenerate.zig create mode 100644 src/sync.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50c6be1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.zig-cache/ +zig-out/ +/generate-branch/ diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..e790b83 --- /dev/null +++ b/build.zig @@ -0,0 +1,164 @@ +const std = @import("std"); +const Build = std.Build; + +const version = "25.0.28-preview"; + +pub fn build(b: *Build) !void { + const sync_step = b.step("sync", "Sync the generate/main branches"); + b.default_step = sync_step; + + const nofetch = b.option(bool, "nofetch", "disable fetching the generate branch on every build") orelse false; + + const desc_line_prefix = [_]u8{' '} ** 31; + const sha_file = blk: { + const exe = b.addExecutable(.{ + .name = "fetchgenerate", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/fetchgenerate.zig"), + .target = b.graph.host, + }), + }); + const run = b.addRunArtifact(exe); + run.has_side_effects = if (nofetch) false else true; + run.addDirectoryArg(b.path("generate-branch")); + const sha_file = run.addOutputFileArg("generate-sha"); + b.step("fetchgenerate", ("Fetches the generate branch in 'generate-branch'\n" ++ desc_line_prefix ++ + "and stores its sha in a file for other\n" ++ desc_line_prefix ++ + "steps to use as an input.")).dependOn(&run.step); + break :blk sha_file; + }; + + { + const exe = b.addExecutable(.{ + .name = "sync", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/sync.zig"), + .target = b.graph.host, + }), + }); + const run = b.addRunArtifact(exe); + run.addFileArg(sha_file); + sync_step.dependOn(&run.step); + } + // TODO: default step clones/updates a copy of the "generate" branch + // a copy of the repo + // into a subdirectory (work/ + + // const metadata_nupkg_file = blk: { + // const download_winmd_nupkg = b.addSystemCommand(&.{ + // "curl", + // "https://www.nuget.org/api/v2/package/Microsoft.Windows.SDK.Win32Metadata/" ++ version, + // "--location", + // "--output", + // }); + // break :blk download_winmd_nupkg.addOutputFileArg("win32metadata.nupkg"); + // }; + + // const metadata_nupkg_out = blk: { + // const zipcmdline = b.dependency("zipcmdline", .{ + // .target = b.graph.host, + // }); + // const unzip_exe = zipcmdline.artifact("unzip"); + // const unzip_metadata = b.addRunArtifact(unzip_exe); + // unzip_metadata.addFileArg(metadata_nupkg_file); + // unzip_metadata.addArg("-d"); + // break :blk unzip_metadata.addOutputDirectoryArg("nupkg"); + // }; + + // const winmd = metadata_nupkg_out.path(b, "Windows.Win32.winmd"); + // b.step( + // "show-winmd", + // "Print the winmd file path in the cache", + // ).dependOn( + // &PrintLazyPath.create(b, winmd).step, + // ); + + // const gen_step = b.step("gen", "Generate JSON files (in .zig-cache)"); + + // const winmd_dep = b.dependency("winmd", .{}); + // const gen_out_dir = blk: { + // const exe = b.addExecutable(.{ + // .name = "genjson", + // .root_module = b.createModule(.{ + // .root_source_file = b.path("src/genjson.zig"), + // .target = b.graph.host, + // }), + // }); + // exe.root_module.addImport("winmd", winmd_dep.module("winmd")); + // const run = b.addRunArtifact(exe); + // run.addFileArg(winmd); + // const out_dir = run.addOutputDirectoryArg("."); + // gen_step.dependOn(&run.step); + // break :blk out_dir; + // }; + + // { + // // we can't use b.installDirectory because we need to make sure we delete + // // all existing files before installing the new ones in case JSON files are removed + // // b.installDirectory(.{ + // // .source_dir = gen_out_dir, + // // .install_dir = .prefix, + // // .install_subdir = ".", + // // }); + // const install_exe = b.addExecutable(.{ + // .name = "install", + // .root_module = b.createModule(.{ + // .root_source_file = b.path("src/install.zig"), + // .target = b.graph.host, + // }), + // }); + // const install = b.addRunArtifact(install_exe); + // install.addDirectoryArg(gen_out_dir); + // install.addArg(b.pathResolve(&.{ b.build_root.path.?, "win32json", "api" })); + // b.getInstallStep().dependOn(&install.step); + // } + + // const test_step = b.step("test", "Run all the tests"); + // { + // const validate_exe = b.addExecutable(.{ + // .name = "validate", + // .root_module = b.createModule(.{ + // .root_source_file = b.path("src/validate.zig"), + // .target = b.graph.host, + // .optimize = .Debug, + // }), + // }); + // const validate = b.addRunArtifact(validate_exe); + // validate.addDirectoryArg(gen_out_dir); + // validate.expectExitCode(0); + // test_step.dependOn(&validate.step); + // } +} + +// const PrintLazyPath = struct { +// step: std.Build.Step, +// lazy_path: Build.LazyPath, +// pub fn create( +// b: *Build, +// lazy_path: Build.LazyPath, +// ) *PrintLazyPath { +// const print = b.allocator.create(PrintLazyPath) catch unreachable; +// print.* = .{ +// .step = std.Build.Step.init(.{ +// .id = .custom, +// .name = "print the given lazy path", +// .owner = b, +// .makeFn = make, +// }), +// .lazy_path = lazy_path, +// }; +// lazy_path.addStepDependencies(&print.step); +// return print; +// } +// fn make(step: *std.Build.Step, opt: std.Build.Step.MakeOptions) !void { +// _ = opt; +// const print: *PrintLazyPath = @fieldParentPtr("step", step); +// var write_buf: [1024]u8 = undefined; +// var stdout = std.fs.File.stdout().writer(&write_buf); +// try stdout.interface.print( +// "{s}\n", +// .{print.lazy_path.getPath(step.owner)}, +// ); +// try stdout.interface.flush(); +// } +// }; diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..96e1d3f --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,22 @@ +.{ + .name = .generatewin32json, + .fingerprint = 0xfd7a77ded32ab056, + .version = "0.0.0", + .zig_version = "0.15.2", + .minimum_zig_version = "0.15.0", + .dependencies = .{ + .zipcmdline = .{ + .url = "git+https://github.com/marler8997/zipcmdline#a6de65875023c52d2f56f21a31c6487266f8105f", + .hash = "zipcmdline-0.0.0-DFii07hmAwCFNhtMpml64Pj_hr8kN4WesClygZErQHsk", + }, + .winmd = .{ + .url = "git+https://github.com/marler8997/winmd-zig#d0d8daff01b4fb1a6324c79a855c34ced5bbc8d2", + .hash = "winmd-0.0.0-2QIrG4MpAQCCh0ReC0jZKAps9Frn3jbbR--W7szyGAOS", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/src/common.zig b/src/common.zig new file mode 100644 index 0000000..3567a63 --- /dev/null +++ b/src/common.zig @@ -0,0 +1,98 @@ +pub fn run( + allocator: std.mem.Allocator, + name: []const u8, + argv: []const []const u8, +) !void { + var child = std.process.Child.init(argv, allocator); + std.log.info("{f}", .{fmtArgv(child.argv)}); + try child.spawn(); + const term = try child.wait(); + if (childProcFailed(term)) { + errExit("{s} {f}", .{ name, fmtTerm(term) }); + } +} + +pub fn childProcFailed(term: std.process.Child.Term) bool { + return switch (term) { + .Exited => |code| code != 0, + .Signal => true, + .Stopped => true, + .Unknown => true, + }; +} + +pub fn fmtArgv(argv: []const []const u8) FormatArgv { + return .{ .argv = argv }; +} +const FormatArgv = struct { + argv: []const []const u8, + pub fn format(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void { + var prefix: []const u8 = ""; + for (self.argv) |arg| { + try writer.print("{s}{s}", .{ prefix, arg }); + prefix = " "; + } + } +}; + +pub fn fmtTerm(term: std.process.Child.Term) FormatTerm { + return .{ .term = term }; +} +const FormatTerm = struct { + term: std.process.Child.Term, + pub fn format(self: @This(), writer: *std.Io.Writer) error{WriteFailed}!void { + switch (self.term) { + .Exited => |code| try writer.print("exited with code {}", .{code}), + .Signal => |sig| try writer.print("exited with signal {}", .{sig}), + .Stopped => |sig| try writer.print("stopped with signal {}", .{sig}), + .Unknown => |sig| try writer.print("terminated abnormally with signal {}", .{sig}), + } + } +}; + +pub fn makeRepo(path: []const u8) !void { + std.fs.cwd().access(path, .{}) catch |err| switch (err) { + error.FileNotFound => try gitInit(path), + else => |e| return e, + }; +} +fn gitInit(repo: []const u8) !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const tmp_repo = try std.mem.concat(allocator, u8, &.{ repo, ".initializing" }); + defer allocator.free(tmp_repo); + try std.fs.cwd().deleteTree(tmp_repo); + try std.fs.cwd().makeDir(tmp_repo); + try run(allocator, "git init", &.{ + "git", + "-C", + tmp_repo, + "init", + }); + try std.fs.cwd().rename(tmp_repo, repo); +} + +pub fn readSha(sha_file: []const u8) !union(enum) { + invalid: []const u8, + good: [40]u8, +} { + var buffer: [41]u8 = undefined; + var out_file = std.fs.cwd().openFile(sha_file, .{}) catch |err| switch (err) { + error.FileNotFound => return .{ .invalid = "file not found" }, + else => |e| return e, + }; + defer out_file.close(); + const len = try out_file.readAll(&buffer); + if (len < 40) return .{ .invalid = "file too small" }; + if (len > 40) return .{ .invalid = "file too big" }; + return .{ .good = buffer[0..40].* }; +} + +pub fn errExit(comptime fmt: []const u8, args: anytype) noreturn { + std.log.err(fmt, args); + std.process.exit(0xff); +} + +const std = @import("std"); diff --git a/src/fetchgenerate.zig b/src/fetchgenerate.zig new file mode 100644 index 0000000..7505f20 --- /dev/null +++ b/src/fetchgenerate.zig @@ -0,0 +1,93 @@ +pub fn main() !u8 { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = arena.allocator(); + const all_args = try std.process.argsAlloc(allocator); + // don't care about freeing args + + const cmd_args = all_args[1..]; + if (cmd_args.len != 2) { + std.log.err("expected 2 cmdline arguments but got {}", .{cmd_args.len}); + return 1; + } + const gen_repo = cmd_args[0]; + const out_file_path = cmd_args[1]; + + try common.makeRepo(gen_repo); + + const repo_url = "https://github.com/marlersoft/win32json"; + try common.run(allocator, "git fetch", &.{ + "git", + "-C", + gen_repo, + "fetch", + repo_url, + "generate", + }); + + const latest_sha = blk: { + const argv = [_][]const u8{ + "git", + "-C", + gen_repo, + "rev-parse", + "FETCH_HEAD", + }; + std.log.info("{f}", .{common.fmtArgv(&argv)}); + const result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &argv, + }); + // don't free stdout, we're using it + //defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + if (result.stderr.len > 0) { + var fw = std.fs.File.stderr().writer(&.{}); + try fw.interface.writeAll(result.stderr); + } + if (common.childProcFailed(result.term)) + errExit("git rev-parse {f}", .{common.fmtTerm(result.term)}); + break :blk std.mem.trimRight(u8, result.stdout, "\r\n"); + }; + if (latest_sha.len != 40) errExit( + "expected sha from git rev-parse to be 40 characters but got {}: '{s}'", + .{ latest_sha.len, latest_sha }, + ); + std.log.info("latest generate sha: {s}", .{latest_sha}); + + switch (try common.readSha(out_file_path)) { + .invalid => |reason| { + std.log.info("existing generate sha: invalid: {s}", .{reason}); + }, + .good => |existing_sha| { + std.log.info("existing generate sha: {s}", .{existing_sha}); + if (std.mem.eql(u8, &existing_sha, latest_sha)) { + std.log.info("status: already up-to-date", .{}); + return 0; + } + std.log.info("status: needs update", .{}); + }, + } + + { + var file = try std.fs.cwd().createFile(out_file_path, .{}); + defer file.close(); + var fw = file.writer(&.{}); + try fw.interface.writeAll(latest_sha); + } + + // sanity check + switch (try common.readSha(out_file_path)) { + .invalid => |reason| errExit("can't read sha after writing it: {s}", .{reason}), + .good => |sha_from_file| if (!std.mem.eql(u8, &sha_from_file, latest_sha)) errExit( + "sha changed to '{s}'", + .{sha_from_file}, + ), + } + std.log.info("{s}", .{out_file_path}); + return 0; +} + +const errExit = common.errExit; + +const std = @import("std"); +const common = @import("common.zig"); diff --git a/src/sync.zig b/src/sync.zig new file mode 100644 index 0000000..9892892 --- /dev/null +++ b/src/sync.zig @@ -0,0 +1,38 @@ +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = arena.allocator(); + + const all_args = try std.process.argsAlloc(allocator); + // don't care about freeing args + + const cmd_args = all_args[1..]; + if (cmd_args.len != 1) { + std.log.err("expected 1 cmdline argument but got {}", .{cmd_args.len}); + std.process.exit(0xff); + } + const generate_sha_file_path = cmd_args[0]; + // const gen_repo = cmd_args[0]; + // const main_sha_file_path = cmd_args[1]; + // const releases_file_path = cmd_args[2]; + // const zigwin32_repo = cmd_args[3]; + // const clean_arg = cmd_args[4]; + + // const do_clean = if (std.mem.eql(u8, clean_arg, "noclean")) + // false + // else if (std.mem.eql(u8, clean_arg, "clean")) + // true + // else + // fatal("unexpected clean cmdline argument '{s}'", .{clean_arg}); + + const generate_sha = switch (try common.readSha(generate_sha_file_path)) { + .invalid => |reason| errExit("read sha from '{s}' failed: {s}", .{ generate_sha_file_path, reason }), + .good => |sha| sha, + }; + std.log.info("generate branch sha: {s}", .{&generate_sha}); + @panic("todo"); +} + +const errExit = common.errExit; + +const std = @import("std"); +const common = @import("common.zig");