From 047640383e5e635ffe52ab360e03dbe08e73d025 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 23 Jul 2024 20:49:00 -0700 Subject: add `--fuzz` CLI argument to `zig build` This flag makes the build runner rebuild unit tests after the pipeline finishes, if it finds any unit tests. I did not make this integrate with file system watching yet. The test runner is updated to detect which tests are fuzz tests. Run step is updated to track which test indexes are fuzz tests. --- lib/std/Build.zig | 1 + 1 file changed, 1 insertion(+) (limited to 'lib/std/Build.zig') diff --git a/lib/std/Build.zig b/lib/std/Build.zig index f76e3263cb..1ad2e0ee51 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -977,6 +977,7 @@ pub fn addRunArtifact(b: *Build, exe: *Step.Compile) *Step.Run { // Consider that this is declarative; the run step may not be run unless a user // option is supplied. const run_step = Step.Run.create(b, b.fmt("run {s}", .{exe.name})); + run_step.producer = exe; if (exe.kind == .@"test") { if (exe.exec_cmd_args) |exec_cmd_args| { for (exec_cmd_args) |cmd_arg| { -- cgit v1.2.3 From 711ed56ce361fd9051fcf6039de48022b8dbc2d1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Tue, 23 Jul 2024 21:17:14 -0700 Subject: build runner: extract logic to std.Build.Fuzz --- lib/compiler/build_runner.zig | 40 ++----------------------------------- lib/std/Build.zig | 1 + lib/std/Build/Fuzz.zig | 46 +++++++++++++++++++++++++++++++++++++++++++ lib/std/Build/Step/Run.zig | 4 ++++ 4 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 lib/std/Build/Fuzz.zig (limited to 'lib/std/Build.zig') diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 5f7c12fe92..e27b4e8762 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -9,6 +9,7 @@ const ArrayList = std.ArrayList; const File = std.fs.File; const Step = std.Build.Step; const Watch = std.Build.Watch; +const Fuzz = std.Build.Fuzz; const Allocator = std.mem.Allocator; const fatal = std.process.fatal; const runner = @This(); @@ -400,7 +401,7 @@ pub fn main() !void { else => return err, }; if (fuzz) { - startFuzzing(&run.thread_pool, run.step_stack.keys(), main_progress_node); + Fuzz.start(&run.thread_pool, run.step_stack.keys(), main_progress_node); } if (!watch) return cleanExit(); @@ -439,43 +440,6 @@ pub fn main() !void { } } -fn startFuzzing(thread_pool: *std.Thread.Pool, all_steps: []const *Step, prog_node: std.Progress.Node) void { - { - const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0); - defer rebuild_node.end(); - var count: usize = 0; - var wait_group: std.Thread.WaitGroup = .{}; - defer wait_group.wait(); - for (all_steps) |step| { - const run = step.cast(Step.Run) orelse continue; - if (run.fuzz_tests.items.len > 0 and run.producer != null) { - thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, prog_node }); - count += 1; - } - } - if (count == 0) { - std.debug.lockStdErr(); - std.debug.print("no fuzz tests found\n", .{}); - process.exit(2); - } - rebuild_node.setEstimatedTotalItems(count); - } - @panic("TODO do something with the rebuilt unit tests"); -} - -fn rebuildTestsWorkerRun(run: *Step.Run, parent_prog_node: std.Progress.Node) void { - const compile_step = run.producer.?; - const prog_node = parent_prog_node.start(compile_step.step.name, 0); - defer prog_node.end(); - const rebuilt_bin_path = compile_step.rebuildInFuzzMode(prog_node) catch |err| { - std.debug.print("failed to rebuild {s} in fuzz mode: {s}", .{ - compile_step.step.name, @errorName(err), - }); - return; - }; - std.debug.print("rebuilt binary: '{s}'\n", .{rebuilt_bin_path}); -} - fn markFailedStepsDirty(gpa: Allocator, all_steps: []const *Step) void { for (all_steps) |step| switch (step.state) { .dependency_failure, .failure, .skipped => step.recursiveReset(gpa), diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 1ad2e0ee51..8a4f3e87dd 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -21,6 +21,7 @@ pub const Cache = @import("Build/Cache.zig"); pub const Step = @import("Build/Step.zig"); pub const Module = @import("Build/Module.zig"); pub const Watch = @import("Build/Watch.zig"); +pub const Fuzz = @import("Build/Fuzz.zig"); /// Shared state among all Build instances. graph: *Graph, diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig new file mode 100644 index 0000000000..a0e754fab1 --- /dev/null +++ b/lib/std/Build/Fuzz.zig @@ -0,0 +1,46 @@ +const std = @import("../std.zig"); +const Fuzz = @This(); +const Step = std.Build.Step; +const assert = std.debug.assert; +const fatal = std.process.fatal; + +pub fn start(thread_pool: *std.Thread.Pool, all_steps: []const *Step, prog_node: std.Progress.Node) void { + { + const rebuild_node = prog_node.start("Rebuilding Unit Tests", 0); + defer rebuild_node.end(); + var count: usize = 0; + var wait_group: std.Thread.WaitGroup = .{}; + defer wait_group.wait(); + for (all_steps) |step| { + const run = step.cast(Step.Run) orelse continue; + if (run.fuzz_tests.items.len > 0 and run.producer != null) { + thread_pool.spawnWg(&wait_group, rebuildTestsWorkerRun, .{ run, prog_node }); + count += 1; + } + } + if (count == 0) fatal("no fuzz tests found", .{}); + rebuild_node.setEstimatedTotalItems(count); + } + + // Detect failure. + for (all_steps) |step| { + const run = step.cast(Step.Run) orelse continue; + if (run.fuzz_tests.items.len > 0 and run.rebuilt_executable == null) + fatal("one or more unit tests failed to be rebuilt in fuzz mode", .{}); + } + + @panic("TODO do something with the rebuilt unit tests"); +} + +fn rebuildTestsWorkerRun(run: *Step.Run, parent_prog_node: std.Progress.Node) void { + const compile_step = run.producer.?; + const prog_node = parent_prog_node.start(compile_step.step.name, 0); + defer prog_node.end(); + const rebuilt_bin_path = compile_step.rebuildInFuzzMode(prog_node) catch |err| { + std.debug.print("failed to rebuild {s} in fuzz mode: {s}", .{ + compile_step.step.name, @errorName(err), + }); + return; + }; + run.rebuilt_executable = rebuilt_bin_path; +} diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index 7927ac9479..9b7997306f 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -89,6 +89,9 @@ has_side_effects: bool, /// If this is a Zig unit test binary, this tracks the indexes of the unit /// tests that are also fuzz tests. fuzz_tests: std.ArrayListUnmanaged(u32), +/// Populated during the fuzz phase if this run step corresponds to a unit test +/// executable that contains fuzz tests. +rebuilt_executable: ?[]const u8, /// If this Run step was produced by a Compile step, it is tracked here. producer: ?*Step.Compile, @@ -183,6 +186,7 @@ pub fn create(owner: *std.Build, name: []const u8) *Run { .dep_output_file = null, .has_side_effects = false, .fuzz_tests = .{}, + .rebuilt_executable = null, .producer = null, }; return run; -- cgit v1.2.3 From a3c74aca99c7eaf719c745ec6e9ee7366cad1910 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 24 Jul 2024 17:41:44 -0700 Subject: add --debug-rt CLI arg to the compiler + bonus edits The flag makes compiler_rt and libfuzzer be in debug mode. Also: * fuzzer: override debug logs and disable debug logs for frequently called functions * std.Build.Fuzz: fix bug of rerunning the old unit test binary * report errors from rebuilding the unit tests better * link.Elf: additionally add tsan lib and fuzzer lib to the hash --- lib/compiler/build_runner.zig | 10 +++--- lib/fuzzer.zig | 72 ++++++++++++++++++++++++++++++++++++------ lib/std/Build.zig | 1 + lib/std/Build/Fuzz.zig | 36 +++++++++++++-------- lib/std/Build/Step/Compile.zig | 10 ++++++ lib/std/Build/Step/Run.zig | 5 ++- src/Compilation.zig | 4 ++- src/link/Elf.zig | 2 ++ src/main.zig | 5 +++ 9 files changed, 116 insertions(+), 29 deletions(-) (limited to 'lib/std/Build.zig') diff --git a/lib/compiler/build_runner.zig b/lib/compiler/build_runner.zig index 8c22e6d291..91d7b8a043 100644 --- a/lib/compiler/build_runner.zig +++ b/lib/compiler/build_runner.zig @@ -208,6 +208,8 @@ pub fn main() !void { try debug_log_scopes.append(next_arg); } else if (mem.eql(u8, arg, "--debug-pkg-config")) { builder.debug_pkg_config = true; + } else if (mem.eql(u8, arg, "--debug-rt")) { + graph.debug_compiler_runtime_libs = true; } else if (mem.eql(u8, arg, "--debug-compile-errors")) { builder.debug_compile_errors = true; } else if (mem.eql(u8, arg, "--system")) { @@ -1072,7 +1074,8 @@ fn workerMakeOneStep( std.debug.lockStdErr(); defer std.debug.unlockStdErr(); - printErrorMessages(b, s, run.ttyconf, run.stderr, run.prominent_compile_errors) catch {}; + const gpa = b.allocator; + printErrorMessages(gpa, s, run.ttyconf, run.stderr, run.prominent_compile_errors) catch {}; } handle_result: { @@ -1126,14 +1129,12 @@ fn workerMakeOneStep( } pub fn printErrorMessages( - b: *std.Build, + gpa: Allocator, failing_step: *Step, ttyconf: std.io.tty.Config, stderr: File, prominent_compile_errors: bool, ) !void { - const gpa = b.allocator; - // Provide context for where these error messages are coming from by // printing the corresponding Step subtree. @@ -1313,6 +1314,7 @@ fn usage(b: *std.Build, out_stream: anytype) !void { \\ --seed [integer] For shuffling dependency traversal order (default: random) \\ --debug-log [scope] Enable debugging the compiler \\ --debug-pkg-config Fail if unknown pkg-config flags encountered + \\ --debug-rt Debug compiler runtime libraries \\ --verbose-link Enable compiler debug output for linking \\ --verbose-air Enable compiler debug output for Zig AIR \\ --verbose-llvm-ir[=file] Enable compiler debug output for LLVM IR diff --git a/lib/fuzzer.zig b/lib/fuzzer.zig index 8329d1eb2e..26ef66d1c1 100644 --- a/lib/fuzzer.zig +++ b/lib/fuzzer.zig @@ -1,14 +1,40 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +pub const std_options = .{ + .logFn = logOverride, +}; + +var log_file: ?std.fs.File = null; + +fn logOverride( + comptime level: std.log.Level, + comptime scope: @TypeOf(.EnumLiteral), + comptime format: []const u8, + args: anytype, +) void { + const f = if (log_file) |f| f else f: { + const f = std.fs.cwd().createFile("libfuzzer.log", .{}) catch @panic("failed to open fuzzer log file"); + log_file = f; + break :f f; + }; + const prefix1 = comptime level.asText(); + const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): "; + f.writer().print(prefix1 ++ prefix2 ++ format ++ "\n", args) catch @panic("failed to write to fuzzer log"); +} + export threadlocal var __sancov_lowest_stack: usize = 0; export fn __sanitizer_cov_8bit_counters_init(start: [*]u8, stop: [*]u8) void { std.log.debug("__sanitizer_cov_8bit_counters_init start={*}, stop={*}", .{ start, stop }); } -export fn __sanitizer_cov_pcs_init(pcs_beg: [*]const usize, pcs_end: [*]const usize) void { - std.log.debug("__sanitizer_cov_pcs_init pcs_beg={*}, pcs_end={*}", .{ pcs_beg, pcs_end }); +export fn __sanitizer_cov_pcs_init(pc_start: [*]const usize, pc_end: [*]const usize) void { + std.log.debug("__sanitizer_cov_pcs_init pc_start={*}, pc_end={*}", .{ pc_start, pc_end }); + fuzzer.pc_range = .{ + .start = @intFromPtr(pc_start), + .end = @intFromPtr(pc_start), + }; } export fn __sanitizer_cov_trace_const_cmp1(arg1: u8, arg2: u8) void { @@ -48,34 +74,45 @@ export fn __sanitizer_cov_trace_switch(val: u64, cases_ptr: [*]u64) void { const len = cases_ptr[0]; const val_size_in_bits = cases_ptr[1]; const cases = cases_ptr[2..][0..len]; - std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{ - pc, val, val_size_in_bits, cases.len, - }); + _ = val; + _ = pc; + _ = val_size_in_bits; + _ = cases; + //std.log.debug("0x{x}: switch on value {d} ({d} bits) with {d} cases", .{ + // pc, val, val_size_in_bits, cases.len, + //}); } export fn __sanitizer_cov_trace_pc_indir(callee: usize) void { const pc = @returnAddress(); - std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee }); + _ = callee; + _ = pc; + //std.log.debug("0x{x}: indirect call to 0x{x}", .{ pc, callee }); } fn handleCmp(pc: usize, arg1: u64, arg2: u64) void { - std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 }); + _ = pc; + _ = arg1; + _ = arg2; + //std.log.debug("0x{x}: comparison of {d} and {d}", .{ pc, arg1, arg2 }); } const Fuzzer = struct { gpa: Allocator, rng: std.Random.DefaultPrng, input: std.ArrayListUnmanaged(u8), + pc_range: PcRange, + count: usize, const Slice = extern struct { ptr: [*]const u8, len: usize, - fn toSlice(s: Slice) []const u8 { + fn toZig(s: Slice) []const u8 { return s.ptr[0..s.len]; } - fn fromSlice(s: []const u8) Slice { + fn fromZig(s: []const u8) Slice { return .{ .ptr = s.ptr, .len = s.len, @@ -83,14 +120,27 @@ const Fuzzer = struct { } }; + const PcRange = struct { + start: usize, + end: usize, + }; + fn next(f: *Fuzzer) ![]const u8 { const gpa = f.gpa; + + // Prepare next input. const rng = fuzzer.rng.random(); const len = rng.uintLessThan(usize, 64); try f.input.resize(gpa, len); rng.bytes(f.input.items); + f.resetCoverage(); + f.count += 1; return f.input.items; } + + fn resetCoverage(f: *Fuzzer) void { + _ = f; + } }; var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .{}; @@ -99,10 +149,12 @@ var fuzzer: Fuzzer = .{ .gpa = general_purpose_allocator.allocator(), .rng = std.Random.DefaultPrng.init(0), .input = .{}, + .pc_range = .{ .start = 0, .end = 0 }, + .count = 0, }; export fn fuzzer_next() Fuzzer.Slice { - return Fuzzer.Slice.fromSlice(fuzzer.next() catch |err| switch (err) { + return Fuzzer.Slice.fromZig(fuzzer.next() catch |err| switch (err) { error.OutOfMemory => @panic("out of memory"), }); } diff --git a/lib/std/Build.zig b/lib/std/Build.zig index 8a4f3e87dd..e2e417d178 100644 --- a/lib/std/Build.zig +++ b/lib/std/Build.zig @@ -113,6 +113,7 @@ pub const Graph = struct { arena: Allocator, system_library_options: std.StringArrayHashMapUnmanaged(SystemLibraryMode) = .{}, system_package_mode: bool = false, + debug_compiler_runtime_libs: bool = false, cache: Cache, zig_exe: [:0]const u8, env_map: EnvMap, diff --git a/lib/std/Build/Fuzz.zig b/lib/std/Build/Fuzz.zig index 7a86e3857b..2628b92516 100644 --- a/lib/std/Build/Fuzz.zig +++ b/lib/std/Build/Fuzz.zig @@ -55,22 +55,32 @@ pub fn start( } fn rebuildTestsWorkerRun(run: *Step.Run, ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node) void { - const compile_step = run.producer.?; - const prog_node = parent_prog_node.start(compile_step.step.name, 0); + const gpa = run.step.owner.allocator; + const stderr = std.io.getStdErr(); + + const compile = run.producer.?; + const prog_node = parent_prog_node.start(compile.step.name, 0); defer prog_node.end(); - if (compile_step.rebuildInFuzzMode(prog_node)) |rebuilt_bin_path| { + + const result = compile.rebuildInFuzzMode(prog_node); + + const show_compile_errors = compile.step.result_error_bundle.errorMessageCount() > 0; + const show_error_msgs = compile.step.result_error_msgs.items.len > 0; + const show_stderr = compile.step.result_stderr.len > 0; + + if (show_error_msgs or show_compile_errors or show_stderr) { + std.debug.lockStdErr(); + defer std.debug.unlockStdErr(); + build_runner.printErrorMessages(gpa, &compile.step, ttyconf, stderr, false) catch {}; + } + + if (result) |rebuilt_bin_path| { run.rebuilt_executable = rebuilt_bin_path; } else |err| switch (err) { - error.MakeFailed => { - const b = run.step.owner; - const stderr = std.io.getStdErr(); - std.debug.lockStdErr(); - defer std.debug.unlockStdErr(); - build_runner.printErrorMessages(b, &compile_step.step, ttyconf, stderr, false) catch {}; - }, + error.MakeFailed => {}, else => { std.debug.print("step '{s}': failed to rebuild in fuzz mode: {s}\n", .{ - compile_step.step.name, @errorName(err), + compile.step.name, @errorName(err), }); }, } @@ -82,6 +92,7 @@ fn fuzzWorkerRun( ttyconf: std.io.tty.Config, parent_prog_node: std.Progress.Node, ) void { + const gpa = run.step.owner.allocator; const test_name = run.cached_test_metadata.?.testName(unit_test_index); const prog_node = parent_prog_node.start(test_name, 0); @@ -89,11 +100,10 @@ fn fuzzWorkerRun( run.rerunInFuzzMode(unit_test_index, prog_node) catch |err| switch (err) { error.MakeFailed => { - const b = run.step.owner; const stderr = std.io.getStdErr(); std.debug.lockStdErr(); defer std.debug.unlockStdErr(); - build_runner.printErrorMessages(b, &run.step, ttyconf, stderr, false) catch {}; + build_runner.printErrorMessages(gpa, &run.step, ttyconf, stderr, false) catch {}; }, else => { std.debug.print("step '{s}': failed to rebuild '{s}' in fuzz mode: {s}\n", .{ diff --git a/lib/std/Build/Step/Compile.zig b/lib/std/Build/Step/Compile.zig index ffb2337ac5..8a418760c1 100644 --- a/lib/std/Build/Step/Compile.zig +++ b/lib/std/Build/Step/Compile.zig @@ -1483,6 +1483,8 @@ fn getZigArgs(compile: *Compile, fuzz: bool) ![][]const u8 { try zig_args.append("--global-cache-dir"); try zig_args.append(b.graph.global_cache_root.path orelse "."); + if (b.graph.debug_compiler_runtime_libs) try zig_args.append("--debug-rt"); + try zig_args.append("--name"); try zig_args.append(compile.name); @@ -1840,6 +1842,14 @@ fn make(step: *Step, options: Step.MakeOptions) !void { } pub fn rebuildInFuzzMode(c: *Compile, progress_node: std.Progress.Node) ![]const u8 { + const gpa = c.step.owner.allocator; + + c.step.result_error_msgs.clearRetainingCapacity(); + c.step.result_stderr = ""; + + c.step.result_error_bundle.deinit(gpa); + c.step.result_error_bundle = std.zig.ErrorBundle.empty; + const zig_args = try getZigArgs(c, true); const maybe_output_bin_path = try c.step.evalZigProcess(zig_args, progress_node, false); return maybe_output_bin_path.?; diff --git a/lib/std/Build/Step/Run.zig b/lib/std/Build/Step/Run.zig index 43e3ddedfd..c2d25cd82c 100644 --- a/lib/std/Build/Step/Run.zig +++ b/lib/std/Build/Step/Run.zig @@ -865,7 +865,10 @@ pub fn rerunInFuzzMode(run: *Run, unit_test_index: u32, prog_node: std.Progress. }, .artifact => |pa| { const artifact = pa.artifact; - const file_path = artifact.installed_path orelse artifact.generated_bin.?.path.?; + const file_path = if (artifact == run.producer.?) + run.rebuilt_executable.? + else + (artifact.installed_path orelse artifact.generated_bin.?.path.?); try argv_list.append(arena, b.fmt("{s}{s}", .{ pa.prefix, file_path })); }, .output_file, .output_directory => unreachable, diff --git a/src/Compilation.zig b/src/Compilation.zig index bc5a2bb456..8808e72e04 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -2180,7 +2180,9 @@ pub fn update(comp: *Compilation, main_progress_node: std.Progress.Node) !void { comp.bin_file = try link.File.createEmpty(arena, comp, emit, whole.lf_open_opts); } }, - .incremental => {}, + .incremental => { + log.debug("Compilation.update for {s}, CacheMode.incremental", .{comp.root_name}); + }, } // From this point we add a preliminary set of file system inputs that diff --git a/src/link/Elf.zig b/src/link/Elf.zig index ecb38974ca..7c1d695bd9 100644 --- a/src/link/Elf.zig +++ b/src/link/Elf.zig @@ -2286,6 +2286,8 @@ fn linkWithLLD(self: *Elf, arena: Allocator, tid: Zcu.PerThread.Id, prog_node: s } try man.addOptionalFile(module_obj_path); try man.addOptionalFile(compiler_rt_path); + try man.addOptionalFile(if (comp.tsan_lib) |l| l.full_object_path else null); + try man.addOptionalFile(if (comp.fuzzer_lib) |l| l.full_object_path else null); // We can skip hashing libc and libc++ components that we are in charge of building from Zig // installation sources because they are always a product of the compiler version + target information. diff --git a/src/main.zig b/src/main.zig index 9940312bcd..ddd2e79f44 100644 --- a/src/main.zig +++ b/src/main.zig @@ -655,6 +655,7 @@ const usage_build_generic = \\ --debug-log [scope] Enable printing debug/info log messages for scope \\ --debug-compile-errors Crash with helpful diagnostics at the first compile error \\ --debug-link-snapshot Enable dumping of the linker's state in JSON format + \\ --debug-rt Debug compiler runtime libraries \\ ; @@ -912,6 +913,7 @@ fn buildOutputType( var minor_subsystem_version: ?u16 = null; var mingw_unicode_entry_point: bool = false; var enable_link_snapshots: bool = false; + var debug_compiler_runtime_libs = false; var opt_incremental: ?bool = null; var install_name: ?[]const u8 = null; var hash_style: link.File.Elf.HashStyle = .both; @@ -1367,6 +1369,8 @@ fn buildOutputType( } else { enable_link_snapshots = true; } + } else if (mem.eql(u8, arg, "--debug-rt")) { + debug_compiler_runtime_libs = true; } else if (mem.eql(u8, arg, "-fincremental")) { dev.check(.incremental); opt_incremental = true; @@ -3408,6 +3412,7 @@ fn buildOutputType( // noise when --search-prefix and --mod are combined. .global_cc_argv = try cc_argv.toOwnedSlice(arena), .file_system_inputs = &file_system_inputs, + .debug_compiler_runtime_libs = debug_compiler_runtime_libs, }) catch |err| switch (err) { error.LibCUnavailable => { const triple_name = try target.zigTriple(arena); -- cgit v1.2.3