const std = @import("std"); const builtin = @import("builtin"); comptime { if (builtin.zig_version.major != 0 or builtin.zig_version.minor != 16) { @compileError("nxdns requires Zig 0.16.x, found " ++ builtin.zig_version_string); } } const cross_targets = [_][]const u8{ "x86_64-linux-musl", "aarch64-linux-musl", }; /// The deploy target, read out of `cross_targets` so `test-aarch64` and `dist` /// cannot describe different machines. Reordering the array is caught here /// rather than by a qemu job that quietly ran the wrong architecture. const aarch64_triple = cross_targets[1]; comptime { if (!std.mem.startsWith(u8, aarch64_triple, "aarch64-")) { @compileError("cross_targets[1] must be the aarch64 triple, found " ++ aarch64_triple); } } pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const integration = b.option(bool, "integration", "Run hermetic integration tests (loopback sockets only)") orelse false; const live = b.option(bool, "live", "Run tests that reach external network hosts") orelse false; const fuzz = b.option(bool, "fuzz", "Build the fuzz targets with the LLVM backend (required for --fuzz)") orelse false; // Milestone-17 ruling 5. The embedded golden carries no source-tree path, // so regeneration names the destination explicitly: // zig build test -Dintegration \ // -Dcontract-samples-out="$PWD/web/src/lib/contractSamples.gen.ts" const contract_samples_out = b.option( []const u8, "contract-samples-out", "Absolute path the contract-sample generator writes instead of comparing", ) orelse ""; // `dist` requires this option (milestone-14 ruling 4): a release must carry // the tag's version, never a development default. Every other step keeps // the default, so `zig build` and `zig build test` need no flag. const version_option = b.option([]const u8, "version-string", "Version reported by `nxdns version` (required by `dist`)"); const version_string = version_option orelse "0.1.0-dev"; const git_commit = b.option([]const u8, "git-commit", "Git commit reported by `nxdns version`") orelse "unknown"; const web_dist = b.option( []const u8, "web-dist", "Built web UI directory to embed (default: the placeholder page). " ++ "Only the exact value `web/dist` gets the freshness check; " ++ "the placeholder and any other path skip it.", ) orelse "web/dist-placeholder"; // `b.path` panics on absolute paths, and a CI artifact directory is one. const web_dist_path: std.Build.LazyPath = if (std.fs.path.isAbsolute(web_dist)) .{ .cwd_relative = web_dist } else b.path(web_dist); // A stale `web/dist` shipped a crashing settings page once (milestone-15 // ruling 5). The stamp is checked only for the real dist tree: the // placeholder has no sources to be stale against, and an explicit path is a // CI artifact that was built elsewhere. const web_dist_check: ?*std.Build.Step = if (std.mem.eql(u8, web_dist, "web/dist")) check: { const run_check = b.addSystemCommand(&.{"node"}); // The script path goes through `b.path` so it resolves against the // build root: `zig build` run from any other directory would not find // a cwd-relative one. run_check.addFileArg(b.path("web/scripts/stamp-dist.mjs")); run_check.addArg("--check"); break :check &run_check.step; } else null; const web_assets = webAssetsIndex(b, web_dist_path, web_dist_check); const options = b.addOptions(); options.addOption(bool, "integration", integration); options.addOption(bool, "live", live); options.addOption([]const u8, "version_string", version_string); options.addOption([]const u8, "git_commit", git_commit); options.addOption([]const u8, "zig_version_string", builtin.zig_version_string); options.addOption([]const u8, "contract_samples_out", contract_samples_out); const exe = addExecutable(b, target, optimize, options, web_assets); b.installArtifact(exe); const run = b.addRunArtifact(exe); run.step.dependOn(b.getInstallStep()); if (b.args) |args| run.addArgs(args); b.step("run", "Run nxdns").dependOn(&run.step); // Zig collects tests only from the root module, so a file missing from // src/tests.zig silently contributes no tests. The list stays hand-written // (generating it from a staged copy would point diagnostics at cache // paths); this makes it complete by construction instead. checkTestImports(b); // A successful `zig build test` still prints `failed command: .../test // ... --listen=-` as its last line. This is an upstream zig 0.16.0 // build-runner labelling defect, not a failure here, and not something // this build script can suppress without hiding real failures. // // Mechanism, verified against the 0.16.0 sources on 2026-08-07: // - std/Build/Step/Run.zig:1540 sets `result_failed_command` for every // spawn, unconditionally ("if an error occurs, it's caused by this // command"). Nothing clears it when the child succeeds. // - compiler/build_runner.zig:1381 prints a step's diagnostics whenever // `result_stderr` is non-empty, explicitly "no matter the result". // - compiler/build_runner.zig:1515, reached from there, emits the // `failed command: ` line because `result_failed_command` is non-null. // So any Run step that both succeeds and writes one byte to stderr gets // the label. Our suite writes plenty: the tests that exercise the warning // paths log through the real sink. // // Minimal reproducer, no mbedTLS and no C: one passing test whose body is // `std.debug.print` plus `try expect(true)`, in a build.zig with nothing // but `addTest` + `addRunArtifact`. It prints the label and reports // "3/3 steps succeeded; 1/1 tests passed". Deleting the print removes the // label. The test child does not crash and does not abort in teardown. // (Running the cached test binary by hand with `--listen=-` does abort, // but only because stdin is then closed and the IPC runner panics on // `EndOfStream`; that is an artifact of the manual invocation.) // // No upstream issue matched a search of ziglang/zig for this behaviour; // the reference is the 0.16.0 source lines above. See AGENTS.md. const licenses_files = licensesFilesRoot(b); const tests = addTestSuite(b, target, optimize, options, web_assets, licenses_files); const test_step = b.step("test", "Run the test suite"); test_step.dependOn(&b.addRunArtifact(tests).step); // Fuzz targets compile as a second test artifact with `dns` as a named module // (a file belongs to one module per compilation; the aggregator keeps owning // the in-file tests). `-Dfuzz` opts into the LLVM backend, which `--fuzz` // needs for sanitizer coverage; stock 0.16.0 also requires a patched // test_runner.zig for fuzz mode — see specs/milestone-2.md. addFuzzSuite(b, target, optimize, fuzz, test_step, .{ .name = "fuzz", .root = "tests/fuzz/dns_fuzz.zig", .import_name = "dns", .import_module = sourceModule(b, target, optimize, "src/dns/dns.zig"), }); addFuzzSuite(b, target, optimize, fuzz, test_step, .{ .name = "blocklist-fuzz", .root = "tests/fuzz/blocklist_fuzz.zig", .import_name = "parsers", .import_module = sourceModule(b, target, optimize, "src/filter/parsers.zig"), }); // `src/web/http_util.zig` imports only std, so its fuzz module roots // directly at the file — no aggregator needed (milestone-15 ruling 6c). addFuzzSuite(b, target, optimize, fuzz, test_step, .{ .name = "http-util-fuzz", .root = "tests/fuzz/http_util_fuzz.zig", .import_name = "http_util", .import_module = sourceModule(b, target, optimize, "src/web/http_util.zig"), }); // The bench harness (milestone-12 ruling 1). The measured roots // (matcher.zig, dns_cache.zig, compiler.zig) share files in their relative // import closures (model.zig, types.zig, ...), and a file may belong to // only one module per compilation — separate modules per root cannot link // into one executable. So one staged module: a copy of src/ plus a // generated aggregator root, imported by the bench as `core`. No sqlite, // no mbedTLS: the closure is pure Zig. // // The compiler fuzz target reuses the same staged tree (milestone-15 // ruling 6b): `compiler.zig` imports `../dns/`, so a module rooted under // `src/filter/` fails with ImportOutsideModulePath. const bench_stage = b.addWriteFiles(); _ = bench_stage.addCopyDirectory(b.path("src"), "src", .{}); const bench_core = bench_stage.add("bench_core.zig", \\pub const matcher = @import("src/filter/matcher.zig"); \\pub const dns_cache = @import("src/cache/dns_cache.zig"); \\pub const compiler = @import("src/filter/compiler.zig"); \\pub const model = @import("src/config/model.zig"); \\pub const dns_name = @import("src/dns/name.zig"); \\pub const dns_types = @import("src/dns/types.zig"); \\pub const packet = @import("src/dns/packet.zig"); \\ ); const bench_core_mod = b.createModule(.{ .root_source_file = bench_core, .target = target, .optimize = optimize, }); addFuzzSuite(b, target, optimize, fuzz, test_step, .{ .name = "compiler-fuzz", .root = "tests/fuzz/compiler_fuzz.zig", .import_name = "core", .import_module = bench_core_mod, }); const bench_mod = b.createModule(.{ .root_source_file = b.path("tools/bench.zig"), .target = target, .optimize = optimize, }); bench_mod.addImport("core", bench_core_mod); const bench_exe = b.addExecutable(.{ .name = "bench", .root_module = bench_mod }); const bench_run = b.addRunArtifact(bench_exe); if (b.args) |args| bench_run.addArgs(args); b.step("bench", "Run the performance benchmarks (PLAN §18)").dependOn(&bench_run.step); // aarch64 test execution (milestone-12 ruling 6): the plain suite // cross-built for the deploy target and run under qemu-user // (`zig build test-aarch64 -fqemu`). Fuzz artifacts stay native-only, and // -Dintegration stays out (ruling 7): qemu-user's slowdown makes the // wall-clock-budgeted loopback TLS tests a flake source. const aarch64_target = b.resolveTargetQuery( std.Target.Query.parse(.{ .arch_os_abi = aarch64_triple }) catch unreachable, ); const aarch64_tests = addTestSuite(b, aarch64_target, optimize, options, web_assets, licenses_files); aarch64_tests.linkage = .static; const aarch64_run = b.addRunArtifact(aarch64_tests); aarch64_run.skip_foreign_checks = true; b.step("test-aarch64", "Run the test suite for aarch64-linux-musl (use -fqemu)") .dependOn(&aarch64_run.step); addDist(b, options, web_assets, .{ .version = version_option, .version_string = version_string, .git_commit = git_commit, .web_dist = web_dist, }); } /// Byte budgets from PLAN §18, asserted by `verify-dist` rather than by shell. const max_binary_bytes = 15_728_640; const max_asset_free_binary_bytes = 10_485_760; /// The default of `-Dweb-dist`. A release built without the flag would ship the /// placeholder admin page, so `dist` refuses it (milestone-14 ruling 4). There /// is deliberately no override. const placeholder_web_dist = "web/dist-placeholder"; /// Repository files that go into the tarball verbatim. `nxdns.conf` is /// `sysusers.conf` under the name it is installed with, so nothing renames a /// file during install (milestone-14 ruling 4). const service_unit_path = "deploy/systemd/nxdns.service"; const sysusers_path = "deploy/systemd/nxdns.conf"; const license_path = "LICENSE"; const install_md_path = "INSTALL.md"; const dockerfile_path = "deploy/docker/Dockerfile"; /// The reviewed third-party inventory `dist` turns into `THIRD-PARTY-NOTICES` /// (milestone-14 ruling 3). It is committed and audited, never scraped. const licenses_dir = "licenses"; const inventory_path = "licenses/inventory.zon"; const DistOptions = struct { /// `-Dversion-string` exactly as given, so a missing one is distinguishable /// from one that happens to equal the default. version: ?[]const u8, version_string: []const u8, git_commit: []const u8, web_dist: []const u8, }; /// `dist` builds everything releasable; `verify-dist` asserts the result. /// Both run on a laptop exactly as they run on the runner, which is the point: /// release checks that only exist in CI shell are the brittleness milestone 14 /// set out to remove. fn addDist( b: *std.Build, options: *std.Build.Step.Options, web_assets: std.Build.LazyPath, dist_options: DistOptions, ) void { const dist_step = b.step("dist", "Build the release tarballs, checksums and staged payloads"); const verify_step = b.step("verify-dist", "Verify the release artifacts under zig-out/dist"); if (distPreflight(b, dist_options)) |problem| { const fail = b.addFail(problem); dist_step.dependOn(&fail.step); verify_step.dependOn(&fail.step); return; } const stage_tool = hostTool(b, "dist_stage"); // A Run step hashes only the resolved path string of a directory argument, // not its contents, so the inventory is staged through WriteFiles first: // the staged copy lives at a content-hashed path, and editing a licence // text re-runs the staging tool instead of replaying a stale cache. const licenses_stage = b.addWriteFiles(); const staged_licenses = licenses_stage.addCopyDirectory(b.path(licenses_dir), ".", .{}); const sums_run = b.addRunArtifact(stage_tool); sums_run.addArg("sums"); sums_run.addArg("--out"); const sums_file = sums_run.addOutputFileArg("SHA256SUMS"); const verify_tool = hostTool(b, "verify_dist"); const verify_run = b.addRunArtifact(verify_tool); // A verifier that a cache can replay is not a verifier. This also makes the // tool's report reach the terminal instead of a captured pipe. verify_run.has_side_effects = true; verify_run.addArgs(&.{ "--dist-dir", b.getInstallPath(.prefix, "dist") }); verify_run.addArgs(&.{ "--work-dir", b.getInstallPath(.prefix, "dist-verify") }); verify_run.addArgs(&.{ "--version", dist_options.version_string }); verify_run.addArgs(&.{ "--git-commit", dist_options.git_commit }); verify_run.addArg("--zon"); verify_run.addFileArg(b.path("build.zig.zon")); verify_run.addArgs(&.{ "--max-bytes", b.fmt("{d}", .{max_binary_bytes}) }); verify_run.addArgs(&.{ "--asset-free-max-bytes", b.fmt("{d}", .{max_asset_free_binary_bytes}) }); verify_run.addArgs(&.{ "--host-arch", @tagName(b.graph.host.result.cpu.arch) }); if (b.enable_qemu) verify_run.addArg("--qemu"); // The asset-free budget gets its own build against a generated empty assets // directory (milestone-14 ruling 5). Not the placeholder: ruling 4 makes // that unbuildable, and re-admitting it for one size check through a back // door would defeat the point of the refusal. const empty_web_assets = webAssetsIndex(b, b.addWriteFiles().getDirectory(), null); for (cross_targets) |triple| { const query = std.Target.Query.parse(.{ .arch_os_abi = triple }) catch |err| { std.debug.panic("invalid cross target '{s}': {t}", .{ triple, err }); }; const target = b.resolveTargetQuery(query); const name = b.fmt("nxdns-{s}-{s}", .{ dist_options.version_string, triple }); // ReleaseSafe is not read from `-Doptimize`: the release artifact must // not change shape because a flag was forgotten. `.strip` is // `std.Build.Module.strip`, which emits `-fstrip` (Module.zig:545), so // no objcopy and no binutils-aarch64-linux-gnu on the runner. const exe = addExecutable(b, target, .ReleaseSafe, options, web_assets); exe.linkage = .static; exe.root_module.strip = true; const stage_run = b.addRunArtifact(stage_tool); stage_run.addArg("stage"); stage_run.addArg("--out"); const staged = stage_run.addOutputDirectoryArg(name); stage_run.addArg("--binary"); stage_run.addFileArg(exe.getEmittedBin()); stage_run.addArg("--service"); stage_run.addFileArg(b.path(service_unit_path)); stage_run.addArg("--sysusers"); stage_run.addFileArg(b.path(sysusers_path)); stage_run.addArg("--license"); stage_run.addFileArg(b.path(license_path)); stage_run.addArg("--install-md"); stage_run.addFileArg(b.path(install_md_path)); stage_run.addArg("--licenses"); stage_run.addDirectoryArg(staged_licenses); // Two commands, never one: `addSystemCommand` executes argv directly // and does not interpret `|`, and a shell pipeline without `pipefail` // would report only gzip's status while a failed tar passed silently. const tar_run = b.addSystemCommand(&.{ "tar", "--format=gnu", "--sort=name", "--mtime=@0", "--owner=0", "--group=0", "--numeric-owner", "-c", "-f", }); setReproducibleEnv(tar_run); const tar_file = tar_run.addOutputFileArg(b.fmt("{s}.tar", .{name})); tar_run.addArg("-C"); // The staged payload is the sole entry of its cache directory, so its // parent is what `-C` needs and declaring it declares the payload. tar_run.addDirectoryArg(staged.dirname()); tar_run.addArg(name); // `-n` is required because `--mtime=@0` normalises the tar member times // but not the timestamp gzip writes into its own header. `-c` is // required because plain `gzip ` rewrites its input in place, and // the input here is a content-addressed cache entry. const gzip_run = b.addSystemCommand(&.{ "gzip", "-n", "-9", "-c" }); setReproducibleEnv(gzip_run); gzip_run.addFileArg(tar_file); const tarball = gzip_run.captureStdOut(.{ .basename = b.fmt("{s}.tar.gz", .{name}) }); const install_binary = b.addInstallFile( staged.path(b, "nxdns"), b.fmt("dist/bin/{s}/nxdns", .{triple}), ); const install_stage = b.addInstallDirectory(.{ .source_dir = staged, .install_dir = .prefix, .install_subdir = b.fmt("dist/stage/{s}", .{name}), }); const install_tarball = b.addInstallFile(tarball, b.fmt("dist/{s}.tar.gz", .{name})); dist_step.dependOn(&install_binary.step); dist_step.dependOn(&install_stage.step); dist_step.dependOn(&install_tarball.step); // The checksum file covers the two tarballs and nothing else. It cannot // cover the container image: that digest does not exist until buildx // has pushed, which happens later and elsewhere. sums_run.addArg("--entry"); sums_run.addArg(b.fmt("{s}.tar.gz", .{name})); sums_run.addFileArg(tarball); const asset_free_exe = addExecutable(b, target, .ReleaseSafe, options, empty_web_assets); asset_free_exe.linkage = .static; asset_free_exe.root_module.strip = true; verify_run.addArgs(&.{ "--archive", triple, b.fmt("{s}.tar.gz", .{name}) }); verify_run.addArgs(&.{ "--asset-free", triple }); verify_run.addFileArg(asset_free_exe.getEmittedBin()); } const install_sums = b.addInstallFile(sums_file, "dist/SHA256SUMS"); dist_step.dependOn(&install_sums.step); verify_run.step.dependOn(dist_step); verify_step.dependOn(&verify_run.step); } /// The one message `dist` and `verify-dist` fail with when the release inputs /// are not there, or null when they are. Returning it rather than calling /// `std.process.fatal` keeps `zig build` and `zig build test` working: the /// checks gate only the two release steps. fn distPreflight(b: *std.Build, dist_options: DistOptions) ?[]const u8 { var problems: std.ArrayList([]const u8) = .empty; if (dist_options.version == null) { problems.append(b.allocator, "-Dversion-string= is required by `dist` " ++ "(the release tag without its leading `v`); it has no default here.") catch @panic("OOM"); } if (std.mem.eql(u8, b.pathFromRoot(dist_options.web_dist), b.pathFromRoot(placeholder_web_dist))) { problems.append(b.allocator, "-Dweb-dist resolves to " ++ placeholder_web_dist ++ ", which a release must never ship. Build the real admin UI " ++ "(cd web && npm ci && npm run build) and pass -Dweb-dist=web/dist.") catch @panic("OOM"); } for ([_][]const u8{ service_unit_path, sysusers_path, license_path, install_md_path, inventory_path, }) |path| { _ = b.build_root.handle.statFile(b.graph.io, path, .{}) catch |err| { problems.append(b.allocator, b.fmt( "`dist` needs '{s}', which is not readable: {t}", .{ path, err }, )) catch @panic("OOM"); }; } if (problems.items.len == 0) return null; return std.mem.join(b.allocator, "\n", problems.items) catch @panic("OOM"); } /// A build-time helper compiled for the host: `tools/.zig`. fn hostTool(b: *std.Build, name: []const u8) *std.Build.Step.Compile { return b.addExecutable(.{ .name = name, .root_module = b.createModule(.{ .root_source_file = b.path(b.fmt("tools/{s}.zig", .{name})), .target = b.graph.host, .optimize = .ReleaseSafe, }), }); } /// Locale and time zone leak into archive metadata and into tool output. /// Pinning both is the cheap half of reproducibility (milestone-14 ruling 12). fn setReproducibleEnv(run: *std.Build.Step.Run) void { run.setEnvironmentVariable("LC_ALL", "C"); run.setEnvironmentVariable("TZ", "UTC"); } /// Milestone-15 ruling 4: every `*.zig` under `src/` must appear in /// `src/tests.zig` as a line that trims to exactly `_ = @import("");`, /// where `` is relative to `src/`. Whole-line equality, not a substring /// search: a commented-out import trims to a line starting with `//` and does /// not match, and `db.zig` cannot satisfy the requirement for `db2.zig`. /// Duplicate lines are an error too — they hide a botched merge. No allowlist: /// a file with no tests still gets imported, because the import is free and an /// exception is the thing that lets a real gap through. fn checkTestImports(b: *std.Build) void { const gpa = b.allocator; const io = b.graph.io; const root = b.build_root.handle; const tests_src = root.readFileAlloc(io, "src/tests.zig", gpa, .limited(4 << 20)) catch |err| { std.process.fatal("cannot read src/tests.zig: {t}", .{err}); }; var src_dir = root.openDir(io, "src", .{ .iterate = true }) catch |err| { std.process.fatal("cannot open src/: {t}", .{err}); }; defer src_dir.close(io); var walker = src_dir.walk(gpa) catch @panic("OOM"); defer walker.deinit(); while (walker.next(io) catch |err| { std.process.fatal("cannot walk src/: {t}", .{err}); }) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.path, ".zig")) continue; if (std.mem.eql(u8, entry.path, "tests.zig")) continue; const needle = b.fmt("_ = @import(\"{s}\");", .{entry.path}); var matches: usize = 0; var lines = std.mem.splitScalar(u8, tests_src, '\n'); while (lines.next()) |line| { if (std.mem.eql(u8, std.mem.trim(u8, line, " \t\r"), needle)) matches += 1; } if (matches == 0) { std.process.fatal("src/tests.zig is missing `{s}`", .{needle}); } if (matches > 1) { std.process.fatal("src/tests.zig repeats `{s}` {d} times", .{ needle, matches }); } } } /// The `src/tests.zig` suite, wired for one target. The host build and the /// aarch64 build take the same artifact; their only differences (`linkage` and /// `skip_foreign_checks`) stay at the call sites. fn addTestSuite( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, options: *std.Build.Step.Options, web_assets: std.Build.LazyPath, licenses_files: std.Build.LazyPath, ) *std.Build.Step.Compile { const tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/tests.zig"), .target = target, .optimize = optimize, .link_libc = true, }), }); tests.root_module.addOptions("build_options", options); tests.root_module.linkLibrary(sqliteLibrary(b, target, optimize)); tests.root_module.linkLibrary(mbedtlsLibrary(b, target, optimize)); tests.root_module.addCSourceFile(.{ .file = b.path("src/platform/mbedtls_shim.c") }); addMbedtlsThreadingMacros(tests.root_module); tests.root_module.addAnonymousImport("test_fixtures", .{ .root_source_file = b.path("tests/fixtures/fixtures.zig"), }); tests.root_module.addAnonymousImport("docs_files", .{ .root_source_file = b.path("docs/docs.zig"), }); // An anonymous-import root must be Zig, so the committed TypeScript golden // is reached through a one-decl wrapper beside it. tests.root_module.addAnonymousImport("contract_samples", .{ .root_source_file = b.path("web/src/lib/contract_samples.zig"), }); tests.root_module.addAnonymousImport("web_assets", .{ .root_source_file = web_assets }); tests.root_module.addAnonymousImport("licenses_files", .{ .root_source_file = licenses_files }); return tests; } /// The root of the `licenses_files` module (milestone-14 ruling 3), through /// which `src/licenses_drift_test.zig` embeds the licence inventory and the two /// dependency manifests whose identity it guards. /// /// A module cannot embed anything above its own root directory, so the trees /// are merged into one WriteFiles directory: `licenses/` at the root, plus /// copies of `build.zig.zon`, `web/package-lock.json` and the image Dockerfile /// beside it. The module root is the copy of `licenses/licenses.zig`, which is /// why that file's `@embedFile` paths name files that do not sit beside it in /// the repository. fn licensesFilesRoot(b: *std.Build) std.Build.LazyPath { const stage = b.addWriteFiles(); _ = stage.addCopyDirectory(b.path("licenses"), ".", .{}); _ = stage.addCopyFile(b.path("build.zig.zon"), "build.zig.zon"); _ = stage.addCopyFile(b.path("web/package-lock.json"), "package-lock.json"); _ = stage.addCopyFile(b.path(dockerfile_path), "Dockerfile"); return stage.getDirectory().path(b, "licenses.zig"); } /// One fuzz test artifact: a module rooted at a `tests/fuzz/` file plus the one /// named import through which that target reaches the code under test. The fuzz /// suites cannot share `addTestSuite` — they take no `build_options`, no /// anonymous imports and no C libraries, and they carry `use_llvm`, which the /// main suite must not set. fn addFuzzSuite( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, fuzz: bool, test_step: *std.Build.Step, spec: struct { name: []const u8, root: []const u8, import_name: []const u8, import_module: *std.Build.Module, }, ) void { const mod = b.createModule(.{ .root_source_file = b.path(spec.root), .target = target, .optimize = optimize, }); mod.addImport(spec.import_name, spec.import_module); const tests = b.addTest(.{ .name = spec.name, .use_llvm = if (fuzz) true else null, .root_module = mod, }); test_step.dependOn(&b.addRunArtifact(tests).step); } /// A plain module over one source file, with no imports of its own: what a fuzz /// target reaches its code under test through. fn sourceModule( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8, ) *std.Build.Module { return b.createModule(.{ .root_source_file = b.path(path), .target = target, .optimize = optimize, }); } fn addExecutable( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, options: *std.Build.Step.Options, web_assets: std.Build.LazyPath, ) *std.Build.Step.Compile { const exe = b.addExecutable(.{ .name = "nxdns", .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, .optimize = optimize, .link_libc = true, }), }); exe.root_module.addOptions("build_options", options); exe.root_module.addAnonymousImport("web_assets", .{ .root_source_file = web_assets }); exe.root_module.linkLibrary(sqliteLibrary(b, target, optimize)); exe.root_module.linkLibrary(mbedtlsLibrary(b, target, optimize)); exe.root_module.addCSourceFile(.{ .file = b.path("src/platform/mbedtls_shim.c") }); addMbedtlsThreadingMacros(exe.root_module); return exe; } /// The embedded web UI (milestone-8 ruling 24): the dist directory plus the /// generated `assets.zig` index and build-time gzip siblings, merged into one /// WriteFiles directory so every `@embedFile` path resolves inside the module /// root. Returns the index file, the root of the `web_assets` module. /// /// The dist is staged through its own WriteFiles step before it reaches the /// tool because a Run step hashes only the resolved path string of a directory /// argument, not its contents; the staged copy lives at a content-hashed path, /// so editing an asset re-runs the tool instead of replaying a stale cache. fn webAssetsIndex( b: *std.Build, dist: std.Build.LazyPath, freshness_check: ?*std.Build.Step, ) std.Build.LazyPath { const stage = b.addWriteFiles(); if (freshness_check) |check| stage.step.dependOn(check); const staged = stage.addCopyDirectory(dist, ".", .{}); const tool = b.addExecutable(.{ .name = "gen_web_assets", .root_module = b.createModule(.{ .root_source_file = b.path("tools/gen_web_assets.zig"), .target = b.graph.host, .optimize = .ReleaseSafe, }), }); const run = b.addRunArtifact(tool); run.addDirectoryArg(staged); const generated = run.addOutputDirectoryArg("web_assets"); const merged = b.addWriteFiles(); _ = merged.addCopyDirectory(staged, ".", .{}); _ = merged.addCopyDirectory(generated, ".", .{}); return merged.getDirectory().path(b, "assets.zig"); } /// SQLite 3.53.4 amalgamation (see build.zig.zon for the pinned URL and hash). fn sqliteLibrary( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) *std.Build.Step.Compile { const dep = b.dependency("sqlite", .{}); const lib = b.addLibrary(.{ .name = "sqlite3", .linkage = .static, .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true, }), }); lib.root_module.addIncludePath(dep.path("")); lib.root_module.addCSourceFile(.{ .file = dep.path("sqlite3.c"), .flags = &.{ "-DSQLITE_ENABLE_FTS5", "-DSQLITE_THREADSAFE=1", "-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1", "-DSQLITE_OMIT_LOAD_EXTENSION", }, }); return lib; } /// Mbed TLS 3.6.7 LTS, stock `mbedtls_config.h` (see build.zig.zon for the pinned URL and hash). fn mbedtlsLibrary( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) *std.Build.Step.Compile { const dep = b.dependency("mbedtls", .{}); const lib = b.addLibrary(.{ .name = "mbedtls", .linkage = .static, .root_module = b.createModule(.{ .target = target, .optimize = optimize, .link_libc = true, }), }); addMbedtlsThreadingMacros(lib.root_module); for ([_][]const u8{ "include", "library", "3rdparty/everest/include", "3rdparty/everest/include/everest", "3rdparty/everest/include/everest/kremlib", "3rdparty/p256-m/p256-m/include", "3rdparty/p256-m/p256-m_driver_interface", }) |include_dir| { lib.root_module.addIncludePath(dep.path(include_dir)); } lib.root_module.addCSourceFiles(.{ .root = dep.path("library"), .files = &mbedtls_library_sources, }); lib.root_module.addCSourceFiles(.{ .root = dep.path("3rdparty"), .files = &mbedtls_3rdparty_sources, }); lib.installHeadersDirectory(dep.path("include/mbedtls"), "mbedtls", .{}); lib.installHeadersDirectory(dep.path("include/psa"), "psa", .{}); return lib; } /// Context sizes change with threading enabled, so every compilation unit that /// includes mbedTLS headers (the library itself and `mbedtls_shim.c`) must see /// the same macros. Concurrent handshakes share `ssl_config`, the CTR-DRBG, and /// global PSA state; without MBEDTLS_THREADING_C those race. fn addMbedtlsThreadingMacros(m: *std.Build.Module) void { m.addCMacro("MBEDTLS_THREADING_C", "1"); m.addCMacro("MBEDTLS_THREADING_PTHREAD", "1"); } /// Every `library/*.c` of the release, matching `library/Makefile`. const mbedtls_library_sources = [_][]const u8{ "aes.c", "aesce.c", "aesni.c", "aria.c", "asn1parse.c", "asn1write.c", "base64.c", "bignum.c", "bignum_core.c", "bignum_mod.c", "bignum_mod_raw.c", "block_cipher.c", "camellia.c", "ccm.c", "chacha20.c", "chachapoly.c", "cipher.c", "cipher_wrap.c", "cmac.c", "constant_time.c", "ctr_drbg.c", "debug.c", "des.c", "dhm.c", "ecdh.c", "ecdsa.c", "ecjpake.c", "ecp.c", "ecp_curves.c", "ecp_curves_new.c", "entropy.c", "entropy_poll.c", "error.c", "gcm.c", "hkdf.c", "hmac_drbg.c", "lmots.c", "lms.c", "md.c", "md5.c", "memory_buffer_alloc.c", "mps_reader.c", "mps_trace.c", "net_sockets.c", "nist_kw.c", "oid.c", "padlock.c", "pem.c", "pk.c", "pk_ecc.c", "pk_wrap.c", "pkcs12.c", "pkcs5.c", "pkcs7.c", "pkparse.c", "pkwrite.c", "platform.c", "platform_util.c", "poly1305.c", "psa_crypto.c", "psa_crypto_aead.c", "psa_crypto_cipher.c", "psa_crypto_client.c", "psa_crypto_driver_wrappers_no_static.c", "psa_crypto_ecp.c", "psa_crypto_ffdh.c", "psa_crypto_hash.c", "psa_crypto_mac.c", "psa_crypto_pake.c", "psa_crypto_random.c", "psa_crypto_rsa.c", "psa_crypto_se.c", "psa_crypto_slot_management.c", "psa_crypto_storage.c", "psa_its_file.c", "psa_util.c", "ripemd160.c", "rsa.c", "rsa_alt_helpers.c", "sha1.c", "sha256.c", "sha3.c", "sha512.c", "ssl_cache.c", "ssl_ciphersuites.c", "ssl_client.c", "ssl_cookie.c", "ssl_debug_helpers_generated.c", "ssl_msg.c", "ssl_ticket.c", "ssl_tls.c", "ssl_tls12_client.c", "ssl_tls12_server.c", "ssl_tls13_client.c", "ssl_tls13_generic.c", "ssl_tls13_keys.c", "ssl_tls13_server.c", "threading.c", "timing.c", "version.c", "version_features.c", "x509.c", "x509_create.c", "x509_crl.c", "x509_crt.c", "x509_csr.c", "x509write.c", "x509write_crt.c", "x509write_csr.c", }; /// The object lists of `3rdparty/everest/Makefile.inc` and `3rdparty/p256-m/Makefile.inc`. /// The stock config enables neither driver, so these compile to empty objects. const mbedtls_3rdparty_sources = [_][]const u8{ "everest/library/everest.c", "everest/library/x25519.c", "everest/library/Hacl_Curve25519_joined.c", "p256-m/p256-m_driver_entrypoints.c", "p256-m/p256-m/p256-m.c", };