# Install the potrace + agg development headers so `pypotrace` can build from source. # # Why a playbook and not a one-liner: `pypotrace` is an **sdist that compiles at install # time**, so every machine and every CI runner that resolves it needs these headers present # FIRST. That makes this a recurring per-box action, not a one-off. Requested by forseti # (pewpewstudio) for `core.image_pipeline`, which vectorises raster art into laser-ready # contours; the operator chose pypotrace over shelling out to the potrace binary (2026-08-28). # # Run: scripts/elway infra-ops@ --playbook playbooks/install-potrace-headers.yaml # Rerunnable: a second run shows the install `skipped`. # # ⚠ TWO THINGS THAT WILL SEND YOU DOWN THE WRONG PATH ON A BOX WHERE THIS FAILS # # 1. **Only libagg is a pkg-config consumer. potrace is not.** # # libagg /usr/lib/x86_64-linux-gnu/pkgconfig/libagg.pc present # potrace NO .pc file — found via /usr/include/potracelib.h and the library # # So `pkg-config --exists potrace` returns FALSE on a correctly configured box. It looks # exactly like the cause and never is. The real build error names libagg and only libagg: # # Package libagg was not found in the pkg-config search path. # Package 'libagg', required by 'virtual:world', not found # # A wrong model that produces a plausible-looking diagnostic costs more than no model. # # 2. **libagg's pkg-config modversion disagrees with its Debian package version.** # # pkg-config --modversion libagg -> 2.7.0 # dpkg version -> 1:2.6.1-r134+dfsg1-2+b1 # # Comparing those two numbers convinces you the wrong package is installed. It is not a # problem; it is upstream's version vs Debian's packaging of it. # # Both of these were learned on the nh3-dev install and are recorded here rather than in an # althing thread, at forseti's suggestion, because a thread is not where the next person looks. vars: probe_venv: /tmp/pypotrace-probe # The user whose toolchain builds the probe. The headers are installed system-wide # as root; the build check is a developer action and runs as this user. dev_user: lkraven steps: - name: Install the potrace and agg development headers shell: sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libpotrace-dev libagg-dev when: "! dpkg -s libpotrace-dev >/dev/null 2>&1 || ! dpkg -s libagg-dev >/dev/null 2>&1" verify: - name: libagg's pkg-config file is discoverable (this is the one that actually gates the build) shell: pkg-config --exists libagg changed_when: "false" - name: potrace's header is present (NOT via pkg-config — it ships no .pc) shell: test -f /usr/include/potracelib.h changed_when: "false" - name: pypotrace COMPILES against them # ⚠ `uv` is NOT on a non-interactive ssh PATH — infra-ops gets # /usr/local/bin:/usr/bin:/bin:/usr/games and nothing else. It also lives in a # different place on every box: /home/lkraven/bin/uv on nh3-dev, # /home/infra-ops/.local/bin/uv on nh3-extdev. Search rather than assume, and say # so loudly if it is genuinely absent — a build probe that silently does not run # is the failure this whole playbook exists to prevent. shell: | # ...and on nh3-dev it is inside a 0700 home, so `test -x` from infra-ops fails # even with the right absolute path — the directory cannot be traversed. The # headers are system-wide (root's business); the build probe is a DEVELOPER # action and has to run as the user who owns the toolchain. RUNAS={{ dev_user }} UV="" for c in /home/{{ dev_user }}/bin/uv /home/{{ dev_user }}/.local/bin/uv /usr/local/bin/uv; do sudo -u "$RUNAS" test -x "$c" && { UV="$c"; break; } done [ -n "$UV" ] || { echo "no uv reachable as $RUNAS; cannot run the build probe"; exit 1; } echo " using uv at $UV (as $RUNAS)" sudo -u "$RUNAS" rm -rf {{ probe_venv }} sudo -u "$RUNAS" "$UV" venv {{ probe_venv }} >/dev/null 2>&1 sudo -u "$RUNAS" "$UV" pip install --python {{ probe_venv }}/bin/python pypotrace 2>&1 | tail -2 changed_when: "false" - name: and the built extension actually TRACES, which a successful build does not prove # A square must come back as one curve of four CornerSegments. If the extension linked # against something wrong it can still import and return nonsense; the geometry is the # assertion, not the import. shell: | sudo -u {{ dev_user }} {{ probe_venv }}/bin/python -c " import numpy as np, potrace a = np.zeros((40,40), np.uint32); a[10:30,10:30] = 1 curves = list(potrace.Bitmap(a).trace()) segs = [s for c in curves for s in c] assert len(curves) == 1, curves assert len(segs) == 4, segs assert {type(s).__name__ for s in segs} == {'CornerSegment'}, segs " changed_when: "false" - name: Remove the probe venv shell: sudo -u {{ dev_user }} rm -rf {{ probe_venv }} changed_when: "false"