Something went wrong. Try again.
Open SIMH's computer history simulators, built with the Zig build system
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209#!/usr/bin/env python3# SPDX-FileCopyrightText: 2026 Jeffrey C. Ollie <jeff@ocjtech.us>## SPDX-License-Identifier: MIT
"""Write simulators.zon from an Open SIMH checkout.
Upstream generates one CMakeLists.txt per simulator directory from itsmakefile (cmake/generate.py), and those files are regular enough to readback: a handful of `set(NAME ...)` source lists and one `add_simulator(...)`call per simulator. This turns them into the table build.zig compiles from,so that following upstream is a matter of running this again:
python3 tools/generate.py path/to/simh > simulators.zon
Anything conditional in those files (BESM6's font search, PDP-10 panellights, Windows-only resources) is resolved here to the plain, portablevariant; build.zig adds back what it supports."""
import osimport reimport sys
OPTIONS = { "FEATURE_INT64", "FEATURE_FULL64", "BUILDROMS", "FEATURE_VIDEO", "FEATURE_DISPLAY", "NO_INSTALL", "BESM6_SDL_HACK", "USES_AIO",}ONE_ARG = {"TEST", "LABEL", "PKG_FAMILY"}MANY_ARGS = {"DEFINES", "INCLUDES", "SOURCES", "TEST_ARGS"}
# Sources that upstream only compiles in when building with video.DISPLAY_VARS = {"DISPLAY340", "DISPLAYIII", "DISPLAYNG", "DISPLAYVT"}
def directory_variables(root): """The ${XXXD} directory variables from cmake/simh-simulators.cmake.""" variables = {"CMAKE_SOURCE_DIR": ""} text = open(os.path.join(root, "cmake", "simh-simulators.cmake")).read() for name, value in re.findall(r'^set\((\w+)\s+"([^"]*)"\)', text, re.M): variables[name] = expand(value, variables).lstrip("/") return variables
def expand(value, variables): return re.sub(r"\$\{(\w+)\}", lambda m: variables[m.group(1)], value)
def calls(text, name): """Yield the argument tokens of each top-level `name(...)` call.""" for match in re.finditer(r"^\s*" + re.escape(name) + r"\((.*?)\)\s*$", text, re.M | re.S): body = match.group(1) # A call ends at the first line ending in ')'; the non-greedy match # above already stops there, since no argument holds parentheses. yield body.split()
def parse_file(path, root, dirvars): here = os.path.relpath(os.path.dirname(path), root) text = open(path).read() # Comments, and the multi-line strings BESM6 builds its messages from, # would otherwise be read as arguments. text = re.sub(r"#[^\n]*", "", text)
variables = dict(dirvars) variables["CMAKE_CURRENT_SOURCE_DIR"] = here
lists = {} for args in calls(text, "set"): if len(args) > 1 and all(a.endswith(".c") or a.startswith("${") for a in args[1:]): lists[args[0]] = args[1:]
def resolve(token): """A source token, as (path relative to the root, is display-only).""" m = re.fullmatch(r"\$\{(\w+)\}", token) if m and m.group(1) in DISPLAY_VARS: return [(dirvars[m.group(1)], True)] if m and m.group(1) in lists: return [r for t in lists[m.group(1)] for r in resolve(t)] # Anything starting with a variable is rooted at the top of the tree; # a bare path is relative to the directory of the CMakeLists.txt. if token.startswith("${"): token = expand(token, variables) else: token = os.path.join(here, token) return [(os.path.normpath(token), False)]
seen = set() for args in calls(text, "add_simulator"): name, args = args[0], args[1:] # BESM6 declares itself twice, with and without its video panel; the # first is the one that needs no font file found at configure time. if name in seen: continue seen.add(name)
sim = {"name": name, "dir": here} key = None for arg in args: if arg in OPTIONS: sim[arg] = True key = None elif arg in ONE_ARG or arg in MANY_ARGS: key = arg sim.setdefault(key, []) elif key in ONE_ARG: sim[key].append(arg) key = None elif key in MANY_ARGS: sim[key].append(arg) else: raise SystemExit(f"{path}: {name}: unexpected {arg!r}")
sources, display = [], [] for token in sim.get("SOURCES", []): for source, is_display in resolve(token): (display if is_display else sources).append(source) includes = [ os.path.normpath(expand(i, variables) if i.startswith("${") else os.path.join(here, i)) for i in sim.get("INCLUDES", []) ]
test = None if "TEST" in sim: candidate = os.path.join(here, "tests", sim["TEST"][0] + "_test.ini") if os.path.exists(os.path.join(root, candidate)): test = candidate
yield { "name": name, "label": sim.get("LABEL", [name])[0], "sources": sources, "display_sources": display, "includes": includes, "defines": sim.get("DEFINES", []), "int64": bool(sim.get("FEATURE_INT64") or sim.get("FEATURE_FULL64")), "addr64": bool(sim.get("FEATURE_FULL64")), "video": bool(sim.get("FEATURE_VIDEO") or sim.get("FEATURE_DISPLAY")), "display": bool(sim.get("FEATURE_DISPLAY")), "aio": bool(sim.get("USES_AIO")), "test": test, "test_args": [a.strip("\"") for a in sim.get("TEST_ARGS", [])], }
def zon_string(s): return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
def zon_list(items, indent): if not items: return ".{}" pad = " " * indent body = "".join(f"{pad} {zon_string(i)},\n" for i in items) return ".{\n" + body + pad + "}"
def main(): if len(sys.argv) != 2: raise SystemExit(f"usage: {sys.argv[0]} SIMH-CHECKOUT > simulators.zon") root = sys.argv[1] dirvars = directory_variables(root)
paths = [] for dirpath, _, filenames in os.walk(root): rel = os.path.relpath(dirpath, root) if rel == "." or rel.split(os.sep)[0] in {"cmake", "slirp", ".git"} or "unit-tests" in rel: continue if "CMakeLists.txt" in filenames: paths.append(os.path.join(dirpath, "CMakeLists.txt"))
sims = [] for path in sorted(paths): sims.extend(parse_file(path, root, dirvars)) sims.sort(key=lambda s: s["name"])
out = sys.stdout # REUSE-IgnoreStart out.write("// SPDX-FileCopyrightText: 2026 Jeffrey C. Ollie <jeff@ocjtech.us>\n") out.write("//\n// SPDX-License-Identifier: MIT\n\n") # REUSE-IgnoreEnd out.write("// Generated from Open SIMH's CMakeLists.txt files by tools/generate.py.\n") out.write("// Do not edit; regenerate it instead.\n\n") out.write(".{\n") for s in sims: out.write(" .{\n") out.write(f" .name = {zon_string(s['name'])},\n") out.write(f" .label = {zon_string(s['label'])},\n") out.write(f" .sources = {zon_list(s['sources'], 8)},\n") out.write(f" .display_sources = {zon_list(s['display_sources'], 8)},\n") out.write(f" .includes = {zon_list(s['includes'], 8)},\n") out.write(f" .defines = {zon_list(s['defines'], 8)},\n") for flag in ("int64", "addr64", "video", "display", "aio"): out.write(f" .{flag} = {'true' if s[flag] else 'false'},\n") out.write(f" .test_script = {zon_string(s['test']) if s['test'] else 'null'},\n") out.write(f" .test_args = {zon_list(s['test_args'], 8)},\n") out.write(" },\n") out.write("}\n")
if __name__ == "__main__": main()