Make generate_grub_config return strings instead of writing files

Separate config generation from file I/O by having generate_grub_config()
and its helpers return strings. The base class make_bootable() now handles
writing grub.cfg.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael.hudson@canonical.com 2026-02-19 13:04:18 +13:00
parent a5cffa8414
commit a6466ab0a3
No known key found for this signature in database
GPG Key ID: 80E627A0AB757E23
6 changed files with 114 additions and 139 deletions

View File

@ -87,102 +87,95 @@ class AMD64BootConfigurator(UEFIBootConfigurator):
["*.mod", "*.lst", "*.o"], ["*.mod", "*.lst", "*.o"],
) )
def generate_grub_config(self) -> None: def generate_grub_config(self) -> str:
"""Generate grub.cfg and loopback.cfg for the boot tree.""" """Generate grub.cfg content for AMD64."""
boot_grub_dir = self.iso_root.joinpath("boot", "grub") result = self.grub_header()
boot_grub_dir.mkdir(parents=True, exist_ok=True)
grub_cfg = boot_grub_dir.joinpath("grub.cfg")
if self.project == "ubuntu-mini-iso": if self.project == "ubuntu-mini-iso":
self.write_grub_header(grub_cfg) result += """\
with grub_cfg.open("a") as f: menuentry "Choose an Ubuntu version to install" {
f.write(
"""menuentry "Choose an Ubuntu version to install" {
set gfxpayload=keep set gfxpayload=keep
linux /casper/vmlinuz iso-chooser-menu ip=dhcp --- linux /casper/vmlinuz iso-chooser-menu ip=dhcp ---
initrd /casper/initrd initrd /casper/initrd
} }
""" """
) return result
return
# Generate grub.cfg
kernel_params = default_kernel_params(self.project) kernel_params = default_kernel_params(self.project)
# Write common GRUB header
self.write_grub_header(grub_cfg)
# Main menu entry # Main menu entry
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "Try or Install {self.humanproject}" {{
f"""menuentry "Try or Install {self.humanproject}" {{
set gfxpayload=keep set gfxpayload=keep
linux /casper/vmlinuz {kernel_params} linux /casper/vmlinuz {kernel_params}
initrd /casper/initrd initrd /casper/initrd
}} }}
""" """
)
# All but server get safe-graphics mode # All but server get safe-graphics mode
if self.project != "ubuntu-server": if self.project != "ubuntu-server":
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "{self.humanproject} (safe graphics)" {{
f"""menuentry "{self.humanproject} (safe graphics)" {{
set gfxpayload=keep set gfxpayload=keep
linux /casper/vmlinuz nomodeset {kernel_params} linux /casper/vmlinuz nomodeset {kernel_params}
initrd /casper/initrd initrd /casper/initrd
}} }}
""" """
)
# ubiquity based projects get OEM mode # ubiquity based projects get OEM mode
if "maybe-ubiquity" in kernel_params: if "maybe-ubiquity" in kernel_params:
oem_kernel_params = kernel_params.replace( oem_kernel_params = kernel_params.replace(
"maybe-ubiquity", "only-ubiquity oem-config/enable=true" "maybe-ubiquity", "only-ubiquity oem-config/enable=true"
) )
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "OEM install (for manufacturers)" {{
f"""menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep set gfxpayload=keep
linux /casper/vmlinuz {oem_kernel_params} linux /casper/vmlinuz {oem_kernel_params}
initrd /casper/initrd initrd /casper/initrd
}} }}
""" """
)
# Calamares-based projects get OEM mode # Calamares-based projects get OEM mode
if self.project in CALAMARES_PROJECTS: if self.project in CALAMARES_PROJECTS:
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "OEM install (for manufacturers)" {{
f"""menuentry "OEM install (for manufacturers)" {{
set gfxpayload=keep set gfxpayload=keep
linux /casper/vmlinuz {kernel_params} oem-config/enable=true linux /casper/vmlinuz {kernel_params} oem-config/enable=true
initrd /casper/initrd initrd /casper/initrd
}} }}
""" """
)
# Currently only server is built with HWE, hence no safe-graphics/OEM # Currently only server is built with HWE, hence no safe-graphics/OEM
if self.hwe: if self.hwe:
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "{self.humanproject} with the HWE kernel" {{
f"""menuentry "{self.humanproject} with the HWE kernel" {{
set gfxpayload=keep set gfxpayload=keep
linux /casper/hwe-vmlinuz {kernel_params} linux /casper/hwe-vmlinuz {kernel_params}
initrd /casper/hwe-initrd initrd /casper/hwe-initrd
}} }}
""" """
)
# Create the loopback config, based on the main config # UEFI Entries (wrapped in grub_platform check for dual BIOS/UEFI support)
with grub_cfg.open("r") as f: uefi_menu_entries = self.uefi_menu_entries()
content = f.read()
# sed: delete from line 1 to menu_color_highlight, delete from result += f"""\
# grub_platform to end and replace '---' with grub_platform
# 'iso-scan/filename=${iso_path} ---' in lines with 'linux' if [ "$grub_platform" = "efi" ]; then
lines = content.split("\n") {uefi_menu_entries}
fi
"""
return result
@staticmethod
def generate_loopback_config(grub_content: str) -> str:
"""Derive loopback.cfg from grub.cfg content.
Strips the header (up to menu_color_highlight) and the UEFI
trailer (from grub_platform to end), and adds iso-scan/filename
to linux lines.
"""
lines = grub_content.split("\n")
start_idx = 0 start_idx = 0
for i, line in enumerate(lines): for i, line in enumerate(lines):
if "menu_color_highlight" in line: if "menu_color_highlight" in line:
@ -205,16 +198,20 @@ class AMD64BootConfigurator(UEFIBootConfigurator):
for line in loopback_lines for line in loopback_lines
] ]
loopback_cfg = boot_grub_dir.joinpath("loopback.cfg") return "\n".join(loopback_lines)
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) def make_bootable(
with grub_cfg.open("a") as f: self,
f.write("grub_platform\n") workdir: pathlib.Path,
f.write('if [ "$grub_platform" = "efi" ]; then\n') project: str,
capproject: str,
self.write_uefi_menu_entries(grub_cfg) subarch: str,
hwe: bool,
with grub_cfg.open("a") as f: ) -> None:
f.write("fi\n") """Make the ISO bootable, including generating loopback.cfg."""
super().make_bootable(workdir, project, capproject, subarch, hwe)
grub_cfg = self.iso_root.joinpath("boot", "grub", "grub.cfg")
grub_content = grub_cfg.read_text()
self.iso_root.joinpath("boot", "grub", "loopback.cfg").write_text(
self.generate_loopback_config(grub_content)
)

View File

@ -33,19 +33,15 @@ class ARM64BootConfigurator(UEFIBootConfigurator):
with self.logger.logged("extracting ARM64 boot files"): with self.logger.logged("extracting ARM64 boot files"):
self.extract_uefi_files() self.extract_uefi_files()
def generate_grub_config(self) -> None: def generate_grub_config(self) -> str:
"""Generate grub.cfg for ARM64.""" """Generate grub.cfg for ARM64."""
kernel_params = default_kernel_params(self.project) kernel_params = default_kernel_params(self.project)
grub_cfg = self.iso_root.joinpath("boot", "grub", "grub.cfg") result = self.grub_header()
# Write common GRUB header
self.write_grub_header(grub_cfg)
# ARM64-specific: Snapdragon workarounds # ARM64-specific: Snapdragon workarounds
with grub_cfg.open("a") as f: result += f"""\
f.write( set cmdline=
"""set cmdline=
smbios --type 4 --get-string 5 --set proc_version smbios --type 4 --get-string 5 --set proc_version
regexp "Snapdragon.*" "$proc_version" regexp "Snapdragon.*" "$proc_version"
if [ $? = 0 ]; then if [ $? = 0 ]; then
@ -63,11 +59,9 @@ menuentry "Try or Install {self.humanproject}" {{
\tinitrd\t/casper/initrd \tinitrd\t/casper/initrd
}} }}
""" """
)
# HWE kernel option if available # HWE kernel option if available
self.write_hwe_menu_entry( result += self.hwe_menu_entry(
grub_cfg,
"vmlinuz", "vmlinuz",
f"{kernel_params} console=tty0", f"{kernel_params} console=tty0",
extra_params="$cmdline ", extra_params="$cmdline ",
@ -77,4 +71,6 @@ menuentry "Try or Install {self.humanproject}" {{
# but it's not actually set anywhere in the grub.cfg, so we omit it here # 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) # UEFI Entries (ARM64 is UEFI-only, no grub_platform check needed)
self.write_uefi_menu_entries(grub_cfg) result += self.uefi_menu_entries()
return result

View File

@ -39,59 +39,52 @@ class GrubBootConfigurator(BaseBootConfigurator):
Subclasses must implement generate_grub_config(). Subclasses must implement generate_grub_config().
""" """
def write_grub_header( def grub_header(self, include_loadfont: bool = True) -> str:
self, grub_cfg: pathlib.Path, include_loadfont: bool = True """Return common GRUB config header (timeout, colors).
) -> None:
"""Write common GRUB config header (timeout, colors).
Args: Args:
grub_cfg: Path to grub.cfg file
include_loadfont: Whether to include 'loadfont unicode' include_loadfont: Whether to include 'loadfont unicode'
(not needed for RISC-V) (not needed for RISC-V)
""" """
with grub_cfg.open("a") as f: result = "set timeout=30\n\n"
f.write("set timeout=30\n\n") if include_loadfont:
if include_loadfont: result += "loadfont unicode\n\n"
f.write("loadfont unicode\n\n") result += """\
f.write( set menu_color_normal=white/black
"""set menu_color_normal=white/black
set menu_color_highlight=black/light-gray set menu_color_highlight=black/light-gray
""" """
) return result
def write_hwe_menu_entry( def hwe_menu_entry(
self, self,
grub_cfg: pathlib.Path,
kernel_name: str, kernel_name: str,
kernel_params: str, kernel_params: str,
extra_params: str = "", extra_params: str = "",
) -> None: ) -> str:
"""Write HWE kernel menu entry if HWE is enabled. """Return HWE kernel menu entry if HWE is enabled.
Args: Args:
grub_cfg: Path to grub.cfg file
kernel_name: Kernel binary name (vmlinuz or vmlinux) kernel_name: Kernel binary name (vmlinuz or vmlinux)
kernel_params: Kernel parameters to append kernel_params: Kernel parameters to append
extra_params: Additional parameters (e.g., console=tty0, $cmdline) extra_params: Additional parameters (e.g., console=tty0, $cmdline)
""" """
if self.hwe: if not self.hwe:
with grub_cfg.open("a") as f: return ""
f.write( return f"""\
f"""menuentry "{self.humanproject} with the HWE kernel" {{ menuentry "{self.humanproject} with the HWE kernel" {{
\tset gfxpayload=keep set gfxpayload=keep
\tlinux\t/casper/hwe-{kernel_name} {extra_params}{kernel_params} linux /casper/hwe-{kernel_name} {extra_params}{kernel_params}
\tinitrd\t/casper/hwe-initrd initrd /casper/hwe-initrd
}} }}
""" """
)
@abstractmethod @abstractmethod
def generate_grub_config(self) -> None: def generate_grub_config(self) -> str:
"""Generate grub.cfg configuration file. """Generate grub.cfg content.
Each GRUB-based architecture must implement this to create its Each GRUB-based architecture must implement this to return the
specific GRUB configuration. GRUB configuration.
""" """
... ...
@ -106,4 +99,7 @@ set menu_color_highlight=black/light-gray
"""Make the ISO bootable by extracting files and generating GRUB config.""" """Make the ISO bootable by extracting files and generating GRUB config."""
super().make_bootable(workdir, project, capproject, subarch, hwe) super().make_bootable(workdir, project, capproject, subarch, hwe)
with self.logger.logged("generating grub config"): with self.logger.logged("generating grub config"):
self.generate_grub_config() content = self.generate_grub_config()
grub_dir = self.iso_root.joinpath("boot", "grub")
grub_dir.mkdir(parents=True, exist_ok=True)
grub_dir.joinpath("grub.cfg").write_text(content)

View File

@ -53,27 +53,22 @@ class PPC64ELBootConfigurator(GrubBootConfigurator):
grub_pkg_dir, self.iso_root, "powerpc-ieee1275", ["*.mod", "*.lst"] grub_pkg_dir, self.iso_root, "powerpc-ieee1275", ["*.mod", "*.lst"]
) )
def generate_grub_config(self) -> None: def generate_grub_config(self) -> str:
"""Generate grub.cfg for PPC64EL.""" """Generate grub.cfg for PPC64EL."""
kernel_params = default_kernel_params(self.project) kernel_params = default_kernel_params(self.project)
grub_cfg = self.iso_root.joinpath("boot", "grub", "grub.cfg") result = self.grub_header()
# Write common GRUB header
self.write_grub_header(grub_cfg)
# Main menu entry # Main menu entry
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "Try or Install {self.humanproject}" {{
f"""menuentry "Try or Install {self.humanproject}" {{ set gfxpayload=keep
\tset gfxpayload=keep linux /casper/vmlinux quiet {kernel_params}
\tlinux\t/casper/vmlinux quiet {kernel_params} initrd /casper/initrd
\tinitrd\t/casper/initrd
}} }}
""" """
)
# HWE kernel option if available # HWE kernel option if available
self.write_hwe_menu_entry( result += self.hwe_menu_entry("vmlinux", kernel_params, extra_params="quiet ")
grub_cfg, "vmlinux", kernel_params, extra_params="quiet "
) return result

