Add Python boot configuration package

Add architecture-specific boot configurators that translate the
debian-cd boot shell scripts (boot-amd64, boot-arm64, boot-ppc64el,
boot-riscv64, boot-s390x) into Python.

The package uses a class hierarchy:
- BaseBootConfigurator: abstract base with common functionality
- GrubBootConfigurator: shared GRUB config generation
- UEFIBootConfigurator: UEFI-specific shim/ESP handling
- Architecture classes: AMD64, ARM64, PPC64EL, RISCV64, S390X

A factory function make_boot_configurator_for_arch() creates the
appropriate configurator for each architecture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael.hudson@canonical.com 2026-02-11 14:26:08 +13:00
parent 6a6b00d68b
commit edf0acbeac
No known key found for this signature in database
GPG Key ID: 80E627A0AB757E23
9 changed files with 1211 additions and 0 deletions

View File

@ -0,0 +1,43 @@
"""Boot configuration package for ISO builder.
This package contains architecture-specific boot configurators for building
bootable ISOs for different architectures.
"""
import pathlib
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..apt_state import AptStateManager
from ..builder import Logger
from .base import BaseBootConfigurator
def make_boot_configurator_for_arch(
arch: str,
logger: "Logger",
apt_state: "AptStateManager",
iso_root: pathlib.Path,
) -> "BaseBootConfigurator":
"""Factory function to create boot configurator for a specific architecture."""
from .amd64 import AMD64BootConfigurator
from .arm64 import ARM64BootConfigurator
from .ppc64el import PPC64ELBootConfigurator
from .riscv64 import RISCV64BootConfigurator
from .s390x import S390XBootConfigurator
if arch == "amd64":
return AMD64BootConfigurator(logger, apt_state, iso_root)
elif arch == "arm64":
return ARM64BootConfigurator(logger, apt_state, iso_root)
elif arch == "ppc64el":
return PPC64ELBootConfigurator(logger, apt_state, iso_root)
elif arch == "riscv64":
return RISCV64BootConfigurator(logger, apt_state, iso_root)
elif arch == "s390x":
return S390XBootConfigurator(logger, apt_state, iso_root)
else:
raise ValueError(f"Unsupported architecture: {arch}")
__all__ = ["make_boot_configurator_for_arch"]

View File

