1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
|
//! 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;
}
|