Skip to content

Commit

Permalink
Add mkosi-addon and kernel-install plugin
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bluca committed Jan 17, 2025
1 parent 3f7ccb6 commit 61a60c9
Show file tree
Hide file tree
Showing 15 changed files with 456 additions and 59 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.cache-pre-inst
.cache
.mkosi.1
.mkosi-addon.1
.mkosi-initrd.1
.mkosi-sandbox.1
.mypy_cache/
Expand Down
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions bin/mkosi-addon
111 changes: 111 additions & 0 deletions kernel-install/51-mkosi-addon.install
Original file line number Diff line number Diff line change
@@ -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()
1 change: 1 addition & 0 deletions mkosi-addon
97 changes: 72 additions & 25 deletions mkosi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."""
Expand All @@ -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"]:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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 (
Expand All @@ -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):
Expand Down Expand Up @@ -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"):
Expand All @@ -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"):
Expand Down Expand Up @@ -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"):
Expand All @@ -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"):
Expand Down Expand Up @@ -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))
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 61a60c9

Please sign in to comment.