From 311f00a9c8215b5fbcf8e61c8a881ddb0873f3d8 Mon Sep 17 00:00:00 2001 From: Daniel Rossier Date: Sat, 7 Nov 2026 14:21:51 +0100 Subject: [PATCH] scripts: add the tezi-custom sources recovery-edgem.sh runs the installer that scripts/tezi-custom/build.sh produces, but that directory was never imported: the script could not work, and a Verdin flash needed a manual tezictl feed-add on the serial console, lost at every reboot. Take the sources from Infrabase and edgem1. They patch the Toradex installer only, so nothing here depends on Torizon or the capsules, and the board finds the feed deploy.sh publishes on IB_HTTP_FEED_PORT by itself. Ignore what the build leaves next to them. --- .gitignore | 7 ++ scripts/tezi-custom/build.sh | 122 +++++++++++++++++++++++++ scripts/tezi-custom/patch_fit.py | 44 +++++++++ scripts/tezi-custom/rc.local.append.in | 20 ++++ scripts/tezi-feed-serve.sh | 6 +- 5 files changed, 196 insertions(+), 3 deletions(-) create mode 100755 scripts/tezi-custom/build.sh create mode 100755 scripts/tezi-custom/patch_fit.py create mode 100644 scripts/tezi-custom/rc.local.append.in diff --git a/.gitignore b/.gitignore index e5cb6d927..b5ba5cf94 100755 --- a/.gitignore +++ b/.gitignore @@ -152,3 +152,10 @@ so3/so3/scripts/kconfig/zconf.tab.c *.attach.pf __pycache__/ /scripts.old/ + +# Left by scripts/tezi-custom/build.sh +scripts/tezi-custom/recovery/ +scripts/tezi-custom/boot.scr +scripts/tezi-custom/imx-boot-recoverytezi +scripts/tezi-custom/imx-boot-sd +scripts/tezi-custom/overlays.txt diff --git a/scripts/tezi-custom/build.sh b/scripts/tezi-custom/build.sh new file mode 100755 index 000000000..3f9b8b87d --- /dev/null +++ b/scripts/tezi-custom/build.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash + +# Copyright (c) 2025-2026 EDGEMTech SA + +# Build a customized Toradex Easy Installer (tezi.itb) for the verdin-imx8mp +# board: same as the upstream Toradex one, with /etc/rc.local extended to +# auto-register a local nginx image feed (see rc.local.append.in). +# +# Zero-config defaults: +# - host IP detected via `ip route get 1.1.1.1` (override: EDGEM_HOST_IP) +# - upstream downloaded from tezi.toradex.com into build/tmp/tezi-installer/ +# (override: TEZI_SOURCE_DIR — pre-existing local archive root) +# +# Output: tezi.itb in this directory + symlinks/copies of unchanged upstream +# artefacts. Launcher: scripts/recovery-edgem.sh + +set -euo pipefail + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +PROJECT_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd) +WORK=${WORK:-/tmp/tezi-custom-build} + +TEZI_VERSION=${TEZI_VERSION:-7.5.0+build.9} +TEZI_RELEASE=${TEZI_RELEASE:-9} +TEZI_ARCHIVE_URL=${TEZI_ARCHIVE_URL:-"https://tezi.toradex.com/artifactory/tezi-oe-prod-frankfurt/scarthgap-7.x.y/release/${TEZI_RELEASE}/verdin-imx8mp/tezi/tezi-run/oedeploy/Verdin-iMX8MP_ToradexEasyInstaller_${TEZI_VERSION}.zip"} +TEZI_CACHE_DIR=${TEZI_CACHE_DIR:-"$PROJECT_ROOT/build/tmp/tezi-installer"} +TEZI_SOURCE_DIR=${TEZI_SOURCE_DIR:-} + +for t in dumpimage unsquashfs mksquashfs fakeroot sha256sum python3 curl unzip ip; do + command -v "$t" >/dev/null || { echo "missing tool: $t" >&2; exit 1; } +done + +if [ -z "$TEZI_SOURCE_DIR" ]; then + TEZI_SOURCE_DIR="$TEZI_CACHE_DIR/Verdin-iMX8MP_ToradexEasyInstaller_$TEZI_VERSION" + if [ ! -f "$TEZI_SOURCE_DIR/tezi.itb" ]; then + echo "Toradex Easy Installer ${TEZI_VERSION} not in cache — downloading..." + mkdir -p "$TEZI_CACHE_DIR" + TMP_ZIP="$TEZI_CACHE_DIR/.tezi-${TEZI_VERSION}.zip" + curl -fL --retry 3 -o "$TMP_ZIP" "$TEZI_ARCHIVE_URL" + rm -rf "$TEZI_SOURCE_DIR" + unzip -q -d "$TEZI_CACHE_DIR" "$TMP_ZIP" + rm "$TMP_ZIP" + if [ ! -f "$TEZI_SOURCE_DIR/tezi.itb" ]; then + echo "Archive extracted but tezi.itb not at expected path: $TEZI_SOURCE_DIR" >&2 + echo "Inspect $TEZI_CACHE_DIR and set TEZI_SOURCE_DIR to the right directory." >&2 + exit 1 + fi + fi +elif [ ! -f "$TEZI_SOURCE_DIR/tezi.itb" ]; then + echo "TEZI_SOURCE_DIR=$TEZI_SOURCE_DIR does not contain tezi.itb" >&2 + exit 1 +fi +echo "Using upstream TEZI at: $TEZI_SOURCE_DIR" + +if [ -z "${EDGEM_HOST_IP:-}" ]; then + # Walk default routes in metric order; skip tunnel-like interfaces (vpn0, + # tun*, wg*, ...) so an active VPN doesn't capture the board-flashing IP. + while IFS= read -r dev; do + case "$dev" in vpn*|tun*|tap*|wg*|ppp*) continue ;; esac + EDGEM_HOST_IP=$(ip -4 -o addr show dev "$dev" 2>/dev/null \ + | awk 'NR==1 {sub(/\/.*/,"",$4); print $4}') + [ -n "$EDGEM_HOST_IP" ] && break + done < <(ip -4 route show default | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}') +fi +if [ -z "${EDGEM_HOST_IP:-}" ]; then + echo "Could not auto-detect host IP. Set EDGEM_HOST_IP= manually." >&2 + exit 1 +fi +# Port 8080 by default: the feed is served straight out of the build tree by +# scripts/tezi-feed-serve.sh, running as your user, so it cannot bind :80. +# Keep this in step with IB_HTTP_FEED_PORT in build/conf/local.conf — the URL +# is baked into the TEZI image here, so a port change means rebuilding it. +EDGEM_FEED_PORT=${EDGEM_FEED_PORT:-8080} +EDGEM_FEED_URL=${EDGEM_FEED_URL:-"http://${EDGEM_HOST_IP}:${EDGEM_FEED_PORT}/image_list.json"} +echo "Embedded feed URL: $EDGEM_FEED_URL" + +VENV=$SCRIPT_DIR/.venv +if ! "$VENV/bin/python" -c 'import libfdt' 2>/dev/null; then + python3 -m venv "$VENV" + "$VENV/bin/pip" -q install pylibfdt +fi + +rm -rf "$WORK" +mkdir -p "$WORK" + +sed "s|@EDGEM_FEED_URL@|$EDGEM_FEED_URL|g" "$SCRIPT_DIR/rc.local.append.in" > "$WORK/rc.local.append" + +dumpimage -T flat_dt -p 29 -o "$WORK/initramfs.squashfs" "$TEZI_SOURCE_DIR/tezi.itb" >/dev/null + +fakeroot bash -c " + set -e + cd '$WORK' + unsquashfs -d rootfs initramfs.squashfs >/dev/null + cat rc.local.append >> rootfs/etc/rc.local + mksquashfs rootfs new.sqfs -comp gzip -b 131072 -noappend -no-progress >/dev/null +" + +"$VENV/bin/python" "$SCRIPT_DIR/patch_fit.py" \ + "$TEZI_SOURCE_DIR/tezi.itb" \ + "$WORK/new.sqfs" \ + "$SCRIPT_DIR/tezi.itb" + +ln -sfn "$TEZI_SOURCE_DIR/boot.scr" "$SCRIPT_DIR/boot.scr" +ln -sfn "$TEZI_SOURCE_DIR/imx-boot-recoverytezi" "$SCRIPT_DIR/imx-boot-recoverytezi" +ln -sfn "$TEZI_SOURCE_DIR/imx-boot-sd" "$SCRIPT_DIR/imx-boot-sd" +ln -sfn "$TEZI_SOURCE_DIR/overlays.txt" "$SCRIPT_DIR/overlays.txt" + +# recovery/ must be a real subdirectory: when uuu resolves the relative paths +# in uuu.auto, the kernel walks `recovery/../` and if `recovery/` were a +# symlink to upstream Toradex it would end up loading the unmodified upstream +# tezi.itb. Only the uuu binaries are symlinked. uuu also unconditionally +# prepends the cmdlist's directory to each `-f` path, so absolute paths break +# with "recovery//home/...". Keep them relative. + +rm -rf "$SCRIPT_DIR/recovery" +mkdir -p "$SCRIPT_DIR/recovery" +ln -sfn "$TEZI_SOURCE_DIR/recovery/uuu" "$SCRIPT_DIR/recovery/uuu" +ln -sfn "$TEZI_SOURCE_DIR/recovery/uuu.exe" "$SCRIPT_DIR/recovery/uuu.exe" +cp "$TEZI_SOURCE_DIR/recovery/uuu.auto" "$SCRIPT_DIR/recovery/uuu.auto" + +echo +echo "tezi-custom built (feed=$EDGEM_FEED_URL). Launch with: scripts/recovery-edgem.sh" diff --git a/scripts/tezi-custom/patch_fit.py b/scripts/tezi-custom/patch_fit.py new file mode 100755 index 000000000..2146bbee7 --- /dev/null +++ b/scripts/tezi-custom/patch_fit.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2025-2026 EDGEMTech SA + +"""Replace /images/ramdisk-1/data in a FIT and refresh its sha256 hash.""" +import hashlib +import sys +import libfdt + +if len(sys.argv) != 4: + sys.exit(f"usage: {sys.argv[0]} ") + +orig_fit, new_ramdisk, out_fit = sys.argv[1:] + +with open(orig_fit, "rb") as f: + blob = bytearray(f.read()) +with open(new_ramdisk, "rb") as f: + new_data = f.read() + +fdt = libfdt.Fdt(blob) +node = fdt.path_offset("/images/ramdisk-1") +old_data = fdt.getprop(node, "data") +print(f"orig ramdisk-1 data: {len(old_data)} bytes") +print(f"new ramdisk-1 data: {len(new_data)} bytes") + +delta = len(new_data) - len(old_data) +if delta > 0: + fdt.resize(len(blob) + delta + 4096) + +fdt.setprop(node, "data", new_data) + +hash_node = fdt.subnode_offset(node, "hash-1") +algo = bytes(fdt.getprop(hash_node, "algo")).rstrip(b"\0").decode() +if algo != "sha256": + sys.exit(f"unexpected hash algo: {algo}") +new_hash = hashlib.sha256(new_data).digest() +fdt.setprop(hash_node, "value", new_hash) +print(f"hash-1 sha256 -> {new_hash.hex()}") + +fdt.pack() +out = bytes(fdt.as_bytearray()) +with open(out_fit, "wb") as f: + f.write(out) +print(f"wrote {out_fit} ({len(out)} bytes)") diff --git a/scripts/tezi-custom/rc.local.append.in b/scripts/tezi-custom/rc.local.append.in new file mode 100644 index 000000000..b6d5c8d83 --- /dev/null +++ b/scripts/tezi-custom/rc.local.append.in @@ -0,0 +1,20 @@ + +# EDGE-M1 — auto-register the local image feed once tezi is ready. +# Default URL injected at build time by scripts/tezi-custom/build.sh +# (substitutes the host's LAN IP and feed port). Override at runtime via +# kernel cmdline: +# edgem_feed=http://:8080/image_list.json +( + EDGEM_FEED_URL="@EDGEM_FEED_URL@" + for p in $(cat /proc/cmdline); do + case "$p" in + edgem_feed=*) EDGEM_FEED_URL="${p#edgem_feed=}";; + esac + done + for _ in $(seq 1 60); do + tezictl status >/dev/null 2>&1 && break + sleep 1 + done + tezictl feed-add "$EDGEM_FEED_URL" + echo "EDGE-M1: tezictl feed-add $EDGEM_FEED_URL done" +) >> ${RW_MEMORY_DIR}/${TEZI_LOGFILE} 2>&1 & diff --git a/scripts/tezi-feed-serve.sh b/scripts/tezi-feed-serve.sh index b13de05ca..a932f0e29 100755 --- a/scripts/tezi-feed-serve.sh +++ b/scripts/tezi-feed-serve.sh @@ -97,9 +97,9 @@ logfile="$runtime_dir/tezi-feed.log" # --- helpers ---------------------------------------------------------------- # Host IP as the board sees it: first default route, skipping tunnel-like -# interfaces so an active VPN does not capture the flashing address. This is -# the address printed below and handed to the board with `tezictl feed-add`, -# so a wrong pick sends the board to a host it cannot reach. +# interfaces so an active VPN does not capture the flashing address. Same +# selection as scripts/tezi-custom/build.sh, which bakes the URL into the +# custom TEZI image — the two must agree or the board pulls the wrong host. host_ip() { ip -4 route show default 2>/dev/null \