-
Notifications
You must be signed in to change notification settings - Fork 29
Expand file tree
/
Copy pathbuild
More file actions
executable file
·3488 lines (3226 loc) · 139 KB
/
Copy pathbuild
File metadata and controls
executable file
·3488 lines (3226 loc) · 139 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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""Small, stateless build graph runner with a content-addressed cache."""
from __future__ import annotations
import sys
sys.dont_write_bytecode = True
import argparse
import ast
import base64
import concurrent.futures
import dataclasses
import errno
import fcntl
import glob as globlib
import hashlib
import importlib.util
import json
import os
import queue
import re
import shlex
import shutil
import signal
import stat
import subprocess
import sysconfig
import tempfile
import threading
import time
import types
import uuid
from collections import defaultdict, deque
from pathlib import Path
from typing import Any, Iterable
# sqlite backs only the strace input index; a build box whose python lacks
# the module still runs every untraced build
try:
import sqlite3
except ImportError:
sqlite3 = None
S = "$(S)"
B = "$(B)"
ANSI_COLORS = {
"red": "31", "green": "32", "yellow": "33", "blue": "34",
"magenta": "35", "cyan": "36", "white": "37",
"light-red": "91", "light-green": "92", "light-yellow": "93",
"light-blue": "94", "light-magenta": "95", "light-cyan": "96",
"light-white": "97",
}
def native_target() -> str:
target = sysconfig.get_config_var("HOST_GNU_TYPE")
if isinstance(target, str) and target:
return target
machine = os.uname().machine
machine = {"amd64": "x86_64", "arm64": "aarch64"}.get(machine, machine)
return f"{machine}-unknown-linux-gnu"
class BuildError(RuntimeError):
pass
class AllowSpec(Exception):
"""Raised by Flags.allow() in --help mode to hand the declared flag
spec back to main() for rendering; never seen during a real build."""
def __init__(self, spec: dict[str, dict]):
super().__init__("flag spec")
self.spec = spec
class Flags(types.ModuleType):
"""`-D` build flags, exposed to build.py as `build.flags`.
An unset flag reads as the empty string, so `if f.A:` is a plain
truthiness test with no string comparison. `-DA=B` sets A to "B",
a bare `-DA` sets it to "yes".
`allow({name: {'descr': ..., 'default': ...}})` declares the accepted
flags: a `-D` outside the set is an error (an empty set forbids all -D),
and an unset declared flag reads as its default. Under `--help` allow()
instead raises AllowSpec so the flags can be listed in the help text.
"""
def __init__(self, values: dict[str, str], help_mode: bool = False):
super().__init__("build.flags")
object.__setattr__(self, "_values", dict(values))
object.__setattr__(self, "_spec", None)
object.__setattr__(self, "_help", help_mode)
def allow(self, spec: dict[str, dict]) -> None:
norm = {
str(name): {
"descr": str((meta or {}).get("descr", "")),
"default": (meta or {}).get("default", ""),
}
for name, meta in (spec or {}).items()
}
if object.__getattribute__(self, "_help"):
raise AllowSpec(norm)
unknown = [k for k in object.__getattribute__(self, "_values") if k not in norm]
if unknown:
allowed = ", ".join(sorted(norm)) or "(none)"
raise BuildError(
f"unknown -D flag(s): {', '.join(sorted(unknown))}; allowed: {allowed}"
)
object.__setattr__(self, "_spec", norm)
def _get(self, name: str) -> str:
values = object.__getattribute__(self, "_values")
if name in values:
return values[name]
spec = object.__getattribute__(self, "_spec")
if spec is not None and name in spec:
return spec[name]["default"]
return ""
def __getattr__(self, name: str) -> str:
if name.startswith("__"):
raise AttributeError(name)
return self._get(name)
def __getitem__(self, name: str) -> str:
return self._get(name)
def __contains__(self, name: str) -> bool:
return name in object.__getattribute__(self, "_values")
def parse_defines(items: Iterable[str]) -> dict[str, str]:
values: dict[str, str] = {}
for item in items:
key, sep, value = item.partition("=")
if not key:
raise BuildError(f"malformed -D flag: {item!r}")
values[key] = value if sep else "yes"
return values
def _items(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, (str, bytes)):
return [value]
return list(value)
def _uniq(values: Iterable[Any]) -> list[Any]:
out: list[Any] = []
seen: set[Any] = set()
for value in values:
if value not in seen:
seen.add(value)
out.append(value)
return out
def _source_path(path: str) -> str:
path = str(path)
if path.startswith((S + "/", B + "/")) or os.path.isabs(path):
return path
raise BuildError(f"input path must start with $(S)/ or $(B)/: {path}")
def _output_path(path: str) -> str:
path = str(path)
if path.startswith(B + "/"):
return path
raise BuildError(f"output path must start with $(B)/: {path}")
def _include_path(path: str) -> str:
path = str(path).rstrip("/")
if path in (S, B) or path.startswith((S + "/", B + "/")) or os.path.isabs(path):
return path
raise BuildError(f"include path must start with $(S)/ or $(B)/: {path}")
def _cwd_path(path: str) -> str:
path = str(path).rstrip("/")
if path in (S, B) or path.startswith((S + "/", B + "/")) or os.path.isabs(path):
return path
raise BuildError(f"cwd path must start with $(S)/ or $(B)/: {path}")
def _validate_descr(descr: str) -> None:
if len(descr) != 2 or not descr.isascii() or not descr.isalpha():
raise BuildError(f"descr must be exactly two ASCII letters: {descr!r}")
@dataclasses.dataclass(eq=False)
class Node:
inputs: list[str]
outputs: list[str]
commands: list[list[str]]
deps: list["Node"] = dataclasses.field(default_factory=list)
cwd: str = B
env: dict[str, str] = dataclasses.field(default_factory=dict)
descr: str = "GN"
color: str = ""
include_roots: list[str] = dataclasses.field(default_factory=list)
scan_sources: bool = False
source_inputs: set[str] = dataclasses.field(default_factory=set)
# A local node never leaves this machine under --dist: it needs the
# network, the real source root, or something else a remote lacks.
local: bool = False
uid: str | None = None
def __post_init__(self) -> None:
_validate_descr(self.descr)
def canonical(self) -> dict[str, Any]:
return {
"inputs": self.inputs,
"outputs": self.outputs,
"cmd": self.commands,
"cwd": self.cwd,
"env": self.env,
}
@dataclasses.dataclass(eq=False)
class Target:
kind: str
name: str | None = None
srcs: list[Any] = dataclasses.field(default_factory=list)
deps: list["Target"] = dataclasses.field(default_factory=list)
cflags: list[str] = dataclasses.field(default_factory=list)
cxxflags: list[str] = dataclasses.field(default_factory=list)
cppflags: list[str] = dataclasses.field(default_factory=list)
includes: list[str] = dataclasses.field(default_factory=list)
public_cflags: list[str] = dataclasses.field(default_factory=list)
public_cxxflags: list[str] = dataclasses.field(default_factory=list)
public_cppflags: list[str] = dataclasses.field(default_factory=list)
ldflags: list[str] = dataclasses.field(default_factory=list)
output: str | None = None
commands: list[list[str]] = dataclasses.field(default_factory=list)
inputs: list[str] = dataclasses.field(default_factory=list)
outputs: list[str] = dataclasses.field(default_factory=list)
cwd: str = B
env: dict[str, str] = dataclasses.field(default_factory=dict)
descr: str = "GN"
color: str = "yellow"
linker: str | None = None
enabled: bool = True
local: bool = False
host_deps: list["Target"] = dataclasses.field(default_factory=list)
root: Node | None = None
nodes: list[Node] = dataclasses.field(default_factory=list)
uses_cxx: bool = False
def __bool__(self) -> bool:
return self.enabled
@dataclasses.dataclass
class ImportedGraph:
nodes: list[Node]
primary: set[Node]
exports: list[dict[str, Any]]
index_primary: bool
registered: set[Node] = dataclasses.field(default_factory=set)
selections: dict[tuple[str, bool, str | None], tuple[Node, str, str | None]] = dataclasses.field(default_factory=dict)
class BuildContext:
def __init__(self, source_root: Path, build_root: Path, flags: Flags | None = None,
*, target: str | None = None, host: str | None = None):
self.source_root = source_root.resolve()
self.build_root = build_root.resolve()
self.flags = flags if flags is not None else Flags({})
self.host = host or native_target()
self.target = target or self.host
self.build_file: Path | None = None
self.targets: list[Target] = []
self.groups: dict[str, list[Target]] = {}
self.nodes: list[Node] = []
self.output_nodes: dict[str, Node] = {}
self.target_names: dict[str, Target] = {}
self.program_outputs: dict[str, Target] = {}
self.command_host_deps: dict[Target, list[Target]] = {}
self.file_hashes: dict[str, str] = {}
self.includes: list[str] = []
self.external_cflags = shlex.split(os.environ.get("CFLAGS", ""))
self.external_cxxflags = shlex.split(os.environ.get("CXXFLAGS", ""))
self.external_cppflags = shlex.split(os.environ.get("CPPFLAGS", ""))
self.external_ldflags = shlex.split(os.environ.get("LDFLAGS", ""))
self.external_ldflags += shlex.split(os.environ.get("CTRFLAGS", ""))
# Expose the external flags while build.py is evaluated. load() splits
# them back out afterwards so command-line flags can be emitted last
# and override project defaults (for example, -std=c++17 for Clang).
self.cflags = self.external_cflags.copy()
self.cxxflags = self.external_cxxflags.copy()
self.cppflags = self.external_cppflags.copy()
self.ldflags = self.external_ldflags.copy()
self.host_cflags = shlex.split(os.environ.get("HOST_CFLAGS", ""))
self.host_cxxflags = shlex.split(os.environ.get("HOST_CXXFLAGS", ""))
self.host_cppflags = shlex.split(os.environ.get("HOST_CPPFLAGS", ""))
self.cc = os.environ.get("CC", "cc")
self.cxx = os.environ.get("CXX", "c++")
self.ar = os.environ.get("AR") or shutil.which("ar") or shutil.which("llvm-ar") or "ar"
self.tool_paths: dict[str, str] = {}
self.tool_kinds: dict[str, str] = {}
self.imported_graphs: dict[tuple[Any, ...], ImportedGraph] = {}
def resolve_tool(self, name: str) -> str:
cached = self.tool_paths.get(name)
if cached is None:
found = shutil.which(name)
if found is None:
raise BuildError(f"command not found in PATH: {name}")
# Preserve the name selected through PATH. Multi-call binaries use
# argv[0] to select an applet (for example Alpine's /bin/sh ->
# busybox), so replacing the symlink with its real path changes
# the command's meaning. Nix PATH entries already contain their
# store hash, making the absolute invocation path a stable UID
# input without dereferencing the final symlink.
cached = os.path.abspath(found)
self.tool_paths[name] = cached
return cached
def _compiler_kind(self, name: str, *, allow_unknown: bool = False) -> str:
path = self.resolve_tool(name)
cached = self.tool_kinds.get(path)
if cached is not None:
return cached
basename = Path(path).name.lower()
if "clang" in basename:
kind = "clang"
elif re.search(r"(?:^|-)(?:gcc|g\+\+)(?:-\d+(?:\.\d+)*)?$", basename):
kind = "gcc"
else:
try:
version = subprocess.check_output(
[path, "--version"], text=True, stderr=subprocess.STDOUT,
)
except (OSError, subprocess.CalledProcessError) as exc:
if allow_unknown:
self.tool_kinds[path] = "other"
return "other"
raise BuildError(f"cannot identify C/C++ compiler {name}: {exc}") from exc
lowered = version.lower()
if "clang" in lowered:
kind = "clang"
elif "gcc" in lowered or "free software foundation" in lowered:
kind = "gcc"
else:
kind = "other"
self.tool_kinds[path] = kind
return kind
def _compiler_command(self, name: str, *, allow_other: bool = False) -> list[str]:
kind = self._compiler_kind(name, allow_unknown=allow_other)
if kind == "clang":
return [name, f"--target={self.target}"]
if kind == "gcc" and self.target != self.host:
raise BuildError(
f"GCC cross-compilation is not supported: host {self.host}, target {self.target}"
)
if kind == "other" and not allow_other:
raise BuildError(f"unsupported C/C++ compiler: {self.resolve_tool(name)}")
return [name]
@staticmethod
def _configuration_uid(target: str, defines: dict[str, str]) -> str:
data = json.dumps(
{
"target": target,
"defines": defines,
},
sort_keys=True, separators=(",", ":"), ensure_ascii=False,
).encode()
return hashlib.md5(data).hexdigest()
def _target_configuration_uid(self) -> str:
defines = dict(object.__getattribute__(self.flags, "_values"))
return self._configuration_uid(self.target, defines)
def _host_configuration_uid(self) -> str:
return self._configuration_uid(self.host, {})
def interface(self, *, cflags=(), cxxflags=(), cppflags=(), ldflags=(), enabled=True, name=None) -> Target:
target = Target(
"interface", name=name,
public_cflags=_items(cflags), public_cxxflags=_items(cxxflags),
public_cppflags=_items(cppflags), ldflags=_items(ldflags),
enabled=enabled,
)
self.targets.append(target)
return target
def pkg_config(self, *packages: str, required: bool = True) -> Target:
tool = os.environ.get("PKG_CONFIG", "pkg-config")
base = [tool, *packages]
stderr = None if required else subprocess.DEVNULL
try:
cflags = shlex.split(subprocess.check_output(base + ["--cflags"], text=True, stderr=stderr).strip())
ldflags = shlex.split(subprocess.check_output(base + ["--libs"], text=True, stderr=stderr).strip())
except (OSError, subprocess.CalledProcessError) as exc:
if required:
raise BuildError(f"pkg-config failed for {' '.join(packages)}: {exc}") from exc
return self.interface(enabled=False, name="pkg:" + ",".join(packages))
return self.interface(cflags=cflags, ldflags=ldflags, name="pkg:" + ",".join(packages))
def pkg_config_variable(self, package: str, variable: str) -> str:
tool = os.environ.get("PKG_CONFIG", "pkg-config")
try:
return subprocess.check_output(
[tool, f"--variable={variable}", package], text=True,
).strip()
except (OSError, subprocess.CalledProcessError) as exc:
raise BuildError(f"pkg-config variable {package}:{variable} failed: {exc}") from exc
def have_header(self, header: str) -> bool:
if not re.fullmatch(r"[A-Za-z0-9_./+-]+", header):
raise BuildError(f"invalid header name: {header!r}")
flags = [
*self.cppflags,
*self.cflags,
*self.cxxflags,
]
source = f"#include <{header}>\n"
command = [
*self._compiler_command(self.cxx),
*flags,
"-E", "-x", "c++", "-",
]
command = [
argument.replace(S, str(self.source_root)).replace(B, str(self.build_root))
for argument in command
]
try:
subprocess.run(
command,
input=source,
text=True,
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except (OSError, subprocess.CalledProcessError):
return False
return True
def glob(self, pattern: str) -> list[str]:
pattern = str(pattern)
if pattern.startswith(S + "/"):
relative_pattern = pattern[len(S) + 1:]
matches = globlib.glob(str(self.source_root / relative_pattern), recursive=True)
result: list[str] = []
for match in matches:
try:
relative = Path(match).relative_to(self.source_root)
except ValueError as exc:
raise BuildError(f"glob escaped $(S): {pattern}") from exc
result.append(S + "/" + relative.as_posix())
return sorted(result)
if os.path.isabs(pattern):
return sorted(globlib.glob(pattern, recursive=True))
raise BuildError(f"glob pattern must start with $(S)/ or be absolute: {pattern}")
def library(self, *, srcs, name=None, cflags=(), cxxflags=(), cppflags=(), includes=(),
public_cflags=(), public_cxxflags=(), public_cppflags=(),
deps=(), ldflags=(), output=None) -> Target:
return self._compiled_target(
"library", srcs, name, cflags, cxxflags, cppflags,
public_cflags, public_cxxflags, public_cppflags,
deps, ldflags, output, None, includes,
)
def program(self, *, srcs, name=None, cflags=(), cxxflags=(), cppflags=(), includes=(),
public_cflags=(), public_cxxflags=(), public_cppflags=(),
deps=(), ldflags=(), output=None, linker=None) -> Target:
return self._compiled_target(
"program", srcs, name, cflags, cxxflags, cppflags,
public_cflags, public_cxxflags, public_cppflags,
deps, ldflags, output, linker, includes,
)
def _compiled_target(self, kind, srcs, name, cflags, cxxflags, cppflags,
public_cflags, public_cxxflags, public_cppflags,
deps, ldflags, output, linker, includes) -> Target:
sources = []
for source in _items(srcs):
if isinstance(source, dict):
source = dict(source)
if "src" not in source:
raise BuildError("source mapping has no 'src' field")
source["src"] = _source_path(source["src"])
source["inputs"] = [_source_path(path) for path in _items(source.get("inputs", ()))]
unknown = set(source) - {"src", "inputs"}
if unknown:
raise BuildError(f"unknown source field(s): {', '.join(sorted(unknown))}")
sources.append(source)
else:
sources.append(_source_path(source))
target = Target(
kind, name=name, srcs=sources,
deps=_items(deps),
cflags=_items(cflags), cxxflags=_items(cxxflags), cppflags=_items(cppflags),
includes=[_include_path(path) for path in _items(includes)],
public_cflags=_items(public_cflags),
public_cxxflags=_items(public_cxxflags),
public_cppflags=_items(public_cppflags),
ldflags=_items(ldflags), output=_output_path(output) if output else None,
linker=linker,
)
self._validate_deps(target)
self.targets.append(target)
return target
def command(self, *, inputs=(), outputs, cmd, deps=(), name=None, cwd=B, env=None,
cflags=(), cxxflags=(), cppflags=(), ldflags=(), descr="GN", color="yellow",
local=False) -> Target:
commands = cmd
if not commands or isinstance(commands[0], (str, bytes)):
commands = [commands]
target = Target(
"command", name=name, deps=_items(deps), inputs=[_source_path(x) for x in _items(inputs)],
outputs=[_output_path(x) for x in _items(outputs)],
commands=[[str(x) for x in command] for command in commands], cwd=_cwd_path(cwd),
env=dict(env or {}),
public_cflags=_items(cflags), public_cxxflags=_items(cxxflags),
public_cppflags=_items(cppflags), ldflags=_items(ldflags),
descr=str(descr), color=str(color), local=bool(local),
)
self._validate_deps(target)
self.targets.append(target)
return target
def group(self, name: str, *targets: Target) -> None:
if not isinstance(name, str) or not name:
raise BuildError(f"group name must be a non-empty string, got {name!r}")
for target in targets:
if not isinstance(target, Target):
raise BuildError(f"group {name!r} expects targets, got {target!r}")
self.groups.setdefault(name, []).extend(targets)
def install(self, *targets: Target) -> None:
self.group("install", *targets)
@staticmethod
def _validate_deps(target: Target) -> None:
for dep in target.deps:
if not isinstance(dep, Target):
raise BuildError(f"{target.kind} dependency is not a target: {dep!r}")
def load(self, path: Path) -> Any:
self.build_file = path.resolve()
if not self.build_file.is_file():
raise BuildError(f"build file does not exist: {self.build_file}")
spec = importlib.util.spec_from_file_location("_project_build", path)
if spec is None or spec.loader is None:
raise BuildError(f"cannot load {path}")
module = importlib.util.module_from_spec(spec)
build_module = types.ModuleType("build")
build_module.includes = []
build_module.glob = self.glob
build_module.flags = self.flags
build_module.host = self.host
build_module.target = self.target
for name in ("cflags", "cxxflags", "cppflags", "ldflags"):
setattr(build_module, name, getattr(self, name).copy())
build_module.have_header = self.have_header
module.__dict__.update({
"pkg_config": self.pkg_config,
"pkg_config_variable": self.pkg_config_variable,
"have_header": self.have_header,
"dependency": self.interface,
"import_build": self.import_build,
"library": self.library,
"program": self.program,
"command": self.command,
"group": self.group,
"install": self.install,
})
sys.modules[spec.name] = module
missing = object()
previous_build = sys.modules.get("build", missing)
previous_flags = sys.modules.get("build.flags", missing)
sys.modules["build"] = build_module
# so `import build.flags as f` resolves straight from sys.modules
sys.modules["build.flags"] = self.flags
try:
spec.loader.exec_module(module)
finally:
if previous_build is missing:
del sys.modules["build"]
else:
sys.modules["build"] = previous_build
if previous_flags is missing:
del sys.modules["build.flags"]
else:
sys.modules["build.flags"] = previous_flags
self.includes = [_include_path(path) for path in _items(build_module.includes)]
for name in ("cflags", "cxxflags", "cppflags", "ldflags"):
values = [str(flag) for flag in _items(getattr(build_module, name))]
external_name = "external_" + name
external = getattr(self, external_name)
# The ordinary `build.foo += [...]` form preserves the initial
# external prefix. Keep supporting build files that deliberately
# replace or reorder the whole list by treating their result as-is.
if values[:len(external)] == external:
values = values[len(external):]
else:
setattr(self, external_name, [])
setattr(self, name, values)
self._infer_names(module)
return module
def _invoke_graph(self, build_file: Path, target: str,
env: dict[str, str]) -> str:
result = subprocess.run(
[
sys.executable,
str(Path(__file__).resolve()),
"-G",
"--target",
target,
"--build-file",
str(build_file),
],
cwd=build_file.parent,
env=env,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if result.returncode != 0:
detail = result.stderr.rstrip()
message = f"cannot import build graph from {build_file}"
if detail:
message += "\n" + detail
raise BuildError(message)
return result.stdout
@staticmethod
def _decode_graph(graph: Any, origin: Path) -> tuple[
list[Node], set[Node], list[dict[str, Any]]]:
if not isinstance(graph, dict) or graph.get("version") not in (1, 2):
raise BuildError(f"unsupported build graph from {origin}")
version = graph["version"]
records = graph.get("nodes")
if not isinstance(records, list):
raise BuildError(f"build graph has no node list: {origin}")
nodes: list[Node] = []
record_inputs: list[list[str]] = []
record_deps: list[list[str]] = []
ids: dict[str, Node] = {}
primary: set[Node] = set()
producers: dict[str, Node] = {}
for record in records:
if not isinstance(record, dict):
raise BuildError(f"invalid node in build graph from {origin}")
inputs = record.get("inputs")
outputs = record.get("outputs")
commands = record.get("cmd")
cwd = record.get("cwd", B)
env = record.get("env", {})
node_id = record.get("id")
deps = record.get("deps", [])
if (not isinstance(inputs, list) or not all(isinstance(x, str) for x in inputs)
or not isinstance(outputs, list)
or not all(isinstance(x, str) for x in outputs)
or not isinstance(commands, list)
or not all(isinstance(command, list)
and all(isinstance(x, str) for x in command)
for command in commands)
or not isinstance(cwd, str) or not isinstance(env, dict)
or not all(isinstance(key, str) and isinstance(value, str)
for key, value in env.items())
or version == 2 and (
not isinstance(node_id, str) or not node_id
or not isinstance(deps, list)
or not all(isinstance(dep, str) for dep in deps)
or not isinstance(record.get("primary", False), bool)
)):
raise BuildError(f"invalid node fields in build graph from {origin}")
node = Node(
inputs=[] if version == 1 else _uniq(inputs),
outputs=outputs,
commands=commands,
cwd=cwd,
env=env,
descr=str(record.get("descr", "GN")),
color=str(record.get("color", "" if version == 2 else "yellow")),
)
for path in inputs:
if path.startswith(S + "/") or os.path.isabs(path):
node.source_inputs.add(path)
nodes.append(node)
record_inputs.append(inputs)
record_deps.append(deps)
if version == 2:
if node_id in ids:
raise BuildError(f"duplicate node id in build graph from {origin}: {node_id}")
ids[node_id] = node
if record.get("primary", False):
primary.add(node)
else:
primary.add(node)
for output in outputs:
if output in producers:
raise BuildError(f"multiple imported producers for {output}")
producers[output] = node
if version == 2:
for node, deps in zip(nodes, record_deps):
try:
node.deps = _uniq(ids[dep] for dep in deps)
except KeyError as exc:
raise BuildError(
f"imported dependency has no node: {exc.args[0]}"
) from exc
else:
for node, inputs in zip(nodes, record_inputs):
for path in inputs:
producer = producers.get(path)
if producer is not None:
node.deps.append(producer)
else:
if path.startswith(B + "/"):
raise BuildError(f"imported input has no producer: {path}")
node.inputs.append(path)
node.deps = _uniq(node.deps)
node.inputs = _uniq(node.inputs)
exports: list[dict[str, Any]] = []
if version == 2:
encoded_exports = graph.get("exports", [])
if (not isinstance(encoded_exports, list)
or not all(isinstance(export, dict) for export in encoded_exports)):
raise BuildError(f"invalid exports in build graph from {origin}")
for export in encoded_exports:
node_id = export.get("node")
name = export.get("name")
kind = export.get("kind")
output = export.get("output")
if (not isinstance(node_id, str) or node_id not in ids
or not isinstance(name, str) or not isinstance(kind, str)
or not isinstance(output, str)):
raise BuildError(f"invalid export in build graph from {origin}")
exports.append({
"node": ids[node_id],
"name": name,
"kind": kind,
"output": output,
})
return nodes, primary, exports
@staticmethod
def _select_imported_root(nodes: Iterable[Node],
exports: Iterable[dict[str, Any]],
output_name: str, *,
exact: bool = False,
target_name: str | None = None) -> tuple[Node, str, str | None]:
matches: list[tuple[Node, str, str | None]] = []
for export in exports:
output = export["output"]
if target_name is not None and export["name"] != target_name:
continue
matched = output == output_name if exact else Path(output).name == output_name
if matched:
matches.append((export["node"], export["kind"], export["name"]))
if not matches:
for node in nodes:
for output in node.outputs:
matched = output == output_name if exact else Path(output).name == output_name
if matched:
matches.append((node, "library", None))
matches = _uniq(matches)
if len(matches) != 1:
label = "output" if exact else "output named"
raise BuildError(f"expected one imported {label} {output_name!r}, got {len(matches)}")
return matches[0]
@staticmethod
def _node_closure(roots: Iterable[Node]) -> set[Node]:
closure: set[Node] = set()
stack = list(roots)
while stack:
node = stack.pop()
if node in closure:
continue
closure.add(node)
stack.extend(node.deps)
return closure
def _load_imported_graph(self, build_file: Path, target: str, *,
extra_cflags: Iterable[str] = (),
extra_cxxflags: Iterable[str] = (),
extra_cppflags: Iterable[str] = (),
source_prefix: str | None = None,
build_prefix: str | None = None,
index_primary: bool) -> ImportedGraph:
extras = (
tuple(str(flag) for flag in extra_cflags),
tuple(str(flag) for flag in extra_cxxflags),
tuple(str(flag) for flag in extra_cppflags),
)
key = (
str(build_file),
target,
extras,
source_prefix,
build_prefix,
index_primary,
)
cached = self.imported_graphs.get(key)
if cached is not None:
return cached
env = os.environ.copy()
# Each import appends its flags to the ordinary inherited environment.
# Nested imports therefore accumulate flags in parent-to-child order,
# and the imported build.py can inspect them through build.*flags.
for variable, values in zip(("CFLAGS", "CXXFLAGS", "CPPFLAGS"), extras):
if not values:
continue
existing = env.get(variable, "")
env[variable] = f"{existing} {shlex.join(values)}".strip() if existing else shlex.join(values)
encoded = self._invoke_graph(build_file, target, env)
if source_prefix is not None:
assert build_prefix is not None
encoded = encoded.replace(S, source_prefix).replace(B, build_prefix)
try:
graph = json.loads(encoded)
except json.JSONDecodeError as exc:
raise BuildError(f"invalid build graph from {build_file}: {exc}") from exc
nodes, primary, exports = self._decode_graph(graph, build_file)
imported = ImportedGraph(nodes, primary, exports, index_primary)
self.imported_graphs[key] = imported
return imported
def _select_graph_root(self, graph: ImportedGraph, output_name: str, *,
exact: bool = False,
target_name: str | None = None) -> tuple[Node, str, str | None]:
key = (output_name, exact, target_name)
cached = graph.selections.get(key)
if cached is None:
cached = self._select_imported_root(
graph.nodes,
graph.exports,
output_name,
exact=exact,
target_name=target_name,
)
graph.selections[key] = cached
return cached
def _register_imported_nodes(self, graph: ImportedGraph,
roots: Iterable[Node]) -> list[Node]:
closure = self._node_closure(roots)
imported_nodes = [node for node in graph.nodes if node in closure]
for node in imported_nodes:
if node in graph.registered:
continue
if graph.index_primary and node in graph.primary:
self._add_node(node)
else:
self.nodes.append(node)
graph.registered.add(node)
return imported_nodes
def import_build(self, path: str, output_name: str,
extra_cflags=(), extra_cxxflags=(),
extra_cppflags=(), deps=(), namespace=None) -> Target:
path = str(path)
if path.startswith(S + "/"):
build_file = self.physical(path)
elif os.path.isabs(path):
build_file = Path(path)
else:
build_file = self.source_root / path
build_file = build_file.resolve()
if not build_file.is_file():
raise BuildError(f"imported build file does not exist: {build_file}")
try:
source_namespace = build_file.parent.relative_to(self.source_root).as_posix()
except ValueError as exc:
raise BuildError(
f"imported build file is outside the source tree: {build_file}"
) from exc
if source_namespace in ("", "."):
raise BuildError("cannot import the root build file into itself")
# An explicit namespace gives the import its own build prefix, so the
# same build file can be imported more than once with different flags.
source_prefix = f"{S}/{source_namespace}"
build_prefix = f"{B}/{namespace or source_namespace}"
graph = self._load_imported_graph(
build_file,
self.target,
extra_cflags=[str(x) for x in _items(extra_cflags)],
extra_cxxflags=[str(x) for x in _items(extra_cxxflags)],
extra_cppflags=[str(x) for x in _items(extra_cppflags)],
source_prefix=source_prefix,
build_prefix=build_prefix,
index_primary=True,
)
root, kind, exported_name = self._select_graph_root(graph, output_name)
# Link importer-built targets into the imported root. The imported
# graph cannot reference this graph's outputs itself: its markers are
# namespaced during the import, so a path smuggled in through flags
# would be rewritten. Injecting after the rewrite keeps paths intact.
dep_targets = _items(deps)
if dep_targets and kind != "program":
raise BuildError(
f"import_build deps apply to a program root, got {kind!r} for {output_name!r}")
for dep in dep_targets:
if not isinstance(dep, Target) or dep.root is None or not dep.output:
raise BuildError(
f"import_build dep for {output_name!r} must be a built target with an output")
if dep.output in root.inputs:
continue
root.inputs.append(dep.output)
# Appended past the imported link line, after the archives it
# references; static resolution order stays correct.
root.commands[-1].append(dep.output)
# The libraries the dep resolves against follow it for the same
# reason. The imported graph never saw this dependency, so it
# cannot have asked for them, and only CFLAGS, CXXFLAGS and
# CPPFLAGS reach an import through the environment.
root.commands[-1].extend(self._link_flags([dep]))
root.deps.append(dep.root)
imported_nodes = self._register_imported_nodes(graph, [root])
target = Target(
kind,
# The exported name, so an imported target held in a local can
# skip the global-alias naming pass.
name=exported_name,
public_cppflags=["-I" + source_prefix],
output=root.outputs[0],
root=root,
nodes=imported_nodes,
uses_cxx=True,
)
self.targets.append(target)
return target
def serialize_graph(self) -> str:
bases: dict[Node, dict[str, Any]] = {}
for node in self.nodes:
record: dict[str, Any] = {
"inputs": sorted(set(node.inputs) | node.source_inputs),
"outputs": node.outputs,
"cmd": node.commands,
}
if node.cwd != B:
record["cwd"] = node.cwd
if node.env:
record["env"] = node.env
if node.descr != "GN":
record["descr"] = node.descr
if node.color:
record["color"] = node.color
if any(self.output_nodes.get(output) is node for output in node.outputs):
record["primary"] = True
bases[node] = record
identities: dict[Node, str] = {}
active: set[Node] = set()
def identify(node: Node) -> str:
cached = identities.get(node)
if cached is not None:
return cached
if node in active:
raise BuildError(f"cannot serialize cyclic node: {node.outputs}")
active.add(node)
deps = [identify(dep) for dep in node.deps]
canonical = json.dumps(
[bases[node], deps],
sort_keys=True, separators=(",", ":"), ensure_ascii=False,
).encode()
identity = hashlib.md5(canonical).hexdigest()
identities[node] = identity
active.remove(node)
return identity
for node in self.nodes:
identify(node)
records_by_id: dict[str, dict[str, Any]] = {}
for node in self.nodes:
identity = identities[node]
record = {
**bases[node],