@ -0,0 +1,201 @@
"""AMD64/x86_64 architecture boot configuration."""
import pathlib
import shutil
from .uefi import UEFIBootConfigurator
from .base import default_kernel_params
class AMD64BootConfigurator(UEFIBootConfigurator):
"""Boot setup for AMD64/x86_64 architecture."""
efi_suffix = "x64"
grub_target = "x86_64"
arch = "amd64"
def mkisofs_opts(self) -> list[str | pathlib.Path]:
# Boring mkisofs options that should be set somewhere architecture independent.
opts: list[str | pathlib.Path] = ["-J", "-joliet-long", "-l"]
# Generalities on booting
#
# There is a 2x2 matrix of boot modes we care about: legacy or UEFI
# boot modes and having the installer be on a cdrom or a disk. Booting
# from cdrom uses the el torito standard and booting from disk expects
# a MBR or GPT partition table.
#
# https://wiki.osdev.org/El-Torito has a lot more background on this.
# ## Set up the mkisofs options for legacy boot.
# Set the el torito boot image "name", i.e. the path on the ISO
# containing the bootloader for legacy-cdrom boot.
opts.extend(["-b", "boot/grub/i386-pc/eltorito.img"])
# Back in the day, el torito booting worked by emulating a floppy
# drive. This hasn't been a useful way of operating for a long time.
opts.append("-no-emul-boot")
# Misc options to make the legacy-cdrom boot work.
opts.extend(["-boot-load-size", "4", "-boot-info-table", "--grub2-boot-info"])
# The bootloader to write to the MBR for legacy-disk boot.
#
# We use the grub stage1 bootloader, boot_hybrid.img, which then jumps
# to the eltorito image based on the information xorriso provides it
# via the --grub2-boot-info option.
opts.extend(
[
"--grub2-mbr",
self.grub_dir.joinpath("usr/lib/grub/i386-pc/boot_hybrid.img"),
]
)
# ## Set up the mkisofs options for UEFI boot.
opts.extend(self.get_uefi_mkisofs_opts())
# ## Add cd-boot-tree to the ISO
opts.append(str(self.boot_tree))
return opts
def extract_files(self) -> None:
with self.logger.logged("extracting AMD64 boot files"):
# Extract UEFI files (common with ARM64)
self.extract_uefi_files()
# AMD64-specific: Add BIOS/legacy boot files
with self.logger.logged("adding BIOS/legacy boot files"):
self.download_and_extract_package("grub-pc-bin", self.grub_dir)
grub_boot_dir = self.boot_tree.joinpath("boot", "grub", "i386-pc")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = self.grub_dir.joinpath("usr", "lib", "grub", "i386-pc")
shutil.copy(src_grub_dir.joinpath("eltorito.img"), grub_boot_dir)
self.copy_grub_modules(
src_grub_dir, grub_boot_dir, ["*.mod", "*.lst", "*.o"]
)
def generate_grub_config(self) -> None:
"""Generate grub.cfg and loopback.cfg for the boot tree."""
# Generate grub.cfg
kernel_params = default_kernel_params(self.project)
boot_grub_dir = self.boot_tree.joinpath("boot", "grub")
boot_grub_dir.mkdir(parents=True, exist_ok=True)
grub_cfg = boot_grub_dir.joinpath("grub.cfg")
# Write common GRUB header
self.write_grub_header(grub_cfg)
# Main menu entry
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep
linux /casper/vmlinuz {kernel_params}
initrd /casper/initrd
}}
"""
)
# All but server get safe-graphics mode
if self.project != "ubuntu-server":
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "{self.humanproject} (safe graphics)" {{
set gfxpayload=keep
linux /casper/vmlinuz nomodeset {kernel_params}
initrd /casper/initrd
}}
"""
)
# ubiquity based projects get OEM mode
if "maybe-ubiquity" in kernel_params:
oem_kernel_params = kernel_params.replace(
"maybe-ubiquity", "only-ubiquity oem-config/enable=true"
)
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep
linux /casper/vmlinuz {oem_kernel_params}
initrd /casper/initrd
}}
"""
)
# Calamares-based projects get OEM mode
if self.project in ["lubuntu", "kubuntu"]:
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep
linux /casper/vmlinuz {kernel_params} oem-config/enable=true
initrd /casper/initrd
}}
"""
)
# Currently only server is built with HWE, hence no safe-graphics/OEM
if self.hwe:
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "{self.humanproject} with the HWE kernel" {{
set gfxpayload=keep
linux /casper/hwe-vmlinuz {kernel_params}
initrd /casper/hwe-initrd
}}
"""
)
# Create the loopback config, based on the main config
with grub_cfg.open("r") as f:
content = f.read()
# sed: delete from line 1 to menu_color_highlight, delete from
# grub_platform to end and replace '---' with
# 'iso-scan/filename=${iso_path} ---' in lines with 'linux'
lines = content.split("\n")
start_idx = 0
for i, line in enumerate(lines):
if "menu_color_highlight" in line:
start_idx = i + 1
break
end_idx = len(lines)
for i, line in enumerate(lines):
if "grub_platform" in line:
end_idx = i
break
loopback_lines = lines[start_idx:end_idx]
loopback_lines = [
(
line.replace("---", "iso-scan/filename=${iso_path} ---")
if "linux" in line
else line
)
for line in loopback_lines
]
loopback_cfg = boot_grub_dir.joinpath("loopback.cfg")
with loopback_cfg.open("w") as f:
f.write("\n".join(loopback_lines))
# UEFI Entries (wrapped in grub_platform check for dual BIOS/UEFI support)
with grub_cfg.open("a") as f:
f.write("grub_platform\n")
f.write('if [ "$grub_platform" = "efi" ]; then\n')
self.write_uefi_menu_entries(grub_cfg)
with grub_cfg.open("a") as f:
f.write("fi\n")

View File

@ -0,0 +1,81 @@
"""ARM 64-bit architecture boot configuration."""
import pathlib
from .uefi import UEFIBootConfigurator
from .base import default_kernel_params
class ARM64BootConfigurator(UEFIBootConfigurator):
"""Boot setup for ARM 64-bit architecture."""
efi_suffix = "aa64"
grub_target = "arm64"
arch = "arm64"
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for ARM64."""
opts: list[str | pathlib.Path] = [
"-J",
"-joliet-long",
"-l",
"-c",
"boot/boot.cat",
]
# Add common UEFI options
opts.extend(self.get_uefi_mkisofs_opts())
# ARM64-specific: partition cylinder alignment
opts.extend(["-partition_cyl_align", "all"])
opts.append(self.boot_tree)
return opts
def extract_files(self) -> None:
"""Download and extract bootloader packages for ARM64."""
with self.logger.logged("extracting ARM64 boot files"):
self.extract_uefi_files()
def generate_grub_config(self) -> None:
"""Generate grub.cfg for ARM64."""
kernel_params = default_kernel_params(self.project)
grub_cfg = self.grub_dir.joinpath("grub.cfg")
# Write common GRUB header
self.write_grub_header(grub_cfg)
# ARM64-specific: Snapdragon workarounds
with grub_cfg.open("a") as f:
f.write(
"""set cmdline=
smbios --type 4 --get-string 5 --set proc_version
regexp "Snapdragon.*" "$proc_version"
if [ $? = 0 ]; then
# Work around Snapdragon X firmware bug. cutmem is not allowed in lockdown mode.
if [ $lockdown != "y" ]; then
cutmem 0x8800000000 0x8fffffffff
fi
# arm64.nopauth works around 8cx Gen 3 firmware bug
cmdline="clk_ignore_unused pd_ignore_unused arm64.nopauth"
fi
menuentry "Try or Install {self.humanproject}" {{
\tset gfxpayload=keep
\tlinux\t/casper/vmlinuz $cmdline {kernel_params} console=tty0
\tinitrd\t/casper/initrd
}}
"""
)
# HWE kernel option if available
self.write_hwe_menu_entry(
grub_cfg,
"vmlinuz",
f"{kernel_params} console=tty0",
extra_params="$cmdline ",
)
# Note: ARM64 HWE also includes $dtb in the original shell script,
# but it's not actually set anywhere in the grub.cfg, so we omit it here
# UEFI Entries (ARM64 is UEFI-only, no grub_platform check needed)
self.write_uefi_menu_entries(grub_cfg)

