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