//! Build-time generator for the web LOOK filesystem. //! //! Prefer `git ls-files` so ignored build/cache/vendor files never leak into //! the browser bundle. A source archive should still be buildable without Git, //! so a conservative directory walk is the fallback. const std = @import("std"); pub fn main(init: std.process.Init) !void { const gpa = init.gpa; const io = init.io; const args = try init.minimal.args.toSlice(init.arena.allocator()); if (args.len != 3) return error.BadArgs; const root = args[1]; const output_path = args[2]; var paths: std.ArrayList([]const u8) = .empty; defer { for (paths.items) |path| gpa.free(path); paths.deinit(gpa); } if (!try gitTrackedZigFiles(gpa, io, root, &paths)) try walkedZigFiles(gpa, io, root, &paths); std.mem.sort([]const u8, paths.items, {}, lessThanPath); var generated: std.Io.Writer.Allocating = .init(gpa); defer generated.deinit(); try generated.writer.writeAll( \\// Generated by tools/embed_zig_sources.zig. Do not edit. \\pub const Source = struct { path: []const u8, contents: []const u8 }; \\pub const all = [_]Source{ \\ ); for (paths.items) |path| { const full_path = try std.Io.Dir.path.join(gpa, &.{ root, path }); defer gpa.free(full_path); const contents = try std.Io.Dir.cwd().readFileAlloc(io, full_path, gpa, .limited(64 * 1024 * 1024)); defer gpa.free(contents); try generated.writer.print(" .{{ .path = \"{f}\", .contents = \"{f}\" }},\n", .{ std.zig.fmtString(path), std.zig.fmtString(contents), }); } try generated.writer.writeAll("};\n"); const output = if (std.Io.Dir.path.isAbsolute(output_path)) try std.Io.Dir.createFileAbsolute(io, output_path, .{}) else try std.Io.Dir.cwd().createFile(io, output_path, .{}); defer output.close(io); try output.writeStreamingAll(io, generated.written()); } /// Returns true only when Git ran successfully. The working-tree contents are /// read later; Git supplies the tracked path filter, not historical blobs. fn gitTrackedZigFiles( gpa: std.mem.Allocator, io: std.Io, root: []const u8, paths: *std.ArrayList([]const u8), ) !bool { const result = std.process.run(gpa, io, .{ .argv = &.{ "git", "-C", root, "ls-files", "-z", "--", "*.zig" }, .stdout_limit = .limited(16 * 1024 * 1024), .stderr_limit = .limited(1024 * 1024), }) catch return false; defer gpa.free(result.stdout); defer gpa.free(result.stderr); switch (result.term) { .exited => |code| if (code != 0) return false, else => return false, } var it = std.mem.splitScalar(u8, result.stdout, 0); while (it.next()) |raw_path| { if (raw_path.len == 0 or !std.mem.endsWith(u8, raw_path, ".zig")) continue; try paths.append(gpa, try normalizedPath(gpa, raw_path)); } return true; } fn walkedZigFiles( gpa: std.mem.Allocator, io: std.Io, root: []const u8, paths: *std.ArrayList([]const u8), ) !void { var dir = if (std.Io.Dir.path.isAbsolute(root)) try std.Io.Dir.openDirAbsolute(io, root, .{ .iterate = true }) else try std.Io.Dir.cwd().openDir(io, root, .{ .iterate = true }); defer dir.close(io); var walker = try dir.walk(gpa); defer walker.deinit(); while (try walker.next(io)) |entry| { if (entry.kind == .directory and ignoredFallbackDir(entry.path)) { walker.leave(io); continue; } if ((entry.kind != .file and entry.kind != .sym_link) or !std.mem.endsWith(u8, entry.path, ".zig")) continue; try paths.append(gpa, try normalizedPath(gpa, entry.path)); } } fn ignoredFallbackDir(path: []const u8) bool { var parts = std.mem.tokenizeAny(u8, path, "/\\"); while (parts.next()) |part| { if (std.mem.eql(u8, part, ".git") or std.mem.eql(u8, part, ".zig-cache") or std.mem.eql(u8, part, "zig-cache") or std.mem.eql(u8, part, "zig-out") or std.mem.eql(u8, part, "zig-pkg")) return true; } return false; } fn normalizedPath(gpa: std.mem.Allocator, path: []const u8) ![]u8 { const copy = try gpa.dupe(u8, path); if (std.Io.Dir.path.sep != '/') std.mem.replaceScalar(u8, copy, std.Io.Dir.path.sep, '/'); return copy; } fn lessThanPath(_: void, a: []const u8, b: []const u8) bool { return std.mem.order(u8, a, b) == .lt; }