Packages

Nerves System - OpenWRT One (MediaTek MT7981B / Filogic 820)

Current section

Files

Jump to
nerves_system_openwrt_one scripts wrap-firmware.sh
Raw

scripts/wrap-firmware.sh

#!/usr/bin/env bash
#
# wrap-firmware.sh — turn a Nerves `.fw` into a bootable OpenWRT One FIT image
#
# Background: a Nerves `.fw` file is a zip containing a squashfs rootfs (with
# the Erlang VM and your application's release inside). The OpenWRT One boot
# chain expects a U-Boot FIT image (`openwrt-one-initramfs.itb`) containing
# kernel + DTB + cpio.gz initramfs. This script bridges the two:
#
# 1. Extract data/rootfs.img (squashfs) from the .fw
# 2. Extract data/Image from the .fw
# 3. Extract data/mt7981b-openwrt-one.dtb from the .fw
# 4. unsquashfs the rootfs into a temp dir
# 5. Add the bits the kernel initramfs needs that the squashfs lacks:
# - /init shell script that mounts devtmpfs and execs /sbin/init
# - /dev/console char device node (5,1)
# 6. Repack as cpio.gz
# 7. mkimage everything into a FIT (.itb) using the system's openwrt-one.its
#
# Usage:
# ./scripts/wrap-firmware.sh <input.fw> <output.itb> [output.ubi]
#
# If <output.ubi> is given, the script also produces a multi-volume UBI
# image suitable for `mtd write` to /dev/mtd5 from a running Linux
# (or `mtd write spi-nand0 ... 0x100000 ...` from U-Boot). The UBI
# layout matches the OpenWrt 24.10 factory: ubootenv/ubootenv2/fip/fit/
# rootfs_data, where `fit` is the .itb we just built and `fip` is the
# pre-built ATF blob from prebuilt/openwrt-one-fip.bin.
#
# Requires (host): unzip, unsquashfs, cpio, gzip, mkimage, ubinize,
# sudo (for mknod & preserving file ownership in the cpio).
#
# This is a Phase 2 development convenience script. Phase 3 will replace
# this with proper fwup.conf integration so `mix firmware` produces the
# .itb directly and `mix upload` can write to NAND via UBI volumes.
set -euo pipefail
if [ $# -lt 2 ] || [ $# -gt 3 ]; then
echo "Usage: $0 <input.fw> <output.itb> [output.ubi]" >&2
exit 1
fi
FW_FILE="$1"
OUT_ITB="$2"
OUT_UBI="${3:-}"
if [ ! -f "$FW_FILE" ]; then
echo "ERROR: $FW_FILE does not exist" >&2
exit 1
fi
# Locate this script's directory so we can find the .its template alongside it.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SYSTEM_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
ITS_TEMPLATE="${SYSTEM_DIR}/openwrt-one.its"
if [ ! -f "$ITS_TEMPLATE" ]; then
echo "ERROR: ITS template not found at $ITS_TEMPLATE" >&2
exit 1
fi
# Tool checks
for tool in unzip unsquashfs cpio gzip sudo; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "ERROR: required tool '$tool' not found in PATH" >&2
exit 1
fi
done
# mkimage gets special treatment: prefer /usr/bin/mkimage (the distro-built
# u-boot-tools) over any nerves_system mkimage that may be earlier on PATH
# when this script is invoked from inside `mix upload`. The system one in
# Nerves' host/bin is compiled with -DMKIMAGE_DTC="" because Buildroot
# doesn't set CONFIG_MKIMAGE_DTC_PATH; that mkimage's internal dtc invoke
# becomes `system(" -I dts ...")` and the shell tries to run `-I` as a
# command, exploding this script. The distro mkimage has the right dtc
# path baked in.
if [ -x /usr/bin/mkimage ]; then
MKIMAGE=/usr/bin/mkimage
elif command -v mkimage >/dev/null 2>&1; then
MKIMAGE=$(command -v mkimage)
else
echo "ERROR: required tool 'mkimage' not found in PATH" >&2
exit 1
fi
WORK="$(mktemp -d -t openwrt-one-wrap.XXXXXX)"
trap 'sudo rm -rf "$WORK"' EXIT
echo "==> Working in $WORK"
# 1-3: extract the three things we need from the .fw
echo "==> Extracting kernel, dtb, rootfs from $FW_FILE"
unzip -p "$FW_FILE" data/Image > "$WORK/Image"
unzip -p "$FW_FILE" data/mt7981b-openwrt-one.dtb > "$WORK/mt7981b-openwrt-one.dtb"
unzip -p "$FW_FILE" data/rootfs.img > "$WORK/rootfs.squashfs"
# 4: unsquashfs the rootfs (sudo to preserve owners/permissions)
echo "==> Unpacking squashfs..."
sudo unsquashfs -no-progress -d "$WORK/rootfs" "$WORK/rootfs.squashfs" > /dev/null
# 5: add the initramfs essentials. The squashfs is built without device nodes
# or a top-level /init because Nerves normally mounts it as a real
# block-device root, not as initramfs. For our boot path we need both.
echo "==> Adding initramfs essentials (/init, /dev/console)"
# /init script: mount devtmpfs (so /dev/console etc. exist before erlinit
# tries to use them) and exec the real init (erlinit at /sbin/init).
sudo tee "$WORK/rootfs/init" > /dev/null <<'EOF'
#!/bin/sh
# devtmpfs does not get automounted for initramfs
/bin/mount -t devtmpfs devtmpfs /dev
# use the /dev/console device node from devtmpfs if possible (avoids
# glibc ttyname_r confusion). Wrapped in a subshell so a failing exec
# doesn't terminate the parent.
if (exec 0</dev/console) 2>/dev/null; then
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
fi
exec /sbin/init "$@"
EOF
sudo chmod +x "$WORK/rootfs/init"
# /dev/console — needed BEFORE devtmpfs is mounted for the redirect above.
sudo mkdir -p "$WORK/rootfs/dev"
sudo mknod -m 622 "$WORK/rootfs/dev/console" c 5 1
# 6: repack as cpio.gz (deterministic)
echo "==> Packing cpio.gz..."
( cd "$WORK/rootfs" && \
sudo find . -mindepth 1 | LC_ALL=C sort | \
sudo cpio --reproducible --quiet -o -H newc ) | gzip -9 > "$WORK/rootfs.cpio.gz"
CPIO_SIZE=$(stat -c%s "$WORK/rootfs.cpio.gz" 2>/dev/null || stat -f%z "$WORK/rootfs.cpio.gz")
echo " rootfs.cpio.gz: $((CPIO_SIZE / 1024 / 1024)) MiB"
# 7: build the FIT image
echo "==> Building FIT image..."
cp "$ITS_TEMPLATE" "$WORK/openwrt-one.its"
( cd "$WORK" && "$MKIMAGE" -f openwrt-one.its "$OUT_ITB" >/dev/null )
OUT_SIZE=$(stat -c%s "$OUT_ITB" 2>/dev/null || stat -f%z "$OUT_ITB")
echo "==> Wrote $OUT_ITB ($((OUT_SIZE / 1024 / 1024)) MiB)"
# 8 (optional): wrap the .itb in a multi-volume UBI image for SPI NAND.
if [ -n "$OUT_UBI" ]; then
UBINIZE=""
if command -v ubinize >/dev/null 2>&1; then
UBINIZE=ubinize
else
# Try the system's built host tree (Buildroot puts ubinize in host/sbin)
for cand in \
"${SYSTEM_DIR}/.nerves/artifacts/nerves_system_openwrt_one-portable-"*/host/sbin/ubinize \
"${SYSTEM_DIR}/.nerves/artifacts/nerves_system_openwrt_one-portable-"*/host/bin/ubinize; do
if [ -x "$cand" ]; then
UBINIZE="$cand"
break
fi
done
fi
if [ -z "$UBINIZE" ]; then
echo "WARNING: ubinize not found (install mtd-utils or build the system first), skipping UBI image generation" >&2
else
UBI_CFG_TEMPLATE="${SYSTEM_DIR}/ubinize-fit.cfg"
if [ ! -f "$UBI_CFG_TEMPLATE" ]; then
echo "WARNING: $UBI_CFG_TEMPLATE not found, skipping UBI image generation" >&2
else
echo "==> Building UBI image (multi-volume layout for SPI NAND)..."
UBI_CFG="$WORK/ubinize-fit.cfg"
# Generate a populated U-Boot environment binary so freshly
# flashed devices already have the Nerves metadata. Without
# this, NervesMOTD shows "unknown" until UBootEnv.write/1 is
# called manually -- nerves_runtime caches KV at boot so just
# writing later doesn't help.
#
# We parse meta-* lines from the .fw file's meta.conf and emit
# an env text file in the format mkenvimage expects, then turn
# it into a 64 KiB binary block (matching /etc/fw_env.config's
# env_size = 0x10000).
MKENVIMAGE=""
if command -v mkenvimage >/dev/null 2>&1; then
MKENVIMAGE=mkenvimage
else
for cand in "${SYSTEM_DIR}/.nerves/artifacts/nerves_system_openwrt_one-portable-"*/host/bin/mkenvimage; do
if [ -x "$cand" ]; then MKENVIMAGE="$cand"; break; fi
done
fi
if [ -z "$MKENVIMAGE" ]; then
echo "WARNING: mkenvimage not found. ubootenv volume will be empty," >&2
echo " meaning Nerves metadata won't be populated until you" >&2
echo " call UBootEnv.write/1 manually after first boot." >&2
# Still need an env binary for ubinize, even if empty.
printf '\xff%.0s' $(seq 1 126976) > "$WORK/uboot-env.bin"
else
echo "==> Generating U-Boot env binary from .fw metadata..."
# Extract meta-* lines from meta.conf and convert to env format.
# meta.conf entries look like: meta-product=openwrt_one_test
# meta-version=0.1.0
# meta-author="Herman verschooten"
# We strip surrounding quotes and rename them to nerves_fw_*.
ENV_TXT="$WORK/uboot-env.txt"
# Start from the OpenWrt U-Boot default env template so the
# baked env contains a complete bootcmd / boot_* / ubi_* /
# bootmenu_* tree. With those present U-Boot accepts our
# env directly (CRC matches at 0x1F000), no warnings from
# boardid, and Nerves still gets nerves_fw_* alongside.
cat "${SYSTEM_DIR}/prebuilt/uboot-env-template.txt" > "$ENV_TXT"
{
printf '\n# nerves_fw_* keys appended at build time:\n'
# Slot tracking: assume single-slot install on first flash.
printf 'nerves_fw_active=a\n'
# Pull each meta-* field from meta.conf, transform name.
unzip -p "$FW_FILE" meta.conf | awk '
/^meta-/ {
# Remove "meta-" prefix
sub(/^meta-/, "")
# Split on first "="
eq = index($0, "=")
if (eq == 0) next
key = substr($0, 1, eq - 1)
val = substr($0, eq + 1)
# Strip surrounding double quotes if present
gsub(/^"|"$/, "", val)
# Map fwup meta-* names to Nerves env keys
if (key == "product") printf "a.nerves_fw_product=%s\n", val
else if (key == "version") printf "a.nerves_fw_version=%s\n", val
else if (key == "platform") printf "a.nerves_fw_platform=%s\n", val
else if (key == "architecture") printf "a.nerves_fw_architecture=%s\n", val
else if (key == "author") printf "a.nerves_fw_author=%s\n", val
else if (key == "description") printf "a.nerves_fw_description=%s\n", val
else if (key == "vcs-identifier") printf "a.nerves_fw_vcs_identifier=%s\n", val
else if (key == "misc") printf "a.nerves_fw_misc=%s\n", val
else if (key == "uuid") printf "a.nerves_fw_uuid=%s\n", val
}
'
} >> "$ENV_TXT"
echo " appended nerves_fw_* keys:"
grep '^nerves_fw_\|^a\.nerves_fw_' "$ENV_TXT" | sed 's/^/ /'
# -r: redundant env (5-byte header = 4-byte CRC + 1-byte flags)
# because we have ubootenv + ubootenv2 volumes. Without -r the
# CRC check in fw_printenv / Erlang UBootEnv fails because
# the data is offset by one byte.
# env_size 0x1F000 (= one UBI LEB) MUST match CONFIG_ENV_SIZE
# in OpenWrt's mt7981 U-Boot. We start from the OpenWrt
# default env template (with bootcmd, boot_*, ubi_*, etc.)
# and append nerves_fw_* keys, so the resulting env satisfies
# both U-Boot (full boot scripts present, CRC matches) and
# Nerves (nerves_fw_* present for KV reads).
"$MKENVIMAGE" -r -s 0x1F000 -o "$WORK/uboot-env.bin" "$ENV_TXT"
fi
# The template uses BOARD_DIR and BINARIES_DIR placeholders that
# we substitute. Point BOARD_DIR at the system tree (so it can
# find prebuilt/openwrt-one-fip.bin) and BINARIES_DIR at $WORK
# (where we just produced the .itb and uboot-env.bin). The .itb
# in $WORK has the name openwrt-one-initramfs.itb because that's
# what the .cfg references; copy our $OUT_ITB to that name.
cp "$OUT_ITB" "$WORK/openwrt-one-initramfs.itb"
sed -e "s|BOARD_DIR|${SYSTEM_DIR}|g" \
-e "s|BINARIES_DIR|${WORK}|g" \
"$UBI_CFG_TEMPLATE" > "$UBI_CFG"
"$UBINIZE" -p 128KiB -m 2048 -o "$OUT_UBI" "$UBI_CFG"
UBI_SIZE=$(stat -c%s "$OUT_UBI" 2>/dev/null || stat -f%z "$OUT_UBI")
echo "==> Wrote $OUT_UBI ($((UBI_SIZE / 1024 / 1024)) MiB)"
fi
fi
fi
echo ""
echo "To boot via TFTP from U-Boot (no NAND changes):"
echo " tftpboot \$loadaddr $(basename "$OUT_ITB")"
echo " bootm"
if [ -n "$OUT_UBI" ] && [ -f "$OUT_UBI" ]; then
echo ""
echo "To install on NAND from a running OpenWrt (or any Linux on this board):"
echo " scp $(basename "$OUT_UBI") root@<ip>:/tmp/"
echo " ssh root@<ip>"
echo " ubidetach -p /dev/mtd5 2>/dev/null || true"
echo " flash_erase /dev/mtd5 0 0"
echo " nandwrite -p /dev/mtd5 /tmp/$(basename "$OUT_UBI")"
echo " reboot"
echo ""
echo "To install on a blank NAND from U-Boot:"
echo " tftpboot \$loadaddr $(basename "$OUT_UBI")"
echo " ubi detach"
echo " mtd erase ubi"
echo " mtd write spi-nand0 \$loadaddr 0x100000 \$filesize"
echo " reset"
fi