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.
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
---
|
||||
name: build-firmware
|
||||
description: 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
|
||||
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
|
||||
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
|
||||
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.
|
||||
Reference in New Issue
Block a user