Harden optional science toolbelt probe

This commit is contained in:
Brandon Schneider 2026-05-12 05:12:15 -05:00
parent 3ac033db9b
commit fdb6761786
4 changed files with 134 additions and 17 deletions

View file

@ -22,9 +22,16 @@ Equivalent npm convenience commands:
```bash ```bash
npm run setup-science-light npm run setup-science-light
npm run setup-science-all
npm run probe-science npm run probe-science
``` ```
`setup-science-light` installs the pure Python and wheel-backed baseline.
`setup-science-all` also asks for Dedalus and `liboqs-python`; those packages
still need native libraries on the host. On Arch/CachyOS, Dedalus needed
`fftw-openmpi`, and `liboqs-python` was completed by installing liboqs under
`$HOME/_oqs`.
Native command-line tools are listed in: Native command-line tools are listed in:
```text ```text

View file

@ -7,7 +7,8 @@
"setup-cad-env": "cd 5-Applications/text-to-cad && python3.11 -m venv .venv && ./.venv/bin/pip install -r requirements-cad.txt", "setup-cad-env": "cd 5-Applications/text-to-cad && python3.11 -m venv .venv && ./.venv/bin/pip install -r requirements-cad.txt",
"verify-cad": "cd 5-Applications/text-to-cad && ./.venv/bin/python -c \"import build123d; import OCP; print('CAD dependencies OK')\"", "verify-cad": "cd 5-Applications/text-to-cad && ./.venv/bin/python -c \"import build123d; import OCP; print('CAD dependencies OK')\"",
"setup-science-light": "uv venv .venv-science && . .venv-science/bin/activate && uv pip install -r requirements-optional-science.txt", "setup-science-light": "uv venv .venv-science && . .venv-science/bin/activate && uv pip install -r requirements-optional-science.txt",
"probe-science": "python3 scripts/probe_science_toolbelt.py" "setup-science-all": "uv venv --python 3.11 .venv-science && . .venv-science/bin/activate && uv pip install -r requirements-optional-science.txt 'dedalus>=3.0.5' 'liboqs-python>=0.14.1'",
"probe-science": "sh -c 'if [ -x .venv-science/bin/python ]; then .venv-science/bin/python scripts/probe_science_toolbelt.py; else python3 scripts/probe_science_toolbelt.py; fi'"
}, },
"dependencies": { "dependencies": {
"better-sqlite3": "^12.4.1" "better-sqlite3": "^12.4.1"

View file

@ -12,8 +12,9 @@ Exit code:
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import importlib import importlib.metadata
import json import json
import os
import shutil import shutil
import subprocess import subprocess
import sys import sys
@ -115,34 +116,136 @@ COMMAND_TOOLS: tuple[CommandTool, ...] = (
) )
def _module_version(module: Any) -> str | None: _DISTRIBUTION_NAMES: dict[str, str] = {
for attr in ("__version__", "VERSION", "version"): "pywavelets": "PyWavelets",
value = getattr(module, attr, None) }
if value is None:
continue
if callable(value): def _distribution_version(tool: PythonTool) -> str | None:
try: name = _DISTRIBUTION_NAMES.get(tool.key, tool.key)
value = value() try:
except Exception: return importlib.metadata.version(name)
continue except importlib.metadata.PackageNotFoundError:
return str(value) return None
return None
def _first_line(text: str) -> str:
for line in text.splitlines():
stripped = line.strip()
if stripped:
return stripped
return text.strip()
def _liboqs_env() -> dict[str, str]:
env = dict(os.environ)
if "OQS_INSTALL_PATH" in env:
return env
home_install = Path.home() / "_oqs"
if (home_install / "lib" / "liboqs.so").exists() or (
home_install / "lib64" / "liboqs.so"
).exists():
env["OQS_INSTALL_PATH"] = str(home_install)
return env
def _has_native_liboqs() -> bool:
candidates = [
Path(os.environ["OQS_INSTALL_PATH"])
for key in ("OQS_INSTALL_PATH",)
if key in os.environ
]
candidates.append(Path.home() / "_oqs")
for root in candidates:
if (root / "lib" / "liboqs.so").exists() or (
root / "lib64" / "liboqs.so"
).exists():
return True
for pattern in ("/usr/lib/liboqs.so*", "/usr/local/lib/liboqs.so*"):
if list(Path("/").glob(pattern.lstrip("/"))):
return True
return False
def _probe_python(tool: PythonTool) -> dict[str, Any]: def _probe_python(tool: PythonTool) -> dict[str, Any]:
installed_version = _distribution_version(tool)
if installed_version is None:
return {
**asdict(tool),
"available": False,
"error": "package is not installed in this Python environment",
}
if tool.key == "liboqs-python" and not _has_native_liboqs():
return {
**asdict(tool),
"available": False,
"installed_version": installed_version,
"error": "Python package is installed, but native liboqs.so is missing",
}
marker = "__SCIENCE_TOOLBELT_PROBE__"
code = f"""
import importlib
import json
module = importlib.import_module({tool.module!r})
version = None
for attr in ("__version__", "VERSION", "version"):
value = getattr(module, attr, None)
if value is None:
continue
if callable(value):
try:
value = value()
except Exception:
continue
version = str(value)
break
print({marker!r} + json.dumps({{"version": version, "path": getattr(module, "__file__", None)}}))
"""
env = _liboqs_env() if tool.key == "liboqs-python" else dict(os.environ)
try: try:
module = importlib.import_module(tool.module) result = subprocess.run(
[sys.executable, "-c", code],
capture_output=True,
text=True,
timeout=20,
check=False,
env=env,
)
except Exception as exc: except Exception as exc:
return { return {
**asdict(tool), **asdict(tool),
"available": False, "available": False,
"installed_version": installed_version,
"error": f"{type(exc).__name__}: {exc}", "error": f"{type(exc).__name__}: {exc}",
} }
payload = None
for line in result.stdout.splitlines():
if line.startswith(marker):
payload = json.loads(line[len(marker) :])
if result.returncode != 0 or payload is None:
error = _first_line(result.stderr) or _first_line(result.stdout)
return {
**asdict(tool),
"available": False,
"installed_version": installed_version,
"error": error or f"import subprocess exited {result.returncode}",
}
return { return {
**asdict(tool), **asdict(tool),
"available": True, "available": True,
"version": _module_version(module), "version": installed_version,
"path": getattr(module, "__file__", None), **(
{"module_version": payload.get("version")}
if payload.get("version") and payload.get("version") != installed_version
else {}
),
"path": payload.get("path"),
} }

View file

@ -22,5 +22,11 @@ zstd
graphviz graphviz
paraview paraview
# CFD/PDE native helpers
# Arch/CachyOS: fftw-openmpi
# Chemistry / materials native helpers # Chemistry / materials native helpers
openbabel openbabel
# Cryptography native helpers
# liboqs may need a local source install when no distro package exists.