]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add support for assert sections
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 15 Oct 2025 10:32:13 +0000 (12:32 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Wed, 15 Oct 2025 14:25:52 +0000 (16:25 +0200)
Often you only want to support a single distribution. Adding a
[Match] section to mkosi.conf will be very confusing for users as
they will end up with an empty image. By using [Assert], they'll get
a clear error that what they're doing is not supported.

mkosi/config.py
mkosi/resources/man/mkosi.1.md
tests/test_config.py

index c2ef2ceed9798e296cadefae1394af39359d2400..2fbc9e7762f80ecbb7c810d2641e99e0c83f595a 100644 (file)
@@ -2566,7 +2566,7 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
     if section and setting and value is not None:
         yield section, setting, value
 
-    if section:
+    if section and (not only_sections or section in only_sections):
         yield section, "", ""
 
 
@@ -4808,7 +4808,7 @@ class ParseContext:
 
         return default
 
-    def match_config(self, path: Path) -> bool:
+    def match_config(self, path: Path, asserts: bool = False) -> bool:
         condition_triggered: Optional[bool] = None
         match_triggered: Optional[bool] = None
         skip = False
@@ -4818,12 +4818,17 @@ class ParseContext:
         if not path.exists():
             return True
 
-        for section, k, v in parse_ini(path, only_sections=["Match", "TriggerMatch"]):
+        sections = ("Assert", "TriggerAssert") if asserts else ("Match", "TriggerMatch")
+
+        for section, k, v in parse_ini(path, only_sections=sections):
             if not k and not v:
-                if section == "Match" and condition_triggered is False:
-                    return False
+                if condition_triggered is False:
+                    if section == "Assert":
+                        die(f"{path.absolute()}: Trigger condition in [Assert] section was not satisfied")
+                    elif section == "Match":
+                        return False
 
-                if section == "TriggerMatch":
+                if section in ("TriggerAssert", "TriggerMatch"):
                     match_triggered = bool(match_triggered) or condition_triggered is not False
 
                 condition_triggered = None
@@ -4833,6 +4838,7 @@ class ParseContext:
             if skip:
                 continue
 
+            raw = v
             trigger = v.startswith("|")
             v = v.removeprefix("|")
             negate = v.startswith("!")
@@ -4867,15 +4873,21 @@ class ParseContext:
             if negate:
                 result = not result
             if not trigger and not result:
-                if section == "TriggerMatch":
+                if section.startswith("Trigger"):
                     skip = True
                     condition_triggered = False
                     continue
 
+                if asserts:
+                    die(f"{path.absolute()}: {k}={raw} in [Assert] section was not satisfied")
+
                 return False
             if trigger:
                 condition_triggered = bool(condition_triggered) or result
 
+        if match_triggered is False and asserts:
+            die(f"{path.absolute()}: None of the [TriggerAssert] sections was satisfied")
+
         return match_triggered is not False
 
     def parse_config_one(self, path: Path, parse_profiles: bool = False, parse_local: bool = False) -> bool:
@@ -4889,6 +4901,8 @@ class ParseContext:
         if not self.match_config(path):
             return False
 
+        self.match_config(path, asserts=True)
+
         if extras:
             if parse_local:
                 for localpath in (
index 59649451485133d03b7f9aa426aa04255c9cecae..82c2e49b6902b6f19362b8440dad17e767f26369 100644 (file)
@@ -466,6 +466,12 @@ matches. The absence of match sections is valued as true. Logically this means:
 (⋀ᵢ Matchᵢ) ∧ (⋁ᵢ TriggerMatchᵢ)
 ```
 
+There is also support for `[Assert]` and `[TriggerAssert]` sections which
+behave identically to match sections except parsing configuration will
+fail if the assert sections are not satisfied, i.e. all `[Assert]`
+sections in a file as well as at least one `[TriggertAssert]` section
+have to be satisfied or config parsing will fail.
+
 Command line options that take no argument are shown without `=` in
 their long version. In the config files, they should be specified with a
 boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
index 2385b2445e8e712e1785643e97b53c7298e57b54..f5fb9c9f1bd606901da92220159d3adc69c91dc3 100644 (file)
@@ -1578,3 +1578,75 @@ def test_subdir(tmp_path: Path) -> None:
 
         _, _, [config] = parse_config()
         assert config.output == "abc"
+
+
+def test_assert(tmp_path: Path) -> None:
+    d = tmp_path
+
+    with chdir(d):
+        (d / "mkosi.conf").write_text(
+            """
+            [Assert]
+            ImageId=abcde
+            """
+        )
+
+        with pytest.raises(SystemExit):
+            parse_config()
+
+        # Does not raise, i.e. parses successfully, but we don't care for the content.
+        parse_config(["--image-id", "abcde"])
+
+        (d / "mkosi.conf").write_text(
+            """
+            [Assert]
+            ImageId=abcde
+
+            [Assert]
+            Environment=ABC=QED
+            """
+        )
+
+        with pytest.raises(SystemExit):
+            parse_config([])
+        with pytest.raises(SystemExit):
+            parse_config(["--image-id", "abcde"])
+        with pytest.raises(SystemExit):
+            parse_config(["--environment", "ABC=QED"])
+
+        parse_config(["--image-id", "abcde", "--environment", "ABC=QED"])
+
+        (d / "mkosi.conf").write_text(
+            """
+            [TriggerAssert]
+            ImageId=abcde
+
+            [TriggerAssert]
+            Environment=ABC=QED
+            """
+        )
+
+        with pytest.raises(SystemExit):
+            parse_config()
+
+        parse_config(["--image-id", "abcde"])
+        parse_config(["--environment", "ABC=QED"])
+
+        (d / "mkosi.conf").write_text(
+            """
+            [Assert]
+            ImageId=abcde
+
+            [TriggerAssert]
+            Environment=ABC=QED
+
+            [TriggerAssert]
+            Environment=DEF=QEE
+            """
+        )
+
+        with pytest.raises(SystemExit):
+            parse_config()
+
+        parse_config(["--image-id", "abcde", "--environment", "ABC=QED"])
+        parse_config(["--image-id", "abcde", "--environment", "DEF=QEE"])