Pin the nightly, and keep the toolchain off the image layer

478e173 · Gabriel Morell · 2026-09-16 18:46

6 files +68 -22
Message
{commit_body(@commit)}

Files changed

modified .forgejo/workflows/firmware.yml
+28 −13
@@ -101,13 +101,15 @@ jobs:
101 101 set -euo pipefail
102 102
103 103 # Espressif's own Rust image, pinned to a version rather than
104 − # `esp32c6_latest`. The pin is load-bearing twice over: it fixes the
105 − # ESP-IDF-capable toolchain, and it fixes the *nightly* the image
106 − # installed when it was built. `rust-toolchain.toml` asks for
107 − # "nightly", rustup uses whatever nightly is already installed rather
108 − # than fetching a newer one, and a current nightly does not build
109 − # `std` for this target at all (see CLAUDE.md, failure 1 —
110 − # `AT_FDCWD`).
104 + # `esp32c6_latest`, for the ESP-IDF-capable toolchain it carries:
105 + # rustup, ldproxy and espflash.
106 + #
107 + # It is NOT what pins the compiler — rust-toolchain.toml is. Run #5
108 + # is why that distinction is written down: the image's `nightly` was
109 + # missing rust-src, so rustup resynced the channel to satisfy the
110 + # toolchain file and pulled the current nightly, which cannot build
111 + # `std` for this target at all (CLAUDE.md, failure 1 — `AT_FDCWD`).
112 + # A dated channel has nothing to resync to.
111 113 #
112 114 # Kept next to the command that uses it. Run #4 lost the image
113 115 # argument from the `docker run` below in an edit, podman quietly
@@ -183,18 +185,25 @@ jobs:
183 185 # and the image's `esp` user need not agree, and root can write
184 186 # either way.
185 187 #
186 − # HOME points inside the workspace so ~/.espressif and ~/.cargo are
187 − # cacheable; RUSTUP_HOME stays in the image, where the pinned
188 − # nightly already lives — pointing it at the workspace would make
189 − # rustup fetch a current nightly, which is the one thing this pin
190 − # exists to prevent.
188 + # HOME, CARGO_HOME and RUSTUP_HOME all point inside the workspace,
189 + # so they are cacheable — and so that nothing writes into the image.
190 + # Run #5 tried the image's own /home/esp/.rustup and rustup failed
191 + # with "Invalid cross-device link (os error 18)": renaming a file out
192 + # of a lower overlayfs layer is EXDEV under rootless podman.
193 + #
194 + # The toolchain that lands there is pinned by rust-toolchain.toml to
195 + # a dated nightly. Run #5 also showed why that matters: the image's
196 + # `nightly` was missing rust-src, so rustup went to satisfy the
197 + # toolchain file and resynced the channel — pulling *today\'s*
198 + # nightly, the one that cannot build std for this target at all.
199 + # A dated channel has nothing to resync to.
191 200 docker run --rm \
192 201 --user 0:0 \
193 202 "${mount[@]}" \
194 203 -w "$PWD/lambent-flex-c6" \
195 204 -e HOME="$PWD/.home" \
196 205 -e CARGO_HOME="$PWD/.home/.cargo" \
197 − -e RUSTUP_HOME=/home/esp/.rustup \
206 + -e RUSTUP_HOME="$PWD/.home/.rustup" \
198 207 -e LAMBENT_FLEX_VERSION="$VERSION" \
199 208 -e HOST_UID="$(id -u)" \
200 209 -e HOST_GID="$(id -g)" \
@@ -217,6 +226,12 @@ jobs:
217 226
218 227 echo "container: ulimit-u=$(ulimit -u) pids.max=$(cat /sys/fs/cgroup/pids.max 2>/dev/null || echo n/a) jobs=$CARGO_BUILD_JOBS"
219 228
229 + # Install it explicitly, so a toolchain problem reports itself
230 + # here rather than as a confusing failure inside cargo. Reads
231 + # the channel from rust-toolchain.toml; idempotent once cached.
232 + rustup show active-toolchain || rustup toolchain install --profile minimal
233 + rustup show
234 +
220 235 cargo build --release
221 236
222 237 mkdir -p dist
modified CLAUDE.md
+19 −6
@@ -81,7 +81,8 @@ cargo build --release
81 81 cargo run --release # runner = ./flash.sh (espflash + the OTA layout)
82 82 ```
83 83
84 −Needs nightly + `rust-src` (see `rust-toolchain.toml`) for `build-std`, plus
84 +Needs the pinned nightly + `rust-src` (see `rust-toolchain.toml`; the pin is
85 +load-bearing — failure 1 below) for `build-std`, plus
85 86 `ldproxy` and `espflash` on PATH. `~/.espressif` holds ESP-IDF v5.1.1 and
86 87 v5.2.3 side by side; v5.2.3 is the one in use.
87 88
@@ -185,11 +186,23 @@ five of these were hit in sequence; the first is still a live local patch.
185 186 says ESP-IDF should skip `O_NOFOLLOW`, so the intended behaviour is plain
186 187 `chmod`.
187 188
188 − **Worked around by patching the rustup toolchain in place**, adding an
189 − `any(target_os = "espidf", target_os = "horizon")` arm that calls
190 − `set_perm`. Backup at `unix.rs.orig` beside it. **`rustup update` silently
191 − reverts this and the build breaks again** — if `AT_FDCWD` reappears, that's
192 − why. Not fixed by the dependency bump; it is purely a `std` problem.
189 + ~~**Worked around by patching the rustup toolchain in place**~~ —
190 + superseded on 2026-09-16 by **pinning the nightly**. `rust-toolchain.toml`
191 + now says `channel = "nightly-2026-04-27"`, whose `unix.rs` has no
192 + `set_perm_nofollow` in it at all (checked against the published `rust-src`
193 + for that date, not assumed). So there is nothing to patch, `rustup update`
194 + cannot break the build any more, and the same compiler builds it on a
195 + laptop and in CI — it is also the nightly the CI image ships.
196 +
197 + The old workaround was an in-place edit of the toolchain's `unix.rs`
198 + adding an `any(target_os = "espidf", target_os = "horizon")` arm calling
199 + `set_perm`, with a backup at `unix.rs.orig` beside it. Harmless to leave;
200 + nothing reads that toolchain now.
201 +
202 + **Moving the pin means re-checking this.** Take the date's `rust-src`
203 + tarball and grep `library/std/src/sys/fs/unix.rs` for `set_perm_nofollow`
204 + before trusting a newer nightly. It is purely a `std` problem — the
205 + dependency bump had nothing to do with it.
193 206
194 207 2. **~40 bindgen types emitted as opaque `{ _address: u8 }`**, including
195 208 `spi_transaction_t`, `struct stat`, `netif`, `esp_netif_config`,
modified README.md
+3 −2
@@ -126,8 +126,9 @@ cargo build --release
126 126 cargo run --release # -> flash.sh: espflash with the OTA layout
127 127 ```
128 128
129 −Requires the nightly toolchain with `rust-src` (see `rust-toolchain.toml`) for
130 −`build-std`, plus `ldproxy` and `espflash`.
129 +Requires the pinned nightly with `rust-src` (`rust-toolchain.toml` — rustup
130 +installs it for you), plus `ldproxy` and `espflash`. The pin is deliberate:
131 +current nightlies do not build `std` for this target.
131 132
132 133 `cargo run` goes through `flash.sh`, which adds three things OTA needs: the
133 134 two-slot `partitions.csv`, our own bootloader (rollback is a bootloader
modified rust-toolchain.toml
+18 −1
@@ -1,3 +1,20 @@
1 1 [toolchain]
2 −channel = "nightly"
2 +# Pinned, not "nightly".
3 +#
4 +# `build-std` needs a nightly, and an unpinned one means the build depends on
5 +# whatever was published this morning. That is not hypothetical here: the
6 +# current nightly's `set_perm_nofollow` in library/std/src/sys/fs/unix.rs calls
7 +# `libc::fchmodat(libc::AT_FDCWD, …)` on a `_` arm that espidf falls through
8 +# to, and the espidf libc module defines neither symbol — so `std` does not
9 +# compile at all (CLAUDE.md, failure 1). This date predates that: its
10 +# unix.rs has no `set_perm_nofollow` in it, verified against the published
11 +# rust-src for 2026-04-27 rather than assumed.
12 +#
13 +# It is also the nightly `espressif/idf-rust:esp32c6_1.95.0.0` ships, which is
14 +# what CI builds in — so the pin makes a laptop and CI agree instead of merely
15 +# being reproducible on their own.
16 +#
17 +# Moving this means re-checking that `std` still builds for a RISC-V espidf
18 +# target. `rustup update` no longer silently decides that for you.
19 +channel = "nightly-2026-04-27"
3 20 components = ["rust-src"]

Parents: 5614525