//! Build-time generator for the web LOOK filesystem. //! //! Prefer Git's tracked-plus-untracked/nonignored view so a new source works in //! the browser before it is staged, while ignored build/cache/vendor files can //! never leak into the 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 gitWorkingTreeZigFiles(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); // Cached entries can still name a file 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 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 working-tree source list. /// `--cached --others --exclude-standard` includes a newly-created module /// before it is staged but retains Git's ignore rules. Contents are read later; /// Git supplies only the path filter, never historical blobs. fn gitWorkingTreeZigFiles( 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", "--cached", "--others", "--exclude-standard", "--", "*.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; }