| 1 | const builtin = @import("builtin"); |
| 2 | |
| 3 | const std = @import("std"); |
| 4 | const Io = std.Io; |
| 5 | const mem = std.mem; |
| 6 | const Allocator = std.mem.Allocator; |
| 7 | const assert = std.debug.assert; |
| 8 | const Cache = std.Build.Cache; |
| 9 | |
| 10 | fn 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 | |
| 24 | pub 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 | |
| 91 | fn 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 | |
| 125 | const 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 | |
| 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, .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 | |
| 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 | 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 | |
| 188 | fn 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 | |
| 259 | fn 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 | |
| 298 | const autodoc_root_name = "autodoc"; |
| 299 | const autodoc_arch_os_abi = "wasm32-freestanding"; |
| 300 | const autodoc_cpu_features = "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext"; |
| 301 | |
| 302 | fn 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 | |
| 448 | fn 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 | |
| 455 | fn 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 | } |