gnbuild 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gnbuild/__init__.py +1 -0
- gnbuild/build/__init__.py +1 -0
- gnbuild/build/cmake.py +562 -0
- gnbuild/build/compdb.py +35 -0
- gnbuild/build/postgen.py +477 -0
- gnbuild/cli.py +213 -0
- gnbuild/gn/BUILD.gn +14 -0
- gnbuild/gn/BUILDCONFIG.gn +117 -0
- gnbuild/gn/build/cmake.gni +197 -0
- gnbuild/gn/profiles/debug.gni +1 -0
- gnbuild/gn/profiles/release.gni +1 -0
- gnbuild/gn/toolchain.gni +77 -0
- gnbuild/gn/toolchains/BUILD.gn +9 -0
- gnbuild/gn/toolchains/clang.gni +7 -0
- gnbuild/gn/toolchains/emscripten.gni +7 -0
- gnbuild/gn/toolchains/gcc.gni +7 -0
- gnbuild/gn/toolchains/host.gni +7 -0
- gnbuild/launcher.py +9 -0
- gnbuild/package/__init__.py +1 -0
- gnbuild/package/deps.py +145 -0
- gnbuild/package/desc.py +271 -0
- gnbuild/package/fetch.py +459 -0
- gnbuild/package/gen.py +323 -0
- gnbuild/package/pkgs.py +100 -0
- gnbuild/package/repos.py +189 -0
- gnbuild/project.py +84 -0
- gnbuild/util.py +77 -0
- gnbuild-0.1.0.data/scripts/gnb +7 -0
- gnbuild-0.1.0.dist-info/METADATA +661 -0
- gnbuild-0.1.0.dist-info/RECORD +33 -0
- gnbuild-0.1.0.dist-info/WHEEL +5 -0
- gnbuild-0.1.0.dist-info/licenses/LICENSE +21 -0
- gnbuild-0.1.0.dist-info/top_level.txt +1 -0
gnbuild/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from __future__ import annotations
|
gnbuild/build/cmake.py
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Sequence, Tuple
|
|
15
|
+
|
|
16
|
+
from gnbuild.build import compdb
|
|
17
|
+
from gnbuild.util import LAUNCHER, GnbError, find_tool, ninja_escape, ninja_paths, ninja_unescape, run, write_if_changed
|
|
18
|
+
|
|
19
|
+
# Compiler, source location or fetched content changes require a clean CMake cache.
|
|
20
|
+
IDENTITY_KEYS = ("cc", "cxx", "ar", "triple", "system", "sysroot", "clang_target", "source_dir", "pkg_source_dir", "source_identity")
|
|
21
|
+
LANGS = (("C", "cflags_c"), ("CXX", "cflags_cc"), ("ASM", "asmflags"))
|
|
22
|
+
# Compilers and target metadata come from toolchain descriptors; flags and launchers come from the profile.
|
|
23
|
+
FORBIDDEN_OPTION_PATTERNS = ("CMAKE_*_FLAGS*", "CMAKE_*_COMPILER*", "CMAKE_AR", "CMAKE_BUILD_TYPE",
|
|
24
|
+
"CMAKE_TOOLCHAIN_FILE", "CMAKE_SYSTEM_*", "CMAKE_SYSROOT*", "CMAKE_*_LAUNCHER")
|
|
25
|
+
LIBRARY_KINDS = ("STATIC_LIBRARY", "SHARED_LIBRARY")
|
|
26
|
+
|
|
27
|
+
# CMake evaluates the public interface after all subdirectories and target relationships exist.
|
|
28
|
+
INTERFACE_HOOK = r'''function(gnb_export_interfaces directory)
|
|
29
|
+
if(directory STREQUAL CMAKE_SOURCE_DIR)
|
|
30
|
+
get_property(languages GLOBAL PROPERTY ENABLED_LANGUAGES)
|
|
31
|
+
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/gnb_interfaces/languages.txt" CONTENT "${languages}")
|
|
32
|
+
endif()
|
|
33
|
+
get_property(targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS)
|
|
34
|
+
foreach(target IN LISTS targets)
|
|
35
|
+
get_target_property(kind "${target}" TYPE)
|
|
36
|
+
if(kind STREQUAL "STATIC_LIBRARY" OR kind STREQUAL "SHARED_LIBRARY")
|
|
37
|
+
string(SHA256 key "${target}")
|
|
38
|
+
foreach(property IN ITEMS INTERFACE_INCLUDE_DIRECTORIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES)
|
|
39
|
+
file(GENERATE OUTPUT "${CMAKE_BINARY_DIR}/gnb_interfaces/${key}/${property}-$<COMPILE_LANGUAGE>.txt"
|
|
40
|
+
CONTENT "$<JOIN:$<TARGET_PROPERTY:${target},${property}>,\n>" TARGET "${target}")
|
|
41
|
+
endforeach()
|
|
42
|
+
endif()
|
|
43
|
+
endforeach()
|
|
44
|
+
get_property(children DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES)
|
|
45
|
+
foreach(child IN LISTS children)
|
|
46
|
+
gnb_export_interfaces("${child}")
|
|
47
|
+
endforeach()
|
|
48
|
+
endfunction()
|
|
49
|
+
get_property(scheduled GLOBAL PROPERTY gnb_interfaces_scheduled)
|
|
50
|
+
if(NOT scheduled)
|
|
51
|
+
set_property(GLOBAL PROPERTY gnb_interfaces_scheduled TRUE)
|
|
52
|
+
cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL gnb_export_interfaces "${CMAKE_SOURCE_DIR}")
|
|
53
|
+
endif()
|
|
54
|
+
'''
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_options(options: Dict[str, Any]) -> None:
|
|
58
|
+
for key in options:
|
|
59
|
+
if any(fnmatch.fnmatchcase(key, pat) for pat in FORBIDDEN_OPTION_PATTERNS):
|
|
60
|
+
raise GnbError(f"package option {key} sets build intent; put flags in profile cflags or ldflags, "
|
|
61
|
+
"and compiler/target/sysroot fields in the toolchain descriptor instead")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def check_build_dir(path: Path) -> None:
|
|
65
|
+
# shortcut: quoted CMake build paths are unsupported; revisit after verifying a fixed stock release.
|
|
66
|
+
if '"' in str(path):
|
|
67
|
+
raise GnbError(f"CMake build directory contains a double quote: {path}; "
|
|
68
|
+
"use an output directory and profile name without double quotes")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def dep_var_name(name: str) -> str:
|
|
72
|
+
# GNB_DEP_<NAME>_* for a dependency package: uppercase, every non-alphanumeric character replaced by _
|
|
73
|
+
return re.sub(r"[^A-Za-z0-9]", "_", name).upper()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def toolchain_file_text(pkg: dict) -> str:
|
|
77
|
+
lines = [f'set(CMAKE_C_COMPILER "{pkg["cc"]}")',
|
|
78
|
+
f'set(CMAKE_CXX_COMPILER "{pkg["cxx"]}")',
|
|
79
|
+
f'set(CMAKE_ASM_COMPILER "{pkg["cc"]}")',
|
|
80
|
+
f'set(CMAKE_AR "{pkg["ar"]}")']
|
|
81
|
+
if pkg["system"] == "Emscripten":
|
|
82
|
+
sdk = Path(find_tool(pkg["cc"])).resolve().parent / "cmake/Modules/Platform/Emscripten.cmake"
|
|
83
|
+
if not sdk.is_file():
|
|
84
|
+
raise GnbError(f"Emscripten SDK toolchain file missing: {sdk}; install a complete SDK and add emcc to PATH")
|
|
85
|
+
sdk_path = sdk.as_posix().replace('"', '\\"')
|
|
86
|
+
lines.insert(0, f'include("{sdk_path}")')
|
|
87
|
+
if pkg["system"]:
|
|
88
|
+
lines.append(f'set(CMAKE_SYSTEM_NAME {pkg["system"]})')
|
|
89
|
+
if pkg["clang_target"]:
|
|
90
|
+
lines += [f'set(CMAKE_{lang}_COMPILER_TARGET {pkg["clang_target"]})' for lang, _ in LANGS]
|
|
91
|
+
if pkg["sysroot"]:
|
|
92
|
+
lines.append(f'set(CMAKE_SYSROOT "{pkg["sysroot"]}")')
|
|
93
|
+
if pkg["system"] == "Generic":
|
|
94
|
+
# try_compile cannot link in a bare-metal environment
|
|
95
|
+
lines.append("set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)")
|
|
96
|
+
return "\n".join(lines) + "\n"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _read_interface(out: Path, build_dir: Path, name: str) -> dict:
|
|
100
|
+
directory = build_dir / "gnb_interfaces"
|
|
101
|
+
key = hashlib.sha256(name.encode()).hexdigest()
|
|
102
|
+
variants = {}
|
|
103
|
+
try:
|
|
104
|
+
for language in (directory / "languages.txt").read_text().split(";"):
|
|
105
|
+
paths = (directory / key / f"INTERFACE_INCLUDE_DIRECTORIES-{language}.txt").read_text().splitlines()
|
|
106
|
+
system = set((directory / key / f"INTERFACE_SYSTEM_INCLUDE_DIRECTORIES-{language}.txt").read_text().splitlines())
|
|
107
|
+
variants[language] = {
|
|
108
|
+
"include_dirs": list(dict.fromkeys(p for p in paths if p not in system)),
|
|
109
|
+
"system_include_dirs": list(dict.fromkeys(p for p in paths if p in system)),
|
|
110
|
+
}
|
|
111
|
+
except OSError as e:
|
|
112
|
+
raise GnbError(f"CMake target {name} did not export its public headers; reconfigure {build_dir}: {e}") from e
|
|
113
|
+
first = next(iter(variants.values()))
|
|
114
|
+
uniform = all(value == first for value in variants.values())
|
|
115
|
+
common = {field: [p for p in paths if all(p in value[field] for value in variants.values())]
|
|
116
|
+
for field, paths in first.items()}
|
|
117
|
+
exported = {}
|
|
118
|
+
for field in first:
|
|
119
|
+
exported[field] = (first[field] if uniform else
|
|
120
|
+
[f"$<$<COMPILE_LANGUAGE:{language}>:{p}>" for language, value in variants.items() for p in value[field]] +
|
|
121
|
+
[f"$<$<NOT:$<COMPILE_LANGUAGE:{','.join(variants)}>>:{p}>" for p in common[field]])
|
|
122
|
+
for language, field in LANGS:
|
|
123
|
+
value = variants.get(language, first if uniform else common)
|
|
124
|
+
exported[f"include_{field}"] = ([f"-I{Path(os.path.relpath(p, out)).as_posix()}" for p in value["include_dirs"]] +
|
|
125
|
+
[f"-isystem{Path(os.path.relpath(p, out)).as_posix()}" for p in value["system_include_dirs"]])
|
|
126
|
+
return exported
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def read_exports(out: Path, build_dir: Path) -> dict:
|
|
130
|
+
"""Read CMake's codemodel-v2 reply, retaining native names and actual artifacts."""
|
|
131
|
+
reply = build_dir / ".cmake/api/v1/reply"
|
|
132
|
+
indexes = sorted(reply.glob("index-*.json"))
|
|
133
|
+
if not indexes:
|
|
134
|
+
raise GnbError(f"CMake did not publish a File API reply for {build_dir}")
|
|
135
|
+
index = json.loads(indexes[-1].read_text())
|
|
136
|
+
response = index.get("reply", {}).get("codemodel-v2", {})
|
|
137
|
+
if "jsonFile" not in response:
|
|
138
|
+
raise GnbError(f"CMake codemodel-v2 reply unavailable for {build_dir}: {response}")
|
|
139
|
+
model = json.loads((reply / response["jsonFile"]).read_text())
|
|
140
|
+
version = model.get("version", {})
|
|
141
|
+
if version.get("major") != 2 or version.get("minor", 0) < 9:
|
|
142
|
+
raise GnbError(f"CMake codemodel 2.9 or newer (CMake 4.2+) is required for {build_dir}")
|
|
143
|
+
configurations = model.get("configurations", [])
|
|
144
|
+
if len(configurations) != 1:
|
|
145
|
+
raise GnbError(f"expected one Ninja CMake configuration for {build_dir}")
|
|
146
|
+
records = configurations[0]["targets"]
|
|
147
|
+
native = {record["id"]: json.loads((reply / record["jsonFile"]).read_text())
|
|
148
|
+
for record in [*records, *configurations[0].get("abstractTargets", [])]}
|
|
149
|
+
artifacts, system_libs, link_flags, links = {}, {}, {}, {}
|
|
150
|
+
for identity, target in native.items():
|
|
151
|
+
kind = target["type"]
|
|
152
|
+
paths = []
|
|
153
|
+
if kind in LIBRARY_KINDS:
|
|
154
|
+
for artifact in target.get("artifacts", []):
|
|
155
|
+
path = Path(artifact["path"])
|
|
156
|
+
if not path.is_absolute():
|
|
157
|
+
path = build_dir / path
|
|
158
|
+
if kind == "SHARED_LIBRARY" and path.suffix.lower() == ".dll":
|
|
159
|
+
raise GnbError(f"CMake native target {target['name']} exports a DLL runtime artifact; "
|
|
160
|
+
"DLL/import-library selection is unsupported")
|
|
161
|
+
paths.append(Path(os.path.relpath(path, out)).as_posix())
|
|
162
|
+
if not paths and not target.get("imported"):
|
|
163
|
+
raise GnbError(f"CMake native library target {target['name']} has no File API artifacts")
|
|
164
|
+
artifacts[identity], system_libs[identity], link_flags[identity] = paths, [], []
|
|
165
|
+
links[identity] = []
|
|
166
|
+
|
|
167
|
+
for identity, target in native.items():
|
|
168
|
+
items = list(target.get("interfaceLinkLibraries", []))
|
|
169
|
+
if target["type"] == "STATIC_LIBRARY":
|
|
170
|
+
items = [*target.get("linkLibraries", []), *items]
|
|
171
|
+
for item in items:
|
|
172
|
+
if "id" in item:
|
|
173
|
+
child = item["id"]
|
|
174
|
+
if child not in native:
|
|
175
|
+
raise GnbError(f"CMake native target {target['name']} links unknown native target {child}")
|
|
176
|
+
else:
|
|
177
|
+
fragment = item["fragment"]
|
|
178
|
+
if fragment.startswith("-"):
|
|
179
|
+
child = ("flag", fragment)
|
|
180
|
+
link_flags[child] = [fragment]
|
|
181
|
+
artifacts[child], system_libs[child] = [], []
|
|
182
|
+
elif Path(fragment).is_absolute():
|
|
183
|
+
path = Path(os.path.relpath(fragment, out)).as_posix()
|
|
184
|
+
child = ("path", path)
|
|
185
|
+
artifacts[child] = [path]
|
|
186
|
+
system_libs[child], link_flags[child] = [], []
|
|
187
|
+
else:
|
|
188
|
+
if (not fragment or "$<" in fragment or fragment.startswith("<")
|
|
189
|
+
or "/" in fragment or "\\" in fragment or any(c.isspace() for c in fragment)):
|
|
190
|
+
raise GnbError(f"CMake native target {target['name']} has unsupported link fragment {fragment!r}")
|
|
191
|
+
child = ("library", fragment)
|
|
192
|
+
system_libs[child] = [fragment]
|
|
193
|
+
artifacts[child], link_flags[child] = [], []
|
|
194
|
+
links[child] = []
|
|
195
|
+
if child not in links[identity]:
|
|
196
|
+
links[identity].append(child)
|
|
197
|
+
|
|
198
|
+
targets = []
|
|
199
|
+
for record in records:
|
|
200
|
+
identity = record["id"]
|
|
201
|
+
target = native[identity]
|
|
202
|
+
exported = {"name": target["name"], "kind": target["type"], "paths": [],
|
|
203
|
+
"rescan": False, "system_libs": [], "link_flags": []}
|
|
204
|
+
if target["type"] in LIBRARY_KINDS and not target.get("imported"):
|
|
205
|
+
exported.update(_read_interface(out, build_dir, target["name"]))
|
|
206
|
+
seen, active, order = set(), set(), []
|
|
207
|
+
|
|
208
|
+
def visit(identity):
|
|
209
|
+
if identity in active:
|
|
210
|
+
exported["rescan"] = True
|
|
211
|
+
if identity in seen:
|
|
212
|
+
return
|
|
213
|
+
seen.add(identity)
|
|
214
|
+
active.add(identity)
|
|
215
|
+
for child in reversed(links[identity]):
|
|
216
|
+
visit(child)
|
|
217
|
+
active.remove(identity)
|
|
218
|
+
order.append(identity)
|
|
219
|
+
|
|
220
|
+
# Consumers precede providers; cycles are rescanned as one complete link group.
|
|
221
|
+
visit(identity)
|
|
222
|
+
for member in reversed(order):
|
|
223
|
+
dependency = native.get(member)
|
|
224
|
+
if dependency and dependency["type"] in LIBRARY_KINDS and not artifacts[member]:
|
|
225
|
+
raise GnbError(f"CMake linked target {dependency['name']} has no File API artifacts; "
|
|
226
|
+
"imported library artifact selection is unsupported")
|
|
227
|
+
if dependency and dependency["type"] not in (*LIBRARY_KINDS, "INTERFACE_LIBRARY", "OBJECT_LIBRARY"):
|
|
228
|
+
raise GnbError(f"CMake linked target {dependency['name']} ({dependency['type']}) is unsupported")
|
|
229
|
+
exported["paths"].extend(artifacts[member])
|
|
230
|
+
exported["system_libs"].extend(system_libs[member])
|
|
231
|
+
exported["link_flags"].extend(link_flags[member])
|
|
232
|
+
for field in ("paths", "system_libs", "link_flags"):
|
|
233
|
+
exported[field] = list(dict.fromkeys(reversed(exported[field])))[::-1]
|
|
234
|
+
targets.append(exported)
|
|
235
|
+
return {"index": indexes[-1].name, "ready": True,
|
|
236
|
+
"targets": sorted(targets, key=lambda target: target["name"])}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def publish_exports(path: Path, model: dict) -> None:
|
|
240
|
+
text = json.dumps(model, sort_keys=True)
|
|
241
|
+
if path.is_file() and path.read_text() == text:
|
|
242
|
+
return
|
|
243
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
fd, temporary = tempfile.mkstemp(prefix=".gnb_exports-", dir=path.parent)
|
|
245
|
+
try:
|
|
246
|
+
with os.fdopen(fd, "w") as stream:
|
|
247
|
+
stream.write(text)
|
|
248
|
+
os.replace(temporary, path)
|
|
249
|
+
finally:
|
|
250
|
+
if os.path.exists(temporary):
|
|
251
|
+
os.unlink(temporary)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def ensure_exports(path: Path) -> None:
|
|
255
|
+
# GN must validate selections against the current native configuration, not a stale target list.
|
|
256
|
+
if path.is_file():
|
|
257
|
+
model = json.loads(path.read_text())
|
|
258
|
+
if not model["ready"]:
|
|
259
|
+
return
|
|
260
|
+
bdir = path.parent
|
|
261
|
+
out = bdir.parents[2]
|
|
262
|
+
manifest = bdir / "gnb_package.json"
|
|
263
|
+
indexes = sorted((bdir / ".cmake/api/v1/reply").glob("index-*.json"))
|
|
264
|
+
inputs_file = bdir / "gnb_inputs.txt"
|
|
265
|
+
if manifest.is_file() and indexes and inputs_file.is_file():
|
|
266
|
+
pkg = json.loads(manifest.read_text())
|
|
267
|
+
if (model.get("index") == indexes[-1].name and "profile_file" in pkg
|
|
268
|
+
and "toolchain_file" in pkg):
|
|
269
|
+
configured = indexes[-1].stat().st_mtime_ns
|
|
270
|
+
inputs = [Path(name) for name in inputs_file.read_text().splitlines()]
|
|
271
|
+
inputs += [Path(__file__).resolve(), manifest, Path(pkg["profile_file"]),
|
|
272
|
+
Path(pkg["toolchain_file"]), out / "args.gn",
|
|
273
|
+
out / "gnb_pkgs" / bdir.name / "BUILD.gn",
|
|
274
|
+
out / "gnb_pkgs" / bdir.name / "options.json", bdir / "gnb_gn_libs.json"]
|
|
275
|
+
if all(item.exists() and item.stat().st_mtime_ns <= configured for item in inputs):
|
|
276
|
+
return
|
|
277
|
+
publish_exports(path, {"ready": False, "targets": []})
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def selected_targets(model: dict, names: list, package: str) -> list:
|
|
281
|
+
if not model.get("ready"):
|
|
282
|
+
raise GnbError(f"CMake exports for dependency {package} are not ready")
|
|
283
|
+
targets = {target["name"]: target for target in model["targets"]}
|
|
284
|
+
selected = []
|
|
285
|
+
for name in names:
|
|
286
|
+
target = targets.get(name)
|
|
287
|
+
if target is None or target["kind"] not in LIBRARY_KINDS:
|
|
288
|
+
available = ", ".join(f"{t['name']} ({t['kind']})" for t in model["targets"])
|
|
289
|
+
actual = f" ({target['kind']})" if target else ""
|
|
290
|
+
raise GnbError(f"CMake dependency {package} native target {name}{actual} is not a linkable library; "
|
|
291
|
+
f"available targets: {available}")
|
|
292
|
+
selected.append(target)
|
|
293
|
+
return selected
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def normalize_artifact_nodes(text: str, out: Path, model: dict, dependencies: Sequence[str] = ()) -> str:
|
|
297
|
+
# Ninja distinguishes absolute and relative nodes even when they name the same native artifact.
|
|
298
|
+
artifacts = [path for target in model["targets"] for path in target["paths"]]
|
|
299
|
+
paths = {os.path.abspath(out / path): path for path in [*artifacts, *dependencies]}
|
|
300
|
+
|
|
301
|
+
def replace(token: re.Match) -> str:
|
|
302
|
+
original = token.group(0)
|
|
303
|
+
relative = paths.get(ninja_unescape(original))
|
|
304
|
+
return ninja_escape(relative) if relative is not None else original
|
|
305
|
+
|
|
306
|
+
lines = []
|
|
307
|
+
for line in text.split("\n"):
|
|
308
|
+
if line.startswith("build "):
|
|
309
|
+
outputs, separator, inputs = line[6:].partition(": ")
|
|
310
|
+
outputs = re.sub(r"(?:\$[ $:]|[^ ])+", replace, outputs)
|
|
311
|
+
inputs = re.sub(r"(?:\$[ $:]|[^ ])+", replace, inputs)
|
|
312
|
+
line = "build " + outputs + separator + inputs
|
|
313
|
+
lines.append(line)
|
|
314
|
+
return "\n".join(lines)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def fix_package_ninja(text: str, prefix: str, dep_libs: List[str], config_inputs: Sequence[str] = ()) -> Tuple[str, List[str]]:
|
|
318
|
+
# CMake does not prefix the output of this phony target, so it would collide across packages/instances
|
|
319
|
+
text = re.sub(r"(?<![\w/])cmake_object_order_depends_target_",
|
|
320
|
+
prefix + "/cmake_object_order_depends_target_", text)
|
|
321
|
+
order_edge = f"build {prefix}/cmake_object_order_depends_target_"
|
|
322
|
+
pkg_json = f"{prefix}/gnb_package.json"
|
|
323
|
+
# Configuration inputs: a change to either reconfigures this package exactly once
|
|
324
|
+
pkg_inputs = " ".join(ninja_escape(p) for p in [pkg_json, f"{prefix}/gnb_gn_libs.json", *config_inputs])
|
|
325
|
+
keep, inputs = [], []
|
|
326
|
+
rerun = False
|
|
327
|
+
for ln in text.split("\n"):
|
|
328
|
+
if ln.startswith("build "):
|
|
329
|
+
rerun = ln.startswith(f"build {prefix}/build.ninja ") and ": RERUN_CMAKE" in ln
|
|
330
|
+
if rerun and ln.startswith(" pool = "):
|
|
331
|
+
ln = " pool = gnb_cmake_configure_pool"
|
|
332
|
+
if ln.startswith("default "):
|
|
333
|
+
continue # CMake's default would build every package library; only gn-selected libraries get built
|
|
334
|
+
if ln.startswith("build /") and ln.endswith(": phony"):
|
|
335
|
+
# Absolute paths (CMake modules, sources) are shared by all packages; move them to the merged file to dedupe; prefixed in-package files stay put
|
|
336
|
+
paths = ninja_paths(ln[len("build "):-len(": phony")])
|
|
337
|
+
inputs += [p for p in paths if p.startswith("/")]
|
|
338
|
+
local = [p for p in paths if not p.startswith("/")]
|
|
339
|
+
if local:
|
|
340
|
+
keep.append("build " + " ".join(ninja_escape(p) for p in local) + ": phony")
|
|
341
|
+
continue
|
|
342
|
+
if ln.startswith(f"build {prefix}/build.ninja ") and ": RERUN_CMAKE |" in ln and pkg_json not in ln:
|
|
343
|
+
ln = ln.replace(": RERUN_CMAKE |", f": RERUN_CMAKE | {pkg_inputs}", 1)
|
|
344
|
+
if dep_libs and ln.startswith(order_edge):
|
|
345
|
+
# Every compile of this package waits for the dependency packages' libraries (and headers they generate)
|
|
346
|
+
main, _, order = ln.partition(" || ")
|
|
347
|
+
have = order.split()
|
|
348
|
+
ln = f"{main} || " + " ".join(have + [ninja_escape(lib) for lib in dep_libs if ninja_escape(lib) not in have])
|
|
349
|
+
keep.append(ln)
|
|
350
|
+
return "\n".join(keep), inputs
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def fix_link_depfile(out: Path, prefix: str, depfile: Path) -> None:
|
|
354
|
+
bdir = out / prefix
|
|
355
|
+
model = json.loads((bdir / "gnb_exports.json").read_text())
|
|
356
|
+
paths = [path for target in model["targets"] for path in target["paths"]]
|
|
357
|
+
pkg = json.loads((bdir / "gnb_package.json").read_text())
|
|
358
|
+
gn_exports = json.loads((bdir / "gnb_gn_libs.json").read_text())
|
|
359
|
+
for dep in pkg["deps"]:
|
|
360
|
+
paths += [path for label in dep["gn_libs"] for path in gn_exports[label]["paths"]]
|
|
361
|
+
if dep["cmake_prefix"]:
|
|
362
|
+
child = json.loads((out / dep["cmake_prefix"] / "gnb_exports.json").read_text())
|
|
363
|
+
paths += [path for target in selected_targets(child, dep["targets"], dep["name"]) for path in target["paths"]]
|
|
364
|
+
|
|
365
|
+
def escape(path: str) -> str:
|
|
366
|
+
return path.replace("\\", "\\\\").replace("$", "$$").replace("#", "\\#").replace(" ", "\\ ").replace(":", "\\:")
|
|
367
|
+
|
|
368
|
+
replacements = {}
|
|
369
|
+
for path in paths:
|
|
370
|
+
for spelling in (path, os.path.abspath(out / path)):
|
|
371
|
+
replacements[spelling] = replacements[escape(spelling)] = escape(path)
|
|
372
|
+
if replacements:
|
|
373
|
+
# GNU ld leaves spaces unescaped; keep known artifacts as single, out-relative Ninja dependencies.
|
|
374
|
+
pattern = r"(?<!\S)(?:" + "|".join(re.escape(key) for key in sorted(replacements, key=len, reverse=True)) + r")(?=\s|:|$)"
|
|
375
|
+
text = re.sub(pattern, lambda match: replacements[match[0]], depfile.read_text())
|
|
376
|
+
write_if_changed(depfile, text)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def fix_rules_ninja(text: str, configure_cmd: str, depfile_cmd: str) -> str:
|
|
380
|
+
def replace(rule: re.Match) -> str:
|
|
381
|
+
lines = [ln for ln in rule.group(0).splitlines() if not ln.startswith(" pool = ")]
|
|
382
|
+
lines[1] = " command = " + configure_cmd
|
|
383
|
+
lines.append(" pool = gnb_cmake_configure_pool")
|
|
384
|
+
return "\n".join(lines) + "\n"
|
|
385
|
+
|
|
386
|
+
text = re.sub(r"(?m)^rule RERUN_CMAKE\n(?: [^\n]*(?:\n|$))*", replace, text)
|
|
387
|
+
|
|
388
|
+
def fix_link(rule: re.Match) -> str:
|
|
389
|
+
lines = rule[0].splitlines()
|
|
390
|
+
if " depfile = $DEP_FILE" in lines:
|
|
391
|
+
for index, line in enumerate(lines):
|
|
392
|
+
if line.startswith(" command = ") and not line.endswith(" && " + depfile_cmd):
|
|
393
|
+
lines[index] = line + " && " + depfile_cmd
|
|
394
|
+
return "\n".join(lines) + "\n"
|
|
395
|
+
|
|
396
|
+
return re.sub(r"(?m)^rule \S*LINKER\S*\n(?: [^\n]*(?:\n|$))*", fix_link, text)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def configure_cmd(out: Path, prefix: str) -> str:
|
|
400
|
+
return shlex.join([sys.executable, str(LAUNCHER), "_cmake_configure", str(out), prefix]).replace("$", "$$")
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def depfile_cmd(out: Path, prefix: str) -> str:
|
|
404
|
+
command = shlex.join([sys.executable, str(LAUNCHER), "_cmake_depfile", str(out), prefix]).replace("$", "$$")
|
|
405
|
+
return command + ' "$DEP_FILE"'
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def stub_text(out: Path, prefix: str) -> str:
|
|
409
|
+
pkg = json.loads((out / prefix / "gnb_package.json").read_text())
|
|
410
|
+
inputs = [f"{prefix}/gnb_package.json", f"{prefix}/gnb_gn_libs.json"]
|
|
411
|
+
inputs += [f"{dep['cmake_prefix']}/build.ninja" for dep in pkg["deps"] if dep["cmake_prefix"]]
|
|
412
|
+
inputs += [f"{header['cmake_prefix']}/build.ninja" for dep in pkg["deps"] for header in dep["header_deps"]]
|
|
413
|
+
return (
|
|
414
|
+
"rule gnb_cmake_configure\n"
|
|
415
|
+
f" command = {configure_cmd(out, prefix)}\n"
|
|
416
|
+
f" description = CMAKE CONFIGURE {prefix}\n"
|
|
417
|
+
" generator = 1\n"
|
|
418
|
+
" pool = gnb_cmake_configure_pool\n"
|
|
419
|
+
f"build {prefix}/build.ninja: gnb_cmake_configure | " + " ".join(ninja_escape(p) for p in inputs) + "\n"
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def write_stub(out: Path, prefix: str) -> None:
|
|
424
|
+
sub = out / prefix / "build.ninja"
|
|
425
|
+
write_if_changed(sub, stub_text(out, prefix))
|
|
426
|
+
os.utime(sub, (0, 0)) # make ninja consider it stale so configuration runs when the dependency graph loads
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def write_inputs(out: Path, prefixes: List[str]) -> None:
|
|
430
|
+
inputs = set()
|
|
431
|
+
for prefix in prefixes:
|
|
432
|
+
f = out / prefix / "gnb_inputs.txt"
|
|
433
|
+
if f.is_file():
|
|
434
|
+
inputs.update(f.read_text().splitlines())
|
|
435
|
+
write_if_changed(out / "gnb_cmake_inputs.ninja",
|
|
436
|
+
"build " + " ".join(ninja_escape(p) for p in sorted(inputs)) + ": phony\n" if inputs else "")
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def configure(out: Path, prefix: str) -> None:
|
|
440
|
+
out = out.absolute()
|
|
441
|
+
bdir = out / prefix
|
|
442
|
+
check_build_dir(bdir)
|
|
443
|
+
pkg = json.loads((bdir / "gnb_package.json").read_text())
|
|
444
|
+
check_options(pkg["options"])
|
|
445
|
+
cmake_tool = find_tool("cmake")
|
|
446
|
+
version = subprocess.run([cmake_tool, "--version"], text=True, capture_output=True)
|
|
447
|
+
match = re.search(r"^cmake version (\d+)\.(\d+)", version.stdout)
|
|
448
|
+
if version.returncode != 0 or not match or tuple(map(int, match.groups())) < (4, 2):
|
|
449
|
+
raise GnbError("native CMake package exports require CMake >= 4.2; install it and add it to PATH")
|
|
450
|
+
|
|
451
|
+
if pkg["system"] == "Emscripten":
|
|
452
|
+
pkg = dict(pkg)
|
|
453
|
+
for key in ("cc", "cxx", "ar"):
|
|
454
|
+
pkg[key] = os.path.abspath(find_tool(pkg[key]))
|
|
455
|
+
toolchain_text = toolchain_file_text(pkg) # Validate the SDK before clearing an existing cache.
|
|
456
|
+
identity_fields = {k: pkg[k] for k in IDENTITY_KEYS}
|
|
457
|
+
if pkg["system"] == "Emscripten":
|
|
458
|
+
identity_fields["emscripten_compilers"] = {key: str(Path(pkg[key]).resolve()) for key in ("cc", "cxx", "ar")}
|
|
459
|
+
identity = json.dumps(identity_fields, sort_keys=True)
|
|
460
|
+
id_file = bdir / "gnb_identity.json"
|
|
461
|
+
if (bdir / "CMakeCache.txt").is_file() and (not id_file.is_file() or id_file.read_text() != identity):
|
|
462
|
+
print(f"gnb: toolchain or source changed, clearing {prefix}", flush=True)
|
|
463
|
+
for e in bdir.iterdir():
|
|
464
|
+
if e.name in ("gnb_package.json", "gnb_gn_libs.json", "gnb_exports.json"):
|
|
465
|
+
continue
|
|
466
|
+
if e.is_dir() and not e.is_symlink():
|
|
467
|
+
shutil.rmtree(e)
|
|
468
|
+
else:
|
|
469
|
+
e.unlink()
|
|
470
|
+
|
|
471
|
+
tc = bdir / "gnb_toolchain.cmake"
|
|
472
|
+
write_if_changed(tc, toolchain_text)
|
|
473
|
+
query = bdir / ".cmake/api/v1/query/codemodel-v2"
|
|
474
|
+
write_if_changed(query, "")
|
|
475
|
+
hook = bdir / "gnb_interfaces.cmake"
|
|
476
|
+
write_if_changed(hook, INTERFACE_HOOK)
|
|
477
|
+
|
|
478
|
+
cmd = [cmake_tool, "-G", "Ninja", "-S", pkg["source_dir"], "-B", str(bdir),
|
|
479
|
+
f"-DCMAKE_NINJA_OUTPUT_PATH_PREFIX={prefix}/",
|
|
480
|
+
f"-DCMAKE_TOOLCHAIN_FILE={tc}",
|
|
481
|
+
"-DCMAKE_BUILD_TYPE="]
|
|
482
|
+
cmd += [f"-DCMAKE_{lang}_FLAGS={shlex.join(pkg[key])}" for lang, key in LANGS]
|
|
483
|
+
cmd.append(f"-DCMAKE_EXE_LINKER_FLAGS={shlex.join(pkg['ldflags'])}")
|
|
484
|
+
cmd += [f"-DCMAKE_{lang}_COMPILER_LAUNCHER={pkg['launcher']}" for lang, _ in LANGS]
|
|
485
|
+
cmd.append("-DCMAKE_EXPORT_COMPILE_COMMANDS=ON")
|
|
486
|
+
if pkg["pkg_source_dir"]:
|
|
487
|
+
cmd.append(f"-DGNB_PKG_SOURCE_DIR={pkg['pkg_source_dir']}")
|
|
488
|
+
# Option keys and GNB_DEP_* variables possibly in the cache: journaled before the attempt because CMake saves -D
|
|
489
|
+
# values even when configuration fails; removed keys are reset with -U so they return to the upstream default
|
|
490
|
+
defs = {k: ("ON" if v else "OFF") if isinstance(v, bool) else v for k, v in pkg["options"].items()}
|
|
491
|
+
hooks = [defs["CMAKE_PROJECT_INCLUDE"]] if defs.get("CMAKE_PROJECT_INCLUDE") else []
|
|
492
|
+
defs["CMAKE_PROJECT_INCLUDE"] = ";".join([*hooks, str(hook)])
|
|
493
|
+
# A GN dependency's libraries are the outputs gn evaluated for its targets (postgen resolves them after gn gen)
|
|
494
|
+
gn_libs = json.loads((bdir / "gnb_gn_libs.json").read_text()) if any(d["gn_libs"] for d in pkg["deps"]) else {}
|
|
495
|
+
dep_libs = {}
|
|
496
|
+
dep_rescan = {}
|
|
497
|
+
dep_system_libs, dep_link_flags = {}, {}
|
|
498
|
+
config_inputs = [str(Path(__file__).resolve())]
|
|
499
|
+
dep_includes = {}
|
|
500
|
+
for dep in pkg["deps"]:
|
|
501
|
+
paths = [path for label in dep["gn_libs"] for path in gn_libs[label]["paths"]]
|
|
502
|
+
dep_rescan[dep["name"]] = False
|
|
503
|
+
dep_system_libs[dep["name"]] = [lib for label in dep["gn_libs"] for lib in gn_libs[label]["system_libs"]]
|
|
504
|
+
dep_link_flags[dep["name"]] = []
|
|
505
|
+
dep_includes[dep["name"]] = []
|
|
506
|
+
if dep["cmake_prefix"]:
|
|
507
|
+
exports_path = f"{dep['cmake_prefix']}/gnb_exports.json"
|
|
508
|
+
config_inputs += [f"{dep['cmake_prefix']}/build.ninja", exports_path]
|
|
509
|
+
model = json.loads((out / exports_path).read_text())
|
|
510
|
+
selected = selected_targets(model, dep["targets"], dep["name"])
|
|
511
|
+
paths += [path for target in selected for path in target["paths"]]
|
|
512
|
+
dep_rescan[dep["name"]] = any(target["rescan"] for target in selected)
|
|
513
|
+
dep_system_libs[dep["name"]] += [lib for target in selected for lib in target["system_libs"]]
|
|
514
|
+
dep_link_flags[dep["name"]] += [flag for target in selected for flag in target["link_flags"]]
|
|
515
|
+
dep_includes[dep["name"]] += [path for target in selected
|
|
516
|
+
for field in ("include_dirs", "system_include_dirs") for path in target[field]]
|
|
517
|
+
for header in dep["header_deps"]:
|
|
518
|
+
exports_path = f"{header['cmake_prefix']}/gnb_exports.json"
|
|
519
|
+
config_inputs += [f"{header['cmake_prefix']}/build.ninja", exports_path]
|
|
520
|
+
selected = selected_targets(json.loads((out / exports_path).read_text()), header["targets"], header["name"])
|
|
521
|
+
dep_includes[dep["name"]] += [path for target in selected
|
|
522
|
+
for field in ("include_dirs", "system_include_dirs") for path in target[field]]
|
|
523
|
+
dep_includes[dep["name"]] += dep["include_dirs"]
|
|
524
|
+
dep_libs[dep["name"]] = paths
|
|
525
|
+
for dep in pkg["deps"]:
|
|
526
|
+
# Paths only: the libraries are built after configuration, before this package's compiles
|
|
527
|
+
var = f"GNB_DEP_{dep_var_name(dep['name'])}"
|
|
528
|
+
defs[f"{var}_INCLUDE_DIRS"] = ";".join(dict.fromkeys(dep_includes[dep["name"]]))
|
|
529
|
+
libraries = [str(out / path) for path in dep_libs[dep["name"]]]
|
|
530
|
+
# Keep providers after the last consumer occurrence when native closures overlap.
|
|
531
|
+
libraries = list(dict.fromkeys(reversed([*libraries, *dep_system_libs[dep["name"]]])))[::-1]
|
|
532
|
+
if dep_rescan[dep["name"]]:
|
|
533
|
+
libraries = ["$<LINK_GROUP:RESCAN," + ",".join(lib.replace(",", "$<COMMA>") for lib in libraries) + ">"]
|
|
534
|
+
libraries += list(dict.fromkeys(reversed(dep_link_flags[dep["name"]])))[::-1]
|
|
535
|
+
defs[f"{var}_LIBRARIES"] = ";".join(libraries)
|
|
536
|
+
keys_file = bdir / "gnb_option_keys.json"
|
|
537
|
+
old_keys = json.loads(keys_file.read_text()) if keys_file.is_file() else []
|
|
538
|
+
write_if_changed(keys_file, json.dumps(sorted(set(old_keys) | set(defs))))
|
|
539
|
+
cmd += [f"-U{k}" for k in sorted(set(old_keys) - set(defs))]
|
|
540
|
+
cmd += [f"-D{k}={v}" for k, v in defs.items()]
|
|
541
|
+
try:
|
|
542
|
+
run(cmd, quiet=True)
|
|
543
|
+
except GnbError:
|
|
544
|
+
if not (bdir / "build.ninja").is_file():
|
|
545
|
+
write_stub(out, prefix) # package dir was cleared: leave a stub so the ninja graph loads; the next build retries configuration
|
|
546
|
+
raise
|
|
547
|
+
print(f"gnb: cmake configure {prefix}", flush=True)
|
|
548
|
+
|
|
549
|
+
build_ninja = bdir / "build.ninja"
|
|
550
|
+
order = [lib for libs in dep_libs.values() for lib in libs]
|
|
551
|
+
exports = read_exports(out, bdir)
|
|
552
|
+
native = normalize_artifact_nodes(build_ninja.read_text(), out, exports, order)
|
|
553
|
+
text, inputs = fix_package_ninja(native, prefix, order, config_inputs)
|
|
554
|
+
write_if_changed(build_ninja, text)
|
|
555
|
+
write_if_changed(bdir / "gnb_inputs.txt", "\n".join(inputs) + "\n")
|
|
556
|
+
rules = bdir / "CMakeFiles" / "rules.ninja"
|
|
557
|
+
write_if_changed(rules, fix_rules_ninja(rules.read_text(), configure_cmd(out, prefix), depfile_cmd(out, prefix)))
|
|
558
|
+
write_inputs(out, (out / "gnb_packages.txt").read_text().split())
|
|
559
|
+
compdb.merge(out)
|
|
560
|
+
write_if_changed(keys_file, json.dumps(sorted(defs)))
|
|
561
|
+
write_if_changed(id_file, identity)
|
|
562
|
+
publish_exports(bdir / "gnb_exports.json", exports)
|
gnbuild/build/compdb.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
from gnbuild.util import GnbError, find_tool, write_if_changed
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def snapshot_gn(out: Path, rules: List[str]) -> None:
|
|
12
|
+
# Ninja owns expansion and JSON serialization; snapshot only GN compile rules before CMake subninjas are injected.
|
|
13
|
+
entries = []
|
|
14
|
+
if rules:
|
|
15
|
+
result = subprocess.run([find_tool("ninja"), "-t", "compdb", *rules], cwd=out, text=True, capture_output=True)
|
|
16
|
+
if result.returncode != 0:
|
|
17
|
+
raise GnbError(f"cannot get GN compilation commands in {out}: {result.stdout}{result.stderr}")
|
|
18
|
+
try:
|
|
19
|
+
entries = json.loads(result.stdout)
|
|
20
|
+
except json.JSONDecodeError as e:
|
|
21
|
+
raise GnbError(f"cannot parse Ninja compilation commands in {out}: {e}")
|
|
22
|
+
write_if_changed(out / "gnb_compdb_gn.json", json.dumps(entries, indent=2) + "\n")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def merge(out: Path) -> None:
|
|
26
|
+
parts = [out / "gnb_compdb_gn.json"]
|
|
27
|
+
parts += [out / p / "compile_commands.json" for p in (out / "gnb_packages.txt").read_text().split()]
|
|
28
|
+
merged = []
|
|
29
|
+
for f in parts:
|
|
30
|
+
if f.is_file():
|
|
31
|
+
try:
|
|
32
|
+
merged += json.loads(f.read_text())
|
|
33
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
34
|
+
raise GnbError(f"cannot parse {f}: {e}")
|
|
35
|
+
write_if_changed(out / "compile_commands.json", json.dumps(merged, indent=2) + "\n")
|