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.
129 lines
6.3 KiB
Markdown
129 lines
6.3 KiB
Markdown
---
|
|
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.
|