From 9e1a2f18b8d3ffba5e2d08d9659a7a70bdaa6f9b Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Wed, 15 Oct 2025 12:32:13 +0200 Subject: [PATCH] Add support for assert sections 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 | 28 +++++++++---- mkosi/resources/man/mkosi.1.md | 6 +++ tests/test_config.py | 72 ++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 7 deletions(-) diff --git a/mkosi/config.py b/mkosi/config.py index c2ef2ceed..2fbc9e776 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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 ( diff --git a/mkosi/resources/man/mkosi.1.md b/mkosi/resources/man/mkosi.1.md index 596494514..82c2e49b6 100644 --- a/mkosi/resources/man/mkosi.1.md +++ b/mkosi/resources/man/mkosi.1.md @@ -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`, diff --git a/tests/test_config.py b/tests/test_config.py index 2385b2445..f5fb9c9f1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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"]) -- 2.47.3