]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add `UnifiedKernelImageFormat=` with specifiers 2733/head
authorMichael Ferrari <nekkodroid404@gmail.com>
Tue, 4 Jun 2024 11:26:00 +0000 (13:26 +0200)
committerMichael Ferrari <nekkodroid404@gmail.com>
Tue, 4 Jun 2024 12:41:30 +0000 (14:41 +0200)
This can be used to control the name to use for the UKI during image
generation. Special `&` specifiers can be used to include kernel
specific information in the filename.

This is useful for the `systemd-sysupdate` case, as you can set this to
`%i_%v` to use a format that can be parse by its configuration. The
current format used includes both a roothash as well as the kernel
version which both can't be matched by sysupdate.

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

index d75b7d97c7e9cb364d9b37254307f698246d15a1..fd2916d34b734b368180204059976b42f7e417d5 100644 (file)
@@ -2299,14 +2299,50 @@ def install_type1(
             f.write("fi\n")
 
 
+def expand_kernel_specifiers(text: str, kver: str, token: str, roothash: str, boot_count: str) -> str:
+    specifiers = {
+        "&": "&",
+        "e": token,
+        "k": kver,
+        "h": roothash,
+        "c": boot_count
+    }
+
+    def replacer(match: re.Match[str]) -> str:
+        m = match.group("specifier")
+        if specifier := specifiers.get(m):
+            return specifier
+
+        logging.warning(f"Unknown specifier '&{m}' found in {text}, ignoring")
+        return ""
+
+    return re.sub(r"&(?P<specifier>[&a-zA-Z])", replacer, text)
+
+
 def install_uki(context: Context, kver: str, kimg: Path, token: str, partitions: Sequence[Partition]) -> None:
+    bootloader_entry_format = context.config.unified_kernel_image_format or "&e-&k"
+
     roothash_value = ""
     if roothash := finalize_roothash(partitions):
-        roothash_value = f"-{roothash.partition("=")[2]}"
+        roothash_value = roothash.partition("=")[2]
+
+        if not context.config.unified_kernel_image_format:
+            bootloader_entry_format += "-&h"
 
     boot_count = ""
     if (context.root / "etc/kernel/tries").exists():
-        boot_count = f'+{(context.root / "etc/kernel/tries").read_text().strip()}'
+        boot_count = (context.root / "etc/kernel/tries").read_text().strip()
+
+        if not context.config.unified_kernel_image_format:
+            bootloader_entry_format += "+&c"
+
+    bootloader_entry = expand_kernel_specifiers(
+        bootloader_entry_format,
+        kver=kver,
+        token=token,
+        roothash=roothash_value,
+        boot_count=boot_count,
+    )
 
     if context.config.bootloader == Bootloader.uki:
         if context.config.shim_bootloader != ShimBootloader.none:
@@ -2314,7 +2350,7 @@ def install_uki(context: Context, kver: str, kimg: Path, token: str, partitions:
         else:
             boot_binary = context.root / efi_boot_binary(context)
     else:
-        boot_binary = context.root / f"boot/EFI/Linux/{token}-{kver}{roothash_value}{boot_count}.efi"
+        boot_binary = context.root / f"boot/EFI/Linux/{bootloader_entry}.efi"
 
     # Make sure the parent directory where we'll be writing the UKI exists.
     with umask(~0o700):
index cf0fa95e5fcb7d02f7ef8b1507481ea96ba55b98..995d52db68e7c438174bf77efab77735c53293bd 100644 (file)
@@ -950,16 +950,17 @@ def is_valid_filename(s: str) -> bool:
     return not (s == "." or s == ".." or "/" in s)
 
 
-def config_parse_output(value: Optional[str], old: Optional[str]) -> Optional[str]:
-    if not value:
-        return None
+def config_make_filename_parser(hint: str) -> ConfigParseCallback:
+    def config_parse_filename(value: Optional[str], old: Optional[str]) -> Optional[str]:
+        if not value:
+            return None
 
-    if not is_valid_filename(value):
-        die(f"{value!r} is not a valid filename.",
-            hint="Output= or --output= requires a filename with no path components. "
-                 "Use OutputDirectory= or --output-dir= to configure the output directory.")
+        if not is_valid_filename(value):
+            die(f"{value!r} is not a valid filename.", hint=hint)
 
-    return value
+        return value
+
+    return config_parse_filename
 
 
 def match_path_exists(value: str) -> bool:
@@ -1431,6 +1432,7 @@ class Config:
     bios_bootloader: BiosBootloader
     shim_bootloader: ShimBootloader
     unified_kernel_images: ConfigFeature
+    unified_kernel_image_format: str
     initrds: list[Path]
     initrd_packages: list[str]
     initrd_volatile_packages: list[str]
@@ -1971,7 +1973,10 @@ SETTINGS = (
         metavar="NAME",
         section="Output",
         specifier="o",
-        parse=config_parse_output,
+        parse=config_make_filename_parser(
+            "Output= or --output= requires a filename with no path components. "
+            "Use OutputDirectory= or --output-dir= to configure the output directory."
+        ),
         default_factory=config_default_output,
         default_factory_depends=("image_id", "image_version"),
         help="Output name",
@@ -2381,6 +2386,19 @@ SETTINGS = (
         parse=config_parse_feature,
         help="Specify whether to use UKIs with grub/systemd-boot in UEFI mode",
     ),
+    ConfigSetting(
+        dest="unified_kernel_image_format",
+        section="Content",
+        parse=config_make_filename_parser(
+            "UnifiedKernelImageFormat= or --unified-kernel-image-format= "
+            "requires a filename with no path components."
+        ),
+        # The default value is set in `__init__.py` in `install_uki`.
+        # `None` is used to determin if the roothash and boot count format
+        # should be appended to the filename if they are found.
+        #default=
+        help="Specify the format used for the UKI filename",
+    ),
     ConfigSetting(
         dest="initrds",
         long="--initrd",
@@ -3986,6 +4004,7 @@ def summary(config: Config) -> str:
                     BIOS Bootloader: {config.bios_bootloader}
                     Shim Bootloader: {config.shim_bootloader}
               Unified Kernel Images: {config.unified_kernel_images}
+        Unified Kernel Image Format: {config.unified_kernel_image_format}
                             Initrds: {line_join_list(config.initrds)}
                     Initrd Packages: {line_join_list(config.initrd_packages)}
            Initrd Volatile Packages: {line_join_list(config.initrd_volatile_packages)}
index 321020599b99a0bd8ec763ef497ae577327ff615..e85b1df4352b806b05275bb88c1bec5ce07a6549 100644 (file)
@@ -997,6 +997,25 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
     Otherwise Type 1 entries as defined by the Boot Loader Specification
     will be used instead. If disabled, Type 1 entries will always be used.
 
+`UnifiedKernelImageFormat=`, `--unified-kernel-image-format=`
+:   Takes a filename without any path components to specify the format that
+    unified kernel images should be installed as. This may include both the
+    regular specifiers (see **Specifiers**) and special delayed specifiers, that
+    are expanded during the installation of the files, which are described below.
+    The default format for this parameter is `&e-&k` with `-&h` being appended
+    if `roothash=` or `usrhash=` is found on the kernel command line and `+&c`
+    if `/etc/kernel/tries` is found in the image.
+
+    The following specifiers may be used:
+
+    | Specifier | Value                                              |
+    |-----------|----------------------------------------------------|
+    | `&&`      | `&` character                                      |
+    | `&e`      | Entry Token                                        |
+    | `&k`      | Kernel version                                     |
+    | `&h`      | `roothash=` or `usrhash=` value of kernel argument |
+    | `&c`      | Number of tries used for boot attempt counting     |
+
 `Initrds=`, `--initrd`
 :   Use user-provided initrd(s). Takes a comma separated list of paths to initrd
     files. This option may be used multiple times in which case the initrd lists
index e7cea38795e8b65d5d97a7d39c012feb7ee47bff..dc9332179ae4c418370184ac2dd65d66e41edc91 100644 (file)
@@ -10,6 +10,7 @@ from typing import Optional
 
 import pytest
 
+from mkosi import expand_kernel_specifiers
 from mkosi.config import (
     Architecture,
     Compression,
@@ -1012,6 +1013,31 @@ def test_specifiers(tmp_path: Path) -> None:
         assert {k: v for k, v in config.environment.items() if k in expected} == expected
 
 
+def test_kernel_specifiers(tmp_path: Path) -> None:
+    kver = "13.0.8-5.10.0-1057-oem"     # taken from reporter of #1638
+    token = "MySystemImage"
+    roothash = "67e893261799236dcf20529115ba9fae4fd7c2269e1e658d42269503e5760d38"
+    boot_count = "3"
+
+    def test_expand_kernel_specifiers(text: str) -> str:
+        return expand_kernel_specifiers(
+            text,
+            kver=kver,
+            token=token,
+            roothash=roothash,
+            boot_count=boot_count,
+        )
+
+    assert test_expand_kernel_specifiers("&&") == "&"
+    assert test_expand_kernel_specifiers("&k") == kver
+    assert test_expand_kernel_specifiers("&e") == token
+    assert test_expand_kernel_specifiers("&h") == roothash
+    assert test_expand_kernel_specifiers("&c") == boot_count
+
+    assert test_expand_kernel_specifiers("Image_1.0.3") == "Image_1.0.3"
+    assert test_expand_kernel_specifiers("Image~&c+&h-&k-&e") == f"Image~{boot_count}+{roothash}-{kver}-{token}"
+
+
 def test_output_id_version(tmp_path: Path) -> None:
     d = tmp_path
 
index 88ce42b332b75c4166bbe06c80f3c1e842fb1fdc..051a21ca783bc5091d93df807f8732fedba93682 100644 (file)
@@ -332,6 +332,7 @@ def test_config() -> None:
             "ToolsTreeRepositories": [
                 "abc"
             ],
+            "UnifiedKernelImageFormat": "myuki",
             "UnifiedKernelImages": "auto",
             "UnitProperties": [
                 "PROPERTY=VALUE"
@@ -495,6 +496,7 @@ def test_config() -> None:
         tools_tree_packages=[],
         tools_tree_release=None,
         tools_tree_repositories=["abc"],
+        unified_kernel_image_format="myuki",
         unified_kernel_images=ConfigFeature.auto,
         unit_properties=["PROPERTY=VALUE"],
         use_subvolumes=ConfigFeature.auto,