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}