const std = @import("std"); const fs = @import("fs.zig"); const Context = @import("main.zig").Context; const log = std.log.scoped(.exe); pub fn WaitPool( comptime T: type, comptime worker: anytype, ) type { return struct { const Self = @This(); pub fn work(ctx: *Context, items: []const T, args: anytype) !void { var wg: std.Thread.WaitGroup = undefined; wg.reset(); var pool: std.Thread.Pool = undefined; try pool.init(.{ .n_jobs = (try std.Thread.getCpuCount()) - 1, .allocator = ctx.allocator(), }); defer pool.deinit(); for (items) |*item| { // the inline struct trick is a workardoun for Thread.Pool::spawn not // accepting functions that can error: // https://github.com/ziglang/zig/issues/18810 try pool.spawn(struct { fn func( wwg: *std.Thread.WaitGroup, wctx: *Context, witem: *const T, wargs: anytype, ) void { wwg.start(); defer wwg.finish(); const result = @call( .auto, worker, .{ wctx, witem.* } ++ wargs, ); result catch |err| std.log.err("worker failed with: {}", .{err}); } }.func, .{ &wg, ctx, item, args }); } pool.waitAndWork(&wg); } }; } pub fn runPandoc(ctx: *Context) !void { ctx.generated.clearRetainingCapacity(); ctx.progress_start("Processing markdown files", true); defer ctx.progress_end(); var md = try std.ArrayList([]const u8).initCapacity(ctx.allocator(), 1); defer md.deinit(ctx.allocator()); // TODO: support more than one site for (ctx.sites.getLast().pages.items) |p| if (p.kind == .Markdown) try md.append(ctx.allocator(), p.path); // TODO: check if pandoc is installed and exit gracefully try WaitPool( []const u8, pandocWorker, ).work(ctx, md.items, .{}); log.info("Processed {d} markdown files.", .{md.items.len}); } fn pandocWorker(ctx: *Context, page: []const u8) !void { ctx.progress_start(page, true); defer ctx.progress_end(); const source_path = try std.fs.path.join(ctx.allocator(), &.{ Context.params.PAGE_PATH, page }); defer ctx.allocator().free(source_path); const target_path = try fs.replaceExtension(ctx.allocator(), source_path, "htm"); defer ctx.allocator().free(target_path); // get mtime of the source document const mtime = try fs.getLastEdit(source_path); const mtime_str = try fs.formatTimespec(ctx.allocator(), mtime, "%s"); defer ctx.allocator().free(mtime_str); const f = try fs.FileSwapper.createAt(ctx.allocator(), target_path); var buffer: [1024]u8 = undefined; var w = f.writer(&buffer); // open new scope to close the file swapper when we're done writing try w.interface.print( \\ \\ , .{page}, ); // make pandoc runs reproducable var env_map = std.process.EnvMap.init(ctx.allocator()); defer env_map.deinit(); try env_map.put("SOURCE_DATE_EPOCH", mtime_str); try captureStdout(ctx, &.{ Context.params.PANDOC, "--from", "markdown+wikilinks_title_after_pipe+autolink_bare_uris", "--to", "html5", "--lua-filter", // TODO: embed the preproc filter Context.params.PANDOC_PREPROC, "--strip-comments", "--output=-", source_path, }, &env_map, &w.interface); f.close(); if (f.swapped.*) { try ctx.generated.append( ctx.allocator(), try ctx.allocator().dupe(u8, target_path), ); } } pub fn commit(ctx: *Context, paths: []const []const u8) !void { // skip early if we have nothing to do if (paths.len == 0) return; // skip if staging is unclean shell(ctx, \\git diff --cached --exit-code , null) catch |e| switch (e) { error.NonZeroExitCode => return, else => return e, }; for (paths) |f| { const cmd = try std.fmt.allocPrint(ctx.allocator(), "git add {s}", .{f}); defer ctx.allocator().free(cmd); try shell(ctx, cmd, null); } try shell(ctx, \\GIT_COMMITTER_NAME=rbc git commit --quiet --message=* , null); try shell(ctx, \\git show --stat --pretty=oneline , null); log.info("Committed {d} paths", .{paths.len}); } pub fn captureStdout(ctx: *Context, argv: []const []const u8, env_map: ?*std.process.EnvMap, w: *std.Io.Writer) !void { const command = try std.mem.join(ctx.allocator(), " ", argv); defer ctx.allocator().free(command); const label = try std.fmt.allocPrint(ctx.allocator(), "running {s}", .{command}); defer ctx.allocator().free(label); ctx.progress_start(label, true); defer ctx.progress_end(); log.debug("running {s}", .{command}); var p = std.process.Child.init(argv, std.heap.page_allocator); p.stdin_behavior = .Ignore; p.stdout_behavior = .Pipe; p.stderr_behavior = .Ignore; p.progress_node = ctx.progress_nodes.getLast(); p.env_map = env_map; try p.spawn(); var buffer: [1024]u8 = undefined; var r = p.stdout.?.readerStreaming(&buffer); _ = try w.sendFileAll(&r, .unlimited); try w.flush(); const result = try p.wait(); if (result.Exited != 0) return error.NonZeroExitCode; } pub fn shell(ctx: *Context, command: []const u8, env_map: ?*std.process.EnvMap) !void { const label = try std.fmt.allocPrint(ctx.allocator(), "running {s}", .{command}); defer ctx.allocator().free(label); ctx.progress_start(label, true); defer ctx.progress_end(); var p = std.process.Child.init(&.{ "sh", "-c", command, }, std.heap.page_allocator); p.stdout_behavior = .Inherit; p.stderr_behavior = .Inherit; p.progress_node = ctx.progress_nodes.getLast(); p.env_map = env_map; try p.spawn(); const result = try p.wait(); if (result.Exited != 0) return error.NonZeroExitCode; }