// SPDX-FileCopyrightText: 2026 Jeffrey C. Ollie // // SPDX-License-Identifier: MIT const std = @import("std"); /// One simulator, as upstream's CMakeLists.txt describes it. The table is /// generated by tools/generate.py; see that for what each field came from. const Simulator = struct { name: []const u8, label: []const u8, sources: []const []const u8, /// Only compiled in when building with video. display_sources: []const []const u8, includes: []const []const u8, defines: []const []const u8, int64: bool, addr64: bool, video: bool, display: bool, aio: bool, /// The script `zig build test` runs, relative to the top of upstream. test_script: ?[]const u8, test_args: []const []const u8, }; const simulators: []const Simulator = @import("simulators.zon"); /// The simulator framework every simulator links: the command processor and /// the device libraries. const sim_sources: []const []const u8 = &.{ "scp.c", "sim_card.c", "sim_console.c", "sim_disk.c", "sim_ether.c", "sim_fio.c", "sim_imd.c", "sim_scsi.c", "sim_serial.c", "sim_sock.c", "sim_tape.c", "sim_timer.c", "sim_tmxr.c", "sim_video.c", }; const sim_video_sources: []const []const u8 = &.{ "display/display.c", "display/sim_ws.c", }; const slirp_sources: []const []const u8 = &.{ "slirp/arp_table.c", "slirp/bootp.c", "slirp/cksum.c", "slirp/dnssearch.c", "slirp/if.c", "slirp/ip_icmp.c", "slirp/ip_input.c", "slirp/ip_output.c", "slirp/mbuf.c", "slirp/misc.c", "slirp/sbuf.c", "slirp/slirp.c", "slirp/socket.c", "slirp/tcp_input.c", "slirp/tcp_output.c", "slirp/tcp_subr.c", "slirp/tcp_timer.c", "slirp/tftp.c", "slirp/udp.c", "slirp_glue/glib_qemu_stubs.c", "slirp_glue/sim_slirp.c", }; /// Compiler flags for all of simh's C. Upstream's own: `-U__STRICT_ANSI__` /// for the POSIX and GNU declarations, and no strict overflow, which the VAX /// simulators in particular depend on. /// /// `SHOW VERSION` reports `__DATE__` and `__TIME__`, which Zig makes an /// error in release builds. Clang takes both from SOURCE_DATE_EPOCH when it /// is set, as it is in a Nix build, so the result is still reproducible there. const c_flags: []const []const u8 = &.{ "-std=gnu99", "-U__STRICT_ANSI__", "-fno-strict-overflow", "-Wno-date-time", }; const Features = struct { video: bool, regex: bool, editline: bool, network: bool, pcap: bool, slirp: bool, tap: bool, vde: bool, aio: bool, }; /// What upstream builds a framework library for: one per combination of /// these that some simulator asks for. const Variant = struct { int64: bool, addr64: bool, video: bool, aio: bool, }; const Context = struct { b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, sanitize: bool, upstream: *std.Build.Dependency, libpcap: *std.Build.Dependency, features: Features, slirp: ?*std.Build.Step.Compile, cores: std.AutoHashMapUnmanaged(Variant, *std.Build.Step.Compile) = .empty, fn createModule(ctx: *Context) *std.Build.Module { return ctx.b.createModule(.{ .target = ctx.target, .optimize = ctx.optimize, .link_libc = true, // simh is decades of C that shifts into the sign bit and // overflows on purpose; the sanitizer stops it in its tracks. .sanitize_c = if (ctx.sanitize) .full else .off, }); } /// The definitions and include paths that the framework and every /// simulator compiled against it must agree on -- upstream's PUBLIC and /// INTERFACE ones. fn addShared(ctx: *Context, mod: *std.Build.Module, variant: Variant) void { const os = ctx.target.result.os.tag; const f = ctx.features; mod.addIncludePath(ctx.upstream.path(".")); // os_features mod.addCMacro("_GNU_SOURCE", "1"); if (os != .windows) { mod.addCMacro("HAVE_SYS_IOCTL", "1"); mod.addCMacro("HAVE_UTIME", "1"); mod.addCMacro("HAVE_GLOB", "1"); mod.addCMacro("HAVE_SHM_OPEN", "1"); mod.addCMacro("SIM_HAVE_DLOPEN", if (os.isDarwin()) "dylib" else "so"); mod.linkSystemLibrary("m", .{}); if (os == .linux) { mod.addCMacro("HAVE_LINUX_CDROM", "1"); mod.linkSystemLibrary("dl", .{}); mod.linkSystemLibrary("rt", .{}); } } else { mod.addCMacro("HAVE_WINMM", "1"); mod.linkSystemLibrary("ws2_32", .{}); mod.linkSystemLibrary("wsock32", .{}); mod.linkSystemLibrary("winmm", .{}); } if (f.editline) { mod.addCMacro("HAVE_EDITLINE", "1"); mod.linkSystemLibrary("libedit", .{}); } // thread_lib, and the asynchronous I/O built on it if (f.aio and variant.aio) { mod.linkSystemLibrary("pthread", .{}); mod.addCMacro("HAVE_SEMAPHORE", "1"); mod.addCMacro("USE_READER_THREAD", "1"); mod.addCMacro("SIM_ASYNCH_IO", "1"); } else { mod.addCMacro("DONT_USE_READER_THREAD", "1"); } if (variant.int64) mod.addCMacro("USE_INT64", "1"); if (variant.addr64) mod.addCMacro("USE_ADDR64", "1"); // simh_regexp; zlib comes with either it or video if (f.regex) { mod.addCMacro("HAVE_PCRE2_H", "1"); mod.linkSystemLibrary("libpcre2-8", .{}); } if (f.regex or (f.video and variant.video)) { mod.addCMacro("HAVE_ZLIB", "1"); mod.linkSystemLibrary("zlib", .{}); } // simh_video if (f.video and variant.video) { mod.addCMacro("USE_SIM_VIDEO", "1"); mod.addCMacro("HAVE_LIBSDL", "1"); mod.addCMacro("HAVE_LIBPNG", "1"); mod.linkSystemLibrary("sdl2", .{}); mod.linkSystemLibrary("libpng", .{}); if (os.isDarwin()) mod.addCMacro("SDL_MAIN_AVAILABLE", "1"); } // simh_network if (f.network) { mod.addCMacro("USE_SHARED", "1"); if (f.pcap) { // libpcap is loaded at run time; only its headers are needed // here. Every libpcap for over a decade declares // pcap_compile's filter `const`. mod.addIncludePath(ctx.libpcap.path(".")); mod.addCMacro("HAVE_PCAP_NETWORK", "1"); mod.addCMacro("BPF_CONST_STRING", "1"); } if (f.tap) mod.addCMacro("HAVE_TAP_NETWORK", "1"); if (f.vde) { mod.addCMacro("HAVE_VDE_NETWORK", "1"); mod.linkSystemLibrary("vdeplug", .{}); } if (ctx.slirp) |slirp| { mod.addCMacro("HAVE_SLIRP_NETWORK", "1"); mod.addCMacro("USE_SIMH_SLIRP_DEBUG", "1"); mod.addIncludePath(ctx.upstream.path("slirp_glue")); mod.linkLibrary(slirp); } } } /// The framework library for `variant`, built the first time a simulator /// asks for it. fn core(ctx: *Context, variant: Variant) *std.Build.Step.Compile { if (ctx.cores.get(variant)) |lib| return lib; const b = ctx.b; const mod = ctx.createModule(); ctx.addShared(mod, variant); mod.addCMacro("USE_SIM_CARD", "1"); mod.addCMacro("USE_SIM_IMD", "1"); mod.addCMacro("SIM_BUILD_TOOL", b.fmt("Zig {s}", .{@import("builtin").zig_version_string})); mod.addCSourceFiles(.{ .root = ctx.upstream.path("."), .files = sim_sources, .flags = c_flags }); if (ctx.features.video and variant.video) { mod.addCSourceFiles(.{ .root = ctx.upstream.path("."), .files = sim_video_sources, .flags = c_flags }); } const lib = b.addLibrary(.{ .name = b.fmt("simh{s}{s}{s}", .{ if (variant.addr64) "z64" else if (variant.int64) "i64" else "core", if (variant.video) "_video" else "", if (variant.aio) "_aio" else "", }), .root_module = mod, }); ctx.cores.put(b.allocator, variant, lib) catch @panic("OOM"); return lib; } fn simulator(ctx: *Context, sim: Simulator) *std.Build.Step.Compile { const b = ctx.b; const video = ctx.features.video and sim.video; const variant: Variant = .{ .int64 = sim.int64, .addr64 = sim.addr64, .video = sim.video, .aio = sim.aio, }; const mod = ctx.createModule(); ctx.addShared(mod, variant); mod.linkLibrary(ctx.core(variant)); for (sim.includes) |dir| mod.addIncludePath(ctx.upstream.path(dir)); for (sim.defines) |define| { if (std.mem.findScalar(u8, define, '=')) |eq| { mod.addCMacro(define[0..eq], define[eq + 1 ..]); } else { mod.addCMacro(define, "1"); } } mod.addCSourceFiles(.{ .root = ctx.upstream.path("."), .files = sim.sources, .flags = c_flags }); if (video and sim.display) { mod.addCMacro("USE_DISPLAY", "1"); mod.addCSourceFiles(.{ .root = ctx.upstream.path("."), .files = sim.display_sources, .flags = c_flags }); } return b.addExecutable(.{ .name = sim.name, .root_module = mod }); } }; pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const os = target.result.os.tag; const selected = b.option( []const []const u8, "simulator", "Build only this simulator; may be given more than once (default: all of them)", ); const network = b.option(bool, "network", "Ethernet device support (default: true)") orelse true; const features: Features = .{ .video = b.option(bool, "video", "Graphical displays, with SDL2 and libpng (default: true)") orelse true, .regex = b.option(bool, "regex", "Regular expressions in EXPECT, with PCRE2 and zlib (default: true)") orelse true, .editline = b.option(bool, "editline", "Command line editing and history, with libedit (default: true)") orelse (os != .windows), .network = network, .pcap = network and (b.option(bool, "pcap", "Host Ethernet through libpcap, loaded at run time (default: true)") orelse true), .slirp = network and (b.option(bool, "slirp", "Built-in user-mode NAT networking (default: true)") orelse true), .tap = network and (b.option(bool, "tap", "Linux TUN/TAP networking (default: true on Linux)") orelse (os == .linux)), .vde = network and (b.option(bool, "vde", "Virtual Distributed Ethernet, with libvdeplug (default: false)") orelse false), .aio = b.option(bool, "aio", "Asynchronous I/O on threads (default: true)") orelse (os != .windows), }; const sanitize = b.option(bool, "sanitize", "Build the C with the undefined behavior sanitizer (default: false)") orelse false; const upstream = b.dependency("simh", .{}); var ctx: Context = .{ .b = b, .target = target, .optimize = optimize, .sanitize = sanitize, .upstream = upstream, .libpcap = b.dependency("libpcap", .{}), .features = features, .slirp = null, }; if (features.slirp) { const mod = ctx.createModule(); mod.addIncludePath(upstream.path(".")); mod.addIncludePath(upstream.path("slirp")); mod.addIncludePath(upstream.path("slirp_glue")); mod.addIncludePath(upstream.path("slirp_glue/qemu")); if (os == .windows) { mod.addIncludePath(upstream.path("slirp_glue/qemu/win32/include")); mod.linkSystemLibrary("iphlpapi", .{}); } mod.addCMacro("_GNU_SOURCE", "1"); mod.addCMacro("HAVE_SLIRP_NETWORK", "1"); mod.addCMacro("USE_SIMH_SLIRP_DEBUG", "1"); mod.addCSourceFiles(.{ .root = upstream.path("."), .files = slirp_sources, .flags = c_flags }); ctx.slirp = b.addLibrary(.{ .name = "slirp", .root_module = mod }); } if (selected) |names| { for (names) |name| { for (simulators) |sim| { if (std.mem.eql(u8, name, sim.name)) break; } else { // What the build runner itself does with an unknown option. std.log.err("-Dsimulator: there is no simulator named '{s}'", .{name}); b.invalid_user_input = true; } } if (b.invalid_user_input) return; } const test_step = b.step("test", "Run upstream's test script for each simulator that has one"); // Test scripts change into their own directory and some write files // there -- i650's punches its card decks into I650/sw -- so they run on // a copy in the build cache rather than in the fetched package, which is // read-only in a Nix build and must not change anywhere else. One copy // per simulator directory, shared by every simulator tested from it. // tools/copy_tree.zig makes it, since a copy that kept the package's // modes would be as read-only as the package. const copy_tree = b.addExecutable(.{ .name = "copy_tree", .root_module = b.createModule(.{ .root_source_file = b.path("tools/copy_tree.zig"), // Always the machine running the build, never whatever -Dtarget // the simulators are being built for. .target = b.graph.host, .optimize = .Debug, }), }); var test_dirs: std.StringHashMapUnmanaged(std.Build.LazyPath) = .empty; for (simulators) |sim| { if (selected) |names| { for (names) |name| { if (std.mem.eql(u8, name, sim.name)) break; } else continue; } const exe = ctx.simulator(sim); b.installArtifact(exe); // Upstream installs the VAX simulator under its model name as well. if (std.mem.eql(u8, sim.name, "vax")) { b.getInstallStep().dependOn(&b.addInstallArtifact(exe, .{ .dest_sub_path = b.fmt("microvax3900{s}", .{target.result.exeFileExt()}), }).step); } const run = b.addRunArtifact(exe); run.addPassthruArgs(); const run_step = b.step(b.fmt("run-{s}", .{sim.name}), b.fmt("Run the {s} simulator", .{sim.name})); run_step.dependOn(&run.step); // The way upstream's CTest runs them: the register sanity check, // then the simulator's own test script, verbosely. const check = b.addRunArtifact(exe); check.setName(b.fmt("test {s}", .{sim.name})); check.addArg("RegisterSanityCheck"); check.addArgs(sim.test_args); if (sim.test_script) |script| { // The directory above the script's `tests`, e.g. VAX. const dir = std.fs.path.dirname(std.fs.path.dirname(script).?).?; const copy = test_dirs.getOrPut(b.allocator, dir) catch @panic("OOM"); if (!copy.found_existing) { const run_copy = b.addRunArtifact(copy_tree); run_copy.setName(b.fmt("copy {s} for testing", .{dir})); run_copy.addDirectoryArg(upstream.path(dir)); copy.value_ptr.* = run_copy.addOutputDirectoryArg(dir); } check.addFileArg(copy.value_ptr.path(b, script[dir.len + 1 ..])); check.addArg("-v"); } check.setEnvironmentVariable("SDL_VIDEODRIVER", "dummy"); check.setEnvironmentVariable("SDL_AUDIODRIVER", "dummy"); check.expectExitCode(0); const check_step = b.step(b.fmt("test-{s}", .{sim.name}), b.fmt("Test the {s} simulator", .{sim.name})); check_step.dependOn(&check.step); test_step.dependOn(&check.step); } }