Files
espresso_frame/.claude/skills/build-firmware/SKILL.md
T
tfaour 8602ee3add Add build-firmware skill: native ESP-IDF build, no Docker needed
CI builds firmware inside the espressif/idf Docker image, but this
sandbox can't run containers at all -- it strips cap_sys_admin (and
blocks unshare) from the capability set even for root, which container
image-layer extraction and namespace setup both need. Confirmed by
hand: docker.io installs and dockerd starts fine, but even a bare
`docker run hello-world` fails to extract its own layer.

Works around it by installing ESP-IDF natively instead (git clone +
its own install.sh, scoped to just this project's esp32c6 target) --
the same way a developer would set it up on their own machine, needing
nothing this sandbox disallows. Verified end-to-end: both board
variants (devkit, xiao) build clean from a fresh checkout via the
packaged setup.sh/build.sh.
2026-07-27 22:31:30 +00:00

6.3 KiB

name, description
name description
build-firmware Compile the espresso_frame ESP32-C6 firmware (firmware/) for both board variants without Docker -- a native, non-container ESP-IDF v6.0 install. Use when asked to build the firmware, verify a firmware/main/*.c change actually compiles, or check both the devkit and xiao board targets.

Compiles firmware/ (ESP-IDF, targeting ESP32-C6) locally, without Docker -- CI's firmware-build-check.yml/firmware-release-build.yml build inside the espressif/idf:release-v6.0 container image, but this sandbox cannot run containers at all: docker.io installs and dockerd starts fine even as root, but the sandbox strips cap_sys_admin (and blocks the bare unshare syscall) from the capability set regardless of uid, which container image-layer extraction and namespace setup both require. Confirmed by hand: docker run hello-world fails to extract even the tiny hello-world layer ("failed to extract layer... operation not permitted" with the overlayfs snapshotter; "unshare: operation not permitted" even with the vfs storage driver instead). This is a hard restriction of the sandbox itself, not a permissions/setup problem -- don't spend time re-trying --privileged-equivalent flags or alternate storage drivers, none of it routes around a missing cap_sys_admin.

The workaround: skip containers entirely and install ESP-IDF the same way a developer would set it up on their own machine (git clone + ESP-IDF's own install.sh) -- that path needs nothing this sandbox disallows, just normal file/process operations.

Setup (once per fresh container)

bash .claude/skills/build-firmware/setup.sh

Installs (via real apt-get -- this container actually has root and a working package manager, unlike run-server's Chromium bootstrap which had neither):

  • OS build deps: python3/venv/pip, cmake, ninja-build, flex/bison/gperf, build-essential, libusb-1.0-0.
  • ESP-IDF itself: a shallow, single-branch, recursive-submodule clone of release/v6.0 (~700MB) into ~/.espressif-idf/esp-idf -- matches the IDF version CI's Docker image pins. Only clones once; re-running setup.sh never touches an existing checkout.
  • The esp32c6 toolchain + Python venv, via ESP-IDF's own ./install.sh esp32c6 -- scoped to just this project's one target (see firmware/README.md's board table), not every chip ESP-IDF supports, to keep the download/disk footprint down. install.sh is already idempotent on its own, so setup.sh always calls it rather than duplicating that check -- a re-run costs a few seconds once everything's cached.

Takes a few minutes on a cold run (mostly install.sh's own pip/tool downloads), well under a minute on a re-run. Needs real root (apt-get install) -- if this container ever runs as non-root, this setup doesn't apply as-is (would need the same non-root apt-download + dpkg-deb -x extraction dance run-server's setup.sh uses for Chromium).

Disk: budget ~4GB free before starting (esp-idf checkout + toolchain + Python env land around 3.4GB in ~/.espressif, plus the ~700MB checkout itself). Confirmed working with as little as ~7GB free.

Build

bash .claude/skills/build-firmware/build.sh            # devkit (default)
bash .claude/skills/build-firmware/build.sh xiao
bash .claude/skills/build-firmware/build.sh both        # both variants

Each board gets its own build directory and generated sdkconfig (see firmware/build_for_board.sh's own comment) -- building one never disturbs the other. build.sh auto-runs set-target esp32c6 the very first time a board is built (no generated sdkconfig yet); later builds skip straight to idf.py build. Extra arguments pass straight through to idf.py, e.g.:

bash .claude/skills/build-firmware/build.sh xiao flash -p /dev/ttyUSB0

flash/monitor need an actual attached device and serial port -- this sandbox has neither, so those only work when this skill runs somewhere hardware is actually plugged in (a real dev machine, or a differently-configured environment with device passthrough).

A clean build of one board takes ~30s once the target's already been configured (~1,000 build steps total split across both boards, most of it ESP-IDF's own components -- this project's own firmware/main/*.c and firmware/components/* sources are a small fraction of that and compile in a few seconds). Output lands at firmware/build/espresso_frame.bin (devkit) or firmware/build_xiao/espresso_frame.bin (xiao) -- both paths are gitignored (firmware/.gitignore... actually the repo root .gitignore's "ESP-IDF firmware build output" section), so nothing here needs cleaning up before a commit.

Verified

Both board variants (devkit set-target esp32c6 + build, xiao set-target esp32c6 + build) built successfully end-to-end using this exact setup.sh/build.sh pair, producing real espresso_frame.bin images with normal free-space margins (41%/36% of their respective app partitions) and no errors -- only one pre-existing, unrelated warning (battery.c's unused TAG when that file's logging is compiled out). This is a real compile check, not just a syntax read -- if a future change breaks the build, this skill will actually catch it.

Troubleshooting

  • docker: ... unshare: operation not permitted / failed to extract layer ... operation not permitted: expected in this sandbox, see the top of this file. Don't debug it further -- use this skill's native install instead.
  • ESP-IDF not found at ... -- run setup.sh first: build.sh's own check for a missing $IDF_DIR/export.sh -- run setup.sh (see above) before the first build.
  • idf.py: command not found if you try to run it directly: same gotcha firmware/build_for_board.sh already documents -- idf.py is normally a shell function from ESP-IDF's export.sh, not on PATH as a real executable, so it isn't inherited into a script's own subshell even after sourcing export.sh in your interactive shell first. Use build.sh (or build_for_board.sh, which calls python "$IDF_PATH/tools/idf.py" directly) instead of typing idf.py in a fresh script/subshell.
  • Disk pressure during install.sh: this environment runs close to full (single-digit GB free is normal, not a sign of a leak) -- df -h / before running setup.sh if a build mysteriously fails partway with a "no space left on device"-shaped error.