[Concept,06/13] scripts: ubuntu: Install U-Boot on target ESP

Message ID 20260507221507.505998-7-sjg@u-boot.org
State New
Headers
Series bootstd: bls: Scan every partition; Ubuntu autoinstall via BLS |

Commit Message

Simon Glass May 7, 2026, 10:14 p.m. UTC
  From: Simon Glass <sjg@chromium.org>

Once subiquity finishes the install it has written shim and GRUB to the
target ESP and registered an 'ubuntu' NVRAM entry pointing at
/EFI/ubuntu/shimx64.efi. Firmware prefers that entry over the
removable-media fallback, so control leaves U-Boot as soon as the
install reboots - and once the live ISO is ejected there is no U-Boot on
the installed system at all.

Plant u-boot.efi at /usr/lib/u-boot/u-boot.efi inside the modified
install squashfs so it rides through the install, then copy it onto the
target ESP at two locations:

 - /EFI/BOOT/BOOTX64.EFI - the firmware fallback path
 - /EFI/ubuntu/shimx64.efi - what the 'ubuntu' NVRAM entry points at

Overwriting both means the installed disk boots U-Boot whichever path
firmware takes. BLS then picks up /loader/entries/ on the same ESP and
the installed kernel/initrd come up without the ISO.

The autoinstall late-commands do the copy via 'curtin in-target' so the
first reboot after install lands in U-Boot directly. The interactive
first-boot unit does the same from the running installed system.

Both paths read u-boot.efi from /usr/lib/u-boot/u-boot.efi inside the
running install, so refuse --autoinstall when the squashfs is not being
modified (--install-squashfs ''); otherwise the late-commands would
silently fail to install U-Boot on the target.

Signed-off-by: Simon Glass <sjg@chromium.org>
---

 doc/usage/os/ubuntu-live.rst   | 40 +++++++++++++++++++++++--------
 scripts/ubuntu-iso-to-uboot.py | 44 ++++++++++++++++++++++++++++++----
 2 files changed, 70 insertions(+), 14 deletions(-)
  

Patch

diff --git a/doc/usage/os/ubuntu-live.rst b/doc/usage/os/ubuntu-live.rst
index cfcc5243ca2..34d7b0eb71a 100644
--- a/doc/usage/os/ubuntu-live.rst
+++ b/doc/usage/os/ubuntu-live.rst
@@ -173,22 +173,42 @@  for the installer's kernel:
 
 2. **Interactive path.** The script unpacks the default install squashfs
    (``casper/minimal.squashfs``), drops in a ``oneshot`` systemd unit plus its
