neiam
/lambent-flex-c6
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
Mount the workspace the nested container can actually see
7636eed · Gabriel Morell · 2026-09-16 18:38
Message
{commit_body(@commit)}
Files changed
modified
.forgejo/workflows/firmware.yml
+54
−18
@@ -11,19 +11,20 @@ on:
| 11 | 11 | |
| 12 | 12 | jobs: |
| 13 | 13 | firmware: |
| 14 | − # On the host, NOT in a container — and the compiler runs in one `docker | |
| 15 | − # run` instead. | |
| 14 | + # No `container:` — the compiler runs in one nested `docker run` instead. | |
| 16 | 15 | # |
| 17 | 16 | # The obvious shape, `container: espressif/idf-rust:…`, does not work: the |
| 18 | 17 | # runner executes JavaScript actions with node *inside* the job container, |
| 19 | 18 | # and that image has no node (its apt line is git curl gcc clang |
| 20 | 19 | # ninja-build unzip libudev-dev tar xz-utils libusb libssl-dev pkg-config |
| 21 | − # python3 — and nothing else). `actions/checkout@v4` dies in one second, | |
| 22 | − # and `actions/cache@v4` would die next. | |
| 20 | + # python3 — and nothing else). `actions/checkout@v4` died in one second on | |
| 21 | + # run #1, and `actions/cache@v4` would have died next. | |
| 23 | 22 | # |
| 24 | − # Inverting it costs nothing and fixes the lot: checkout and cache run on | |
| 25 | − # the host where node exists, and the toolchain image is used for the one | |
| 26 | − # thing it is actually for. | |
| 23 | + # So the job takes the runner's own image, where node exists and checkout | |
| 24 | + # and cache work, and the toolchain image is used for the one thing it is | |
| 25 | + # actually for. Note that this job is itself in a container (this runner | |
| 26 | + # uses rootless podman), which is what the mount dance in "Build and | |
| 27 | + # package" is about. | |
| 27 | 28 | runs-on: self-hosted |
| 28 | 29 | |
| 29 | 30 | env: |
@@ -131,11 +132,46 @@ jobs:
| 131 | 132 | echo "host: nproc=$cores ulimit-u=$(ulimit -u) pids.max=$(cat /sys/fs/cgroup/pids.max 2>/dev/null || echo n/a)" |
| 132 | 133 | free -m 2>/dev/null | head -2 || true |
| 133 | 134 | |
| 134 | − # As root, then chown back: the workspace belongs to whatever user | |
| 135 | − # the runner runs as, the image's own user is `esp`, and the two | |
| 136 | − # need not agree. Root can write either way, and the chown at the | |
| 137 | − # end hands the results back so the steps after this one (and the | |
| 138 | − # cache) can read them. | |
| 135 | + # The job itself runs in a container (rootless podman), so `$PWD` | |
| 136 | + # here is a path inside *that* container, not on the host. Handing | |
| 137 | + # it to `-v` mounted an empty directory and run #3 died with "could | |
| 138 | + # not find Cargo.toml in /work/lambent-flex-c6". | |
| 139 | + # | |
| 140 | + # `--volumes-from` is the fix: the nested container inherits the job | |
| 141 | + # container's mounts at the same paths, so $PWD means the same thing | |
| 142 | + # on both sides. Podman writes its own container id to | |
| 143 | + # /run/.containerenv, which is more reliable than $(hostname) — | |
| 144 | + # act sets a hostname of its own. | |
| 145 | + cid="" | |
| 146 | + if [ -f /run/.containerenv ]; then | |
| 147 | + cid=$(sed -n 's/^id="\(.*\)"$/\1/p' /run/.containerenv) | |
| 148 | + fi | |
| 149 | + [ -n "$cid" ] || cid=$(hostname) | |
| 150 | + echo "job container: ${cid:-<none>}" | |
| 151 | + | |
| 152 | + mount=(--volumes-from "$cid") | |
| 153 | + if ! docker run --rm "${mount[@]}" -w "$PWD/lambent-flex-c6" \ | |
| 154 | + "$TOOLCHAIN_IMAGE" test -f Cargo.toml 2>/dev/null; then | |
| 155 | + # Not in a container after all (a runner label mapped to :host), | |
| 156 | + # or volumes-from is unavailable: a plain bind mount is then | |
| 157 | + # correct, because $PWD really is a host path. | |
| 158 | + echo "volumes-from did not expose the workspace; trying a bind mount" | |
| 159 | + mount=(-v "$PWD:$PWD") | |
| 160 | + | |
| 161 | + if ! docker run --rm "${mount[@]}" -w "$PWD/lambent-flex-c6" \ | |
| 162 | + "$TOOLCHAIN_IMAGE" test -f Cargo.toml 2>/dev/null; then | |
| 163 | + echo "Neither mount strategy exposes the checkout. Diagnostics:" >&2 | |
| 164 | + cat /etc/os-release 2>/dev/null | head -2 >&2 || true | |
| 165 | + echo "pwd=$PWD uid=$(id -u)" >&2 | |
| 166 | + cat /run/.containerenv 2>/dev/null >&2 || echo "(no /run/.containerenv)" >&2 | |
| 167 | + docker version --format '{{.Server.Version}}' >&2 2>/dev/null || true | |
| 168 | + exit 1 | |
| 169 | + fi | |
| 170 | + fi | |
| 171 | + | |
| 172 | + # Runs as root, then chowns the workspace back: the runner's user | |
| 173 | + # and the image's `esp` user need not agree, and root can write | |
| 174 | + # either way. | |
| 139 | 175 | # |
| 140 | 176 | # HOME points inside the workspace so ~/.espressif and ~/.cargo are |
| 141 | 177 | # cacheable; RUSTUP_HOME stays in the image, where the pinned |
@@ -144,10 +180,10 @@ jobs:
| 144 | 180 | # exists to prevent. |
| 145 | 181 | docker run --rm \ |
| 146 | 182 | --user 0:0 \ |
| 147 | − -v "$PWD:/work" \ | |
| 148 | − -w /work/lambent-flex-c6 \ | |
| 149 | − -e HOME=/work/.home \ | |
| 150 | − -e CARGO_HOME=/work/.home/.cargo \ | |
| 183 | + "${mount[@]}" \ | |
| 184 | + -w "$PWD/lambent-flex-c6" \ | |
| 185 | + -e HOME="$PWD/.home" \ | |
| 186 | + -e CARGO_HOME="$PWD/.home/.cargo" \ | |
| 151 | 187 | -e RUSTUP_HOME=/home/esp/.rustup \ |
| 152 | 188 | -e LAMBENT_FLEX_VERSION="$VERSION" \ |
| 153 | 189 | -e HOST_UID="$(id -u)" \ |
@@ -156,8 +192,8 @@ jobs:
| 156 | 192 | -e NUM_JOBS="$jobs" \ |
| 157 | 193 | -e CMAKE_BUILD_PARALLEL_LEVEL="$jobs" \ |
| 158 | 194 | -e MAKEFLAGS="-j$jobs" \ |
| 195 | + -e WORKSPACE="$PWD" \ | |
| 159 | 196 | --pids-limit 8192 \ |
| 160 | − "$TOOLCHAIN_IMAGE" \ | |
| 161 | 197 | bash -euo pipefail -c ' |
| 162 | 198 | # ESP-IDF builds with cmake and the image ships ninja but not |
| 163 | 199 | # cmake. embuild installs one into IDF_TOOLS_PATH when it has |
@@ -194,7 +230,7 @@ jobs:
| 194 | 230 | dist/flash-all.bin |
| 195 | 231 | cp partitions.csv dist/partitions.csv |
| 196 | 232 | |
| 197 | − chown -R "$HOST_UID:$HOST_GID" /work | |
| 233 | + chown -R "$HOST_UID:$HOST_GID" "$WORKSPACE" | |
| 198 | 234 | ' |
| 199 | 235 | |
| 200 | 236 | - name: Check it still fits in a slot |
modified
CLAUDE.md
+29
−17
@@ -456,26 +456,38 @@ failing on `AT_FDCWD`, something moved that pin. It is also why the workflow
| 456 | 456 | sets `RUSTUP_HOME` back into the image rather than into the cached workspace: |
| 457 | 457 | a fresh RUSTUP_HOME would have rustup fetch a current nightly and undo the pin. |
| 458 | 458 | |
| 459 | −**The job runs on the host and the compiler runs in one `docker run`.** The | |
| 459 | +**There is no `container:`; the compiler runs in a nested `docker run`.** The | |
| 460 | 460 | obvious shape — `container: espressif/idf-rust:…` — fails immediately, and run |
| 461 | −#1 of this repo is the proof: the runner executes JavaScript actions with node | |
| 462 | −*inside* the job container, and that image has no node at all (verified from | |
| 463 | −its registry config: `git curl gcc clang ninja-build unzip libudev-dev tar | |
| 464 | −xz-utils libusb libssl-dev pkg-config python3`, nothing else). | |
| 465 | −`actions/checkout@v4` died in one second; `actions/cache@v4` would have died | |
| 466 | −next. Inverted, checkout and cache run on the host where node exists, and the | |
| 467 | −image is used for the one thing it is for. | |
| 468 | − | |
| 469 | −Consequences of the inversion, all in the workflow: | |
| 470 | − | |
| 461 | +#1 is the proof: the runner executes JavaScript actions with node *inside* the | |
| 462 | +job container, and that image has no node at all (verified from its registry | |
| 463 | +config: `git curl gcc clang ninja-build unzip libudev-dev tar xz-utils libusb | |
| 464 | +libssl-dev pkg-config python3`, nothing else). `actions/checkout@v4` died in one | |
| 465 | +second; `actions/cache@v4` would have died next. So the job takes the runner's | |
| 466 | +own image, where node exists, and the toolchain image is used for compiling. | |
| 467 | + | |
| 468 | +Three consequences, each of them a run that failed first: | |
| 469 | + | |
| 470 | +- **The workspace has to be mounted with `--volumes-from`, not `-v`.** This | |
| 471 | + runner puts jobs in rootless podman containers, so `$PWD` inside a step is a | |
| 472 | + path in *that* container and a bind mount of it lands empty — run #3 died | |
| 473 | + with "could not find Cargo.toml in /work/lambent-flex-c6". `--volumes-from` | |
| 474 | + inherits the job container's mounts at the same paths, so `$PWD` means the | |
| 475 | + same thing on both sides. The container id comes from `/run/.containerenv` | |
| 476 | + (podman writes it there); `$(hostname)` is a fallback, since act sets a | |
| 477 | + hostname of its own. There is a bind-mount fallback for a runner label that | |
| 478 | + maps to `:host`, and a diagnostic dump if neither works. | |
| 479 | +- **Parallelism is capped at four jobs.** Run #2 died with `bash: fork: retry: | |
| 480 | + Resource temporarily unavailable` — the runner's cgroup allows 4096 pids and | |
| 481 | + the host has 64 cores, so cargo and ninja each defaulting to one job per core | |
| 482 | + is enough to exhaust it. `CARGO_BUILD_JOBS`, `NUM_JOBS`, | |
| 483 | + `CMAKE_BUILD_PARALLEL_LEVEL` and `MAKEFLAGS` are all set, since each covers a | |
| 484 | + different part of the build. | |
| 471 | 485 | - The container runs `--user 0:0` and `chown`s the workspace back at the end. |
| 472 | 486 | The runner's user and the image's `esp` user need not agree, and root can |
| 473 | − write either way. | |
| 474 | −- `HOME` points at `.home` inside the workspace, so `~/.espressif` (ESP-IDF | |
| 475 | − v5.2.3 plus toolchains, ~2GB, ~15 minutes to install) and `~/.cargo` land | |
| 476 | − somewhere `actions/cache` can see from the host. | |
| 477 | −- cmake is apt-installed in the container if missing — the image ships ninja | |
| 478 | − but not cmake. | |
| 487 | + write either way. `HOME` points at `.home` inside the workspace, so | |
| 488 | + `~/.espressif` (ESP-IDF v5.2.3 plus toolchains, ~2GB, ~15 minutes to install) | |
| 489 | + and `~/.cargo` land somewhere `actions/cache` can see. cmake is apt-installed | |
| 490 | + in the container if missing — the image ships ninja but not cmake. | |
| 479 | 491 | |
| 480 | 492 | Debian bookworm, so `libxml2.so.2` is the system soname and the symlink |
| 481 | 493 | workaround above is not needed there. |
Parents: 65e23ac