// Finding the Python the third-party oracles run under. // // The oracles are what stop the QR writer and the PDF writer being checked // only against themselves: zxing-cpp decodes what we drew, python-qrcode // draws what we drew and we compare matrices, PyMuPDF and pypdf read the // documents back. An encoder tested against its own decoder proves the two // agree and nothing else. // // They need python3 with numpy, zxing-cpp, qrcode, pillow, pymupdf and // pypdf. A machine — or a shared CI runner — may not have those, and the // failure was a raw ModuleNotFoundError traceback that said nothing about // what to do. This resolves the interpreter and turns a missing dependency // into an instruction. // // Order: $CONTRA_PYTHON, then .venv/bin/python3 if `make oracles` has been // run, then python3. So a developer whose system already has the packages // needs no venv, and CI gets a reproducible one without changing the box. import { execFileSync } from "node:child_process"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const here = dirname(fileURLToPath(import.meta.url)); const repo = join(here, ".."); export function oraclePython() { if (process.env.CONTRA_PYTHON) return process.env.CONTRA_PYTHON; const venv = join(repo, ".venv", "bin", "python3"); if (existsSync(venv)) return venv; return "python3"; } // oracleEnv adds .oracles/ to PYTHONPATH when `make oracles` had to fall back // to `pip --target` — which it does on a python3 with no ensurepip, where a // venv cannot be created at all. Harmless when the directory is absent. function oracleEnv() { const target = join(repo, ".oracles"); if (!existsSync(target)) return process.env; const existing = process.env.PYTHONPATH; return { ...process.env, PYTHONPATH: existing ? `${target}:${existing}` : target, }; } // run executes one oracle and returns its stdout. // // It does NOT skip when the packages are missing. A skipped oracle is // indistinguishable from a passing one, and these guard the two documents a // person's whole account depends on being able to read back — that is // exactly the class of check this repo refuses to let degrade silently. // What it does is fail with the command that fixes it. export function run(script, input) { const python = oraclePython(); try { return execFileSync(python, [join(here, script)], { input: JSON.stringify(input), maxBuffer: 64 << 20, env: oracleEnv(), }).toString(); } catch (err) { const text = String(err.stderr || err.message || ""); if (/ModuleNotFoundError|No module named|ENOENT/.test(text)) { throw new Error( `the ${script} oracle cannot run: its Python packages are missing.\n` + ` Interpreter tried: ${python}\n` + ` Fix it with: make oracles\n` + ` (or set CONTRA_PYTHON to an interpreter that has numpy, zxing-cpp,\n` + ` qrcode, pillow, pymupdf and pypdf)\n\n` + text); } throw err; } }