Skip to content

Commit fead7ab

Browse files
committed
feat(crossrepo): std index at highest c++ std (superset) + per-project standard on consuming side; gitignore sqlite stores
1 parent 5fa21f5 commit fead7ab

4 files changed

Lines changed: 527 additions & 12 deletions

File tree

.gitignore

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ outputs/packed/1024/build_summary.json
8383
outputs/packed/1024/build.log
8484
outputs/verification_report/_summary.json
8585
outputs/verification_report/_samples/_manifest.json
86-
outputs/crossrepo/global_symbols.sqlite
87-
outputs/crossrepo/global_symbols.sqlite-shm
88-
outputs/crossrepo/global_symbols.sqlite-wal
86+
# Cross-repo symbol stores are large (100MB+) regenerable binaries — never commit.
87+
outputs/crossrepo/*.sqlite
88+
outputs/crossrepo/*.sqlite-shm
89+
outputs/crossrepo/*.sqlite-wal
8990
outputs/reindexed_commits/*
9091
outputs/non_strict_backup_20260626_165907/*
9192
outputs/profiles/*

scripts/crossrepo/build_global_symbol_index.py

Lines changed: 306 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import json
5252
import os
5353
import re
54+
import shutil
5455
import sqlite3
5556
import subprocess
5657
import sys
@@ -148,7 +149,17 @@
148149
],
149150
"index_extensionless_headers": True,
150151
# ---- per-lib C++ standard-library compile environment ----
151-
"cxx_std": "-std=c++20", # std umbrella headers need >= c++20
152+
# cxx_std is chosen at BUILD TIME by an explicit probe (see
153+
# _probe_highest_cxx_std): we try the candidates below highest
154+
# first and pick the highest one libclang ACTUALLY accepts on a
155+
# real libc++ <ranges>/<format> parse. A SUPERSET std captures
156+
# the c++23/26 standard surface (ranges/concepts/format/expected/
157+
# flat_map). The static "cxx_std" is the documented FLOOR used
158+
# only if no probe candidates are configured; the probe (which
159+
# includes c++23 as its lowest rung) RAISES rather than silently
160+
# dropping below c++23 (RULE #1).
161+
"cxx_std": "-std=c++23", # floor; probe bumps to c++26/2c when accepted
162+
"cxx_std_probe_candidates": ("-std=c++26", "-std=c++2c", "-std=c++23"),
152163
"needs_cxx_stdlib_env": True, # inject clang builtins + C sysroot
153164
"allow_system_types": True, # KEEP std:: TYPE definitions
154165
"stdlib_include_markers": [ # per-FILE -I root (no cross-tree mix)
@@ -663,6 +674,151 @@ def extract_many_subtrees(tarball: Path, lib_specs: dict[str, dict], dest_root:
663674
return counts
664675

665676

677+
def _tarball_fingerprint(tarball: Path) -> dict[str, object]:
678+
st = tarball.stat()
679+
return {
680+
"path": str(tarball.resolve()),
681+
"size": st.st_size,
682+
"mtime_ns": st.st_mtime_ns,
683+
}
684+
685+
686+
def _extract_cache_signature(
687+
tarball: Path,
688+
spec: dict,
689+
*,
690+
max_files: int,
691+
member_cap: int | None,
692+
) -> dict[str, object]:
693+
return {
694+
"version": 1,
695+
"tarball": _tarball_fingerprint(tarball),
696+
"subtrees": list(spec["subtrees"]),
697+
"include_path_markers": list(spec.get("include_path_markers") or []),
698+
"index_extensionless_headers": bool(spec.get("index_extensionless_headers")),
699+
"max_files": max_files,
700+
"member_cap": member_cap,
701+
"max_bytes_per_file": MAX_BYTES_PER_FILE,
702+
}
703+
704+
705+
def _extract_cache_manifest_path(cache_root: Path, lib: str) -> Path:
706+
return cache_root / lib / ".gsi_extract_complete.json"
707+
708+
709+
def _read_extract_cache_manifest(cache_root: Path, lib: str) -> dict | None:
710+
path = _extract_cache_manifest_path(cache_root, lib)
711+
try:
712+
return json.loads(path.read_text())
713+
except FileNotFoundError:
714+
return None
715+
except json.JSONDecodeError as exc:
716+
raise RuntimeError(f"corrupt extraction cache manifest: {path}: {exc}") from exc
717+
718+
719+
def _write_extract_cache_manifest(
720+
cache_root: Path,
721+
lib: str,
722+
signature: dict[str, object],
723+
*,
724+
count: int,
725+
) -> None:
726+
path = _extract_cache_manifest_path(cache_root, lib)
727+
path.parent.mkdir(parents=True, exist_ok=True)
728+
payload = dict(signature)
729+
payload.update({
730+
"lib": lib,
731+
"count": count,
732+
"finished_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
733+
})
734+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
735+
736+
737+
def _extract_cache_hit(
738+
tarball: Path,
739+
spec: dict,
740+
cache_root: Path,
741+
lib: str,
742+
*,
743+
max_files: int,
744+
member_cap: int | None,
745+
) -> int | None:
746+
manifest = _read_extract_cache_manifest(cache_root, lib)
747+
if manifest is None:
748+
return None
749+
signature = _extract_cache_signature(
750+
tarball, spec, max_files=max_files, member_cap=member_cap
751+
)
752+
for key, value in signature.items():
753+
if manifest.get(key) != value:
754+
return None
755+
count = int(manifest.get("count", 0))
756+
if count <= 0:
757+
return None
758+
if not (cache_root / lib).is_dir():
759+
return None
760+
return count
761+
762+
763+
def prepare_extraction_cache(
764+
tarball: Path,
765+
lib_specs: dict[str, dict],
766+
cache_root: Path,
767+
*,
768+
max_files: int,
769+
member_cap: int | None = None,
770+
) -> tuple[dict[str, Path], dict[str, int]]:
771+
"""Return extracted source dirs, populating a reusable extraction cache.
772+
773+
The expensive operation is the 235 GiB ``zstd | tar`` stream. This cache
774+
makes that step one-time per (tarball fingerprint, lib spec, max_files,
775+
member_cap): later rebuilds re-index the cached source dirs without reading
776+
the tarball again. The cache stores SOURCE FILES only; symbol extraction still
777+
uses the current parser/indexer code, so C++20/23/26 parser fixes take effect
778+
on every rebuild without re-extraction.
779+
"""
780+
cache_root.mkdir(parents=True, exist_ok=True)
781+
counts: dict[str, int] = {}
782+
dirs: dict[str, Path] = {}
783+
missing: dict[str, dict] = {}
784+
785+
for lib, spec in lib_specs.items():
786+
hit = _extract_cache_hit(
787+
tarball, spec, cache_root, lib,
788+
max_files=max_files, member_cap=member_cap,
789+
)
790+
dirs[lib] = cache_root / lib
791+
if hit is None:
792+
missing[lib] = spec
793+
else:
794+
counts[lib] = hit
795+
print(f" [{lib}] extraction cache hit: {hit} files",
796+
file=sys.stderr, flush=True)
797+
798+
if missing:
799+
for lib in missing:
800+
shutil.rmtree(cache_root / lib, ignore_errors=True)
801+
print(f" extraction cache miss for {list(missing)} -> {cache_root}",
802+
file=sys.stderr, flush=True)
803+
extracted = extract_many_subtrees(
804+
tarball, missing, cache_root,
805+
max_files=max_files, member_cap=member_cap,
806+
)
807+
for lib, count in extracted.items():
808+
counts[lib] = count
809+
if count > 0:
810+
_write_extract_cache_manifest(
811+
cache_root, lib,
812+
_extract_cache_signature(
813+
tarball, missing[lib], max_files=max_files,
814+
member_cap=member_cap,
815+
),
816+
count=count,
817+
)
818+
819+
return dirs, counts
820+
821+
666822
# --------------------------------------------------------------------------- #
667823
# Per-lib C++ standard-library compile environment (the std cross-link fix).
668824
#
@@ -778,6 +934,95 @@ def _stdlib_include_root_for(filepath: str, markers: Sequence[str] | None) -> st
778934
return None
779935

780936

937+
def _find_libcxx_include_root(cpp_files: Sequence[str]) -> str | None:
938+
"""The libc++ public-header include root (``.../libcxx/include``) from the
939+
already-discovered std source files. Used as the ``-I`` root for the cxx-std
940+
probe so ``#include <ranges>`` / ``#include <format>`` resolve to the REAL
941+
extracted libc++ umbrella headers (not a system libc++)."""
942+
for fp in cpp_files:
943+
root = _stdlib_include_root_for(fp, ("/libcxx/include/",))
944+
if root:
945+
return root
946+
return None
947+
948+
949+
def _probe_highest_cxx_std(
950+
candidates: Sequence[str],
951+
include_root: str | None,
952+
sysroot_args: Sequence[str],
953+
*,
954+
lib: str,
955+
) -> str:
956+
"""Pick the HIGHEST ``-std=c++NN`` flag libclang ACTUALLY accepts, verified by
957+
a real probe parse of libc++ ``<ranges>`` + ``<format>``.
958+
959+
Candidates are tried highest-first. A candidate is REJECTED only when libclang
960+
emits a *driver-level* std diagnostic for it — ``invalid value '<v>' in
961+
'-std=<v>'`` or ``unknown argument '<v>'`` — i.e. the toolchain does not know
962+
that standard; we then step DOWN to the next candidate. (A missing-header or
963+
template error is NOT a std-flag rejection — stepping down would not fix it —
964+
so it never triggers a step-down.) The first accepted candidate wins and is
965+
logged with its probe evidence. If NO candidate is accepted we RAISE (RULE #1:
966+
this is an EXPLICIT probe, never a silent except — and we never drop below the
967+
configured candidate set)."""
968+
if not candidates:
969+
raise RuntimeError(f"[{lib}] cxx_std probe requested with no candidates")
970+
ip._configure_libclang()
971+
probe_index = ip.Index.create()
972+
parse_opts = int(getattr(ip.TranslationUnit, "PARSE_INCOMPLETE", 0) or 0)
973+
rejections: list[str] = []
974+
with tempfile.TemporaryDirectory(prefix=f"gsi_stdprobe_{lib}_") as td:
975+
probe_src = os.path.join(td, "cxx_std_probe.cpp")
976+
with open(probe_src, "w") as fh:
977+
fh.write("#include <ranges>\n#include <format>\nint main() { return 0; }\n")
978+
for cand in candidates:
979+
value = cand.split("=", 1)[1] if "=" in cand else cand
980+
compile_args = ["-x", "c++", cand]
981+
if include_root:
982+
compile_args += ["-I", include_root]
983+
compile_args += list(sysroot_args)
984+
# An UNKNOWN -std makes libclang's driver fail so hard it produces no
985+
# TU and raises TranslationUnitLoadError. That raised parse IS the
986+
# rejection signal for this candidate: record it and step DOWN. This
987+
# is the probe's explicit detection mechanism ("if the std flag errors,
988+
# step down"), NOT a silent fallback — if NO candidate parses we RAISE
989+
# below with every collected reason (RULE #1: fail loud).
990+
try:
991+
tu = probe_index.parse(probe_src, args=compile_args,
992+
options=parse_opts)
993+
except Exception as exc: # noqa: BLE001 - per-candidate probe rejection
994+
rejections.append(f"{cand} (parse raised: {type(exc).__name__})")
995+
print(f" [{lib}] cxx_std probe: {cand} REJECTED by libclang "
996+
f"({type(exc).__name__}) -> stepping down",
997+
file=sys.stderr, flush=True)
998+
continue
999+
diags = list(tu.diagnostics)
1000+
std_rejected = any(
1001+
value in d.spelling
1002+
and ("invalid value" in d.spelling or "unknown argument" in d.spelling)
1003+
for d in diags
1004+
)
1005+
if std_rejected:
1006+
rejections.append(f"{cand} (driver diagnostic)")
1007+
print(f" [{lib}] cxx_std probe: {cand} REJECTED by libclang "
1008+
f"(driver diagnostic) -> stepping down",
1009+
file=sys.stderr, flush=True)
1010+
continue
1011+
n_fatal = sum(1 for d in diags if d.severity >= 4)
1012+
headers_parsed = bool(include_root) and n_fatal == 0
1013+
print(f" [{lib}] cxx_std probe: chose {cand} "
1014+
f"(libclang accepts; diagnostics={len(diags)} fatal={n_fatal} "
1015+
f"libcxx_ranges_format="
1016+
f"{'parsed' if headers_parsed else 'flag-accepted'})",
1017+
file=sys.stderr, flush=True)
1018+
return cand
1019+
raise RuntimeError(
1020+
f"[{lib}] no C++ standard from {tuple(candidates)} was accepted by "
1021+
f"libclang on a libc++ <ranges>/<format> probe parse (tried: "
1022+
f"{rejections}). RULE #1: refusing to index std with an unknown -std flag."
1023+
)
1024+
1025+
7811026
# --------------------------------------------------------------------------- #
7821027
# Per-file libclang parse worker (isolated subprocess so a segfault is local).
7831028
# --------------------------------------------------------------------------- #
@@ -895,13 +1140,6 @@ def index_lib_from_dir(lib: str, spec: dict, dest: Path, store: GlobalSymbolStor
8951140
sysroot_args: list[str] = []
8961141
if spec.get("needs_cxx_stdlib_env"):
8971142
sysroot_args = _cxx_stdlib_sysroot_args() # RAISES if no builtins found
898-
lib_env = {
899-
"cxx_std": spec.get("cxx_std"),
900-
"sysroot_args": sysroot_args,
901-
"stdlib_include_markers": spec.get("stdlib_include_markers"),
902-
"msvc_path_marker": spec.get("msvc_path_marker"),
903-
"allow_system_types": bool(spec.get("allow_system_types")),
904-
}
9051143

9061144
cpp_files = ip.find_cpp_files(str(dest))
9071145
if spec.get("index_extensionless_headers"):
@@ -921,6 +1159,31 @@ def index_lib_from_dir(lib: str, spec: dict, dest: Path, store: GlobalSymbolStor
9211159
)
9221160
project_dir = str(dest)
9231161
include_dirs = _discover_include_dirs(dest, subtrees)
1162+
1163+
# ---- choose the C++ standard for this lib ----
1164+
# When the spec configures cxx_std_probe_candidates (only std does), pick the
1165+
# HIGHEST -std the installed libclang actually accepts on a real libc++
1166+
# <ranges>/<format> probe parse — a SUPERSET std so the c++23/26 standard
1167+
# surface (ranges/concepts/format/expected/flat_map) is captured. The static
1168+
# spec["cxx_std"] is only the documented FLOOR; the probe (explicit, logged,
1169+
# RAISES on total failure) replaces it. Non-std libs keep their static cxx_std.
1170+
probe_candidates = spec.get("cxx_std_probe_candidates")
1171+
if probe_candidates:
1172+
libcxx_root = _find_libcxx_include_root(cpp_files)
1173+
chosen_cxx_std = _probe_highest_cxx_std(
1174+
probe_candidates, libcxx_root, sysroot_args, lib=lib,
1175+
)
1176+
else:
1177+
chosen_cxx_std = spec.get("cxx_std")
1178+
1179+
lib_env = {
1180+
"cxx_std": chosen_cxx_std,
1181+
"sysroot_args": sysroot_args,
1182+
"stdlib_include_markers": spec.get("stdlib_include_markers"),
1183+
"msvc_path_marker": spec.get("msvc_path_marker"),
1184+
"allow_system_types": bool(spec.get("allow_system_types")),
1185+
}
1186+
9241187
print(f" [{lib}] lang={lang} cxx_std={lib_env['cxx_std'] or std_arg} "
9251188
f"include_dirs={len(include_dirs)} "
9261189
f"stdlib_env={'on' if sysroot_args else 'off'} "
@@ -1069,6 +1332,10 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
10691332
p.add_argument("--work-dir", default=None,
10701333
help="Staging root for extracted source (default: a fresh "
10711334
"mkdtemp deleted after the run).")
1335+
p.add_argument("--extract-cache-dir", default=None,
1336+
help="Persistent source extraction cache. When set, the "
1337+
"tarball is streamed only for libs missing from the "
1338+
"cache; parser/indexer code still re-runs every build.")
10721339
p.add_argument("--libclang-path", default=None)
10731340
p.add_argument("--report-only", action="store_true",
10741341
help="Print store counts/cost and exit (no indexing).")
@@ -1093,6 +1360,8 @@ def resolve_libs(spec: str) -> list[str]:
10931360
def main(argv: list[str]) -> int:
10941361
args = parse_args(argv)
10951362
ip._configure_libclang(args.libclang_path)
1363+
if args.extract_cache_dir and args.work_dir:
1364+
raise SystemExit("--extract-cache-dir and --work-dir are mutually exclusive")
10961365

10971366
tarball = Path(args.tarball)
10981367
store = GlobalSymbolStore(args.output, read_only=args.report_only)
@@ -1133,9 +1402,37 @@ def _finish(lib: str, spec: dict, res: LibResult) -> None:
11331402
f"types={res.n_types} errors={res.n_errors} "
11341403
f"elapsed={res.elapsed_s:.1f}s", file=sys.stderr, flush=True)
11351404

1405+
# Persistent cache: extract source once, re-index many times. This is the
1406+
# path for full A1/A2 rebuilds while std/C++20/23/26 parser work evolves.
1407+
if args.extract_cache_dir:
1408+
extract_root = Path(args.extract_cache_dir)
1409+
todo_specs = {lib: BASE_LIBS[lib] for lib in todo}
1410+
print(f"\n=== extraction cache for {todo} -> {extract_root} ===",
1411+
file=sys.stderr, flush=True)
1412+
t_ext = time.time()
1413+
dirs, counts = prepare_extraction_cache(
1414+
tarball, todo_specs, extract_root,
1415+
max_files=args.max_files_per_lib, member_cap=args.member_cap,
1416+
)
1417+
print(f" extraction cache ready in {time.time()-t_ext:.1f}s: {counts}",
1418+
file=sys.stderr, flush=True)
1419+
for lib in todo:
1420+
spec = BASE_LIBS[lib]
1421+
if counts.get(lib, 0) == 0:
1422+
raise RuntimeError(
1423+
f"[{lib}] extracted 0 files from {spec['subtrees']} — "
1424+
f"subtree name(s) wrong/absent (RULE #1: fail loud)."
1425+
)
1426+
print(f"\n=== indexing base-lib: {lib} (tier {spec['tier']}) ===",
1427+
file=sys.stderr, flush=True)
1428+
res = index_lib_from_dir(
1429+
lib, spec, dirs[lib], store,
1430+
workers=args.workers, std_arg=args.std_arg,
1431+
)
1432+
_finish(lib, spec, res)
11361433
# ONE tarball pass extracts ALL todo libs to a shared staging root (the
11371434
# 235 GiB tarball is streamed ONCE, not once per lib), unless --per-lib-pass.
1138-
if len(todo) > 1 and not args.per_lib_pass:
1435+
elif len(todo) > 1 and not args.per_lib_pass:
11391436
extract_root = Path(args.work_dir) if args.work_dir else Path(
11401437
tempfile.mkdtemp(prefix="gsi_extract_"))
11411438
extract_root.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)