Files
esh-pfi-infrastructure/playbooks/install-potrace-headers.yaml
T
vh 590b55f7d8 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.
2026-08-28 13:38:12 -07:00

101 lines
4.9 KiB
YAML

# 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@<host> --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"