Skip to content

Commit 83c8d92

Browse files
sawenzelclaude
andcommitted
Report why PyROOT is missing and fail --csg required on it
This fixes a problem in o2-cad-to-tgeo's CSG path when PyROOT does not import, and adds a self-test. - The import error was discarded, so a broken environment showed only as "ROOT unavailable" on every part. - --csg required then shipped all accepted parts as meshes and exited 0. - The converter now names the interpreter and the import error up front. - --csg required stops before reading the STEP file; --csg auto warns and goes on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1982073 commit 83c8d92

2 files changed

Lines changed: 86 additions & 6 deletions

File tree

‎Detectors/CADSupport/tools/O2_CADtoTGeo.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2403,6 +2403,59 @@ def disc_like(cable_first: bool):
24032403
return tally.failures
24042404

24052405

2406+
def run_missing_root_self_test() -> int:
2407+
"""Assert that a PyROOT that does not import is reported with its cause, and that
2408+
--csg required refuses to run instead of shipping the accepted parts as meshes.
2409+
2410+
Returns the number of failures; prints one line per check.
2411+
"""
2412+
import subprocess
2413+
import tempfile
2414+
2415+
tally = _Checks()
2416+
report = tally.report
2417+
2418+
print("\nCSG without PyROOT: name the cause, and --csg required fails")
2419+
2420+
# Runs the converter with `import ROOT` raising, as in a broken environment.
2421+
blocker = ("import sys, runpy\n"
2422+
"class _NoRoot:\n"
2423+
" def find_spec(self, name, path=None, target=None):\n"
2424+
" if name == 'ROOT':\n"
2425+
" raise ImportError('libCore.so: cannot open shared object file')\n"
2426+
"sys.meta_path.insert(0, _NoRoot())\n"
2427+
"sys.argv = sys.argv[1:]\n"
2428+
"sys.path.insert(0, sys.argv[0].rsplit('/', 1)[0])\n"
2429+
"runpy.run_path(sys.argv[0], run_name='__main__')\n")
2430+
step = _Path(__file__).resolve().parent.parent / "examples" / "as1-oc-214.stp"
2431+
if not step.exists():
2432+
report(False, "the example STEP file is there", f"missing: {step}")
2433+
return tally.failures
2434+
2435+
def convert(mode):
2436+
with tempfile.TemporaryDirectory() as out:
2437+
res = subprocess.run([sys.executable, "-c", blocker, str(_Path(__file__).resolve()),
2438+
str(step), "--output-folder", out, "--csg", mode],
2439+
capture_output=True, text=True)
2440+
return res.returncode, res.stdout + res.stderr
2441+
2442+
code, log = convert("required")
2443+
report(code != 0 and "PyROOT" in log and "libCore.so" in log,
2444+
"--csg required stops, naming PyROOT and the import error",
2445+
f"exit {code}; " + (log.strip().splitlines() or ["no output"])[-1][:160])
2446+
report("PyROOT" in log and "Placement check" not in log,
2447+
"and it stops before reading the STEP file",
2448+
"the conversion ran" if "Placement check" in log else "stopped early")
2449+
2450+
code, log = convert("auto")
2451+
report(code == 0 and "libCore.so" in log,
2452+
"--csg auto still converts, and says why the CSG parts are not shipped",
2453+
f"exit {code}")
2454+
2455+
print(f"\n{tally.checks} checks, {tally.failures} failure(s)")
2456+
return tally.failures
2457+
2458+
24062459
def _recognized_inner_wall(face, rec) -> Optional[bool]:
24072460
"""Decide, by measurement, which side of a RECOGNIZED quadric is outside the solid.
24082461
@@ -4844,6 +4897,7 @@ def main():
48444897
+ run_duplicate_placement_self_test()
48454898
+ run_multibody_leaf_self_test()
48464899
+ run_name_filter_self_test()
4900+
+ run_missing_root_self_test()
48474901
+ run_in_field_media_self_test()
48484902
+ run_bom_token_self_test()) else 0)
48494903
if args.step is None:
@@ -4854,6 +4908,16 @@ def main():
48544908
print_geom(step_path)
48554909
return
48564910

4911+
if args.csg in ("auto", "required"):
4912+
# CSG shapes are written through PyROOT; say up front when it does not import.
4913+
root_error = import_csg_hook().root_import_error()
4914+
if root_error and args.csg == "required":
4915+
ap.error(f"--csg required needs PyROOT, which does not import in {sys.executable}: "
4916+
f"{root_error}")
4917+
if root_error:
4918+
print(f"[WARN] PyROOT does not import in {sys.executable}: {root_error}. Accepted CSG "
4919+
"parts will ship one tier down (exact surfaces or mesh).")
4920+
48574921
out_folder = _Path(args.output_folder)
48584922

48594923
clip_box = None

‎Detectors/CADSupport/tools/cadsupport/hook.py‎

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,23 @@
2525
from cadsupport import emit, planar, primitives as prim, recognise # noqa: E402
2626

2727

28+
_ROOT_IMPORT_ERROR = False
29+
30+
31+
def root_import_error():
32+
"""None when PyROOT imports in this interpreter, else the error it raised, as text."""
33+
global _ROOT_IMPORT_ERROR
34+
if _ROOT_IMPORT_ERROR is False:
35+
try:
36+
import ROOT # noqa: F401
37+
_ROOT_IMPORT_ERROR = None
38+
except Exception as exc: # noqa: BLE001
39+
_ROOT_IMPORT_ERROR = f"{type(exc).__name__}: {exc}"
40+
return _ROOT_IMPORT_ERROR
41+
42+
2843
def have_root():
29-
try:
30-
import ROOT # noqa: F401
31-
return True
32-
except Exception: # noqa: BLE001
33-
return False
44+
return root_import_error() is None
3445

3546

3647
def scaled_to_cm(shape, scale_to_cm):
@@ -133,13 +144,18 @@ def recognise_and_emit(def_shapes, def_names, scale_to_cm, out_folder, sanitize_
133144
f"{len(flat_files)} as flat halfspace solids)")
134145
if n_csg and not root_available:
135146
n_deferred = sum(1 for r in records if r.get("shapeDeferred"))
136-
print(f" [WARN] PyROOT is not importable in this interpreter: {n_deferred} accepted "
147+
print(f" [WARN] PyROOT is not importable in {sys.executable} ({root_import_error()}): "
148+
f"{n_deferred} accepted "
137149
"CSG part(s) were NOT emitted and geom.C dispatches them one tier down. "
138150
"csg_report.json records each as 'csg deferred: ROOT unavailable'. Run "
139151
"`python3 -m cadsupport.emit --from-json <output folder>` from the directory holding the cadsupport "
140152
"package, under the O2 environment, then "
141153
"reconvert (or re-run the gate), to ship them as CSG.")
142154
if mode == "required":
155+
deferred = [r for r in records if r.get("shapeDeferred")]
156+
if deferred:
157+
raise ValueError(f"--csg required: {len(deferred)} accepted part(s) were not written "
158+
f"because PyROOT does not import ({root_import_error()})")
143159
failed = [r for r in records if not r["accepted"]]
144160
if failed:
145161
lines = [f"--csg required: {len(failed)}/{len(records)} leaf solid(s) are not CSG:"]

0 commit comments

Comments
 (0)