# SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie # SPDX-License-Identifier: MIT { description = "zig-maildir"; inputs = { nixpkgs = { url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; }; # Mine, not the one in nixpkgs, which is Jari Vetoniemi's original and # takes different options. zon2nix = { url = "github:jcollie/zon2nix"; inputs = { nixpkgs.follows = "nixpkgs"; }; }; }; outputs = { nixpkgs, zon2nix, ... }: let inherit (nixpkgs) lib; makePackages = system: import nixpkgs { inherit system; }; forAllSystems = lib.genAttrs lib.systems.flakeExposed; # The virtual machine tests are NixOS ones, so they exist only where # NixOS does. Without this `nix flake check` tries to evaluate them for # Darwin and fails before it has run anything. forLinuxSystems = lib.genAttrs ( lib.filter (system: lib.hasSuffix "-linux" system) lib.systems.flakeExposed ); # The devshell's Zig, with one line of its own standard library put # right, because without it `zig build fuzz --fuzz` cannot compile. # # Zig 0.16.0's `compiler/test_runner.zig` reports a failing fuzz input by # asking `std.debug.writeStackTrace` to print what `@errorReturnTrace()` # gave it. Those are two different types: an error return trace is a # `builtin.StackTrace`, a ring buffer with a write index, and that # function takes a `debug.StackTrace`, which is a plain slice and a count # of what was skipped. It is a type error, it is on the path taken only # under `-ffuzz`, and it stops *any* project with a fuzz test in it from # building one. The fix is the function next door: `writeErrorReturnTrace` # takes exactly the type in hand and is what the other three places in # the same file use. # # `--replace-fail` is the whole safety of this: the day Zig ships the fix # the pattern will not be found, the build will fail here rather than # patch something else, and this can go. # # It buys the fuzzer and not its coverage. Nothing in this release # populates the table of program counters, so a bounded run ends with # "corrupted coverage file: pcs_len was zero" and an unbounded one # panics in the build runner's coverage thread; neither is a finding, # and a finding says "input saved to" above the report. The properties # in `tests/fuzz.zig` run as ordinary tests either way, and # `zig build fuzz-run` drives them with a loop of our own. fuzzableZig = pkgs: let # A farm of symlinks rather than a copy: the library is 217 MB, and # exactly one file of it is being changed. library = pkgs.runCommand "zig-0.16.0-lib-fuzz-fix" { } '' cp -rs --no-preserve=mode ${pkgs.zig_0_16}/lib/zig $out chmod -R u+w $out rm $out/compiler/test_runner.zig cp --no-preserve=mode \ ${pkgs.zig_0_16}/lib/zig/compiler/test_runner.zig \ $out/compiler/test_runner.zig substituteInPlace $out/compiler/test_runner.zig \ --replace-fail \ 'std.debug.writeStackTrace(trace, stderr)' \ 'std.debug.writeErrorReturnTrace(trace, stderr)' ''; in pkgs.symlinkJoin { name = "zig-0.16.0-fuzzable"; paths = [ pkgs.zig_0_16 ]; nativeBuildInputs = [ pkgs.makeWrapper ]; postBuild = '' wrapProgram $out/bin/zig --set ZIG_LIB_DIR ${library} ''; }; in { packages = forAllSystems ( system: let pkgs = makePackages system; in rec { zig-maildir = pkgs.callPackage ./package.nix { }; default = zig-maildir; # The dependency farm on its own, so that a workflow job which runs # `zig build` for something other than the package -- the # documentation -- can be handed it without building the package to # get at it. zig-deps = pkgs.callPackage ./build.zig.zon.nix { }; } ); # The virtual machine test, which is here rather than in `zig build # test` because what it tests cannot be reached from a test binary: a # real Dovecot opening the same maildir this library wrote, with its own # idea of what a valid name and a valid flag are. checks = forLinuxSystems ( system: let pkgs = makePackages system; zig-maildir = pkgs.callPackage ./package.nix { }; test = file: pkgs.testers.runNixOSTest (import file { inherit zig-maildir; }); in { inherit zig-maildir; dovecot = test ./tests/nixos/dovecot.nix; } ); devShells = forAllSystems ( system: let pkgs = makePackages system; in { default = pkgs.mkShell { name = "zig-maildir"; nativeBuildInputs = [ (fuzzableZig pkgs) pkgs.git-pages-cli pkgs.pinact pkgs.reuse # Wrapped so that the Zig it shells out to for `zig env` is the # one this project builds with, rather than whatever happens to # be on the caller's PATH. Without a Zig at all it prints # "unable to execute zig" and writes nothing, leaving the # previous build.zig.zon.nix in place looking untouched. (pkgs.symlinkJoin { name = "zon2nix"; paths = [ zon2nix.packages.${system}.zon2nix ]; nativeBuildInputs = [ pkgs.makeWrapper ]; postBuild = '' wrapProgram $out/bin/zon2nix \ --prefix PATH : ${lib.makeBinPath [ pkgs.zig_0_16 ]} ''; }) ] ++ lib.optionals pkgs.stdenv.hostPlatform.isLinux [ # The other implementations to read what this one writes. # Dovecot is the one whose interpretation of a maildir is the de # facto standard; isync/mbsync and offlineimap each keep state of # their own alongside it and are unforgiving about names. pkgs.dovecot pkgs.isync pkgs.kcov ]; }; } ); }; }