Hot reload via `zig build --watch`

Hey,

I wondered if anyone might be able to help me understand the behavior of the build system, specifically when trying to use it in the style of Zine or the Tigerbeetle docs: Why We Designed TigerBeetle's Docs from Scratch.

My build.zig currently looks like this:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimise = b.standardOptimizeOption(.{});

    const server = b.addExecutable(.{ .name = "boringsite", .root_module = b.createModule(.{
        .root_source_file = b.path("src/server.zig"),
        .target = target,
        .optimize = optimise,
    }) });
    b.installArtifact(server);

    const run_server = b.addRunArtifact(server);
    b.step("serve", "Build site and run the server").dependOn(&run_server.step);

    const pagegen = b.addExecutable(.{ .name = "pagegen", .root_module = b.createModule(.{
        .root_source_file = b.path("src/pagegen.zig"),
        .target = b.graph.host,
    }) });

    const website = b.addWriteFiles();

    // TODO: make this walk the pages dir
    const pages = [_][]const u8{ "index", "about" };

    for (pages) |page| {
        const out_name = b.fmt("{s}.html", .{page});

        const gen = b.addRunArtifact(pagegen);
        gen.addFileArg(b.path("src/partials/_header.html"));
        gen.addFileArg(b.path(b.fmt("site/{s}.html", .{page})));
        gen.addFileArg(b.path("src/partials/_footer.html"));
        const out = gen.addOutputFileArg(out_name);

        _ = website.addCopyFile(out, out_name);
    }
    _ = website.addCopyFile((b.path("src/style.css")), "style.css");

    b.installDirectory(.{
        .source_dir = website.getDirectory(),
        .install_dir = .prefix,
        .install_subdir = "site",
    });
}

which, for now, takes a couple of HTML files sans headers and footers, sandwiches them with the missing content and serves them like so:

// server.zig
const std = @import("std");
const log = std.log.scoped(.boringsite);

pub fn main(init: std.process.Init) !void {
    const io = init.io;
    const alloc = init.gpa;

    const addr = std.Io.net.IpAddress.parseIp4("127.0.0.1", 8000) catch unreachable;

    var server = try addr.listen(io, .{ .reuse_address = true });
    defer server.deinit(io);

    log.info("listening on http://127.0.0.1:8000", .{});

    while (true) {
        var stream = try server.accept(io);
        defer stream.close(io);

        var read_buf: [4096]u8 = undefined;
        var write_buf: [4096]u8 = undefined;
        var reader = stream.reader(io, &read_buf);
        var writer = stream.writer(io, &write_buf);
        var http_server = std.http.Server.init(&reader.interface, &writer.interface);

        while (true) {
            var request = http_server.receiveHead() catch break;
            handle(alloc, io, &request) catch |err| {
                log.err("request failed: {}", .{err});
                break;
            };
        }
    }
}

fn handle(alloc: std.mem.Allocator, io: std.Io, req: *std.http.Server.Request) !void {
    const method = req.head.method;
    const target = try alloc.dupe(u8, req.head.target);

    if (method == .GET)
        return serveStatic(alloc, io, req, target);

    try respondNotFound(req);
}

fn respondNotFound(req: *std.http.Server.Request) !void {
    try req.respond("<h1>404 Not Found</h1>", .{
        .status = .not_found,
        .extra_headers = &.{
            .{ .name = "content-type", .value = "text/html; charset=utf-8" },
        },
    });
}

fn serveStatic(
    alloc: std.mem.Allocator,
    io: std.Io,
    req: *std.http.Server.Request,
    target: []const u8,
) !void {
    var rel = if (std.mem.eql(u8, target, "/")) "index.html" else target[1..];

    if (std.mem.findScalar(u8, rel, '.') == null)
        rel = try std.fmt.allocPrint(alloc, "{s}.html", .{rel});

    const path = try std.fmt.allocPrint(alloc, "zig-out/site/{s}", .{rel});
    const file = std.Io.Dir.cwd().openFile(io, path, .{}) catch
        return respondNotFound(req);
    defer file.close(io);

    var buf: [4096]u8 = undefined;
    var file_reader = file.reader(io, &buf);
    const data = try file_reader.interface.allocRemaining(alloc, .unlimited);

    const content_type = if (std.mem.endsWith(u8, rel, ".css"))
        "text/css"
    else
        "text/html; charset=utf-8";

    try req.respond(data, .{ .extra_headers = &.{
        .{ .name = "content-type", .value = content_type },
    } });
}

My expectation was that running zig build serve --watch would both serve the site and result in an automatic update for any HTML if the relevant header/footer free HTML source file is updated, but that doesn’t happen, I’m guessing because server.zig contains an infinite loop, and the --watch flag only matters once the program as a whole terminates (right?).

My solution has been to open two tabs and simultaneously run zig build --watch and zig build serve, but this feels wrong. What am I missing?

1 Like

Yeah the issue is you have two applications and you expect to restart http server when you have updates on your pagegen application. I guess you’re right and zig build serve --watch won’t support that since it waits the process to end.

If you want to do it using only build.zig I don’t think it will be possible to hot reload the pages. I guess you have more options if you import pagegen as a library into your http server application, and use something like inotify (if you’re on linux) to monitor file changes and call pagegen inside your http server. This way you will have both zig build generation and html hot reload, but don’t know if this is what you want.

1 Like