From 61a60c9cc6d2c2c12b93e33b55286621409b8e71 Mon Sep 17 00:00:00 2001 From: Luca Boccassi <luca.boccassi@gmail.com> Date: Fri, 6 Dec 2024 00:28:13 +0000 Subject: [PATCH] Add mkosi-addon and kernel-install plugin Add new mkosi-addon and kernel-install plugin to build local customizations into an EFI addon. This allows us to move closer to the desired goal of having universal UKIs, built by vendors, used together with locally built enhancements. --- .gitignore | 1 + README.md | 15 +-- bin/mkosi-addon | 1 + kernel-install/51-mkosi-addon.install | 111 ++++++++++++++++++ mkosi-addon | 1 + mkosi/__init__.py | 97 ++++++++++++---- mkosi/addon.py | 151 +++++++++++++++++++++++++ mkosi/config.py | 14 ++- mkosi/initrd.py | 22 +--- mkosi/resources/man/mkosi-addon.1.md | 49 ++++++++ mkosi/resources/man/mkosi.1.md | 4 +- mkosi/resources/man/mkosi.news.7.md | 2 +- mkosi/resources/mkosi-addon/mkosi.conf | 19 ++++ mkosi/util.py | 26 +++++ pyproject.toml | 2 + 15 files changed, 456 insertions(+), 59 deletions(-) create mode 120000 bin/mkosi-addon create mode 100755 kernel-install/51-mkosi-addon.install create mode 120000 mkosi-addon create mode 100644 mkosi/addon.py create mode 100644 mkosi/resources/man/mkosi-addon.1.md create mode 100644 mkosi/resources/mkosi-addon/mkosi.conf diff --git a/.gitignore b/.gitignore index b6e54df8a..eef237602 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.cache-pre-inst .cache .mkosi.1 +.mkosi-addon.1 .mkosi-initrd.1 .mkosi-sandbox.1 .mypy_cache/ diff --git a/README.md b/README.md index 3ff31bf3d..82019db2a 100644 --- a/README.md +++ b/README.md @@ -81,15 +81,16 @@ mkosi binary and is not to be considered a public API. ## kernel-install plugin -mkosi can also be used as a kernel-install plugin to build initrds. To -enable this feature, install `kernel-install/50-mkosi.install` -into `/usr/lib/kernel/install.d`. Extra distro configuration for the -initrd can be configured in `/usr/lib/mkosi-initrd`. Users can add their -own customizations in `/etc/mkosi-initrd`. +mkosi can also be used as a kernel-install plugin to build initrds and addons. +To enable these features, install `kernel-install/50-mkosi.install` and +`kernel-install/51-mkosi-addon.install` into `/usr/lib/kernel/install.d`. +Extra distro configuration for the initrd can be configured in +`/usr/lib/mkosi-initrd`. Users can add their own customizations in +`/etc/mkosi-initrd` and `/etc/mkosi-addon`. Once installed, the mkosi plugin can be enabled by writing -`initrd_generator=mkosi-initrd` to `/usr/lib/kernel/install.conf` or to -`/etc/kernel/install.conf`. +`initrd_generator=mkosi-initrd` and `layout=uki` to `/usr/lib/kernel/install.conf` +or to `/etc/kernel/install.conf`. # Hacking on mkosi diff --git a/bin/mkosi-addon b/bin/mkosi-addon new file mode 120000 index 000000000..b5f44fa8e --- /dev/null +++ b/bin/mkosi-addon @@ -0,0 +1 @@ +mkosi \ No newline at end of file diff --git a/kernel-install/51-mkosi-addon.install b/kernel-install/51-mkosi-addon.install new file mode 100755 index 000000000..8a2036a5c --- /dev/null +++ b/kernel-install/51-mkosi-addon.install @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1-or-later + +import argparse +import dataclasses +import logging +import os +import sys +import tempfile +from pathlib import Path +from typing import Optional + +from mkosi import identify_cpu +from mkosi.archive import make_cpio +from mkosi.config import OutputFormat +from mkosi.log import die, log_setup +from mkosi.run import run, uncaught_exception_handler +from mkosi.sandbox import __version__, umask +from mkosi.types import PathString + + +@dataclasses.dataclass(frozen=True) +class Context: + command: str + kernel_version: str + entry_dir: Path + kernel_image: Path + staging_area: Path + layout: str + image_type: str + verbose: bool + + +def we_are_wanted(context: Context) -> bool: + return context.layout == "uki" + + +def mandatory_variable(name: str) -> str: + try: + return os.environ[name] + except KeyError: + die(f"${name} must be set in the environment") + + +@uncaught_exception_handler() +def main() -> None: + log_setup() + + parser = argparse.ArgumentParser( + description="kernel-install plugin to build local addon for initrd/cmdline/ucode", + allow_abbrev=False, + usage="51-mkosi-addon.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE…", + ) + + parser.add_argument( + "command", + metavar="COMMAND", + help="The action to perform. Only 'add' is supported.", + ) + parser.add_argument( + "kernel_version", + metavar="KERNEL_VERSION", + help="Kernel version string", + ) + parser.add_argument( + "entry_dir", + metavar="ENTRY_DIR", + type=Path, + nargs="?", + help="Type#1 entry directory (ignored)", + ) + parser.add_argument( + "kernel_image", + metavar="KERNEL_IMAGE", + type=Path, + nargs="?", + help="Kernel image", + ) + parser.add_argument( + "--version", + action="version", + version=f"mkosi {__version__}", + ) + + context = Context( + **vars(parser.parse_args()), + staging_area=Path(mandatory_variable("KERNEL_INSTALL_STAGING_AREA")), + layout=mandatory_variable("KERNEL_INSTALL_LAYOUT"), + image_type=mandatory_variable("KERNEL_INSTALL_IMAGE_TYPE"), + verbose=int(os.getenv("KERNEL_INSTALL_VERBOSE", 0)) > 0, + ) + + if context.command != "add" or not context.layout == "uki": + return + + cmdline: list[PathString] = [ + "mkosi-addon", + "--output", "mkosi-local.addon.efi", + "--output-dir", context.staging_area / "uki.efi.extra.d", + ] # fmt: skip + + if context.verbose: + cmdline += ["--debug"] + + logging.info(f"Building mkosi-local.addon.efi") + + run(cmdline, stdin=sys.stdin, stdout=sys.stdout) + + +if __name__ == "__main__": + main() diff --git a/mkosi-addon b/mkosi-addon new file mode 120000 index 000000000..c8442ca02 --- /dev/null +++ b/mkosi-addon @@ -0,0 +1 @@ +mkosi/resources/mkosi-addon \ No newline at end of file diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 565d0d009..1edfd29d1 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -192,7 +192,7 @@ def remove_files(context: Context) -> None: def install_distribution(context: Context) -> None: - if context.config.base_trees: + if context.config.base_trees or context.config.output_format == OutputFormat.addon: if not context.config.packages: return @@ -283,7 +283,10 @@ def remove_packages(context: Context) -> None: def check_root_populated(context: Context) -> None: - if context.config.output_format.is_extension_image(): + if ( + context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return """Check that the root was populated by looking for a os-release file.""" @@ -303,7 +306,11 @@ def configure_os_release(context: Context) -> None: if not (context.config.image_id or context.config.image_version or context.config.hostname): return - if context.config.overlay or context.config.output_format.is_extension_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return for candidate in ["usr/lib/os-release", "usr/lib/initrd-release", "etc/os-release"]: @@ -2084,7 +2091,7 @@ def install_kernel(context: Context, partitions: Sequence[Partition]) -> None: # 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): + if context.config.output_format in (OutputFormat.uki, OutputFormat.esp, OutputFormat.addon): return if context.config.bootable == ConfigFeature.disabled: @@ -2157,7 +2164,7 @@ def make_uki( extract_pe_section(context, output, ".initrd", context.staging / context.config.output_split_initrd) -def make_initrd_addon(context: Context, stub: Path, output: Path) -> None: +def make_initrd_addon(context: Context, stub: Path, microcodes: list[Path], output: Path) -> None: make_cpio(context.root, context.workspace / "initrd", sandbox=context.sandbox) maybe_compress( context, @@ -2170,11 +2177,25 @@ def make_initrd_addon(context: Context, stub: Path, output: Path) -> None: "--ro-bind", context.workspace / "initrd", workdir(context.workspace / "initrd") ] # fmt: skip + if microcodes: + # new .ucode section support? + check_ukify( + context.config, + version="256~devel", + reason="build addon with .ucode section support", + hint=("Use ToolsTree=default to download most required tools including ukify automatically"), + ) + + for microcode in microcodes: + arguments += ["--microcode", workdir(microcode)] + options += ["--ro-bind", microcode, workdir(microcode)] + with complete_step(f"Generating initrd PE addon {output}"): run_ukify( context, stub, output, + cmdline=context.config.kernel_command_line, arguments=arguments, options=options, ) @@ -2613,7 +2634,7 @@ def check_tools(config: Config, verb: Verb) -> None: check_tool(config, "depmod", reason="generate kernel module dependencies") if want_efi(config): - if config.unified_kernel_image_profiles: + if config.unified_kernel_image_profiles or config.output_format == OutputFormat.addon: check_ukify( config, version="257~devel", @@ -2748,7 +2769,11 @@ def configure_ssh(context: Context) -> None: def configure_initrd(context: Context) -> None: - if context.config.overlay or context.config.output_format.is_extension_or_portable_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_or_portable_image() + or context.config.output_format == OutputFormat.addon + ): return if ( @@ -2769,7 +2794,11 @@ def configure_initrd(context: Context) -> None: def configure_clock(context: Context) -> None: - if context.config.overlay or context.config.output_format.is_extension_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return with umask(~0o644): @@ -2816,7 +2845,11 @@ def run_depmod(context: Context, *, cache: bool = False) -> None: def run_sysusers(context: Context) -> None: - if context.config.overlay or context.config.output_format.is_extension_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return if not context.config.find_binary("systemd-sysusers"): @@ -2831,7 +2864,11 @@ def run_sysusers(context: Context) -> None: def run_tmpfiles(context: Context) -> None: - if context.config.overlay or context.config.output_format.is_extension_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return if not context.config.find_binary("systemd-tmpfiles"): @@ -2872,7 +2909,11 @@ def run_tmpfiles(context: Context) -> None: def run_preset(context: Context) -> None: - if context.config.overlay or context.config.output_format.is_extension_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return if not context.config.find_binary("systemctl"): @@ -2891,7 +2932,11 @@ def run_preset(context: Context) -> None: def run_hwdb(context: Context) -> None: - if context.config.overlay or context.config.output_format.is_extension_image(): + if ( + context.config.overlay + or context.config.output_format.is_extension_image() + or context.config.output_format == OutputFormat.addon + ): return if not context.config.find_binary("systemd-hwdb"): @@ -3083,15 +3128,20 @@ def reuse_cache(context: Context) -> bool: def save_esp_components( context: Context, ) -> tuple[Optional[Path], Optional[str], Optional[Path], list[Path]]: - if context.config.output_format == OutputFormat.initrd_addon: + if not context.config.architecture.to_efi(): + die(f"Architecture {context.config.architecture} does not support UEFI") + + if context.config.output_format not in (OutputFormat.uki, OutputFormat.esp, OutputFormat.addon): + return None, None, None, [] + + microcode = build_microcode_initrd(context) + + if context.config.output_format == OutputFormat.addon: stub = systemd_addon_stub_binary(context) if not stub.exists(): die(f"sd-stub not found at /{stub.relative_to(context.root)} in the image") - return shutil.copy2(stub, context.workspace), None, None, [] - - if context.config.output_format not in (OutputFormat.uki, OutputFormat.esp): - return None, None, None, [] + return shutil.copy2(stub, context.workspace), None, None, microcode try: kver, kimg = next(gen_kernel_images(context)) @@ -3100,15 +3150,11 @@ def save_esp_components( kimg = shutil.copy2(context.root / kimg, context.workspace) - if not context.config.architecture.to_efi(): - die(f"Architecture {context.config.architecture} does not support UEFI") - stub = systemd_stub_binary(context) if not stub.exists(): die(f"sd-stub not found at /{stub.relative_to(context.root)} in the image") stub = shutil.copy2(stub, context.workspace) - microcode = build_microcode_initrd(context) return stub, kver, kimg, microcode @@ -3761,15 +3807,15 @@ def build_image(context: Context) -> None: assert stub and kver and kimg make_uki(context, stub, kver, kimg, microcode, context.staging / context.config.output_split_uki) make_esp(context, context.staging / context.config.output_split_uki) - elif context.config.output_format == OutputFormat.initrd_addon: + elif context.config.output_format == OutputFormat.addon: assert stub - make_initrd_addon(context, stub, context.staging / context.config.output_with_format) + make_initrd_addon(context, stub, microcode, context.staging / context.config.output_with_format) elif context.config.output_format.is_extension_or_portable_image(): make_extension_or_portable_image(context, context.staging / context.config.output_with_format) elif context.config.output_format == OutputFormat.directory: context.root.rename(context.staging / context.config.output_with_format) - if context.config.output_format not in (OutputFormat.uki, OutputFormat.esp): + if context.config.output_format not in (OutputFormat.uki, OutputFormat.esp, OutputFormat.addon): maybe_compress( context, context.config.compress_output, @@ -3857,7 +3903,7 @@ def run_sandbox(args: Args, config: Config) -> None: def run_shell(args: Args, config: Config) -> None: opname = "acquire shell in" if args.verb == Verb.shell else "boot" - if config.output_format in (OutputFormat.tar, OutputFormat.cpio): + if config.output_format in (OutputFormat.tar, OutputFormat.cpio, OutputFormat.addon): die(f"Sorry, can't {opname} a {config.output_format} archive.") if config.output_format.use_outer_compression() and config.compress_output: die(f"Sorry, can't {opname} a compressed image.") @@ -4630,6 +4676,7 @@ def run_verb(args: Args, images: Sequence[Config], *, resources: Path) -> None: if args.verb == Verb.documentation: if args.cmdline: manual = { + "addon": "mkosi-addon", "initrd": "mkosi-initrd", "sandbox": "mkosi-sandbox", "news": "mkosi.news", diff --git a/mkosi/addon.py b/mkosi/addon.py new file mode 100644 index 000000000..c5c4527cf --- /dev/null +++ b/mkosi/addon.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +import argparse +import contextlib +import os +import platform +import sys +import tempfile +from pathlib import Path +from typing import cast + +import mkosi.resources +from mkosi.config import DocFormat, OutputFormat +from mkosi.documentation import show_docs +from mkosi.log import log_notice, log_setup +from mkosi.run import run, uncaught_exception_handler +from mkosi.sandbox import __version__, umask +from mkosi.tree import copy_tree +from mkosi.types import PathString +from mkosi.util import initrd_copy_host_config, resource_path + + +@uncaught_exception_handler() +def main() -> None: + log_setup() + + parser = argparse.ArgumentParser( + prog="mkosi-addon", + description="Build initrd/cmdline/ucode addon for the current system using mkosi", + allow_abbrev=False, + usage="mkosi-addon [options...]", + ) + + parser.add_argument( + "--kernel-version", + metavar="KERNEL_VERSION", + help="Kernel version string", + default=platform.uname().release, + ) + parser.add_argument( + "-o", + "--output", + metavar="NAME", + help="Output name", + default="mkosi-local.addon.efi", + ) + parser.add_argument( + "-O", + "--output-dir", + metavar="DIR", + help="Output directory", + default="", + ) + parser.add_argument( + "--debug", + help="Turn on debugging output", + action="store_true", + default=False, + ) + parser.add_argument( + "--debug-shell", + help="Spawn debug shell if a sandboxed command fails", + action="store_true", + default=False, + ) + parser.add_argument( + "-D", + "--show-documentation", + help="Show the man page", + action="store_true", + default=False, + ) + parser.add_argument( + "--version", + action="version", + version=f"mkosi {__version__}", + ) + + args = parser.parse_args() + + if args.show_documentation: + with resource_path(mkosi.resources) as r: + show_docs("mkosi-addon", DocFormat.all(), resources=r) + return + + # No local configuration? Then nothing to do + if not Path("/etc/mkosi-addon").exists() and not Path("/run/mkosi-addon").exists(): + return + + with tempfile.TemporaryDirectory() as staging_dir: + cmdline: list[PathString] = [ + "mkosi", + "--force", + "--directory", "", + f"--format={str(OutputFormat.addon)}", + "--output", args.output, + "--output-directory", staging_dir, + "--build-sources", "", + "--include=mkosi-addon", + ] # fmt: skip + + cmdline += [ + "--extra-tree=/usr/lib/systemd/boot/efi:/usr/lib/systemd/boot/efi", + "--remove-files=/usr/lib/systemd/boot/efi/", + "--sandbox-tree", + f"/usr/lib/modules/{args.kernel_version}:/usr/lib/modules/{args.kernel_version}", + "--sandbox-tree=/usr/lib/firmware:/usr/lib/firmware", + ] # fmt: skip + + if args.debug: + cmdline += ["--debug"] + if args.debug_shell: + cmdline += ["--debug-shell"] + + if os.getuid() == 0: + cmdline += [ + "--workspace-dir=/var/tmp", + "--package-cache-dir=/var", + "--cache-only=metadata", + "--output-mode=600", + ] + + for d in ( + "/usr/lib/mkosi-addon", + "/usr/local/lib/mkosi-addon", + "/run/mkosi-addon", + "/etc/mkosi-addon", + ): + if Path(d).exists(): + cmdline += ["--include", d] + + cmdline += initrd_copy_host_config(staging_dir) + + run(cmdline, stdin=sys.stdin, stdout=sys.stdout) + + if args.output_dir: + with umask(~0o700) if os.getuid() == 0 else cast(umask, contextlib.nullcontext()): + Path(args.output_dir).mkdir(parents=True, exist_ok=True) + else: + args.output_dir = Path.cwd() + + log_notice(f"Copying {staging_dir}/{args.output} to {args.output_dir}/{args.output}") + # mkosi symlinks the expected output image, so dereference it + copy_tree( + Path(f"{staging_dir}/{args.output}").resolve(), + Path(f"{args.output_dir}/{args.output}"), + ) + + +if __name__ == "__main__": + main() diff --git a/mkosi/config.py b/mkosi/config.py index ffbb78fe5..f885e50c5 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -191,7 +191,7 @@ class OutputFormat(StrEnum): tar = enum.auto() uki = enum.auto() oci = enum.auto() - initrd_addon = enum.auto() + addon = enum.auto() def extension(self) -> str: return { @@ -203,7 +203,7 @@ def extension(self) -> str: OutputFormat.sysext: ".raw", OutputFormat.tar: ".tar", OutputFormat.uki: ".efi", - OutputFormat.initrd_addon: ".efi", + OutputFormat.addon: ".efi", }.get(self, "") # fmt: skip def use_outer_compression(self) -> bool: @@ -217,7 +217,7 @@ def use_outer_compression(self) -> bool: ) def is_extension_image(self) -> bool: - return self in (OutputFormat.sysext, OutputFormat.confext, OutputFormat.initrd_addon) + return self in (OutputFormat.sysext, OutputFormat.confext, OutputFormat.addon) def is_extension_or_portable_image(self) -> bool: return self.is_extension_image() or self == OutputFormat.portable @@ -843,7 +843,13 @@ def config_parse_mode(value: Optional[str], old: Optional[int]) -> Optional[int] def config_default_compression(namespace: argparse.Namespace) -> Compression: - if namespace.output_format in (OutputFormat.tar, OutputFormat.cpio, OutputFormat.uki, OutputFormat.esp): + if namespace.output_format in ( + OutputFormat.tar, + OutputFormat.cpio, + OutputFormat.uki, + OutputFormat.esp, + OutputFormat.addon, + ): return Compression.zstd elif namespace.output_format == OutputFormat.oci: return Compression.gz diff --git a/mkosi/initrd.py b/mkosi/initrd.py index 6673605a0..189bf4b05 100644 --- a/mkosi/initrd.py +++ b/mkosi/initrd.py @@ -18,7 +18,7 @@ from mkosi.sandbox import __version__, umask from mkosi.tree import copy_tree from mkosi.types import PathString -from mkosi.util import resource_path +from mkosi.util import initrd_copy_host_config, resource_path @uncaught_exception_handler() @@ -180,25 +180,7 @@ def main() -> None: cmdline += ["--sandbox-tree", sandbox_tree] - # Generate crypttab with all the x-initrd.attach entries - if Path("/etc/crypttab").exists(): - crypttab = [ - line - for line in Path("/etc/crypttab").read_text().splitlines() - if ( - len(entry := line.split()) >= 4 - and not entry[0].startswith("#") - and "x-initrd.attach" in entry[3] - ) - ] - if crypttab: - with (Path(staging_dir) / "crypttab").open("w") as f: - f.write("# Automatically generated by mkosi-initrd\n") - f.write("\n".join(crypttab)) - cmdline += ["--extra-tree", f"{staging_dir}/crypttab:/etc/crypttab"] - - if Path("/etc/kernel/cmdline").exists(): - cmdline += ["--kernel-command-line", Path("/etc/kernel/cmdline").read_text()] + cmdline += initrd_copy_host_config(staging_dir) # Resolve dnf binary to determine which version the host uses by default # (to avoid preferring dnf5 if the host uses dnf4) diff --git a/mkosi/resources/man/mkosi-addon.1.md b/mkosi/resources/man/mkosi-addon.1.md new file mode 100644 index 000000000..891ab5b73 --- /dev/null +++ b/mkosi/resources/man/mkosi-addon.1.md @@ -0,0 +1,49 @@ +% mkosi-addon(1) +% +% + +# NAME + +mkosi-addon — Build addons for unified kernel images for the current system +using mkosi + +# SYNOPSIS + +`mkosi-addon [options…]` + +# DESCRIPTION + +`mkosi-addon` is wrapper on top of `mkosi` to simplify the generation of +addons containing customizations for a Unified Kernel Images specific for the +current running system. Will include entries in `/etc/crypttab` marked with +`x-initrd.attach`, `/etc/kernel/cmdline`, kernel modules, firmwares and microcode +for the running hardware. + +# OPTIONS + +`--kernel-version=` +: Kernel version where to look for the kernel modules to include. Defaults to + the kernel version of the running system (`uname -r`). + +`--output=`, `-o` +: Name to use for the generated output addon. Defaults to + `mkosi-local.addon.efi`. + +`--output-dir=`, `-O` +: Path to a directory where to place all generated artifacts. Defaults to the + current working directory. + +`--debug=` +: Enable additional debugging output. + +`--debug-shell=` +: Spawn debug shell in sandbox if a sandboxed command fails. + +`--version` +: Show package version. + +`--help`, `-h` +: Show brief usage information. + +# SEE ALSO +`mkosi(1)` diff --git a/mkosi/resources/man/mkosi.1.md b/mkosi/resources/man/mkosi.1.md index 3db114147..8f0b76a0f 100644 --- a/mkosi/resources/man/mkosi.1.md +++ b/mkosi/resources/man/mkosi.1.md @@ -519,7 +519,7 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, the `.initrd` PE section), `esp` (`uki` but wrapped in a disk image with only an ESP partition), `oci` (a directory compatible with the OCI image specification), `sysext`, `confext`, `portable`, - `initrd-addon` or `none` (the OS image is solely intended as a build + `addon` or `none` (the OS image is solely intended as a build image to produce another artifact). If the `disk` output format is used, the disk image is generated using @@ -565,7 +565,7 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`, compression means the image cannot be started directly but needs to be decompressed first. This also means that the `shell`, `boot`, `vm` verbs are not available when this option is used. Implied for `tar`, `cpio`, `uki`, - `esp`, and `oci`. + `esp`, `oci`, and `addon`. `CompressLevel=`, `--compress-level=` : Configure the compression level to use. Takes an integer. The possible diff --git a/mkosi/resources/man/mkosi.news.7.md b/mkosi/resources/man/mkosi.news.7.md index 0dd486aaa..bbbde0119 100644 --- a/mkosi/resources/man/mkosi.news.7.md +++ b/mkosi/resources/man/mkosi.news.7.md @@ -110,7 +110,7 @@ - The `coredumpctl` and `journalctl` verbs will now always operate on the image, even if `ForwardJournal=` is configured. - Bumped default Fedora release to `41`. -- Added `initrd-addon` output format to build initrd addons. +- Added `addon` output format to build initrd addons. - Renamed `[Host]` section to `[Runtime]` section. - Renamed various settings from `[Host]`. - Binaries coming from `ExtraSearchPaths=` are now executed with the diff --git a/mkosi/resources/mkosi-addon/mkosi.conf b/mkosi/resources/mkosi-addon/mkosi.conf new file mode 100644 index 000000000..3eac2090a --- /dev/null +++ b/mkosi/resources/mkosi-addon/mkosi.conf @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +[Distribution] +Distribution=custom + +[Output] +Output=addon +Format=addon +ManifestFormat= +SplitArtifacts= + +[Content] +Bootable=no +RemoveFiles= + # Including kernel images in the initrd is generally not useful. + # This also stops mkosi from extracting the kernel image out of the image as a separate output. + /usr/lib/modules/*/vmlinuz* + /usr/lib/modules/*/vmlinux* + /usr/lib/modules/*/System.map diff --git a/mkosi/util.py b/mkosi/util.py index 248d8e0cc..ebe82f9ba 100644 --- a/mkosi/util.py +++ b/mkosi/util.py @@ -257,3 +257,29 @@ def current_home_dir() -> Optional[Path]: def unique(seq: Sequence[T]) -> list[T]: return list(dict.fromkeys(seq)) + + +def initrd_copy_host_config(staging_dir: str) -> list[str]: + cmdline = [] + + # Generate crypttab with all the x-initrd.attach entries + if Path("/etc/crypttab").exists(): + crypttab = [ + line + for line in Path("/etc/crypttab").read_text().splitlines() + if ( + len(entry := line.split()) >= 4 + and not entry[0].startswith("#") + and "x-initrd.attach" in entry[3] + ) + ] + if crypttab: + with (Path(staging_dir) / "crypttab").open("w") as f: + f.write("# Automatically generated by mkosi-initrd\n") + f.write("\n".join(crypttab)) + cmdline += ["--extra-tree", f"{staging_dir}/crypttab:/etc/crypttab"] + + if Path("/etc/kernel/cmdline").exists(): + cmdline += ["--kernel-command-line", Path("/etc/kernel/cmdline").read_text()] + + return cmdline diff --git a/pyproject.toml b/pyproject.toml index 7426a7a78..0011fb89f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ bootable = [ mkosi = "mkosi.__main__:main" mkosi-initrd = "mkosi.initrd:main" mkosi-sandbox = "mkosi.sandbox:main" +mkosi-addon = "mkosi.addon:main" [tool.setuptools] packages = [ @@ -35,6 +36,7 @@ packages = [ "mkosi.resources" = [ "completion.*", "man/*", + "mkosi-addon/**/*", "mkosi-initrd/**/*", "mkosi-tools/**/*", "mkosi-vm/**/*",