| ... | @@ -15,9 +15,8 @@ pub fn main() !void { | ... | @@ -15,9 +15,8 @@ pub fn main() !void { |
| 15 | const zig_exe_path = args[2]; | 15 | const zig_exe_path = args[2]; |
| 16 | const global_cache_path = args[3]; | 16 | const global_cache_path = args[3]; |
| 17 | | 17 | |
| 18 | const docs_path = try std.fs.path.join(arena, &.{ zig_lib_directory, "docs" }); | 18 | var lib_dir = try std.fs.cwd().openDir(zig_lib_directory, .{}); |
| 19 | var docs_dir = try std.fs.cwd().openDir(docs_path, .{}); | 19 | defer lib_dir.close(); |
| 20 | defer docs_dir.close(); | | |
| 21 | | 20 | |
| 22 | const listen_port: u16 = 0; | 21 | const listen_port: u16 = 0; |
| 23 | const address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable; | 22 | const address = std.net.Address.parseIp("127.0.0.1", listen_port) catch unreachable; |
| ... | @@ -29,6 +28,14 @@ pub fn main() !void { | ... | @@ -29,6 +28,14 @@ pub fn main() !void { |
| 29 | std.log.err("unable to open browser: {s}", .{@errorName(err)}); | 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 | var read_buffer: [8000]u8 = undefined; | 39 | var read_buffer: [8000]u8 = undefined; |
| 33 | accept: while (true) { | 40 | accept: while (true) { |
| 34 | const connection = try http_server.accept(); | 41 | const connection = try http_server.accept(); |
| ... | @@ -43,7 +50,7 @@ pub fn main() !void { | ... | @@ -43,7 +50,7 @@ pub fn main() !void { |
| 43 | continue :accept; | 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 | std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) }); | 54 | std.log.err("unable to serve {s}: {s}", .{ request.head.target, @errorName(err) }); |
| 48 | continue :accept; | 55 | continue :accept; |
| 49 | }; | 56 | }; |
| ... | @@ -51,25 +58,31 @@ pub fn main() !void { | ... | @@ -51,25 +58,31 @@ pub fn main() !void { |
| 51 | } | 58 | } |
| 52 | } | 59 | } |
| 53 | | 60 | |
| 54 | fn serveRequest( | 61 | const Context = struct { |
| 55 | request: *std.http.Server.Request, | | |
| 56 | gpa: Allocator, | 62 | gpa: Allocator, |
| 57 | docs_dir: std.fs.Dir, | 63 | lib_dir: std.fs.Dir, |
| | 64 | zig_lib_directory: []const u8, |
| 58 | zig_exe_path: []const u8, | 65 | zig_exe_path: []const u8, |
| 59 | global_cache_path: []const u8, | 66 | global_cache_path: []const u8, |
| 60 | ) !void { | 67 | }; |
| | 68 | |
| | 69 | fn serveRequest(request: *std.http.Server.Request, context: *Context) !void { |
| 61 | if (std.mem.eql(u8, request.head.target, "/") or | 70 | if (std.mem.eql(u8, request.head.target, "/") or |
| 62 | std.mem.eql(u8, request.head.target, "/debug/")) | 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 | } else if (std.mem.eql(u8, request.head.target, "/main.js") or | 74 | } else if (std.mem.eql(u8, request.head.target, "/main.js") or |
| 66 | std.mem.eql(u8, request.head.target, "/debug/main.js")) | 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 | } else if (std.mem.eql(u8, request.head.target, "/main.wasm")) { | 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 | } else if (std.mem.eql(u8, request.head.target, "/debug/main.wasm")) { | 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 | } else { | 86 | } else { |
| 74 | try request.respond("not found", .{ | 87 | try request.respond("not found", .{ |
| 75 | .status = .not_found, | 88 | .status = .not_found, |
| ... | @@ -80,68 +93,219 @@ fn serveRequest( | ... | @@ -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 | fn serveDocsFile( | 101 | fn serveDocsFile( |
| 84 | request: *std.http.Server.Request, | 102 | request: *std.http.Server.Request, |
| 85 | gpa: Allocator, | 103 | context: *Context, |
| 86 | docs_dir: std.fs.Dir, | | |
| 87 | name: []const u8, | 104 | name: []const u8, |
| 88 | content_type: []const u8, | 105 | content_type: []const u8, |
| 89 | ) !void { | 106 | ) !void { |
| | 107 | const gpa = context.gpa; |
| 90 | // The desired API is actually sendfile, which will require enhancing std.http.Server. | 108 | // The desired API is actually sendfile, which will require enhancing std.http.Server. |
| 91 | // We load the file with every request so that the user can make changes to the file | 109 | // We load the file with every request so that the user can make changes to the file |
| 92 | // and refresh the HTML page without restarting this server. | 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 | defer gpa.free(file_contents); | 112 | defer gpa.free(file_contents); |
| 95 | try request.respond(file_contents, .{ | 113 | try request.respond(file_contents, .{ |
| 96 | .status = .ok, | 114 | .status = .ok, |
| 97 | .extra_headers = &.{ | 115 | .extra_headers = &.{ |
| 98 | .{ .name = "content-type", .value = content_type }, | 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 | fn serveWasm( | 128 | fn serveWasm( |
| 104 | request: *std.http.Server.Request, | 129 | request: *std.http.Server.Request, |
| 105 | gpa: Allocator, | 130 | context: *Context, |
| 106 | zig_exe_path: []const u8, | | |
| 107 | global_cache_path: []const u8, | | |
| 108 | optimize_mode: std.builtin.OptimizeMode, | 131 | optimize_mode: std.builtin.OptimizeMode, |
| 109 | ) !void { | 132 | ) !void { |
| 110 | _ = request; | 133 | const gpa = context.gpa; |
| 111 | _ = gpa; | 134 | |
| 112 | _ = zig_exe_path; | 135 | var arena_instance = std.heap.ArenaAllocator.init(gpa); |
| 113 | _ = global_cache_path; | 136 | defer arena_instance.deinit(); |
| 114 | _ = optimize_mode; | 137 | const arena = arena_instance.allocator(); |
| 115 | @panic("TODO serve wasm"); | 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 { | 154 | fn buildWasmBinary( |
| 119 | zig_exe_path: []const u8, | 155 | arena: Allocator, |
| 120 | global_cache_path: []const u8, | 156 | context: *Context, |
| 121 | main_src_path: []const u8, | 157 | optimize_mode: std.builtin.OptimizeMode, |
| 122 | }; | 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 | var argv: std.ArrayListUnmanaged([]const u8) = .{}; | 165 | var argv: std.ArrayListUnmanaged([]const u8) = .{}; |
| | 166 | |
| 126 | try argv.appendSlice(arena, &.{ | 167 | try argv.appendSlice(arena, &.{ |
| 127 | options.zig_exe_path, | 168 | context.zig_exe_path, |
| 128 | "build-exe", | 169 | "build-exe", |
| 129 | "-fno-entry", | 170 | "-fno-entry", |
| 130 | "-OReleaseSmall", | 171 | "-O", |
| | 172 | @tagName(optimize_mode), |
| 131 | "-target", | 173 | "-target", |
| 132 | "wasm32-freestanding", | 174 | "wasm32-freestanding", |
| 133 | "-mcpu", | 175 | "-mcpu", |
| 134 | "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", | 176 | "baseline+atomics+bulk_memory+multivalue+mutable_globals+nontrapping_fptoint+reference_types+sign_ext", |
| 135 | "--cache-dir", | 177 | "--cache-dir", |
| 136 | options.global_cache_path, | 178 | context.global_cache_path, |
| 137 | "--global-cache-dir", | 179 | "--global-cache-dir", |
| 138 | options.global_cache_path, | 180 | context.global_cache_path, |
| 139 | "--name", | 181 | "--name", |
| 140 | "autodoc", | 182 | "autodoc", |
| 141 | "-rdynamic", | 183 | "-rdynamic", |
| 142 | options.main_src_path, | 184 | main_src_path, |
| 143 | "--listen=-", | 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 | fn openBrowserTab(gpa: Allocator, url: []const u8) !void { | 311 | fn openBrowserTab(gpa: Allocator, url: []const u8) !void { |