From 590b55f7d8c223153448c8c139ec6e64d19c7dca Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Fri, 28 Aug 2026 13:38:12 -0700 Subject: [PATCH] feat(playbooks): potrace/agg headers, with the two traps that mislead pypotrace is an sdist that compiles at install time, so every machine and every CI runner resolving it needs these headers first. That makes it a recurring per-box action rather than the one-off it arrived as. Two things learned installing it on nh3-dev are recorded here rather than left in an althing thread, at forseti's suggestion, because a thread is not where the next person looks: Only libagg is a pkg-config consumer. potrace ships no .pc file and is found via potracelib.h directly, so `pkg-config --exists potrace` returns false on a correctly configured box. It looks exactly like the cause and never is. libagg's pkg-config modversion is 2.7.0 while its Debian package version is 1:2.6.1-r134. Comparing those two numbers convinces you the wrong package is installed. The verify phase asserts the geometry, not the import: a square must come back as one curve of four CornerSegments. An extension linked against the wrong thing can import cleanly and return nonsense, so a successful build is not evidence the module works. Getting the build probe to run took three passes and the reason is worth keeping. uv is not on a non-interactive ssh PATH; it is in a different place on each box; and on nh3-dev it sits inside a 0700 home, so even the correct absolute path fails `test -x` for the ssh user because the directory cannot be traversed. The headers are system-wide and root's business, but the build check is a developer action and has to run as the user who owns the toolchain. --- playbooks/install-potrace-headers.yaml | 100 +++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 playbooks/install-potrace-headers.yaml 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"