A maildir and Maildir++ library for Zig 0.16: delivery, flags, folders and quota.
6.6 kB
171 lines
1# SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>
2# SPDX-License-Identifier: MIT
3
4{
5 description = "zig-maildir";
6
7 inputs = {
8 nixpkgs = {
9 url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
10 };
11 # Mine, not the one in nixpkgs, which is Jari Vetoniemi's original and
12 # takes different options.
13 zon2nix = {
14 url = "github:jcollie/zon2nix";
15 inputs = {
16 nixpkgs.follows = "nixpkgs";
17 };
18 };
19 };
20
21 outputs =
22 {
23 nixpkgs,
24 zon2nix,
25 ...
26 }:
27 let
28 inherit (nixpkgs) lib;
29 makePackages =
30 system:
31 import nixpkgs {
32 inherit system;
33 };
34 forAllSystems = lib.genAttrs lib.systems.flakeExposed;
35 # The virtual machine tests are NixOS ones, so they exist only where
36 # NixOS does. Without this `nix flake check` tries to evaluate them for
37 # Darwin and fails before it has run anything.
38 forLinuxSystems = lib.genAttrs (
39 lib.filter (system: lib.hasSuffix "-linux" system) lib.systems.flakeExposed
40 );
41
42 # The devshell's Zig, with one line of its own standard library put
43 # right, because without it `zig build fuzz --fuzz` cannot compile.
44 #
45 # Zig 0.16.0's `compiler/test_runner.zig` reports a failing fuzz input by
46 # asking `std.debug.writeStackTrace` to print what `@errorReturnTrace()`
47 # gave it. Those are two different types: an error return trace is a
48 # `builtin.StackTrace`, a ring buffer with a write index, and that
49 # function takes a `debug.StackTrace`, which is a plain slice and a count
50 # of what was skipped. It is a type error, it is on the path taken only
51 # under `-ffuzz`, and it stops *any* project with a fuzz test in it from
52 # building one. The fix is the function next door: `writeErrorReturnTrace`
53 # takes exactly the type in hand and is what the other three places in
54 # the same file use.
55 #
56 # `--replace-fail` is the whole safety of this: the day Zig ships the fix
57 # the pattern will not be found, the build will fail here rather than
58 # patch something else, and this can go.
59 #
60 # It buys the fuzzer and not its coverage. Nothing in this release
61 # populates the table of program counters, so a bounded run ends with
62 # "corrupted coverage file: pcs_len was zero" and an unbounded one
63 # panics in the build runner's coverage thread; neither is a finding,
64 # and a finding says "input saved to" above the report. The properties
65 # in `tests/fuzz.zig` run as ordinary tests either way, and
66 # `zig build fuzz-run` drives them with a loop of our own.
67 fuzzableZig =
68 pkgs:
69 let
70 # A farm of symlinks rather than a copy: the library is 217 MB, and
71 # exactly one file of it is being changed.
72 library = pkgs.runCommand "zig-0.16.0-lib-fuzz-fix" { } ''
73 cp -rs --no-preserve=mode ${pkgs.zig_0_16}/lib/zig $out
74 chmod -R u+w $out
75 rm $out/compiler/test_runner.zig
76 cp --no-preserve=mode \
77 ${pkgs.zig_0_16}/lib/zig/compiler/test_runner.zig \
78 $out/compiler/test_runner.zig
79 substituteInPlace $out/compiler/test_runner.zig \
80 --replace-fail \
81 'std.debug.writeStackTrace(trace, stderr)' \
82 'std.debug.writeErrorReturnTrace(trace, stderr)'
83 '';
84 in
85 pkgs.symlinkJoin {
86 name = "zig-0.16.0-fuzzable";
87 paths = [ pkgs.zig_0_16 ];
88 nativeBuildInputs = [ pkgs.makeWrapper ];
89 postBuild = ''
90 wrapProgram $out/bin/zig --set ZIG_LIB_DIR ${library}
91 '';
92 };
93 in
94 {
95 packages = forAllSystems (
96 system:
97 let
98 pkgs = makePackages system;
99 in
100 rec {
101 zig-maildir = pkgs.callPackage ./package.nix { };
102 default = zig-maildir;
103 # The dependency farm on its own, so that a workflow job which runs
104 # `zig build` for something other than the package -- the
105 # documentation -- can be handed it without building the package to
106 # get at it.
107 zig-deps = pkgs.callPackage ./build.zig.zon.nix { };
108 }
109 );
110
111 # The virtual machine test, which is here rather than in `zig build
112 # test` because what it tests cannot be reached from a test binary: a
113 # real Dovecot opening the same maildir this library wrote, with its own
114 # idea of what a valid name and a valid flag are.
115 checks = forLinuxSystems (
116 system:
117 let
118 pkgs = makePackages system;
119 zig-maildir = pkgs.callPackage ./package.nix { };
120 test = file: pkgs.testers.runNixOSTest (import file { inherit zig-maildir; });
121 in
122 {
123 inherit zig-maildir;
124 dovecot = test ./tests/nixos/dovecot.nix;
125 }
126 );
127
128 devShells = forAllSystems (
129 system:
130 let
131 pkgs = makePackages system;
132 in
133 {
134 default = pkgs.mkShell {
135 name = "zig-maildir";
136 nativeBuildInputs = [
137 (fuzzableZig pkgs)
138 pkgs.git-pages-cli
139 pkgs.pinact
140 pkgs.reuse
141
142 # Wrapped so that the Zig it shells out to for `zig env` is the
143 # one this project builds with, rather than whatever happens to
144 # be on the caller's PATH. Without a Zig at all it prints
145 # "unable to execute zig" and writes nothing, leaving the
146 # previous build.zig.zon.nix in place looking untouched.
147 (pkgs.symlinkJoin {
148 name = "zon2nix";
149 paths = [ zon2nix.packages.${system}.zon2nix ];
150 nativeBuildInputs = [ pkgs.makeWrapper ];
151 postBuild = ''
152 wrapProgram $out/bin/zon2nix \
153 --prefix PATH : ${lib.makeBinPath [ pkgs.zig_0_16 ]}
154 '';
155 })
156 ]
157 ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [
158 # The other implementations to read what this one writes.
159 # Dovecot is the one whose interpretation of a maildir is the de
160 # facto standard; isync/mbsync and offlineimap each keep state of
161 # their own alongside it and are unforgiving about names.
162 pkgs.dovecot
163 pkgs.isync
164
165 pkgs.kcov
166 ];
167 };
168 }
169 );
170 };
171}