]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Move more configuration parsing logic to config.py
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 7 Apr 2023 12:39:37 +0000 (14:39 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 7 Apr 2023 13:03:07 +0000 (15:03 +0200)
mkosi/__init__.py
mkosi/__main__.py
mkosi/config.py
tests/test_parse_load_args.py

index 095d8f4b21cf48b60eaf8e40ede991de6b073e9c..b914485824f530df3188bee7aadc8c8f9afcfa6e 100644 (file)
@@ -22,7 +22,7 @@ import tempfile
 import uuid
 from collections.abc import Iterator, Sequence
 from pathlib import Path
-from textwrap import dedent, wrap
+from textwrap import dedent
 from typing import Callable, ContextManager, Optional, TextIO, TypeVar, Union, cast
 
 from mkosi.backend import (
@@ -33,7 +33,6 @@ from mkosi.backend import (
     OutputFormat,
     Verb,
     current_user_uid_gid,
-    detect_distribution,
     flatten,
     format_rlimit,
     is_dnf_distribution,
@@ -42,26 +41,6 @@ from mkosi.backend import (
     should_compress_output,
     tmp_dir,
 )
-from mkosi.config import (
-    MkosiConfigParser,
-    MkosiConfigSetting,
-    config_default_release,
-    config_make_action,
-    config_make_enum_matcher,
-    config_make_enum_parser,
-    config_make_list_parser,
-    config_make_path_parser,
-    config_match_string,
-    config_parse_base_packages,
-    config_parse_boolean,
-    config_parse_compression,
-    config_parse_feature,
-    config_parse_script,
-    config_parse_string,
-    make_enum_parser,
-    make_path_parser,
-    parse_source_target_paths,
-)
 from mkosi.install import (
     add_dropin_config_from_resource,
     copy_path,
@@ -92,10 +71,6 @@ from mkosi.types import PathString
 complete_step = MkosiPrinter.complete_step
 color_error = MkosiPrinter.color_error
 
-
-__version__ = "14"
-
-
 MKOSI_COMMANDS_NEED_BUILD = (Verb.shell, Verb.boot, Verb.qemu, Verb.serve)
 MKOSI_COMMANDS_SUDO = (Verb.shell, Verb.boot)
 MKOSI_COMMANDS_CMDLINE = (Verb.build, Verb.shell, Verb.boot, Verb.qemu, Verb.ssh)
@@ -1033,1025 +1008,6 @@ def print_output_size(config: MkosiConfig) -> None:
         MkosiPrinter.print_step(f"Resulting image size is {size}, consumes {space}.")
 
 
-def remove_duplicates(items: list[T]) -> list[T]:
-    "Return list with any repetitions removed"
-    # We use a dictionary to simulate an ordered set
-    return list({x: None for x in items})
-
-
-class CustomHelpFormatter(argparse.HelpFormatter):
-    def _format_action_invocation(self, action: argparse.Action) -> str:
-        if not action.option_strings or action.nargs == 0:
-            return super()._format_action_invocation(action)
-        default = self._get_default_metavar_for_optional(action)
-        args_string = self._format_args(action, default)
-        return ", ".join(action.option_strings) + " " + args_string
-
-    def _split_lines(self, text: str, width: int) -> list[str]:
-        """Wraps text to width, each line separately.
-        If the first line of text ends in a colon, we assume that
-        this is a list of option descriptions, and subindent them.
-        Otherwise, the text is wrapped without indentation.
-        """
-        lines = text.splitlines()
-        subindent = '    ' if lines[0].endswith(':') else ''
-        return flatten(wrap(line, width, break_long_words=False, break_on_hyphens=False,
-                            subsequent_indent=subindent) for line in lines)
-
-
-USAGE = """
-       mkosi [options...] {b}summary{e}
-       mkosi [options...] {b}build{e} [script parameters...]
-       mkosi [options...] {b}shell{e} [command line...]
-       mkosi [options...] {b}boot{e} [nspawn settings...]
-       mkosi [options...] {b}qemu{e} [qemu parameters...]
-       mkosi [options...] {b}ssh{e} [command line...]
-       mkosi [options...] {b}clean{e}
-       mkosi [options...] {b}serve{e}
-       mkosi [options...] {b}bump{e}
-       mkosi [options...] {b}genkey{e}
-       mkosi [options...] {b}help{e}
-       mkosi -h | --help
-       mkosi --version
-""".format(b=MkosiPrinter.bold, e=MkosiPrinter.reset)
-
-
-SETTINGS = (
-    MkosiConfigSetting(
-        dest="distribution",
-        section="Distribution",
-        parse=config_make_enum_parser(Distribution),
-        match=config_make_enum_matcher(Distribution),
-        default=detect_distribution()[0],
-    ),
-    MkosiConfigSetting(
-        dest="release",
-        section="Distribution",
-        parse=config_parse_string,
-        match=config_match_string,
-        default_factory=config_default_release,
-    ),
-    MkosiConfigSetting(
-        dest="architecture",
-        section="Distribution",
-        default=platform.machine(),
-    ),
-    MkosiConfigSetting(
-        dest="mirror",
-        section="Distribution",
-    ),
-    MkosiConfigSetting(
-        dest="local_mirror",
-        section="Distribution",
-    ),
-    MkosiConfigSetting(
-        dest="repository_key_check",
-        section="Distribution",
-        default=True,
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="repositories",
-        section="Distribution",
-        parse=config_make_list_parser(delimiter=","),
-    ),
-    MkosiConfigSetting(
-        dest="repo_dirs",
-        name="RepositoryDirectories",
-        section="Distribution",
-        parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=True)),
-        paths=("mkosi.reposdir",),
-    ),
-    MkosiConfigSetting(
-        dest="output_format",
-        name="Format",
-        section="Output",
-        parse=config_make_enum_parser(OutputFormat),
-        default=OutputFormat.disk,
-    ),
-    MkosiConfigSetting(
-        dest="manifest_format",
-        section="Output",
-        parse=config_make_list_parser(delimiter=",", parse=make_enum_parser(ManifestFormat)),
-        default=[ManifestFormat.json],
-    ),
-    MkosiConfigSetting(
-        dest="output",
-        section="Output",
-        parse=config_make_path_parser(required=False),
-    ),
-    MkosiConfigSetting(
-        dest="output_dir",
-        name="OutputDirectory",
-        section="Output",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.output",),
-    ),
-    MkosiConfigSetting(
-        dest="workspace_dir",
-        name="WorkspaceDirectory",
-        section="Output",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.workspace",),
-    ),
-    MkosiConfigSetting(
-        dest="bootable",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="kernel_command_line",
-        section="Output",
-        parse=config_make_list_parser(delimiter=" "),
-    ),
-    MkosiConfigSetting(
-        dest="secure_boot",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="secure_boot_key",
-        section="Output",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.secure-boot.key",),
-    ),
-    MkosiConfigSetting(
-        dest="secure_boot_certificate",
-        section="Output",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.secure-boot.crt",),
-    ),
-    MkosiConfigSetting(
-        dest="secure_boot_valid_days",
-        section="Output",
-        default="730",
-    ),
-    MkosiConfigSetting(
-        dest="secure_boot_common_name",
-        section="Output",
-        default="mkosi of %u",
-    ),
-    MkosiConfigSetting(
-        dest="sign_expected_pcr",
-        section="Output",
-        parse=config_parse_feature,
-    ),
-    MkosiConfigSetting(
-        dest="passphrase",
-        section="Output",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.passphrase",),
-    ),
-    MkosiConfigSetting(
-        dest="compress_output",
-        section="Output",
-        parse=config_parse_compression,
-    ),
-    MkosiConfigSetting(
-        dest="hostname",
-        section="Output",
-    ),
-    MkosiConfigSetting(
-        dest="image_version",
-        section="Output",
-    ),
-    MkosiConfigSetting(
-        dest="image_id",
-        section="Output",
-    ),
-    MkosiConfigSetting(
-        dest="auto_bump",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="tar_strip_selinux_context",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="incremental",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="cache_initrd",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="split_artifacts",
-        section="Output",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="repart_dirs",
-        name="RepartDirectories",
-        section="Output",
-        parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=True)),
-        paths=("mkosi.repart",),
-    ),
-    MkosiConfigSetting(
-        dest="initrds",
-        section="Output",
-        parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=False)),
-    ),
-    MkosiConfigSetting(
-        dest="base_packages",
-        section="Content",
-        parse=config_parse_base_packages,
-        default=True,
-    ),
-    MkosiConfigSetting(
-        dest="packages",
-        section="Content",
-        parse=config_make_list_parser(delimiter=","),
-    ),
-    MkosiConfigSetting(
-        dest="remove_packages",
-        section="Content",
-        parse=config_make_list_parser(delimiter=","),
-    ),
-    MkosiConfigSetting(
-        dest="with_docs",
-        section="Content",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="with_tests",
-        section="Content",
-        parse=config_parse_boolean,
-        default=True,
-    ),
-    MkosiConfigSetting(
-        dest="password",
-        section="Content",
-    ),
-    MkosiConfigSetting(
-        dest="password_is_hashed",
-        section="Content",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="autologin",
-        section="Content",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="cache_dir",
-        name="CacheDirectory",
-        section="Content",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.cache",),
-    ),
-    MkosiConfigSetting(
-        dest="extra_trees",
-        section="Content",
-        parse=config_make_list_parser(delimiter=",", parse=parse_source_target_paths),
-        paths=("mkosi.extra", "mkosi.extra.tar"),
-    ),
-    MkosiConfigSetting(
-        dest="skeleton_trees",
-        section="Content",
-        parse=config_make_list_parser(delimiter=",", parse=parse_source_target_paths),
-        paths=("mkosi.skeleton", "mkosi.skeleton.tar"),
-    ),
-    MkosiConfigSetting(
-        dest="clean_package_metadata",
-        section="Content",
-        parse=config_parse_feature,
-    ),
-    MkosiConfigSetting(
-        dest="remove_files",
-        section="Content",
-        parse=config_make_list_parser(delimiter=","),
-    ),
-    MkosiConfigSetting(
-        dest="environment",
-        section="Content",
-        parse=config_make_list_parser(delimiter=" "),
-    ),
-    MkosiConfigSetting(
-        dest="build_sources",
-        section="Content",
-        parse=config_make_path_parser(required=True),
-        default=".",
-    ),
-    MkosiConfigSetting(
-        dest="build_dir",
-        name="BuildDirectory",
-        section="Content",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.builddir",),
-    ),
-    MkosiConfigSetting(
-        dest="install_dir",
-        name="InstallDirectory",
-        section="Content",
-        parse=config_make_path_parser(required=False),
-        paths=("mkosi.installdir",),
-    ),
-    MkosiConfigSetting(
-        dest="build_packages",
-        section="Content",
-        parse=config_make_list_parser(delimiter=","),
-    ),
-    MkosiConfigSetting(
-        dest="build_script",
-        section="Content",
-        parse=config_parse_script,
-        paths=("mkosi.build",),
-    ),
-    MkosiConfigSetting(
-        dest="prepare_script",
-        section="Content",
-        parse=config_parse_script,
-        paths=("mkosi.prepare",),
-    ),
-    MkosiConfigSetting(
-        dest="postinst_script",
-        name="PostInstallationScript",
-        section="Content",
-        parse=config_parse_script,
-        paths=("mkosi.postinst",),
-    ),
-    MkosiConfigSetting(
-        dest="finalize_script",
-        section="Content",
-        parse=config_parse_script,
-        paths=("mkosi.finalize",),
-    ),
-    MkosiConfigSetting(
-        dest="with_network",
-        section="Content",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="cache_only",
-        section="Content",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="nspawn_settings",
-        name="NSpawnSettings",
-        section="Content",
-        parse=config_make_path_parser(required=True),
-        paths=("mkosi.nspawn",),
-    ),
-    MkosiConfigSetting(
-        dest="base_image",
-        section="Content",
-        parse=config_make_path_parser(required=True),
-    ),
-    MkosiConfigSetting(
-        dest="checksum",
-        section="Validation",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="sign",
-        section="Validation",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="key",
-        section="Validation",
-    ),
-    MkosiConfigSetting(
-        dest="extra_search_paths",
-        section="Host",
-        parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=True)),
-    ),
-    MkosiConfigSetting(
-        dest="qemu_gui",
-        section="Host",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="qemu_smp",
-        section="Host",
-        default="1",
-    ),
-    MkosiConfigSetting(
-        dest="qemu_mem",
-        section="Host",
-        default="2G",
-    ),
-    MkosiConfigSetting(
-        dest="qemu_kvm",
-        section="Host",
-        parse=config_parse_feature,
-    ),
-    MkosiConfigSetting(
-        dest="qemu_args",
-        section="Host",
-        parse=config_make_list_parser(delimiter=" "),
-    ),
-    MkosiConfigSetting(
-        dest="ephemeral",
-        section="Host",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="ssh",
-        section="Host",
-        parse=config_parse_boolean,
-    ),
-    MkosiConfigSetting(
-        dest="credentials",
-        section="Host",
-        parse=config_make_list_parser(delimiter=" "),
-    ),
-    MkosiConfigSetting(
-        dest="kernel_command_line_extra",
-        section="Host",
-        parse=config_make_list_parser(delimiter=" "),
-    ),
-    MkosiConfigSetting(
-        dest="acl",
-        section="Host",
-        parse=config_parse_boolean,
-    ),
-)
-
-
-def create_argument_parser() -> argparse.ArgumentParser:
-    action = config_make_action(SETTINGS)
-
-    parser = argparse.ArgumentParser(
-        prog="mkosi",
-        description="Build Bespoke OS Images",
-        usage=USAGE,
-        add_help=False,
-        allow_abbrev=False,
-        argument_default=argparse.SUPPRESS,
-    )
-
-    parser.add_argument(
-        "verb",
-        type=Verb,
-        choices=list(Verb),
-        default=Verb.build,
-        help=argparse.SUPPRESS,
-    )
-    parser.add_argument(
-        "cmdline",
-        nargs=argparse.REMAINDER,
-        help=argparse.SUPPRESS,
-    )
-    parser.add_argument(
-        "-h", "--help",
-        action="help",
-        help=argparse.SUPPRESS,
-    )
-    parser.add_argument(
-        "--version",
-        action="version",
-        version="%(prog)s " + __version__,
-        help=argparse.SUPPRESS,
-    )
-    parser.add_argument(
-        "-f", "--force",
-        action="count",
-        dest="force",
-        default=0,
-        help="Remove existing image file before operation",
-    )
-    parser.add_argument(
-        "-C", "--directory",
-        help="Change to specified directory before doing anything",
-        type=Path,
-        metavar="PATH",
-    )
-    parser.add_argument(
-        "--debug",
-        help="Turn on debugging output",
-        action="append",
-        default=[],
-    )
-
-
-    group = parser.add_argument_group("Distribution options")
-    group.add_argument(
-        "-d", "--distribution",
-        choices=Distribution.__members__,
-        help="Distribution to install",
-        action=action,
-    )
-    group.add_argument(
-        "-r", "--release",
-        metavar="RELEASE",
-        help="Distribution release to install",
-        action=action,
-    )
-    group.add_argument(
-        "--architecture",
-        metavar="ARCHITECTURE",
-        help="Override the architecture of installation",
-        action=action,
-    )
-    group.add_argument(
-        "-m", "--mirror",
-        metavar="MIRROR",
-        help="Distribution mirror to use",
-        action=action,
-    )
-    group.add_argument(
-        "--local-mirror",
-        help="Use a single local, flat and plain mirror to build the image",
-        action=action,
-    )
-    group.add_argument(
-        "--repository-key-check",
-        metavar="BOOL",
-        help="Controls signature and key checks on repositories",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--repositories",
-        metavar="REPOS",
-        help="Repositories to use",
-        action=action,
-    )
-    group.add_argument(
-        "--repo-dir",
-        metavar="PATH",
-        help="Specify a directory containing extra distribution specific repository files",
-        dest="repo_dirs",
-        action=action,
-    )
-
-    group = parser.add_argument_group("Output options")
-    group.add_argument(
-        "-t", "--format",
-        metavar="FORMAT",
-        choices=OutputFormat.__members__,
-        dest="output_format",
-        help="Output Format",
-        action=action,
-    )
-    group.add_argument(
-        "--manifest-format",
-        metavar="FORMAT",
-        help="Manifest Format",
-        action=action,
-    )
-    group.add_argument(
-        "-o", "--output",
-        metavar="PATH",
-        help="Output image path",
-        action=action,
-    )
-    group.add_argument(
-        "-O", "--output-dir",
-        metavar="DIR",
-        help="Output root directory",
-        action=action,
-    )
-    group.add_argument(
-        "--workspace-dir",
-        metavar="DIR",
-        help="Workspace directory",
-        action=action,
-    )
-    group.add_argument(
-        "-b", "--bootable",
-        metavar="BOOL",
-        help="Make image bootable on EFI",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--kernel-command-line",
-        metavar="OPTIONS",
-        help="Set the kernel command line (only bootable images)",
-        action=action,
-    )
-    group.add_argument(
-        "--secure-boot",
-        metavar="BOOL",
-        help="Sign the resulting kernel/initrd image for UEFI SecureBoot",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--secure-boot-key",
-        metavar="PATH",
-        help="UEFI SecureBoot private key in PEM format",
-        action=action,
-    )
-    group.add_argument(
-        "--secure-boot-certificate",
-        metavar="PATH",
-        help="UEFI SecureBoot certificate in X509 format",
-        action=action,
-    )
-    group.add_argument(
-        "--secure-boot-valid-days",
-        metavar="DAYS",
-        help="Number of days UEFI SecureBoot keys should be valid when generating keys",
-        action=action,
-    )
-    group.add_argument(
-        "--secure-boot-common-name",
-        metavar="CN",
-        help="Template for the UEFI SecureBoot CN when generating keys",
-        action=action,
-    )
-    group.add_argument(
-        "--sign-expected-pcr",
-        metavar="FEATURE",
-        help="Measure the components of the unified kernel image (UKI) and embed the PCR signature into the UKI",
-        action=action,
-    )
-    group.add_argument(
-        "--passphrase",
-        metavar="PATH",
-        help="Path to a file containing the passphrase to use when LUKS encryption is selected",
-        action=action,
-    )
-    group.add_argument(
-        "--compress-output",
-        metavar="ALG",
-        help="Enable whole-output compression (with images or archives)",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument("--hostname", help="Set hostname", action=action)
-    group.add_argument("--image-version", help="Set version for image", action=action)
-    group.add_argument("--image-id", help="Set ID for image", action=action)
-    group.add_argument(
-        "-B", "--auto-bump",
-        metavar="BOOL",
-        help="Automatically bump image version after building",
-        action=action,
-    )
-    group.add_argument(
-        "--tar-strip-selinux-context",
-        metavar="BOOL",
-        help="Do not include SELinux file context information in tar. Not compatible with bsdtar.",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "-i", "--incremental",
-        metavar="BOOL",
-        help="Make use of and generate intermediary cache images",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--cache-initrd",
-        metavar="BOOL",
-        help="When using incremental mode, build the initrd in the cache image and don't rebuild it in the final image",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--split-artifacts",
-        metavar="BOOL",
-        help="Generate split partitions",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--repart-dir",
-        metavar="PATH",
-        help="Directory containing systemd-repart partition definitions",
-        dest="repart_dirs",
-        action=action,
-    )
-    group.add_argument(
-        "--initrd",
-        help="Add a user-provided initrd to image",
-        metavar="PATH",
-        dest="initrds",
-        action=action,
-    )
-
-    group = parser.add_argument_group("Content options")
-    group.add_argument(
-        "--base-packages",
-        metavar="OPTION",
-        help="Automatically inject basic packages in the system (systemd, kernel, …)",
-        action=action,
-    )
-    group.add_argument(
-        "-p", "--package",
-        metavar="PACKAGE",
-        help="Add an additional package to the OS image",
-        dest="packages",
-        action=action,
-    )
-    group.add_argument(
-        "--remove-package",
-        metavar="PACKAGE",
-        help="Remove package from the image OS image after installation",
-        dest="remove_packages",
-        action=action,
-    )
-    group.add_argument(
-        "--with-docs",
-        metavar="BOOL",
-        help="Install documentation",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "-T", "--without-tests",
-        help="Do not run tests as part of build script, if supported",
-        nargs="?",
-        const="no",
-        dest="with_tests",
-        action=action,
-    )
-
-    group.add_argument("--password", help="Set the root password", action=action)
-    group.add_argument(
-        "--password-is-hashed",
-        metavar="BOOL",
-        help="Indicate that the root password has already been hashed",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--autologin",
-        metavar="BOOL",
-        help="Enable root autologin",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--cache-dir",
-        metavar="PATH",
-        help="Package cache path",
-        action=action,
-    )
-    group.add_argument(
-        "--extra-tree",
-        metavar="PATH",
-        help="Copy an extra tree on top of image",
-        dest="extra_trees",
-        action=action,
-    )
-    group.add_argument(
-        "--skeleton-tree",
-        metavar="PATH",
-        help="Use a skeleton tree to bootstrap the image before installing anything",
-        dest="skeleton_trees",
-        action=action,
-    )
-    group.add_argument(
-        "--clean-package-metadata",
-        metavar="FEATURE",
-        help="Remove package manager database and other files",
-        action=action,
-    )
-    group.add_argument(
-        "--remove-files",
-        metavar="GLOB",
-        help="Remove files from built image",
-        action=action,
-    )
-    group.add_argument(
-        "-E", "--environment",
-        metavar="NAME[=VALUE]",
-        help="Set an environment variable when running scripts",
-        action=action,
-    )
-    group.add_argument(
-        "--build-sources",
-        metavar="PATH",
-        help="Path for sources to build",
-        action=action,
-    )
-    group.add_argument(
-        "--build-dir",
-        metavar="PATH",
-        help="Path to use as persistent build directory",
-        action=action,
-    )
-    group.add_argument(
-        "--install-dir",
-        metavar="PATH",
-        help="Path to use as persistent install directory",
-        action=action,
-    )
-    group.add_argument(
-        "--build-package",
-        metavar="PACKAGE",
-        help="Additional packages needed for build script",
-        dest="build_packages",
-        action=action,
-    )
-    group.add_argument(
-        "--build-script",
-        metavar="PATH",
-        help="Build script to run inside image",
-        action=action,
-    )
-    group.add_argument(
-        "--prepare-script",
-        metavar="PATH",
-        help="Prepare script to run inside the image before it is cached",
-        action=action,
-    )
-    group.add_argument(
-        "--postinst-script",
-        metavar="PATH",
-        help="Postinstall script to run inside image",
-        action=action,
-    )
-    group.add_argument(
-        "--finalize-script",
-        metavar="PATH",
-        help="Postinstall script to run outside image",
-        action=action,
-    )
-    group.add_argument(
-        "--with-network",
-        metavar="BOOL",
-        help="Run build and postinst scripts with network access (instead of private network)",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--cache-only",
-        metavar="BOOL",
-        help="Only use the package cache when installing packages",
-        action=action,
-    )
-    group.add_argument(
-        "--settings",
-        metavar="PATH",
-        help="Add in .nspawn settings file",
-        dest="nspawn_settings",
-        action=action,
-    )
-    group.add_argument(
-        '--base-image',
-        metavar='IMAGE',
-        help='Use the given image as base (e.g. lower sysext layer)',
-        action=action,
-    )
-
-    group = parser.add_argument_group("Validation options")
-    group.add_argument(
-        "--checksum",
-        metavar="BOOL",
-        help="Write SHA256SUMS file",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--sign",
-        help="Write and sign SHA256SUMS file",
-        metavar="BOOL",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument("--key", help="GPG key to use for signing", action=action)
-
-    group = parser.add_argument_group("Host configuration options")
-    group.add_argument(
-        "--extra-search-path",
-        help="List of colon-separated paths to look for programs before looking in PATH",
-        metavar="PATH",
-        dest="extra_search_paths",
-        action=action,
-    )
-    group.add_argument(
-        "--qemu-gui",
-        help="Start QEMU in graphical mode",
-        metavar="BOOL",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--qemu-smp",
-        metavar="SMP",
-        help="Configure guest's SMP settings",
-        action=action,
-    )
-    group.add_argument(
-        "--qemu-mem",
-        metavar="MEM",
-        help="Configure guest's RAM size",
-        action=action,
-    )
-    group.add_argument(
-        "--qemu-kvm",
-        metavar="BOOL",
-        help="Configure whether to use KVM or not",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--qemu-args",
-        metavar="ARGS",
-        # Suppress the command line option because it's already possible to pass qemu args as normal
-        # arguments.
-        help=argparse.SUPPRESS,
-        action=action,
-    )
-    group.add_argument(
-        "--ephemeral",
-        metavar="BOOL",
-        help=('If specified, the container/VM is run with a temporary snapshot of the output '
-              'image that is removed immediately when the container/VM terminates'),
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--ssh",
-        metavar="BOOL",
-        help="Set up SSH access from the host to the final image via 'mkosi ssh'",
-        nargs="?",
-        action=action,
-    )
-    group.add_argument(
-        "--credential",
-        metavar="NAME=VALUE",
-        help="Pass a systemd credential to systemd-nspawn or qemu",
-        dest="credentials",
-        action=action,
-    )
-    group.add_argument(
-        "--kernel-command-line-extra",
-        metavar="OPTIONS",
-        help="Append extra entries to the kernel command line when booting the image",
-        action=action,
-    )
-    group.add_argument(
-        "--acl",
-        metavar="BOOL",
-        help="Set ACLs on generated directories to permit the user running mkosi to remove them",
-        nargs="?",
-        action=action,
-    )
-
-    try:
-        import argcomplete
-
-        argcomplete.autocomplete(parser)
-    except ImportError:
-        pass
-
-    return parser
-
-
-def parse_args(argv: Optional[Sequence[str]] = None) -> argparse.Namespace:
-    if argv is None:
-        argv = sys.argv[1:]
-    argv = list(argv)  # make a copy 'cause we'll be modifying the list later on
-
-    # Make sure the verb command gets explicitly passed. Insert a -- before the positional verb argument
-    # otherwise it might be considered as an argument of a parameter with nargs='?'. For example mkosi -i
-    # summary would be treated as -i=summary.
-    for verb in Verb:
-        try:
-            v_i = argv.index(verb.name)
-        except ValueError:
-            continue
-
-        if v_i > 0 and argv[v_i - 1] != "--":
-            argv.insert(v_i, "--")
-        break
-    else:
-        argv += ["--", "build"]
-
-    argparser = create_argument_parser()
-    namespace = argparser.parse_args(argv)
-
-    if namespace.verb == Verb.help:
-        argparser.print_help()
-        argparser.exit()
-
-    if "directory" not in namespace:
-        setattr(namespace, "directory", None)
-
-    if namespace.directory and not namespace.directory.is_dir():
-        die(f"Error: {namespace.directory} is not a directory!")
-
-    namespace = MkosiConfigParser(SETTINGS).parse(namespace.directory or Path("."), namespace)
-
-    for s in SETTINGS:
-        if s.dest in namespace:
-            continue
-
-        if s.default_factory:
-            default = s.default_factory(namespace)
-        elif s.default is None:
-            default = s.parse(s.dest, None, namespace)
-        else:
-            default = s.default
-
-        setattr(namespace, s.dest, default)
-
-    return namespace
-
-
 def empty_directory(path: Path) -> None:
     try:
         for f in os.listdir(path):
@@ -3583,9 +2539,7 @@ def needs_build(config: Union[argparse.Namespace, MkosiConfig]) -> bool:
     return config.verb == Verb.build or (config.verb in MKOSI_COMMANDS_NEED_BUILD and (not config.output.exists() or config.force > 0))
 
 
-def run_verb(args: argparse.Namespace) -> None:
-    config = load_args(args)
-
+def run_verb(config: MkosiConfig) -> None:
     with prepend_to_environ_path(config.extra_search_paths):
         if config.verb == Verb.genkey:
             return generate_secure_boot_key(config)
index af8a69381e1c87399ca09bfbbdbaa07d0922f5a7..60a3ff3511e17e58c6630fea7df893810b400977 100644 (file)
@@ -7,7 +7,8 @@ import sys
 from collections.abc import Iterator
 from subprocess import CalledProcessError
 
-from mkosi import parse_args, run_verb
+from mkosi import load_args, run_verb
+from mkosi.config import MkosiConfigParser
 from mkosi.log import MkosiException, die
 from mkosi.run import excepthook
 
@@ -25,7 +26,7 @@ def propagate_failed_return() -> Iterator[None]:
 
 @propagate_failed_return()
 def main() -> None:
-    args = parse_args()
+    args = MkosiConfigParser().parse()
 
     if args.directory:
         if args.directory.isdir():
@@ -33,7 +34,7 @@ def main() -> None:
         else:
             die(f"Error: {args.directory} is not a directory!")
 
-    run_verb(args)
+    run_verb(load_args(args))
 
 
 if __name__ == "__main__":
index 8a8de5c678f58f1fb57362361c0eaf80c967f589..2cae297d44dd40d89856090fb5f8ff5896319ba0 100644 (file)
@@ -4,12 +4,29 @@ import dataclasses
 import enum
 import fnmatch
 import os
+import platform
+import sys
+import textwrap
 from collections.abc import Sequence
 from pathlib import Path
 from typing import Any, Callable, Optional, Type, Union, cast
 
-from mkosi.backend import Distribution, detect_distribution
-from mkosi.log import die
+from mkosi.backend import (
+    Distribution,
+    ManifestFormat,
+    OutputFormat,
+    Verb,
+    detect_distribution,
+    flatten,
+)
+from mkosi.log import MkosiPrinter, die
+
+__version__ = "14"
+
+
+ConfigParseCallback = Callable[[str, Optional[str], argparse.Namespace], Any]
+ConfigMatchCallback = Callable[[str, str, argparse.Namespace], bool]
+ConfigDefaultCallback = Callable[[argparse.Namespace], Any]
 
 
 def parse_boolean(s: str) -> bool:
@@ -125,9 +142,86 @@ def config_default_release(namespace: argparse.Namespace) -> Any:
     }.get(d, "rolling")
 
 
-ConfigParseCallback = Callable[[str, Optional[str], argparse.Namespace], Any]
-ConfigMatchCallback = Callable[[str, str, argparse.Namespace], bool]
-ConfigDefaultCallback = Callable[[argparse.Namespace], Any]
+def make_enum_parser(type: Type[enum.Enum]) -> Callable[[str], enum.Enum]:
+    def parse_enum(value: str) -> enum.Enum:
+        try:
+            return type[value]
+        except KeyError:
+            die(f"Invalid {type.__name__} value \"{value}\"")
+
+    return parse_enum
+
+
+def config_make_enum_parser(type: Type[enum.Enum]) -> ConfigParseCallback:
+    def config_parse_enum(dest: str, value: Optional[str], namespace: argparse.Namespace) -> Optional[enum.Enum]:
+        if dest in namespace:
+            return getattr(namespace, dest) # type: ignore
+
+        return make_enum_parser(type)(value) if value else None
+
+    return config_parse_enum
+
+
+def config_make_enum_matcher(type: Type[enum.Enum]) -> ConfigMatchCallback:
+    def config_match_enum(dest: str, value: str, namespace: argparse.Namespace) -> bool:
+        return cast(bool, make_enum_parser(type)(value) == getattr(namespace, dest))
+
+    return config_match_enum
+
+
+def config_make_list_parser(delimiter: str, parse: Callable[[str], Any] = str) -> ConfigParseCallback:
+    ignore: set[str] = set()
+
+    def config_parse_list(dest: str, value: Optional[str], namespace: argparse.Namespace) -> list[Any]:
+        if dest not in namespace:
+            ignore.clear()
+            l = []
+        else:
+            l = getattr(namespace, dest).copy()
+
+        if not value:
+            return l # type: ignore
+
+        value = value.replace("\n", delimiter)
+        values = [v for v in value.split(delimiter) if v]
+
+        for v in values:
+            if v.startswith("!"):
+                ignore.add(v[1:])
+                continue
+
+            for i in ignore:
+                if fnmatch.fnmatchcase(v, i):
+                    break
+            else:
+                l.insert(0, parse(v))
+
+        return l
+
+    return config_parse_list
+
+
+def make_path_parser(required: bool) -> Callable[[str], Path]:
+    def parse_path(value: str) -> Path:
+        if required and not Path(value).exists():
+            die(f"{value} does not exist")
+
+        return Path(value)
+
+    return parse_path
+
+
+def config_make_path_parser(required: bool) -> ConfigParseCallback:
+    def config_parse_path(dest: str, value: Optional[str], namespace: argparse.Namespace) -> Optional[Path]:
+        if dest in namespace:
+            return getattr(namespace, dest) # type: ignore
+
+        if value and required and not Path(value).exists():
+            die(f"{value} does not exist")
+
+        return Path(value) if value else None
+
+    return config_parse_path
 
 
 @dataclasses.dataclass(frozen=True)
@@ -146,12 +240,460 @@ class MkosiConfigSetting:
             object.__setattr__(self, 'name', ''.join(x.capitalize() for x in self.dest.split('_') if x))
 
 
-class MkosiConfigParser:
-    def __init__(self, settings: Sequence[MkosiConfigSetting]) -> None:
-        self.settings = settings
-        self.lookup = {s.name: s for s in settings}
+class CustomHelpFormatter(argparse.HelpFormatter):
+    def _format_action_invocation(self, action: argparse.Action) -> str:
+        if not action.option_strings or action.nargs == 0:
+            return super()._format_action_invocation(action)
+        default = self._get_default_metavar_for_optional(action)
+        args_string = self._format_args(action, default)
+        return ", ".join(action.option_strings) + " " + args_string
+
+    def _split_lines(self, text: str, width: int) -> list[str]:
+        """Wraps text to width, each line separately.
+        If the first line of text ends in a colon, we assume that
+        this is a list of option descriptions, and subindent them.
+        Otherwise, the text is wrapped without indentation.
+        """
+        lines = text.splitlines()
+        subindent = '    ' if lines[0].endswith(':') else ''
+        return flatten(textwrap.wrap(line, width, break_long_words=False, break_on_hyphens=False,
+                                     subsequent_indent=subindent) for line in lines)
+
+
+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(self.dest, values, namespace))
+            else:
+                for v in values:
+                    assert isinstance(v, str)
+                    setattr(namespace, s.dest, s.parse(self.dest, v, namespace))
+
+    return MkosiAction
+
 
