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