carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1// Finding the Python the third-party oracles run under.
2//
3// The oracles are what stop the QR writer and the PDF writer being checked
4// only against themselves: zxing-cpp decodes what we drew, python-qrcode
5// draws what we drew and we compare matrices, PyMuPDF and pypdf read the
6// documents back. An encoder tested against its own decoder proves the two
7// agree and nothing else.
8//
9// They need python3 with numpy, zxing-cpp, qrcode, pillow, pymupdf and
10// pypdf. A machine — or a shared CI runner — may not have those, and the
11// failure was a raw ModuleNotFoundError traceback that said nothing about
12// what to do. This resolves the interpreter and turns a missing dependency
13// into an instruction.
14//
15// Order: $CONTRA_PYTHON, then .venv/bin/python3 if `make oracles` has been
16// run, then python3. So a developer whose system already has the packages
17// needs no venv, and CI gets a reproducible one without changing the box.
18
19import { execFileSync } from "node:child_process";
20import { existsSync } from "node:fs";
21import { fileURLToPath } from "node:url";
22import { dirname, join } from "node:path";
23
24const here = dirname(fileURLToPath(import.meta.url));
25const repo = join(here, "..");
26
27export function oraclePython() {
28 if (process.env.CONTRA_PYTHON) return process.env.CONTRA_PYTHON;
29 const venv = join(repo, ".venv", "bin", "python3");
30 if (existsSync(venv)) return venv;
31 return "python3";
32}
33
34// oracleEnv adds .oracles/ to PYTHONPATH when `make oracles` had to fall back
35// to `pip --target` — which it does on a python3 with no ensurepip, where a
36// venv cannot be created at all. Harmless when the directory is absent.
37function oracleEnv() {
38 const target = join(repo, ".oracles");
39 if (!existsSync(target)) return process.env;
40 const existing = process.env.PYTHONPATH;
41 return {
42 ...process.env,
43 PYTHONPATH: existing ? `${target}:${existing}` : target,
44 };
45}
46
47// run executes one oracle and returns its stdout.
48//
49// It does NOT skip when the packages are missing. A skipped oracle is
50// indistinguishable from a passing one, and these guard the two documents a
51// person's whole account depends on being able to read back — that is
52// exactly the class of check this repo refuses to let degrade silently.
53// What it does is fail with the command that fixes it.
54export function run(script, input) {
55 const python = oraclePython();
56 try {
57 return execFileSync(python, [join(here, script)], {
58 input: JSON.stringify(input),
59 maxBuffer: 64 << 20,
60 env: oracleEnv(),
61 }).toString();
62 } catch (err) {
63 const text = String(err.stderr || err.message || "");
64 if (/ModuleNotFoundError|No module named|ENOENT/.test(text)) {
65 throw new Error(
66 `the ${script} oracle cannot run: its Python packages are missing.\n` +
67 ` Interpreter tried: ${python}\n` +
68 ` Fix it with: make oracles\n` +
69 ` (or set CONTRA_PYTHON to an interpreter that has numpy, zxing-cpp,\n` +
70 ` qrcode, pillow, pymupdf and pypdf)\n\n` + text);
71 }
72 throw err;
73 }
74}
75