]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add UnifiedKernelImages= 2475/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 10 Mar 2024 15:45:32 +0000 (16:45 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 10 Mar 2024 20:09:37 +0000 (21:09 +0100)
Allows configuring whether we use UKIs or BLS Type 1 entries with
systemd-boot and grub on UEFI.

The BLS Type 1 logic we already had for grub on BIOS is made generic
and reused to implement this feature.

Partially fixes #2472.

mkosi/__init__.py
mkosi/config.py
mkosi/resources/mkosi.md
tests/test_json.py

index 6e2a551bc0b8cfca724f9c16652729b83febaf31..eb7881f06b027fc6e6d41d6a65034d432abd2498 100644 (file)
@@ -1133,15 +1133,12 @@ def find_grub_binary(binary: str, root: Path = Path("/")) -> Optional[Path]:
 
 
 def want_grub_efi(context: Context) -> bool:
-    if context.config.bootable == ConfigFeature.disabled:
+    if not want_efi(context.config):
         return False
 
     if context.config.bootloader != Bootloader.grub:
         return False
 
-    if context.config.overlay or context.config.output_format.is_extension_image():
-        return False
-
     if not any((context.root / "efi").rglob("grub*.efi")):
         if context.config.bootable == ConfigFeature.enabled:
             die("A bootable EFI image with grub was requested but grub for EFI is not installed in /efi")
@@ -1205,97 +1202,23 @@ def prepare_grub_config(context: Context) -> Optional[Path]:
         with umask(~0o600), config.open("w") as f:
             f.write("set timeout=0\n")
 
-    return config
-
-
-def prepare_grub_efi(context: Context) -> None:
-    if not want_grub_efi(context):
-        return
-
-    # Signed EFI grub shipped by distributions reads its configuration from /EFI/<distribution>/grub.cfg in
-    # the ESP so let's put a shim there to redirect to the actual configuration file.
-    earlyconfig = context.root / "efi/EFI" / context.config.distribution.name / "grub.cfg"
-    with umask(~0o700):
-        earlyconfig.parent.mkdir(parents=True, exist_ok=True)
-
-    # Read the actual config file from the root of the ESP.
-    earlyconfig.write_text(f"configfile /{context.config.distribution.grub_prefix()}/grub.cfg\n")
-
-    config = prepare_grub_config(context)
-    assert config
+    if want_grub_efi(context):
+        # Signed EFI grub shipped by distributions reads its configuration from /EFI/<distribution>/grub.cfg in
+        # the ESP so let's put a shim there to redirect to the actual configuration file.
+        earlyconfig = context.root / "efi/EFI" / context.config.distribution.name / "grub.cfg"
+        with umask(~0o700):
+            earlyconfig.parent.mkdir(parents=True, exist_ok=True)
 
-    with config.open("a") as f:
-        f.write('if [ "${grub_platform}" == "efi" ]; then\n')
+        # Read the actual config file from the root of the ESP.
+        earlyconfig.write_text(f"configfile /{context.config.distribution.grub_prefix()}/grub.cfg\n")
 
-        for uki in (context.root / "boot/EFI/Linux").glob("*.efi"):
-            f.write(
-                textwrap.dedent(
-                    f"""\
-                    menuentry "{uki.stem}" {{
-                        chainloader /{uki.relative_to(context.root / "boot")}
-                    }}
-                    """
-                )
-            )
-
-        f.write("fi\n")
+    return config
 
 
-def prepare_grub_bios(context: Context, partitions: Sequence[Partition]) -> None:
+def grub_bios_install(context: Context, partitions: Sequence[Partition]) -> None:
     if not want_grub_bios(context, partitions):
         return
 
-    config = prepare_grub_config(context)
-    assert config
-
-    root = finalize_root(partitions)
-    assert root
-
-    token = find_entry_token(context)
-
-    dst = context.root / "boot" / token
-    with umask(~0o700):
-        dst.mkdir(exist_ok=True)
-
-    with config.open("a") as f:
-        f.write('if [ "${grub_platform}" == "pc" ]; then\n')
-
-        for kver, kimg in gen_kernel_images(context):
-            kdst = dst / kver
-            with umask(~0o700):
-                kdst.mkdir(exist_ok=True)
-
-            microcode = build_microcode_initrd(context)
-            kmods = build_kernel_modules_initrd(context, kver)
-
-            with umask(~0o600):
-                kimg = Path(shutil.copy2(context.root / kimg, kdst / "vmlinuz"))
-                initrds = [Path(shutil.copy2(microcode, kdst / "microcode"))] if microcode else []
-                initrds += [
-                    Path(shutil.copy2(initrd, dst / initrd.name))
-                    for initrd in (context.config.initrds or [build_default_initrd(context)])
-                ]
-                initrds += [Path(shutil.copy2(kmods, kdst / "kmods"))]
-
-                image = Path("/") / kimg.relative_to(context.root / "boot")
-                cmdline = " ".join(context.config.kernel_command_line)
-                initrds = " ".join(
-                    [os.fspath(Path("/") / initrd.relative_to(context.root / "boot")) for initrd in initrds]
-                )
-
-                f.write(
-                    textwrap.dedent(
-                        f"""\
-                        menuentry "{token}-{kver}" {{
-                            linux {image} {root} {cmdline}
-                            initrd {initrds}
-                        }}
-                        """
-                    )
-                )
-
-        f.write('fi\n')
-
     # grub-install insists on opening the root partition device to probe it's filesystem which requires root
     # so we're forced to reimplement its functionality. Luckily that's pretty simple, run grub-mkimage to
     # generate the required core.img and copy the relevant files to the ESP.
@@ -1364,7 +1287,7 @@ def prepare_grub_bios(context: Context, partitions: Sequence[Partition]) -> None
             shutil.copy2(unicode, dst)
 
 
-def install_grub_bios(context: Context, partitions: Sequence[Partition]) -> None:
+def grub_bios_setup(context: Context, partitions: Sequence[Partition]) -> None:
     if not want_grub_bios(context, partitions):
         return
 
@@ -1880,15 +1803,7 @@ def build_uki(
     initrds: Sequence[Path],
     cmdline: Sequence[str],
     output: Path,
-    roothash: Optional[str] = None,
 ) -> None:
-    cmdline = list(cmdline)
-
-    if roothash:
-        cmdline += [roothash]
-
-    cmdline += context.config.kernel_command_line
-
     # Older versions of systemd-stub expect the cmdline section to be null terminated. We can't embed
     # nul terminators in argv so let's communicate the cmdline via a file instead.
     (context.workspace / "cmdline").write_text(f"{' '.join(cmdline).strip()}\x00")
@@ -2008,6 +1923,20 @@ def want_efi(config: Config) -> bool:
     return True
 
 
+def systemd_stub_binary(context: Context) -> Path:
+    arch = context.config.architecture.to_efi()
+    stub = context.root / f"usr/lib/systemd/boot/efi/linux{arch}.efi.stub"
+    return stub
+
+
+def want_uki(context: Context) -> bool:
+    return want_efi(context.config) and (
+        context.config.bootloader == Bootloader.uki or
+        context.config.unified_kernel_images == ConfigFeature.enabled or
+        (context.config.unified_kernel_images == ConfigFeature.auto and systemd_stub_binary(context).exists())
+    )
+
+
 def find_entry_token(context: Context) -> str:
     if (
         "--version" not in run(["kernel-install", "--help"],
@@ -2024,67 +1953,199 @@ def find_entry_token(context: Context) -> str:
     return cast(str, output["EntryToken"])
 
 
-def install_uki(context: Context, partitions: Sequence[Partition]) -> None:
-    # Iterates through all kernel versions included in the image and generates a combined
-    # kernel+initrd+cmdline+osrelease EFI file from it and places it in the /EFI/Linux directory of the ESP.
-    # sd-boot iterates through them and shows them in the menu. These "unified" single-file images have the
-    # benefit that they can be signed like normal EFI binaries, and can encode everything necessary to boot a
-    # specific root device, including the root hash.
+def finalize_cmdline(context: Context, roothash: Optional[str]) -> list[str]:
+    if (context.root / "etc/kernel/cmdline").exists():
+        cmdline = [(context.root / "etc/kernel/cmdline").read_text().strip()]
+    elif (context.root / "usr/lib/kernel/cmdline").exists():
+        cmdline = [(context.root / "usr/lib/kernel/cmdline").read_text().strip()]
+    else:
+        cmdline = []
 
-    if not want_efi(context.config) or context.config.output_format in (OutputFormat.uki, OutputFormat.esp):
-        return
+    if roothash:
+        cmdline += [roothash]
 
-    arch = context.config.architecture.to_efi()
-    stub = context.root / f"usr/lib/systemd/boot/efi/linux{arch}.efi.stub"
-    if not stub.exists() and context.config.bootable == ConfigFeature.auto:
-        return
+    return cmdline + context.config.kernel_command_line
 
-    if context.config.bootable == ConfigFeature.enabled and not gen_kernel_images(context):
-        die("A bootable image was requested but no kernel was found")
 
-    roothash = finalize_roothash(partitions)
+def install_type1(
+    context: Context,
+    kver: str,
+    kimg: Path,
+    token: str,
+    partitions: Sequence[Partition],
+) -> None:
+    dst = context.root / "boot" / token / kver
+    entry = context.root / f"boot/loader/entries/{token}-{kver}.conf"
+    with umask(~0o700):
+        dst.mkdir(parents=True, exist_ok=True)
+        entry.parent.mkdir(parents=True, exist_ok=True)
 
-    for kver, kimg in gen_kernel_images(context):
-        # See https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/#boot-counting
-        boot_count = ""
-        if (context.root / "etc/kernel/tries").exists():
-            boot_count = f'+{(context.root / "etc/kernel/tries").read_text().strip()}'
+    microcode = build_microcode_initrd(context)
+    kmods = build_kernel_modules_initrd(context, kver)
+    cmdline = finalize_cmdline(context, finalize_roothash(partitions))
 
-        if context.config.bootloader == Bootloader.uki:
-            if context.config.shim_bootloader != ShimBootloader.none:
-                boot_binary = context.root / shim_second_stage_binary(context)
-            else:
-                boot_binary = context.root / efi_boot_binary(context)
+    with umask(~0o600):
+        if (
+            want_efi(context.config) and
+            context.config.secure_boot and
+            KernelType.identify(context.config, kimg) == KernelType.pe
+        ):
+            kimg = sign_efi_binary(context, kimg, dst / "vmlinuz")
         else:
-            token = find_entry_token(context)
-            if roothash:
-                _, _, h = roothash.partition("=")
-                boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}-{h}{boot_count}.efi"
-            else:
-                boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}{boot_count}.efi"
+            kimg = Path(shutil.copy2(context.root / kimg, dst / "vmlinuz"))
 
-        microcode = build_microcode_initrd(context)
+        initrds = [Path(shutil.copy2(microcode, dst.parent / "microcode.initrd"))] if microcode else []
+        initrds += [
+            Path(shutil.copy2(initrd, dst.parent / initrd.name))
+            for initrd in (context.config.initrds or [build_default_initrd(context)])
+        ]
+        initrds += [Path(shutil.copy2(kmods, dst / "kernel-modules.initrd"))]
 
-        initrds = [microcode] if microcode else []
-        initrds += context.config.initrds or [build_default_initrd(context)]
+        with entry.open("w") as f:
+            f.write(
+                textwrap.dedent(
+                    f"""\
+                    title {token} {kver}
+                    version {kver}
+                    linux /{kimg.relative_to(context.root / "boot")}
+                    options {" ".join(cmdline)}
+                    """
+                )
+            )
 
-        if context.config.kernel_modules_initrd:
-            initrds += [build_kernel_modules_initrd(context, kver)]
+            for initrd in initrds:
+                f.write(f'initrd /{initrd.relative_to(context.root / "boot")}\n')
 
-        # Make sure the parent directory where we'll be writing the UKI exists.
-        with umask(~0o700):
-            boot_binary.parent.mkdir(parents=True, exist_ok=True)
+    if want_grub_efi(context) or want_grub_bios(context, partitions):
+        config = prepare_grub_config(context)
+        assert config
+
+        root = finalize_root(partitions)
+        assert root
+
+        with config.open("a") as f:
+            f.write("if ")
+
+            conditions = []
+            if want_grub_efi(context) and not want_uki(context):
+                conditions += ['[ "${grub_platform}" = efi ]']
+            if want_grub_bios(context, partitions):
+                conditions += ['[ "${grub_platform}" = pc ]']
+
+            f.write(" || ".join(conditions))
+            f.write("; then\n")
 
-        if (context.root / "etc/kernel/cmdline").exists():
-            cmdline = [(context.root / "etc/kernel/cmdline").read_text().strip()]
-        elif (context.root / "usr/lib/kernel/cmdline").exists():
-            cmdline = [(context.root / "usr/lib/kernel/cmdline").read_text().strip()]
+            f.write(
+                textwrap.dedent(
+                    f"""\
+                    menuentry "{token}-{kver}" {{
+                        linux /{kimg.relative_to(context.root / "boot")} {root} {" ".join(cmdline)}
+                        initrd {" ".join(os.fspath(Path("/") / i.relative_to(context.root / "boot")) for i in initrds)}
+                    }}
+                    """
+                )
+            )
+
+            f.write("fi\n")
+
+
+def install_uki(context: Context, kver: str, kimg: Path, token: str, partitions: Sequence[Partition]) -> None:
+    roothash = finalize_roothash(partitions)
+
+    boot_count = ""
+    if (context.root / "etc/kernel/tries").exists():
+        boot_count = f'+{(context.root / "etc/kernel/tries").read_text().strip()}'
+
+    if context.config.bootloader == Bootloader.uki:
+        if context.config.shim_bootloader != ShimBootloader.none:
+            boot_binary = context.root / shim_second_stage_binary(context)
+        else:
+            boot_binary = context.root / efi_boot_binary(context)
+    else:
+        if roothash:
+            _, _, h = roothash.partition("=")
+            boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}-{h}{boot_count}.efi"
         else:
-            cmdline = []
+            boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}{boot_count}.efi"
+
+    microcode = build_microcode_initrd(context)
+
+    initrds = [microcode] if microcode else []
+    initrds += context.config.initrds or [build_default_initrd(context)]
 
-        build_uki(context, stub, kver, context.root / kimg, initrds, cmdline, boot_binary, roothash=roothash)
+    if context.config.kernel_modules_initrd:
+        initrds += [build_kernel_modules_initrd(context, kver)]
 
-        print_output_size(boot_binary)
+    # Make sure the parent directory where we'll be writing the UKI exists.
+    with umask(~0o700):
+        boot_binary.parent.mkdir(parents=True, exist_ok=True)
+
+    build_uki(
+        context,
+        systemd_stub_binary(context),
+        kver,
+        context.root / kimg,
+        initrds,
+        finalize_cmdline(context, roothash),
+        boot_binary,
+    )
+
+    print_output_size(boot_binary)
+
+    if want_grub_efi(context):
+        config = prepare_grub_config(context)
+        assert config
+
+        with config.open("a") as f:
+            f.write('if [ "${grub_platform}" = efi ]; then\n')
+
+            f.write(
+                textwrap.dedent(
+                    f"""\
+                    menuentry "{boot_binary.stem}" {{
+                        chainloader /{boot_binary.relative_to(context.root / "boot")}
+                    }}
+                    """
+                )
+            )
+
+            f.write("fi\n")
+
+
+def install_kernel(context: Context, partitions: Sequence[Partition]) -> None:
+    # Iterates through all kernel versions included in the image and generates a combined
+    # kernel+initrd+cmdline+osrelease EFI file from it and places it in the /EFI/Linux directory of the ESP.
+    # sd-boot iterates through them and shows them in the menu. These "unified" single-file images have the
+    # benefit that they can be signed like normal EFI binaries, and can encode everything necessary to boot a
+    # specific root device, including the root hash.
+
+    if context.config.output_format in (OutputFormat.uki, OutputFormat.esp):
+        return
+
+    if context.config.bootable == ConfigFeature.disabled:
+        return
+
+    if context.config.bootable == ConfigFeature.auto and (
+        context.config.output_format == OutputFormat.cpio or
+        context.config.output_format.is_extension_image() or
+        context.config.overlay
+    ):
+        return
+
+    stub = systemd_stub_binary(context)
+    if want_uki(context) and not stub.exists():
+        die(f"Unified kernel image(s) requested but systemd-stub not found at /{stub.relative_to(context.root)}")
+
+    if context.config.bootable == ConfigFeature.enabled and not gen_kernel_images(context):
+        die("A bootable image was requested but no kernel was found")
+
+    token = find_entry_token(context)
+
+    for kver, kimg in gen_kernel_images(context):
+        if want_uki(context):
+            install_uki(context, kver, kimg, token, partitions)
+        if not want_uki(context) or want_grub_bios(context, partitions):
+            install_type1(context, kver, kimg, token, partitions)
 
         if context.config.bootloader == Bootloader.uki:
             break