-    def parse(self, path: Path, namespace: argparse.Namespace) -> argparse.Namespace:
+class MkosiConfigParser:
+    SETTINGS = (
+        MkosiConfigSetting(
+            dest="distribution",
+            section="Distribution",
+            parse=config_make_enum_parser(Distribution),
+            match=config_make_enum_matcher(Distribution),
+            default=detect_distribution()[0],
+        ),
+        MkosiConfigSetting(
+            dest="release",
+            section="Distribution",
+            parse=config_parse_string,
+            match=config_match_string,
+            default_factory=config_default_release,
+        ),
+        MkosiConfigSetting(
+            dest="architecture",
+            section="Distribution",
+            default=platform.machine(),
+        ),
+        MkosiConfigSetting(
+            dest="mirror",
+            section="Distribution",
+        ),
+        MkosiConfigSetting(
+            dest="local_mirror",
+            section="Distribution",
+        ),
+        MkosiConfigSetting(
+            dest="repository_key_check",
+            section="Distribution",
+            default=True,
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="repositories",
+            section="Distribution",
+            parse=config_make_list_parser(delimiter=","),
+        ),
+        MkosiConfigSetting(
+            dest="repo_dirs",
+            name="RepositoryDirectories",
+            section="Distribution",
+            parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=True)),
+            paths=("mkosi.reposdir",),
+        ),
+        MkosiConfigSetting(
+            dest="output_format",
+            name="Format",
+            section="Output",
+            parse=config_make_enum_parser(OutputFormat),
+            default=OutputFormat.disk,
+        ),
+        MkosiConfigSetting(
+            dest="manifest_format",
+            section="Output",
+            parse=config_make_list_parser(delimiter=",", parse=make_enum_parser(ManifestFormat)),
+            default=[ManifestFormat.json],
+        ),
+        MkosiConfigSetting(
+            dest="output",
+            section="Output",
+            parse=config_make_path_parser(required=False),
+        ),
+        MkosiConfigSetting(
+            dest="output_dir",
+            name="OutputDirectory",
+            section="Output",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.output",),
+        ),
+        MkosiConfigSetting(
+            dest="workspace_dir",
+            name="WorkspaceDirectory",
+            section="Output",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.workspace",),
+        ),
+        MkosiConfigSetting(
+            dest="bootable",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="kernel_command_line",
+            section="Output",
+            parse=config_make_list_parser(delimiter=" "),
+        ),
+        MkosiConfigSetting(
+            dest="secure_boot",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="secure_boot_key",
+            section="Output",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.secure-boot.key",),
+        ),
+        MkosiConfigSetting(
+            dest="secure_boot_certificate",
+            section="Output",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.secure-boot.crt",),
+        ),
+        MkosiConfigSetting(
+            dest="secure_boot_valid_days",
+            section="Output",
+            default="730",
+        ),
+        MkosiConfigSetting(
+            dest="secure_boot_common_name",
+            section="Output",
+            default="mkosi of %u",
+        ),
+        MkosiConfigSetting(
+            dest="sign_expected_pcr",
+            section="Output",
+            parse=config_parse_feature,
+        ),
+        MkosiConfigSetting(
+            dest="passphrase",
+            section="Output",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.passphrase",),
+        ),
+        MkosiConfigSetting(
+            dest="compress_output",
+            section="Output",
+            parse=config_parse_compression,
+        ),
+        MkosiConfigSetting(
+            dest="hostname",
+            section="Output",
+        ),
+        MkosiConfigSetting(
+            dest="image_version",
+            section="Output",
+        ),
+        MkosiConfigSetting(
+            dest="image_id",
+            section="Output",
+        ),
+        MkosiConfigSetting(
+            dest="auto_bump",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="tar_strip_selinux_context",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="incremental",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="cache_initrd",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="split_artifacts",
+            section="Output",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="repart_dirs",
+            name="RepartDirectories",
+            section="Output",
+            parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=True)),
+            paths=("mkosi.repart",),
+        ),
+        MkosiConfigSetting(
+            dest="initrds",
+            section="Output",
+            parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=False)),
+        ),
+        MkosiConfigSetting(
+            dest="base_packages",
+            section="Content",
+            parse=config_parse_base_packages,
+            default=True,
+        ),
+        MkosiConfigSetting(
+            dest="packages",
+            section="Content",
+            parse=config_make_list_parser(delimiter=","),
+        ),
+        MkosiConfigSetting(
+            dest="remove_packages",
+            section="Content",
+            parse=config_make_list_parser(delimiter=","),
+        ),
+        MkosiConfigSetting(
+            dest="with_docs",
+            section="Content",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="with_tests",
+            section="Content",
+            parse=config_parse_boolean,
+            default=True,
+        ),
+        MkosiConfigSetting(
+            dest="password",
+            section="Content",
+        ),
+        MkosiConfigSetting(
+            dest="password_is_hashed",
+            section="Content",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="autologin",
+            section="Content",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="cache_dir",
+            name="CacheDirectory",
+            section="Content",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.cache",),
+        ),
+        MkosiConfigSetting(
+            dest="extra_trees",
+            section="Content",
+            parse=config_make_list_parser(delimiter=",", parse=parse_source_target_paths),
+            paths=("mkosi.extra", "mkosi.extra.tar"),
+        ),
+        MkosiConfigSetting(
+            dest="skeleton_trees",
+            section="Content",
+            parse=config_make_list_parser(delimiter=",", parse=parse_source_target_paths),
+            paths=("mkosi.skeleton", "mkosi.skeleton.tar"),
+        ),
+        MkosiConfigSetting(
+            dest="clean_package_metadata",
+            section="Content",
+            parse=config_parse_feature,
+        ),
+        MkosiConfigSetting(
+            dest="remove_files",
+            section="Content",
+            parse=config_make_list_parser(delimiter=","),
+        ),
+        MkosiConfigSetting(
+            dest="environment",
+            section="Content",
+            parse=config_make_list_parser(delimiter=" "),
+        ),
+        MkosiConfigSetting(
+            dest="build_sources",
+            section="Content",
+            parse=config_make_path_parser(required=True),
+            default=".",
+        ),
+        MkosiConfigSetting(
+            dest="build_dir",
+            name="BuildDirectory",
+            section="Content",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.builddir",),
+        ),
+        MkosiConfigSetting(
+            dest="install_dir",
+            name="InstallDirectory",
+            section="Content",
+            parse=config_make_path_parser(required=False),
+            paths=("mkosi.installdir",),
+        ),
+        MkosiConfigSetting(
+            dest="build_packages",
+            section="Content",
+            parse=config_make_list_parser(delimiter=","),
+        ),
+        MkosiConfigSetting(
+            dest="build_script",
+            section="Content",
+            parse=config_parse_script,
+            paths=("mkosi.build",),
+        ),
+        MkosiConfigSetting(
+            dest="prepare_script",
+            section="Content",
+            parse=config_parse_script,
+            paths=("mkosi.prepare",),
+        ),
+        MkosiConfigSetting(
+            dest="postinst_script",
+            name="PostInstallationScript",
+            section="Content",
+            parse=config_parse_script,
+            paths=("mkosi.postinst",),
+        ),
+        MkosiConfigSetting(
+            dest="finalize_script",
+            section="Content",
+            parse=config_parse_script,
+            paths=("mkosi.finalize",),
+        ),
+        MkosiConfigSetting(
+            dest="with_network",
+            section="Content",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="cache_only",
+            section="Content",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="nspawn_settings",
+            name="NSpawnSettings",
+            section="Content",
+            parse=config_make_path_parser(required=True),
+            paths=("mkosi.nspawn",),
+        ),
+        MkosiConfigSetting(
+            dest="base_image",
+            section="Content",
+            parse=config_make_path_parser(required=True),
+        ),
+        MkosiConfigSetting(
+            dest="checksum",
+            section="Validation",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="sign",
+            section="Validation",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="key",
+            section="Validation",
+        ),
+        MkosiConfigSetting(
+            dest="extra_search_paths",
+            section="Host",
+            parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=True)),
+        ),
+        MkosiConfigSetting(
+            dest="qemu_gui",
+            section="Host",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="qemu_smp",
+            section="Host",
+            default="1",
+        ),
+        MkosiConfigSetting(
+            dest="qemu_mem",
+            section="Host",
+            default="2G",
+        ),
+        MkosiConfigSetting(
+            dest="qemu_kvm",
+            section="Host",
+            parse=config_parse_feature,
+        ),
+        MkosiConfigSetting(
+            dest="qemu_args",
+            section="Host",
+            parse=config_make_list_parser(delimiter=" "),
+        ),
+        MkosiConfigSetting(
+            dest="ephemeral",
+            section="Host",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="ssh",
+            section="Host",
+            parse=config_parse_boolean,
+        ),
+        MkosiConfigSetting(
+            dest="credentials",
+            section="Host",
+            parse=config_make_list_parser(delimiter=" "),
+        ),
+        MkosiConfigSetting(
+            dest="kernel_command_line_extra",
+            section="Host",
+            parse=config_make_list_parser(delimiter=" "),
+        ),
+        MkosiConfigSetting(
+            dest="acl",
+            section="Host",
+            parse=config_parse_boolean,
+        ),
+    )
+
+    def __init__(self) -> None:
+        self.lookup = {s.name: s for s in self.SETTINGS}
+
+    def parse_config(self, path: Path, namespace: argparse.Namespace) -> argparse.Namespace:
         extras = path.is_dir()
 
         if path.is_dir():
