5151import json
5252import os
5353import re
54+ import shutil
5455import sqlite3
5556import subprocess
5657import sys
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>\n int 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]:
10931360def 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