Something went wrong. Try again.
Open SIMH's computer history simulators, built with the Zig build system
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422// SPDX-FileCopyrightText: 2026 Jeffrey C. Ollie <jeff@ocjtech.us>//// 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); }}