A maildir and Maildir++ library for Zig 0.16: delivery, flags, folders and quota.
7.7 kB
173 lines
1// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>
2// SPDX-License-Identifier: MIT
3
4const std = @import("std");
5
6pub fn build(b: *std.Build) void {
7 const target = b.standardTargetOptions(.{});
8 const optimize = b.standardOptimizeOption(.{});
9
10 // Email messages, which this library reads and writes but deliberately
11 // does not parse: <https://git.jcollie.dev/jeff/zig-mime>. A maildir is a
12 // naming convention over a directory, and what it stores is somebody
13 // else's subject.
14 const mime = b.dependency("mime", .{
15 .target = target,
16 .optimize = optimize,
17 });
18
19 const mod = b.addModule("maildir", .{
20 .root_source_file = b.path("src/root.zig"),
21 .target = target,
22 .imports = &.{.{ .name = "mime", .module = mime.module("mime") }},
23 });
24
25 // The command-line tool, which exists to show what the library looks like
26 // from outside and to give the NixOS tests a second program to point at a
27 // maildir Dovecot is also looking at.
28 const exe = b.addExecutable(.{
29 .name = "zig-maildir",
30 .root_module = b.createModule(.{
31 .root_source_file = b.path("src/main.zig"),
32 .target = target,
33 .optimize = optimize,
34 .imports = &.{
35 .{ .name = "maildir", .module = mod },
36 .{ .name = "mime", .module = mime.module("mime") },
37 },
38 }),
39 });
40 b.installArtifact(exe);
41
42 const run_cmd = b.addRunArtifact(exe);
43 run_cmd.step.dependOn(b.getInstallStep());
44 run_cmd.stdio = .inherit;
45 if (b.args) |args| run_cmd.addArgs(args);
46 const run_step = b.step("run", "Run the command-line tool");
47 run_step.dependOn(&run_cmd.step);
48
49 // A test executable covers one module, so each needs its own. Missing one
50 // out would not fail: its tests would simply never run.
51 const test_step = b.step("test", "Run tests");
52 for ([_]*std.Build.Module{ mod, exe.root_module }) |m| {
53 test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = m })).step);
54 }
55
56 // What the library does to a real directory: delivery, flags, folders and
57 // the quota ledger, all against a temporary maildir. A module of its own
58 // so that `zig build test` runs them and a consumer never compiles them.
59 const e2e_mod = b.createModule(.{
60 .root_source_file = b.path("tests/e2e.zig"),
61 .target = target,
62 .optimize = optimize,
63 .imports = &.{
64 .{ .name = "maildir", .module = mod },
65 .{ .name = "mime", .module = mime.module("mime") },
66 },
67 });
68 test_step.dependOn(&b.addRunArtifact(b.addTest(.{ .root_module = e2e_mod })).step);
69
70 // The fuzz targets: what the name, flag, folder and quota parsers must do
71 // with input nobody wrote. They are ordinary tests as well as fuzz
72 // targets, so `zig build test` exercises the same properties on the seeds
73 // checked in beside them.
74 const fuzz_mod = b.createModule(.{
75 .root_source_file = b.path("tests/fuzz.zig"),
76 .target = target,
77 .optimize = optimize,
78 .imports = &.{.{ .name = "maildir", .module = mod }},
79 });
80 // Zig's fuzzer takes one test at a time and keeps a coverage file per
81 // test, so naming a target is what you want when a finding is being
82 // chased: `zig build fuzz --fuzz -Dfuzz-filter=name`.
83 const fuzz_filter = b.option(
84 []const u8,
85 "fuzz-filter",
86 "Fuzz or test only the targets whose name contains this",
87 );
88 const fuzz_tests = b.addTest(.{
89 .root_module = fuzz_mod,
90 .filters = if (fuzz_filter) |f| &.{f} else &.{},
91 });
92 test_step.dependOn(&b.addRunArtifact(fuzz_tests).step);
93
94 // A step of its own for `zig build fuzz --fuzz`, which needs a run step
95 // holding nothing else: the fuzzer takes over the terminal and runs until
96 // it is stopped, so it must not be reached by `zig build test`.
97 const fuzz_step = b.step("fuzz", "The fuzz targets: add --fuzz to fuzz them");
98 fuzz_step.dependOn(&b.addRunArtifact(fuzz_tests).step);
99
100 // The loop that drives those same targets without Zig's fuzzer, which
101 // this toolchain cannot usefully run: `tools/fuzz.zig` says why, and the
102 // short version is that the coverage table comes back empty. Optimised,
103 // because a fuzzer's whole job is how many inputs it gets through, and
104 // ReleaseSafe keeps every check that makes a failure a failure.
105 const fuzz_run = b.addExecutable(.{
106 .name = "zig-maildir-fuzz",
107 .root_module = b.createModule(.{
108 .root_source_file = b.path("tools/fuzz.zig"),
109 .target = b.graph.host,
110 .optimize = .ReleaseSafe,
111 .imports = &.{.{ .name = "fuzz_targets", .module = fuzz_mod }},
112 }),
113 });
114 const run_fuzz = b.addRunArtifact(fuzz_run);
115 run_fuzz.stdio = .inherit;
116 if (b.args) |a| run_fuzz.addArgs(a);
117 const fuzz_run_step = b.step("fuzz-run", "Fuzz the targets with a loop of our own");
118 fuzz_run_step.dependOn(&run_fuzz.step);
119
120 // Nothing else builds the fuzz driver, so without this it could stop
121 // compiling and `zig build test` would not notice.
122 const check_step = b.step("check", "Compile everything without running it");
123 check_step.dependOn(&fuzz_run.step);
124
125 // -- documentation -------------------------------------------------------
126 //
127 // Zig emits the API documentation as a side effect of compiling, so the
128 // module is built as a library purely to get at it. What comes out is not
129 // a page but a program: a WebAssembly viewer, its javascript, and a tar of
130 // the sources it reads from.
131 const library = b.addLibrary(.{ .name = "maildir", .root_module = mod });
132 const install_docs = b.addInstallDirectory(.{
133 .source_dir = library.getEmittedDocs(),
134 .install_dir = .prefix,
135 .install_subdir = "docs",
136 });
137 const docs_step = b.step("docs", "Build the API documentation into zig-out/docs");
138 docs_step.dependOn(&install_docs.step);
139
140 // That viewer fetches `sources.tar` and `main.wasm` at runtime, which a
141 // browser refuses to do from a `file://` page, so reading the docs
142 // locally means serving them. It is the same reason `zig std` runs a
143 // server rather than opening a file.
144 const docs_port = b.option(u16, "docs-port", "Port for `zig build docs-serve` (default 8000)") orelse 8000;
145
146 const docs_server = b.addExecutable(.{
147 .name = "docs-server",
148 .root_module = b.createModule(.{
149 .root_source_file = b.path("tools/docs_server.zig"),
150 // Always built for the machine running the build, never for
151 // whatever -Dtarget the library is being built for.
152 .target = b.graph.host,
153 .optimize = .Debug,
154 }),
155 });
156
157 const run_docs_server = b.addRunArtifact(docs_server);
158 run_docs_server.step.dependOn(&install_docs.step);
159 run_docs_server.addArg(b.getInstallPath(.prefix, "docs"));
160 run_docs_server.addArg(b.fmt("{d}", .{docs_port}));
161 // The server runs until interrupted, so its output has to reach the
162 // terminal rather than being captured by the build runner.
163 run_docs_server.stdio = .inherit;
164
165 const docs_serve_step = b.step("docs-serve", "Serve the API documentation over HTTP");
166 docs_serve_step.dependOn(&run_docs_server.step);
167
168 // The server has tests of its own; without this they would never run.
169 test_step.dependOn(&b.addRunArtifact(
170 b.addTest(.{ .root_module = docs_server.root_module }),
171 ).step);
172 check_step.dependOn(&docs_server.step);
173}