From c7c7ad1b78fc8f79b5a0ddebdd374630f272be9a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Wed, 6 Mar 2024 17:51:28 -0700 Subject: [PATCH] zig std: implement serving the wasm binary --- lib/compiler/std-docs.zig | 240 ++++++++++++++++++++++++++++++++------ 1 file changed, 202 insertions(+), 38 deletions(-) diff --git a/lib/compiler/std-docs.zig b/lib/compiler/std-docs.zig index e9961d0781..e398d4c2ad 100644 --- a/lib/compiler/std-docs.zig +++ b/lib/compiler/std-docs.zig @@ -15,9 +15,8 @@ pub fn main() !void { const zig_exe_path = args[2]; const global_cache_path = args[3]; - const docs_path = try std.fs.path.join(arena, &.{ zig_lib_directory, "docs" }); - var docs_dir = try std.fs.cwd().openDir(docs_path, .{}); - defer docs_dir.close(); + var lib_dir = try std.fs.cwd().openDir(zig_lib_directory, .{}); + defer lib_dir.close(); const listen_port: u16 = 0; const address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable; @@ -29,6 +28,14 @@ pub fn main() !void { std.log.err("unable to open browser: {s}", .{@errorName(err)}); }; + var context: Context = .{ + .gpa = gpa, + .zig_exe_path = zig_exe_path, + .global_cache_path = global_cache_path, + .lib_dir = lib_dir, + .zig_lib_directory = zig_lib_directory, + }; + var read_buffer: [8000]u8 = undefined; accept: while (true) { const connection = try http_server.accept(); @@ -43,7 +50,7 @@ pub fn main() !void { continue :accept; }, }; - serveRequest(&request, gpa, docs_dir, zig_exe_path, global_cache_path) catch |err| { + serveRequest(&request, &context) catch |err| { std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) }); continue :accept; }; @@ -51,25 +58,31 @@ pub fn main() !void { } } -fn serveRequest( - request: *std.http.Server.Request, +const Context = struct { gpa: Allocator, - docs_dir: std.fs.Dir, + lib_dir: std.fs.Dir, + zig_lib_directory: []const u8, zig_exe_path: []const u8, global_cache_path: []const u8, -) !void { +}; + +fn serveRequest(request: *std.http.Server.Request, context: *Context) !void { if (std.mem.eql(u8, request.head.target, "/") or std.mem.eql(u8, request.head.target, "/debug/")) { - try serveDocsFile(request, gpa, docs_dir, "index.html", "text/html"); + try serveDocsFile(request, context, "docs/index.html", "text/html"); } else if (std.mem.eql(u8, request.head.target, "/main.js") or std.mem.eql(u8, request.head.target, "/debug/main.js")) { - try serveDocsFile(request, gpa, docs_dir, "main.js", "application/javascript"); + try serveDocsFile(request, context, "docs/main.js", "application/javascript"); } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) { - try serveWasm(request, gpa, zig_exe_path, global_cache_path, .ReleaseFast); + try serveWasm(request, context, .ReleaseFast); } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) { - try serveWasm(request, gpa, zig_exe_path, global_cache_path, .Debug); + try serveWasm(request, context, .Debug); + } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or + std.mem.eql(u8, request.head.target, "/debug/sources.tar")) + { + try serveSourcesTar(request, context); } else { try request.respond("not found", .{ .status = .not_found, @@ -80,68 +93,219 @@ fn serveRequest( } } +const cache_control_header: std.http.Header = .{ + .name = "cache-control", + .value = "max-age=0, must-revalidate", +}; + fn serveDocsFile( request: *std.http.Server.Request, - gpa: Allocator, - docs_dir: std.fs.Dir, + context: *Context, name: []const u8, content_type: []const u8, ) !void { + const gpa = context.gpa; // The desired API is actually sendfile, which will require enhancing std.http.Server. // We load the file with every request so that the user can make changes to the file // and refresh the HTML page without restarting this server. - const file_contents = try docs_dir.readFileAlloc(gpa, name, 10 * 1024 * 1024); + const file_contents = try context.lib_dir.readFileAlloc(gpa, name, 10 * 1024 * 1024); defer gpa.free(file_contents); try request.respond(file_contents, .{ .status = .ok, .extra_headers = &.{ .{ .name = "content-type", .value = content_type }, + cache_control_header, }, }); } -fn serveWasm( - request: *std.http.Server.Request, - gpa: Allocator, - zig_exe_path: []const u8, - global_cache_path: []const u8, - optimize_mode: std.builtin.OptimizeMode, -) !void { +fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void { _ = request; - _ = gpa; - _ = zig_exe_path; - _ = global_cache_path; - _ = optimize_mode; - @panic("TODO serve wasm"); + _ = context; + @panic("TODO"); } -const BuildWasmBinaryOptions = struct { - zig_exe_path: []const u8, - global_cache_path: []const u8, - main_src_path: []const u8, -}; +fn serveWasm( + request: *std.http.Server.Request, + context: *Context, + optimize_mode: std.builtin.OptimizeMode, +) !void { + const gpa = context.gpa; + + var arena_instance = std.heap.ArenaAllocator.init(gpa); + defer arena_instance.deinit(); + const arena = arena_instance.allocator(); + + // Do the compilation every request, so that the user can edit the files + // and see the changes without restarting the server. + const wasm_binary_path = try buildWasmBinary(arena, context, optimize_mode); + // std.http.Server does not have a sendfile API yet. + const file_contents = try std.fs.cwd().readFileAlloc(gpa, wasm_binary_path, 10 * 1024 * 1024); + defer gpa.free(file_contents); + try request.respond(file_contents, .{ + .status = .ok, + .extra_headers = &.{ + .{ .name = "content-type", .value = "application/wasm" }, + cache_control_header, + }, + }); +} + +fn buildWasmBinary( + arena: Allocator, + context: *Context, + optimize_mode: std.builtin.OptimizeMode, +) ![]const u8 { + const gpa = context.gpa; + + const main_src_path = try std.fs.path.join(arena, &.{ + context.zig_lib_directory, "docs", "wasm", "main.zig", + }); -fn buildWasmBinary(arena: Allocator, options: BuildWasmBinaryOptions) ![]const u8 { var argv: std.ArrayListUnmanaged([]const u8) = .{}; + try argv.appendSlice(arena, &.{ - options.zig_exe_path, + context.zig_exe_path, "build-exe", "-fno-entry", - "-OReleaseSmall", + "-O", + @tagName(optimize_mode), "-target", "wasm32-freestanding", "-mcpu", "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", "--cache-dir", - options.global_cache_path, + context.global_cache_path, "--global-cache-dir", - options.global_cache_path, + context.global_cache_path, "--name", "autodoc", "-rdynamic", - options.main_src_path, + main_src_path, "--listen=-", }); + + var child = std.ChildProcess.init(argv.items, gpa); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; + try child.spawn(); + + var poller = std.io.poll(gpa, enum { stdout, stderr }, .{ + .stdout = child.stdout.?, + .stderr = child.stderr.?, + }); + defer poller.deinit(); + + try sendMessage(child.stdin.?, .update); + try sendMessage(child.stdin.?, .exit); + + const Header = std.zig.Server.Message.Header; + var result: ?[]const u8 = null; + var result_error_bundle = std.zig.ErrorBundle.empty; + + const stdout = poller.fifo(.stdout); + + poll: while (true) { + while (stdout.readableLength() < @sizeOf(Header)) { + if (!(try poller.poll())) break :poll; + } + const header = stdout.reader().readStruct(Header) catch unreachable; + while (stdout.readableLength() < header.bytes_len) { + if (!(try poller.poll())) break :poll; + } + const body = stdout.readableSliceOfLen(header.bytes_len); + + switch (header.tag) { + .zig_version => { + if (!std.mem.eql(u8, builtin.zig_version_string, body)) { + return error.ZigProtocolVersionMismatch; + } + }, + .error_bundle => { + const EbHdr = std.zig.Server.Message.ErrorBundle; + const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body)); + const extra_bytes = + body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len]; + const string_bytes = + body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len]; + // TODO: use @ptrCast when the compiler supports it + const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes); + const extra_array = try arena.alloc(u32, unaligned_extra.len); + @memcpy(extra_array, unaligned_extra); + result_error_bundle = .{ + .string_bytes = try arena.dupe(u8, string_bytes), + .extra = extra_array, + }; + }, + .emit_bin_path => { + const EbpHdr = std.zig.Server.Message.EmitBinPath; + const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body)); + if (!ebp_hdr.flags.cache_hit) { + std.log.info("source changes detected; rebuilding wasm component", .{}); + } + result = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]); + }, + else => {}, // ignore other messages + } + + stdout.discard(body.len); + } + + const stderr = poller.fifo(.stderr); + if (stderr.readableLength() > 0) { + const owned_stderr = try stderr.toOwnedSlice(); + defer gpa.free(owned_stderr); + std.debug.print("{s}", .{owned_stderr}); + } + + // Send EOF to stdin. + child.stdin.?.close(); + child.stdin = null; + + switch (try child.wait()) { + .Exited => |code| { + if (code != 0) { + std.log.err( + "the following command exited with error code {d}:\n{s}", + .{ code, try std.Build.Step.allocPrintCmd(arena, null, argv.items) }, + ); + return error.AlreadyReported; + } + }, + .Signal, .Stopped, .Unknown => { + std.log.err( + "the following command terminated unexpectedly:\n{s}", + .{try std.Build.Step.allocPrintCmd(arena, null, argv.items)}, + ); + return error.AlreadyReported; + }, + } + + if (result_error_bundle.errorMessageCount() > 0) { + const color = std.zig.Color.auto; + result_error_bundle.renderToStdErr(color.renderOptions()); + std.log.err("the following command failed with {d} compilation errors:\n{s}", .{ + result_error_bundle.errorMessageCount(), + try std.Build.Step.allocPrintCmd(arena, null, argv.items), + }); + return error.AlreadyReported; + } + + return result orelse { + std.log.err("child process failed to report result\n{s}", .{ + try std.Build.Step.allocPrintCmd(arena, null, argv.items), + }); + return error.AlreadyReported; + }; +} + +fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void { + const header: std.zig.Client.Message.Header = .{ + .tag = tag, + .bytes_len = 0, + }; + try file.writeAll(std.mem.asBytes(&header)); } fn openBrowserTab(gpa: Allocator, url: []const u8) !void {