summaryrefslogtreecommitdiff
path: root/tools/amalgamate.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/amalgamate.py')
-rw-r--r--tools/amalgamate.py94
1 files changed, 94 insertions, 0 deletions
diff --git a/tools/amalgamate.py b/tools/amalgamate.py
new file mode 100644
index 0000000..f2cdf3b
--- /dev/null
+++ b/tools/amalgamate.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+"""Write the term core as one term.c and one term.h.
+
+term.h joins the public headers. term.c joins every source file and puts
+each private header in at its first include. A host compiles term.c once,
+with any C11 compiler, and links libm.
+
+ amalgamate.py TERM_DIR OUT_DIR [DEPFILE]
+
+TERM_DIR is the directory with src/ and include/. DEPFILE lists every file
+read, for the build system.
+"""
+
+import re
+import sys
+from pathlib import Path
+
+INCLUDE = re.compile(r'^\s*#\s*include\s+"([^"]+)"')
+PUBLIC = ("term_types.h", "term_input.h", "term.h")
+
+
+class Joiner:
+ def __init__(self, root: Path):
+ self.root = root
+ # The sources find the public headers without the term/ prefix.
+ self.search = (root / "src", root / "include" / "term")
+ self.seen: set[Path] = set()
+
+ def resolve(self, name: str, base: Path) -> Path:
+ for d in (base,) + self.search:
+ p = (d / name).resolve()
+ if p.is_file():
+ return p
+ raise SystemExit(f"{base}: cannot find {name}")
+
+ def emit(self, path: Path, out: list[str]) -> None:
+ """Copy path to out. Replace each local include with the file,
+ the first time only."""
+ self.seen.add(path)
+ rel = path.relative_to(self.root)
+ out.append(f"/* ---- {rel} ---- */\n")
+ for line in path.read_text().splitlines(keepends=True):
+ m = INCLUDE.match(line)
+ if not m:
+ out.append(line)
+ continue
+ dep = self.resolve(m.group(1), path.parent)
+ if dep not in self.seen:
+ self.emit(dep, out)
+ out.append(f"/* ---- end {rel} ---- */\n")
+
+
+def sources(src: Path) -> list[Path]:
+ files = sorted(src.glob("*.c")) + sorted((src / "osc").glob("*.c"))
+ return [f.resolve() for f in files]
+
+
+def main() -> int:
+ if len(sys.argv) not in (3, 4):
+ print(__doc__, file=sys.stderr)
+ return 2
+ root = Path(sys.argv[1]).resolve()
+ public = root / "include" / "term"
+ out_dir = Path(sys.argv[2])
+ out_dir.mkdir(parents=True, exist_ok=True)
+ banner = "/* Generated by tools/amalgamate.py. Do not edit. */\n"
+
+ header = Joiner(root)
+ text = [banner]
+ header.emit(public / "term.h", text)
+ (out_dir / "term.h").write_text("".join(text))
+
+ body = Joiner(root)
+ text = [banner,
+ "/* The private headers have helpers that only some platforms or\n"
+ " * callers outside the core use. Clang warns about them here. */\n"
+ "#if defined(__clang__)\n"
+ '#pragma clang diagnostic ignored "-Wunused-function"\n'
+ "#endif\n",
+ '#include "term.h"\n']
+ # The public headers are in term.h, so the sources skip them.
+ body.seen.update((public / name).resolve() for name in PUBLIC)
+ for src in sources(root / "src"):
+ body.emit(src, text)
+ (out_dir / "term.c").write_text("".join(text))
+ if len(sys.argv) == 4:
+ read = sorted(header.seen | body.seen)
+ deps = " ".join(str(p).replace(" ", "\\ ") for p in read)
+ Path(sys.argv[3]).write_text(f"{out_dir / 'term.c'}: {deps}\n")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())