From: Daan De Meyer Date: Fri, 7 Apr 2023 12:39:37 +0000 (+0200) Subject: Move more configuration parsing logic to config.py X-Git-Tag: v15~265^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=1044a19f9490865d78a43a02759258de6b1dff2a;p=thirdparty%2Fmkosi.git Move more configuration parsing logic to config.py --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 095d8f4b2..b91448582 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -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) diff --git a/mkosi/__main__.py b/mkosi/__main__.py index af8a69381..60a3ff351 100644 --- a/mkosi/__main__.py +++ b/mkosi/__main__.py @@ -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__": diff --git a/mkosi/config.py b/mkosi/config.py index 8a8de5c67..2cae297d4 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -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 diff --git a/tests/test_parse_load_args.py b/tests/test_parse_load_args.py index 0fe97a269..f18cc5b1d 100644 --- a/tests/test_parse_load_args.py +++ b/tests/test_parse_load_args.py @@ -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: