//! 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]; const paths = (try gitTrackedZigFiles(gpa, io, root)) orelse try walkedZigFiles(gpa, io, root); defer { for (paths) |path| gpa.free(path); gpa.free(paths); } std.mem.sort([]const u8, paths, {}, 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) |path| { const full_path = try std.Io.Dir.path.join(gpa, &.{ root, path }); defer gpa.free(full_path); // `git ls-files` reports the INDEX, not the working tree, and the two // disagree the moment a file is moved or deleted without staging it — // an ordinary mid-edit state, and in a jj-colocated repo the normal one // until the change is exported. A path that is no longer on disk is // simply not a source to embed; failing the whole web build over it // means an unrelated refactor breaks a shell it never touched. const contents = std.Io.Dir.cwd().readFileAlloc(io, full_path, gpa, .limited(64 * 1024 * 1024)) catch |err| switch (err) { error.FileNotFound => continue, else => return err, }; 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 null only when Git cannot supply the tracked-file list. Working-tree /// contents are read later; Git supplies the path filter, not historical blobs. fn gitTrackedZigFiles( gpa: std.mem.Allocator, io: std.Io, root: []const u8, ) !?[][]const u8 { 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 null; defer gpa.free(result.stdout); defer gpa.free(result.stderr); switch (result.term) { .exited => |code| if (code != 0) return null, else => return null, } var count: usize = 0; var count_it = std.mem.splitScalar(u8, result.stdout, 0); while (count_it.next()) |raw_path| { if (raw_path.len == 0 or !std.mem.endsWith(u8, raw_path, ".zig")) continue; count += 1; } const paths = try gpa.alloc([]const u8, count); errdefer gpa.free(paths); var index: usize = 0; errdefer for (paths[0..index]) |path| gpa.free(path); 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; paths[index] = try normalizedPath(gpa, raw_path); index += 1; } std.debug.assert(index == paths.len); return paths; } fn walkedZigFiles( gpa: std.mem.Allocator, io: std.Io, root: []const u8, ) ![][]const u8 { var count: usize = 0; { 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; count += 1; } } const paths = try gpa.alloc([]const u8, count); errdefer gpa.free(paths); var index: usize = 0; errdefer for (paths[0..index]) |path| gpa.free(path); 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; paths[index] = try normalizedPath(gpa, entry.path); index += 1; } std.debug.assert(index == paths.len); return paths; } 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; }