diff --git a/playbooks/install-potrace-headers.yaml b/playbooks/install-potrace-headers.yaml new file mode 100644 index 0000000..b29d27c --- /dev/null +++ b/playbooks/install-potrace-headers.yaml @@ -0,0 +1,100 @@ +# 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"