diff --git a/docs/optional-science-toolbelt.md b/docs/optional-science-toolbelt.md index 9899c6b0..266fe517 100644 --- a/docs/optional-science-toolbelt.md +++ b/docs/optional-science-toolbelt.md @@ -22,9 +22,16 @@ Equivalent npm convenience commands: ```bash npm run setup-science-light +npm run setup-science-all 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: ```text diff --git a/package.json b/package.json index a5472dd1..4be3f106 100644 --- a/package.json +++ b/package.json @@ -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", "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", - "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": { "better-sqlite3": "^12.4.1" diff --git a/scripts/probe_science_toolbelt.py b/scripts/probe_science_toolbelt.py index 46db5a87..76ac571e 100755 --- a/scripts/probe_science_toolbelt.py +++ b/scripts/probe_science_toolbelt.py @@ -12,8 +12,9 @@ Exit code: from __future__ import annotations import argparse -import importlib +import importlib.metadata import json +import os import shutil import subprocess import sys @@ -115,34 +116,136 @@ COMMAND_TOOLS: tuple[CommandTool, ...] = ( ) -def _module_version(module: Any) -> str | 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 - return str(value) - return None +_DISTRIBUTION_NAMES: dict[str, str] = { + "pywavelets": "PyWavelets", +} + + +def _distribution_version(tool: PythonTool) -> str | None: + name = _DISTRIBUTION_NAMES.get(tool.key, tool.key) + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + 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]: + 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: - 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: return { **asdict(tool), "available": False, + "installed_version": installed_version, "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 { **asdict(tool), "available": True, - "version": _module_version(module), - "path": getattr(module, "__file__", None), + "version": installed_version, + **( + {"module_version": payload.get("version")} + if payload.get("version") and payload.get("version") != installed_version + else {} + ), + "path": payload.get("path"), } diff --git a/system-packages-optional-science.txt b/system-packages-optional-science.txt index b6d3ca6a..19a3d5b8 100644 --- a/system-packages-optional-science.txt +++ b/system-packages-optional-science.txt @@ -22,5 +22,11 @@ zstd graphviz paraview +# CFD/PDE native helpers +# Arch/CachyOS: fftw-openmpi + # Chemistry / materials native helpers openbabel + +# Cryptography native helpers +# liboqs may need a local source install when no distro package exists.