]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add support for qemu's direct Linux boot
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 7 Mar 2022 16:10:41 +0000 (16:10 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 21 Jun 2022 00:48:01 +0000 (02:48 +0200)
We add a "linux" boot protocol that, when enabled, instructs mkosi
to extract the kernel image, initrd and kernel cmdline out of the
image when building it.

On top, we add a --qemu-boot option that takes one of the possible
values from --boot-protocols and instructs qemu to use that boot
protocol. In case of the new "linux" boot protocol, we use qemu's
-kernel, -initrd and -append options to do a direct Linux kernel
boot.

Being able to do direct Linux boots with qemu is a prerequisite for
booting images of architectures that don't support UEFI in qemu. It's
also faster than booting in UEFI mode which is useful when iterating
and not working on the bootloader.

.github/workflows/ci.yml
mkosi.md
mkosi/__init__.py
mkosi/backend.py
mkosi/gentoo.py
tests/test_config_parser.py

index ee8cddf88bfc02f2ac1b45f00dd94caafdc27954..49db1ecf4103930113ecd803501456026d5216e4 100644 (file)
@@ -188,6 +188,9 @@ jobs:
         tee mkosi.default <<- EOF
         [Output]
         BootProtocols=uefi
+
+        [Host]
+        QemuBoot=uefi
         EOF
 
         sudo MKOSI_TEST_DEFAULT_VERB=qemu python3 -m pytest -m integration -sv tests
@@ -198,6 +201,9 @@ jobs:
         [Output]
         BootProtocols=uefi
         WithUnifiedKernelImages=no
+
+        [Host]
+        QemuBoot=uefi
         EOF
 
         sudo MKOSI_TEST_DEFAULT_VERB=qemu python3 -m pytest -m integration -sv tests
@@ -208,6 +214,22 @@ jobs:
         [Output]
         BootProtocols=bios
         WithUnifiedKernelImages=no
+
+        [Host]
+        QemuBoot=bios
+        EOF
+
+        sudo MKOSI_TEST_DEFAULT_VERB=qemu python3 -m pytest -m integration -sv tests
+
+    - name: Build/Boot ${{ matrix.distro }}/${{ matrix.format}} QEMU Linux Boot
+      run: |
+        tee mkosi.default <<- EOF
+        [Output]
+        BootProtocols=linux
+        WithUnifiedKernelImages=no
+
+        [Host]
+        QemuBoot=linux
         EOF
 
         sudo MKOSI_TEST_DEFAULT_VERB=qemu python3 -m pytest -m integration -sv tests
index aee6de1f9ff13889e3ad279dab00b89add228a8f..140d9c6536bcec3a756ee09b54d0e0e07e721d8c 100644 (file)
--- a/mkosi.md
+++ b/mkosi.md
@@ -456,10 +456,14 @@ a boolean argument: either "1", "yes", or "true" to enable, or "0",
 
 : Pick one or more boot protocols to support when generating a
   bootable image, as enabled with `Bootable=`. Takes a comma-separated
-  list of `uefi` or `bios`. May be specified more than once in which
+  list of `uefi`, `bios`, or `linux`. May be specified more than once in which
   case the specified lists are merged. If `uefi` is specified the
   `sd-boot` UEFI boot loader is used, if `bios` is specified the GNU
-  Grub boot loader is used. Use "!\*" to remove all previously added
+  Grub boot loader is used. If `linux` is specified, the kernel image, initrd
+  and kernel cmdline are extracted from the image and stored in the output
+  directory. When running the `qemu` verb and setting the `--qemu-boot` option
+  to `linux`, qemu will be instructed to do a direct Linux kernel boot using
+  the previously extracted files. Use "!\*" to remove all previously added
   protocols or "!protocol" to remove one protocol.
 
 `KernelCommandLine=`, `--kernel-command-line=`
@@ -1129,6 +1133,13 @@ a machine ID.
   scope unit for the containers. This option should be used when mkosi is
   run by a service unit.
 
+`QemuBoot=`, `--qemu-boot=`
+
+: When used with the `qemu` verb, this option sets the boot protocol to be used
+  by qemu. Can be set to either `uefi`, `bios`, or `linux`. Note that a boot
+  procotol needs to be included in `BootProtocols=` when building the image for
+  it to be usable with this option.
+
 `Netdev=`, `--netdev`
 
 : When used with the boot or qemu verbs, this option creates a virtual
index b59ebb4748d9924d13ac33e821171cc413b8deeb..b9db52eaff9216ee1a3f0adaf5e15ac7d8232749 100644 (file)
@@ -1820,8 +1820,10 @@ def prepare_tree(args: MkosiArgs, root: Path, do_run_build_script: bool, cached:
                 root.joinpath("boot").mkdir(mode=0o700)
                 # Make sure kernel-install actually runs when needed by creating the machine-id subdirectory
                 # under /boot. For "bios" on Debian/Ubuntu, it's required for grub to pick up the generated
-                # initrd.
-                if args.distribution in (Distribution.debian, Distribution.ubuntu) and "bios" in args.boot_protocols:
+                # initrd. For "linux", we need kernel-install to run so we can extract the generated initrd
+                # from /boot later.
+                if ((args.distribution in (Distribution.debian, Distribution.ubuntu) and "bios" in args.boot_protocols) or
+                    ("linux" in args.boot_protocols and "uefi" not in args.boot_protocols)):
                     root.joinpath("boot", args.machine_id).mkdir(mode=0o700)
 
             if args.get_partition(PartitionIdentifier.esp):
@@ -4461,6 +4463,70 @@ def extract_unified_kernel(
     return f
 
 
+def extract_kernel_image_initrd(
+    args: MkosiArgs,
+    root: Path,
+    do_run_build_script: bool,
+    for_cache: bool,
+    mount: Callable[[], ContextManager[None]],
+) -> Union[Tuple[BinaryIO, BinaryIO], Tuple[None, None]]:
+    if do_run_build_script or for_cache or "linux" not in args.boot_protocols:
+        return None, None
+
+    prefix = "efi" if args.get_partition(PartitionIdentifier.esp) else "boot"
+
+    with mount():
+        kimgabs = None
+        initrd = None
+
+        for kver, kimg in gen_kernel_images(args, root):
+            kimgabs = root / kimg
+            initrd = root / prefix / args.machine_id / kver / "initrd"
+
+        if kimgabs is None:
+            die("No kernel image found, can't extract.")
+        assert initrd is not None
+
+        fkimg = copy_file_temporary(kimgabs, args.output_dir or Path())
+        finitrd = copy_file_temporary(initrd, args.output_dir or Path())
+
+    return (fkimg, finitrd)
+
+
+def extract_kernel_cmdline(
+    args: MkosiArgs,
+    root: Path,
+    do_run_build_script: bool,
+    for_cache: bool,
+    mount: Callable[[], ContextManager[None]],
+) -> Optional[TextIO]:
+    if do_run_build_script or for_cache or "linux" not in args.boot_protocols:
+        return None
+
+    with mount():
+        if root.joinpath("etc/kernel/cmdline").exists():
+            p = root / "etc/kernel/cmdline"
+        elif root.joinpath("usr/lib/kernel/cmdline").exists():
+            p = root / "usr/lib/kernel/cmdline"
+        else:
+            die("No cmdline found")
+
+        # Direct Linux boot means we can't rely on systemd-gpt-auto-generator to
+        # figure out the root partition for us so we have to encode it manually
+        # in the kernel cmdline.
+        cmdline = f"{p.read_text().strip()} root=LABEL={PartitionIdentifier.root.name}\n"
+
+        f = cast(
+            TextIO,
+            tempfile.NamedTemporaryFile(mode="w+", prefix=".mkosi-", encoding="utf-8", dir=args.output_dir or Path()),
+        )
+
+        f.write(cmdline)
+        f.flush()
+
+    return f
+
+
 def compress_output(
     args: MkosiArgs, data: Optional[BinaryIO], suffix: Optional[str] = None
 ) -> Optional[BinaryIO]:
@@ -4835,10 +4901,31 @@ def link_output_split_verity_sig(args: MkosiArgs, split_verity_sig: Optional[Som
 def link_output_split_kernel(args: MkosiArgs, split_kernel: Optional[SomeIO]) -> None:
     if split_kernel:
         assert args.output_split_kernel
-        with complete_step("Linking split kernel image…", f"Linked {path_relative_to_cwd(args.output_split_kernel)}"):
+        with complete_step("Linking split kernel…", f"Linked {path_relative_to_cwd(args.output_split_kernel)}"):
             _link_output(args, split_kernel.name, args.output_split_kernel)
 
 
+def link_output_split_kernel_image(args: MkosiArgs, split_kernel_image: Optional[SomeIO]) -> None:
+    if split_kernel_image:
+        output = build_auxiliary_output_path(args, '.vmlinuz')
+        with complete_step("Linking split kernel image…", f"Linked {path_relative_to_cwd(output)}"):
+            _link_output(args, split_kernel_image.name, output)
+
+
+def link_output_split_initrd(args: MkosiArgs, split_initrd: Optional[SomeIO]) -> None:
+    if split_initrd:
+        output = build_auxiliary_output_path(args, '.initrd')
+        with complete_step("Linking split initrd…", f"Linked {path_relative_to_cwd(output)}"):
+            _link_output(args, split_initrd.name, output)
+
+
+def link_output_split_kernel_cmdline(args: MkosiArgs, split_kernel_cmdline: Optional[SomeIO]) -> None:
+    if split_kernel_cmdline:
+        output = build_auxiliary_output_path(args, '.cmdline')
+        with complete_step("Linking split cmdline…", f"Linked {path_relative_to_cwd(output)}"):
+            _link_output(args, split_kernel_cmdline.name, output)
+
+
 def dir_size(path: PathString) -> int:
     dir_sum = 0
     for entry in os.scandir(path):
@@ -5898,6 +5985,12 @@ def create_parser() -> ArgumentParserMkosi:
         action=BooleanAction,
         help="If specified, underlying systemd-nspawn containers use the resources of the current unit.",
     )
+    group.add_argument(
+        "--qemu-boot",
+        help="Configure which qemu boot protocol to use",
+        choices=["uefi", "bios", "linux", None],
+        metavar="PROTOCOL",
+    )
     group.add_argument(
         "--network-veth",     # Compatibility option
         dest="netdev",
@@ -6265,6 +6358,11 @@ def unlink_output(args: MkosiArgs) -> None:
                 unlink_try_hard(args.output_split_verity_sig)
                 unlink_try_hard(args.output_split_kernel)
 
+            if "linux" in args.boot_protocols:
+                unlink_try_hard(build_auxiliary_output_path(args, ".vmlinuz"))
+                unlink_try_hard(build_auxiliary_output_path(args, ".initrd"))
+                unlink_try_hard(build_auxiliary_output_path(args, ".cmdline"))
+
             if args.nspawn_settings is not None:
                 unlink_try_hard(args.output_nspawn_settings)
 
@@ -6472,7 +6570,7 @@ def xescape(s: str) -> str:
     return ret
 
 
-def build_auxiliary_output_path(args: argparse.Namespace, suffix: str, can_compress: bool = False) -> Path:
+def build_auxiliary_output_path(args: Union[argparse.Namespace, MkosiArgs], suffix: str, can_compress: bool = False) -> Path:
     output = strip_suffixes(args.output)
     compression = should_compress_output(args) if can_compress else False
     return output.with_name(f"{output.name}{suffix}{compression or ''}")
@@ -6582,7 +6680,7 @@ def load_args(args: argparse.Namespace) -> MkosiArgs:
             if args.distribution == Distribution.photon:
                 args.boot_protocols = ["bios"]
 
-        if not {"uefi", "bios"}.issuperset(args.boot_protocols):
+        if not {"uefi", "bios", "linux"}.issuperset(args.boot_protocols):
             die("Not a valid boot protocol")
 
         if "uefi" in args.boot_protocols and args.distribution == Distribution.photon:
@@ -6612,7 +6710,7 @@ def load_args(args: argparse.Namespace) -> MkosiArgs:
     if args.distribution == Distribution.clear and args.output_format == OutputFormat.gpt_btrfs:
         die("Sorry, Clear Linux does not support btrfs", MkosiNotSupportedException)
 
-    if args.distribution == Distribution.clear and "," in args.boot_protocols:
+    if args.distribution == Distribution.clear and {"uefi", "bios"}.issubset(args.boot_protocols):
         die("Sorry, Clear Linux does not support hybrid BIOS/UEFI images", MkosiNotSupportedException)
 
     if shutil.which("bsdtar") and args.distribution == Distribution.openmandriva and args.tar_strip_selinux_context:
@@ -7321,13 +7419,16 @@ class BuildOutput:
     split_verity: Optional[BinaryIO]
     split_verity_sig: Optional[BinaryIO]
     split_kernel: Optional[BinaryIO]
+    split_kernel_image: Optional[BinaryIO]
+    split_initrd: Optional[BinaryIO]
+    split_kernel_cmdline: Optional[TextIO]
 
     def raw_name(self) -> Optional[str]:
         return self.raw.name if self.raw is not None else None
 
     @classmethod
     def empty(cls) -> BuildOutput:
-        return cls(None, None, None, None, None, None, None, None, None)
+        return cls(None, None, None, None, None, None, None, None, None, None, None, None)
 
 
 def build_image(
@@ -7469,6 +7570,8 @@ def build_image(
                 if args.split_artifacts
                 else None
             )
+            split_kernel_image, split_initrd = extract_kernel_image_initrd(args, root, do_run_build_script, for_cache, mount)
+            split_kernel_cmdline = extract_kernel_cmdline(args, root, do_run_build_script, for_cache, mount)
 
     archive = make_tar(args, root, do_run_build_script, for_cache) or \
               make_cpio(args, root, do_run_build_script, for_cache)
@@ -7483,6 +7586,9 @@ def build_image(
         split_verity,
         split_verity_sig,
         split_kernel,
+        split_kernel_image,
+        split_initrd,
+        split_kernel_cmdline,
     )
 
 
@@ -7704,6 +7810,9 @@ def build_stuff(args: MkosiArgs) -> Manifest:
         link_output_split_verity(args, split_verity)
         link_output_split_verity_sig(args, split_verity_sig)
         link_output_split_kernel(args, split_kernel)
+        link_output_split_kernel_image(args, image.split_kernel_image)
+        link_output_split_initrd(args, image.split_initrd)
+        link_output_split_kernel_cmdline(args, image.split_kernel_cmdline)
 
         if image.root_hash is not None:
             MkosiPrinter.print_step(f"Root hash is {image.root_hash}.")
@@ -7938,10 +8047,14 @@ def qemu_check_kvm_support() -> bool:
 def run_qemu_cmdline(args: MkosiArgs) -> Iterator[List[str]]:
     accel = "kvm" if args.qemu_kvm else "tcg"
 
-    if "uefi" in args.boot_protocols:
+    if args.qemu_boot:
+        mode = args.qemu_boot
+    elif "uefi" in args.boot_protocols:
         mode = "uefi"
     elif "bios" in args.boot_protocols:
         mode = "bios"
+    elif "linux" in args.boot_protocols:
+        mode = "linux"
     else:
         mode = "uefi"
 
@@ -7992,6 +8105,13 @@ def run_qemu_cmdline(args: MkosiArgs) -> Iterator[List[str]]:
     if mode == "uefi":
         cmdline += ["-drive", f"if=pflash,format=raw,readonly=on,file={firmware}"]
 
+    if mode == "linux":
+        cmdline += [
+            "-kernel", str(build_auxiliary_output_path(args, ".vmlinuz")),
+            "-initrd", str(build_auxiliary_output_path(args, ".initrd")),
+            "-append", build_auxiliary_output_path(args, ".cmdline").read_text().strip(),
+        ]
+
     with contextlib.ExitStack() as stack:
         if mode == "uefi" and fw_supports_sb:
             ovmf_vars = stack.enter_context(copy_file_temporary(src=find_ovmf_vars(), dir=tmp_dir()))
index b3514b4ddaacaa2f19ed7a68433b54330b11e756..9bf9942de2f62f1b3e9024e01b63018a53f86e1c 100644 (file)
@@ -516,6 +516,7 @@ class MkosiArgs:
     qemu_mem: str
     qemu_kvm: bool
     qemu_args: Sequence[str]
+    qemu_boot: str
 
     # systemd-nspawn specific options
     nspawn_keep_unit: bool
index 0b6d96e6fd23df47646fd115d52977f5efc22942..6e48e07123a14b8eb50092345c1eb463a4415f22 100644 (file)
@@ -240,6 +240,8 @@ class Gentoo:
             elif args.get_partition(PartitionIdentifier.bios):
                 self.pkgs_boot = ["sys-boot/grub"]
                 self.grub_platforms = ["coreboot", "qemu", "pc"]
+            else:
+                self.pkgs_boot = []
 
             self.pkgs_boot += ["sys-kernel/gentoo-kernel-bin",
                                "sys-firmware/edk2-ovmf"]
index aa6157692fc66304388f9f6f713384b7f1e5fb52..52f6cbc64e6249bd2b24c05933dbfa19a3f99986 100644 (file)
@@ -135,6 +135,7 @@ class MkosiConfig:
             "qemu_kvm": mkosi.qemu_check_kvm_support(),
             "qemu_args": [],
             "nspawn_keep_unit": False,
+            "qemu_boot": None,
             "netdev": False,
             "ephemeral": False,
             "with_unified_kernel_images": True,