]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Support multiple [Match] sections
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 15 Dec 2023 13:23:22 +0000 (14:23 +0100)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Sat, 16 Dec 2023 17:15:21 +0000 (18:15 +0100)
Instead of considering all match sections part of the same match,
let's consider each [Match] section on its own. This allows doing
multiple independent triggers, such as:

"""
[Match]
Format=|disk
Format=|directory

[Match]
Architecture=|x86-64
Architecture=|arm64
"""

Which now means to match if the format is one of disk or directory
and the architecture is one of x86-64 or arm64.

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

index c733ef879ad4c1cbc352e19e86e1987cda4b3146..74c53f538443ab9a164bf26e06de9998cb4aeb3e 100644 (file)
@@ -1224,6 +1224,10 @@ def parse_ini(path: Path, only_sections: Collection[str] = ()) -> Iterator[tuple
             if line[-1] != ']':
                 die(f"{line} is not a valid section")
 
+            # Yield 3 empty strings to indicate we've finished the current section.
+            if section:
+                yield "", "", ""
+
             section = line[1:-1].strip()
             if not section:
                 die("Section name cannot be empty or whitespace")
@@ -2527,14 +2531,21 @@ def parse_config(argv: Sequence[str] = ()) -> tuple[MkosiArgs, tuple[MkosiConfig
         return default
 
     def match_config(path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool:
-        triggered = None
+        triggered: Optional[bool] = None
 
         # If the config file does not exist, we assume it matches so that we look at the other files in the
         # directory as well (mkosi.conf.d/ and extra files).
         if not path.exists():
             return True
 
-        for _, k, v in parse_ini(path, only_sections=["Match"]):
+        for section, k, v in parse_ini(path, only_sections=["Match"]):
+            if not section:
+                if triggered is False:
+                    return False
+
+                triggered = None
+                continue
+
             trigger = v.startswith("|")
             v = v.removeprefix("|")
             negate = v.startswith("!")
@@ -2613,6 +2624,9 @@ def parse_config(argv: Sequence[str] = ()) -> tuple[MkosiArgs, tuple[MkosiConfig
             logging.debug(f"Including configuration file {Path.cwd() / path}")
 
             for section, k, v in parse_ini(path, only_sections={s.section for s in SETTINGS} | {"Preset"}):
+                if not section:
+                    continue
+
                 name = k.removeprefix("@")
                 ns = namespace if k == name else defaults
 
index 2c8dd3e036bb62105bae27212874b7d28790ade2..80eccb37744ee00fe525cf4e13e1f5d28ceb8295 100644 (file)
@@ -328,6 +328,24 @@ the entire directory. If the conditions are not satisfied, the entire
 directory is skipped. The `[Match]` sections of files in `mkosi.conf.d/`
 and `mkosi.local.conf` only apply to the file itself.
 
+If there are multiple `[Match]` sections in the same configuration file,
+each of them has to be satisified in order for the configuration file to
+be included. Specifically, triggering matches only apply to the current
+`[Match]` section and are reset between multiple `[Match]` sections. As
+an example, the following will only match if the output format is one
+of `disk` or `directory` and the architecture is one of `x86-64` or
+`arm64`:
+
+```conf
+[Match]
+Format=|disk
+Format=|directory
+
+[Match]
+Architecture=|x86-64
+Architecture=|arm64
+```
+
 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 7539347d1c229868ff39d27729b3190d91aed3c0..7d37387ca41301341b8c417e3693b03ac7f1978a 100644 (file)
@@ -84,6 +84,8 @@ def test_parse_ini(tmp_path: Path) -> None:
     assert next(g) == ("MySection", "Value", "abc")
     assert next(g) == ("MySection", "Other", "def")
     assert next(g) == ("MySection", "ALLCAPS", "txt")
+    assert next(g) == ("", "", "")
+    assert next(g) == ("", "", "")
     assert next(g) == ("AnotherSection", "EmptyValue", "")
     assert next(g) == ("AnotherSection", "Multiline", "abc\ndef\nqed\nord")
 
@@ -355,6 +357,36 @@ def test_compression(tmp_path: Path) -> None:
         assert config.compress_output == Compression.none
 
 
+def test_match_multiple(tmp_path: Path) -> None:
+    with chdir(tmp_path):
+        Path("mkosi.conf").write_text(
+            """\
+            [Match]
+            Format=|disk
+            Format=|directory
+
+            [Match]
+            Architecture=|x86-64
+            Architecture=|arm64
+
+            [Output]
+            ImageId=abcde
+            """
+        )
+
+        # Both sections are not matched, so image ID should not be "abcde".
+        _, [config] = parse_config(["--format", "tar", "--architecture", "s390x"])
+        assert config.image_id != "abcde"
+
+        # Only a single section is matched, so image ID should not be "abcde".
+        _, [config] = parse_config(["--format", "disk", "--architecture", "s390x"])
+        assert config.image_id != "abcde"
+
+        # Both sections are matched, so image ID should be "abcde".
+        _, [config] = parse_config(["--format", "disk", "--architecture", "x86-64"])
+        assert config.image_id == "abcde"
+
+
 @pytest.mark.parametrize("dist1,dist2", itertools.combinations_with_replacement(Distribution, 2))
 def test_match_distribution(tmp_path: Path, dist1: Distribution, dist2: Distribution) -> None:
     with chdir(tmp_path):