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")
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("!")
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
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`,
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")
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):