View File

@ -0,0 +1,111 @@
"""Base classes and helper functions for boot configuration."""
import pathlib
import shutil
import subprocess
import tempfile
from abc import ABC, abstractmethod
from ..builder import Logger
from ..apt_state import AptStateManager
def default_kernel_params(project: str) -> str:
if project == "ubuntukylin":
return (
"file=/cdrom/preseed/ubuntu.seed locale=zh_CN "
"keyboard-configuration/layoutcode?=cn quiet splash --- "
)
if project == "ubuntu-server":
return " --- "
else:
return " --- quiet splash"
class BaseBootConfigurator(ABC):
"""Abstract base class for architecture-specific boot configurators.
Subclasses must implement:
- extract_files(): Download and extract bootloader packages
- mkisofs_opts(): Return mkisofs command-line options
"""
def __init__(
self,
logger: Logger,
apt_state: AptStateManager,
iso_root: pathlib.Path,
) -> None:
self.logger = logger
self.apt_state = apt_state
self.iso_root = iso_root
def create_dirs(self, workdir):
self.scratch = workdir.joinpath("boot-stuff")
self.scratch.mkdir(exist_ok=True)
self.boot_tree = self.scratch.joinpath("cd-boot-tree")
def download_and_extract_package(
self, pkg_name: str, target_dir: pathlib.Path
) -> None:
"""Download a Debian package and extract its contents to target directory."""
self.logger.log(f"downloading and extracting {pkg_name}")
target_dir.mkdir(exist_ok=True, parents=True)
with tempfile.TemporaryDirectory() as tdir_str:
tdir = pathlib.Path(tdir_str)
self.apt_state.download_direct(pkg_name, tdir)
[deb] = tdir.glob("*.deb")
dpkg_proc = subprocess.Popen(
["dpkg-deb", "--fsys-tarfile", deb], stdout=subprocess.PIPE
)
tar_proc = subprocess.Popen(
["tar", "xf", "-", "-C", target_dir], stdin=dpkg_proc.stdout
)
assert dpkg_proc.stdout is not None
dpkg_proc.stdout.close()
tar_proc.communicate()
def copy_grub_modules(
self, src_dir: pathlib.Path, dest_dir: pathlib.Path, extensions: list[str]
) -> None:
"""Copy GRUB module files matching given extensions from src to dest."""
for ext in extensions:
for file in src_dir.glob(ext):
shutil.copy(file, dest_dir)
@abstractmethod
def extract_files(self) -> None:
"""Download and extract bootloader packages to the boot tree.
Each architecture must implement this to set up its specific bootloader files.
"""
...
@abstractmethod
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs command-line options for this architecture.
Returns:
List of command-line options to pass to mkisofs/xorriso.
"""
...
def post_process_iso(self, iso_path: pathlib.Path) -> None:
"""Post-process the ISO image after xorriso creates it."""
def make_bootable(
self,
workdir: pathlib.Path,
project: str,
capproject: str,
subarch: str,
hwe: bool,
) -> None:
"""Make the ISO bootable by extracting bootloader files."""
self.project = project
self.humanproject = capproject.replace("-", " ")
self.subarch = subarch
self.hwe = hwe
self.create_dirs(workdir)
with self.logger.logged("configuring boot"):
self.extract_files()

View File

@ -0,0 +1,103 @@
"""GRUB boot configuration for multiple architectures."""
import pathlib
import shutil
from abc import abstractmethod
from .base import BaseBootConfigurator
def copy_grub_common_files_to_boot_tree(
grub_dir: pathlib.Path, boot_tree: pathlib.Path
) -> None:
fonts_dir = boot_tree.joinpath("boot", "grub", "fonts")
fonts_dir.mkdir(parents=True, exist_ok=True)
src = grub_dir.joinpath("usr", "share", "grub", "unicode.pf2")
dst = fonts_dir.joinpath("unicode.pf2")
shutil.copy(src, dst)
class GrubBootConfigurator(BaseBootConfigurator):
"""Base class for architectures that use GRUB (all except S390X).
Common GRUB functionality shared across AMD64, ARM64, PPC64EL, and RISC-V64.
Subclasses must implement generate_grub_config().
"""
def create_dirs(self, workdir):
super().create_dirs(workdir)
self.grub_dir = self.boot_tree.joinpath("grub")
def setup_grub_common_files(self) -> None:
"""Copy common GRUB files (fonts, etc.) to boot tree."""
copy_grub_common_files_to_boot_tree(self.grub_dir, self.boot_tree)
def write_grub_header(
self, grub_cfg: pathlib.Path, include_loadfont: bool = True
) -> None:
"""Write common GRUB config header (timeout, colors).
Args:
grub_cfg: Path to grub.cfg file
include_loadfont: Whether to include 'loadfont unicode'
(not needed for RISC-V)
"""
with grub_cfg.open("a") as f:
f.write("set timeout=30\n\n")
if include_loadfont:
f.write("loadfont unicode\n\n")
f.write(
"""set menu_color_normal=white/black
set menu_color_highlight=black/light-gray
"""
)
def write_hwe_menu_entry(
self,
grub_cfg: pathlib.Path,
kernel_name: str,
kernel_params: str,
extra_params: str = "",
) -> None:
"""Write HWE kernel menu entry if HWE is enabled.
Args:
grub_cfg: Path to grub.cfg file
kernel_name: Kernel binary name (vmlinuz or vmlinux)
kernel_params: Kernel parameters to append
extra_params: Additional parameters (e.g., console=tty0, $cmdline)
"""
if self.hwe:
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "{self.humanproject} with the HWE kernel" {{
\tset gfxpayload=keep
\tlinux\t/casper/hwe-{kernel_name} {extra_params}{kernel_params}
\tinitrd\t/casper/hwe-initrd
}}
"""
)
@abstractmethod
def generate_grub_config(self) -> None:
"""Generate grub.cfg configuration file.
Each GRUB-based architecture must implement this to create its
specific GRUB configuration.
"""
...
def make_bootable(
self,
workdir: pathlib.Path,
project: str,
capproject: str,
subarch: str,
hwe: bool,
) -> None:
"""Make the ISO bootable by extracting files and generating GRUB config."""
super().make_bootable(workdir, project, capproject, subarch, hwe)
with self.logger.logged("generating grub config"):
self.generate_grub_config()

