From ae0a228ab79f856459648798e1036153f8c36a3f Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Fri, 3 Apr 2026 07:56:04 -0400 Subject: [PATCH 01/11] zig 0.16.0 --- .gitignore | 1 + build.zig | 221 ++++++++++++++++++++++------------------- build.zig.zon | 6 +- build/ClarTestStep.zig | 20 ++-- build/chmod.zig | 15 ++- build/clar_fix.zig | 29 +++--- 6 files changed, 160 insertions(+), 132 deletions(-) diff --git a/.gitignore b/.gitignore index 880cd5d..bcb3fab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ zig-out .zig-cache +zig-pkg \ No newline at end of file diff --git a/build.zig b/build.zig index bcaf370..3c110cf 100644 --- a/build.zig +++ b/build.zig @@ -7,14 +7,16 @@ pub fn build(b: *std.Build) !void { const libgit_src = b.dependency("libgit2", .{}); const libgit_root = libgit_src.path("."); + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); + const lib = b.addLibrary(.{ .name = "git2", .linkage = .static, - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + .root_module = mod, }); const features = b.addConfigHeader( @@ -32,7 +34,7 @@ pub fn build(b: *std.Build) !void { // default on APPLE targets switch (target.result.os.tag) { .macos => { - lib.linkSystemLibrary("iconv"); + mod.linkSystemLibrary("iconv", .{}); features.addValues(.{ .GIT_USE_ICONV = 1, .GIT_USE_STAT_MTIMESPEC = 1, @@ -76,12 +78,12 @@ pub fn build(b: *std.Build) !void { ) orelse if (target.result.os.tag == .macos) .securetransport else .mbedtls; if (target.result.os.tag == .windows) { - lib.linkSystemLibrary("winhttp"); - lib.linkSystemLibrary("rpcrt4"); - lib.linkSystemLibrary("crypt32"); - lib.linkSystemLibrary("ole32"); - lib.linkSystemLibrary("ws2_32"); - lib.linkSystemLibrary("secur32"); + mod.linkSystemLibrary("winhttp", .{}); + mod.linkSystemLibrary("rpcrt4", .{}); + mod.linkSystemLibrary("crypt32", .{}); + mod.linkSystemLibrary("ole32", .{}); + mod.linkSystemLibrary("ws2_32", .{}); + mod.linkSystemLibrary("secur32", .{}); features.addValues(.{ .GIT_HTTPS = 1, @@ -93,13 +95,17 @@ pub fn build(b: *std.Build) !void { .GIT_IO_WSAPOLL = 1, }); - lib.addWin32ResourceFile(.{ .file = libgit_src.path("src/libgit2/git2.rc") }); - lib.addCSourceFiles(.{ .root = libgit_root, .files = &util_win32_sources, .flags = &flags }); + mod.addWin32ResourceFile(.{ .file = libgit_src.path("src/libgit2/git2.rc") }); + mod.addCSourceFiles(.{ + .root = libgit_root, + .files = &util_win32_sources, + .flags = &flags, + }); } else { switch (tls_backend) { .securetransport => { - lib.linkFramework("Security"); - lib.linkFramework("CoreFoundation"); + mod.linkFramework("Security", .{}); + mod.linkFramework("CoreFoundation", .{}); features.addValues(.{ .GIT_HTTPS = 1, .GIT_SECURE_TRANSPORT = 1, @@ -117,7 +123,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }); - if (tls_dep) |tls| lib.linkLibrary(tls.artifact("openssl")); + if (tls_dep) |tls| mod.linkLibrary(tls.artifact("openssl")); features.addValues(.{ .GIT_HTTPS = 1, .GIT_OPENSSL = 1, @@ -135,7 +141,7 @@ pub fn build(b: *std.Build) !void { .target = target, .optimize = optimize, }); - if (tls_dep) |tls| lib.linkLibrary(tls.artifact("mbedtls")); + if (tls_dep) |tls| mod.linkLibrary(tls.artifact("mbedtls")); features.addValues(.{ .GIT_HTTPS = 1, .GIT_MBEDTLS = 1, @@ -152,14 +158,10 @@ pub fn build(b: *std.Build) !void { // ntlmclient { - const ntlm = b.addLibrary(.{ - .name = "ntlmclient", - .linkage = .static, - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + const ntlm = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, }); ntlm.addIncludePath(libgit_src.path("deps/ntlmclient")); maybeAddTlsIncludes(ntlm, tls_dep, tls_backend); @@ -189,17 +191,21 @@ pub fn build(b: *std.Build) !void { .flags = &(ntlm_cflags ++ .{"-Wno-deprecated"}), }); - lib.linkLibrary(ntlm); - lib.addAfterIncludePath(libgit_src.path("deps/ntlmclient")); // avoid aliasing ntlmclient/util.h and src/util/util.h + mod.linkLibrary(b.addLibrary(.{ + .name = "ntlmclient", + .linkage = .static, + .root_module = ntlm, + })); + mod.addAfterIncludePath(libgit_src.path("deps/ntlmclient")); // avoid aliasing ntlmclient/util.h and src/util/util.h features.addValues(.{ .GIT_NTLM = 1 }); } - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = libgit_root, .files = &util_unix_sources, .flags = &flags, }); - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = libgit_root, .files = switch (tls_backend) { .openssl => &.{"src/util/hash/openssl.c"}, @@ -211,7 +217,7 @@ pub fn build(b: *std.Build) !void { } // SHA1 collisiondetect - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = libgit_root, .files = &util_sha1dc_sources, .flags = &(flags ++ .{ @@ -222,7 +228,7 @@ pub fn build(b: *std.Build) !void { }); if (b.option(bool, "enable-ssh", "Enable SSH support") orelse false) { - lib.linkSystemLibrary("ssh2"); + mod.linkSystemLibrary("ssh2", .{}); features.addValues(.{ .GIT_SSH = 1, .GIT_SSH_LIBSSH2 = 1, @@ -232,14 +238,10 @@ pub fn build(b: *std.Build) !void { // Bundled dependencies { - const llhttp = b.addLibrary(.{ - .name = "llhttp", - .linkage = .static, - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + const llhttp = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, }); llhttp.addIncludePath(libgit_src.path("deps/llhttp")); llhttp.addCSourceFiles(.{ @@ -248,21 +250,21 @@ pub fn build(b: *std.Build) !void { .flags = &.{ "-Wno-unused-parameter", "-Wno-missing-declarations" }, }); - lib.addIncludePath(libgit_src.path("deps/llhttp")); - lib.linkLibrary(llhttp); + mod.addIncludePath(libgit_src.path("deps/llhttp")); + mod.linkLibrary(b.addLibrary(.{ + .name = "llhttp", + .linkage = .static, + .root_module = llhttp, + })); features.addValues(.{ .GIT_HTTPPARSER_BUILTIN = 1 }); } if (target.result.os.tag != .macos) { - const pcre = b.addLibrary(.{ - .name = "pcre", - .linkage = .static, - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + const pcre = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, }); - pcre.root_module.addConfigHeader(b.addConfigHeader( + pcre.addConfigHeader(b.addConfigHeader( .{ .style = .{ .cmake = libgit_src.path("deps/pcre/config.h.in") } }, .{ .SUPPORT_PCRE8 = 1, @@ -288,20 +290,20 @@ pub fn build(b: *std.Build) !void { }, }); - lib.addIncludePath(libgit_src.path("deps/pcre")); - lib.linkLibrary(pcre); + mod.addIncludePath(libgit_src.path("deps/pcre")); + mod.linkLibrary(b.addLibrary(.{ + .name = "pcre", + .linkage = .static, + .root_module = pcre, + })); features.addValues(.{ .GIT_REGEX_BUILTIN = 1 }); } { // @Todo: support using system zlib? - const zlib = b.addLibrary(.{ - .name = "z", - .linkage = .static, - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + const zlib = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, }); zlib.addIncludePath(libgit_src.path("deps/zlib")); zlib.addCSourceFiles(.{ @@ -318,20 +320,24 @@ pub fn build(b: *std.Build) !void { }, }); - lib.addIncludePath(libgit_src.path("deps/zlib")); - lib.linkLibrary(zlib); + mod.addIncludePath(libgit_src.path("deps/zlib")); + mod.linkLibrary(b.addLibrary(.{ + .name = "z", + .linkage = .static, + .root_module = zlib, + })); features.addValues(.{ .GIT_COMPRESSION_ZLIB = 1 }); } // xdiff { // Bundled xdiff dependency relies on libgit2 headers & utils, so we // just add the source files directly instead of making a static lib step. - lib.addCSourceFiles(.{ + mod.addCSourceFiles(.{ .root = libgit_root, .files = &xdiff_sources, .flags = &.{ "-Wno-sign-compare", "-Wno-unused-parameter" }, }); - lib.addIncludePath(libgit_src.path("deps/xdiff")); + mod.addIncludePath(libgit_src.path("deps/xdiff")); } switch (target.result.ptrBitWidth()) { @@ -340,27 +346,32 @@ pub fn build(b: *std.Build) !void { else => |size| std.debug.panic("Unsupported architecture ({d}bit)", .{size}), } - lib.addConfigHeader(features); + mod.addConfigHeader(features); - lib.addIncludePath(libgit_src.path("src/libgit2")); - lib.addIncludePath(libgit_src.path("src/util")); - lib.addIncludePath(libgit_src.path("include")); + mod.addIncludePath(libgit_src.path("src/libgit2")); + mod.addIncludePath(libgit_src.path("src/util")); + mod.addIncludePath(libgit_src.path("include")); - lib.addCSourceFiles(.{ .root = libgit_root, .files = &libgit_sources, .flags = &flags }); - lib.addCSourceFiles(.{ .root = libgit_root, .files = &util_sources, .flags = &flags }); + mod.addCSourceFiles(.{ + .root = libgit_root, + .files = &libgit_sources, + .flags = &flags, + }); + mod.addCSourceFiles(.{ + .root = libgit_root, + .files = &util_sources, + .flags = &flags, + }); lib.installHeadersDirectory(libgit_src.path("include"), "", .{}); b.installArtifact(lib); const cli_step = b.step("run-cli", "Build and run the command-line interface"); { - const cli = b.addExecutable(.{ - .name = "git2_cli", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + const cli = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, }); cli.addConfigHeader(features); @@ -384,8 +395,12 @@ pub fn build(b: *std.Build) !void { }); // independent install step so you can easily access the binary - const cli_install = b.addInstallArtifact(cli, .{}); - const cli_run = b.addRunArtifact(cli); + const cli_exe = b.addExecutable(.{ + .name = "git2_cli", + .root_module = cli, + }); + const cli_install = b.addInstallArtifact(cli_exe, .{}); + const cli_run = b.addRunArtifact(cli_exe); if (b.args) |args| { for (args) |arg| cli_run.addArg(arg); } @@ -395,17 +410,18 @@ pub fn build(b: *std.Build) !void { const examples_step = b.step("run-example", "Build and run library usage example app"); { - const exe = b.addExecutable(.{ + const lg2 = b.createModule(.{ + .target = target, + .optimize = optimize, + .link_libc = true, + }); + const lg2_exe = b.addExecutable(.{ .name = "lg2", - .root_module = b.createModule(.{ - .target = target, - .optimize = optimize, - .link_libc = true, - }), + .root_module = lg2, }); - exe.addIncludePath(libgit_src.path("examples")); - exe.addCSourceFiles(.{ + lg2.addIncludePath(libgit_src.path("examples")); + lg2.addCSourceFiles(.{ .root = libgit_root, .files = &example_sources, .flags = &.{ @@ -414,12 +430,12 @@ pub fn build(b: *std.Build) !void { }, }); - maybeAddTlsIncludes(exe, tls_dep, tls_backend); - exe.linkLibrary(lib); + maybeAddTlsIncludes(lg2, tls_dep, tls_backend); + lg2.linkLibrary(lib); // independent install step so you can easily access the binary - const examples_install = b.addInstallArtifact(exe, .{}); - const example_run = b.addRunArtifact(exe); + const examples_install = b.addInstallArtifact(lg2_exe, .{}); + const example_run = b.addRunArtifact(lg2_exe); if (b.args) |args| { for (args) |arg| example_run.addArg(arg); } @@ -442,13 +458,14 @@ pub fn build(b: *std.Build) !void { .{}, ); - const runner = b.addExecutable(.{ + const runner = b.createModule(.{ + .target = target, + .optimize = .Debug, + .link_libc = true, + }); + const runner_exe = b.addExecutable(.{ .name = "libgit2_tests", - .root_module = b.createModule(.{ - .target = target, - .optimize = .Debug, - .link_libc = true, - }), + .root_module = runner, }); runner.addIncludePath(clar_suite); runner.addIncludePath(clar_src); @@ -507,7 +524,7 @@ pub fn build(b: *std.Build) !void { run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_off2on_staged")); run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_off2on_workdir")); run_chmod.addFileArg(resources_dir.path(b, "filemodes/exec_on_untracked")); - runner.step.dependOn(&run_chmod.step); + runner_exe.step.dependOn(&run_chmod.step); break :dir resources_dir; }, @@ -529,7 +546,7 @@ pub fn build(b: *std.Build) !void { // run_fix.has_side_effects = true; // @Todo is this necessary? What are the rules for cache invalidation with Run steps? run_fix.addFileArg(clar_src.path(b, "clar/fixtures.h")); run_fix.addDirectoryArg(resources_dir); - runner.step.dependOn(&run_fix.step); + runner_exe.step.dependOn(&run_fix.step); } const TestHelper = struct { @@ -567,7 +584,7 @@ pub fn build(b: *std.Build) !void { const helper: TestHelper = .{ .b = b, .top_level_step = test_step, - .runner = runner, + .runner = runner_exe, }; if (b.option([]const u8, "test-filter", "Comma seperated list of specific tests to run")) |tests| { @@ -591,7 +608,7 @@ pub fn build(b: *std.Build) !void { pub const TlsBackend = enum { openssl, mbedtls, securetransport }; fn maybeAddTlsIncludes( - compile: *std.Build.Step.Compile, + compile: *std.Build.Module, dep: ?*std.Build.Dependency, backend: TlsBackend, ) void { diff --git a/build.zig.zon b/build.zig.zon index 86a41cf..965db13 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,7 @@ .{ .name = .libgit2, .version = "1.9.0", - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .fingerprint = 0x7f0051374dea2cba, .dependencies = .{ .libgit2 = .{ @@ -9,8 +9,8 @@ .hash = "N-V-__8AAJbmLwHHxHDWkz0i6WIR6FpNe6tXSLzaPuWtvBBg", }, .openssl = .{ - .url = "git+https://github.com/allyourcodebase/openssl.git#cad7ccba47e42fa608ca655ec14ae33202df86e1", - .hash = "openssl-3.3.2-TC9C3Wa3ZACgB1hZbrLOQCK9XccIBaW_F3imFfIeYP06", + .url = "git+https://github.com/allyourcodebase/openssl.git#0ac677650d43eadf669067d050992118b13338d2", + .hash = "openssl-3.3.2-TC9C3Ze3ZABaKR5hc8KRnRPhTClKrKQKqTnXTUYkvmSR", .lazy = true, }, .mbedtls = .{ diff --git a/build/ClarTestStep.zig b/build/ClarTestStep.zig index 91cc013..d3018e9 100644 --- a/build/ClarTestStep.zig +++ b/build/ClarTestStep.zig @@ -18,7 +18,7 @@ pub fn create(owner: *std.Build, name: []const u8, runner: *Step.Compile) *ClarT .makeFn = make, }), .runner = runner, - .args = .{}, + .args = .empty, }; runner.getEmittedBin().addStepDependencies(&clar.step); return clar; @@ -60,15 +60,15 @@ fn make(step: *Step, options: Step.MakeOptions) !void { } { - var child: std.process.Child = .init(argv_list.items, arena); - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; - - try child.spawn(); + var child = try std.process.spawn(b.graph.io, .{ + .argv = argv_list.items, + .stdin = .close, + .stdout = .pipe, + .stderr = .inherit, + }); var reader_buf: [1024]u8 = undefined; - var file_reader = child.stdout.?.readerStreaming(&reader_buf); + var file_reader = child.stdout.?.readerStreaming(b.graph.io, &reader_buf); const r = &file_reader.interface; var parser: TapParser = .default; @@ -99,8 +99,8 @@ fn make(step: *Step, options: Step.MakeOptions) !void { error.StreamTooLong => return error.TapLineTooLong, } - const term = try child.wait(); - try step.handleChildProcessTerm(term, null, argv_list.items); + const term = try child.wait(b.graph.io); + try step.handleChildProcessTerm(term); } try step.writeManifestAndWatch(&man); diff --git a/build/chmod.zig b/build/chmod.zig index 7a07480..5cb397e 100644 --- a/build/chmod.zig +++ b/build/chmod.zig @@ -2,14 +2,19 @@ //! Accepts a list of file paths as input and changes their permission bits to `0o755`. //! POSIX only. -pub fn main() !void { - var args = std.process.args(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + var args = init.minimal.args.iterate(); _ = args.skip(); while (args.next()) |path| { - const file = std.fs.cwd().openFile(path, .{ .mode = .read_write }) catch |err| + const file = std.Io.Dir.cwd().openFile( + io, + path, + .{ .mode = .read_write }, + ) catch |err| fatal("unable to open file '{s}': {t}", .{ path, err }); - defer file.close(); - file.setPermissions(.{ .inner = .{ .mode = 0o755 } }) catch |err| + defer file.close(io); + file.setPermissions(io, @enumFromInt(0o755)) catch |err| fatal("unable to set permissions on file '{s}': {t}", .{ path, err }); } } diff --git a/build/clar_fix.zig b/build/clar_fix.zig index 8126e67..df82923 100644 --- a/build/clar_fix.zig +++ b/build/clar_fix.zig @@ -6,12 +6,11 @@ const std = @import("std"); const fixture_var_name = "CLAR_FIXTURE_PATH"; -pub fn main() !void { - var arena_inst: std.heap.ArenaAllocator = .init(std.heap.page_allocator); - defer arena_inst.deinit(); - const arena = arena_inst.allocator(); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const arena = init.arena.allocator(); - var args = try std.process.argsWithAllocator(arena); + var args = init.minimal.args.iterate(); _ = args.skip(); const clar_fixture_h = args.next() orelse fatal("expected path to 'clar/fixtures.h' file", .{}); @@ -20,8 +19,11 @@ pub fn main() !void { var cleaned_path: std.ArrayList(u8) = try .initCapacity(arena, std.fs.max_path_bytes + 2); cleaned_path.appendAssumeCapacity('"'); // add string quotes - const abs_path = try std.fs.cwd().realpath(path_arg, cleaned_path.unusedCapacitySlice()); - cleaned_path.items.len += abs_path.len; + const rec_dir = try std.Io.Dir.cwd().openDir(io, path_arg, .{}); + defer rec_dir.close(io); + + const abs_path_len = try rec_dir.realPath(io, cleaned_path.unusedCapacitySlice()); + cleaned_path.items.len += abs_path_len; cleaned_path.appendAssumeCapacity('"'); // clar expects the fixture path to only have posix seperators or else some tests will break @@ -31,12 +33,16 @@ pub fn main() !void { break :blk cleaned_path.items; }; - const file = try std.fs.cwd().openFile(clar_fixture_h, .{ .mode = .read_write }); - defer file.close(); + const file = try std.Io.Dir.cwd().openFile( + io, + clar_fixture_h, + .{ .mode = .read_write }, + ); + defer file.close(io); var buf: [1024]u8 = undefined; var src: std.ArrayList(u8) = src: { - var file_reader = file.reader(&buf); + var file_reader = file.reader(io, &buf); const file_size = try file_reader.getSize(); const to_add = fixture_path.len -| fixture_var_name.len; @@ -55,8 +61,7 @@ pub fn main() !void { const start = i + "return fixture_path(".len; src.replaceRangeAssumeCapacity(start, fixture_var_name.len, fixture_path); - try file.seekTo(0); - var writer = file.writer(&buf); // buf is safe to reuse since file_reader is out of scope + var writer = file.writer(io, &buf); // buf is safe to reuse since file_reader is out of scope try writer.interface.writeAll(src.items); try writer.interface.flush(); } From 7d337d5e1043dc08d92ea50c170e3481b683bfcb Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Sat, 18 Apr 2026 23:48:08 +0200 Subject: [PATCH 02/11] adapt clar step to use host executable for parsing switch back custom run step to zig run step readd clar step as run and parse --- build.zig | 6 ++--- build/ClarParser.zig | 4 ++++ build/ClarTestStep.zig | 53 ++++++++++++++++++++++++------------------ 3 files changed, 36 insertions(+), 27 deletions(-) create mode 100644 build/ClarParser.zig diff --git a/build.zig b/build.zig index 3c110cf..88c16ae 100644 --- a/build.zig +++ b/build.zig @@ -543,7 +543,6 @@ pub fn build(b: *std.Build) !void { }); const run_fix = b.addRunArtifact(clar_fix); - // run_fix.has_side_effects = true; // @Todo is this necessary? What are the rules for cache invalidation with Run steps? run_fix.addFileArg(clar_src.path(b, "clar/fixtures.h")); run_fix.addDirectoryArg(resources_dir); runner_exe.step.dependOn(&run_fix.step); @@ -553,7 +552,6 @@ pub fn build(b: *std.Build) !void { b: *std.Build, top_level_step: *std.Build.Step, runner: *std.Build.Step.Compile, - const ClarStep = @import("build/ClarTestStep.zig"); fn addTest( @@ -562,7 +560,7 @@ pub fn build(b: *std.Build) !void { args: []const []const u8, ) void { const clar = ClarStep.create(self.b, name, self.runner); - self.top_level_step.dependOn(&clar.step); + self.top_level_step.dependOn(clar.step); clar.addArgs(args); } @@ -573,7 +571,7 @@ pub fn build(b: *std.Build) !void { tests: []const u8, ) void { const clar = ClarStep.create(self.b, name, self.runner); - self.top_level_step.dependOn(&clar.step); + self.top_level_step.dependOn(clar.step); var iter = std.mem.tokenizeScalar(u8, tests, ','); while (iter.next()) |filter| { clar.addArg(self.b.fmt("-s{s}", .{filter})); diff --git a/build/ClarParser.zig b/build/ClarParser.zig new file mode 100644 index 0000000..321ea72 --- /dev/null +++ b/build/ClarParser.zig @@ -0,0 +1,4 @@ +const std = @import("std"); +pub fn main() void { + // todo: read and parse arg 1 +} diff --git a/build/ClarTestStep.zig b/build/ClarTestStep.zig index d3018e9..53f7363 100644 --- a/build/ClarTestStep.zig +++ b/build/ClarTestStep.zig @@ -2,31 +2,38 @@ //! reporting progress/errors to the build system. // Based on Step.Run -step: Step, -runner: *Step.Compile, -args: std.ArrayListUnmanaged([]const u8), +step: *Step, +run: *Step.Run, +parse: *Step.Run, const ClarTestStep = @This(); pub fn create(owner: *std.Build, name: []const u8, runner: *Step.Compile) *ClarTestStep { const clar = owner.allocator.create(ClarTestStep) catch @panic("OOM"); - clar.* = .{ - .step = Step.init(.{ - .id = .custom, - .name = name, - .owner = owner, - .makeFn = make, + const run = owner.addRunArtifact(runner); + run.setName(owner.fmt("run-{s}", .{name})); + + const parse = owner.addRunArtifact(owner.addExecutable(.{ + .name = "clar-parser", + .root_module = owner.createModule(.{ + .root_source_file = owner.path("build/ClarParser.zig"), + .target = owner.graph.host, + .optimize = .ReleaseSafe, }), - .runner = runner, - .args = .empty, + })); + parse.setName(owner.fmt("parse-{s}", .{name})); + parse.addFileArg(run.captureStdOut(.{})); + + clar.* = .{ + .step = &parse.step, + .run = run, + .parse = parse, }; - runner.getEmittedBin().addStepDependencies(&clar.step); return clar; } pub fn addArg(clar: *ClarTestStep, arg: []const u8) void { - const b = clar.step.owner; - clar.args.append(b.allocator, b.dupe(arg)) catch @panic("OOM"); + clar.run.addArg(arg); } pub fn addArgs(clar: *ClarTestStep, args: []const []const u8) void { @@ -60,15 +67,15 @@ fn make(step: *Step, options: Step.MakeOptions) !void { } { - var child = try std.process.spawn(b.graph.io, .{ - .argv = argv_list.items, - .stdin = .close, - .stdout = .pipe, - .stderr = .inherit, - }); + var child: std.process.Child = .init(argv_list.items, arena); + child.stdin_behavior = .Ignore; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Inherit; + + try child.spawn(); var reader_buf: [1024]u8 = undefined; - var file_reader = child.stdout.?.readerStreaming(b.graph.io, &reader_buf); + var file_reader = child.stdout.?.readerStreaming(&reader_buf); const r = &file_reader.interface; var parser: TapParser = .default; @@ -99,8 +106,8 @@ fn make(step: *Step, options: Step.MakeOptions) !void { error.StreamTooLong => return error.TapLineTooLong, } - const term = try child.wait(b.graph.io); - try step.handleChildProcessTerm(term); + const term = try child.wait(); + try step.handleChildProcessTerm(term, null, argv_list.items); } try step.writeManifestAndWatch(&man); From 258e377a68bf6c4949d2249eb0ad28be4ffb4ebb Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Sat, 18 Apr 2026 23:58:16 +0200 Subject: [PATCH 03/11] upgrade ci to 0.16.0 --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c55649e..0c0dc31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,9 +14,9 @@ jobs: strategy: fail-fast: false matrix: - zig-version: ["0.15.2"] - os: [ubuntu-latest, macos-latest, windows-latest] - + zig-version: [ "0.16.0" ] + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} steps: - name: Checkout From 15c3414e4bdab5f81113a7c5597512d374685bde Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Sun, 19 Apr 2026 03:52:02 +0200 Subject: [PATCH 04/11] fix args on windows --- build/chmod.zig | 4 +++- build/clar_fix.zig | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build/chmod.zig b/build/chmod.zig index 5cb397e..80d9d92 100644 --- a/build/chmod.zig +++ b/build/chmod.zig @@ -4,7 +4,9 @@ pub fn main(init: std.process.Init) !void { const io = init.io; - var args = init.minimal.args.iterate(); + const arena = init.arena.allocator(); + + var args = try init.minimal.args.iterateAllocator(arena); _ = args.skip(); while (args.next()) |path| { const file = std.Io.Dir.cwd().openFile( diff --git a/build/clar_fix.zig b/build/clar_fix.zig index df82923..4ba91b2 100644 --- a/build/clar_fix.zig +++ b/build/clar_fix.zig @@ -10,7 +10,7 @@ pub fn main(init: std.process.Init) !void { const io = init.io; const arena = init.arena.allocator(); - var args = init.minimal.args.iterate(); + var args = try init.minimal.args.iterateAllocator(arena); _ = args.skip(); const clar_fixture_h = args.next() orelse fatal("expected path to 'clar/fixtures.h' file", .{}); From 5f07e504cd407d98c8a42f0820a28a67dfaf7d36 Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Thu, 28 May 2026 09:40:33 +0200 Subject: [PATCH 05/11] Apply suggestion from @Parzival-3141 Co-authored-by: Julian <29632054+Parzival-3141@users.noreply.github.com> --- build/clar_fix.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/clar_fix.zig b/build/clar_fix.zig index 4ba91b2..bc29c93 100644 --- a/build/clar_fix.zig +++ b/build/clar_fix.zig @@ -63,7 +63,7 @@ pub fn main(init: std.process.Init) !void { var writer = file.writer(io, &buf); // buf is safe to reuse since file_reader is out of scope try writer.interface.writeAll(src.items); - try writer.interface.flush(); + try writer.end(); } fn fatal(comptime fmt: []const u8, args: anytype) noreturn { From 04e83efeef20ac033bff90a3739498087024caba Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Thu, 28 May 2026 09:46:30 +0200 Subject: [PATCH 06/11] add -t to clar runs --- build/ClarTestStep.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/build/ClarTestStep.zig b/build/ClarTestStep.zig index 53f7363..e730f08 100644 --- a/build/ClarTestStep.zig +++ b/build/ClarTestStep.zig @@ -12,6 +12,7 @@ pub fn create(owner: *std.Build, name: []const u8, runner: *Step.Compile) *ClarT const clar = owner.allocator.create(ClarTestStep) catch @panic("OOM"); const run = owner.addRunArtifact(runner); run.setName(owner.fmt("run-{s}", .{name})); + run.addArg("-t"); const parse = owner.addRunArtifact(owner.addExecutable(.{ .name = "clar-parser", From 94f3711f91ba3b4d0cc51bb0be1726e44b170c28 Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Mon, 1 Jun 2026 21:08:20 +0200 Subject: [PATCH 07/11] move clar step parsing to separate binary --- build/ClarParser.zig | 175 +++++++++++++++++++++++++++++++++++++- build/ClarTestStep.zig | 187 ----------------------------------------- 2 files changed, 173 insertions(+), 189 deletions(-) diff --git a/build/ClarParser.zig b/build/ClarParser.zig index 321ea72..c08dd85 100644 --- a/build/ClarParser.zig +++ b/build/ClarParser.zig @@ -1,4 +1,175 @@ const std = @import("std"); -pub fn main() void { - // todo: read and parse arg 1 +const mem = std.mem; +const Allocator = mem.Allocator; + +pub fn main(init: std.process.Init) !void { + const io = init.io; + const arena = init.arena.allocator(); + const cwd = std.Io.Dir.cwd(); + const args = try init.minimal.args.toSlice(arena); + const input = try cwd.openFile(io, args[1], .{}); + defer input.close(io); + + var reader_buf: [1024]u8 = undefined; + var file_reader = input.readerStreaming(io, &reader_buf); + const r = &file_reader.interface; + + var parser: TapParser = .default; + var errors: std.ArrayList([]const u8) = .empty; + + var suite: ?[]const u8 = null; + while (r.takeDelimiter('\n')) |line| { + switch (try parser.parseLine(arena, line orelse break)) { + .start_suite => |s| { + if (suite) |last_suite| { + try check_errors(io, errors.items, last_suite); + suite = null; + errors.clearRetainingCapacity(); + } + suite = s; + }, + .ok => {}, + .failure => |fail| { + // @Cleanup print failures in a nicer way. Avoid redundant "error:" prefixes on newlines with minimal allocations. + try errors.append(arena, fail.description.items); + try errors.appendSlice(arena, fail.reasons.items); + try errors.append(arena, "\n"); + parser.reset(); + }, + .feed_line => {}, + } + } else |err| switch (err) { + error.ReadFailed => return file_reader.err.?, + error.StreamTooLong => return error.TapLineTooLong, + } + + try check_errors(io, errors.items, suite); } + +pub fn check_errors(io: std.Io, errors: []const []const u8, suite: ?[]const u8) !void { + if (errors.len > 0) { + const stderr = std.Io.File.stderr(); + var stderr_buf: [1024]u8 = undefined; + var stderr_writer = stderr.writerStreaming(io, &stderr_buf); + if (suite) |s| try stderr_writer.interface.print("suite: {s}\n", .{s}); + for (errors) |err| { + try stderr_writer.interface.writeAll(err); + } + try stderr_writer.flush(); + std.process.exit(1); + } +} + +const TapParser = struct { + state: State, + wip_failure: Result.Failure, + + const Result = union(enum) { + start_suite: []const u8, + ok, + failure: Failure, + feed_line, + + const Failure = struct { + description: std.ArrayList(u8), + reasons: std.ArrayList([]const u8), + }; + }; + + const keyword = struct { + const suite_start = "# start of suite "; + const ok = "ok "; + const not_ok = "not ok "; + const spacer1 = " - "; + const spacer2 = ": "; + const yaml_blk = " ---"; + const pre_reason = "reason: |"; + const at = "at:"; + const file = "file: "; + const line = "line: "; + }; + + const State = enum { + start, + desc, + yaml_start, + pre_reason, + reason, + file, + line, + }; + + fn parseLine(p: *TapParser, arena: Allocator, line: []const u8) Allocator.Error!Result { + loop: switch (p.state) { + .start => { + if (mem.startsWith(u8, line, keyword.suite_start)) { + const suite_start = skip(line, keyword.spacer2, keyword.suite_start.len) orelse @panic("expected suite number"); + return .{ .start_suite = line[suite_start..] }; + } else if (mem.startsWith(u8, line, keyword.ok)) { + return .ok; + } else if (mem.startsWith(u8, line, keyword.not_ok)) { + p.state = .desc; + continue :loop p.state; + } + }, + + // Failure parsing + .desc => { + const name_start = skip(line, keyword.spacer1, keyword.not_ok.len) orelse @panic("expected spacer"); + const name = mem.trim(u8, line[name_start..], &std.ascii.whitespace); + try p.wip_failure.description.appendSlice(arena, name); + try p.wip_failure.description.appendSlice(arena, ": "); + p.state = .yaml_start; + }, + .yaml_start => { + _ = mem.indexOf(u8, line, keyword.yaml_blk) orelse @panic("expected yaml_blk"); + p.state = .pre_reason; + }, + .pre_reason => { + _ = mem.indexOf(u8, line, keyword.pre_reason) orelse @panic("expected pre_reason"); + p.state = .reason; + }, + .reason => { + if (mem.indexOf(u8, line, keyword.at) != null) { + p.state = .file; + } else { + const ln = mem.trim(u8, line, &std.ascii.whitespace); + try p.wip_failure.reasons.append(arena, try arena.dupe(u8, ln)); + } + }, + .file => { + const file_start = skip(line, keyword.file, 0) orelse @panic("expected file"); + const file = mem.trim(u8, line[file_start..], std.ascii.whitespace ++ "'"); + try p.wip_failure.description.appendSlice(arena, file); + try p.wip_failure.description.append(arena, ':'); + p.state = .line; + }, + .line => { + const line_start = skip(line, keyword.line, 0) orelse @panic("expected line"); + const fail_line = mem.trim(u8, line[line_start..], &std.ascii.whitespace); + try p.wip_failure.description.appendSlice(arena, fail_line); + p.state = .start; + return .{ .failure = p.wip_failure }; + }, + } + + return .feed_line; + } + + fn skip(line: []const u8, to_skip: []const u8, start: usize) ?usize { + const index = mem.indexOfPos(u8, line, start, to_skip) orelse return null; + return to_skip.len + index; + } + + const default: TapParser = .{ + .state = .start, + .wip_failure = .{ + .description = .empty, + .reasons = .empty, + }, + }; + + fn reset(p: *TapParser) void { + p.* = default; + } +}; diff --git a/build/ClarTestStep.zig b/build/ClarTestStep.zig index e730f08..1faf27d 100644 --- a/build/ClarTestStep.zig +++ b/build/ClarTestStep.zig @@ -41,193 +41,6 @@ pub fn addArgs(clar: *ClarTestStep, args: []const []const u8) void { for (args) |arg| clar.addArg(arg); } -fn make(step: *Step, options: Step.MakeOptions) !void { - const clar: *ClarTestStep = @fieldParentPtr("step", step); - const b = step.owner; - const arena = b.allocator; - - var man = b.graph.cache.obtain(); - defer man.deinit(); - - var argv_list: std.ArrayList([]const u8) = .empty; - { - const file_path = clar.runner.installed_path orelse clar.runner.generated_bin.?.path.?; - try argv_list.append(arena, file_path); - _ = try man.addFile(file_path, null); - } - try argv_list.append(arena, "-t"); // force TAP output - for (clar.args.items) |arg| { - try argv_list.append(arena, arg); - man.hash.addBytes(arg); - } - - if (try step.cacheHitAndWatch(&man)) { - // cache hit, skip running command - step.result_cached = true; - return; - } - - { - var child: std.process.Child = .init(argv_list.items, arena); - child.stdin_behavior = .Ignore; - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Inherit; - - try child.spawn(); - - var reader_buf: [1024]u8 = undefined; - var file_reader = child.stdout.?.readerStreaming(&reader_buf); - const r = &file_reader.interface; - - var parser: TapParser = .default; - var node: ?std.Progress.Node = null; - defer if (node) |n| n.end(); - - while (r.takeDelimiter('\n')) |line| { - switch (try parser.parseLine(arena, line orelse break)) { - .start_suite => |suite| { - if (node) |n| n.end(); - node = options.progress_node.start(suite, 0); - }, - .ok => { - if (node) |n| n.completeOne(); - }, - .failure => |fail| { - // @Cleanup print failures in a nicer way. Avoid redundant "error:" prefixes on newlines with minimal allocations. - try step.result_error_msgs.append(arena, fail.description.items); - try step.result_error_msgs.appendSlice(arena, fail.reasons.items); - try step.result_error_msgs.append(arena, "\n"); - if (node) |n| n.completeOne(); - parser.reset(); - }, - .feed_line => {}, - } - } else |err| switch (err) { - error.ReadFailed => return file_reader.err.?, - error.StreamTooLong => return error.TapLineTooLong, - } - - const term = try child.wait(); - try step.handleChildProcessTerm(term, null, argv_list.items); - } - - try step.writeManifestAndWatch(&man); -} - -const TapParser = struct { - state: State, - wip_failure: Result.Failure, - - const Result = union(enum) { - start_suite: []const u8, - ok, - failure: Failure, - feed_line, - - const Failure = struct { - description: std.ArrayList(u8), - reasons: std.ArrayList([]const u8), - }; - }; - - const keyword = struct { - const suite_start = "# start of suite "; - const ok = "ok "; - const not_ok = "not ok "; - const spacer1 = " - "; - const spacer2 = ": "; - const yaml_blk = " ---"; - const pre_reason = "reason: |"; - const at = "at:"; - const file = "file: "; - const line = "line: "; - }; - - const State = enum { - start, - desc, - yaml_start, - pre_reason, - reason, - file, - line, - }; - - fn parseLine(p: *TapParser, step_arena: Allocator, line: []const u8) Allocator.Error!Result { - loop: switch (p.state) { - .start => { - if (mem.startsWith(u8, line, keyword.suite_start)) { - const suite_start = skip(line, keyword.spacer2, keyword.suite_start.len) orelse @panic("expected suite number"); - return .{ .start_suite = line[suite_start..] }; - } else if (mem.startsWith(u8, line, keyword.ok)) { - return .ok; - } else if (mem.startsWith(u8, line, keyword.not_ok)) { - p.state = .desc; - continue :loop p.state; - } - }, - - // Failure parsing - .desc => { - const name_start = skip(line, keyword.spacer1, keyword.not_ok.len) orelse @panic("expected spacer"); - const name = mem.trim(u8, line[name_start..], &std.ascii.whitespace); - try p.wip_failure.description.appendSlice(step_arena, name); - try p.wip_failure.description.appendSlice(step_arena, ": "); - p.state = .yaml_start; - }, - .yaml_start => { - _ = mem.indexOf(u8, line, keyword.yaml_blk) orelse @panic("expected yaml_blk"); - p.state = .pre_reason; - }, - .pre_reason => { - _ = mem.indexOf(u8, line, keyword.pre_reason) orelse @panic("expected pre_reason"); - p.state = .reason; - }, - .reason => { - if (mem.indexOf(u8, line, keyword.at) != null) { - p.state = .file; - } else { - const ln = mem.trim(u8, line, &std.ascii.whitespace); - try p.wip_failure.reasons.append(step_arena, try step_arena.dupe(u8, ln)); - } - }, - .file => { - const file_start = skip(line, keyword.file, 0) orelse @panic("expected file"); - const file = mem.trim(u8, line[file_start..], std.ascii.whitespace ++ "'"); - try p.wip_failure.description.appendSlice(step_arena, file); - try p.wip_failure.description.append(step_arena, ':'); - p.state = .line; - }, - .line => { - const line_start = skip(line, keyword.line, 0) orelse @panic("expected line"); - const fail_line = mem.trim(u8, line[line_start..], &std.ascii.whitespace); - try p.wip_failure.description.appendSlice(step_arena, fail_line); - p.state = .start; - return .{ .failure = p.wip_failure }; - }, - } - - return .feed_line; - } - - fn skip(line: []const u8, to_skip: []const u8, start: usize) ?usize { - const index = mem.indexOfPos(u8, line, start, to_skip) orelse return null; - return to_skip.len + index; - } - - const default: TapParser = .{ - .state = .start, - .wip_failure = .{ - .description = .empty, - .reasons = .empty, - }, - }; - - fn reset(p: *TapParser) void { - p.* = default; - } -}; - const std = @import("std"); const mem = std.mem; const Step = std.Build.Step; From 0594b3960985d642b7fc0bc06b1688fe036885ec Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Mon, 1 Jun 2026 21:12:57 +0200 Subject: [PATCH 08/11] add check for wrong number of arguments --- build/ClarParser.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build/ClarParser.zig b/build/ClarParser.zig index c08dd85..a67905b 100644 --- a/build/ClarParser.zig +++ b/build/ClarParser.zig @@ -7,6 +7,10 @@ pub fn main(init: std.process.Init) !void { const arena = init.arena.allocator(); const cwd = std.Io.Dir.cwd(); const args = try init.minimal.args.toSlice(arena); + if (args.len != 2) { + std.log.err("expected exactly one argument, but received: {d}", .{args.len}); + return error.InvalidArgs; + } const input = try cwd.openFile(io, args[1], .{}); defer input.close(io); From ac62494689050c4e0d3c20d17c087a7a9117abe2 Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Mon, 1 Jun 2026 21:14:19 +0200 Subject: [PATCH 09/11] switch to args.iterate( --- build/chmod.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build/chmod.zig b/build/chmod.zig index 80d9d92..40b0531 100644 --- a/build/chmod.zig +++ b/build/chmod.zig @@ -4,9 +4,8 @@ pub fn main(init: std.process.Init) !void { const io = init.io; - const arena = init.arena.allocator(); - var args = try init.minimal.args.iterateAllocator(arena); + var args = init.minimal.args.iterate(); _ = args.skip(); while (args.next()) |path| { const file = std.Io.Dir.cwd().openFile( From a5a9915d1bded79114c5acb26eb17950d85d5dd3 Mon Sep 17 00:00:00 2001 From: Tobias Simetsreiter Date: Mon, 1 Jun 2026 21:37:52 +0200 Subject: [PATCH 10/11] adapt args passthrough for zig 0.17.0-dev.639+284ab0ad8 --- build.zig | 20 ++++++++++++++------ build/ClarParser.zig | 16 +++++++++++----- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/build.zig b/build.zig index 88c16ae..447abdd 100644 --- a/build.zig +++ b/build.zig @@ -401,9 +401,8 @@ pub fn build(b: *std.Build) !void { }); const cli_install = b.addInstallArtifact(cli_exe, .{}); const cli_run = b.addRunArtifact(cli_exe); - if (b.args) |args| { - for (args) |arg| cli_run.addArg(arg); - } + + passthroughArgs(b, cli_run); cli_run.step.dependOn(&cli_install.step); cli_step.dependOn(&cli_run.step); } @@ -436,9 +435,7 @@ pub fn build(b: *std.Build) !void { // independent install step so you can easily access the binary const examples_install = b.addInstallArtifact(lg2_exe, .{}); const example_run = b.addRunArtifact(lg2_exe); - if (b.args) |args| { - for (args) |arg| example_run.addArg(arg); - } + passthroughArgs(b, example_run); example_run.step.dependOn(&examples_install.step); examples_step.dependOn(&example_run.step); } @@ -603,6 +600,17 @@ pub fn build(b: *std.Build) !void { } } +// zig 0.17.0 and 0.16.0 compatible args passthrough function +inline fn passthroughArgs(b: *std.Build, run: *std.Build.Step.Run) void { + if (comptime @import("builtin").zig_version.order(std.SemanticVersion.parse("0.16.0") catch unreachable) == .gt) { + run.addPassthruArgs(); + } else { + if (b.args) |args| { + for (args) |arg| run.addArg(arg); + } + } +} + pub const TlsBackend = enum { openssl, mbedtls, securetransport }; fn maybeAddTlsIncludes( diff --git a/build/ClarParser.zig b/build/ClarParser.zig index a67905b..f7ea8eb 100644 --- a/build/ClarParser.zig +++ b/build/ClarParser.zig @@ -22,12 +22,14 @@ pub fn main(init: std.process.Init) !void { var errors: std.ArrayList([]const u8) = .empty; var suite: ?[]const u8 = null; + var error_found: bool = false; while (r.takeDelimiter('\n')) |line| { switch (try parser.parseLine(arena, line orelse break)) { .start_suite => |s| { if (suite) |last_suite| { - try check_errors(io, errors.items, last_suite); - suite = null; + if (try check_errors(io, errors.items, last_suite)) { + error_found = true; + } errors.clearRetainingCapacity(); } suite = s; @@ -47,10 +49,13 @@ pub fn main(init: std.process.Init) !void { error.StreamTooLong => return error.TapLineTooLong, } - try check_errors(io, errors.items, suite); + if (try check_errors(io, errors.items, suite)) { + error_found = true; + } + if (error_found) std.process.exit(1); } -pub fn check_errors(io: std.Io, errors: []const []const u8, suite: ?[]const u8) !void { +pub fn check_errors(io: std.Io, errors: []const []const u8, suite: ?[]const u8) !bool { if (errors.len > 0) { const stderr = std.Io.File.stderr(); var stderr_buf: [1024]u8 = undefined; @@ -60,8 +65,9 @@ pub fn check_errors(io: std.Io, errors: []const []const u8, suite: ?[]const u8) try stderr_writer.interface.writeAll(err); } try stderr_writer.flush(); - std.process.exit(1); + return true; } + return false; } const TapParser = struct { From c8223940c1cdd3394ad6e6de3ef4c77f83a6033c Mon Sep 17 00:00:00 2001 From: Parzival-3141 <29632054+Parzival-3141@users.noreply.github.com> Date: Wed, 3 Jun 2026 18:40:36 -0400 Subject: [PATCH 11/11] execute test runner as subprocess of clar_parser The test runner returns a non 0 exit code if a test failed, which fails the build step. Since Run steps don't support ignoring exit codes, the parser step has to execute the runner itself. This is a slight regression since progress and error reporting is no longer integrated with the build system. --- build/ClarTestStep.zig | 27 +++++-------- build/{ClarParser.zig => clar_parser.zig} | 46 ++++++++++++++++------- 2 files changed, 43 insertions(+), 30 deletions(-) rename build/{ClarParser.zig => clar_parser.zig} (78%) diff --git a/build/ClarTestStep.zig b/build/ClarTestStep.zig index 1faf27d..c7d6d7c 100644 --- a/build/ClarTestStep.zig +++ b/build/ClarTestStep.zig @@ -1,40 +1,33 @@ -//! Runs a Clar test and lightly parses it's [TAP](https://testanything.org/) stream, -//! reporting progress/errors to the build system. -// Based on Step.Run +//! See clar_parser.zig step: *Step, -run: *Step.Run, -parse: *Step.Run, +test_and_parse: *Step.Run, const ClarTestStep = @This(); pub fn create(owner: *std.Build, name: []const u8, runner: *Step.Compile) *ClarTestStep { const clar = owner.allocator.create(ClarTestStep) catch @panic("OOM"); - const run = owner.addRunArtifact(runner); - run.setName(owner.fmt("run-{s}", .{name})); - run.addArg("-t"); - const parse = owner.addRunArtifact(owner.addExecutable(.{ + const run = owner.addRunArtifact(owner.addExecutable(.{ .name = "clar-parser", .root_module = owner.createModule(.{ - .root_source_file = owner.path("build/ClarParser.zig"), + .root_source_file = owner.path("build/clar_parser.zig"), .target = owner.graph.host, - .optimize = .ReleaseSafe, + .optimize = .Debug, }), })); - parse.setName(owner.fmt("parse-{s}", .{name})); - parse.addFileArg(run.captureStdOut(.{})); + run.setName(owner.fmt("test-{s}", .{name})); + run.addArtifactArg(runner); clar.* = .{ - .step = &parse.step, - .run = run, - .parse = parse, + .step = &run.step, + .test_and_parse = run, }; return clar; } pub fn addArg(clar: *ClarTestStep, arg: []const u8) void { - clar.run.addArg(arg); + clar.test_and_parse.addArg(arg); } pub fn addArgs(clar: *ClarTestStep, args: []const []const u8) void { diff --git a/build/ClarParser.zig b/build/clar_parser.zig similarity index 78% rename from build/ClarParser.zig rename to build/clar_parser.zig index f7ea8eb..0842309 100644 --- a/build/ClarParser.zig +++ b/build/clar_parser.zig @@ -1,21 +1,35 @@ +//! Runs a Clar test and lightly parses it's [TAP](https://testanything.org/) stream, +//! reporting progress/errors to the build system. +// @Todo report progress/errors to the build system + const std = @import("std"); const mem = std.mem; const Allocator = mem.Allocator; -pub fn main(init: std.process.Init) !void { +pub fn main(init: std.process.Init) !u8 { const io = init.io; const arena = init.arena.allocator(); - const cwd = std.Io.Dir.cwd(); - const args = try init.minimal.args.toSlice(arena); - if (args.len != 2) { - std.log.err("expected exactly one argument, but received: {d}", .{args.len}); + const args = (try init.minimal.args.toSlice(arena))[1..]; + + if (args.len < 1) { + std.log.err("expected at least one argument", .{}); return error.InvalidArgs; } - const input = try cwd.openFile(io, args[1], .{}); - defer input.close(io); + + var argv_list: std.ArrayList([]const u8) = try .initCapacity(arena, args.len + 1); + argv_list.appendAssumeCapacity(args[0]); // test runner executable + argv_list.appendAssumeCapacity("-t"); // force TAP output + argv_list.appendSliceAssumeCapacity(args[1..]); // test runner args + + var runner = try std.process.spawn(io, .{ + .argv = argv_list.items, + .stdin = .ignore, + .stdout = .pipe, + .stderr = .inherit, + }); var reader_buf: [1024]u8 = undefined; - var file_reader = input.readerStreaming(io, &reader_buf); + var file_reader = runner.stdout.?.readerStreaming(io, &reader_buf); const r = &file_reader.interface; var parser: TapParser = .default; @@ -26,17 +40,18 @@ pub fn main(init: std.process.Init) !void { while (r.takeDelimiter('\n')) |line| { switch (try parser.parseLine(arena, line orelse break)) { .start_suite => |s| { + // @Todo integrate with build progress nodes instead if (suite) |last_suite| { if (try check_errors(io, errors.items, last_suite)) { error_found = true; } + arena.free(last_suite); errors.clearRetainingCapacity(); } suite = s; }, .ok => {}, .failure => |fail| { - // @Cleanup print failures in a nicer way. Avoid redundant "error:" prefixes on newlines with minimal allocations. try errors.append(arena, fail.description.items); try errors.appendSlice(arena, fail.reasons.items); try errors.append(arena, "\n"); @@ -46,13 +61,15 @@ pub fn main(init: std.process.Init) !void { } } else |err| switch (err) { error.ReadFailed => return file_reader.err.?, - error.StreamTooLong => return error.TapLineTooLong, + error.StreamTooLong => return error.TapLineTooLong, // if you get this error then the reader buffer is too short! } + _ = try runner.wait(io); // @Todo report term? if (try check_errors(io, errors.items, suite)) { error_found = true; } - if (error_found) std.process.exit(1); + + return @intFromBool(error_found); } pub fn check_errors(io: std.Io, errors: []const []const u8, suite: ?[]const u8) !bool { @@ -60,9 +77,11 @@ pub fn check_errors(io: std.Io, errors: []const []const u8, suite: ?[]const u8) const stderr = std.Io.File.stderr(); var stderr_buf: [1024]u8 = undefined; var stderr_writer = stderr.writerStreaming(io, &stderr_buf); - if (suite) |s| try stderr_writer.interface.print("suite: {s}\n", .{s}); + + try stderr_writer.interface.print("errors in suite: {?s}\n", .{suite}); for (errors) |err| { try stderr_writer.interface.writeAll(err); + try stderr_writer.interface.writeByte('\n'); } try stderr_writer.flush(); return true; @@ -109,12 +128,13 @@ const TapParser = struct { line, }; + /// caller owns any memory allocated in Result (start_suite and failure) fn parseLine(p: *TapParser, arena: Allocator, line: []const u8) Allocator.Error!Result { loop: switch (p.state) { .start => { if (mem.startsWith(u8, line, keyword.suite_start)) { const suite_start = skip(line, keyword.spacer2, keyword.suite_start.len) orelse @panic("expected suite number"); - return .{ .start_suite = line[suite_start..] }; + return .{ .start_suite = try arena.dupe(u8, line[suite_start..]) }; } else if (mem.startsWith(u8, line, keyword.ok)) { return .ok; } else if (mem.startsWith(u8, line, keyword.not_ok)) {