From 6afe187f5443673c3001d0a60566e808b0ac064d Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Jun 2026 12:06:26 -0300 Subject: [PATCH 1/6] feat(strategy): support dual-TFTP boot with initrd image Extend UBootTFTPStrategy to stage an optional initrd image alongside the root image, setting bootfile_initrd in U-Boot env. Update the LibreRouter v1 target YAML to use two TFTP loads (kernel + rootfs CPIO) with rd_start/rd_size bootargs for external initramfs boot. --- strategies/tftpstrategy.py | 19 ++++++-- targets/librerouter_librerouter-v1.yaml | 65 ++++++++++--------------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/strategies/tftpstrategy.py b/strategies/tftpstrategy.py index 4d66cca..bd8b18e 100644 --- a/strategies/tftpstrategy.py +++ b/strategies/tftpstrategy.py @@ -180,9 +180,13 @@ def _resolve_tftp_server_ip(self): logger.info("Using exporter external_ip: %s", exporter_ip) return exporter_ip - def _prepare_uboot_commands(self, staged_file, tftp_server_ip): + def _prepare_uboot_commands(self, staged_file, tftp_server_ip, staged_initrd=None): """Prepare a fresh set of U-Boot init commands for this boot attempt.""" - init_commands = (f"setenv bootfile {staged_file}",) + self._base_init_commands + bootfile_cmds = (f"setenv bootfile {staged_file}",) + if staged_initrd: + bootfile_cmds += (f"setenv bootfile_initrd {staged_initrd}",) + + init_commands = bootfile_cmds + self._base_init_commands if tftp_server_ip: tftp_dut_ip = ipaddress.ip_address(tftp_server_ip) + 1 @@ -286,6 +290,15 @@ def _transition_to_uboot_once(self): """Power-cycle the node and activate the U-Boot console.""" self.target.activate(self.tftp) staged_file = self.tftp.stage(self.target.env.config.get_image_path("root")) + + staged_initrd = None + try: + initrd_path = self.target.env.config.get_image_path("initrd") + staged_initrd = self.tftp.stage(initrd_path) + logger.info("Staged initrd image: %s", staged_initrd) + except KeyError: + pass + tftp_server_ip = self._resolve_tftp_server_ip() self.target.deactivate(self.console) @@ -306,7 +319,7 @@ def _transition_to_uboot_once(self): self._spam_uboot_interrupt() self._drain_serial_buffer() - self._prepare_uboot_commands(staged_file, tftp_server_ip) + self._prepare_uboot_commands(staged_file, tftp_server_ip, staged_initrd) self.target.activate(self.uboot) self.status = Status.uboot diff --git a/targets/librerouter_librerouter-v1.yaml b/targets/librerouter_librerouter-v1.yaml index 7b0f608..4540661 100644 --- a/targets/librerouter_librerouter-v1.yaml +++ b/targets/librerouter_librerouter-v1.yaml @@ -17,7 +17,7 @@ targets: # 1.1.x build (see include/955x.h: only CONFIG_BOOTDELAY=2 and # the simple "Hit any key to stop autoboot" loop in # common/main.c). Any byte sent during the 2 s window aborts - # autoboot, so a single newline is enough — _spam_uboot_interrupt + # autoboot, so a single newline is enough -- _spam_uboot_interrupt # in tftpstrategy.py blasts these for ~12 s by default which # comfortably brackets the bootdelay window. interrupt: "\n" @@ -26,54 +26,38 @@ targets: # space; see include/955x.h CFG_PROMPT). pexpect matches on # substring so the trailing space is optional here. prompt: "ath>" - # init_commands run at the U-Boot prompt before the - # boot_command. Lines starting with `tftp`/`dhcp` are pulled - # out by UBootTFTPStrategy and routed through its retry path - # (TFTPProviderDriver pre-stages the file on the exporter and - # bumps `bootfile` for us, so a bare `tftp ` is enough). - # The remaining lines are passed through verbatim. + # DUAL-TFTP BOOT (rd_start/rd_size): # - # IMAGE FORMAT: this YAML expects LG_IMAGE to point at a - # `*-initramfs-kernel.bin` (a uImage on ath79; DTB appended - # into kernel-bin upstream, LibreMesh CPIO embedded in the - # kernel`s own .init.ramfs section via CONFIG_INITRAMFS_SOURCE). - # U-Boot only needs `bootm ` and the kernel`s - # populate_rootfs() drops the embedded CPIO into rootfs with - # no bootloader cooperation. + # The ath79 ImageBuilder cannot produce an initramfs kernel + # (CONFIG_INITRAMFS_SOURCE requires kernel recompilation). + # Instead we ship the pre-built kernel.bin and a LibreMesh + # rootfs CPIO as two separate files. U-Boot TFTP-loads each + # to a distinct RAM address: # - # The pi-lime-packages CI does NOT build this artifact — see - # docs/followups/imagebuilder_initramfs_limitations.md in that - # repo for why. For local / manual runs, supply your own - # `*-initramfs-kernel.bin` (e.g. from a LibreMesh release or a - # local OpenWrt source build) via LG_IMAGE. + # 0x82000000 kernel.bin (uImage with lzma kernel + appended DTB) + # 0x84000000 rootfs.cpio (newc CPIO with LibreMesh rootfs) # - # Why we still `setenv bootargs` here: the LibreRouter`s - # persistent U-Boot env may hold `bootargs=... - # root=/dev/mtdblock4 ...` left over from sysupgrade. We - # override it RAM-only (no `saveenv`) to keep serial console - # output going and avoid surprises if a future kernel decides - # to re-honour root= from cmdline. The initramfs kernel - # itself ignores root= when populate_rootfs() succeeds. + # The boot_command sets bootargs with rd_start/rd_size so the + # MIPS kernel (arch/mips/kernel/setup.c early_param handlers) + # picks up the external CPIO and unpacks it via populate_rootfs(). + # + # $bootfile is set by UBootTFTPStrategy from the `root` image. + # $bootfile_initrd is set from the `initrd` image. + # $filesize is a U-Boot variable updated after each tftp with + # the transferred size in hex -- used for rd_size. + # + # For manual / local runs, set both env vars: + # export LG_IMAGE=/path/to/kernel.bin + # export LG_IMAGE_INITRD=/path/to/rootfs.cpio init_commands: - - "setenv bootargs console=ttyS0,115200n8" - "tftp 0x82000000" - # `bootm ` on the initramfs uImage: - # * U-Boot reads the 64-byte uImage header at , - # * decompresses the lzma payload (kernel + appended DTB + - # embedded initramfs, all in one blob) to KERNEL_LOADADDR - # (0x80060000, set by the ath79/generic recipe upstream; - # embedded in the uImage header by the build). - # * jumps to the kernel entry point with the linux_argv - # array populated from `bootargs`. The MIPS kernel finds - # its appended DTB at the end of the uncompressed image - # and its embedded CPIO inside .init.ramfs, then runs - # /init from the unpacked rootfs. - boot_command: bootm 0x82000000 + - "tftp 0x84000000 ${bootfile_initrd}" + boot_command: "setenv bootargs console=ttyS0,115200n8 rd_start=0x84000000 rd_size=0x${filesize}; bootm 0x82000000" - ShellDriver: # Strict prompt: only match LibreMesh's default `LiMe-<6 hex>` # hostname so we never engage with pre-`lime-config` prompts like # `root@(none):~#` or `root@OpenWrt:~#`. See openwrt_one.yaml for - # rationale. login_timeout bumped to 180s — first-boot + # rationale. login_timeout bumped to 180s -- first-boot # uci-defaults can take >60s on this device's 8 MiB SPI NOR. prompt: "(?:\x1b\\][^\x07]*\x07)?(?:\x1b\\[[\\d;]*m)*root@LiMe-[0-9A-Fa-f]{6}:[^ ]*[#$] " login_prompt: Please press Enter to activate this console. @@ -88,6 +72,7 @@ targets: images: root: !template $LG_IMAGE + initrd: !template $LG_IMAGE_INITRD imports: - ../strategies/tftpstrategy.py From 29a35a0358988205356ca634749d8795e5cc517b Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Jun 2026 15:49:56 -0300 Subject: [PATCH 2/6] fix(target): rename librerouter YAML to match device name in targets.yml --- targets/{librerouter_librerouter-v1.yaml => librerouter_v1.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename targets/{librerouter_librerouter-v1.yaml => librerouter_v1.yaml} (100%) diff --git a/targets/librerouter_librerouter-v1.yaml b/targets/librerouter_v1.yaml similarity index 100% rename from targets/librerouter_librerouter-v1.yaml rename to targets/librerouter_v1.yaml From 924a13b069bb6d8104efdfd0ef0261e0be6f8254 Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Jun 2026 16:37:38 -0300 Subject: [PATCH 3/6] fix(target): use bootm with ramdisk uImage for LibreRouter Change boot_command from setenv bootargs + bootm to `bootm 0x82000000 0x84000000` which tells U-Boot about the ramdisk natively, bypassing bootargs/cmdline issues with the Atheros U-Boot 1.1.x fork. --- targets/librerouter_v1.yaml | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/targets/librerouter_v1.yaml b/targets/librerouter_v1.yaml index 4540661..4eb8fbd 100644 --- a/targets/librerouter_v1.yaml +++ b/targets/librerouter_v1.yaml @@ -26,33 +26,30 @@ targets: # space; see include/955x.h CFG_PROMPT). pexpect matches on # substring so the trailing space is optional here. prompt: "ath>" - # DUAL-TFTP BOOT (rd_start/rd_size): + # DUAL-TFTP BOOT (kernel + ramdisk uImage): # # The ath79 ImageBuilder cannot produce an initramfs kernel # (CONFIG_INITRAMFS_SOURCE requires kernel recompilation). # Instead we ship the pre-built kernel.bin and a LibreMesh - # rootfs CPIO as two separate files. U-Boot TFTP-loads each - # to a distinct RAM address: + # rootfs wrapped as a uImage ramdisk (mkimage -T ramdisk). + # U-Boot TFTP-loads each to a distinct RAM address: # - # 0x82000000 kernel.bin (uImage with lzma kernel + appended DTB) - # 0x84000000 rootfs.cpio (newc CPIO with LibreMesh rootfs) + # 0x82000000 kernel.bin (uImage lzma kernel + appended DTB) + # 0x84000000 rootfs.uimage (uImage ramdisk wrapping newc CPIO) # - # The boot_command sets bootargs with rd_start/rd_size so the - # MIPS kernel (arch/mips/kernel/setup.c early_param handlers) - # picks up the external CPIO and unpacks it via populate_rootfs(). - # - # $bootfile is set by UBootTFTPStrategy from the `root` image. - # $bootfile_initrd is set from the `initrd` image. - # $filesize is a U-Boot variable updated after each tftp with - # the transferred size in hex -- used for rd_size. + # `bootm 0x82000000 0x84000000` tells U-Boot about both images + # natively. U-Boot's do_bootm_linux() passes initrd_start/end + # to the MIPS kernel via linux_env_set(), bypassing any + # CONFIG_CMDLINE_OVERRIDE that would ignore rd_start/rd_size + # bootargs. # # For manual / local runs, set both env vars: # export LG_IMAGE=/path/to/kernel.bin - # export LG_IMAGE_INITRD=/path/to/rootfs.cpio + # export LG_IMAGE_INITRD=/path/to/rootfs.uimage init_commands: - "tftp 0x82000000" - "tftp 0x84000000 ${bootfile_initrd}" - boot_command: "setenv bootargs console=ttyS0,115200n8 rd_start=0x84000000 rd_size=0x${filesize}; bootm 0x82000000" + boot_command: "bootm 0x82000000 0x84000000" - ShellDriver: # Strict prompt: only match LibreMesh's default `LiMe-<6 hex>` # hostname so we never engage with pre-`lime-config` prompts like From e43211deba4a40a93e9a5fc14476d65e2d4582da Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Jun 2026 17:10:27 -0300 Subject: [PATCH 4/6] fix(target): page-align ramdisk load address for LibreRouter Load the uImage ramdisk to 0x84000FC0 so the CPIO data (past the 64-byte header) starts at 0x84001000 (page-aligned). The kernel requires initrd_start to be page-aligned and rejects it with "initrd start must be page aligned" otherwise. --- targets/librerouter_v1.yaml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/targets/librerouter_v1.yaml b/targets/librerouter_v1.yaml index 4eb8fbd..9f5abfe 100644 --- a/targets/librerouter_v1.yaml +++ b/targets/librerouter_v1.yaml @@ -35,21 +35,26 @@ targets: # U-Boot TFTP-loads each to a distinct RAM address: # # 0x82000000 kernel.bin (uImage lzma kernel + appended DTB) - # 0x84000000 rootfs.uimage (uImage ramdisk wrapping newc CPIO) + # 0x84000FC0 rootfs.uimage (uImage ramdisk wrapping newc CPIO) # - # `bootm 0x82000000 0x84000000` tells U-Boot about both images - # natively. U-Boot's do_bootm_linux() passes initrd_start/end - # to the MIPS kernel via linux_env_set(), bypassing any - # CONFIG_CMDLINE_OVERRIDE that would ignore rd_start/rd_size - # bootargs. + # The ramdisk loads to 0x84000FC0 so the CPIO data past the + # 64-byte uImage header starts at 0x84001000, which is + # page-aligned (PAGE_SIZE=4K). The kernel rejects non-aligned + # initrd with "initrd start must be page aligned". + # + # `bootm 0x82000000 0x84000FC0` tells U-Boot about both + # images natively. do_bootm_linux() passes initrd_start/end + # to the MIPS kernel via linux_env_set(), bypassing the + # CONFIG_CMDLINE_OVERRIDE that ignores rd_start/rd_size + # bootargs on this target. # # For manual / local runs, set both env vars: # export LG_IMAGE=/path/to/kernel.bin # export LG_IMAGE_INITRD=/path/to/rootfs.uimage init_commands: - "tftp 0x82000000" - - "tftp 0x84000000 ${bootfile_initrd}" - boot_command: "bootm 0x82000000 0x84000000" + - "tftp 0x84000FC0 ${bootfile_initrd}" + boot_command: "bootm 0x82000000 0x84000FC0" - ShellDriver: # Strict prompt: only match LibreMesh's default `LiMe-<6 hex>` # hostname so we never engage with pre-`lime-config` prompts like From 4126cce9c54b37f3b6c07bd5cb9f7b70b4e86968 Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Jun 2026 17:20:00 -0300 Subject: [PATCH 5/6] fix(target): accept pre-lime-config prompts in LibreRouter initramfs The initramfs boot reaches a shell with hostname (none) because mount_root fails to pivot from ramfs and lime-config uci-defaults never set the hostname. Accept root@(none) and root@OpenWrt in addition to root@LiMe-XXXXXX. --- targets/librerouter_v1.yaml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/targets/librerouter_v1.yaml b/targets/librerouter_v1.yaml index 9f5abfe..e3d0c49 100644 --- a/targets/librerouter_v1.yaml +++ b/targets/librerouter_v1.yaml @@ -56,12 +56,11 @@ targets: - "tftp 0x84000FC0 ${bootfile_initrd}" boot_command: "bootm 0x82000000 0x84000FC0" - ShellDriver: - # Strict prompt: only match LibreMesh's default `LiMe-<6 hex>` - # hostname so we never engage with pre-`lime-config` prompts like - # `root@(none):~#` or `root@OpenWrt:~#`. See openwrt_one.yaml for - # rationale. login_timeout bumped to 180s -- first-boot - # uci-defaults can take >60s on this device's 8 MiB SPI NOR. - prompt: "(?:\x1b\\][^\x07]*\x07)?(?:\x1b\\[[\\d;]*m)*root@LiMe-[0-9A-Fa-f]{6}:[^ ]*[#$] " + # Initramfs boot: lime-config (uci-defaults) may not set the + # hostname before the console is ready because mount_root fails + # to pivot_root from ramfs. Accept LiMe-XXXXXX (normal) and + # (none) / OpenWrt (pre-lime-config fallbacks). + prompt: "(?:\\x1b\\][^\\x07]*\\x07)?(?:\\x1b\\[[\\d;]*m)*root@(?:LiMe-[0-9A-Fa-f]{6}|\\(none\\)|OpenWrt):[^ ]*[#$] " login_prompt: Please press Enter to activate this console. await_login_timeout: 15 login_timeout: 180 From e9513ead7c4515dc345d88547ec0ab9395a8e579 Mon Sep 17 00:00:00 2001 From: Franco Date: Thu, 18 Jun 2026 18:12:24 -0300 Subject: [PATCH 6/6] revert: restore strict LiMe-XXXXXX prompt regex The root cause of hostname (none) is missing /proc in the initramfs CPIO, not the prompt regex. Keep the strict match so CI fails fast when lime-config doesn't run. --- targets/librerouter_v1.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/targets/librerouter_v1.yaml b/targets/librerouter_v1.yaml index e3d0c49..472b17b 100644 --- a/targets/librerouter_v1.yaml +++ b/targets/librerouter_v1.yaml @@ -56,11 +56,11 @@ targets: - "tftp 0x84000FC0 ${bootfile_initrd}" boot_command: "bootm 0x82000000 0x84000FC0" - ShellDriver: - # Initramfs boot: lime-config (uci-defaults) may not set the - # hostname before the console is ready because mount_root fails - # to pivot_root from ramfs. Accept LiMe-XXXXXX (normal) and - # (none) / OpenWrt (pre-lime-config fallbacks). - prompt: "(?:\\x1b\\][^\\x07]*\\x07)?(?:\\x1b\\[[\\d;]*m)*root@(?:LiMe-[0-9A-Fa-f]{6}|\\(none\\)|OpenWrt):[^ ]*[#$] " + # Strict prompt: only match LibreMesh's default `LiMe-<6 hex>` + # hostname. If this times out, lime-config didn't run - fix the + # initramfs (missing /proc mountpoint, mount_root failure, etc.) + # rather than relaxing this regex. + prompt: "(?:\\x1b\\][^\\x07]*\\x07)?(?:\\x1b\\[[\\d;]*m)*root@LiMe-[0-9A-Fa-f]{6}:[^ ]*[#$] " login_prompt: Please press Enter to activate this console. await_login_timeout: 15 login_timeout: 180