]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Define our own config for PE addons and UKI profiles
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 6 Oct 2024 13:30:57 +0000 (15:30 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 7 Oct 2024 10:30:28 +0000 (12:30 +0200)
ukify's config parser uses python's configparser module and as such
suffers from all its issues just like we used to in mkosi. Having ukify
parse the config file also means that we have to make sure any paths
configured in the profile are available in the sandbox.

Instead, let's define our own configs for the PE addons and UKI profiles
so we get to take advantage of our own config file parser and have full
knowledge of all the configured settings so we can mount extra stuff into
the sandbox if needed.

It also gets rid of the hack where we parse ukify's config file to figure
out the command line.

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

index cfb852428b0d989c91bfb9b122d4afc9a4be9445..a96943f79bf304f5715a5971ab058da21bd9de9c 100644 (file)
@@ -70,7 +70,6 @@ from mkosi.config import (
     format_bytes,
     parse_boolean,
     parse_config,
-    parse_ini,
     summary,
     systemd_tool_version,
     want_selinux_relabel,
@@ -1487,7 +1486,7 @@ def run_ukify(
     stub: Path,
     output: Path,
     *,
-    cmdline: str = "",
+    cmdline: Sequence[str] = (),
     arguments: Sequence[PathString] = (),
     options: Sequence[PathString] = (),
     sign: bool = True,
@@ -1499,11 +1498,9 @@ def run_ukify(
     if not (arch := context.config.architecture.to_efi()):
         die(f"Architecture {context.config.architecture} does not support UEFI")
 
-    cmdline = cmdline.strip()
-
     # 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"{cmdline}\x00")
+    (context.workspace / "cmdline").write_text(f"{' '.join(cmdline)}\x00")
 
     cmd = [
         python_binary(context.config, binary=ukify),
@@ -1645,7 +1642,7 @@ def build_uki(
         options += ["--ro-bind", initrd, workdir(initrd)]
 
     with complete_step(f"Generating unified kernel image for kernel version {kver}"):
-        run_ukify(context, stub, output, cmdline=" ".join(cmdline), arguments=arguments, options=options)
+        run_ukify(context, stub, output, cmdline=cmdline, arguments=arguments, options=options)
 
 
 def systemd_stub_binary(context: Context) -> Path:
@@ -1977,15 +1974,14 @@ def install_pe_addons(context: Context) -> None:
         addon_dir.mkdir(parents=True, exist_ok=True)
 
     for addon in context.config.pe_addons:
-        output = addon_dir / addon.with_suffix(".addon.efi").name
+        output = addon_dir / f"{addon.output}.addon.efi"
 
         with complete_step(f"Generating PE addon /{output.relative_to(context.root)}"):
             run_ukify(
                 context,
                 stub,
                 output,
-                arguments=["--config", workdir(addon)],
-                options=["--ro-bind", addon, workdir(addon)],
+                cmdline=addon.cmdline,
             )
 
 
@@ -2008,25 +2004,23 @@ def build_uki_profiles(context: Context, cmdline: Sequence[str]) -> list[Path]:
     profiles = []
 
     for profile in context.config.unified_kernel_image_profiles:
-        output = context.workspace / "uki-profiles" / profile.with_suffix(".efi").name
-
-        # We want to append the cmdline from the ukify config file to the base kernel command line so parse
-        # it from the ukify config file and append it to our own kernel command line.
+        id = profile.profile["ID"]
+        output = context.workspace / f"uki-profiles/{id}.efi"
 
-        profile_cmdline = ""
+        profile_section = context.workspace / f"uki-profiles/{id}.profile"
 
-        for section, k, v in parse_ini(profile):
-            if section == "UKI" and k == "Cmdline":
-                profile_cmdline = v.replace("\n", " ")
+        with profile_section.open("w") as f:
+            for k, v in profile.profile.items():
+                f.write(f"{k}={v}\n")
 
-        with complete_step(f"Generating UKI profile '{profile.stem}'"):
+        with complete_step(f"Generating UKI profile '{id}'"):
             run_ukify(
                 context,
                 stub,
                 output,
-                cmdline=f"{' '.join(cmdline)} {profile_cmdline}",
-                arguments=["--config", workdir(profile)],
-                options=["--ro-bind", profile, workdir(profile)],
+                cmdline=[*cmdline, *profile.cmdline],
+                arguments=["--profile", f"@{profile_section}"],
+                options=["--ro-bind", profile_section, profile_section],
                 sign=False,
             )
 
@@ -2410,6 +2404,20 @@ def check_inputs(config: Config) -> None:
     if config.secure_boot_key_source != config.sign_expected_pcr_key_source:
         die("Secure boot key source and expected PCR signatures key source have to be the same")
 
+    for addon in config.pe_addons:
+        if not addon.output:
+            die(
+                "PE addon configured without output filename",
+                hint="Use Output= to configure the output filename",
+            )
+
+    for profile in config.unified_kernel_image_profiles:
+        if "ID" not in profile.profile:
+            die(
+                "UKI Profile is missing ID key in its .profile section",
+                hint="Use Profile= to configure the profile ID",
+            )
+
 
 def check_tool(config: Config, *tools: PathString, reason: str, hint: Optional[str] = None) -> Path:
     tool = config.find_binary(*tools)
index c7bed5374aeace33111c2d1d800e7f474396b136..fecea6a56ddbb2b3c2770eb3e0d3d66df74352d2 100644 (file)
@@ -887,8 +887,8 @@ def config_make_enum_matcher(type: type[StrEnum]) -> ConfigMatchCallback:
 
 
 def config_make_list_parser(
-    delimiter: str,
     *,
+    delimiter: Optional[str] = None,
     parse: Callable[[str], Any] = str,
     unescape: bool = False,
     reset: bool = True,
@@ -904,13 +904,15 @@ def config_make_list_parser(
         if unescape:
             lex = shlex.shlex(value, posix=True)
             lex.whitespace_split = True
-            lex.whitespace = f"\n{delimiter}"
+            lex.whitespace = f"\n{delimiter or ''}"
             lex.commenters = ""
             values = list(lex)
             if reset and not values:
                 return None
         else:
-            values = value.replace(delimiter, "\n").split("\n")
+            if delimiter:
+                value = value.replace(delimiter, "\n")
+            values = value.split("\n")
             if reset and len(values) == 1 and values[0] == "":
                 return None
 
@@ -947,8 +949,8 @@ def config_match_version(match: str, value: str) -> bool:
 
 
 def config_make_dict_parser(
-    delimiter: str,
     *,
+    delimiter: Optional[str] = None,
     parse: Callable[[str], tuple[str, Any]],
     unescape: bool = False,
     allow_paths: bool = False,
@@ -985,13 +987,15 @@ def config_make_dict_parser(
         if unescape:
             lex = shlex.shlex(value, posix=True)
             lex.whitespace_split = True
-            lex.whitespace = f"\n{delimiter}"
+            lex.whitespace = f"\n{delimiter or ''}"
             lex.commenters = ""
             values = list(lex)
             if reset and not values:
                 return None
         else:
-            values = value.replace(delimiter, "\n").split("\n")
+            if delimiter:
+                value = value.replace(delimiter, "\n")
+            values = value.split("\n")
             if reset and len(values) == 1 and values[0] == "":
                 return None
 
@@ -1007,7 +1011,7 @@ def parse_environment(value: str) -> tuple[str, str]:
     return (key, value)
 
 
-def parse_credential(value: str) -> tuple[str, str]:
+def parse_key_value(value: str) -> tuple[str, str]:
     key, _, value = value.partition("=")
     key, value = key.strip(), value.strip()
     return (key, value)
@@ -1512,6 +1516,45 @@ PACKAGE_GLOBS = (
 )
 
 
+@dataclasses.dataclass(frozen=True)
+class PEAddon:
+    output: str
+    cmdline: list[str]
+
+
+@dataclasses.dataclass(frozen=True)
+class UKIProfile:
+    profile: dict[str, str]
+    cmdline: list[str]
+
+
+def make_simple_config_parser(settings: Sequence[ConfigSetting], type: type[Any]) -> Callable[[str], Any]:
+    lookup = {s.name: s for s in settings}
+
+    def parse_simple_config(value: str) -> Any:
+        path = parse_path(value)
+        config = argparse.Namespace()
+
+        for section, name, value in parse_ini(path, only_sections=[s.section for s in settings]):
+            if not name and not value:
+                continue
+
+            if not (s := lookup.get(name)):
+                die(f"Unknown setting {name}")
+
+            if section != s.section:
+                logging.warning(f"Setting {name} should be configured in [{s.section}], not [{section}].")
+
+            if name != s.name:
+                logging.warning(f"Setting {name} is deprecated, please use {s.name} instead.")
+
+            setattr(config, s.dest, s.parse(value, getattr(config, s.dest, None)))
+
+        return type(**{k: v for k, v in vars(config).items() if k in inspect.signature(type).parameters})
+
+    return parse_simple_config
+
+
 @dataclasses.dataclass(frozen=True)
 class Config:
     """Type-hinted storage for command line arguments.
@@ -1584,7 +1627,7 @@ class Config:
     shim_bootloader: ShimBootloader
     unified_kernel_images: ConfigFeature
     unified_kernel_image_format: str
-    unified_kernel_image_profiles: list[Path]
+    unified_kernel_image_profiles: list[UKIProfile]
     initrds: list[Path]
     initrd_packages: list[str]
     initrd_volatile_packages: list[str]
@@ -1593,7 +1636,7 @@ class Config:
     kernel_modules_include: list[str]
     kernel_modules_exclude: list[str]
     kernel_modules_include_host: bool
-    pe_addons: list[Path]
+    pe_addons: list[PEAddon]
 
     kernel_modules_initrd: bool
     kernel_modules_initrd_include: list[str]
@@ -1981,6 +2024,35 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
         yield section, "", ""
 
 
+PE_ADDON_SETTINGS = (
+    ConfigSetting(
+        dest="output",
+        section="PEAddon",
+        parse=config_make_filename_parser("Output= requires a filename with no path components."),
+        default="",
+    ),
+    ConfigSetting(
+        dest="cmdline",
+        section="PEAddon",
+        parse=config_make_list_parser(delimiter=" "),
+    ),
+)
+
+
+UKI_PROFILE_SETTINGS = (
+    ConfigSetting(
+        dest="profile",
+        section="UKIProfile",
+        parse=config_make_dict_parser(parse=parse_key_value),
+    ),
+    ConfigSetting(
+        dest="cmdline",
+        section="UKIProfile",
+        parse=config_make_list_parser(delimiter=" "),
+    ),
+)
+
+
 SETTINGS = (
     # Include section
     ConfigSetting(
@@ -2501,7 +2573,10 @@ SETTINGS = (
         long="--uki-profile",
         metavar="PATH",
         section="Content",
-        parse=config_make_list_parser(delimiter=",", parse=make_path_parser()),
+        parse=config_make_list_parser(
+            delimiter=",",
+            parse=make_simple_config_parser(UKI_PROFILE_SETTINGS, UKIProfile),
+        ),
         recursive_paths=("mkosi.uki-profiles/",),
         help="Configuration files to generate UKI profiles",
     ),
@@ -2571,7 +2646,10 @@ SETTINGS = (
         long="--pe-addon",
         metavar="PATH",
         section="Content",
-        parse=config_make_list_parser(delimiter=",", parse=make_path_parser()),
+        parse=config_make_list_parser(
+            delimiter=",",
+            parse=make_simple_config_parser(PE_ADDON_SETTINGS, PEAddon),
+        ),
         recursive_paths=("mkosi.pe-addons/",),
         help="Configuration files to generate PE addons",
     ),
@@ -3153,9 +3231,7 @@ SETTINGS = (
         long="--credential",
         metavar="NAME=VALUE",
         section="Host",
-        parse=config_make_dict_parser(
-            delimiter=" ", parse=parse_credential, allow_paths=True, unescape=True
-        ),
+        parse=config_make_dict_parser(delimiter=" ", parse=parse_key_value, allow_paths=True, unescape=True),
         help="Pass a systemd credential to systemd-nspawn or qemu",
         paths=("mkosi.credentials",),
     ),
@@ -4657,6 +4733,15 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
         assert "Type" in keysource
         return KeySource(type=KeySourceType(keysource["Type"]), source=keysource.get("Source", ""))
 
+    def pe_addon_transformer(addons: list[dict[str, Any]], fieldtype: type[PEAddon]) -> list[PEAddon]:
+        return [PEAddon(output=addon["Output"], cmdline=addon["Cmdline"]) for addon in addons]
+
+    def uki_profile_transformer(
+        profiles: list[dict[str, Any]],
+        fieldtype: type[UKIProfile],
+    ) -> list[UKIProfile]:
+        return [UKIProfile(profile=profile["Profile"], cmdline=profile["Cmdline"]) for profile in profiles]
+
     # The type of this should be
     # dict[
     #     type,
@@ -4696,6 +4781,8 @@ def json_type_transformer(refcls: Union[type[Args], type[Config]]) -> Callable[[
         Network: enum_transformer,
         KeySource: key_source_transformer,
         Vmm: enum_transformer,
+        list[PEAddon]: pe_addon_transformer,
+        list[UKIProfile]: uki_profile_transformer,
     }
 
     def json_transformer(key: str, val: Any) -> Any:
index c873c83e71a0e282dcd6c7911c198587e20ae65e..028127c2226b69589acb0c63507e13b892c73e87 100644 (file)
@@ -942,20 +942,26 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 
 `UnifiedKernelImageProfiles=`, `--uki-profile=`
 :   Build additional UKI profiles. Takes a comma separated list of paths
-    to `ukify` config files. This option may be used multiple times in
+    to UKI profile config files. This option may be used multiple times in
     which case each config gets built into a corresponding UKI profile.
     Config files in the `mkosi.uki-profiles/` directory are
     automatically picked up. All configured UKI profiles are added as
     additional UKI profiles to each UKI built by mkosi.
 
+    See the documentation for the `UKIProfile` section for information
+    on which settings can be configured in UKI profile config files.
+
 `PeAddons=`, `--pe-addon`
 :   Build additional PE addons. Takes a comma separated list of paths to
-    `ukify` config files. This option may be used multiple times in which case
+    PE addon config files. This option may be used multiple times in which case
     each config gets built into a corresponding addon. Each addon has the name
     of the config file, with the extension replaced with `.addon.efi`.
     Config files in the `mkosi.pe-addons/` directory are automatically picked
     up.
 
+    See the documentation for the `PEAddon` section for information on
+    which settings can be configured in PE addon config files.
+
 `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
@@ -1932,6 +1938,41 @@ config file is read:
     each individual subimage as if they were "universal" settings. See
     the **Building multiple images** section for more information.
 
+### [PEAddon] Section
+
+The `PEAddon` section can be used in UKI profile config files which are
+passed to the `PEAddons=` setting. The following settings can be
+specified in the `PEAddon` section:
+
+`Output=`
+:   The name the addon should have in the addons directory in the ESP.
+    The final name is the name specified here suffixed with
+    `.addon.efi`.
+
+`Cmdline=`
+:   The kernel command line arguments to store in the `.cmdline` section
+    of the addon. Takes a space delimited list of extra kernel command
+    line arguments.
+
+### [UKIProfile] Section
+
+The `UKIProfile` section can be used in UKI profile config files which
+are passed to the `UnifiedKernelImageProfiles=` setting. The following
+settings can be specified in the `UKIProfile` section:
+
+`Profile=`
+:   The contents of the `.profile` section of the UKI profile. Takes a
+    list of key/value pairs separated by `=`. The `ID=` key must be
+    specified. See the UKI [specification](https://uapi-group.org/specifications/specs/unified_kernel_image/#multi-profile-ukis)
+    for a full list of possible keys.
+
+`Cmdline=`
+:   Extra kernel command line options for the UKI profile. Takes a space
+    delimited list of extra kernel command line arguments. Note that
+    the final `.cmdline` section will the combination of the base
+    `.cmdline` section and the extra kernel command line arguments
+    specified with this setting.
+
 ## Specifiers
 
 The current value of various settings can be accessed when parsing
index 21be8a404c11f40f6730c857f9903228d14851c7..317152e338916a4963b7231949dd85bf11511b31 100644 (file)
@@ -25,11 +25,13 @@ from mkosi.config import (
     ManifestFormat,
     Network,
     OutputFormat,
+    PEAddon,
     QemuDrive,
     QemuFirmware,
     QemuVsockCID,
     SecureBootSignTool,
     ShimBootloader,
+    UKIProfile,
     Verb,
     Vmm,
 )
@@ -208,7 +210,12 @@ def test_config() -> None:
             ],
             "Passphrase": null,
             "PeAddons": [
-                "/my-addon.conf"
+                {
+                    "Cmdline": [
+                        "key=value"
+                    ],
+                    "Output": "abc"
+                }
             ],
             "PostInstallationScripts": [
                 "/bar/qux"
@@ -351,7 +358,14 @@ def test_config() -> None:
             ],
             "UnifiedKernelImageFormat": "myuki",
             "UnifiedKernelImageProfiles": [
-                "/profile"
+                {
+                    "Cmdline": [
+                        "key=value"
+                    ],
+                    "Profile": {
+                        "key": "value"
+                    }
+                }
             ],
             "UnifiedKernelImages": "auto",
             "UnitProperties": [
@@ -454,7 +468,7 @@ def test_config() -> None:
         packages=[],
         pass_environment=["abc"],
         passphrase=None,
-        pe_addons=[Path("/my-addon.conf")],
+        pe_addons=[PEAddon(output="abc", cmdline=["key=value"])],
         postinst_scripts=[Path("/bar/qux")],
         postoutput_scripts=[Path("/foo/src")],
         prepare_scripts=[Path("/run/foo")],
@@ -532,7 +546,7 @@ def test_config() -> None:
         tools_tree_release=None,
         tools_tree_repositories=["abc"],
         unified_kernel_image_format="myuki",
-        unified_kernel_image_profiles=[Path("/profile")],
+        unified_kernel_image_profiles=[UKIProfile(profile={"key": "value"}, cmdline=["key=value"])],
         unified_kernel_images=ConfigFeature.auto,
         unit_properties=["PROPERTY=VALUE"],
         use_subvolumes=ConfigFeature.auto,