-   helper script, and repacks. On first boot of the installed system the unit
-   runs::
-
-       apt-get update
-       apt-get install -y systemd-boot-efi
-       mkdir -p /boot/loader/entries
-       for k in /usr/lib/modules/*; do
-           kernel-install add "$(basename "$k")" "$k/vmlinuz" || true
-       done
-       touch /var/lib/ubuntu-iso-to-uboot-bls-setup.done
+   helper script and a copy of ``u-boot.efi`` at ``/usr/lib/u-boot/u-boot.efi``,
+   and repacks. On first boot of the installed system the unit runs
+   ``apt install systemd-boot-efi``, back-fills ``/boot/loader/entries/`` via
+   ``kernel-install``, and installs ``u-boot.efi`` onto the target ESP (see
+   :ref:`target-uboot-install` below).
 
    The unit is gated on ``ConditionPathExists=!/cdrom`` so it stays dormant
    inside the live session, and the done-file stops it re-running on subsequent
    boots. It needs a network on first boot for the apt call; if that is not
    available it fails gracefully and retries on the next boot.
 
+.. _target-uboot-install:
+
+Installing U-Boot onto the target ESP
+-------------------------------------
+
+Subiquity writes shim and GRUB to the installed ESP and registers an ``ubuntu``
+NVRAM entry pointing at ``/EFI/ubuntu/shimx64.efi``. Without further action,
+firmware prefers that entry over the removable-media fallback, so control leaves
+U-Boot as soon as the install reboots.
+
+Both install paths therefore plant ``u-boot.efi`` on the target ESP at two
+locations:
+
+* ``/EFI/BOOT/BOOTX64.EFI`` -- the firmware fallback, picked up when no NVRAM
+  entry matches (for example after a CMOS reset).
+* ``/EFI/ubuntu/shimx64.efi`` -- the file the ``ubuntu`` NVRAM entry points at,
+  overwritten in place so the entry boots U-Boot rather than chaining through
+  shim into GRUB.
+
+The autoinstall path does the copy in its ``late-commands`` via
+``curtin in-target``; the interactive first-boot unit does it from the running
+installed system. Either way ``u-boot.efi`` travels through the install inside
+the modified squashfs at ``/usr/lib/u-boot/u-boot.efi``, so the installed disk
+boots U-Boot + BLS on its own once the live ISO has been ejected.
+
 For interactive installs, every subsequent ``apt install linux-image-*`` on the
 target updates ``/boot/loader/entries/`` automatically via the
 ``kernel-install`` hooks, and U-Boot's BLS bootmeth keeps picking up the current
diff --git a/scripts/ubuntu-iso-to-uboot.py b/scripts/ubuntu-iso-to-uboot.py
index c22bd9423f2..c431e15b9ae 100755
--- a/scripts/ubuntu-iso-to-uboot.py
+++ b/scripts/ubuntu-iso-to-uboot.py
@@ -90,7 +90,8 @@  MIB = 1024 * 1024
 # subsequent boot.
 FIRST_BOOT_SCRIPT = '''\
 #!/bin/sh
-# Set up BLS entries for future kernel updates
+# Set up BLS entries and install U-Boot on the target ESP so the
+# installed system boots via U-Boot + BLS without the live ISO
 set -e
 apt-get update
 apt-get install -y systemd-boot-efi
@@ -104,6 +105,15 @@  for k in /usr/lib/modules/*; do
 	v=$(basename "$k")
 	kernel-install add "$v" "/boot/vmlinuz-$v" || true
 done
+# Plant u-boot.efi on the installed ESP. BOOTX64.EFI is the firmware
+# fallback; shimx64.efi is what the 'ubuntu' NVRAM entry Subiquity
+# registers points at, so overwriting both means the disk boots
+# U-Boot whichever path the firmware takes.
+UBOOT=/usr/lib/u-boot/u-boot.efi
+install -D -m 644 "$UBOOT" /boot/efi/EFI/BOOT/BOOTX64.EFI
+if [ -f /boot/efi/EFI/ubuntu/shimx64.efi ]; then
+	install -m 644 "$UBOOT" /boot/efi/EFI/ubuntu/shimx64.efi
+fi
 touch /var/lib/ubuntu-iso-to-uboot-bls-setup.done
 '''
 
@@ -265,10 +275,15 @@  def repack_iso(
 
 
 def inject_first_boot_unit(
-    iso: Path, sqfs_in_iso: str, work: Path,
+    iso: Path, sqfs_in_iso: str, uboot_efi: Path, work: Path,
 ) -> Path:
     """Unpack the install squashfs, drop in a first-boot BLS-setup unit
-    plus its helper script, and repack.
+    plus its helper script and a copy of u-boot.efi, and repack.
+
+    u-boot.efi travels through the install at /usr/lib/u-boot/u-boot.efi
+    so the first-boot unit (and the autoinstall late-commands, which
+    also run in-target after the squashfs has been unpacked) can copy
+    it onto the installed ESP.
 
     The whole unpack/modify/repack cycle runs under one fakeroot
     invocation so ownership survives round-tripping through a regular
@@ -303,6 +318,8 @@  install -D -m 755 -o root -g root '{script_src}' \\
     '{stage}/usr/local/sbin/ubuntu-iso-to-uboot-bls-setup'
 install -D -m 644 -o root -g root '{unit_src}' \\
     '{stage}/etc/systemd/system/ubuntu-iso-to-uboot-bls-setup.service'
+install -D -m 644 -o root -g root '{uboot_efi}' \\
+    '{stage}/usr/lib/u-boot/u-boot.efi'
 mkdir -p '{stage}/etc/systemd/system/multi-user.target.wants'
 ln -sf ../ubuntu-iso-to-uboot-bls-setup.service \\
     '{stage}/etc/systemd/system/multi-user.target.wants/ubuntu-iso-to-uboot-bls-setup.service'
@@ -411,6 +428,19 @@  options root=UUID=%s ro console=ttyS0,115200 console=tty0\\n"\
  if [ -n "$last_v" ]; then\
  cp "$ESP/loader/entries/$last_v.conf" "$ESP/loader/entry.conf";\
  fi'
+'''
+        # Plant u-boot.efi on the installed ESP in-target, overwriting
+        # both the fallback BOOTX64.EFI and the shimx64.efi that the
+        # 'ubuntu' NVRAM entry points at, so the disk boots U-Boot
+        # whichever path firmware takes - no need to keep the ISO
+        # attached after the install reboot.
+        '''    - curtin in-target -- install -D -m 644\
+ /usr/lib/u-boot/u-boot.efi /boot/efi/EFI/BOOT/BOOTX64.EFI
+    - |
+      curtin in-target -- sh -c '[ -f\
+ /boot/efi/EFI/ubuntu/shimx64.efi ] && install -m 644\
+ /usr/lib/u-boot/u-boot.efi\
+ /boot/efi/EFI/ubuntu/shimx64.efi; true'
 '''
     )
     return head + unattended_block + body
@@ -473,6 +503,12 @@  def main() -> None:
         tout.fatal(f'EFI app not found: {args.uboot}')
     if args.autoinstall and args.no_target_bls:
         tout.fatal('--autoinstall requires target-BLS wiring; drop -N')
+    if args.autoinstall and not args.install_squashfs:
+        tout.fatal(
+            '--autoinstall needs --install-squashfs; the autoinstall '
+            "late-commands copy u-boot.efi from /usr/lib/u-boot/u-boot.efi, "
+            'which is only planted when the install squashfs is modified'
+        )
     check_tools()
     if args.autoinstall and not shutil.which('openssl'):
         tout.fatal('openssl is required when --autoinstall is set')
@@ -532,7 +568,7 @@  def main() -> None:
             file_maps.append((ai, '/autoinstall.yaml'))
             if args.install_squashfs:
                 modified_sqfs = inject_first_boot_unit(
-                    args.iso, args.install_squashfs, work,
+                    args.iso, args.install_squashfs, args.uboot, work,
                 )
                 file_maps.append(
                     (modified_sqfs, '/' + args.install_squashfs.lstrip('/')),