master
  1const builtin = @import("builtin");
  2const std = @import("std");
  3const mem = std.mem;
  4const Allocator = std.mem.Allocator;
  5const assert = std.debug.assert;
  6const Cache = std.Build.Cache;
  7
  8fn usage() noreturn {
  9    std.fs.File.stdout().writeAll(
 10        \\Usage: zig std [options]
 11        \\
 12        \\Options:
 13        \\  -h, --help                Print this help and exit
 14        \\  -p [port], --port [port]  Port to listen on. Default is 0, meaning an ephemeral port chosen by the system.
 15        \\  --[no-]open-browser       Force enabling or disabling opening a browser tab to the served website.
 16        \\                            By default, enabled unless a port is specified.
 17        \\
 18    ) catch {};
 19    std.process.exit(1);
 20}
 21
 22pub fn main() !void {
 23    var arena_instance = std.heap.ArenaAllocator.init(std.heap.page_allocator);
 24    defer arena_instance.deinit();
 25    const arena = arena_instance.allocator();
 26
 27    var general_purpose_allocator: std.heap.GeneralPurposeAllocator(.{}) = .init;
 28    const gpa = general_purpose_allocator.allocator();
 29
 30    var argv = try std.process.argsWithAllocator(arena);
 31    defer argv.deinit();
 32    assert(argv.skip());
 33    const zig_lib_directory = argv.next().?;
 34    const zig_exe_path = argv.next().?;
 35    const global_cache_path = argv.next().?;
 36
 37    var lib_dir = try std.fs.cwd().openDir(zig_lib_directory, .{});
 38    defer lib_dir.close();
 39
 40    var listen_port: u16 = 0;
 41    var force_open_browser: ?bool = null;
 42    while (argv.next()) |arg| {
 43        if (mem.eql(u8, arg, "-h") or mem.eql(u8, arg, "--help")) {
 44            usage();
 45        } else if (mem.eql(u8, arg, "-p") or mem.eql(u8, arg, "--port")) {
 46            listen_port = std.fmt.parseInt(u16, argv.next() orelse usage(), 10) catch |err| {
 47                std.log.err("expected port number: {}", .{err});
 48                usage();
 49            };
 50        } else if (mem.eql(u8, arg, "--open-browser")) {
 51            force_open_browser = true;
 52        } else if (mem.eql(u8, arg, "--no-open-browser")) {
 53            force_open_browser = false;
 54        } else {
 55            std.log.err("unrecognized argument: {s}", .{arg});
 56            usage();
 57        }
 58    }
 59    const should_open_browser = force_open_browser orelse (listen_port == 0);
 60
 61    const address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable;
 62    var http_server = try address.listen(.{
 63        .reuse_address = true,
 64    });
 65    const port = http_server.listen_address.in.getPort();
 66    const url_with_newline = try std.fmt.allocPrint(arena, "http://127.0.0.1:{d}/\n", .{port});
 67    std.fs.File.stdout().writeAll(url_with_newline) catch {};
 68    if (should_open_browser) {
 69        openBrowserTab(gpa, url_with_newline[0 .. url_with_newline.len - 1 :'\n']) catch |err| {
 70            std.log.err("unable to open browser: {s}", .{@errorName(err)});
 71        };
 72    }
 73
 74    var context: Context = .{
 75        .gpa = gpa,
 76        .zig_exe_path = zig_exe_path,
 77        .global_cache_path = global_cache_path,
 78        .lib_dir = lib_dir,
 79        .zig_lib_directory = zig_lib_directory,
 80    };
 81
 82    while (true) {
 83        const connection = try http_server.accept();
 84        _ = std.Thread.spawn(.{}, accept, .{ &context, connection }) catch |err| {
 85            std.log.err("unable to accept connection: {s}", .{@errorName(err)});
 86            connection.stream.close();
 87            continue;
 88        };
 89    }
 90}
 91
 92fn accept(context: *Context, connection: std.net.Server.Connection) void {
 93    defer connection.stream.close();
 94
 95    var recv_buffer: [4000]u8 = undefined;
 96    var send_buffer: [4000]u8 = undefined;
 97    var conn_reader = connection.stream.reader(&recv_buffer);
 98    var conn_writer = connection.stream.writer(&send_buffer);
 99    var server = std.http.Server.init(conn_reader.interface(), &conn_writer.interface);
100    while (server.reader.state == .ready) {
101        var request = server.receiveHead() catch |err| switch (err) {
102            error.HttpConnectionClosing => return,
103            else => {
104                std.log.err("closing http connection: {s}", .{@errorName(err)});
105                return;
106            },
107        };
108        serveRequest(&request, context) catch |err| switch (err) {
109            error.WriteFailed => {
110                if (conn_writer.err) |e| {
111                    std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(e) });
112                } else {
113                    std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) });
114                }
115                return;
116            },
117            else => {
118                std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) });
119                return;
120            },
121        };
122    }
123}
124
125const Context = struct {
126    gpa: Allocator,
127    lib_dir: std.fs.Dir,
128    zig_lib_directory: []const u8,
129    zig_exe_path: []const u8,
130    global_cache_path: []const u8,
131};
132
133fn serveRequest(request: *std.http.Server.Request, context: *Context) !void {
134    if (std.mem.eql(u8, request.head.target, "/") or
135        std.mem.eql(u8, request.head.target, "/debug") or
136        std.mem.eql(u8, request.head.target, "/debug/"))
137    {
138        try serveDocsFile(request, context, "docs/index.html", "text/html");
139    } else if (std.mem.eql(u8, request.head.target, "/main.js") or
140        std.mem.eql(u8, request.head.target, "/debug/main.js"))
141    {
142        try serveDocsFile(request, context, "docs/main.js", "application/javascript");
143    } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) {
144        try serveWasm(request, context, .ReleaseFast);
145    } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) {
146        try serveWasm(request, context, .Debug);
147    } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or
148        std.mem.eql(u8, request.head.target, "/debug/sources.tar"))
149    {
150        try serveSourcesTar(request, context);
151    } else {
152        try request.respond("not found", .{
153            .status = .not_found,
154            .extra_headers = &.{
155                .{ .name = "content-type", .value = "text/plain" },
156            },
157        });
158    }
159}
160
161const cache_control_header: std.http.Header = .{
162    .name = "cache-control",
163    .value = "max-age=0, must-revalidate",
164};
165
166fn serveDocsFile(
167    request: *std.http.Server.Request,
168    context: *Context,
169    name: []const u8,
170    content_type: []const u8,
171) !void {
172    const gpa = context.gpa;
173    // The desired API is actually sendfile, which will require enhancing std.http.Server.
174    // We load the file with every request so that the user can make changes to the file
175    // and refresh the HTML page without restarting this server.
176    const file_contents = try context.lib_dir.readFileAlloc(name, gpa, .limited(10 * 1024 * 1024));
177    defer gpa.free(file_contents);
178    try request.respond(file_contents, .{
179        .extra_headers = &.{
180            .{ .name = "content-type", .value = content_type },
181            cache_control_header,
182        },
183    });
184}
185
186fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void {
187    const gpa = context.gpa;
188
189    var send_buffer: [0x4000]u8 = undefined;
190    var response = try request.respondStreaming(&send_buffer, .{
191        .respond_options = .{
192            .extra_headers = &.{
193                .{ .name = "content-type", .value = "application/x-tar" },
194                cache_control_header,
195            },
196        },
197    });
198
199    var std_dir = try context.lib_dir.openDir("std", .{ .iterate = true });
200    defer std_dir.close();
201
202    var walker = try std_dir.walk(gpa);
203    defer walker.deinit();
204
205    var archiver: std.tar.Writer = .{ .underlying_writer = &response.writer };
206    archiver.prefix = "std";
207
208    while (try walker.next()) |entry| {
209        switch (entry.kind) {
210            .file => {
211                if (!std.mem.endsWith(u8, entry.basename, ".zig"))
212                    continue;
213                if (std.mem.endsWith(u8, entry.basename, "test.zig"))
214                    continue;
215            },
216            else => continue,
217        }
218        var file = try entry.dir.openFile(entry.basename, .{});
219        defer file.close();
220        const stat = try file.stat();
221        var file_reader: std.fs.File.Reader = .{
222            .file = file,
223            .interface = std.fs.File.Reader.initInterface(&.{}),
224            .size = stat.size,
225        };
226        try archiver.writeFile(entry.path, &file_reader, stat.mtime);
227    }
228
229    {
230        // Since this command is JIT compiled, the builtin module available in
231        // this source file corresponds to the user's host system.
232        const builtin_zig = @embedFile("builtin");
233        archiver.prefix = "builtin";
234        try archiver.writeFileBytes("builtin.zig", builtin_zig, .{});
235    }
236
237    // intentionally omitting the pointless trailer
238    //try archiver.finish();
239    try response.end();
240}
241
242fn serveWasm(
243    request: *std.http.Server.Request,
244    context: *Context,
245    optimize_mode: std.builtin.OptimizeMode,
246) !void {
247    const gpa = context.gpa;
248
249    var arena_instance = std.heap.ArenaAllocator.init(gpa);
250    defer arena_instance.deinit();
251    const arena = arena_instance.allocator();
252
253    // Do the compilation every request, so that the user can edit the files
254    // and see the changes without restarting the server.
255    const wasm_base_path = try buildWasmBinary(arena, context, optimize_mode);
256    const bin_name = try std.zig.binNameAlloc(arena, .{
257        .root_name = autodoc_root_name,
258        .target = &(std.zig.system.resolveTargetQuery(std.Build.parseTargetQuery(.{
259            .arch_os_abi = autodoc_arch_os_abi,
260            .cpu_features = autodoc_cpu_features,
261        }) catch unreachable) catch unreachable),
262        .output_mode = .Exe,
263    });
264    // std.http.Server does not have a sendfile API yet.
265    const bin_path = try wasm_base_path.join(arena, bin_name);
266    const file_contents = try bin_path.root_dir.handle.readFileAlloc(bin_path.sub_path, gpa, .limited(10 * 1024 * 1024));
267    defer gpa.free(file_contents);
268    try request.respond(file_contents, .{
269        .extra_headers = &.{
270            .{ .name = "content-type", .value = "application/wasm" },
271            cache_control_header,
272        },
273    });
274}
275
276const autodoc_root_name = "autodoc";
277const autodoc_arch_os_abi = "wasm32-freestanding";
278const autodoc_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext";
279
280fn buildWasmBinary(
281    arena: Allocator,
282    context: *Context,
283    optimize_mode: std.builtin.OptimizeMode,
284) !Cache.Path {
285    const gpa = context.gpa;
286
287    var argv: std.ArrayList([]const u8) = .empty;
288
289    try argv.appendSlice(arena, &.{
290        context.zig_exe_path, //
291        "build-exe", //
292        "-fno-entry", //
293        "-O", @tagName(optimize_mode), //
294        "-target", autodoc_arch_os_abi, //
295        "-mcpu", autodoc_cpu_features, //
296        "--cache-dir", context.global_cache_path, //
297        "--global-cache-dir", context.global_cache_path, //
298        "--name", autodoc_root_name, //
299        "-rdynamic", //
300        "--dep", "Walk", //
301        try std.fmt.allocPrint(
302            arena,
303            "-Mroot={s}/docs/wasm/main.zig",
304            .{context.zig_lib_directory},
305        ),
306        try std.fmt.allocPrint(
307            arena,
308            "-MWalk={s}/docs/wasm/Walk.zig",
309            .{context.zig_lib_directory},
310        ),
311        "--listen=-", //
312    });
313
314    var child = std.process.Child.init(argv.items, gpa);
315    child.stdin_behavior = .Pipe;
316    child.stdout_behavior = .Pipe;
317    child.stderr_behavior = .Pipe;
318    try child.spawn();
319
320    var poller = std.Io.poll(gpa, enum { stdout, stderr }, .{
321        .stdout = child.stdout.?,
322        .stderr = child.stderr.?,
323    });
324    defer poller.deinit();
325
326    try sendMessage(child.stdin.?, .update);
327    try sendMessage(child.stdin.?, .exit);
328
329    var result: ?Cache.Path = null;
330    var result_error_bundle = std.zig.ErrorBundle.empty;
331
332    const stdout = poller.reader(.stdout);
333
334    poll: while (true) {
335        const Header = std.zig.Server.Message.Header;
336        while (stdout.buffered().len < @sizeOf(Header)) if (!try poller.poll()) break :poll;
337        const header = stdout.takeStruct(Header, .little) catch unreachable;
338        while (stdout.buffered().len < header.bytes_len) if (!try poller.poll()) break :poll;
339        const body = stdout.take(header.bytes_len) catch unreachable;
340
341        switch (header.tag) {
342            .zig_version => {
343                if (!std.mem.eql(u8, builtin.zig_version_string, body)) {
344                    return error.ZigProtocolVersionMismatch;
345                }
346            },
347            .error_bundle => {
348                result_error_bundle = try std.zig.Server.allocErrorBundle(arena, body);
349            },
350            .emit_digest => {
351                var r: std.Io.Reader = .fixed(body);
352                const emit_digest = r.takeStruct(std.zig.Server.Message.EmitDigest, .little) catch unreachable;
353                if (!emit_digest.flags.cache_hit) {
354                    std.log.info("source changes detected; rebuilt wasm component", .{});
355                }
356                const digest = r.takeArray(Cache.bin_digest_len) catch unreachable;
357                result = .{
358                    .root_dir = Cache.Directory.cwd(),
359                    .sub_path = try std.fs.path.join(arena, &.{
360                        context.global_cache_path, "o" ++ std.fs.path.sep_str ++ Cache.binToHex(digest.*),
361                    }),
362                };
363            },
364            else => {}, // ignore other messages
365        }
366    }
367
368    const stderr = poller.reader(.stderr);
369    if (stderr.bufferedLen() > 0) {
370        std.debug.print("{s}", .{stderr.buffered()});
371    }
372
373    // Send EOF to stdin.
374    child.stdin.?.close();
375    child.stdin = null;
376
377    switch (try child.wait()) {
378        .Exited => |code| {
379            if (code != 0) {
380                std.log.err(
381                    "the following command exited with error code {d}:\n{s}",
382                    .{ code, try std.Build.Step.allocPrintCmd(arena, null, argv.items) },
383                );
384                return error.WasmCompilationFailed;
385            }
386        },
387        .Signal, .Stopped, .Unknown => {
388            std.log.err(
389                "the following command terminated unexpectedly:\n{s}",
390                .{try std.Build.Step.allocPrintCmd(arena, null, argv.items)},
391            );
392            return error.WasmCompilationFailed;
393        },
394    }
395
396    if (result_error_bundle.errorMessageCount() > 0) {
397        result_error_bundle.renderToStdErr(.{}, true);
398        std.log.err("the following command failed with {d} compilation errors:\n{s}", .{
399            result_error_bundle.errorMessageCount(),
400            try std.Build.Step.allocPrintCmd(arena, null, argv.items),
401        });
402        return error.WasmCompilationFailed;
403    }
404
405    return result orelse {
406        std.log.err("child process failed to report result\n{s}", .{
407            try std.Build.Step.allocPrintCmd(arena, null, argv.items),
408        });
409        return error.WasmCompilationFailed;
410    };
411}
412
413fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void {
414    const header: std.zig.Client.Message.Header = .{
415        .tag = tag,
416        .bytes_len = 0,
417    };
418    var w = file.writer(&.{});
419    w.interface.writeStruct(header, .little) catch |err| switch (err) {
420        error.WriteFailed => return w.err.?,
421    };
422}
423
424fn openBrowserTab(gpa: Allocator, url: []const u8) !void {
425    // Until https://github.com/ziglang/zig/issues/19205 is implemented, we
426    // spawn a thread for this child process.
427    _ = try std.Thread.spawn(.{}, openBrowserTabThread, .{ gpa, url });
428}
429
430fn openBrowserTabThread(gpa: Allocator, url: []const u8) !void {
431    const main_exe = switch (builtin.os.tag) {
432        .windows => "explorer",
433        .macos => "open",
434        else => "xdg-open",
435    };
436    var child = std.process.Child.init(&.{ main_exe, url }, gpa);
437    child.stdin_behavior = .Ignore;
438    child.stdout_behavior = .Ignore;
439    child.stderr_behavior = .Ignore;
440    try child.spawn();
441    _ = try child.wait();
442}