The interpreter bind took each link's grandparent, so ~/.local/bin/python3 exposed all of ~/.local inside the sandbox. A tree is now bound only when it is a Python prefix (lib/pythonX.Y) or a venv (pyvenv.cfg), never / or the home folder or above it. Every other link is bound as a single file, and a path inside a bound tree is not bound again, since bwrap cannot mount over a link in a read-only tree.
The sandbox bound only /usr, /lib and /bin. On the CI runner setup-python puts Python under /opt/hostedtoolcache, so the worker could not start and every core_probe and campaign run crashed (7 tests, plus their replay without jsonschema). A venv, pyenv or uv do the same. The interpreter's prefix and every link on the way to it are now bound read-only, each at its own path. Reproduced with uv's Python 3.13 outside /usr, 92 tests pass under it and under the system Python.
bwrap now binds only /usr, the /lib* and /bin links and the files of /etc the loader, fonts, TLS, the clock and user lookups read. Home, /tmp and /mnt are empty, the core, content and staged system directory read-only, the scratch folder the one writable path, every namespace unshared. --require-sandbox refuses to run without it. The worker imports nothing from the repository.
core_probe starts its worker with only the --env values, the worker clears its own environment before loading the core, and a working bubblewrap runs it with a read-only tree, no network and the user's credentials masked. The record names the isolation used. A timeout kills the whole process group. campaign.py starts core_probe, git, make and romfetch with the allowlist, make inside the same sandbox. --record refuses a folder that is not a checkout root.
campaign.py takes buildbot nightlies (or a build of the pinned commit), the test content of tests/campaign/content.yml through romfetch, and runs core_probe for the files, boot, path, alias, variants and bench checks. A fixed rule table settles hle_fallback, required, path and aliases with the record ids as evidence, the rest goes to measurements/findings.jsonl. Resumable state, load, disk and budget guards, retries, content reference counting with an LRU cap, and an autorun job after the nightly.
Each line of measurements/<emulator>.jsonl now holds the whole run:
every repetition, core logs, per-frame hashes and raw timings. The gzip
data files, the data schema and the split and join code are gone, and
core_probe --record appends one line. record_version moves to 3.
The seven records are rewritten once: each new line equals its old line
joined with its data file, and rebuilds the first-layout file its id
hashes.
The site compiles the index and the SQLite files from measurements/
instead of a pinned clone. The pin, the sync script, the CI clone steps,
the freshness area and its refresh handler are gone. core_probe --record
writes into measurements/ of the checkout by default, and a test fails
when a line or data file HEAD holds changed or vanished.
A record is now one line of records/<emulator>.jsonl in the measurements repository, under 20 KB, and a gzip sidecar holding every repetition, the core logs and the raw timings, its sha256 in the line. record_version 2, core-probe-data.schema.json for the sidecar. core_probe --record appends to a measurements clone and refuses a directory inside this repository's tracked tree.
The seven records of issue 85 moved to Abdess/retrobios-measurements: each line plus its sidecar rebuilds the first file, and keeps its first id.
A check is inconclusive when the compared observable differs between repetitions, a run fails to load or a file changes during it. A disagreement on another observable stays flagged without voiding the comparison. Core log lines name the staged directories instead of the host paths.
core_probe.py loads a libretro core with no window, runs it for a
number of frames and reports the files it opened (strace), its log
lines, frame, audio and save-state hashes and its speed. bench repeats
a run after warm-up and gives median, mean, deviation and range; check
swaps one system file between runs and compares one observable.
--record writes a JSON record with the core build, frontend commit,
host and every input by hash, validated against
core-probe-record.schema.json. A busy host is refused, and a
repetition that disagrees with the first is flagged.