View File

@ -126,35 +126,28 @@ class RISCV64BootConfigurator(GrubBootConfigurator):
# Add DTBs to ESP # Add DTBs to ESP
self.logger.run(["mcopy", "-s", "-i", efi_img, dtb_dir, "::/."], check=True) self.logger.run(["mcopy", "-s", "-i", efi_img, dtb_dir, "::/."], check=True)
def generate_grub_config(self) -> None: def generate_grub_config(self) -> str:
"""Generate grub.cfg for RISC-V64.""" """Generate grub.cfg for RISC-V64."""
grub_dir = self.iso_root.joinpath("boot", "grub") result = self.grub_header(include_loadfont=False)
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 # Main menu entry
with grub_cfg.open("a") as f: result += f"""\
f.write( menuentry "Try or Install {self.humanproject}" {{
f"""menuentry "Try or Install {self.humanproject}" {{ set gfxpayload=keep
\tset gfxpayload=keep linux /casper/vmlinux efi=debug sysctl.kernel.watchdog_thresh=60 ---
\tlinux\t/casper/vmlinux efi=debug sysctl.kernel.watchdog_thresh=60 --- initrd /casper/initrd
\tinitrd\t/casper/initrd
}} }}
""" """
)
# HWE kernel option if available # HWE kernel option if available
self.write_hwe_menu_entry( result += self.hwe_menu_entry(
grub_cfg,
"vmlinux", "vmlinux",
"---", "---",
extra_params="efi=debug sysctl.kernel.watchdog_thresh=60 ", extra_params="efi=debug sysctl.kernel.watchdog_thresh=60 ",
) )
return result
def post_process_iso(self, iso_path: pathlib.Path) -> None: def post_process_iso(self, iso_path: pathlib.Path) -> None:
"""Add GPT partitions with U-Boot for SiFive Unmatched board. """Add GPT partitions with U-Boot for SiFive Unmatched board.

View File

@ -122,18 +122,16 @@ class UEFIBootConfigurator(GrubBootConfigurator):
self.logger, self.iso_root, self.scratch.joinpath("cd-boot-efi.img") self.logger, self.iso_root, self.scratch.joinpath("cd-boot-efi.img")
) )
def write_uefi_menu_entries(self, grub_cfg: pathlib.Path) -> None: def uefi_menu_entries(self) -> str:
"""Write UEFI firmware menu entries.""" """Return UEFI firmware menu entries."""
with grub_cfg.open("a") as f: return """\
f.write( menuentry 'Boot from next volume' {
"""menuentry 'Boot from next volume' {
\texit 1 \texit 1
} }
menuentry 'UEFI Firmware Settings' { menuentry 'UEFI Firmware Settings' {
\tfwsetup \tfwsetup
} }
""" """
)
def get_uefi_mkisofs_opts(self) -> list[str | pathlib.Path]: def get_uefi_mkisofs_opts(self) -> list[str | pathlib.Path]:
"""Return common UEFI mkisofs options.""" """Return common UEFI mkisofs options."""