-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathflatten.py
More file actions
executable file
·158 lines (123 loc) · 5.1 KB
/
Copy pathflatten.py
File metadata and controls
executable file
·158 lines (123 loc) · 5.1 KB
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#!/usr/bin/env python3
# Copyright (c) 2017-2026 Jean-Louis Leroy
# Distributed under the Boost Software License, Version 1.0.
# See accompanying file LICENSE_1_0.txt
# or copy at http://www.boost.org/LICENSE_1_0.txt)
"""Generate flattened copies of the public headers, for Compiler Explorer.
CE can include a header from a URL, but only one file at a time: it does not
resolve the includes inside the file it fetches. Each public header thus becomes
a self-sufficient file, so that a CE example includes exactly what a local
example includes, one line per header:
#include <https://.../boost/openmethod.hpp>
#include <https://.../boost/openmethod/initialize.hpp>
`boost/openmethod.hpp` is the root: it carries its entire closure. Every other
header carries only what the root does not already provide - its `detail/`
headers, `interop/virtual_any.hpp` - and replaces the rest with a check on the
include guard, so that including it without the root fails with a diagnostic
instead of a wall of undeclared identifiers.
"""
import argparse
import re
import subprocess
from pathlib import Path
ROOT = "boost/openmethod.hpp"
DETAIL = "boost/openmethod/detail/"
INCLUDE = re.compile(r"#include <(boost/openmethod(?:/[^>]+)?\.hpp)>")
GUARD = re.compile(r"#ifndef (\w+)$")
class Flattener:
def __init__(self, include_dir, base_url, revision):
self.include_dir = include_dir
self.base_url = base_url.rstrip("/")
self.revision = revision
# What the root already brings in, minus the `detail/` headers, which
# are small and are copied into every header that needs them.
self.provided = {
header
for header in self.dependencies(ROOT)
if not header.startswith(DETAIL)
}
def read(self, header):
return (self.include_dir / header).read_text()
def guard_of(self, header):
lines = self.read(header).splitlines()
for line, next_line in zip(lines, lines[1:]):
if (m := GUARD.match(line)) and next_line == f"#define {m[1]}":
return m[1]
raise SystemExit(f"{header}: no include guard")
def dependencies(self, header, found=None):
found = set() if found is None else found
for dep in INCLUDE.findall(self.read(header)):
if dep not in found:
found.add(dep)
self.dependencies(dep, found)
return found
def headers(self):
return [ROOT] + sorted(
header
for path in (self.include_dir / "boost/openmethod").rglob("*.hpp")
if not (
header := path.relative_to(self.include_dir).as_posix()
).startswith(DETAIL)
)
def write(self, header, output):
if header == ROOT:
note = "// This file is self-contained.\n"
else:
note = f"// #include <{self.base_url}/{ROOT}> first.\n"
output.write(
f"// <{header}>, flattened for Compiler Explorer.\n"
f"{note}"
"//\n"
"// Generated by dev/flatten.py from Boost.OpenMethod"
f" {self.revision}. Do not edit.\n"
"// See https://github.com/boostorg/openmethod\n\n"
)
# The root carries everything; the others require what it provides.
provided = set() if header == ROOT else self.provided
self.copy(header, output, provided, header, set())
def copy(self, header, output, provided, top, done):
if header in done:
return
done.add(header)
for line in self.read(header).splitlines(keepends=True):
if m := INCLUDE.match(line):
dep = m[1]
if dep in provided:
output.write(
f"#ifndef {self.guard_of(dep)}\n"
f'#error "<{top}>:'
f" #include <{self.base_url}/{ROOT}> first\"\n"
"#endif\n"
)
else:
output.write("\n")
self.copy(dep, output, provided, top, done)
output.write("\n")
continue
output.write(line)
def revision():
try:
return subprocess.run(
["git", "describe", "--always", "--dirty"],
capture_output=True,
check=True,
text=True,
).stdout.strip()
except (OSError, subprocess.CalledProcessError):
return "(unknown revision)"
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--include-dir", type=Path, default=Path("include"))
parser.add_argument("--output-dir", type=Path, default=Path("flat"))
parser.add_argument(
"--base-url", default="https://boostorg.github.io/openmethod"
)
args = parser.parse_args()
flattener = Flattener(args.include_dir, args.base_url, revision())
for header in flattener.headers():
path = args.output_dir / header
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as output:
flattener.write(header, output)
print(path)
main()