@@ -208,124 +750,604 @@ class MkosiConfigParser:
             if path.parent.joinpath("mkosi.conf.d").exists():
                 for p in sorted(path.parent.joinpath("mkosi.conf.d").iterdir()):
                     if p.is_dir() or p.suffix == ".conf":
-                        namespace = self.parse(p, namespace)
+                        namespace = self.parse_config(p, namespace)
 
-            for s in self.settings:
+            for s in self.SETTINGS:
                 for f in s.paths:
                     if path.parent.joinpath(f).exists():
                         setattr(namespace, s.dest, s.parse(s.dest, str(path.parent / f), namespace))
 
         return namespace
 
+    def create_argument_parser(self) -> argparse.ArgumentParser:
+        action = config_make_action(self.SETTINGS)
+
+        parser = argparse.ArgumentParser(
+            prog="mkosi",
+            description="Build Bespoke OS Images",
+            usage=textwrap.dedent("""
+                    mkosi [options...] {b}summary{e}
+                    mkosi [options...] {b}build{e} [script parameters...]
+                    mkosi [options...] {b}shell{e} [command line...]
+                    mkosi [options...] {b}boot{e} [nspawn settings...]
+                    mkosi [options...] {b}qemu{e} [qemu parameters...]
+                    mkosi [options...] {b}ssh{e} [command line...]
+                    mkosi [options...] {b}clean{e}
+                    mkosi [options...] {b}serve{e}
+                    mkosi [options...] {b}bump{e}
+                    mkosi [options...] {b}genkey{e}
+                    mkosi [options...] {b}help{e}
+                    mkosi -h | --help
+                    mkosi --version
+            """).format(b=MkosiPrinter.bold, e=MkosiPrinter.reset),
+            add_help=False,
+            allow_abbrev=False,
+            argument_default=argparse.SUPPRESS,
+            formatter_class=CustomHelpFormatter,
+        )
 
