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 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..447abdd 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,28 +395,32 @@ 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); - if (b.args) |args| { - for (args) |arg| cli_run.addArg(arg); - } + 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); + + passthroughArgs(b, cli_run); cli_run.step.dependOn(&cli_install.step); cli_step.dependOn(&cli_run.step); } 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,15 +429,13 @@ 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); - if (b.args) |args| { - for (args) |arg| example_run.addArg(arg); - } + const examples_install = b.addInstallArtifact(lg2_exe, .{}); + const example_run = b.addRunArtifact(lg2_exe); + passthroughArgs(b, example_run); example_run.step.dependOn(&examples_install.step); examples_step.dependOn(&example_run.step); } @@ -442,13 +455,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 +521,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; }, @@ -526,17 +540,15 @@ 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.step.dependOn(&run_fix.step); + runner_exe.step.dependOn(&run_fix.step); } const TestHelper = struct { b: *std.Build, top_level_step: *std.Build.Step, runner: *std.Build.Step.Compile, - const ClarStep = @import("build/ClarTestStep.zig"); fn addTest( @@ -545,7 +557,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); } @@ -556,7 +568,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})); @@ -567,7 +579,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| { @@ -588,10 +600,21 @@ 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( - 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..c7d6d7c 100644 --- a/build/ClarTestStep.zig +++ b/build/ClarTestStep.zig @@ -1,225 +1,39 @@ -//! 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, -runner: *Step.Compile, -args: std.ArrayListUnmanaged([]const u8), +step: *Step, +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"); - clar.* = .{ - .step = Step.init(.{ - .id = .custom, - .name = name, - .owner = owner, - .makeFn = make, + + const run = owner.addRunArtifact(owner.addExecutable(.{ + .name = "clar-parser", + .root_module = owner.createModule(.{ + .root_source_file = owner.path("build/clar_parser.zig"), + .target = owner.graph.host, + .optimize = .Debug, }), - .runner = runner, - .args = .{}, + })); + run.setName(owner.fmt("test-{s}", .{name})); + run.addArtifactArg(runner); + + clar.* = .{ + .step = &run.step, + .test_and_parse = run, }; - 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.test_and_parse.addArg(arg); } 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; diff --git a/build/chmod.zig b/build/chmod.zig index 7a07480..40b0531 100644 --- a/build/chmod.zig +++ b/build/chmod.zig @@ -2,14 +2,20 @@ //! 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..bc29c93 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 = try init.minimal.args.iterateAllocator(arena); _ = 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,10 +61,9 @@ 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(); + try writer.end(); } fn fatal(comptime fmt: []const u8, args: anytype) noreturn { diff --git a/build/clar_parser.zig b/build/clar_parser.zig new file mode 100644 index 0000000..0842309 --- /dev/null +++ b/build/clar_parser.zig @@ -0,0 +1,205 @@ +//! 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) !u8 { + const io = init.io; + const arena = init.arena.allocator(); + const args = (try init.minimal.args.toSlice(arena))[1..]; + + if (args.len < 1) { + std.log.err("expected at least one argument", .{}); + return error.InvalidArgs; + } + + 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 = runner.stdout.?.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; + var error_found: bool = false; + 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| { + 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, // 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; + } + + return @intFromBool(error_found); +} + +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; + var stderr_writer = stderr.writerStreaming(io, &stderr_buf); + + 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; + } + return false; +} + +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, + }; + + /// 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 = 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)) { + 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; + } +};