Skip to main content

CHANGELOG.md

# Changelog

## v0.5.2 (2026-05-13)

Consistency follow-up to v0.5.1: `mix upload`'s wiring now matches
the `mix firmware` shape introduced in v0.5.1. User apps no longer
need to ship a ~25-line `defp upload_ota/1` shell-out helper.

* New `Mix.Tasks.OpenwrtOne.Upload` (`mix openwrt_one.upload`) wraps
  `scripts/upload-ota.sh`. Resolves the target host from the first
  arg, then `$MIX_TARGET_HOST`, then `nerves.local`; prefixes `root@`
  automatically.
* `README.md` OTA section: replaced the per-app `defp upload/1`
  boilerplate (single-target and multi-target variants) with a clean
  alias:

  ```elixir
  defp aliases do
    [
      firmware: ["firmware", "openwrt_one.firmware_post"],
      upload:   ["firmware", "openwrt_one.upload"]
    ]
  end
  ```

  Multi-target apps still need a small dispatcher to fall back to
  `mix nerves.upload` for non-openwrt_one targets — also shown in
  the README, but now ~6 lines instead of ~17.

## v0.5.1 (2026-05-13)

Fixes the first-boot failure after `mix burn`:

```
erlinit: No release found in /srv/erlang.
erlinit: Erlang installation not found.
```

Root cause: `mix firmware` baked the system-prebuilt
`data/openwrt-one-initramfs.itb` and `data/openwrt-one-nand.ubi` into
the `.fw` — both assembled at *system* build time, before the user's
Erlang release was merged into the squashfs. `mix burn` then wrote
the stale UBI to the recovery USB stick; after the USB dance, the
board booted into an initramfs with no `/srv/erlang`. The OTA path
(`mix upload`) avoided the bug by re-running `wrap-firmware.sh` at
upload time to rebuild a per-app `.itb` from the `.fw`'s squashfs.

Fix: a new `openwrt_one.firmware_post` Mix task rebuilds **both**
resources from the `.fw`'s combined squashfs and splices them back
into the `.fw` archive, updating `length` / `blake2b-256` in
`meta.conf` and preserving zip entry order. Users wire it into the
`mix firmware` lifecycle with a one-line alias in their `mix.exs`:

```elixir
defp aliases do
  [firmware: ["firmware", "openwrt_one.firmware_post"]]
end
```

With the alias in place, `mix firmware` produces a `.fw` whose
bundled `.itb` and `.ubi` already match the user release, so
`mix burn` and `mix upload` both work natively against the bundled
resources — no per-invocation repack.

Changes:

* New `Mix.Tasks.OpenwrtOne.FirmwarePost` in `lib/mix/tasks/`.
  Requires `ubinize` (from `mtd-utils`) and `b2sum` (from
  `coreutils`) on the host.
* `scripts/upload-ota.sh`: extract `data/openwrt-one-initramfs.itb`
  directly from the `.fw` instead of re-running `wrap-firmware.sh`.
  Errors out with the alias snippet if the resource is missing or
  zero-sized.
* `scripts/wrap-firmware.sh`: also search
  `~/.nerves/artifacts/.../host/{sbin,bin}` for `ubinize` and
  `mkenvimage` (Nerves caches artifacts there, not under
  `$SYSTEM_DIR/.nerves/artifacts`); fail with an install hint
  instead of warning + skipping when `ubinize` cannot be found.
* `mix.exs`: ship `lib/` in the hex package so the Mix task is
  picked up when the system is used as a dep.
* `README.md`: document `mtd-utils` and `coreutils` as host
  prerequisites; new "Step 1: wire up the firmware-post alias"
  section before "build the firmware and prepare a USB stick".

## v0.5.0 (2026-05-08)