View File

@ -0,0 +1,75 @@
"""PowerPC 64-bit Little Endian architecture boot configuration."""
import pathlib
import shutil
from .grub import GrubBootConfigurator
from .base import default_kernel_params
class PPC64ELBootConfigurator(GrubBootConfigurator):
"""Boot setup for PowerPC 64-bit Little Endian architecture."""
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for PPC64EL."""
# Add cd-boot-tree to the ISO
return [self.boot_tree]
def extract_files(self) -> None:
"""Download and extract bootloader packages for PPC64EL."""
self.logger.log("extracting PPC64EL boot files")
# Download and extract bootloader packages
self.download_and_extract_package("grub2-common", self.grub_dir)
self.download_and_extract_package("grub-ieee1275-bin", self.grub_dir)
# Add common files for GRUB to tree
self.setup_grub_common_files()
# Add IEEE1275 ppc boot files
ppc_dir = self.boot_tree.joinpath("ppc")
ppc_dir.mkdir(parents=True, exist_ok=True)
grub_boot_dir = self.boot_tree.joinpath("boot", "grub", "powerpc-ieee1275")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = self.grub_dir.joinpath("usr", "lib", "grub", "powerpc-ieee1275")
# Copy bootinfo.txt to ppc directory
shutil.copy(
src_grub_dir.joinpath("bootinfo.txt"), ppc_dir.joinpath("bootinfo.txt")
)
# Copy eltorito.elf to boot/grub as powerpc.elf
shutil.copy(
src_grub_dir.joinpath("eltorito.elf"),
self.boot_tree.joinpath("boot", "grub", "powerpc.elf"),
)
# Copy GRUB modules
self.copy_grub_modules(src_grub_dir, grub_boot_dir, ["*.mod", "*.lst"])
def generate_grub_config(self) -> None:
"""Generate grub.cfg for PPC64EL."""
kernel_params = default_kernel_params(self.project)
grub_cfg = self.grub_dir.joinpath("grub.cfg")
# Write common GRUB header
self.write_grub_header(grub_cfg)
# Main menu entry
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "Try or Install {self.humanproject}" {{
\tset gfxpayload=keep
\tlinux\t/casper/vmlinux quiet {kernel_params}
\tinitrd\t/casper/initrd
}}
"""
)
# HWE kernel option if available
self.write_hwe_menu_entry(
grub_cfg, "vmlinux", kernel_params, extra_params="quiet "
)

View File