-def config_make_action(settings: Sequence[MkosiConfigSetting]) -> Type[argparse.Action]:
-    lookup = {s.dest: s for s in settings}
+        parser.add_argument(
+            "verb",
+            type=Verb,
+            choices=list(Verb),
+            default=Verb.build,
+            help=argparse.SUPPRESS,
+        )
+        parser.add_argument(
+            "cmdline",
+            nargs=argparse.REMAINDER,
+            help=argparse.SUPPRESS,
+        )
+        parser.add_argument(
+            "-h", "--help",
+            action="help",
+            help=argparse.SUPPRESS,
+        )
+        parser.add_argument(
+            "--version",
+            action="version",
+            version="%(prog)s " + __version__,
+            help=argparse.SUPPRESS,
+        )
+        parser.add_argument(
+            "-f", "--force",
+            action="count",
+            dest="force",
+            default=0,
+            help="Remove existing image file before operation",
+        )
+        parser.add_argument(
+            "-C", "--directory",
+            help="Change to specified directory before doing anything",
+            type=Path,
+            metavar="PATH",
+        )
+        parser.add_argument(
+            "--debug",
+            help="Turn on debugging output",
+            action="append",
+            default=[],
+        )
 
-    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"
+        group = parser.add_argument_group("Distribution options")
+        group.add_argument(
+            "-d", "--distribution",
+            choices=Distribution.__members__,
+            help="Distribution to install",
+            action=action,
+        )
+        group.add_argument(
+            "-r", "--release",
+            metavar="RELEASE",
+            help="Distribution release to install",
+            action=action,
+        )
+        group.add_argument(
+            "--architecture",
+            metavar="ARCHITECTURE",
+            help="Override the architecture of installation",
+            action=action,
+        )
+        group.add_argument(
+            "-m", "--mirror",
+            metavar="MIRROR",
+            help="Distribution mirror to use",
+            action=action,
+        )
+        group.add_argument(
+            "--local-mirror",
+            help="Use a single local, flat and plain mirror to build the image",
+            action=action,
+        )
+        group.add_argument(
+            "--repository-key-check",
+            metavar="BOOL",
+            help="Controls signature and key checks on repositories",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--repositories",
+            metavar="REPOS",
+            help="Repositories to use",
+            action=action,
+        )
+        group.add_argument(
+            "--repo-dir",
+            metavar="PATH",
+            help="Specify a directory containing extra distribution specific repository files",
+            dest="repo_dirs",
+            action=action,
+        )
 
