]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add --initrd option
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 15 Feb 2023 17:09:29 +0000 (18:09 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 10 Mar 2023 14:43:17 +0000 (15:43 +0100)
--initrd allows users to provide their own initrds. When used, we'll
automatically create another initrd per kernel containing just the
kernel modules and all the initrds to ukify.

We don't compress the kernel modules initrd as the kernel modules
should be compressed themselves already.

12 files changed:
.github/workflows/ci.yml
docs/initrd.md [new file with mode: 0644]
mkosi.md
mkosi/__init__.py
mkosi/backend.py
mkosi/distributions/arch.py
mkosi/distributions/centos.py
mkosi/distributions/debian.py
mkosi/distributions/fedora.py
mkosi/distributions/mageia.py
mkosi/distributions/openmandriva.py
mkosi/distributions/opensuse.py

index 43021c69e32dcdd8265ab2ce861411912f741f08..d635856b450e32a56a7a268f44da09b0ecd42f56 100644 (file)
@@ -100,7 +100,7 @@ jobs:
     runs-on: ubuntu-22.04
     needs: unit-test
     concurrency:
-      group: ${{ github.workflow }}-${{ matrix.distro }}-${{ matrix.format }}-${{ github.ref }}
+      group: ${{ github.workflow }}-${{ matrix.distro }}-${{ matrix.format }}-${{ matrix.prebuilt-initrd }}-${{ github.ref }}
       cancel-in-progress: true
     strategy:
       fail-fast: false
@@ -120,6 +120,14 @@ jobs:
           - tar
           - cpio
           - disk
+        prebuilt-initrd:
+          - no
+          - yes
+        exclude:
+          # Debian prebuilt initrds are so large they cause OOMs.
+          # TODO: Revisit this if Debian ever decides to compress their kernel modules.
+          - distro: debian
+            prebuilt-initrd: yes
 
     steps:
     - uses: actions/checkout@v3
@@ -162,6 +170,10 @@ jobs:
     - name: Install
       run: sudo python3 -m pip install .
 
+    - name: Build ${{ matrix.distro }} initrd
+      if: matrix.prebuilt-initrd == 'yes'
+      run: python3 -m mkosi -d ${{ matrix.distro }} -t cpio -p systemd -p udev -p kmod -o $PWD/initrd build
+
     - name: Configure ${{ matrix.distro }}/${{ matrix.format }}
       run: |
         mkdir -p mkosi.conf.d
@@ -193,6 +205,14 @@ jobs:
         sync-uri = https://raw.githubusercontent.com/257/binpkgs/master
         EOF
 
+    - name: Configure ${{ matrix.distro }}/${{ matrix.format }} initrd
+      if: matrix.prebuilt-initrd == 'yes'
+      run: |
+        tee mkosi.conf.d/initrd.conf <<- EOF
+        [Distribution]
+        Initrd=initrd.zst
+        EOF
+
     - name: Configure EPEL
       if: matrix.distro == 'centos' || matrix.distro == 'rocky' || matrix.distro == 'alma'
       run: |
diff --git a/docs/initrd.md b/docs/initrd.md
new file mode 100644 (file)
index 0000000..b83a1c5
--- /dev/null
@@ -0,0 +1,20 @@
+# Building a custom initrd and using it in a mkosi image
+
+Building an image with a mkosi-built initrd is a two step process, because you will build two images - the initrd and your distribution image.
+1. Build an initrd image using the `cpio` output format with the same target distribtution as you want to use for your distribution image. mkosi compresses the `cpio` output format by default.
+```
+[Output]
+Format=cpio
+
+[Content]
+Packages=systemd
+         udev
+         kmod
+```
+2. Invoke `mkosi` passing the initrd image via the `--initrd` option or add the `Initrd=` option to your mkosi config when building your distribution image.
+```bash
+mkosi --initrd=<path-to-initrd-image> ...
+```
+This will build an image using the provided initrd image.
+mkosi will add the kernel modules found in the distribution image to this initrd.
+
index 3d54fc677b2fe9ec2f44edf0a1846f2ef16e03d9..886a23aacfa3b2529cf687594d320f7966e36d12 100644 (file)
--- a/mkosi.md
+++ b/mkosi.md
@@ -453,6 +453,12 @@ a boolean argument: either "1", "yes", or "true" to enable, or "0",
   in building or further container import stages.  This option strips
   SELinux context attributes from the resulting tar archive.
 
+`Initrd=`, `--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
+  are combined.
+
 ### [Content] Section
 
 `BasePackages=`, `--base-packages`
index 4b0d307e920c4ff253ef78ea0a4f86a59eb714dd..5c804e3ab58e6bd5fbb30fea8cd1f966e863ce68 100644 (file)
@@ -205,7 +205,7 @@ def configure_hostname(state: MkosiState, cached: bool) -> None:
 
 
 def configure_dracut(state: MkosiState, cached: bool) -> None:
-    if not state.config.bootable or cached:
+    if not state.config.bootable or cached or state.config.initrds:
         return
 
     dracut_dir = state.root / "etc/dracut.conf.d"
@@ -790,6 +790,22 @@ def gen_kernel_images(state: MkosiState) -> Iterator[tuple[str, Path]]:
         yield kver.name, kimg
 
 
+def gen_kernel_modules_initrd(state: MkosiState, kver: str) -> Path:
+    kmods = state.workspace / f"initramfs-kernel-modules-{kver}.img"
+
+    def files() -> Iterator[Path]:
+        yield state.root.joinpath("usr/lib/modules").relative_to(state.root)
+        yield state.root.joinpath("usr/lib/modules").joinpath(kver).relative_to(state.root)
+        for p in find_files(state.root / "usr/lib/modules" / kver, state.root):
+            if p.name != "vmlinuz":
+                yield p
+
+    with complete_step(f"Generating kernel modules initrd for kernel {kver}"):
+        make_cpio(state.root, files(), kmods)
+
+    return kmods
+
+
 def install_unified_kernel(state: MkosiState, roothash: Optional[str]) -> None:
     # Iterates through all kernel versions included in the image and generates a combined
     # kernel+initrd+cmdline+osrelease EFI file from it and places it in the /EFI/Linux directory of the ESP.
@@ -872,11 +888,16 @@ def install_unified_kernel(state: MkosiState, roothash: Optional[str]) -> None:
                         "--pcr-banks", "sha1,sha256"
                     ]
 
-            initrd = state.root.joinpath(state.installer.initrd_path(kver))
-            if not initrd.exists():
-                die(f"Initrd not found at {initrd}")
+            if state.config.initrds:
+                initrds = state.config.initrds + [gen_kernel_modules_initrd(state, kver)]
+            else:
+                initrd = state.root / state.installer.initrd_path(kver)
+                if not initrd.exists():
+                    die(f"Initrd not found at {initrd}")
+
+                initrds = [initrd]
 
-            cmd += [state.root / kimg, initrd]
+            cmd += [state.root / kimg] + initrds
 
             run(cmd)
 
@@ -1633,6 +1654,15 @@ def create_parser() -> ArgumentParserMkosi:
         dest="repart_dir",
         help="Directory containing systemd-repart partition definitions",
     )
+    group.add_argument(
+        "--initrd",
+        dest="initrds",
+        action=CommaDelimitedListAction,
+        default=[],
+        help="Add a user-provided initrd to image",
+        type=Path,
+        metavar="PATH",
+    )
 
     group = parser.add_argument_group("Content options")
     group.add_argument(
@@ -2574,6 +2604,14 @@ def load_args(args: argparse.Namespace) -> MkosiConfig:
     if args.repositories and not is_dnf_distribution(args.distribution) and args.distribution not in (Distribution.debian, Distribution.ubuntu):
         die("Sorry, the --repositories option is only supported on DNF/Debian based distributions")
 
+    if args.initrds:
+        args.initrds = [p.absolute() for p in args.initrds]
+        for p in args.initrds:
+            if not p.exists():
+                die(f"Initrd {p} not found")
+            if not p.is_file():
+                die(f"Initrd {p} is not a file")
+
     return MkosiConfig(**vars(args))
 
 
@@ -2707,6 +2745,9 @@ def print_summary(config: MkosiConfig) -> None:
     if config.repositories is not None and len(config.repositories) > 0:
         print("              Repositories:", ",".join(config.repositories))
 
+    if config.initrds:
+        print("                   Initrds:", ",".join(os.fspath(p) for p in config.initrds))
+
     print("\nOUTPUT:")
 
     if config.hostname:
@@ -2936,6 +2977,20 @@ def configure_netdev(state: MkosiState, cached: bool) -> None:
         run(["systemctl", "--root", state.root, "enable", "systemd-networkd"])
 
 
+def configure_initrd(state: MkosiState) -> None:
+    if state.for_cache or not state.config.output_format == OutputFormat.cpio:
+        return
+
+    if not state.root.joinpath("init").exists():
+        state.root.joinpath("init").symlink_to("/usr/lib/systemd/systemd")
+
+    if state.root.joinpath("etc/initrd-release").exists():
+        return
+
+    state.root.joinpath("etc/os-release").rename(state.root / "etc/initrd-release")
+    state.root.joinpath("etc/os-release").symlink_to("/etc/initrd-release")
+
+
 def run_kernel_install(state: MkosiState, cached: bool) -> None:
     if not state.config.bootable:
         return
@@ -2954,7 +3009,18 @@ def run_kernel_install(state: MkosiState, cached: bool) -> None:
     else:
         machine_id = None
 
-    with complete_step("Generating initramfs images…"):
+    # Fedora unconditionally pulls in dracut when installing a kernel and upstream dracut refuses to skip
+    # running when implement KERNEL_INSTALL_INITRD_GENERATOR support so let's disable it manually here if the
+    # user provided initrds.
+
+    if state.config.initrds:
+        for p in state.root.joinpath("usr/lib/kernel/install.d").iterdir():
+            if "dracut" in p.name:
+                install = state.root.joinpath("etc/kernel/install.d")
+                install.mkdir(parents=True, exist_ok=True)
+                install.joinpath(p.name).symlink_to("/dev/null")
+
+    with complete_step("Running kernel-install…"):
         for kver, kimg in gen_kernel_images(state):
             cmd: list[PathString] = ["kernel-install", "add", kver, Path("/") / kimg]
 
@@ -2966,6 +3032,11 @@ def run_kernel_install(state: MkosiState, cached: bool) -> None:
             if machine_id and (p := state.root / "boot" / machine_id / kver / "initrd").exists():
                 shutil.move(p, state.root / state.installer.initrd_path(kver))
 
+    if state.config.initrds:
+        for p in state.root.joinpath("etc/kernel/install.d").iterdir():
+            if "dracut" in p.name:
+                os.unlink(p)
+
     if machine_id and (p := state.root / "boot" / machine_id).exists():
         shutil.rmtree(p)
 
@@ -3116,6 +3187,7 @@ def build_image(state: MkosiState, *, manifest: Optional[Manifest] = None) -> No
         configure_autologin(state, cached)
         configure_dracut(state, cached)
         configure_netdev(state, cached)
+        configure_initrd(state)
         run_prepare_script(state, cached, build=False)
         install_build_packages(state, cached)
         run_prepare_script(state, cached, build=True)
index b42c17d44bf0bdd151b3482bfb4a3882a7f97553..e44de931d4dea9ab66f3242cb055e0e08f79270b 100644 (file)
@@ -309,6 +309,7 @@ class MkosiConfig:
     debug: list[str]
     auto_bump: bool
     workspace_dir: Optional[Path]
+    initrds: list[Path]
 
     # QEMU-specific options
     qemu_headless: bool
index 1e00e031fc53135f0c26672fa85e518a6ccead98..867a73e8723b778e2aaa927015bcccb69d9e58f0 100644 (file)
@@ -93,7 +93,7 @@ def install_arch(state: MkosiState) -> None:
     packages = state.config.packages.copy()
     add_packages(state.config, packages, "base")
 
-    if state.config.bootable:
+    if state.config.bootable and not state.config.initrds:
         add_packages(state.config, packages, "dracut")
 
     official_kernel_packages = {
@@ -128,4 +128,7 @@ def invoke_pacman(state: MkosiState, packages: Sequence[str]) -> None:
         "-Sy", *sort_packages(packages),
     ]
 
+    if state.config.initrds:
+        cmdline += ["--assume-installed", "initramfs"]
+
     run_with_apivfs(state, cmdline, env=dict(KERNEL_INSTALL_BYPASS="1"))
index 99952afbc4a87e4ff1844ff448d4e405a5d08493..e821dc17f7da7b1fc06c17640cc719f184daa5a6 100644 (file)
@@ -72,7 +72,9 @@ class CentosInstaller(DistributionInstaller):
         packages = state.config.packages.copy()
         add_packages(state.config, packages, "systemd", "rpm")
         if state.config.bootable:
-            add_packages(state.config, packages, "kernel", "dracut", "dracut-config-generic")
+            add_packages(state.config, packages, "kernel")
+            if not state.config.initrds:
+                add_packages(state.config, packages, "dracut", "dracut-config-generic")
             add_packages(state.config, packages, "systemd-udev", conditional="systemd")
         if state.config.ssh:
             add_packages(state.config, packages, "openssh-server")
index 4e52eca5c6224d917e87f121f2c5295bee0330fb..800b40694f4ac77d57e04335c82559b81a646774 100644 (file)
@@ -97,7 +97,8 @@ class DebianInstaller(DistributionInstaller):
         add_packages(state.config, packages, "systemd", "systemd-sysv", "dbus", "libpam-systemd")
 
         if state.config.bootable:
-            add_packages(state.config, packages, "dracut", "dracut-config-generic")
+            if not state.config.initrds:
+                add_packages(state.config, packages, "dracut", "dracut-config-generic")
             cls._add_default_kernel_package(state, packages)
 
         if state.config.ssh:
@@ -182,9 +183,6 @@ class DebianInstaller(DistributionInstaller):
 
         cls._fixup_resolved(state, packages)
 
-        write_resource(state.root / "etc/kernel/install.d/50-mkosi-dpkg-reconfigure-dracut.install",
-                       "mkosi.resources", "dpkg-reconfigure-dracut.install", executable=True)
-
         # Debian/Ubuntu use a different path to store the locale so let's make sure that path is a symlink to
         # etc/locale.conf.
         state.root.joinpath("etc/default/locale").unlink(missing_ok=True)
@@ -195,6 +193,10 @@ class DebianInstaller(DistributionInstaller):
         presetdir.mkdir(exist_ok=True, mode=0o755)
         presetdir.joinpath("99-mkosi-disable.preset").write_text("disable *")
 
+        if state.config.bootable and not state.config.initrds:
+            write_resource(state.root / "etc/kernel/install.d/50-mkosi-dpkg-reconfigure-dracut.install",
+                           "mkosi.resources", "dpkg-reconfigure-dracut.install", executable=True)
+
     @classmethod
     def install_packages(cls, state: MkosiState, packages: Sequence[str]) -> None:
         invoke_apt(state, "get", "install", ["--assume-yes", "--no-install-recommends", *packages])
index 7310d53ea433c6845cd5655aad462a84ba1c4bfc..a303868cceda2f8e7454a2e6c9ece583f6953892 100644 (file)
@@ -103,9 +103,11 @@ def install_fedora(state: MkosiState) -> None:
     add_packages(state.config, packages, "systemd", "util-linux", "rpm")
 
     if state.config.bootable:
-        add_packages(state.config, packages, "kernel-core", "kernel-modules", "dracut", "dracut-config-generic")
+        add_packages(state.config, packages, "kernel-core", "kernel-modules")
         add_packages(state.config, packages, "systemd-udev", conditional="systemd")
-    if state.config.netdev:
+        if not state.config.initrds:
+            add_packages(state.config, packages, "dracut", "dracut-config-generic")
+    if  state.config.netdev:
         add_packages(state.config, packages, "systemd-networkd", conditional="systemd")
     if state.config.ssh:
         add_packages(state.config, packages, "openssh-server")
index b4b51294f7720e7dabd6045803f8883a367766a3..ff02d1926d0510e279e276cf2231836c02192632 100644 (file)
@@ -63,8 +63,8 @@ def install_mageia(state: MkosiState) -> None:
 
     packages = state.config.packages.copy()
     add_packages(state.config, packages, "basesystem-minimal", "dnf")
-    if state.config.bootable:
-        add_packages(state.config, packages, "kernel-server-latest", "dracut")
+    if state.config.bootable and not state.config.initrds:
+        add_packages(state.config, packages, "dracut")
         # Mageia ships /etc/50-mageia.conf that omits systemd from the initramfs and disables hostonly.
         # We override that again so our defaults get applied correctly on Mageia as well.
         state.root.joinpath("etc/dracut.conf.d/51-mkosi-override-mageia.conf").write_text(
index a0854514d7f95d43528e36ffe12f44518495bcb0..4318f9c3c68a149f539d47ced11c3addc3bde8e0 100644 (file)
@@ -67,7 +67,9 @@ def install_openmandriva(state: MkosiState) -> None:
     add_packages(state.config, packages, "basesystem-minimal", "systemd", "dnf")
     if state.config.bootable:
         add_packages(state.config, packages, "systemd-boot", "systemd-cryptsetup", conditional="systemd")
-        add_packages(state.config, packages, "kernel-release-server", "dracut", "timezone")
+        add_packages(state.config, packages, "kernel-release-server", "timezone")
+        if not state.config.initrds:
+            add_packages(state.config, packages, "dracut")
     if state.config.netdev:
         add_packages(state.config, packages, "systemd-networkd", conditional="systemd")
     if state.config.ssh:
index 6d2bf6049ab8723b8bf0523811fd18b3e34b23db..54f1cda6302d10b0d57ad283c8b393bc2a11ae44 100644 (file)
@@ -156,7 +156,9 @@ def install_opensuse(state: MkosiState) -> None:
         add_packages(state.config, packages, "patterns-base-minimal_base")
 
     if state.config.bootable:
-        add_packages(state.config, packages, "kernel-default", "dracut")
+        add_packages(state.config, packages, "kernel-default")
+        if not state.config.initrds:
+            add_packages(state.config, packages, "dracut")
 
     if state.config.netdev:
         add_packages(state.config, packages, "systemd-network")
@@ -198,8 +200,7 @@ def install_opensuse(state: MkosiState) -> None:
                     shutil.copy2(state.root / f"usr/{prefix}/pam.d/login", state.root / "etc/pam.d/login")
                     break
 
-    if state.config.bootable:
+    if state.config.bootable and not state.config.initrds:
         dracut_dir = state.root / "etc/dracut.conf.d"
         dracut_dir.mkdir(mode=0o755, exist_ok=True)
-
         dracut_dir.joinpath("30-mkosi-opensuse.conf").write_text('hostonly=no\n')