@ -0,0 +1,223 @@
"""RISC-V 64-bit architecture boot configuration."""
import pathlib
import shutil
from .grub import GrubBootConfigurator
def copy_unsigned_monolithic_grub_to_boot_tree(
grub_dir: pathlib.Path, efi_suffix: str, grub_target: str, boot_tree: pathlib.Path
) -> None:
efi_boot_dir = boot_tree.joinpath("EFI", "boot")
efi_boot_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(
grub_dir.joinpath(
"usr",
"lib",
"grub",
f"{grub_target}-efi",
"monolithic",
f"gcd{efi_suffix}.efi",
),
efi_boot_dir.joinpath(f"boot{efi_suffix}.efi"),
)
grub_boot_dir = boot_tree.joinpath("boot", "grub", f"{grub_target}-efi")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = grub_dir.joinpath("usr", "lib", "grub", f"{grub_target}-efi")
for mod_file in src_grub_dir.glob("*.mod"):
shutil.copy(mod_file, grub_boot_dir)
for lst_file in src_grub_dir.glob("*.lst"):
shutil.copy(lst_file, grub_boot_dir)
class RISCV64BootConfigurator(GrubBootConfigurator):
"""Boot setup for RISC-V 64-bit architecture."""
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for RISC-V64."""
efi_img = self.scratch.joinpath("efi.img")
return [
"-joliet",
"on",
"-compliance",
"joliet_long_names",
"--append_partition",
"2",
"0xef",
efi_img,
"-boot_image",
"any",
"partition_offset=10240",
"-boot_image",
"any",
"partition_cyl_align=all",
"-boot_image",
"any",
"efi_path=--interval:appended_partition_2:all::",
"-boot_image",
"any",
"appended_part_as=gpt",
"-boot_image",
"any",
"cat_path=/boot/boot.cat",
"-fs",
"64m",
]
def extract_files(self) -> None:
"""Download and extract bootloader packages for RISC-V64."""
self.logger.log("extracting RISC-V64 boot files")
u_boot_dir = self.scratch.joinpath("u-boot-sifive")
# Download and extract bootloader packages
self.download_and_extract_package("grub2-common", self.grub_dir)
self.download_and_extract_package("grub-efi-riscv64-bin", self.grub_dir)
self.download_and_extract_package("grub-efi-riscv64-unsigned", self.grub_dir)
self.download_and_extract_package("u-boot-sifive", u_boot_dir)
# Add GRUB to tree
self.setup_grub_common_files()
copy_unsigned_monolithic_grub_to_boot_tree(
self.grub_dir, "riscv64", "riscv64", self.boot_tree
)
# Extract DTBs to tree
self.logger.log("extracting device tree files")
kernel_layer = self.scratch.joinpath("kernel-layer")
squashfs_path = self.iso_root.joinpath(
"casper", "ubuntu-server-minimal.squashfs"
)
# Extract device tree firmware from squashfs
self.logger.run(
[
"unsquashfs",
"-no-xattrs",
"-d",
kernel_layer,
squashfs_path,
"usr/lib/firmware",
],
check=True,
)
# Copy DTBs if they exist
dtb_dir = self.boot_tree.joinpath("dtb")
dtb_dir.mkdir(parents=True, exist_ok=True)
firmware_dir = kernel_layer.joinpath("usr", "lib", "firmware")
device_tree_files = list(firmware_dir.glob("*/device-tree/*"))
if device_tree_files:
for dtb_file in device_tree_files:
if dtb_file.is_file():
shutil.copy(dtb_file, dtb_dir)
# Clean up kernel layer
shutil.rmtree(kernel_layer)
# Copy tree contents to live-media rootfs
self.logger.run(["cp", "-aT", self.boot_tree, self.iso_root], check=True)
# Create ESP image with GRUB and dtbs
efi_img = self.scratch.joinpath("efi.img")
self.logger.run(
["mkfs.msdos", "-n", "ESP", "-C", "-v", efi_img, "32768"], check=True
)
# Add EFI files to ESP
efi_dir = self.boot_tree.joinpath("EFI")
self.logger.run(["mcopy", "-s", "-i", efi_img, efi_dir, "::/."], check=True)
# Add DTBs to ESP
self.logger.run(["mcopy", "-s", "-i", efi_img, dtb_dir, "::/."], check=True)
def generate_grub_config(self) -> None:
"""Generate grub.cfg for RISC-V64."""
grub_dir = self.iso_root.joinpath("boot", "grub")
grub_dir.mkdir(parents=True, exist_ok=True)
grub_cfg = grub_dir.joinpath("grub.cfg")
# Write GRUB header (without loadfont for RISC-V)
self.write_grub_header(grub_cfg, include_loadfont=False)
# Main menu entry
with grub_cfg.open("a") as f:
f.write(
f"""menuentry "Try or Install {self.humanproject}" {{
\tset gfxpayload=keep
\tlinux\t/casper/vmlinux efi=debug sysctl.kernel.watchdog_thresh=60 ---
\tinitrd\t/casper/initrd
}}
"""
)
# HWE kernel option if available
self.write_hwe_menu_entry(
grub_cfg,
"vmlinux",
"---",
extra_params="efi=debug sysctl.kernel.watchdog_thresh=60 ",
)
def post_process_iso(self, iso_path: pathlib.Path) -> None:
"""Add GPT partitions with U-Boot for SiFive Unmatched board.
The SiFive Unmatched board needs a GPT table containing U-Boot in
order to boot. U-Boot does not currently support booting from a CD,
so the GPT table also contains an entry pointing to the ESP so that
U-Boot can find it.
"""
u_boot_dir = self.scratch.joinpath(
"u-boot-sifive", "usr", "lib", "u-boot", "sifive_unmatched"
)
self.logger.run(
[
"sgdisk",
iso_path,
"--set-alignment=2",
"-d",
"1",
"-n",
"1:2082:10273",
"-c",
"1:loader2",
"-t",
"1:2E54B353-1271-4842-806F-E436D6AF6985",
"-n",
"3:10274:12321",
"-c",
"3:loader1",
"-t",
"3:5B193300-FC78-40CD-8002-E86C45580B47",
"-c",
"2:ESP",
"-r=2:3",
],
)
self.logger.run(
[
"dd",
f"if={u_boot_dir / 'u-boot.itb'}",
f"of={iso_path}",
"bs=512",
"seek=2082",
"conv=notrunc",
],
)
self.logger.run(
[
"dd",
f"if={u_boot_dir / 'u-boot-spl.bin'}",
f"of={iso_path}",
"bs=512",
"seek=10274",
"conv=notrunc",
],
)

View File

@ -0,0 +1,206 @@
"""IBM S/390 architecture boot configuration."""
import pathlib
import shutil
import struct
from .base import BaseBootConfigurator
README_dot_boot = """\
About the S/390 installation CD
===============================
It is possible to "boot" the installation system off this CD using
the files provided in the /boot directory.
Although you can boot the installer from this CD, the installation
itself is *not* actually done from the CD. Once the initrd is loaded,
the installer will ask you to configure your network connection and
uses the network-console component to allow you to continue the
installation over SSH. The rest of the installation is done over the
network: all installer components and Debian packages are retrieved
from a mirror.
Instead of SSH, one can also use the ASCII terminal available in HMC.
Exporting full .iso contents (including the hidden .disk directory)
allows one to use the result as a valid mirror for installation.
"""
ubuntu_dot_exec = """\
/* REXX EXEC TO IPL Ubuntu for */
/* z Systems FROM THE VM READER. */
/* */
'CP CLOSE RDR'
'PURGE RDR ALL'
'SPOOL PUNCH * RDR'
'PUNCH KERNEL UBUNTU * (NOHEADER'
'PUNCH PARMFILE UBUNTU * (NOHEADER'
'PUNCH INITRD UBUNTU * (NOHEADER'
'CHANGE RDR ALL KEEP NOHOLD'
'CP IPL 000C CLEAR'
"""
ubuntu_dot_ins = """\
* Ubuntu for IBM Z (default kernel)
kernel.ubuntu 0x00000000
initrd.off 0x0001040c
initrd.siz 0x00010414
parmfile.ubuntu 0x00010480
initrd.ubuntu 0x01000000
"""
def gen_s390_cd_kernel(
kernel: pathlib.Path, initrd: pathlib.Path, cmdline: str, outfile: pathlib.Path
) -> None:
"""Generate a bootable S390X CD kernel image.
This is a Python translation of gen-s390-cd-kernel.pl from debian-cd.
It creates a bootable image for S/390 architecture by combining kernel,
initrd, and boot parameters in a specific format.
"""
# Calculate sizes
initrd_size = initrd.stat().st_size
# The initrd is placed at a fixed offset of 16 MiB
initrd_offset = 0x1000000
# Calculate total boot image size (rounded up to 4K blocks)
boot_size = ((initrd_offset + initrd_size) >> 12) + 1
boot_size = boot_size << 12
# Validate cmdline length (max 896 bytes)
if len(cmdline) >= 896:
raise ValueError(f"Kernel commandline too long ({len(cmdline)} bytes)")
# Create output file and fill with zeros
with outfile.open("wb") as out_fh:
# Fill entire file with zeros
out_fh.write(b"\x00" * boot_size)
# Copy kernel to offset 0
out_fh.seek(0)
with kernel.open("rb") as kernel_fh:
out_fh.write(kernel_fh.read())
# Copy initrd to offset 0x1000000 (16 MiB)
out_fh.seek(initrd_offset)
with initrd.open("rb") as initrd_fh:
out_fh.write(initrd_fh.read())
# Write boot loader control value at offset 4
# This tells the S/390 boot loader where to find the kernel
out_fh.seek(4)
out_fh.write(struct.pack("!I", 0x80010000))
# Write kernel command line at offset 0x10480
out_fh.seek(0x10480)
out_fh.write(cmdline.encode("utf-8"))
# Write initrd parameters
# Initrd offset at 0x1040C
out_fh.seek(0x1040C)
out_fh.write(struct.pack("!I", initrd_offset))
# Initrd size at 0x10414
out_fh.seek(0x10414)
out_fh.write(struct.pack("!I", initrd_size))
class S390XBootConfigurator(BaseBootConfigurator):
"""Boot setup for IBM S/390 architecture."""
def mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return mkisofs options for S390X."""
return [
"-J",
"-no-emul-boot",
"-b",
"boot/ubuntu.ikr",
]
def extract_files(self) -> None:
"""Set up boot files for S390X."""
self.logger.log("extracting S390X boot files")
boot_dir = self.iso_root.joinpath("boot")
boot_dir.mkdir(parents=True, exist_ok=True)
# Copy static .ins & exec scripts, docs from data directory
self.iso_root.joinpath("README.boot").write_text(README_dot_boot)
boot_dir.joinpath("ubuntu.exec").write_text(ubuntu_dot_exec)
boot_dir.joinpath("ubuntu.ins").write_text(ubuntu_dot_ins)
# Move kernel image to the name used in .ins & exec scripts
kernel_src = self.iso_root.joinpath("casper", "vmlinuz")
kernel_dst = boot_dir.joinpath("kernel.ubuntu")
kernel_src.replace(kernel_dst)
# Move initrd to the name used in .ins & exec scripts
initrd_src = self.iso_root.joinpath("casper", "initrd")
initrd_dst = boot_dir.joinpath("initrd.ubuntu")
initrd_src.replace(initrd_dst)
# Compute initrd offset & size, store in files used by .ins & exec scripts
# Offset is always 0x1000000 (16 MiB)
initrd_offset_file = boot_dir.joinpath("initrd.off")
with initrd_offset_file.open("wb") as f:
f.write(struct.pack("!I", 0x1000000))
# Size is the actual size of the initrd
initrd_size = initrd_dst.stat().st_size
initrd_size_file = boot_dir.joinpath("initrd.siz")
with initrd_size_file.open("wb") as f:
f.write(struct.pack("!I", initrd_size))
# Compute cmdline, store in parmfile used by .ins & exec scripts
parmfile = boot_dir.joinpath("parmfile.ubuntu")
with parmfile.open("w") as f:
f.write(" --- ")
# Generate secondary top-level ubuntu.ins file
# This transforms lines not starting with * by prepending "boot/"
ubuntu_ins_src = boot_dir.joinpath("ubuntu.ins")
ubuntu_ins_dst = self.iso_root.joinpath("ubuntu.ins")
if ubuntu_ins_src.exists():
self.logger.run(
["sed", "-e", "s,^[^*],boot/&,g", ubuntu_ins_src],
stdout=ubuntu_ins_dst.open("w"),
check=True,
)
# Generate QEMU-KVM boot image using gen_s390_cd_kernel
cmdline = parmfile.read_text().strip()
ikr_file = boot_dir.joinpath("ubuntu.ikr")
gen_s390_cd_kernel(kernel_dst, initrd_dst, cmdline, ikr_file)
# Extract bootloader signing certificate
installed_pem = pathlib.Path("/usr/lib/s390-tools/stage3.pem")
squashfs_root = self.iso_root.joinpath("squashfs-root")
squashfs_path = self.iso_root.joinpath(
"casper", "ubuntu-server-minimal.squashfs"
)
if squashfs_path.exists():
self.logger.run(
[
"unsquashfs",
"-no-xattrs",
"-i",
"-d",
squashfs_root,
squashfs_path,
installed_pem,
],
check=True,
)
# Move certificate to iso root
cert_src = squashfs_root.joinpath(str(installed_pem).lstrip("/"))
cert_dst = self.iso_root.joinpath("ubuntu.pem")
if cert_src.exists():
cert_src.replace(cert_dst)
# Clean up squashfs extraction
shutil.rmtree(squashfs_root)

View File

@ -0,0 +1,168 @@
"""UEFI boot configuration for AMD64 and ARM64 architectures."""
import pathlib
import shutil
from ..builder import Logger
from .grub import GrubBootConfigurator
def copy_signed_shim_grub_to_boot_tree(
shim_dir: pathlib.Path,
grub_dir: pathlib.Path,
efi_suffix: str,
grub_target: str,
boot_tree: pathlib.Path,
) -> None:
efi_boot_dir = boot_tree.joinpath("EFI", "boot")
efi_boot_dir.mkdir(parents=True, exist_ok=True)
shutil.copy(
shim_dir.joinpath("usr", "lib", "shim", f"shim{efi_suffix}.efi.signed.latest"),
efi_boot_dir.joinpath(f"boot{efi_suffix}.efi"),
)
shutil.copy(
shim_dir.joinpath("usr", "lib", "shim", f"mm{efi_suffix}.efi"),
efi_boot_dir.joinpath(f"mm{efi_suffix}.efi"),
)
shutil.copy(
grub_dir.joinpath(
"usr",
"lib",
"grub",
f"{grub_target}-efi-signed",
f"gcd{efi_suffix}.efi.signed",
),
efi_boot_dir.joinpath(f"grub{efi_suffix}.efi"),
)
grub_boot_dir = boot_tree.joinpath("boot", "grub", f"{grub_target}-efi")
grub_boot_dir.mkdir(parents=True, exist_ok=True)
src_grub_dir = grub_dir.joinpath("usr", "lib", "grub", f"{grub_target}-efi")
for mod_file in src_grub_dir.glob("*.mod"):
shutil.copy(mod_file, grub_boot_dir)
for lst_file in src_grub_dir.glob("*.lst"):
shutil.copy(lst_file, grub_boot_dir)
def create_eltorito_esp_image(
logger: Logger, boot_tree: pathlib.Path, target_file: pathlib.Path
) -> None:
logger.log("creating El Torito ESP image")
efi_dir = boot_tree.joinpath("EFI")
# Calculate size: du -s --apparent-size --block-size=1024 + 1024
result = logger.run(
["du", "-s", "--apparent-size", "--block-size=1024", efi_dir],
capture_output=True,
text=True,
check=True,
)
size_kb = int(result.stdout.split()[0]) + 1024
# Create filesystem: mkfs.msdos -n ESP -C -v
logger.run(
["mkfs.msdos", "-n", "ESP", "-C", "-v", target_file, str(size_kb)],
check=True,
)
# Copy files: mcopy -s -i target_file EFI ::/.
logger.run(["mcopy", "-s", "-i", target_file, efi_dir, "::/."], check=True)
class UEFIBootConfigurator(GrubBootConfigurator):
"""Base class for UEFI-based architectures (AMD64, ARM64).
Subclasses should set:
- efi_suffix: EFI binary suffix (e.g., "x64", "aa64")
- grub_target: GRUB target name (e.g., "x86_64", "arm64")
"""
# Subclasses must override these
efi_suffix: str = ""
grub_target: str = ""
arch: str = ""
def create_dirs(self, workdir):
super().create_dirs(workdir)
self.shim_dir = self.boot_tree.joinpath("shim")
def get_uefi_grub_packages(self) -> list[str]:
"""Return list of UEFI GRUB packages to download."""
return [
"grub2-common",
f"grub-efi-{self.arch}-bin",
f"grub-efi-{self.arch}-signed",
]
def extract_uefi_files(self) -> None:
"""Extract common UEFI files to boot tree."""
# Download UEFI packages
self.download_and_extract_package("shim-signed", self.shim_dir)
for pkg in self.get_uefi_grub_packages():
self.download_and_extract_package(pkg, self.grub_dir)
# Add common files for GRUB to tree
self.setup_grub_common_files()
# Add EFI GRUB to tree
copy_signed_shim_grub_to_boot_tree(
self.shim_dir,
self.grub_dir,
self.efi_suffix,
self.grub_target,
self.boot_tree,
)
# Create ESP image for El-Torito catalog and hybrid boot
create_eltorito_esp_image(
self.logger, self.boot_tree, self.scratch.joinpath("cd-boot-efi.img")
)
def write_uefi_menu_entries(self, grub_cfg: pathlib.Path) -> None:
"""Write UEFI firmware menu entries."""
with grub_cfg.open("a") as f:
f.write(
"""menuentry 'Boot from next volume' {
\texit 1
}
menuentry 'UEFI Firmware Settings' {
\tfwsetup
}
"""
)
def get_uefi_mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return common UEFI mkisofs options."""
# To make our ESP / El-Torito image compliant with MBR/GPT standards,
# we first append it as a partition and then point the El Torito at
# it. See https://lists.debian.org/debian-cd/2019/07/msg00007.html
opts: list[str | pathlib.Path] = [
"-append_partition",
"2",
"0xef",
self.scratch.joinpath("cd-boot-efi.img"),
"-appended_part_as_gpt",
]
# Some BIOSes ignore removable disks with no partitions marked bootable
# in the MBR. Make sure our protective MBR partition is marked bootable.
opts.append("--mbr-force-bootable")
# Start a new entry in the el torito boot catalog
opts.append("-eltorito-alt-boot")
# Specify where the el torito UEFI boot image "name". We use a special
# syntax available in latest xorriso to point at our newly-created
# partition.
opts.extend(["-e", "--interval:appended_partition_2:all::"])
# Whether to emulate a floppy or not is a per-boot-catalog-entry
# thing, so we need to say it again.
opts.append("-no-emul-boot")
# Create a partition table entry that covers the iso9660 filesystem
opts.extend(["-partition_offset", "16"])
return opts