From 594b40ebf6afcaeab6abdea2bd64765104349716 Mon Sep 17 00:00:00 2001 From: Michael Ferrari Date: Tue, 4 Jun 2024 13:26:00 +0200 Subject: [PATCH] Add `UnifiedKernelImageFormat=` with specifiers 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 | 42 +++++++++++++++++++++++++++++++++++++--- mkosi/config.py | 37 ++++++++++++++++++++++++++--------- mkosi/resources/mkosi.md | 19 ++++++++++++++++++ tests/test_config.py | 26 +++++++++++++++++++++++++ tests/test_json.py | 2 ++ 5 files changed, 114 insertions(+), 12 deletions(-) diff --git a/mkosi/__init__.py b/mkosi/__init__.py index d75b7d97c..fd2916d34 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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[&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): diff --git a/mkosi/config.py b/mkosi/config.py index cf0fa95e5..995d52db6 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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)} diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 321020599..e85b1df43 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py index e7cea3879..dc9332179 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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 diff --git a/tests/test_json.py b/tests/test_json.py index 88ce42b33..051a21ca7 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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, -- 2.47.2