summaryrefslogtreecommitdiff
path: root/tools/amalgamate.py
blob: 79f011a40e8fc8aeeb7c9ba1ecc12ad16ccf1ca5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#!/usr/bin/env python3
"""Write the wsap_vtcore core as one wsap_vtcore.c and one wsap_vtcore.h.

wsap_vtcore.h joins the public headers. wsap_vtcore.c joins every source
file and puts each private header in at its first include. A host compiles
wsap_vtcore.c once, with any C11 compiler, and links libm and the thread
library.

    amalgamate.py WSAP_VTCORE_DIR OUT_DIR [DEPFILE]

WSAP_VTCORE_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 = ("wsap_vtcore_types.h", "wsap_vtcore_input.h", "wsap_vtcore.h")


class Joiner:
    def __init__(self, root: Path):
        self.root = root
        # The sources find the public headers without the wsap_vtcore/ prefix.
        self.search = (root / "src", root / "include" / "wsap_vtcore")
        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" / "wsap_vtcore"
    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 / "wsap_vtcore.h", text)
    (out_dir / "wsap_vtcore.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 "wsap_vtcore.h"\n']
    # The public headers are in wsap_vtcore.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 / "wsap_vtcore.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 / 'wsap_vtcore.c'}: {deps}\n")
    return 0


if __name__ == "__main__":
    sys.exit(main())