| ... | ... | @@ -15,9 +15,8 @@ pub fn main() !void { |
| 15 | 15 | const zig_exe_path = args[2]; |
| 16 | 16 | const global_cache_path = args[3]; |
| 17 | 17 | |
| 18 | | const docs_path = try std.fs.path.join(arena, &.{ zig_lib_directory, "docs" }); |
| 19 | | var docs_dir = try std.fs.cwd().openDir(docs_path, .{}); |
| 20 | | defer docs_dir.close(); |
| 18 | var lib_dir = try std.fs.cwd().openDir(zig_lib_directory, .{}); |
| 19 | defer lib_dir.close(); |
| 21 | 20 | |
| 22 | 21 | const listen_port: u16 = 0; |
| 23 | 22 | const address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable; |
| ... | ... | @@ -29,6 +28,14 @@ pub fn main() !void { |
| 29 | 28 | std.log.err("unable to open browser: {s}", .{@errorName(err)}); |
| 30 | 29 | }; |
| 31 | 30 | |
| 31 | var context: Context = .{ |
| 32 | .gpa = gpa, |
| 33 | .zig_exe_path = zig_exe_path, |
| 34 | .global_cache_path = global_cache_path, |
| 35 | .lib_dir = lib_dir, |
| 36 | .zig_lib_directory = zig_lib_directory, |
| 37 | }; |
| 38 | |
| 32 | 39 | var read_buffer: [8000]u8 = undefined; |
| 33 | 40 | accept: while (true) { |
| 34 | 41 | const connection = try http_server.accept(); |
| ... | ... | @@ -43,7 +50,7 @@ pub fn main() !void { |
| 43 | 50 | continue :accept; |
| 44 | 51 | }, |
| 45 | 52 | }; |
| 46 | | serveRequest(&request, gpa, docs_dir, zig_exe_path, global_cache_path) catch |err| { |
| 53 | serveRequest(&request, &context) catch |err| { |
| 47 | 54 | std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) }); |
| 48 | 55 | continue :accept; |
| 49 | 56 | }; |
| ... | ... | @@ -51,25 +58,31 @@ pub fn main() !void { |
| 51 | 58 | } |
| 52 | 59 | } |
| 53 | 60 | |
| 54 | | fn serveRequest( |
| 55 | | request: *std.http.Server.Request, |
| 61 | const Context = struct { |
| 56 | 62 | gpa: Allocator, |
| 57 | | docs_dir: std.fs.Dir, |
| 63 | lib_dir: std.fs.Dir, |
| 64 | zig_lib_directory: []const u8, |
| 58 | 65 | zig_exe_path: []const u8, |
| 59 | 66 | global_cache_path: []const u8, |
| 60 | | ) !void { |
| 67 | }; |
| 68 | |
| 69 | fn serveRequest(request: *std.http.Server.Request, context: *Context) !void { |
| 61 | 70 | if (std.mem.eql(u8, request.head.target, "/") or |
| 62 | 71 | std.mem.eql(u8, request.head.target, "/debug/")) |
| 63 | 72 | { |
| 64 | | try serveDocsFile(request, gpa, docs_dir, "index.html", "text/html"); |
| 73 | try serveDocsFile(request, context, "docs/index.html", "text/html"); |
| 65 | 74 | } else if (std.mem.eql(u8, request.head.target, "/main.js") or |
| 66 | 75 | std.mem.eql(u8, request.head.target, "/debug/main.js")) |
| 67 | 76 | { |
| 68 | | try serveDocsFile(request, gpa, docs_dir, "main.js", "application/javascript"); |
| 77 | try serveDocsFile(request, context, "docs/main.js", "application/javascript"); |
| 69 | 78 | } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) { |
| 70 | | try serveWasm(request, gpa, zig_exe_path, global_cache_path, .ReleaseFast); |
| 79 | try serveWasm(request, context, .ReleaseFast); |
| 71 | 80 | } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) { |
| 72 | | try serveWasm(request, gpa, zig_exe_path, global_cache_path, .Debug); |
| 81 | try serveWasm(request, context, .Debug); |
| 82 | } else if (std.mem.eql(u8, request.head.target, "/sources.tar") or |
| 83 | std.mem.eql(u8, request.head.target, "/debug/sources.tar")) |
| 84 | { |
| 85 | try serveSourcesTar(request, context); |
| 73 | 86 | } else { |
| 74 | 87 | try request.respond("not found", .{ |
| 75 | 88 | .status = .not_found, |
| ... | ... | @@ -80,68 +93,219 @@ fn serveRequest( |
| 80 | 93 | } |
| 81 | 94 | } |
| 82 | 95 | |
| 96 | const cache_control_header: std.http.Header = .{ |
| 97 | .name = "cache-control", |
| 98 | .value = "max-age=0, must-revalidate", |
| 99 | }; |
| 100 | |
| 83 | 101 | fn serveDocsFile( |
| 84 | 102 | request: *std.http.Server.Request, |
| 85 | | gpa: Allocator, |
| 86 | | docs_dir: std.fs.Dir, |
| 103 | context: *Context, |
| 87 | 104 | name: []const u8, |
| 88 | 105 | content_type: []const u8, |
| 89 | 106 | ) !void { |
| 107 | const gpa = context.gpa; |
| 90 | 108 | // The desired API is actually sendfile, which will require enhancing std.http.Server. |
| 91 | 109 | // We load the file with every request so that the user can make changes to the file |
| 92 | 110 | // and refresh the HTML page without restarting this server. |
| 93 | | const file_contents = try docs_dir.readFileAlloc(gpa, name, 10 * 1024 * 1024); |
| 111 | const file_contents = try context.lib_dir.readFileAlloc(gpa, name, 10 * 1024 * 1024); |
| 94 | 112 | defer gpa.free(file_contents); |
| 95 | 113 | try request.respond(file_contents, .{ |
| 96 | 114 | .status = .ok, |
| 97 | 115 | .extra_headers = &.{ |
| 98 | 116 | .{ .name = "content-type", .value = content_type }, |
| 117 | cache_control_header, |
| 99 | 118 | }, |
| 100 | 119 | }); |
| 101 | 120 | } |
| 102 | 121 | |
| 122 | fn serveSourcesTar(request: *std.http.Server.Request, context: *Context) !void { |
| 123 | _ = request; |
| 124 | _ = context; |
| 125 | @panic("TODO"); |
| 126 | } |
| 127 | |
| 103 | 128 | fn serveWasm( |
| 104 | 129 | request: *std.http.Server.Request, |
| 105 | | gpa: Allocator, |
| 106 | | zig_exe_path: []const u8, |
| 107 | | global_cache_path: []const u8, |
| 130 | context: *Context, |
| 108 | 131 | optimize_mode: std.builtin.OptimizeMode, |
| 109 | 132 | ) !void { |
| 110 | | _ = request; |
| 111 | | _ = gpa; |
| 112 | | _ = zig_exe_path; |
| 113 | | _ = global_cache_path; |
| 114 | | _ = optimize_mode; |
| 115 | | @panic("TODO serve wasm"); |
| 133 | const gpa = context.gpa; |
| 134 | |
| 135 | var arena_instance = std.heap.ArenaAllocator.init(gpa); |
| 136 | defer arena_instance.deinit(); |
| 137 | const arena = arena_instance.allocator(); |
| 138 | |
| 139 | // Do the compilation every request, so that the user can edit the files |
| 140 | // and see the changes without restarting the server. |
| 141 | const wasm_binary_path = try buildWasmBinary(arena, context, optimize_mode); |
| 142 | // std.http.Server does not have a sendfile API yet. |
| 143 | const file_contents = try std.fs.cwd().readFileAlloc(gpa, wasm_binary_path, 10 * 1024 * 1024); |
| 144 | defer gpa.free(file_contents); |
| 145 | try request.respond(file_contents, .{ |
| 146 | .status = .ok, |
| 147 | .extra_headers = &.{ |
| 148 | .{ .name = "content-type", .value = "application/wasm" }, |
| 149 | cache_control_header, |
| 150 | }, |
| 151 | }); |
| 116 | 152 | } |
| 117 | 153 | |
| 118 | | const BuildWasmBinaryOptions = struct { |
| 119 | | zig_exe_path: []const u8, |
| 120 | | global_cache_path: []const u8, |
| 121 | | main_src_path: []const u8, |
| 122 | | }; |
| 154 | fn buildWasmBinary( |
| 155 | arena: Allocator, |
| 156 | context: *Context, |
| 157 | optimize_mode: std.builtin.OptimizeMode, |
| 158 | ) ![]const u8 { |
| 159 | const gpa = context.gpa; |
| 160 | |
| 161 | const main_src_path = try std.fs.path.join(arena, &.{ |
| 162 | context.zig_lib_directory, "docs", "wasm", "main.zig", |
| 163 | }); |
| 123 | 164 | |
| 124 | | fn buildWasmBinary(arena: Allocator, options: BuildWasmBinaryOptions) ![]const u8 { |
| 125 | 165 | var argv: std.ArrayListUnmanaged([]const u8) = .{}; |
| 166 | |
| 126 | 167 | try argv.appendSlice(arena, &.{ |
| 127 | | options.zig_exe_path, |
| 168 | context.zig_exe_path, |
| 128 | 169 | "build-exe", |
| 129 | 170 | "-fno-entry", |
| 130 | | "-OReleaseSmall", |
| 171 | "-O", |
| 172 | @tagName(optimize_mode), |
| 131 | 173 | "-target", |
| 132 | 174 | "wasm32-freestanding", |
| 133 | 175 | "-mcpu", |
| 134 | 176 | "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", |
| 135 | 177 | "--cache-dir", |
| 136 | | options.global_cache_path, |
| 178 | context.global_cache_path, |
| 137 | 179 | "--global-cache-dir", |
| 138 | | options.global_cache_path, |
| 180 | context.global_cache_path, |
| 139 | 181 | "--name", |
| 140 | 182 | "autodoc", |
| 141 | 183 | "-rdynamic", |
| 142 | | options.main_src_path, |
| 184 | main_src_path, |
| 143 | 185 | "--listen=-", |
| 144 | 186 | }); |
| 187 | |
| 188 | var child = std.ChildProcess.init(argv.items, gpa); |
| 189 | child.stdin_behavior = .Pipe; |
| 190 | child.stdout_behavior = .Pipe; |
| 191 | child.stderr_behavior = .Pipe; |
| 192 | try child.spawn(); |
| 193 | |
| 194 | var poller = std.io.poll(gpa, enum { stdout, stderr }, .{ |
| 195 | .stdout = child.stdout.?, |
| 196 | .stderr = child.stderr.?, |
| 197 | }); |
| 198 | defer poller.deinit(); |
| 199 | |
| 200 | try sendMessage(child.stdin.?, .update); |
| 201 | try sendMessage(child.stdin.?, .exit); |
| 202 | |
| 203 | const Header = std.zig.Server.Message.Header; |
| 204 | var result: ?[]const u8 = null; |
| 205 | var result_error_bundle = std.zig.ErrorBundle.empty; |
| 206 | |
| 207 | const stdout = poller.fifo(.stdout); |
| 208 | |
| 209 | poll: while (true) { |
| 210 | while (stdout.readableLength() < @sizeOf(Header)) { |
| 211 | if (!(try poller.poll())) break :poll; |
| 212 | } |
| 213 | const header = stdout.reader().readStruct(Header) catch unreachable; |
| 214 | while (stdout.readableLength() < header.bytes_len) { |
| 215 | if (!(try poller.poll())) break :poll; |
| 216 | } |
| 217 | const body = stdout.readableSliceOfLen(header.bytes_len); |
| 218 | |
| 219 | switch (header.tag) { |
| 220 | .zig_version => { |
| 221 | if (!std.mem.eql(u8, builtin.zig_version_string, body)) { |
| 222 | return error.ZigProtocolVersionMismatch; |
| 223 | } |
| 224 | }, |
| 225 | .error_bundle => { |
| 226 | const EbHdr = std.zig.Server.Message.ErrorBundle; |
| 227 | const eb_hdr = @as(*align(1) const EbHdr, @ptrCast(body)); |
| 228 | const extra_bytes = |
| 229 | body[@sizeOf(EbHdr)..][0 .. @sizeOf(u32) * eb_hdr.extra_len]; |
| 230 | const string_bytes = |
| 231 | body[@sizeOf(EbHdr) + extra_bytes.len ..][0..eb_hdr.string_bytes_len]; |
| 232 | // TODO: use @ptrCast when the compiler supports it |
| 233 | const unaligned_extra = std.mem.bytesAsSlice(u32, extra_bytes); |
| 234 | const extra_array = try arena.alloc(u32, unaligned_extra.len); |
| 235 | @memcpy(extra_array, unaligned_extra); |
| 236 | result_error_bundle = .{ |
| 237 | .string_bytes = try arena.dupe(u8, string_bytes), |
| 238 | .extra = extra_array, |
| 239 | }; |
| 240 | }, |
| 241 | .emit_bin_path => { |
| 242 | const EbpHdr = std.zig.Server.Message.EmitBinPath; |
| 243 | const ebp_hdr = @as(*align(1) const EbpHdr, @ptrCast(body)); |
| 244 | if (!ebp_hdr.flags.cache_hit) { |
| 245 | std.log.info("source changes detected; rebuilding wasm component", .{}); |
| 246 | } |
| 247 | result = try arena.dupe(u8, body[@sizeOf(EbpHdr)..]); |
| 248 | }, |
| 249 | else => {}, // ignore other messages |
| 250 | } |
| 251 | |
| 252 | stdout.discard(body.len); |
| 253 | } |
| 254 | |
| 255 | const stderr = poller.fifo(.stderr); |
| 256 | if (stderr.readableLength() > 0) { |
| 257 | const owned_stderr = try stderr.toOwnedSlice(); |
| 258 | defer gpa.free(owned_stderr); |
| 259 | std.debug.print("{s}", .{owned_stderr}); |
| 260 | } |
| 261 | |
| 262 | // Send EOF to stdin. |
| 263 | child.stdin.?.close(); |
| 264 | child.stdin = null; |
| 265 | |
| 266 | switch (try child.wait()) { |
| 267 | .Exited => |code| { |
| 268 | if (code != 0) { |
| 269 | std.log.err( |
| 270 | "the following command exited with error code {d}:\n{s}", |
| 271 | .{ code, try std.Build.Step.allocPrintCmd(arena, null, argv.items) }, |
| 272 | ); |
| 273 | return error.AlreadyReported; |
| 274 | } |
| 275 | }, |
| 276 | .Signal, .Stopped, .Unknown => { |
| 277 | std.log.err( |
| 278 | "the following command terminated unexpectedly:\n{s}", |
| 279 | .{try std.Build.Step.allocPrintCmd(arena, null, argv.items)}, |
| 280 | ); |
| 281 | return error.AlreadyReported; |
| 282 | }, |
| 283 | } |
| 284 | |
| 285 | if (result_error_bundle.errorMessageCount() > 0) { |
| 286 | const color = std.zig.Color.auto; |
| 287 | result_error_bundle.renderToStdErr(color.renderOptions()); |
| 288 | std.log.err("the following command failed with {d} compilation errors:\n{s}", .{ |
| 289 | result_error_bundle.errorMessageCount(), |
| 290 | try std.Build.Step.allocPrintCmd(arena, null, argv.items), |
| 291 | }); |
| 292 | return error.AlreadyReported; |
| 293 | } |
| 294 | |
| 295 | return result orelse { |
| 296 | std.log.err("child process failed to report result\n{s}", .{ |
| 297 | try std.Build.Step.allocPrintCmd(arena, null, argv.items), |
| 298 | }); |
| 299 | return error.AlreadyReported; |
| 300 | }; |
| 301 | } |
| 302 | |
| 303 | fn sendMessage(file: std.fs.File, tag: std.zig.Client.Message.Tag) !void { |
| 304 | const header: std.zig.Client.Message.Header = .{ |
| 305 | .tag = tag, |
| 306 | .bytes_len = 0, |
| 307 | }; |
| 308 | try file.writeAll(std.mem.asBytes(&header)); |
| 145 | 309 | } |
| 146 | 310 | |
| 147 | 311 | fn openBrowserTab(gpa: Allocator, url: []const u8) !void { |