match: Callable[[str], bool]
+@dataclasses.dataclass(frozen=True)
+class Specifier:
+ char: str
+ callback: Callable[[argparse.Namespace, Path], str]
+
+
class CustomHelpFormatter(argparse.HelpFormatter):
def _format_action_invocation(self, action: argparse.Action) -> str:
if not action.option_strings or action.nargs == 0:
MATCH_LOOKUP = {m.name: m for m in MATCHES}
+SPECIFIERS = (
+ Specifier(
+ char="C",
+ callback=lambda ns, config: os.fspath(config.resolve().parent),
+ ),
+ Specifier(
+ char="P",
+ callback=lambda ns, config: os.fspath(Path.cwd()),
+ ),
+ Specifier(
+ char="D",
+ callback=lambda ns, config: os.fspath(ns.directory.resolve()),
+ ),
+)
+
+SPECIFIERS_LOOKUP_BY_CHAR = {s.char: s for s in SPECIFIERS}
+
# This regular expression can be used to split "AutoBump" -> ["Auto", "Bump"]
# and "NSpawnSettings" -> ["NSpawn", "Settings"]
# The first part (?<=[a-z]) is a positive look behind for a lower case letter
parsed_includes: set[tuple[int, int]] = set()
immutable_settings: set[str] = set()
- def expand_specifiers(text: str) -> str:
+ def expand_specifiers(text: str, path: Path) -> str:
percent = False
result: list[str] = []
if c == "%":
result += "%"
- else:
- s = SETTINGS_LOOKUP_BY_SPECIFIER.get(c)
- if not s:
- logging.warning(f"Unknown specifier '%{c}' found in {text}, ignoring")
- continue
-
- if (v := finalize_default(s)) is None:
+ elif setting := SETTINGS_LOOKUP_BY_SPECIFIER.get(c):
+ if (v := finalize_default(setting)) is None:
logging.warning(
- f"Setting {s.name} specified by specifier '%{c}' in {text} is not yet set, ignoring"
+ f"Setting {setting.name} specified by specifier '%{c}' in {text} is not yet set, ignoring"
)
continue
result += str(v)
+ elif specifier := SPECIFIERS_LOOKUP_BY_CHAR.get(c):
+ result += specifier.callback(namespace, path)
+ else:
+ logging.warning(f"Unknown specifier '%{c}' found in {text}, ignoring")
elif c == "%":
percent = True
else:
negate = v.startswith("!")
v = v.removeprefix("!")
- v = expand_specifiers(v)
+ v = expand_specifiers(v, path)
if not v:
die("Match value cannot be empty")
canonical = s.name if k == name else f"@{s.name}"
logging.warning(f"Setting {k} is deprecated, please use {canonical} instead.")
- v = expand_specifiers(v)
+ v = expand_specifiers(v, path)
with parse_new_includes():
setattr(ns, s.dest, s.parse(v, getattr(ns, s.dest, None)))
| `ImageVersion=` | `%v` |
| `Profile=` | `%p` |
+There are also specifiers that are independent of settings:
+
+| Specifier | Value |
+|-----------|-----------------------------------------|
+| `%C` | Parent directory of current config file |
+| `%P` | Current working directory |
+| `%D` | Directory that mkosi was invoked in |
+
+Note that the current working directory changes as mkosi parses its
+configuration. Specifically, each time mkosi parses a directory
+containing a `mkosi.conf` file, mkosi changes its working directory to
+that directory.
+
+Note that the directory that mkosi was invoked in is influenced by the
+`--directory=` command line argument.
+
+The following table shows example values for the directory specifiers
+listed above:
+
+| | `$D/mkosi.conf` | `$D/mkosi.conf.d/abc/abc.conf` | `$D/mkosi.conf.d/abc/mkosi.conf` |
+|------|-----------------|--------------------------------|----------------------------------|
+| `%C` | `$D` | `$D/mkosi.conf.d` | `$D/mkosi.conf.d/qed` |
+| `%P` | `$D` | `$D` | `$D/mkosi.conf.d/qed` |
+| `%D` | `$D` | `$D` | `$D` |
+
## Supported distributions
Images may be created containing installations of the following
ImageVersion=%v
OutputDirectory=%O
Output=%o
+ ConfigRootDirectory=%D
+ ConfigRootConfdir=%C
+ ConfigRootPwd=%P
+ """
+ )
+
+ (d / "mkosi.conf.d").mkdir()
+ (d / "mkosi.conf.d/abc.conf").write_text(
+ """\
+ [Content]
+ Environment=ConfigAbcDirectory=%D
+ ConfigAbcConfdir=%C
+ ConfigAbcPwd=%P
+ """
+ )
+ (d / "mkosi.conf.d/qed").mkdir()
+ (d / "mkosi.conf.d/qed/mkosi.conf").write_text(
+ """
+ [Content]
+ Environment=ConfigQedDirectory=%D
+ ConfigQedConfdir=%C
+ ConfigQedPwd=%P
"""
)
"ImageVersion": "1.2.3",
"OutputDirectory": str(Path.cwd() / "abcde"),
"Output": "test",
+ "ConfigRootDirectory": os.fspath(d),
+ "ConfigRootConfdir": os.fspath(d),
+ "ConfigRootPwd": os.fspath(d),
+ "ConfigAbcDirectory": os.fspath(d),
+ "ConfigAbcConfdir": os.fspath(d / "mkosi.conf.d"),
+ "ConfigAbcPwd": os.fspath(d),
+ "ConfigQedDirectory": os.fspath(d),
+ "ConfigQedConfdir": os.fspath(d / "mkosi.conf.d/qed"),
+ "ConfigQedPwd": os.fspath(d / "mkosi.conf.d/qed"),
}
assert {k: v for k, v in config.environment.items() if k in expected} == expected