nativegate 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.
Files changed (50) hide show
  1. nativegate/__init__.py +1 -0
  2. nativegate/__main__.py +4 -0
  3. nativegate/buildinfo.py +344 -0
  4. nativegate/cli.py +2007 -0
  5. nativegate/config.py +991 -0
  6. nativegate/declared_invariants.py +565 -0
  7. nativegate/discovery.py +167 -0
  8. nativegate/driverbuild.py +626 -0
  9. nativegate/drivers/__init__.py +5 -0
  10. nativegate/drivers/cpp.py +616 -0
  11. nativegate/drivers/fortran.py +507 -0
  12. nativegate/generators/__init__.py +0 -0
  13. nativegate/generators/cmake_gen.py +101 -0
  14. nativegate/generators/docker_gen.py +614 -0
  15. nativegate/generators/error_gen.py +104 -0
  16. nativegate/generators/f2py_gen.py +91 -0
  17. nativegate/generators/gateway_gen.py +110 -0
  18. nativegate/generators/golden_gen.py +50 -0
  19. nativegate/generators/k8s_gen.py +212 -0
  20. nativegate/generators/mcp_gen.py +281 -0
  21. nativegate/generators/middleware_gen.py +717 -0
  22. nativegate/generators/pybind_gen.py +406 -0
  23. nativegate/generators/pyproject_gen.py +61 -0
  24. nativegate/generators/python_pkg_gen.py +1164 -0
  25. nativegate/generators/test_gen.py +160 -0
  26. nativegate/golden.py +747 -0
  27. nativegate/invariants.py +532 -0
  28. nativegate/ir.py +789 -0
  29. nativegate/lattice.py +350 -0
  30. nativegate/locking.py +216 -0
  31. nativegate/oracle.py +904 -0
  32. nativegate/parsers/__init__.py +0 -0
  33. nativegate/parsers/cpp.py +105 -0
  34. nativegate/parsers/cpp_ast.py +1652 -0
  35. nativegate/parsers/cpp_regex.py +812 -0
  36. nativegate/parsers/fixed_form.py +868 -0
  37. nativegate/parsers/fortran.py +157 -0
  38. nativegate/parsers/fortran_fparser.py +1116 -0
  39. nativegate/parsers/fortran_regex.py +686 -0
  40. nativegate/preprocess.py +335 -0
  41. nativegate/structural_invariants.py +762 -0
  42. nativegate/suggest.py +208 -0
  43. nativegate/templates/__init__.py +20 -0
  44. nativegate/templates/golden_test_template.py +248 -0
  45. nativegate/wire.py +438 -0
  46. nativegate-0.1.0.dist-info/METADATA +547 -0
  47. nativegate-0.1.0.dist-info/RECORD +50 -0
  48. nativegate-0.1.0.dist-info/WHEEL +5 -0
  49. nativegate-0.1.0.dist-info/entry_points.txt +3 -0
  50. nativegate-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,626 @@
