import argparse
import base64
+import contextlib
import copy
import dataclasses
import enum
import uuid
from collections.abc import Collection, Iterable, Iterator, Sequence
from pathlib import Path
-from typing import Any, Callable, Collection, Optional, Union, cast
+from typing import Any, Callable, Optional, Type, Union, cast
from mkosi.architecture import Architecture
from mkosi.distributions import Distribution, detect_distribution
def config_make_list_parser(delimiter: str,
*,
parse: Callable[[str], Any] = str,
- unescape: bool = False) -> ConfigParseCallback:
+ unescape: bool = False,
+ reset: bool = True) -> ConfigParseCallback:
def config_parse_list(value: Optional[str], old: Optional[list[Any]]) -> Optional[list[Any]]:
new = old.copy() if old else []
values = value.replace(delimiter, "\n").split("\n")
# Empty strings reset the list.
- if len(values) == 1 and values[0] == "":
+ if reset and len(values) == 1 and values[0] == "":
return None
return new + [parse(v) for v in values if v]
logging.warning(f"{option_string} is no longer supported")
-def config_make_action(settings: Sequence[MkosiConfigSetting]) -> type[argparse.Action]:
- lookup = {s.dest: s for s in settings}
-
- class MkosiAction(argparse.Action):
- def __call__(
- self,
- parser: argparse.ArgumentParser,
- namespace: argparse.Namespace,
- values: Union[str, Sequence[Any], None],
- option_string: Optional[str] = None
- ) -> None:
- assert option_string is not None
-
- if values is None and self.nargs == "?":
- values = self.const or "yes"
-
- try:
- s = lookup[self.dest]
- except KeyError:
- die(f"Unknown setting {option_string}")
-
- if values is None or isinstance(values, str):
- setattr(namespace, s.dest, s.parse(values, getattr(namespace, self.dest, None)))
- else:
- for v in values:
- assert isinstance(v, str)
- setattr(namespace, s.dest, s.parse(v, getattr(namespace, self.dest, None)))
-
- return MkosiAction
-
-
class PagerHelpAction(argparse._HelpAction):
def __call__(
self,
access the value from state.
"""
+ include: tuple[str, ...]
presets: tuple[str]
dependencies: tuple[str]
SETTINGS = (
+ MkosiConfigSetting(
+ dest="include",
+ section="Config",
+ parse=config_make_list_parser(delimiter=",", reset=False, parse=make_path_parser()),
+ help="Include configuration from the specified file or directory",
+ ),
MkosiConfigSetting(
dest="presets",
long="--preset",
)
-def create_argument_parser() -> argparse.ArgumentParser:
- action = config_make_action(SETTINGS)
-
+def create_argument_parser(action: Type[argparse.Action]) -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="mkosi",
description="Build Bespoke OS Images",
settings_lookup_by_dest = {s.dest: s for s in SETTINGS}
match_lookup = {m.name: m for m in MATCHES}
+ @contextlib.contextmanager
+ def parse_new_includes(
+ namespace: argparse.Namespace,
+ defaults: argparse.Namespace,
+ ) -> Iterator[None]:
+ l = len(getattr(namespace, "include", []))
+
+ try:
+ yield
+ finally:
+ # Parse any includes that were added after yielding.
+ for p in getattr(namespace, "include", [])[l:]:
+ parse_config(p, namespace, defaults)
+
+ class MkosiAction(argparse.Action):
+ def __call__(
+ self,
+ parser: argparse.ArgumentParser,
+ namespace: argparse.Namespace,
+ values: Union[str, Sequence[Any], None],
+ option_string: Optional[str] = None
+ ) -> None:
+ assert option_string is not None
+
+ if values is None and self.nargs == "?":
+ values = self.const or "yes"
+
+ try:
+ s = settings_lookup_by_dest[self.dest]
+ except KeyError:
+ die(f"Unknown setting {option_string}")
+
+ with parse_new_includes(namespace, defaults):
+ if values is None or isinstance(values, str):
+ setattr(namespace, s.dest, s.parse(values, getattr(namespace, self.dest, None)))
+ else:
+ for v in values:
+ assert isinstance(v, str)
+ setattr(namespace, s.dest, s.parse(v, getattr(namespace, self.dest, None)))
+
def finalize_default(
setting: MkosiConfigSetting,
namespace: argparse.Namespace,
else:
default = setting.default
- setattr(namespace, setting.dest, default)
+ with parse_new_includes(namespace, defaults):
+ setattr(namespace, setting.dest, default)
+
return default
def match_config(path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool:
canonical = s.name if k == name else f"@{s.name}"
logging.warning(f"Setting {k} is deprecated, please use {canonical} instead.")
- setattr(ns, s.dest, s.parse(v, getattr(ns, s.dest, None)))
+ with parse_new_includes(namespace, defaults):
+ setattr(ns, s.dest, s.parse(v, getattr(ns, s.dest, None)))
if extras:
for s in SETTINGS:
argv += ["--", "build"]
namespace = argparse.Namespace()
- argparser = create_argument_parser()
+ argparser = create_argument_parser(MkosiAction)
argparser.parse_args(argv, namespace)
args = load_args(namespace)
Verb: {bold(args.verb)}
Cmdline: {bold(" ".join(args.cmdline))}
+ {bold("CONFIG")}:
+ Include: {line_join_list(config.include)}
+
{bold("PRESET")}:
Presets: {line_join_list(config.presets)}
Dependencies: {line_join_list(config.dependencies)}
import itertools
import logging
import operator
+import os
from pathlib import Path
from typing import Optional
import pytest
from mkosi.architecture import Architecture
-from mkosi.config import Compression, OutputFormat, Verb, parse_config, parse_ini
+from mkosi.config import (
+ Compression,
+ ConfigFeature,
+ OutputFormat,
+ Verb,
+ parse_config,
+ parse_ini,
+)
from mkosi.distributions import Distribution
from mkosi.util import chdir
# ImageVersion= is not set explicitly anymore, so now the version from mkosi.version should be used.
assert config.image_version == "1.2.3"
+ (tmp_path / "abc").mkdir()
+ (tmp_path / "abc/mkosi.conf").write_text(
+ """\
+ [Content]
+ Bootable=yes
+ """
+ )
+ (tmp_path / "abc/mkosi.conf.d").mkdir()
+ (tmp_path / "abc/mkosi.conf.d/abc.conf").write_text(
+ """\
+ [Output]
+ SplitArtifacts=yes
+ """
+ )
+
+ with chdir(tmp_path):
+ _, [config] = parse_config()
+ assert config.bootable == ConfigFeature.auto
+ assert config.split_artifacts == False
+
+ # Passing the directory should include both the main config file and the dropin.
+ _, [config] = parse_config(["--include", os.fspath(tmp_path / "abc")])
+ assert config.bootable == ConfigFeature.enabled
+ assert config.split_artifacts == True
+
+ # Passing the main config file should not include the dropin.
+ _, [config] = parse_config(["--include", os.fspath(tmp_path / "abc/mkosi.conf")])
+ assert config.bootable == ConfigFeature.enabled
+ assert config.split_artifacts == False
+
def test_parse_load_verb(tmp_path: Path) -> None:
(tmp_path / "mkosi.conf").write_text("[Distribution]\nDistribution=fedora")
f"""\
[Distribution]
Distribution=fedora
-
+
[{section}]
ImageId=testimage
"""