summaryrefslogtreecommitdiff
path: root/tools/embed_zig_sources.zig
blob: 91fd128f0ccf530d8b0d4356abfeb50fa445bf98 (plain) (blame)
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
173
174
175
176
177
//! 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;
}