1
+ """T4 — turn a generated oracle driver source into a runnable binary.
2
+
3
+ Spec `design-verification-layers.md` section 2.3 ("one set of object code,
4
+ not two compilations of one source"), section 2.8 (hard preconditions),
5
+ section 4 rules 2-3, 5, 8. Read those, plus `buildinfo.py` (T1) and
6
+ `drivers/fortran.py` (T3), before changing anything here.
7
+
8
+ What this module does NOT do, on purpose:
9
+
10
+ * It never recompiles a library source. The whole bitwise claim in section
11
+ 2.3 rests on the driver and the extension executing the *identical*
12
+ machine code for the library routines — a second compilation of the same
13
+ `.f90`/`.f` files, even with identical flags, is a second body of object
14
+ code the compiler is free to have inlined or contracted differently.
15
+ `link_objects_for_sources` only ever *reads* a `compile_commands.json`
16
+ entry's already-recorded `output` path; it does not invoke a compiler.
17
+ * It never widens the safety gate into a warning. `buildinfo.refuse_unsafe`
18
+ is called before anything is compiled, and a mismatch between the driver's
19
+ own codegen-affecting flags and the extension's is `DriverFlagMismatchError`
20
+ — both hard errors (spec section 2.8, section 4 rule 2).
21
+ * It never guesses at threading. Every subprocess this module runs — the
22
+ driver TU compile is a single-threaded compiler invocation, so that one is
23
+ unaffected, but the *driver executable* — runs under
24
+ `buildinfo.pinned_environment()` (spec section 4 rule 3).
25
+
26
+ What "the built library objects" means here, concretely: for the Fortran/
27
+ f2py-via-meson path this repository actually uses (`services/petro_api`),
28
+ `compile_commands.json`'s per-source `output` field is the `.o` file meson's
29
+ ninja backend wrote for that source — already `-fPIC` (meson builds every
30
+ source of a shared-library/extension target position-independent), and
31
+ linking `-fPIC` objects into a plain executable is legal everywhere this
32
+ runs (spec section 2.3's stated consequence). This module links exactly
33
+ those files, in the order the caller names their sources, plus the freshly
34
+ compiled driver object — nothing else.
35
+
36
+ **A build-pipeline gap this module used to work around, now fixed for the
37
+ Fortran path:** `ngate build <name>` runs `pip wheel .`, which drives
38
+ scikit-build-core → CMake → the generated `add_custom_command` that shells
39
+ out to `python -m numpy.f2py -c --backend meson ...` (see
40
+ `generators/f2py_gen.py`). That command now passes an explicit
41
+ `--build-dir "${CMAKE_CURRENT_SOURCE_DIR}/.nativegate/build"`, so f2py no
42
+ longer falls back to `tempfile.mkdtemp()` for its meson build directory (see
43
+ `numpy/f2py/f2py2e.py:run_compile`) and the build tree survives at a fixed,
44
+ documented location: `services/<name>/.nativegate/build/bbdir/compile_commands.json`,
45
+ with the corresponding `.o` files alongside it. After a real
46
+ `ngate build petro_api`, that `compile_commands.json` is directly
47
+ loadable by `buildinfo.load_compile_commands` and usable by this module's
48
+ `link_objects_for_sources`/`build_and_run_driver` without any workaround.
49
+ The analogous C++/`cmake_gen.py` gap (`CMAKE_EXPORT_COMPILE_COMMANDS`
50
+ pointing into a throwaway CMake build tree rather than a pinned one) is a
51
+ separate, still-open gap — out of scope here.
52
+ `build_extension_with_compile_commands` below is kept as a
53
+ test/CI-isolation helper: it reproduces the same f2py invocation
54
+ `CMakeLists.txt` now generates (including the `--build-dir` flag) but lets a
55
+ test hand it its own throwaway `build_dir` instead of writing into a real
56
+ service's `.nativegate/` tree, so tests don't mutate checked-out fixture
57
+ directories. It performs the library's one and only compilation — the
58
+ driver then links its outputs — so it does not violate "never recompile the
59
+ library sources"; it exists purely for test isolation now, not to route
60
+ around a missing location.
61
+ """
62
+
63
+ from __future__ import annotations
64
+
65
+ import hashlib
66
+ import os
67
+ import subprocess
68
+ import sys
69
+ from dataclasses import dataclass, field
70
+ from pathlib import Path
71
+ from typing import Sequence
72
+
73
+ from . import buildinfo
74
+
75
+ __all__ = [
76
+ "DriverBuildResult",
77
+ "DriverFlagMismatchError",
78
+ "DriverRunError",
79
+ "link_objects_for_sources",
80
+ "compile_driver_tu",
81
+ "link_driver",
82
+ "run_driver",
83
+ "build_and_run_driver",
84
+ "build_extension_with_compile_commands",
85
+ "build_cxx_extension_with_compile_commands",
86
+ ]
87
+
88
+
89
+ class DriverFlagMismatchError(RuntimeError):
90
+ """The driver TU's codegen-affecting flags differ from the extension's.
91
+
92
+ Spec section 2.8: "The driver translation unit's flags differ from the
93
+ extension's extracted flags in any way that affects code generation." A
94
+ hard refusal, not a warning — a divergence here means a subsequent
95
+ bitwise mismatch would measure the flags, not the binding.
96
+ """
97
+
98
+
99
+ class DriverRunError(RuntimeError):
100
+ """The compiled driver exited non-zero. Message includes stderr (T4 §3)."""
101
+
102
+
103
+ @dataclass
104
+ class DriverBuildResult:
105
+ """Provenance and output of one driver build+run (T4 §4)."""
106
+
107
+ driver_sha256: str
108
+ link_target_sha256: str
109
+ extracted_flags: list[str]
110
+ codegen_flags: list[str]
111
+ compile_argv: list[str]
112
+ link_argv: list[str]
113
+ stdout: str
114
+ stderr: str
115
+ returncode: int
116
+ executable: Path
117
+ driver_object: Path
118
+ linked_objects: list[Path] = field(default_factory=list)
119
+
120
+
121
+ # --- locating the extension's own built objects (never recompiled) --------
122
+
123
+
124
+ def link_objects_for_sources(
125
+ compile_commands_path: Path, source_names: Sequence[str]
126
+ ) -> list[Path]:
127
+ """The extension's own built object files for `source_names`, in order.
128
+
129
+ Reads each source's `compile_commands.json` entry and resolves its
130
+ recorded `output` path against `directory` — the object file the
131
+ extension's *own* build already produced. This function never invokes a
132
+ compiler; if an entry has no `output` field (a `command`-form entry that
133
+ used `-o` instead — `buildinfo.flags_for_source` strips that pair rather
134
+ than exposing it, so this reads the raw entry directly), that source's
135
+ object cannot be located and a `KeyError` says so by name rather than
136
+ silently skipping it out of the link line.
137
+ """
138
+ commands = buildinfo.load_compile_commands(Path(compile_commands_path))
139
+ objects: list[Path] = []
140
+ for name in source_names:
141
+ entry = buildinfo.find_entry(commands, name)
142
+ directory = Path(entry.get("directory") or Path(compile_commands_path).parent)
143
+ output = entry.get("output")
144
+ if not output:
145
+ output = _output_from_argv(entry)
146
+ if not output:
147
+ raise KeyError(
148
+ f"compile command for {name!r} has no 'output' field and no "
149
+ "'-o' argument — cannot locate its built object file"
150
+ )
151
+ objects.append((directory / output).resolve())
152
+ return objects
153
+
154
+
155
+ def _output_from_argv(entry: dict) -> str | None:
156
+ # buildinfo.entry_argv is the single shared implementation of "turn a
157
+ # compile_commands.json entry's arguments/command into an argv list" —
158
+ # this module used to keep its own private copy; see buildinfo.py's
159
+ # entry_argv docstring.
160
+ argv = buildinfo.entry_argv(entry)
161
+ for i, token in enumerate(argv):
162
+ if token == "-o" and i + 1 < len(argv):
163
+ return argv[i + 1]
164
+ return None
165
+
166
+
167
+ def _compiler_executable(entry: dict) -> str:
168
+ """argv[0] of the entry's own compile command — the same compiler, not
169
+ whatever happens to be first on PATH."""
170
+ return buildinfo.entry_argv(entry)[0]
171
+
172
+
173
+ def _module_search_flags(flags: Sequence[str], base_dir: Path) -> list[str]:
174
+ """`-I`/`-J` tokens from the extension's flags, as search-only, absolute `-I`s.
175
+
176
+ The driver's `use <module>` needs to find the `.mod` file the
177
+ extension's own compile wrote (gfortran's `-J<dir>` sets both the write
178
+ location and a search location; `-Idir` is already search-only). This is
179
+ a *build-mechanics* necessity, not a codegen-affecting flag — it does
180
+ not change what machine code the compiler emits for a given AST, only
181
+ where it looks up module interfaces — so it is deliberately excluded
182
+ from `buildinfo.codegen_flags` and from the driver/extension divergence
183
+ check, and is added on top of the codegen subset instead.
184
+
185
+ `compile_commands.json` entries record paths relative to their own
186
+ `directory` field, and this module compiles the driver in a different
187
+ working directory (its own `work_dir`), so every path is resolved
188
+ against `base_dir` (the extension entry's `directory`) before use —
189
+ otherwise a relative `-Ifoo.so.p` silently resolves against the wrong
190
+ cwd and the `.mod` file is "not found" even though it exists.
191
+ """
192
+ out: list[str] = []
193
+ for flag in flags:
194
+ if flag.startswith("-J"):
195
+ path = flag[2:]
196
+ out.append("-I" + str((base_dir / path).resolve()))
197
+ elif flag.startswith("-I"):
198
+ path = flag[2:]
199
+ out.append("-I" + str((base_dir / path).resolve()))
200
+ return out
201
+
202
+
203
+ # --- compiling the driver TU only ------------------------------------------
204
+
205
+ # Source suffix for the driver TU, by language — the only thing that used to
206
+ # be hardcoded to Fortran here. Everything else (which compiler to invoke,
207
+ # which flags to use) already came from the extension's own
208
+ # compile_commands.json entry, so it was already language-agnostic.
209
+ _DRIVER_SUFFIX = {"fortran": ".f90", "cpp": ".cpp"}
210
+
211
+
212
+ def compile_driver_tu(
213
+ driver_source: str,
214
+ compile_commands_path: Path,
215
+ extension_source_name: str,
216
+ work_dir: Path,
217
+ *,
218
+ driver_flags: Sequence[str] | None = None,
219
+ language: str = "fortran",
220
+ ) -> tuple[Path, list[str], list[str], list[str]]:
221
+ """Compile ONLY the driver translation unit.
222
+
223
+ Never touches a library source. Returns
224
+ `(driver_object_path, compile_argv, extracted_flags, codegen_flags)`.
225
+
226
+ `language` selects the driver TU's source suffix/filename —
227
+ `"fortran"` (default, `.f90`, unchanged behavior) or `"cpp"` (`.cpp`).
228
+ It does NOT select which compiler runs: the compiler is always
229
+ `argv[0]` of the extension's own compile_commands.json entry for
230
+ `extension_source_name` — the same compiler (and, via
231
+ `driver_flags`/`use_codegen` below, the same codegen-affecting flags)
232
+ that built the extension, whether that happens to be gfortran, clang++,
233
+ or g++. This is what keeps the driver and the extension's objects one
234
+ body of machine code (spec section 2.3) regardless of source language.
235
+
236
+ Preconditions enforced here (spec section 2.8), both hard errors:
237
+
238
+ * `buildinfo.refuse_unsafe` on the extension's full extracted flags.
239
+ * if `driver_flags` is given, its codegen-affecting subset must equal
240
+ the extension's codegen-affecting subset exactly (order-sensitive) —
241
+ otherwise `DriverFlagMismatchError`. When omitted, the extension's own
242
+ codegen flags are used for the driver, so this can never diverge by
243
+ construction; `driver_flags` exists so a caller (and this module's own
244
+ negative test) can demonstrate the refusal.
245
+ """
246
+ work_dir = Path(work_dir)
247
+ work_dir.mkdir(parents=True, exist_ok=True)
248
+
249
+ suffix = _DRIVER_SUFFIX.get(language)
250
+ if suffix is None:
251
+ raise ValueError(
252
+ f"unknown driver language {language!r} — expected one of "
253
+ f"{sorted(_DRIVER_SUFFIX)}"
254
+ )
255
+
256
+ commands = buildinfo.load_compile_commands(Path(compile_commands_path))
257
+ ext_entry = buildinfo.find_entry(commands, extension_source_name)
258
+ extracted_flags = buildinfo.flags_for_source(commands, extension_source_name)
259
+
260
+ buildinfo.refuse_unsafe(extracted_flags)
261
+
262
+ ext_codegen = buildinfo.codegen_flags(extracted_flags)
263
+
264
+ if driver_flags is not None:
265
+ given_codegen = buildinfo.codegen_flags(list(driver_flags))
266
+ if given_codegen != ext_codegen:
267
+ raise DriverFlagMismatchError(
268
+ "refusing to build the oracle driver: its codegen-affecting "
269
+ f"flags {given_codegen} differ from the extension's own "
270
+ f"{ext_codegen} (design-verification-layers.md section 2.8) "
271
+ "— a bitwise comparison built from mismatched flags would "
272
+ "measure the flags, not the binding"
273
+ )
274
+ use_codegen = given_codegen
275
+ else:
276
+ use_codegen = ext_codegen
277
+
278
+ compiler = _compiler_executable(ext_entry)
279
+ ext_directory = Path(ext_entry.get("directory") or Path(compile_commands_path).parent)
280
+ search_flags = _module_search_flags(extracted_flags, ext_directory)
281
+
282
+ driver_path = work_dir / f"n2p_oracle_driver{suffix}"
283
+ driver_path.write_text(driver_source)
284
+
285
+ obj_path = work_dir / "n2p_oracle_driver.o"
286
+ argv = [compiler, *use_codegen, *search_flags, "-c", str(driver_path), "-o", str(obj_path)]
287
+
288
+ completed = subprocess.run(argv, cwd=work_dir, capture_output=True, text=True, check=False)
289
+ if completed.returncode != 0:
290
+ raise DriverRunError(
291
+ f"compiling the oracle driver TU failed ({completed.returncode}):\n"
292
+ f" $ {' '.join(argv)}\n{completed.stdout}\n{completed.stderr}"
293
+ )
294
+
295
+ return obj_path, argv, extracted_flags, use_codegen
296
+
297
+
298
+ # --- linking ----------------------------------------------------------------
299
+
300
+
301
+ def link_driver(
302
+ driver_object: Path,
303
+ link_objects: Sequence[Path],
304
+ compiler: str,
305
+ work_dir: Path,
306
+ *,
307
+ extra_link_args: Sequence[str] = (),
308
+ ) -> tuple[Path, list[str]]:
309
+ """Link the driver object against the extension's own built objects.
310
+
311
+ Nothing here compiles anything — every path in `link_objects` must
312
+ already exist (`link_objects_for_sources`'s job) — so this function
313
+ cannot accidentally become a second compilation of library sources.
314
+ """
315
+ work_dir = Path(work_dir)
316
+ for obj in link_objects:
317
+ if not Path(obj).exists():
318
+ raise FileNotFoundError(
319
+ f"link object {obj} does not exist — it must be the extension's "
320
+ "own build output, never something this module compiles"
321
+ )
322
+ exe_path = work_dir / ("n2p_oracle_driver.exe" if os.name == "nt" else "n2p_oracle_driver")
323
+ argv = [
324
+ compiler,
325
+ str(driver_object),
326
+ *[str(p) for p in link_objects],
327
+ *extra_link_args,
328
+ "-o",
329
+ str(exe_path),
330
+ ]
331
+ completed = subprocess.run(argv, cwd=work_dir, capture_output=True, text=True, check=False)
332
+ if completed.returncode != 0:
333
+ raise DriverRunError(
334
+ f"linking the oracle driver failed ({completed.returncode}):\n"
335
+ f" $ {' '.join(argv)}\n{completed.stdout}\n{completed.stderr}"
336
+ )
337
+ return exe_path, argv
338
+
339
+
340
+ # --- running -----------------------------------------------------------------
341
+
342
+
343
+ def run_driver(executable: Path, work_dir: Path) -> subprocess.CompletedProcess:
344
+ """Run the driver under `buildinfo.pinned_environment()`, capture both
345
+ streams. Non-zero exit is a hard failure whose message includes stderr
346
+ (T4 §3) — raised by the caller, not here, so callers that want the raw
347
+ `CompletedProcess` (e.g. to also assert stdout parses) can still get it.
348
+ """
349
+ env = dict(os.environ)
350
+ env.update(buildinfo.pinned_environment())
351
+ return subprocess.run(
352
+ [str(executable)], cwd=work_dir, capture_output=True, text=True, check=False, env=env
353
+ )
354
+
355
+
356
+ # --- the whole T4 pipeline in one call --------------------------------------
357
+
358
+
359
+ def build_and_run_driver(
360
+ driver_source: str,
361
+ compile_commands_path: Path,
362
+ extension_source_name: str,
363
+ link_source_names: Sequence[str],
364
+ work_dir: Path,
365
+ *,
366
+ driver_flags: Sequence[str] | None = None,
367
+ extra_link_args: Sequence[str] = (),
368
+ language: str = "fortran",
369
+ ) -> DriverBuildResult:
370
+ """Compile the driver TU, link it against the extension's own built
371
+ objects, run it under the pinned environment, and report provenance.
372
+
373
+ `language` — `"fortran"` (default) or `"cpp"` — selects the driver TU's
374
+ source suffix; see `compile_driver_tu` for what it does and does not
375
+ control.
376
+
377
+ Raises `buildinfo.UnsafeFlagError` (fast-math in the extension's flags),
378
+ `DriverFlagMismatchError` (driver/extension codegen flags diverge), or
379
+ `DriverRunError` (compile, link, or a non-zero run — every message
380
+ includes the captured stderr).
381
+ """
382
+ work_dir = Path(work_dir)
383
+ work_dir.mkdir(parents=True, exist_ok=True)
384
+
385
+ driver_object, compile_argv, extracted_flags, used_codegen = compile_driver_tu(
386
+ driver_source,
387
+ compile_commands_path,
388
+ extension_source_name,
389
+ work_dir,
390
+ driver_flags=driver_flags,
391
+ language=language,
392
+ )
393
+
394
+ link_objects = link_objects_for_sources(compile_commands_path, link_source_names)
395
+
396
+ commands = buildinfo.load_compile_commands(Path(compile_commands_path))
397
+ ext_entry = buildinfo.find_entry(commands, extension_source_name)
398
+ compiler = _compiler_executable(ext_entry)
399
+
400
+ executable, link_argv = link_driver(
401
+ driver_object, link_objects, compiler, work_dir, extra_link_args=extra_link_args
402
+ )
403
+
404
+ completed = run_driver(executable, work_dir)
405
+ if completed.returncode != 0:
406
+ raise DriverRunError(
407
+ f"the oracle driver exited {completed.returncode}:\n"
408
+ f"stdout:\n{completed.stdout}\nstderr:\n{completed.stderr}"
409
+ )
410
+
411
+ driver_sha256 = hashlib.sha256(driver_source.encode("utf-8")).hexdigest()
412
+ link_target_sha256 = buildinfo.link_target_hash(link_objects)
413
+
414
+ return DriverBuildResult(
415
+ driver_sha256=driver_sha256,
416
+ link_target_sha256=link_target_sha256,
417
+ extracted_flags=extracted_flags,
418
+ codegen_flags=used_codegen,
419
+ compile_argv=compile_argv,
420
+ link_argv=link_argv,
421
+ stdout=completed.stdout,
422
+ stderr=completed.stderr,
423
+ returncode=completed.returncode,
424
+ executable=executable,
425
+ driver_object=driver_object,
426
+ linked_objects=link_objects,
427
+ )
428
+
429
+
430
+ # --- test/CI fixture helper: produce a *locatable* build ---------------------
431
+
432
+
433
+ def build_extension_with_compile_commands(
434
+ sources: Sequence[Path],
435
+ module_name: str,
436
+ build_dir: Path,
437
+ *,
438
+ only: Sequence[str] | None = None,
439
+ ) -> Path:
440
+ """Build the f2py/meson extension the way `CMakeLists.txt` does, but with
441
+ an explicit `--build-dir` so `compile_commands.json` and the object files
442
+ persist afterwards (see the module docstring's "build-pipeline gap").
443
+
444
+ This performs the library sources' ONE compilation — the same one a real
445
+ `ngate build` would have performed, had its generated CMake pinned a
446
+ location — so a driver subsequently linking the resulting objects is not
447
+ looking at a second body of machine code. It is not part of T4's own
448
+ compile/link/run contract (`build_and_run_driver` above never calls
449
+ this); it exists so tests and CI can hand `build_and_run_driver` a real,
450
+ locatable extension build without reaching into a throwaway temp
451
+ directory numpy.f2py orphaned.
452
+
453
+ Returns the path to `<build_dir>/bbdir/compile_commands.json`.
454
+ """
455
+ build_dir = Path(build_dir)
456
+ build_dir.mkdir(parents=True, exist_ok=True)
457
+ argv = [
458
+ sys.executable,
459
+ "-m",
460
+ "numpy.f2py",
461
+ "-c",
462
+ "--backend",
463
+ "meson",
464
+ "--build-dir",
465
+ str(build_dir),
466
+ "-m",
467
+ module_name,
468
+ *[str(s) for s in sources],
469
+ ]
470
+ if only:
471
+ argv.extend(["only:", *only, ":"])
472
+ # f2py's meson backend both (a) writes its generated wrapper sources
473
+ # into the current working directory before copying them into
474
+ # build_dir (so cwd cannot BE build_dir — that copy collides with
475
+ # itself, "are the same file") and (b) moves the finished extension to
476
+ # cwd when the build completes ("move exec to root"). Use a cwd that is
477
+ # a sibling of build_dir, still under this function's caller-supplied
478
+ # directory, so nothing lands in the repository root.
479
+ run_cwd = build_dir.parent / f"{build_dir.name}-f2py-cwd"
480
+ run_cwd.mkdir(parents=True, exist_ok=True)
481
+ completed = subprocess.run(argv, cwd=run_cwd, capture_output=True, text=True, check=False)
482
+ if completed.returncode != 0:
483
+ raise DriverRunError(
484
+ f"building the f2py extension failed ({completed.returncode}):\n"
485
+ f" $ {' '.join(argv)}\n{completed.stdout}\n{completed.stderr}"
486
+ )
487
+ compile_commands = build_dir / "bbdir" / "compile_commands.json"
488
+ if not compile_commands.exists():
489
+ raise FileNotFoundError(
490
+ f"expected {compile_commands} after building the extension, found nothing "
491
+ "(meson's build directory layout may have changed)"
492
+ )
493
+ return compile_commands
494
+
495
+
496
+ def build_cxx_extension_with_compile_commands(
497
+ module,
498
+ header_names: str | list[str],
499
+ native_sources: Sequence[Path],
500
+ module_name: str,
501
+ build_dir: Path,
502
+ ) -> Path:
503
+ """The C++/pybind11 analogue of `build_extension_with_compile_commands`.
504
+
505
+ Generates a pybind11 bindings TU (`generators.pybind_gen`) and a
506
+ `CMakeLists.txt` (`generators.cmake_gen` — already emits
507
+ `CMAKE_EXPORT_COMPILE_COMMANDS ON`, see that module's docstring) for
508
+ `module`/`native_sources`, then configures and builds them with an
509
+ explicit `-B build_dir`, so — unlike a real `ngate build`'s `pip
510
+ wheel .` (which drives scikit-build-core into a throwaway, pip-managed
511
+ build tree; see this module's docstring for the analogous, still-open
512
+ f2py-era gap on the CMake side) — `compile_commands.json` and the
513
+ compiled `.o`/extension files persist at a known location afterwards.
514
+
515
+ This performs `native_sources`' ONE compilation; a driver subsequently
516
+ linking the resulting objects is not looking at a second body of
517
+ machine code. Like its Fortran counterpart, it exists purely for
518
+ test/CI isolation — `build_and_run_driver` never calls this itself.
519
+
520
+ Returns the path to `<build_dir>/compile_commands.json`. The built
521
+ extension module itself lands directly under `build_dir` (CMake's
522
+ default `pybind11_add_module` output location for a single-config
523
+ generator) — see `_cxx_extension_search_dir`.
524
+ """
525
+ from .generators import cmake_gen, pybind_gen
526
+
527
+ try:
528
+ import pybind11
529
+ except ImportError as exc: # pragma: no cover - environment-dependent
530
+ raise DriverRunError(
531
+ "building the C++/pybind11 extension requires the 'pybind11' "
532
+ "package to be importable (for its CMake package config dir)"
533
+ ) from exc
534
+
535
+ build_dir = Path(build_dir)
536
+ build_dir.mkdir(parents=True, exist_ok=True)
537
+
538
+ src_dir = build_dir.parent / f"{build_dir.name}-src"
539
+ native_dir = src_dir / "native"
540
+ native_dir.mkdir(parents=True, exist_ok=True)
541
+
542
+ for src in native_sources:
543
+ src = Path(src)
544
+ (native_dir / src.name).write_bytes(src.read_bytes())
545
+
546
+ # The implementation sources #include their own header(s) by name, so
547
+ # those headers must live alongside them in native_dir too — copied from
548
+ # wherever the caller's sources actually are (the same directory as
549
+ # native_sources, since that is the service's own native/ tree).
550
+ if native_sources:
551
+ source_dir = Path(native_sources[0]).parent
552
+ for header_name in ([header_names] if isinstance(header_names, str) else header_names):
553
+ header_src = source_dir / header_name
554
+ if header_src.exists():
555
+ (native_dir / header_name).write_bytes(header_src.read_bytes())
556
+
557
+ bindings_source = pybind_gen.generate_bindings(module, header_names)
558
+ bindings_name = f"{module.name}_bindings.cpp"
559
+ (native_dir / bindings_name).write_text(bindings_source)
560
+
561
+ cmake_sources = [f"native/{Path(s).name}" for s in native_sources] + [
562
+ f"native/{bindings_name}"
563
+ ]
564
+ cmake_text = cmake_gen.generate_cmake(module, module_name, cmake_sources)
565
+ (src_dir / "CMakeLists.txt").write_text(cmake_text)
566
+
567
+ configure_argv = [
568
+ "cmake",
569
+ "-S",
570
+ str(src_dir),
571
+ "-B",
572
+ str(build_dir),
573
+ f"-Dpybind11_DIR={pybind11.get_cmake_dir()}",
574
+ # pybind11 still defaults to its "classic" Python search (CMake's
575
+ # deprecated FindPythonInterp/FindPythonLibs) for projects whose
576
+ # `cmake_minimum_required` predates 3.27 — which cmake_gen's
577
+ # generated CMakeLists deliberately is (3.18, for older distros).
578
+ # Classic mode reads the ALL-CAPS `PYTHON_EXECUTABLE` and ignores
579
+ # `Python_EXECUTABLE` entirely, so without this flag the interpreter
580
+ # below is silently discarded ("Manually-specified variables were not
581
+ # used by the project") and pybind11 picks whatever `python3.9`-ish
582
+ # binary happens to sit first on PATH — a pyenv shim, say. The
583
+ # extension then gets built with THAT interpreter's ABI tag and
584
+ # `oracle._import_extension` fails with a bare ModuleNotFoundError,
585
+ # because a `.cpython-39-darwin.so` is invisible to a 3.11 run.
586
+ # Forcing FindPython mode makes `Python_EXECUTABLE` authoritative, so
587
+ # the oracle's extension is always built for the interpreter that is
588
+ # about to import it — the whole point of a same-process comparison.
589
+ # (A real `ngate build` never hits this: scikit-build-core sets this
590
+ # same variable itself before configuring.)
591
+ "-DPYBIND11_FINDPYTHON=ON",
592
+ f"-DPython_EXECUTABLE={sys.executable}",
593
+ ]
594
+ completed = subprocess.run(configure_argv, capture_output=True, text=True, check=False)
595
+ if completed.returncode != 0:
596
+ raise DriverRunError(
597
+ f"configuring the C++/pybind11 extension failed ({completed.returncode}):\n"
598
+ f" $ {' '.join(configure_argv)}\n{completed.stdout}\n{completed.stderr}"
599
+ )
600
+
601
+ build_argv = ["cmake", "--build", str(build_dir)]
602
+ completed = subprocess.run(build_argv, capture_output=True, text=True, check=False)
603
+ if completed.returncode != 0:
604
+ raise DriverRunError(
605
+ f"building the C++/pybind11 extension failed ({completed.returncode}):\n"
606
+ f" $ {' '.join(build_argv)}\n{completed.stdout}\n{completed.stderr}"
607
+ )
608
+
609
+ compile_commands = build_dir / "compile_commands.json"
610
+ if not compile_commands.exists():
611
+ raise FileNotFoundError(
612
+ f"expected {compile_commands} after building the extension, found nothing "
613
+ "(CMake's build directory layout may have changed)"
614
+ )
615
+ return compile_commands
616
+
617
+
618
+ def _cxx_extension_search_dir(build_dir: Path) -> Path:
619
+ """Where the built pybind11 extension `.so`/`.pyd` lands.
620
+
621
+ CMake's default `LIBRARY_OUTPUT_DIRECTORY` for a `pybind11_add_module`
622
+ target, with no `install()` step run and a single-config generator
623
+ (Unix Makefiles/Ninja — what `cmake`'s platform default resolves to on
624
+ the platforms this runs on), is the build directory itself.
625
+ """
626
+ return Path(build_dir)
@@ -0,0 +1,5 @@
1
+ """Oracle drivers: generate a native program that replays golden.json's calls.
2
+
3
+ See `fortran.py` for the Fortran generator (T3 of the verification-layers
4
+ plan, design-verification-layers.md §2).
5
+ """