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", }; 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; const version_string = b.option([]const u8, "version-string", "Version reported by `nxdns version`") 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)") 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); const web_assets = webAssetsIndex(b, web_dist_path); 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); 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); 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"), }); tests.root_module.addAnonymousImport("web_assets", .{ .root_source_file = web_assets }); 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. const dns_mod = b.createModule(.{ .root_source_file = b.path("src/dns/dns.zig"), .target = target, .optimize = optimize, }); const fuzz_mod = b.createModule(.{ .root_source_file = b.path("tests/fuzz/dns_fuzz.zig"), .target = target, .optimize = optimize, }); fuzz_mod.addImport("dns", dns_mod); const fuzz_tests = b.addTest(.{ .name = "fuzz", .use_llvm = if (fuzz) true else null, .root_module = fuzz_mod, }); test_step.dependOn(&b.addRunArtifact(fuzz_tests).step); const parsers_mod = b.createModule(.{ .root_source_file = b.path("src/filter/parsers.zig"), .target = target, .optimize = optimize, }); const blocklist_fuzz_mod = b.createModule(.{ .root_source_file = b.path("tests/fuzz/blocklist_fuzz.zig"), .target = target, .optimize = optimize, }); blocklist_fuzz_mod.addImport("parsers", parsers_mod); const blocklist_fuzz_tests = b.addTest(.{ .name = "blocklist-fuzz", .use_llvm = if (fuzz) true else null, .root_module = blocklist_fuzz_mod, }); test_step.dependOn(&b.addRunArtifact(blocklist_fuzz_tests).step); // 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. 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, }); 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-linux-musl" }) catch unreachable, ); const aarch64_tests = b.addTest(.{ .root_module = b.createModule(.{ .root_source_file = b.path("src/tests.zig"), .target = aarch64_target, .optimize = optimize, .link_libc = true, }), }); aarch64_tests.linkage = .static; aarch64_tests.root_module.addOptions("build_options", options); aarch64_tests.root_module.linkLibrary(sqliteLibrary(b, aarch64_target, optimize)); aarch64_tests.root_module.linkLibrary(mbedtlsLibrary(b, aarch64_target, optimize)); aarch64_tests.root_module.addCSourceFile(.{ .file = b.path("src/platform/mbedtls_shim.c") }); addMbedtlsThreadingMacros(aarch64_tests.root_module); aarch64_tests.root_module.addAnonymousImport("test_fixtures", .{ .root_source_file = b.path("tests/fixtures/fixtures.zig"), }); aarch64_tests.root_module.addAnonymousImport("docs_files", .{ .root_source_file = b.path("docs/docs.zig"), }); aarch64_tests.root_module.addAnonymousImport("web_assets", .{ .root_source_file = web_assets }); 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); const cross = b.step("cross", "Build static musl executables for every deploy target"); 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 cross_exe = addExecutable(b, b.resolveTargetQuery(query), optimize, options, web_assets); cross_exe.linkage = .static; const install = b.addInstallArtifact(cross_exe, .{ .dest_dir = .{ .override = .{ .custom = b.fmt("cross/{s}", .{triple}) } }, }); cross.dependOn(&install.step); } } 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) std.Build.LazyPath { const staged = b.addWriteFiles().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", };