@@ -2098,7 +2159,7 @@ def make_uki(context: Context, stub: Path, kver: str, kimg: Path, output: Path)
     initrds = [microcode] if microcode else []
     initrds += [context.workspace / "initrd"]
 
-    build_uki(context, stub, kver, kimg, initrds, [], output)
+    build_uki(context, stub, kver, kimg, initrds, context.config.kernel_command_line, output)
     extract_pe_section(context, output, ".linux", context.staging / context.config.output_split_kernel)
     extract_pe_section(context, output, ".initrd", context.staging / context.config.output_split_initrd)
 
@@ -2142,7 +2203,7 @@ def copy_uki(context: Context) -> None:
     if (context.staging / context.config.output_split_uki).exists():
         return
 
-    if not want_efi(context.config):
+    if not want_efi(context.config) or context.config.unified_kernel_images == ConfigFeature.disabled:
         return
 
     ukis = sorted(
@@ -2412,7 +2473,7 @@ def check_systemd_tool(
 
 def check_tools(config: Config, verb: Verb) -> None:
     if verb == Verb.build:
-        if want_efi(config):
+        if want_efi(config) and config.unified_kernel_images == ConfigFeature.enabled:
             check_systemd_tool(
                 config,
                 "ukify", "/usr/lib/systemd/ukify",
@@ -2821,10 +2882,10 @@ def save_uki_components(context: Context) -> tuple[Optional[Path], Optional[str]
 
     kimg = shutil.copy2(context.root / kimg, context.workspace)
 
-    if not (arch := context.config.architecture.to_efi()):
+    if not context.config.architecture.to_efi():
         die(f"Architecture {context.config.architecture} does not support UEFI")
 
-    stub = context.root / f"usr/lib/systemd/boot/efi/linux{arch}.efi.stub"
+    stub = systemd_stub_binary(context)
     if not stub.exists():
         die(f"sd-stub not found at /{stub.relative_to(context.root)} in the image")
 
@@ -3264,13 +3325,12 @@ def build_image(context: Context) -> None:
 
     normalize_mtime(context.root, context.config.source_date_epoch)
     partitions = make_disk(context, skip=("esp", "xbootldr"), tabs=True, msg="Generating disk image")
-    install_uki(context, partitions)
-    prepare_grub_efi(context)
-    prepare_grub_bios(context, partitions)
+    install_kernel(context, partitions)
+    grub_bios_install(context, partitions)
     normalize_mtime(context.root, context.config.source_date_epoch, directory=Path("boot"))
     normalize_mtime(context.root, context.config.source_date_epoch, directory=Path("efi"))
     partitions = make_disk(context, msg="Formatting ESP/XBOOTLDR partitions")
-    install_grub_bios(context, partitions)
+    grub_bios_setup(context, partitions)
 
     if context.config.split_artifacts:
         make_disk(context, split=True, msg="Extracting partitions")
index 0883ea3c5bc7434ead6efb5b0377dadd55516b3e..4c3743ec1bc32b2479d96d10bae6895cacb2a9a3 100644 (file)
@@ -1298,6 +1298,7 @@ class Config:
     bootloader: Bootloader
     bios_bootloader: BiosBootloader
     shim_bootloader: ShimBootloader
+    unified_kernel_images: ConfigFeature
     initrds: list[Path]
     initrd_packages: list[str]
     microcode_host: bool
@@ -2160,6 +2161,13 @@ SETTINGS = (
         default=ShimBootloader.none,
         help="Specify whether to use shim",
     ),
+    ConfigSetting(
+        dest="unified_kernel_images",
+        metavar="FEATURE",
+        section="Content",
+        parse=config_parse_feature,
+        help="Specify whether to use UKIs with grub/systemd-boot in UEFI mode",
+    ),
     ConfigSetting(
         dest="initrds",
         long="--initrd",
index a550791e50fc27ad2d0dfddbc0731c18b32bc369..67a119438d1067e9e5b1bd9db535ac42ebd5473c 100644 (file)
@@ -1190,6 +1190,17 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
   on UEFI firmware is requested using other options
   (`Bootable=`, `Bootloader=`).
 
+`UnifiedKernelImages=`, `--unified-kernel-images=`
+
+: Specifies whether to use unified kernel images or not when
+  `Bootloader=` is set to `systemd-boot` or `grub`. Takes a boolean
+  value or `auto`. Defaults to `auto`. If enabled, unified kernel images
+  are always used and the build will fail if any components required to
+  build unified kernel images are missing. If set to `auto`, unified
+  kernel images will be used if all necessary components are available.
+  Otherwise Type 1 entries as defined by the Boot Loader Specification
+  will be used instead. If disabled, Type 1 entries will always be used.
+
 `Initrds=`, `--initrd`
 
 : Use user-provided initrd(s). Takes a comma separated list of paths to
index b1939f5f31d08f4cc3ea5b4df7feea14d24f2b53..1c0281928e879148f82bfa710bcae739e236ed6c 100644 (file)
@@ -303,6 +303,7 @@ def test_config() -> None:
             "ToolsTreeRepositories": [
                 "abc"
             ],
+            "UnifiedKernelImages": "auto",
             "UseSubvolumes": "auto",
             "VerityCertificate": "/path/to/cert",
             "VerityKey": null,
@@ -442,6 +443,7 @@ def test_config() -> None:
         tools_tree_packages = [],
         tools_tree_release = None,
         tools_tree_repositories = ["abc"],
+        unified_kernel_images = ConfigFeature.auto,
         use_subvolumes = ConfigFeature.auto,
         verity_certificate = Path("/path/to/cert"),
         verity_key = None,