Breaking: the embedded `NervesSystemOpenwrtOne.UBootEnvKVBackend` is
gone. It now lives in its own Hex package,
[nerves_uboot_env_ubi](https://hex.pm/packages/nerves_uboot_env_ubi),
under the `Nerves.Runtime.KVBackend.UBootEnvUBI` module.

Migration in your firmware app:

* Add to `mix.exs` deps:
  ```elixir
  {:nerves_uboot_env_ubi, "~> 0.1"}
  ```
* Update `config/target.exs`:
  ```elixir
  config :nerves_runtime,
    kv_backend: {Nerves.Runtime.KVBackend.UBootEnvUBI, []}
  ```
* The system dep can now stay `runtime: false` (the Nerves convention)
  since the system no longer ships any runtime BEAM modules.

Why: keeping the backend in the system forced users to drop
`runtime: false` on the system dep so the backend's BEAMs got
included in their release. That's a non-obvious footgun, and
extracting the backend lets it be properly tested + documented +
reused on other UBI-flash boards.

This release also rolls up the v0.4.x churn (which never made it
to Hex):

* `nerves_system_br` 1.33.4 → 1.33.7 (OTP 28.5, fwup 1.16.0,
  Buildroot 2025.11.3) — already shipped as v0.2.3.
* aarch64 toolchain 13.2.0 → 14.2.0 (GCC 13 → GCC 14) for
  multi-target compatibility with sibling Nerves systems.
* `BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y` so Buildroot's
  host mkimage works inside Docker on macOS.
* `wrap-firmware.sh` no longer needs `sudo` — uses
  `cpio --owner=0:0` to force ownership and `unsquashfs` runs as
  the invoking user. Drops the `--reproducible` flag which BSD
  cpio (macOS) doesn't support.
* `BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT` and `host-dtc` for
  build-time mkimage; `mix burn` USB-stick recovery flow; persistent
  `/root` via UBIFS on UBI volume 5 with lazy mkfs.ubifs;
  `nerves_motd` "Part usage" cell now reports real numbers.

## v0.4.4 (2026-05-07)

* `wrap-firmware.sh`: use numeric `cpio --owner=0:0` instead of
  `root:root`. BSD cpio (macOS default) tries to look up "root"
  as a group name and fails because the gid-0 group on macOS is
  "wheel", not "root". Numeric IDs are portable everywhere.

## v0.4.3 (2026-05-07)

* `wrap-firmware.sh`: drop `cpio --reproducible`. It's a GNU cpio
  extension; BSD cpio (macOS default) rejects it with
  `Option --reproducible is not supported`. Reproducible byte
  output isn't actually needed — the cpio just has to be a valid
  initramfs.

## v0.4.2 (2026-05-07)

Fix v0.4.1's regression: OTA boots failed because the bundled
build-time cpio.gz was the system-only rootfs without the user's
release at `/srv/erlang`, so erlinit died with "No release found"
and the device rolled back.

* `wrap-firmware.sh` is back to unpacking the combined squashfs
  rootfs and repacking as cpio.gz, but now without `sudo`:
  - `unsquashfs` runs as the invoking user (files come out user-
    owned, content intact).
  - `cpio --owner=root:root` forces ownership in the output cpio
    regardless of disk owners, so the result is byte-equivalent
    to the old sudo-based path.
  - `/dev/console` is no longer pre-created with `mknod`; `/init`
    mounts devtmpfs first which provides it.
* Reverts the `rootfs.cpio.gz` resource added to `fwup.conf` in
  v0.4.1 — no longer needed, .fw shrinks by ~9 MiB.

## v0.4.1 (2026-05-07)

OTA upload was supposed to work on macOS without sudo prompts.
This release was BROKEN — see v0.4.2 for the actual fix.

* `wrap-firmware.sh` no longer unsquashes the rootfs and re-cpio's it
  on the host. Previously this required `sudo unsquashfs`,
  `sudo mknod /dev/console`, and `sudo cpio`, which broke when
  invoked non-interactively (e.g. `mix upload` on macOS, where
  sudo can't prompt without a TTY).
* `fwup.conf` now bundles the build-time `rootfs.cpio.gz` alongside
  `rootfs.img` in the .fw archive. `wrap-firmware.sh` extracts it
  directly into the FIT image — no transformation needed.
* Costs ~9 MiB extra in the .fw (the cpio.gz copy), but eliminates
  the sudo dependency end-to-end.

## v0.4.0 (2026-05-07)

Toolchain bump to align with sibling Nerves systems on aarch64.

* `nerves_toolchain_aarch64_nerves_linux_gnu` 13.2.0 → 14.2.0
  (GCC 13 → GCC 14). Buildroot toolchain URL + `BR2_TOOLCHAIN_EXTERNAL_GCC_*`
  flag updated to match.
* Enables `BR2_PACKAGE_HOST_UBOOT_TOOLS_FIT_SUPPORT=y` so Buildroot's
  host mkimage is built with FIT support and host-dtc as a dependency.
  Without it, building inside the Nerves Docker container on macOS
  failed at the post-image step with "sh: 1: -I: not found".

The toolchain change forces a full system rebuild on first build, but
no source/API changes.

## v0.3.0 (2026-05-07)

`/root` (and `/data` via the existing symlink) is now persistent
across reboots, matching standard Nerves convention.

* `rootfs_data` (UBI volume 5) is now mounted at `/root` via a small
  `--pre-run-exec` script (`/usr/sbin/mount-data.sh`). The script
  lazily formats the volume with UBIFS on first boot and falls back
  to tmpfs if the persistent mount can't be brought up, so the
  device always boots even with unhealthy NAND. Without this, `/root`
  lived in the volatile rootfs (initramfs) and SSH host keys,
  NervesTime RTC drift fixes, etc. were regenerated on every reboot.
* `mkfs.ubifs` (`BR2_PACKAGE_MTD_MKFSUBIFS=y`) is now built into the
  target image so the lazy-format step works.
* `nerves_fw_application_part0_devpath=/root` (and `fstype=ubifs`,
  `target=/root`) is now baked into the U-Boot env, so
  `nerves_motd` resolves the data partition for its "Part usage"
  cell instead of falling back to "not available".

Backwards compatibility: existing v0.2.x installs upgrade cleanly.
The first boot of v0.3.0 formats `rootfs_data` (which was an empty
placeholder volume in v0.2.x). Rollback to v0.2.x is fine — the
older slot's initramfs ignores the UBIFS, runs in volatile mode.

## v0.2.3 (2026-05-06)

Routine upstream bump.

* `nerves_system_br` 1.33.4 → 1.33.7
  * Erlang/OTP 28.4.1 → 28.5
  * fwup 1.14.0 → 1.16.0
  * Buildroot 2025.11.2 → 2025.11.3

No system-side changes required.

## v0.2.2 (2026-04-09)

`mix burn` support via OpenWrt's NOR full-recovery mode.

* `mix burn` now prepares a FAT32 USB recovery stick that OpenWrt's
  SPI NOR recovery U-Boot can use to flash the entire SPI NAND —
  no serial console, no TFTP server, no typing of U-Boot commands.
  The stick contains the snand-preloader.bin (BL2) and our
  `openwrt-one-nand.ubi` (renamed to `factory.ubi`). See README.md
  "Initial install" for the full procedure.
* Ship `prebuilt/openwrt-one-snand-preloader.bin` from OpenWrt 24.10
  (same source + license as the FIP).
* `fwup.conf`: replace the erroring `complete` task with one that
  `mbr_write`s + `fat_mkfs`es + `fat_write`s the recovery files.
  The .fw file grows by ~33 MiB (the .ubi) + 234 KiB (preloader)
  but `mix burn` now Just Works with no per-app config.
* `post-createfs.sh`: stage the preloader into images/ so fwup can
  find it at firmware-build time.
* README: document USB stick recovery as the primary initial-install
  path; serial + TFTP demoted to "alternative".

## v0.2.1 (2026-04-08)

USB mass storage support.

* Linux: enable `CONFIG_SCSI`, `CONFIG_BLK_DEV_SD`, `CONFIG_USB_STORAGE`,
  `CONFIG_FAT_FS` + `CONFIG_VFAT_FS`, `CONFIG_EXFAT_FS`, and the
  matching NLS tables (`CP437`, `ISO8859-1`, `UTF-8`). USB sticks now
  enumerate as `/dev/sdN` and FAT/exFAT partitions mount and read.

## v0.2.0 (2026-04-08)

OTA + A/B slot support, kernel bump, several bug fixes that turned
session-1 workarounds into proper fixes.

### Added

* **A/B FIT slots:** UBI layout now uses `fit_a` (vol 3) and `fit_b`
  (vol 4) instead of a single `fit` volume. Each slot is sized at
  50 MiB. Active slot is selected at boot via
  `fit_${nerves_fw_active}` substitution in `ubi_read_production`.
* **Image-level boot fallback:** if `bootm` fails on the active slot,
  U-Boot's `boot_production` flips `nerves_fw_active`, `saveenv`s,
  and retries the other slot.
* **Bootcount-based runtime rollback:** OTA sets `upgrade_available=1`
  + `bootcount=0`; U-Boot's `nerves_count_attempt` script swaps slots
  once `bootcount > bootlimit` (default 3).
  `Nerves.Runtime.StartupGuard` clears the counters once the app is
  healthy.
* **`scripts/upload-ota.sh` + `scripts/apply-ota.exs`:** volume-level
  OTA via SFTP + `ubiupdatevol` + `fw_setenv`. Designed to be aliased
  as `mix upload` from the user app.
* **`NervesSystemOpenwrtOne.UBootEnvKVBackend`:** custom
  `Nerves.Runtime.KVBackend` that reads via the Erlang `UBootEnv`
  library and writes via the C `fw_setenv` (which issues
  `UBI_IOCVOLUP`). Without this the default backend returns `:eperm`
  on every `KV.put` and breaks `validate_firmware/0`.
* **Full OpenWrt 24.10 default U-Boot env baked into ubootenv volumes**
  via `prebuilt/uboot-env-template.txt`, with the `0x1F000` env size
  matching `CONFIG_ENV_SIZE` in OpenWrt's mt7981 U-Boot. Eliminates the
  cosmetic `boardid: U-boot environment CRC32 mismatch` warning that
  was caused by sizing our env smaller than what U-Boot writes back.
* `CONFIG_IP_ADVANCED_ROUTER=y`, `CONFIG_IP_MULTIPLE_TABLES=y`,
  `CONFIG_IP_ROUTE_MULTIPATH=y` so VintageNet's policy-routing setup
  doesn't crash with `RTNETLINK answers: Operation not supported`.

### Changed

* **Linux 6.12 → 6.18.12.**
* **SPI NAND driver path:** dropped the `spi-mtk-snfi` attempt and
  the Etron 0x77-shifted-manufacturer-ID workaround. Now uses
  `spi-mt65xx` with the OpenWrt SPI calibration patch stack
  (patches 121, 330, 431-435, 930) plus `mtk_bmt`. The chip is
  actually a Winbond 256 MiB part, not the 128 MiB Etron we initially
  guessed.
* **U-Boot env path in `fw_env.config`:** `/dev/ubi0_0` and
  `/dev/ubi0_1` instead of `/dev/ubi0:ubootenv`. The Erlang
  `uboot_env` library uses plain `File.open` and doesn't understand
  the `:volname` shorthand that fwup-tool's `fw_printenv` accepts.

### Fixed

* mkimage invocation in `wrap-firmware.sh` now prefers
  `/usr/bin/mkimage` over Buildroot's host build, because the latter
  ships with `MKIMAGE_DTC=""` and explodes with
  `sh: 1: -I: not found` whenever it tries to run dtc internally.

## v0.1.0 (2026-04-07)

Initial Nerves system for the OpenWRT One.

* Linux 6.12 mainline kernel with small patches
* Custom DTS based on mainline `mt7981b-openwrt-one.dts` with full
  peripheral enablement
* WiFi 2.4 / 5 GHz with proper calibration from factory partition
* Both Ethernet ports (1 GbE LAN + 2.5 GbE WAN with EN8811H PHY)
* RTC, GPIO watchdog, LEDs, buttons
* NAND boot via UBI volumes (fip + fit + ubootenv + rootfs_data)
* Pre-built FIP from official OpenWrt 24.10 release