-            try:
-                s = lookup[self.dest]
-            except KeyError:
-                die(f"Unknown setting {option_string}")
+        group = parser.add_argument_group("Output options")
+        group.add_argument(
+            "-t", "--format",
+            metavar="FORMAT",
+            choices=OutputFormat.__members__,
+            dest="output_format",
+            help="Output Format",
+            action=action,
+        )
+        group.add_argument(
+            "--manifest-format",
+            metavar="FORMAT",
+            help="Manifest Format",
+            action=action,
+        )
+        group.add_argument(
+            "-o", "--output",
+            metavar="PATH",
+            help="Output image path",
+            action=action,
+        )
+        group.add_argument(
+            "-O", "--output-dir",
+            metavar="DIR",
+            help="Output root directory",
+            action=action,
+        )
+        group.add_argument(
+            "--workspace-dir",
+            metavar="DIR",
+            help="Workspace directory",
+            action=action,
+        )
+        group.add_argument(
+            "-b", "--bootable",
+            metavar="BOOL",
+            help="Make image bootable on EFI",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--kernel-command-line",
+            metavar="OPTIONS",
+            help="Set the kernel command line (only bootable images)",
+            action=action,
+        )
+        group.add_argument(
+            "--secure-boot",
+            metavar="BOOL",
+            help="Sign the resulting kernel/initrd image for UEFI SecureBoot",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--secure-boot-key",
+            metavar="PATH",
+            help="UEFI SecureBoot private key in PEM format",
+            action=action,
+        )
+        group.add_argument(
+            "--secure-boot-certificate",
+            metavar="PATH",
+            help="UEFI SecureBoot certificate in X509 format",
+            action=action,
+        )
+        group.add_argument(
+            "--secure-boot-valid-days",
+            metavar="DAYS",
+            help="Number of days UEFI SecureBoot keys should be valid when generating keys",
+            action=action,
+        )
+        group.add_argument(
+            "--secure-boot-common-name",
+            metavar="CN",
+            help="Template for the UEFI SecureBoot CN when generating keys",
+            action=action,
+        )
+        group.add_argument(
+            "--sign-expected-pcr",
+            metavar="FEATURE",
+            help="Measure the components of the unified kernel image (UKI) and embed the PCR signature into the UKI",
+            action=action,
+        )
+        group.add_argument(
+            "--passphrase",
+            metavar="PATH",
+            help="Path to a file containing the passphrase to use when LUKS encryption is selected",
+            action=action,
+        )
+        group.add_argument(
+            "--compress-output",
+            metavar="ALG",
+            help="Enable whole-output compression (with images or archives)",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument("--hostname", help="Set hostname", action=action)
+        group.add_argument("--image-version", help="Set version for image", action=action)
+        group.add_argument("--image-id", help="Set ID for image", action=action)
+        group.add_argument(
+            "-B", "--auto-bump",
+            metavar="BOOL",
+            help="Automatically bump image version after building",
+            action=action,
+        )
+        group.add_argument(
+            "--tar-strip-selinux-context",
+            metavar="BOOL",
+            help="Do not include SELinux file context information in tar. Not compatible with bsdtar.",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "-i", "--incremental",
+            metavar="BOOL",
+            help="Make use of and generate intermediary cache images",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--cache-initrd",
+            metavar="BOOL",
+            help="When using incremental mode, build the initrd in the cache image and don't rebuild it in the final image",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--split-artifacts",
+            metavar="BOOL",
+            help="Generate split partitions",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--repart-dir",
+            metavar="PATH",
+            help="Directory containing systemd-repart partition definitions",
+            dest="repart_dirs",
+            action=action,
+        )
+        group.add_argument(
+            "--initrd",
+            help="Add a user-provided initrd to image",
+            metavar="PATH",
+            dest="initrds",
+            action=action,
+        )
 
-            if values is None or isinstance(values, str):
-                setattr(namespace, s.dest, s.parse(self.dest, values, namespace))
-            else:
-                for v in values:
-                    assert isinstance(v, str)
-                    setattr(namespace, s.dest, s.parse(self.dest, v, namespace))
+        group = parser.add_argument_group("Content options")
+        group.add_argument(
+            "--base-packages",
+            metavar="OPTION",
+            help="Automatically inject basic packages in the system (systemd, kernel, …)",
+            action=action,
+        )
+        group.add_argument(
+            "-p", "--package",
+            metavar="PACKAGE",
+            help="Add an additional package to the OS image",
+            dest="packages",
+            action=action,
+        )
+        group.add_argument(
+            "--remove-package",
+            metavar="PACKAGE",
+            help="Remove package from the image OS image after installation",
+            dest="remove_packages",
+            action=action,
+        )
+        group.add_argument(
+            "--with-docs",
+            metavar="BOOL",
+            help="Install documentation",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "-T", "--without-tests",
+            help="Do not run tests as part of build script, if supported",
+            nargs="?",
+            const="no",
+            dest="with_tests",
+            action=action,
+        )
 
-    return MkosiAction
+        group.add_argument("--password", help="Set the root password", action=action)
+        group.add_argument(
+            "--password-is-hashed",
+            metavar="BOOL",
+            help="Indicate that the root password has already been hashed",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--autologin",
+            metavar="BOOL",
+            help="Enable root autologin",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--cache-dir",
+            metavar="PATH",
+            help="Package cache path",
+            action=action,
+        )
+        group.add_argument(
+            "--extra-tree",
+            metavar="PATH",
+            help="Copy an extra tree on top of image",
+            dest="extra_trees",
+            action=action,
+        )
+        group.add_argument(
+            "--skeleton-tree",
+            metavar="PATH",
+            help="Use a skeleton tree to bootstrap the image before installing anything",
+            dest="skeleton_trees",
+            action=action,
+        )
+        group.add_argument(
+            "--clean-package-metadata",
+            metavar="FEATURE",
+            help="Remove package manager database and other files",
+            action=action,
+        )
+        group.add_argument(
+            "--remove-files",
+            metavar="GLOB",
+            help="Remove files from built image",
+            action=action,
+        )
+        group.add_argument(
+            "-E", "--environment",
+            metavar="NAME[=VALUE]",
+            help="Set an environment variable when running scripts",
+            action=action,
+        )
+        group.add_argument(
+            "--build-sources",
+            metavar="PATH",
+            help="Path for sources to build",
+            action=action,
+        )
+        group.add_argument(
+            "--build-dir",
+            metavar="PATH",
+            help="Path to use as persistent build directory",
+            action=action,
+        )
+        group.add_argument(
+            "--install-dir",
+            metavar="PATH",
+            help="Path to use as persistent install directory",
+            action=action,
+        )
+        group.add_argument(
+            "--build-package",
+            metavar="PACKAGE",
+            help="Additional packages needed for build script",
+            dest="build_packages",
+            action=action,
+        )
+        group.add_argument(
+            "--build-script",
+            metavar="PATH",
+            help="Build script to run inside image",
+            action=action,
+        )
+        group.add_argument(
+            "--prepare-script",
+            metavar="PATH",
+            help="Prepare script to run inside the image before it is cached",
+            action=action,
+        )
+        group.add_argument(
+            "--postinst-script",
+            metavar="PATH",
+            help="Postinstall script to run inside image",
+            action=action,
+        )
+        group.add_argument(
+            "--finalize-script",
+            metavar="PATH",
+            help="Postinstall script to run outside image",
+            action=action,
+        )
+        group.add_argument(
+            "--with-network",
+            metavar="BOOL",
+            help="Run build and postinst scripts with network access (instead of private network)",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--cache-only",
+            metavar="BOOL",
+            help="Only use the package cache when installing packages",
+            action=action,
+        )
+        group.add_argument(
+            "--settings",
+            metavar="PATH",
+            help="Add in .nspawn settings file",
+            dest="nspawn_settings",
+            action=action,
+        )
+        group.add_argument(
+            '--base-image',
+            metavar='IMAGE',
+            help='Use the given image as base (e.g. lower sysext layer)',
+            action=action,
+        )
 
+        group = parser.add_argument_group("Validation options")
+        group.add_argument(
+            "--checksum",
+            metavar="BOOL",
+            help="Write SHA256SUMS file",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--sign",
+            help="Write and sign SHA256SUMS file",
+            metavar="BOOL",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument("--key", help="GPG key to use for signing", action=action)
+
+        group = parser.add_argument_group("Host configuration options")
+        group.add_argument(
+            "--extra-search-path",
+            help="List of colon-separated paths to look for programs before looking in PATH",
+            metavar="PATH",
+            dest="extra_search_paths",
+            action=action,
+        )
+        group.add_argument(
+            "--qemu-gui",
+            help="Start QEMU in graphical mode",
+            metavar="BOOL",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--qemu-smp",
+            metavar="SMP",
+            help="Configure guest's SMP settings",
+            action=action,
+        )
+        group.add_argument(
+            "--qemu-mem",
+            metavar="MEM",
+            help="Configure guest's RAM size",
+            action=action,
+        )
+        group.add_argument(
+            "--qemu-kvm",
+            metavar="BOOL",
+            help="Configure whether to use KVM or not",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--qemu-args",
+            metavar="ARGS",
+            # Suppress the command line option because it's already possible to pass qemu args as normal
+            # arguments.
+            help=argparse.SUPPRESS,
+            action=action,
+        )
+        group.add_argument(
+            "--ephemeral",
+            metavar="BOOL",
+            help=('If specified, the container/VM is run with a temporary snapshot of the output '
+                'image that is removed immediately when the container/VM terminates'),
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--ssh",
+            metavar="BOOL",
+            help="Set up SSH access from the host to the final image via 'mkosi ssh'",
+            nargs="?",
+            action=action,
+        )
+        group.add_argument(
+            "--credential",
+            metavar="NAME=VALUE",
+            help="Pass a systemd credential to systemd-nspawn or qemu",
+            dest="credentials",
+            action=action,
+        )
+        group.add_argument(
+            "--kernel-command-line-extra",
+            metavar="OPTIONS",
+            help="Append extra entries to the kernel command line when booting the image",
+            action=action,
+        )
+        group.add_argument(
+            "--acl",
+            metavar="BOOL",
+            help="Set ACLs on generated directories to permit the user running mkosi to remove them",
+            nargs="?",
+            action=action,
+        )
 
-def make_enum_parser(type: Type[enum.Enum]) -> Callable[[str], enum.Enum]:
-    def parse_enum(value: str) -> enum.Enum:
         try:
-            return type[value]
-        except KeyError:
-            die(f"Invalid {type.__name__} value \"{value}\"")
-
-    return parse_enum
+            import argcomplete
 
+            argcomplete.autocomplete(parser)
+        except ImportError:
+            pass
 
-def config_make_enum_parser(type: Type[enum.Enum]) -> ConfigParseCallback:
-    def config_parse_enum(dest: str, value: Optional[str], namespace: argparse.Namespace) -> Optional[enum.Enum]:
-        if dest in namespace:
-            return getattr(namespace, dest) # type: ignore
-
-        return make_enum_parser(type)(value) if value else None
+        return parser
 
-    return config_parse_enum
+    def parse(self, args: Optional[Sequence[str]] = None) -> argparse.Namespace:
+        namespace = argparse.Namespace()
 
+        if args is None:
+            args = sys.argv[1:]
+        args = list(args)
 
-def config_make_enum_matcher(type: Type[enum.Enum]) -> ConfigMatchCallback:
-    def config_match_enum(dest: str, value: str, namespace: argparse.Namespace) -> bool:
-        return cast(bool, make_enum_parser(type)(value) == getattr(namespace, dest))
+        # Make sure the verb command gets explicitly passed. Insert a -- before the positional verb argument
+        # otherwise it might be considered as an argument of a parameter with nargs='?'. For example mkosi -i
+        # summary would be treated as -i=summary.
+        for verb in Verb:
+            try:
+                v_i = args.index(verb.name)
+            except ValueError:
+                continue
 
-    return config_match_enum
+            if v_i > 0 and args[v_i - 1] != "--":
+                args.insert(v_i, "--")
+            break
+        else:
+            args += ["--", "build"]
 
+        argparser = self.create_argument_parser()
+        argparser.parse_args(args, namespace)
 
-def config_make_list_parser(delimiter: str, parse: Callable[[str], Any] = str) -> ConfigParseCallback:
-    ignore: set[str] = set()
+        if namespace.verb == Verb.help:
+            argparser.print_help()
+            argparser.exit()
 
-    def config_parse_list(dest: str, value: Optional[str], namespace: argparse.Namespace) -> list[Any]:
-        if dest not in namespace:
-            ignore.clear()
-            l = []
-        else:
-            l = getattr(namespace, dest).copy()
+        if "directory" not in namespace:
+            setattr(namespace, "directory", None)
 
-        if not value:
-            return l # type: ignore
+        if namespace.directory and not namespace.directory.is_dir():
+            die(f"Error: {namespace.directory} is not a directory!")
 
-        value = value.replace("\n", delimiter)
-        values = [v for v in value.split(delimiter) if v]
+        self.parse_config(namespace.directory or Path("."), namespace)
 
-        for v in values:
-            if v.startswith("!"):
-                ignore.add(v[1:])
+        for s in self.SETTINGS:
+            if s.dest in namespace:
                 continue
 
-            for i in ignore:
-                if fnmatch.fnmatchcase(v, i):
-                    break
+            if s.default_factory:
+                default = s.default_factory(namespace)
+            elif s.default is None:
+                default = s.parse(s.dest, None, namespace)
             else:
-                l.insert(0, parse(v))
-
-        return l
-
-    return config_parse_list
-
-
-def make_path_parser(required: bool) -> Callable[[str], Path]:
-    def parse_path(value: str) -> Path:
-        if required and not Path(value).exists():
-            die(f"{value} does not exist")
-
-        return Path(value)
-
-    return parse_path
-
-
-def config_make_path_parser(required: bool) -> ConfigParseCallback:
-    def config_parse_path(dest: str, value: Optional[str], namespace: argparse.Namespace) -> Optional[Path]:
-        if dest in namespace:
-            return getattr(namespace, dest) # type: ignore
+                default = s.default
 
-        if value and required and not Path(value).exists():
-            die(f"{value} does not exist")
+            setattr(namespace, s.dest, default)
 
-        return Path(value) if value else None
+        return namespace
 
-    return config_parse_path
index 0fe97a2694f8178f313bd2040307ec117806359c..f18cc5b1ddc59b70a35962a91a7e2ff09a5ceb73 100644 (file)
@@ -12,6 +12,7 @@ import pytest
 import mkosi
 from mkosi.backend import Distribution, MkosiConfig, Verb
 from mkosi.log import MkosiException
+from mkosi.config import MkosiConfigParser
 
 
 @contextmanager
@@ -27,7 +28,7 @@ def cd_temp_dir() -> Iterator[None]:
 
 
 def parse(argv: Optional[List[str]] = None) -> MkosiConfig:
-    return mkosi.load_args(mkosi.parse_args(argv))
+    return mkosi.load_args(MkosiConfigParser().parse(argv))
 
 
 def test_parse_load_verb() -> None: