zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

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 }