Repository: systemd/mkosi

Issue:
ext4 images generated on newer hosts may not be bootable
I just stumbled upon this while testing a systemd mkosi run of centos8 on a up-to-date arch host.

The image builds fine, but fails inside the initrd when booting in qemu:
```
Mar 09 13:39:46 localhost systemd[1]: Starting systemd-fsck-root.service...
Mar 09 13:39:46 localhost systemd-fsck[368]: /dev/sda2 has unsupported feature(s): FEATURE_C12
Mar 09 13:39:46 localhost systemd-fsck[368]: e2fsck: Get a newer version of e2fsck!
Mar 09 13:39:46 localhost systemd-fsck[368]: root-x86-64: ********** WARNING: Filesystem still has errors **********
Mar 09 13:39:46 localhost systemd-fsck[366]: fsck failed with exit status 12.
Mar 09 13:39:46 localhost systemd[1]: systemd-fsck-root.service: Main process exited, code=exited, status=1/FAILURE
Mar 09 13:39:46 localhost systemd[1]: systemd-fsck-root.service: Failed with result 'exit-code'.
Mar 09 13:39:46 localhost systemd[1]: Failed to start systemd-fsck-root.service.
```

The issue is that newer userspace tools may use fs features not supported by older tools/kernels of the target image. Coincidentally, I just read this https://lwn.net/Articles/923969/ where it was suggested that the image should be created in a chroot by the userspace tools of the generated image for best support.


Discussion:
Yeah, I thought we might run into this issue sooner or later when reading the article. 

Using two step process to build the image with the same distro as would later run was discussed before, but it's not been pursuit really. Does the also mentioned `tune2fs` trick from the article work? If so that would be a good intermediate solution.
This really is a systemd-repart issue. Tbh I really do not wanna bother with the chroot.

We really need to add configurable filesystem options in systemd-repart. But since adding that is going to take a bit of work, I would just modify systemd-repart to pass the necessary CLI option to disable the new thing for now.

Resolving patch:
diff --git a/mkosi/__init__.py b/mkosi/__init__.py
--- a/mkosi/__init__.py
+++ b/mkosi/__init__.py
@@ -3152,8 +3152,12 @@ def invoke_repart(state: MkosiState, skip: Sequence[str] = [], split: bool = Fal
 
     cmdline += ["--definitions", definitions]
 
+    env = dict(TMPDIR=str(state.workspace))
+    for fs, options in state.installer.filesystem_options(state).items():
+        env[f"SYSTEMD_REPART_MKFS_OPTIONS_{fs.upper()}"] = " ".join(options)
+
     with complete_step("Generating disk image"):
-        output = json.loads(run(cmdline, stdout=subprocess.PIPE, env={"TMPDIR": state.workspace}).stdout)
+        output = json.loads(run(cmdline, stdout=subprocess.PIPE, env=env).stdout)
 
     roothash = usrhash = None
     for p in output:
diff --git a/mkosi/distributions/__init__.py b/mkosi/distributions/__init__.py
--- a/mkosi/distributions/__init__.py
+++ b/mkosi/distributions/__init__.py
@@ -39,6 +39,10 @@ def remove_packages(cls, state: "MkosiState", packages: Sequence[str]) -> None:
     def filesystem(cls) -> str:
         raise NotImplementedError
 
+    @classmethod
+    def filesystem_options(cls, state: "MkosiState") -> dict[str, list[str]]:
+        return {}
+
     @staticmethod
     def kernel_command_line(state: "MkosiState") -> list[str]:
         return []
diff --git a/mkosi/distributions/centos.py b/mkosi/distributions/centos.py
--- a/mkosi/distributions/centos.py
+++ b/mkosi/distributions/centos.py
@@ -38,6 +38,18 @@ def filesystem(cls) -> str:
         # https://marc.info/?l=linux-xfs&m=167450838316386&w=2.
         return "ext4"
 
+    @classmethod
+    def filesystem_options(cls, state: MkosiState) -> dict[str, list[str]]:
+        # Hard code the features from /etc/mke2fs.conf from CentOS 8 Stream to ensure that filesystems
+        # created on distros with newer versions of e2fsprogs are compatible with e2fsprogs from CentOS
+        # Stream 8.
+
+        return {
+            "8": {
+                "ext4": ["-O", "none,has_journal,extent,huge_file,flex_bg,metadata_csum,64bit,dir_nlink,extra_isize"],
+            },
+        }.get(state.config.release, {})
+
     @staticmethod
     def kernel_command_line(state: MkosiState) -> list[str]:
         kcl = []


Test patch: