From: Daan De Meyer Date: Fri, 1 Sep 2023 09:41:29 +0000 (+0200) Subject: Replace MkosiConfigParser with parse_config() X-Git-Tag: v16~23^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4ded1db8acf319deef1adf124100628dc374d333;p=thirdparty%2Fmkosi.git Replace MkosiConfigParser with parse_config() The only reason that MkosiConfigParser is a class is to store the lookup hashmaps required for configuration parsing. Yet we can easily do that as well by just declaring the functions that need those inline in a function. So let's simplify the configuration parsing interface by replacing MkosiConfigParser with a function parse_config(). --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index 0a8951efe..5686d8b27 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -31,11 +31,11 @@ from mkosi.config import ( ManifestFormat, MkosiArgs, MkosiConfig, - MkosiConfigParser, OutputFormat, SecureBootSignTool, Verb, format_source_target, + parse_config, summary, ) from mkosi.install import add_dropin_config_from_resource @@ -946,7 +946,7 @@ def build_initrd(state: MkosiState) -> Path: ] with complete_step("Building initrd"): - args, presets = MkosiConfigParser().parse(cmdline) + args, presets = parse_config(cmdline) config = presets[0] unlink_output(args, config) build_image(args, config) diff --git a/mkosi/__main__.py b/mkosi/__main__.py index 8442cecc0..9b3e1f446 100644 --- a/mkosi/__main__.py +++ b/mkosi/__main__.py @@ -9,7 +9,7 @@ import sys from collections.abc import Iterator from mkosi import run_verb -from mkosi.config import MkosiConfigParser +from mkosi.config import parse_config from mkosi.log import ARG_DEBUG, log_setup from mkosi.run import ensure_exc_info, run @@ -42,7 +42,7 @@ def propagate_failed_return() -> Iterator[None]: @propagate_failed_return() def main() -> None: log_setup() - args, presets = MkosiConfigParser().parse() + args, presets = parse_config() try: run_verb(args, presets) diff --git a/mkosi/config.py b/mkosi/config.py index 4fb7ae908..9b45dc0d9 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -871,776 +871,1005 @@ def parse_ini(path: Path, only_sections: Sequence[str] = ()) -> Iterator[tuple[s yield section, setting, value -class MkosiConfigParser: - SETTINGS = ( - MkosiConfigSetting( - dest="dependencies", - long="--dependency", - section="Preset", - parse=config_make_list_parser(delimiter=","), - help="Specify other presets that this preset depends on", - ), - MkosiConfigSetting( - dest="distribution", - short="-d", - section="Distribution", - parse=config_make_enum_parser(Distribution), - match=config_make_enum_matcher(Distribution), - default=detect_distribution()[0], - choices=Distribution.values(), - help="Distribution to install", - ), - MkosiConfigSetting( - dest="release", - short="-r", - section="Distribution", - parse=config_parse_string, - match=config_make_string_matcher(), - default_factory=config_default_release, - default_factory_depends=("distribution",), - help="Distribution release to install", - ), - MkosiConfigSetting( - dest="architecture", - section="Distribution", - parse=config_make_enum_parser(Architecture), - default=Architecture.native(), - choices=Architecture.values(), - help="Override the architecture of installation", - ), - MkosiConfigSetting( - dest="mirror", - short="-m", - section="Distribution", - default_factory=config_default_mirror, - default_factory_depends=("distribution", "release", "architecture"), - help="Distribution mirror to use", - ), - MkosiConfigSetting( - dest="local_mirror", - section="Distribution", - help="Use a single local, flat and plain mirror to build the image", - ), - MkosiConfigSetting( - dest="repository_key_check", - metavar="BOOL", - nargs="?", - section="Distribution", - default=True, - parse=config_parse_boolean, - help="Controls signature and key checks on repositories", - ), - MkosiConfigSetting( - dest="repositories", - metavar="REPOS", - section="Distribution", - parse=config_make_list_parser(delimiter=","), - help="Repositories to use", - ), - MkosiConfigSetting( - dest="cache_only", - metavar="BOOL", - section="Distribution", - parse=config_parse_boolean, - help="Only use the package cache when installing packages", - ), - - MkosiConfigSetting( - dest="output_format", - short="-t", - long="--format", - metavar="FORMAT", - name="Format", - section="Output", - parse=config_make_enum_parser(OutputFormat), - default=OutputFormat.disk, - choices=OutputFormat.values(), - help="Output Format", - ), - MkosiConfigSetting( - dest="manifest_format", - metavar="FORMAT", - section="Output", - parse=config_make_list_parser(delimiter=",", parse=make_enum_parser(ManifestFormat)), - help="Manifest Format", - ), - MkosiConfigSetting( - dest="output", - short="-o", - metavar="NAME", - section="Output", - parse=config_parse_filename, - help="Output name", - ), - MkosiConfigSetting( - dest="compress_output", - metavar="ALG", - nargs="?", - section="Output", - parse=config_parse_compression, - default_factory=config_default_compression, - default_factory_depends=("distribution", "release", "output_format"), - help="Enable whole-output compression (with images or archives)", - ), - MkosiConfigSetting( - dest="output_dir", - short="-O", - metavar="DIR", - name="OutputDirectory", - section="Output", - parse=config_make_path_parser(required=False), - paths=("mkosi.output",), - default_factory=lambda _: Path.cwd(), - help="Output directory", - ), - MkosiConfigSetting( - dest="workspace_dir", - metavar="DIR", - name="WorkspaceDirectory", - section="Output", - parse=config_make_path_parser(required=False), - paths=("mkosi.workspace",), - default_factory=lambda _: Path.cwd(), - help="Workspace directory", - ), - MkosiConfigSetting( - dest="cache_dir", - metavar="PATH", - name="CacheDirectory", - section="Output", - parse=config_make_path_parser(required=False), - paths=("mkosi.cache",), - help="Package cache path", - ), - MkosiConfigSetting( - dest="build_dir", - metavar="PATH", - name="BuildDirectory", - section="Output", - parse=config_make_path_parser(required=False), - paths=("mkosi.builddir",), - help="Path to use as persistent build directory", - ), - MkosiConfigSetting( - dest="image_version", - match=config_match_image_version, - section="Output", - help="Set version for image", - paths=("mkosi.version",), - path_read_text=True, - ), - MkosiConfigSetting( - dest="image_id", - match=config_make_string_matcher(allow_globs=True), - section="Output", - help="Set ID for image", - ), - MkosiConfigSetting( - dest="split_artifacts", - metavar="BOOL", - nargs="?", - section="Output", - parse=config_parse_boolean, - help="Generate split partitions", - ), - MkosiConfigSetting( - dest="repart_dirs", - long="--repart-dir", - metavar="PATH", - name="RepartDirectories", - section="Output", - parse=config_make_list_parser(delimiter=",", parse=make_path_parser()), - paths=("mkosi.repart",), - path_default=False, - help="Directory containing systemd-repart partition definitions", - ), - MkosiConfigSetting( - dest="sector_size", - section="Output", - parse=config_parse_string, - help="Set the disk image sector size", - ), - MkosiConfigSetting( - dest="overlay", - metavar="BOOL", - nargs="?", - section="Output", - parse=config_parse_boolean, - help="Only output the additions on top of the given base trees", - ), - MkosiConfigSetting( - dest="use_subvolumes", - metavar="FEATURE", - nargs="?", - section="Output", - parse=config_parse_feature, - help="Use btrfs subvolumes for faster directory operations where possible", - ), - MkosiConfigSetting( - dest="seed", - metavar="UUID", - section="Output", - parse=config_parse_seed, - help="Set the seed for systemd-repart", - ), - - MkosiConfigSetting( - dest="packages", - short="-p", - long="--package", - metavar="PACKAGE", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="Add an additional package to the OS image", - ), - MkosiConfigSetting( - dest="build_packages", - long="--build-package", - metavar="PACKAGE", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="Additional packages needed for build script", - ), - MkosiConfigSetting( - dest="with_docs", - metavar="BOOL", - nargs="?", - section="Content", - parse=config_parse_boolean, - help="Install documentation", - ), - MkosiConfigSetting( - dest="base_trees", - long='--base-tree', - metavar='PATH', - section="Content", - parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=False)), - help='Use the given tree as base tree (e.g. lower sysext layer)', - ), - MkosiConfigSetting( - dest="skeleton_trees", - long="--skeleton-tree", - metavar="PATH", - section="Content", - parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), - paths=("mkosi.skeleton", "mkosi.skeleton.tar"), - path_default=False, - help="Use a skeleton tree to bootstrap the image before installing anything", - ), - MkosiConfigSetting( - dest="package_manager_trees", - long="--package-manager-tree", - metavar="PATH", - section="Content", - parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), - default_factory=lambda ns: ns.skeleton_trees, - default_factory_depends=("skeleton_trees",), - help="Use a package manager tree to configure the package manager", - ), - MkosiConfigSetting( - dest="extra_trees", - long="--extra-tree", - metavar="PATH", - section="Content", - parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), - paths=("mkosi.extra", "mkosi.extra.tar"), - path_default=False, - help="Copy an extra tree on top of image", - ), - MkosiConfigSetting( - dest="remove_packages", - long="--remove-package", - metavar="PACKAGE", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="Remove package from the image OS image after installation", - ), - MkosiConfigSetting( - dest="remove_files", - metavar="GLOB", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="Remove files from built image", - ), - MkosiConfigSetting( - dest="clean_package_metadata", - metavar="FEATURE", - section="Content", - parse=config_parse_feature, - help="Remove package manager database and other files", - ), - MkosiConfigSetting( - dest="source_date_epoch", - metavar="TIMESTAMP", - section="Content", - parse=config_parse_source_date_epoch, - default_factory=config_default_source_date_epoch, - default_factory_depends=("environment",), - help="Set the $SOURCE_DATE_EPOCH timestamp", - ), - MkosiConfigSetting( - dest="prepare_script", - metavar="PATH", - section="Content", - parse=config_parse_script, - paths=("mkosi.prepare",), - help="Prepare script to run inside the image before it is cached", - ), - MkosiConfigSetting( - dest="build_script", - metavar="PATH", - section="Content", - parse=config_parse_script, - paths=("mkosi.build",), - help="Build script to run inside image", - ), - MkosiConfigSetting( - dest="postinst_script", - metavar="PATH", - name="PostInstallationScript", - section="Content", - parse=config_parse_script, - paths=("mkosi.postinst",), - help="Postinstall script to run inside image", - ), - MkosiConfigSetting( - dest="finalize_script", - metavar="PATH", - section="Content", - parse=config_parse_script, - paths=("mkosi.finalize",), - help="Postinstall script to run outside image", - ), - MkosiConfigSetting( - dest="build_sources", - metavar="PATH", - section="Content", - parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser(absolute=False)), - help="Path for sources to build", - ), - MkosiConfigSetting( - dest="environment", - short="-E", - metavar="NAME[=VALUE]", - section="Content", - parse=config_make_list_parser(delimiter=" ", unescape=True), - help="Set an environment variable when running scripts", - ), - MkosiConfigSetting( - dest="with_tests", - short="-T", - long="--without-tests", - nargs="?", - const="no", - section="Content", - parse=config_parse_boolean, - default=True, - help="Do not run tests as part of build script, if supported", - ), - MkosiConfigSetting( - dest="with_network", - metavar="BOOL", - nargs="?", - section="Content", - parse=config_parse_boolean, - help="Run build and postinst scripts with network access (instead of private network)", - ), - MkosiConfigSetting( - dest="bootable", - metavar="FEATURE", - nargs="?", - section="Content", - parse=config_parse_feature, - match=config_match_feature, - help="Generate ESP partition with systemd-boot and UKIs for installed kernels", - ), - MkosiConfigSetting( - dest="bootloader", - metavar="BOOTLOADER", - section="Content", - parse=config_make_enum_parser(Bootloader), - choices=Bootloader.values(), - default=Bootloader.systemd_boot, - help="Specify which UEFI bootloader to use", - ), - MkosiConfigSetting( - dest="bios_bootloader", - metavar="BOOTLOADER", - section="Content", - parse=config_make_enum_parser(BiosBootloader), - choices=BiosBootloader.values(), - default=BiosBootloader.none, - help="Specify which BIOS bootloader to use", - ), - MkosiConfigSetting( - dest="initrds", - long="--initrd", - metavar="PATH", - section="Content", - parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=False)), - help="Add a user-provided initrd to image", - ), - MkosiConfigSetting( - dest="kernel_command_line", - metavar="OPTIONS", - section="Content", - parse=config_make_list_parser(delimiter=" "), - default=["console=ttyS0"], - help="Set the kernel command line (only bootable images)", - ), - MkosiConfigSetting( - dest="kernel_modules_include", - metavar="REGEX", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="Only include the specified kernel modules in the image", - ), - MkosiConfigSetting( - dest="kernel_modules_exclude", - metavar="REGEX", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="Exclude the specified kernel modules from the image", - ), - MkosiConfigSetting( - dest="kernel_modules_initrd", - metavar="BOOL", - nargs="?", - section="Content", - parse=config_parse_boolean, - default=True, - help="When building a bootable image, add an extra initrd containing the kernel modules", - ), - MkosiConfigSetting( - dest="kernel_modules_initrd_include", - metavar="REGEX", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="When building a kernel modules initrd, only include the specified kernel modules", - ), - MkosiConfigSetting( - dest="kernel_modules_initrd_exclude", - metavar="REGEX", - section="Content", - parse=config_make_list_parser(delimiter=","), - help="When building a kernel modules initrd, exclude the specified kernel modules", - ), - MkosiConfigSetting( - dest="locale", - section="Content", - parse=config_parse_string, - help="Set the system locale", - ), - MkosiConfigSetting( - dest="locale_messages", - metavar="LOCALE", - section="Content", - parse=config_parse_string, - help="Set the messages locale", - ), - MkosiConfigSetting( - dest="keymap", - metavar="KEYMAP", - section="Content", - parse=config_parse_string, - help="Set the system keymap", - ), - MkosiConfigSetting( - dest="timezone", - metavar="TIMEZONE", - section="Content", - parse=config_parse_string, - help="Set the system timezone", - ), - MkosiConfigSetting( - dest="hostname", - metavar="HOSTNAME", - section="Content", - parse=config_parse_string, - help="Set the system hostname", - ), - MkosiConfigSetting( - dest="root_password", - metavar="PASSWORD", - section="Content", - parse=config_parse_root_password, - paths=("mkosi.rootpw",), - path_read_text=True, - path_secret=True, - help="Set the password for root", - ), - MkosiConfigSetting( - dest="root_shell", - metavar="SHELL", - section="Content", - parse=config_parse_string, - help="Set the shell for root", - ), - MkosiConfigSetting( - dest="autologin", - metavar="BOOL", - nargs="?", - section="Content", - parse=config_parse_boolean, - help="Enable root autologin", - ), - MkosiConfigSetting( - dest="make_initrd", - metavar="BOOL", - nargs="?", - section="Content", - parse=config_parse_boolean, - help="Make sure the image can be used as an initramfs", - ), - MkosiConfigSetting( - dest="ssh", - metavar="BOOL", - nargs="?", - section="Content", - parse=config_parse_boolean, - help="Set up SSH access from the host to the final image via 'mkosi ssh'", - ), - - MkosiConfigSetting( - dest="secure_boot", - metavar="BOOL", - nargs="?", - section="Validation", - parse=config_parse_boolean, - help="Sign the resulting kernel/initrd image for UEFI SecureBoot", - ), - MkosiConfigSetting( - dest="secure_boot_key", - metavar="PATH", - section="Validation", - parse=config_make_path_parser(), - paths=("mkosi.key",), - help="UEFI SecureBoot private key in PEM format", - ), - MkosiConfigSetting( - dest="secure_boot_certificate", - metavar="PATH", - section="Validation", - parse=config_make_path_parser(), - paths=("mkosi.crt",), - help="UEFI SecureBoot certificate in X509 format", - ), - MkosiConfigSetting( - dest="secure_boot_sign_tool", - metavar="TOOL", - section="Validation", - parse=config_make_enum_parser(SecureBootSignTool), - default=SecureBootSignTool.auto, - choices=SecureBootSignTool.values(), - help="Tool to use for signing PE binaries for secure boot", - ), - MkosiConfigSetting( - dest="verity_key", - metavar="PATH", - section="Validation", - parse=config_make_path_parser(), - paths=("mkosi.key",), - help="Private key for signing verity signature in PEM format", - ), - MkosiConfigSetting( - dest="verity_certificate", - metavar="PATH", - section="Validation", - parse=config_make_path_parser(), - paths=("mkosi.crt",), - help="Certificate for signing verity signature in X509 format", - ), - MkosiConfigSetting( - dest="sign_expected_pcr", - metavar="FEATURE", - section="Validation", - parse=config_parse_feature, - help="Measure the components of the unified kernel image (UKI) and embed the PCR signature into the UKI", - ), - MkosiConfigSetting( - dest="passphrase", - metavar="PATH", - section="Validation", - parse=config_make_path_parser(required=False), - paths=("mkosi.passphrase",), - help="Path to a file containing the passphrase to use when LUKS encryption is selected", - ), - MkosiConfigSetting( - dest="checksum", - metavar="BOOL", - nargs="?", - section="Validation", - parse=config_parse_boolean, - help="Write SHA256SUMS file", - ), - MkosiConfigSetting( - dest="sign", - metavar="BOOL", - nargs="?", - section="Validation", - parse=config_parse_boolean, - help="Write and sign SHA256SUMS file", - ), - MkosiConfigSetting( - dest="key", - section="Validation", - help="GPG key to use for signing", - ), - - MkosiConfigSetting( - dest="incremental", - short="-i", - metavar="BOOL", - nargs="?", - section="Host", - parse=config_parse_boolean, - help="Make use of and generate intermediary cache images", - ), - MkosiConfigSetting( - dest="nspawn_settings", - name="NSpawnSettings", - long="--settings", - metavar="PATH", - section="Host", - parse=config_make_path_parser(), - paths=("mkosi.nspawn",), - help="Add in .nspawn settings file", - ), - MkosiConfigSetting( - dest="extra_search_paths", - long="--extra-search-path", - metavar="PATH", - section="Host", - parse=config_make_list_parser(delimiter=",", parse=make_path_parser()), - help="List of comma-separated paths to look for programs before looking in PATH", - ), - MkosiConfigSetting( - dest="qemu_gui", - metavar="BOOL", - nargs="?", - section="Host", - parse=config_parse_boolean, - help="Start QEMU in graphical mode", - ), - MkosiConfigSetting( - dest="qemu_smp", - metavar="SMP", - section="Host", - default="1", - help="Configure guest's SMP settings", - ), - MkosiConfigSetting( - dest="qemu_mem", - metavar="MEM", - section="Host", - default="2G", - help="Configure guest's RAM size", - ), - MkosiConfigSetting( - dest="qemu_kvm", - metavar="FEATURE", - nargs="?", - section="Host", - parse=config_parse_feature, - help="Configure whether to use KVM or not", - ), - MkosiConfigSetting( - dest="qemu_vsock", - metavar="FEATURE", - nargs="?", - section="Host", - parse=config_parse_feature, - help="Configure whether to use qemu with a vsock or not", - ), - MkosiConfigSetting( - dest="qemu_swtpm", - metavar="FEATURE", - nargs="?", - section="Host", - parse=config_parse_feature, - help="Configure whether to use qemu with swtpm or not", - ), - MkosiConfigSetting( - dest="qemu_cdrom", - metavar="BOOLEAN", - nargs="?", - section="Host", - parse=config_parse_boolean, - help="Attach the image as a CD-ROM to the virtual machine", - ), - MkosiConfigSetting( - dest="qemu_bios", - metavar="BOOLEAN", - nargs="?", - section="Host", - parse=config_parse_boolean, - help="Boot QEMU with SeaBIOS instead of EDK2", - ), - MkosiConfigSetting( - dest="qemu_args", - metavar="ARGS", - section="Host", - parse=config_make_list_parser(delimiter=" "), - # Suppress the command line option because it's already possible to pass qemu args as normal - # arguments. - help=argparse.SUPPRESS, - ), - MkosiConfigSetting( - dest="ephemeral", - metavar="BOOL", - section="Host", - parse=config_parse_boolean, - 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="?", - ), - MkosiConfigSetting( - dest="credentials", - long="--credential", - metavar="NAME=VALUE", - section="Host", - parse=config_make_list_parser(delimiter=" "), - help="Pass a systemd credential to systemd-nspawn or qemu", - ), - MkosiConfigSetting( - dest="kernel_command_line_extra", - metavar="OPTIONS", - section="Host", - parse=config_make_list_parser(delimiter=" "), - help="Append extra entries to the kernel command line when booting the image", - ), - MkosiConfigSetting( - dest="acl", - metavar="BOOL", - nargs="?", - section="Host", - parse=config_parse_boolean, - help="Set ACLs on generated directories to permit the user running mkosi to remove them", - ), - MkosiConfigSetting( - dest="tools_tree", - long="--tools-tree", - metavar="PATH", - section="Host", - parse=config_make_path_parser(required=False, absolute=False), - paths=("mkosi.tools",), - help="Look up programs to execute inside the given tree", - ), +SETTINGS = ( + MkosiConfigSetting( + dest="dependencies", + long="--dependency", + section="Preset", + parse=config_make_list_parser(delimiter=","), + help="Specify other presets that this preset depends on", + ), + MkosiConfigSetting( + dest="distribution", + short="-d", + section="Distribution", + parse=config_make_enum_parser(Distribution), + match=config_make_enum_matcher(Distribution), + default=detect_distribution()[0], + choices=Distribution.values(), + help="Distribution to install", + ), + MkosiConfigSetting( + dest="release", + short="-r", + section="Distribution", + parse=config_parse_string, + match=config_make_string_matcher(), + default_factory=config_default_release, + default_factory_depends=("distribution",), + help="Distribution release to install", + ), + MkosiConfigSetting( + dest="architecture", + section="Distribution", + parse=config_make_enum_parser(Architecture), + default=Architecture.native(), + choices=Architecture.values(), + help="Override the architecture of installation", + ), + MkosiConfigSetting( + dest="mirror", + short="-m", + section="Distribution", + default_factory=config_default_mirror, + default_factory_depends=("distribution", "release", "architecture"), + help="Distribution mirror to use", + ), + MkosiConfigSetting( + dest="local_mirror", + section="Distribution", + help="Use a single local, flat and plain mirror to build the image", + ), + MkosiConfigSetting( + dest="repository_key_check", + metavar="BOOL", + nargs="?", + section="Distribution", + default=True, + parse=config_parse_boolean, + help="Controls signature and key checks on repositories", + ), + MkosiConfigSetting( + dest="repositories", + metavar="REPOS", + section="Distribution", + parse=config_make_list_parser(delimiter=","), + help="Repositories to use", + ), + MkosiConfigSetting( + dest="cache_only", + metavar="BOOL", + section="Distribution", + parse=config_parse_boolean, + help="Only use the package cache when installing packages", + ), + + MkosiConfigSetting( + dest="output_format", + short="-t", + long="--format", + metavar="FORMAT", + name="Format", + section="Output", + parse=config_make_enum_parser(OutputFormat), + default=OutputFormat.disk, + choices=OutputFormat.values(), + help="Output Format", + ), + MkosiConfigSetting( + dest="manifest_format", + metavar="FORMAT", + section="Output", + parse=config_make_list_parser(delimiter=",", parse=make_enum_parser(ManifestFormat)), + help="Manifest Format", + ), + MkosiConfigSetting( + dest="output", + short="-o", + metavar="NAME", + section="Output", + parse=config_parse_filename, + help="Output name", + ), + MkosiConfigSetting( + dest="compress_output", + metavar="ALG", + nargs="?", + section="Output", + parse=config_parse_compression, + default_factory=config_default_compression, + default_factory_depends=("distribution", "release", "output_format"), + help="Enable whole-output compression (with images or archives)", + ), + MkosiConfigSetting( + dest="output_dir", + short="-O", + metavar="DIR", + name="OutputDirectory", + section="Output", + parse=config_make_path_parser(required=False), + paths=("mkosi.output",), + default_factory=lambda _: Path.cwd(), + help="Output directory", + ), + MkosiConfigSetting( + dest="workspace_dir", + metavar="DIR", + name="WorkspaceDirectory", + section="Output", + parse=config_make_path_parser(required=False), + paths=("mkosi.workspace",), + default_factory=lambda _: Path.cwd(), + help="Workspace directory", + ), + MkosiConfigSetting( + dest="cache_dir", + metavar="PATH", + name="CacheDirectory", + section="Output", + parse=config_make_path_parser(required=False), + paths=("mkosi.cache",), + help="Package cache path", + ), + MkosiConfigSetting( + dest="build_dir", + metavar="PATH", + name="BuildDirectory", + section="Output", + parse=config_make_path_parser(required=False), + paths=("mkosi.builddir",), + help="Path to use as persistent build directory", + ), + MkosiConfigSetting( + dest="image_version", + match=config_match_image_version, + section="Output", + help="Set version for image", + paths=("mkosi.version",), + path_read_text=True, + ), + MkosiConfigSetting( + dest="image_id", + match=config_make_string_matcher(allow_globs=True), + section="Output", + help="Set ID for image", + ), + MkosiConfigSetting( + dest="split_artifacts", + metavar="BOOL", + nargs="?", + section="Output", + parse=config_parse_boolean, + help="Generate split partitions", + ), + MkosiConfigSetting( + dest="repart_dirs", + long="--repart-dir", + metavar="PATH", + name="RepartDirectories", + section="Output", + parse=config_make_list_parser(delimiter=",", parse=make_path_parser()), + paths=("mkosi.repart",), + path_default=False, + help="Directory containing systemd-repart partition definitions", + ), + MkosiConfigSetting( + dest="sector_size", + section="Output", + parse=config_parse_string, + help="Set the disk image sector size", + ), + MkosiConfigSetting( + dest="overlay", + metavar="BOOL", + nargs="?", + section="Output", + parse=config_parse_boolean, + help="Only output the additions on top of the given base trees", + ), + MkosiConfigSetting( + dest="use_subvolumes", + metavar="FEATURE", + nargs="?", + section="Output", + parse=config_parse_feature, + help="Use btrfs subvolumes for faster directory operations where possible", + ), + MkosiConfigSetting( + dest="seed", + metavar="UUID", + section="Output", + parse=config_parse_seed, + help="Set the seed for systemd-repart", + ), + + MkosiConfigSetting( + dest="packages", + short="-p", + long="--package", + metavar="PACKAGE", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="Add an additional package to the OS image", + ), + MkosiConfigSetting( + dest="build_packages", + long="--build-package", + metavar="PACKAGE", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="Additional packages needed for build script", + ), + MkosiConfigSetting( + dest="with_docs", + metavar="BOOL", + nargs="?", + section="Content", + parse=config_parse_boolean, + help="Install documentation", + ), + MkosiConfigSetting( + dest="base_trees", + long='--base-tree', + metavar='PATH', + section="Content", + parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=False)), + help='Use the given tree as base tree (e.g. lower sysext layer)', + ), + MkosiConfigSetting( + dest="skeleton_trees", + long="--skeleton-tree", + metavar="PATH", + section="Content", + parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), + paths=("mkosi.skeleton", "mkosi.skeleton.tar"), + path_default=False, + help="Use a skeleton tree to bootstrap the image before installing anything", + ), + MkosiConfigSetting( + dest="package_manager_trees", + long="--package-manager-tree", + metavar="PATH", + section="Content", + parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), + default_factory=lambda ns: ns.skeleton_trees, + default_factory_depends=("skeleton_trees",), + help="Use a package manager tree to configure the package manager", + ), + MkosiConfigSetting( + dest="extra_trees", + long="--extra-tree", + metavar="PATH", + section="Content", + parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser()), + paths=("mkosi.extra", "mkosi.extra.tar"), + path_default=False, + help="Copy an extra tree on top of image", + ), + MkosiConfigSetting( + dest="remove_packages", + long="--remove-package", + metavar="PACKAGE", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="Remove package from the image OS image after installation", + ), + MkosiConfigSetting( + dest="remove_files", + metavar="GLOB", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="Remove files from built image", + ), + MkosiConfigSetting( + dest="clean_package_metadata", + metavar="FEATURE", + section="Content", + parse=config_parse_feature, + help="Remove package manager database and other files", + ), + MkosiConfigSetting( + dest="source_date_epoch", + metavar="TIMESTAMP", + section="Content", + parse=config_parse_source_date_epoch, + default_factory=config_default_source_date_epoch, + default_factory_depends=("environment",), + help="Set the $SOURCE_DATE_EPOCH timestamp", + ), + MkosiConfigSetting( + dest="prepare_script", + metavar="PATH", + section="Content", + parse=config_parse_script, + paths=("mkosi.prepare",), + help="Prepare script to run inside the image before it is cached", + ), + MkosiConfigSetting( + dest="build_script", + metavar="PATH", + section="Content", + parse=config_parse_script, + paths=("mkosi.build",), + help="Build script to run inside image", + ), + MkosiConfigSetting( + dest="postinst_script", + metavar="PATH", + name="PostInstallationScript", + section="Content", + parse=config_parse_script, + paths=("mkosi.postinst",), + help="Postinstall script to run inside image", + ), + MkosiConfigSetting( + dest="finalize_script", + metavar="PATH", + section="Content", + parse=config_parse_script, + paths=("mkosi.finalize",), + help="Postinstall script to run outside image", + ), + MkosiConfigSetting( + dest="build_sources", + metavar="PATH", + section="Content", + parse=config_make_list_parser(delimiter=",", parse=make_source_target_paths_parser(absolute=False)), + help="Path for sources to build", + ), + MkosiConfigSetting( + dest="environment", + short="-E", + metavar="NAME[=VALUE]", + section="Content", + parse=config_make_list_parser(delimiter=" ", unescape=True), + help="Set an environment variable when running scripts", + ), + MkosiConfigSetting( + dest="with_tests", + short="-T", + long="--without-tests", + nargs="?", + const="no", + section="Content", + parse=config_parse_boolean, + default=True, + help="Do not run tests as part of build script, if supported", + ), + MkosiConfigSetting( + dest="with_network", + metavar="BOOL", + nargs="?", + section="Content", + parse=config_parse_boolean, + help="Run build and postinst scripts with network access (instead of private network)", + ), + MkosiConfigSetting( + dest="bootable", + metavar="FEATURE", + nargs="?", + section="Content", + parse=config_parse_feature, + match=config_match_feature, + help="Generate ESP partition with systemd-boot and UKIs for installed kernels", + ), + MkosiConfigSetting( + dest="bootloader", + metavar="BOOTLOADER", + section="Content", + parse=config_make_enum_parser(Bootloader), + choices=Bootloader.values(), + default=Bootloader.systemd_boot, + help="Specify which UEFI bootloader to use", + ), + MkosiConfigSetting( + dest="bios_bootloader", + metavar="BOOTLOADER", + section="Content", + parse=config_make_enum_parser(BiosBootloader), + choices=BiosBootloader.values(), + default=BiosBootloader.none, + help="Specify which BIOS bootloader to use", + ), + MkosiConfigSetting( + dest="initrds", + long="--initrd", + metavar="PATH", + section="Content", + parse=config_make_list_parser(delimiter=",", parse=make_path_parser(required=False)), + help="Add a user-provided initrd to image", + ), + MkosiConfigSetting( + dest="kernel_command_line", + metavar="OPTIONS", + section="Content", + parse=config_make_list_parser(delimiter=" "), + default=["console=ttyS0"], + help="Set the kernel command line (only bootable images)", + ), + MkosiConfigSetting( + dest="kernel_modules_include", + metavar="REGEX", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="Only include the specified kernel modules in the image", + ), + MkosiConfigSetting( + dest="kernel_modules_exclude", + metavar="REGEX", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="Exclude the specified kernel modules from the image", + ), + MkosiConfigSetting( + dest="kernel_modules_initrd", + metavar="BOOL", + nargs="?", + section="Content", + parse=config_parse_boolean, + default=True, + help="When building a bootable image, add an extra initrd containing the kernel modules", + ), + MkosiConfigSetting( + dest="kernel_modules_initrd_include", + metavar="REGEX", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="When building a kernel modules initrd, only include the specified kernel modules", + ), + MkosiConfigSetting( + dest="kernel_modules_initrd_exclude", + metavar="REGEX", + section="Content", + parse=config_make_list_parser(delimiter=","), + help="When building a kernel modules initrd, exclude the specified kernel modules", + ), + MkosiConfigSetting( + dest="locale", + section="Content", + parse=config_parse_string, + help="Set the system locale", + ), + MkosiConfigSetting( + dest="locale_messages", + metavar="LOCALE", + section="Content", + parse=config_parse_string, + help="Set the messages locale", + ), + MkosiConfigSetting( + dest="keymap", + metavar="KEYMAP", + section="Content", + parse=config_parse_string, + help="Set the system keymap", + ), + MkosiConfigSetting( + dest="timezone", + metavar="TIMEZONE", + section="Content", + parse=config_parse_string, + help="Set the system timezone", + ), + MkosiConfigSetting( + dest="hostname", + metavar="HOSTNAME", + section="Content", + parse=config_parse_string, + help="Set the system hostname", + ), + MkosiConfigSetting( + dest="root_password", + metavar="PASSWORD", + section="Content", + parse=config_parse_root_password, + paths=("mkosi.rootpw",), + path_read_text=True, + path_secret=True, + help="Set the password for root", + ), + MkosiConfigSetting( + dest="root_shell", + metavar="SHELL", + section="Content", + parse=config_parse_string, + help="Set the shell for root", + ), + MkosiConfigSetting( + dest="autologin", + metavar="BOOL", + nargs="?", + section="Content", + parse=config_parse_boolean, + help="Enable root autologin", + ), + MkosiConfigSetting( + dest="make_initrd", + metavar="BOOL", + nargs="?", + section="Content", + parse=config_parse_boolean, + help="Make sure the image can be used as an initramfs", + ), + MkosiConfigSetting( + dest="ssh", + metavar="BOOL", + nargs="?", + section="Content", + parse=config_parse_boolean, + help="Set up SSH access from the host to the final image via 'mkosi ssh'", + ), + + MkosiConfigSetting( + dest="secure_boot", + metavar="BOOL", + nargs="?", + section="Validation", + parse=config_parse_boolean, + help="Sign the resulting kernel/initrd image for UEFI SecureBoot", + ), + MkosiConfigSetting( + dest="secure_boot_key", + metavar="PATH", + section="Validation", + parse=config_make_path_parser(), + paths=("mkosi.key",), + help="UEFI SecureBoot private key in PEM format", + ), + MkosiConfigSetting( + dest="secure_boot_certificate", + metavar="PATH", + section="Validation", + parse=config_make_path_parser(), + paths=("mkosi.crt",), + help="UEFI SecureBoot certificate in X509 format", + ), + MkosiConfigSetting( + dest="secure_boot_sign_tool", + metavar="TOOL", + section="Validation", + parse=config_make_enum_parser(SecureBootSignTool), + default=SecureBootSignTool.auto, + choices=SecureBootSignTool.values(), + help="Tool to use for signing PE binaries for secure boot", + ), + MkosiConfigSetting( + dest="verity_key", + metavar="PATH", + section="Validation", + parse=config_make_path_parser(), + paths=("mkosi.key",), + help="Private key for signing verity signature in PEM format", + ), + MkosiConfigSetting( + dest="verity_certificate", + metavar="PATH", + section="Validation", + parse=config_make_path_parser(), + paths=("mkosi.crt",), + help="Certificate for signing verity signature in X509 format", + ), + MkosiConfigSetting( + dest="sign_expected_pcr", + metavar="FEATURE", + section="Validation", + parse=config_parse_feature, + help="Measure the components of the unified kernel image (UKI) and embed the PCR signature into the UKI", + ), + MkosiConfigSetting( + dest="passphrase", + metavar="PATH", + section="Validation", + parse=config_make_path_parser(required=False), + paths=("mkosi.passphrase",), + help="Path to a file containing the passphrase to use when LUKS encryption is selected", + ), + MkosiConfigSetting( + dest="checksum", + metavar="BOOL", + nargs="?", + section="Validation", + parse=config_parse_boolean, + help="Write SHA256SUMS file", + ), + MkosiConfigSetting( + dest="sign", + metavar="BOOL", + nargs="?", + section="Validation", + parse=config_parse_boolean, + help="Write and sign SHA256SUMS file", + ), + MkosiConfigSetting( + dest="key", + section="Validation", + help="GPG key to use for signing", + ), + + MkosiConfigSetting( + dest="incremental", + short="-i", + metavar="BOOL", + nargs="?", + section="Host", + parse=config_parse_boolean, + help="Make use of and generate intermediary cache images", + ), + MkosiConfigSetting( + dest="nspawn_settings", + name="NSpawnSettings", + long="--settings", + metavar="PATH", + section="Host", + parse=config_make_path_parser(), + paths=("mkosi.nspawn",), + help="Add in .nspawn settings file", + ), + MkosiConfigSetting( + dest="extra_search_paths", + long="--extra-search-path", + metavar="PATH", + section="Host", + parse=config_make_list_parser(delimiter=",", parse=make_path_parser()), + help="List of comma-separated paths to look for programs before looking in PATH", + ), + MkosiConfigSetting( + dest="qemu_gui", + metavar="BOOL", + nargs="?", + section="Host", + parse=config_parse_boolean, + help="Start QEMU in graphical mode", + ), + MkosiConfigSetting( + dest="qemu_smp", + metavar="SMP", + section="Host", + default="1", + help="Configure guest's SMP settings", + ), + MkosiConfigSetting( + dest="qemu_mem", + metavar="MEM", + section="Host", + default="2G", + help="Configure guest's RAM size", + ), + MkosiConfigSetting( + dest="qemu_kvm", + metavar="FEATURE", + nargs="?", + section="Host", + parse=config_parse_feature, + help="Configure whether to use KVM or not", + ), + MkosiConfigSetting( + dest="qemu_vsock", + metavar="FEATURE", + nargs="?", + section="Host", + parse=config_parse_feature, + help="Configure whether to use qemu with a vsock or not", + ), + MkosiConfigSetting( + dest="qemu_swtpm", + metavar="FEATURE", + nargs="?", + section="Host", + parse=config_parse_feature, + help="Configure whether to use qemu with swtpm or not", + ), + MkosiConfigSetting( + dest="qemu_cdrom", + metavar="BOOLEAN", + nargs="?", + section="Host", + parse=config_parse_boolean, + help="Attach the image as a CD-ROM to the virtual machine", + ), + MkosiConfigSetting( + dest="qemu_bios", + metavar="BOOLEAN", + nargs="?", + section="Host", + parse=config_parse_boolean, + help="Boot QEMU with SeaBIOS instead of EDK2", + ), + MkosiConfigSetting( + dest="qemu_args", + metavar="ARGS", + section="Host", + parse=config_make_list_parser(delimiter=" "), + # Suppress the command line option because it's already possible to pass qemu args as normal + # arguments. + help=argparse.SUPPRESS, + ), + MkosiConfigSetting( + dest="ephemeral", + metavar="BOOL", + section="Host", + parse=config_parse_boolean, + 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="?", + ), + MkosiConfigSetting( + dest="credentials", + long="--credential", + metavar="NAME=VALUE", + section="Host", + parse=config_make_list_parser(delimiter=" "), + help="Pass a systemd credential to systemd-nspawn or qemu", + ), + MkosiConfigSetting( + dest="kernel_command_line_extra", + metavar="OPTIONS", + section="Host", + parse=config_make_list_parser(delimiter=" "), + help="Append extra entries to the kernel command line when booting the image", + ), + MkosiConfigSetting( + dest="acl", + metavar="BOOL", + nargs="?", + section="Host", + parse=config_parse_boolean, + help="Set ACLs on generated directories to permit the user running mkosi to remove them", + ), + MkosiConfigSetting( + dest="tools_tree", + long="--tools-tree", + metavar="PATH", + section="Host", + parse=config_make_path_parser(required=False, absolute=False), + paths=("mkosi.tools",), + help="Look up programs to execute inside the given tree", + ), +) + +MATCHES = ( + MkosiMatch( + name="PathExists", + match=match_path_exists, + ), +) + + +def create_argument_parser() -> argparse.ArgumentParser: + action = config_make_action(SETTINGS) + + parser = argparse.ArgumentParser( + prog="mkosi", + description="Build Bespoke OS Images", + usage="\n " + 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}documentation{e} + mkosi [options...] {b}help{e} + mkosi -h | --help + mkosi --version + """).format(b=Style.bold, e=Style.reset), + add_help=False, + allow_abbrev=False, + argument_default=argparse.SUPPRESS, + formatter_class=CustomHelpFormatter, ) - MATCHES = ( - MkosiMatch( - name="PathExists", - match=match_path_exists, - ), + 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=PagerHelpAction, + 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", + metavar="PATH", + default=None, + ) + parser.add_argument( + "--debug", + help="Turn on debugging output", + action="store_true", + default=False, + ) + parser.add_argument( + "--debug-shell", + help="Spawn an interactive shell in the image if a chroot command fails", + action="store_true", + default=False, + ) + parser.add_argument( + "--no-pager", + action="store_false", + dest="pager", + default=True, + help="Enable paging for long output", + ) + parser.add_argument( + "--genkey-valid-days", + metavar="DAYS", + help="Number of days keys should be valid when generating keys", + action=action, + default="730", + ) + parser.add_argument( + "--genkey-common-name", + metavar="CN", + help="Template for the CN when generating keys", + action=action, + default="mkosi of %u", + ) + parser.add_argument( + "-B", "--auto-bump", + help="Automatically bump image version after building", + action="store_true", + default=False, + ) + parser.add_argument( + "--preset", + action="append", + dest="presets", + metavar="PRESET", + default=[], + help="Build the specified presets and their dependencies", + ) + parser.add_argument( + "--doc-format", + help="The format to show documentation in", + default=DocFormat.auto, + type=DocFormat, ) + parser.add_argument( + "--nspawn-keep-unit", + action="store_true", + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--default", + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--cache", + metavar="PATH", + help=argparse.SUPPRESS, + ) + + last_section = None + + for s in SETTINGS: + if s.section != last_section: + group = parser.add_argument_group(f"{s.section} configuration options") + last_section = s.section + + long = s.long if s.long else f"--{s.dest.replace('_', '-')}" + opts = [s.short, long] if s.short else [long] + + group.add_argument( # type: ignore + *opts, + dest=s.dest, + choices=s.choices, + metavar=s.metavar, + nargs=s.nargs, # type: ignore + const=s.const, + help=s.help, + action=action, + ) + + + try: + import argcomplete + + argcomplete.autocomplete(parser) + except ImportError: + pass + + return parser + + +def backward_compat_stubs(namespace: argparse.Namespace) -> None: + # These can be removed once mkosi v15 is available in LTS distros and compatibility with <= v14 + # is no longer needed in build infrastructure (e.g.: OBS). + if getattr(namespace, "nspawn_keep_unit", None): + delattr(namespace, "nspawn_keep_unit") + logging.warning("--nspawn-keep-unit is no longer supported") + + if getattr(namespace, "default", None): + delattr(namespace, "default") + logging.warning("--default is no longer supported") - def __init__(self) -> None: - self.settings_lookup_by_name = {s.name: s for s in self.SETTINGS} - self.settings_lookup_by_dest = {s.dest: s for s in self.SETTINGS} - self.match_lookup = {m.name: m for m in self.MATCHES} + if getattr(namespace, "cache", None): + delattr(namespace, "cache") + logging.warning("--cache is no longer supported") - def match_config(self, path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool: + +def resolve_deps(args: MkosiArgs, presets: Sequence[MkosiConfig]) -> list[MkosiConfig]: + graph = {p.preset: p.dependencies for p in presets} + + if args.presets: + if any((missing := p) not in graph for p in args.presets): + die(f"No preset found with name {missing}") + + deps = set() + queue = [*args.presets] + + while queue: + if (preset := queue.pop(0)) not in deps: + deps.add(preset) + queue.extend(graph[preset]) + + presets = [p for p in presets if p.preset in deps] + + graph = {p.preset: p.dependencies for p in presets} + + try: + order = list(graphlib.TopologicalSorter(graph).static_order()) + except graphlib.CycleError as e: + die(f"Preset dependency cycle detected: {' => '.join(e.args[1])}") + + return sorted(presets, key=lambda p: order.index(p.preset)) + + +def parse_config(argv: Optional[Sequence[str]] = None) -> tuple[MkosiArgs, tuple[MkosiConfig, ...]]: + settings_lookup_by_name = {s.name: s for s in SETTINGS} + settings_lookup_by_dest = {s.dest: s for s in SETTINGS} + match_lookup = {m.name: m for m in MATCHES} + + def finalize_default( + setting: MkosiConfigSetting, + namespace: argparse.Namespace, + defaults: argparse.Namespace + ) -> None: + if setting.dest in namespace: + return + + for d in setting.default_factory_depends: + finalize_default(settings_lookup_by_dest[d], namespace, defaults) + + if setting.dest in defaults: + default = getattr(defaults, setting.dest) + elif setting.default_factory: + default = setting.default_factory(namespace) + elif setting.default is None: + default = setting.parse(None, None) + else: + default = setting.default + + setattr(namespace, setting.dest, default) + + def match_config(path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool: triggered = None # If the config file does not exist, we assume it matches so that we look at the other files in the @@ -1657,18 +1886,18 @@ class MkosiConfigParser: if not v: die("Match value cannot be empty") - if (s := self.settings_lookup_by_name.get(k)): + if (s := settings_lookup_by_name.get(k)): if not s.match: die(f"{k} cannot be used in [Match]") # If we encounter a setting in [Match] that has not been explicitly configured yet, # we assign the default value first so that we can [Match] on default values for # settings. - self.finalize_default(s, namespace, defaults) + finalize_default(s, namespace, defaults) result = s.match(v, getattr(namespace, s.dest)) - elif (m := self.match_lookup.get(k)): + elif (m := match_lookup.get(k)): result = m.match(v) else: die(f"{k} cannot be used in [Match]") @@ -1682,14 +1911,13 @@ class MkosiConfigParser: return triggered is not False - - def parse_config(self, path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool: + def parse_config( path: Path, namespace: argparse.Namespace, defaults: argparse.Namespace) -> bool: extras = path.is_dir() if path.is_dir(): path = path / "mkosi.conf" - if not self.match_config(path, namespace, defaults): + if not match_config(path, namespace, defaults): return False if path.exists(): @@ -1698,13 +1926,13 @@ class MkosiConfigParser: for _, k, v in parse_ini(path, only_sections=["Distribution", "Output", "Content", "Validation", "Host"]): ns = defaults if k.startswith("@") else namespace - if not (s := self.settings_lookup_by_name.get(k.removeprefix("@"))): + if not (s := settings_lookup_by_name.get(k.removeprefix("@"))): die(f"Unknown setting {k}") setattr(ns, s.dest, s.parse(v, getattr(ns, s.dest, None))) if extras: - for s in self.SETTINGS: + for s in SETTINGS: ns = defaults if s.path_default else namespace for f in s.paths: p = parse_path( @@ -1724,321 +1952,93 @@ class MkosiConfigParser: for p in sorted((path.parent / "mkosi.conf.d").iterdir()): if p.is_dir() or p.suffix == ".conf": with chdir(p if p.is_dir() else Path.cwd()): - self.parse_config(p if p.is_file() else Path("."), namespace, defaults) + parse_config(p if p.is_file() else Path("."), namespace, defaults) return True - def create_argument_parser(self) -> argparse.ArgumentParser: - action = config_make_action(self.SETTINGS) - - parser = argparse.ArgumentParser( - prog="mkosi", - description="Build Bespoke OS Images", - usage="\n " + 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}documentation{e} - mkosi [options...] {b}help{e} - mkosi -h | --help - mkosi --version - """).format(b=Style.bold, e=Style.reset), - add_help=False, - allow_abbrev=False, - argument_default=argparse.SUPPRESS, - formatter_class=CustomHelpFormatter, - ) - - 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=PagerHelpAction, - 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", - metavar="PATH", - default=None, - ) - parser.add_argument( - "--debug", - help="Turn on debugging output", - action="store_true", - default=False, - ) - parser.add_argument( - "--debug-shell", - help="Spawn an interactive shell in the image if a chroot command fails", - action="store_true", - default=False, - ) - parser.add_argument( - "--no-pager", - action="store_false", - dest="pager", - default=True, - help="Enable paging for long output", - ) - parser.add_argument( - "--genkey-valid-days", - metavar="DAYS", - help="Number of days keys should be valid when generating keys", - action=action, - default="730", - ) - parser.add_argument( - "--genkey-common-name", - metavar="CN", - help="Template for the CN when generating keys", - action=action, - default="mkosi of %u", - ) - parser.add_argument( - "-B", "--auto-bump", - help="Automatically bump image version after building", - action="store_true", - default=False, - ) - parser.add_argument( - "--preset", - action="append", - dest="presets", - metavar="PRESET", - default=[], - help="Build the specified presets and their dependencies", - ) - parser.add_argument( - "--doc-format", - help="The format to show documentation in", - default=DocFormat.auto, - type=DocFormat, - ) - parser.add_argument( - "--nspawn-keep-unit", - action="store_true", - help=argparse.SUPPRESS, - ) - parser.add_argument( - "--default", - help=argparse.SUPPRESS, - ) - parser.add_argument( - "--cache", - metavar="PATH", - help=argparse.SUPPRESS, - ) - - last_section = None - - for s in self.SETTINGS: - if s.section != last_section: - group = parser.add_argument_group(f"{s.section} configuration options") - last_section = s.section - - long = s.long if s.long else f"--{s.dest.replace('_', '-')}" - opts = [s.short, long] if s.short else [long] - - group.add_argument( # type: ignore - *opts, - dest=s.dest, - choices=s.choices, - metavar=s.metavar, - nargs=s.nargs, # type: ignore - const=s.const, - help=s.help, - action=action, - ) - - - try: - import argcomplete - - argcomplete.autocomplete(parser) - except ImportError: - pass - - return parser - - def backward_compat_stubs(self, namespace: argparse.Namespace) -> None: - # These can be removed once mkosi v15 is available in LTS distros and compatibility with <= v14 - # is no longer needed in build infrastructure (e.g.: OBS). - if getattr(namespace, "nspawn_keep_unit", None): - delattr(namespace, "nspawn_keep_unit") - logging.warning("--nspawn-keep-unit is no longer supported") - - if getattr(namespace, "default", None): - delattr(namespace, "default") - logging.warning("--default is no longer supported") - - if getattr(namespace, "cache", None): - delattr(namespace, "cache") - logging.warning("--cache is no longer supported") - - def resolve_deps(self, args: MkosiArgs, presets: Sequence[MkosiConfig]) -> list[MkosiConfig]: - graph = {p.preset: p.dependencies for p in presets} - - if args.presets: - if any((missing := p) not in graph for p in args.presets): - die(f"No preset found with name {missing}") - - deps = set() - queue = [*args.presets] + def finalize_defaults(namespace: argparse.Namespace, defaults: argparse.Namespace) -> None: + for s in SETTINGS: + finalize_default(s, namespace, defaults) - while queue: - if (preset := queue.pop(0)) not in deps: - deps.add(preset) - queue.extend(graph[preset]) + presets = [] + namespace = argparse.Namespace() + defaults = argparse.Namespace() - presets = [p for p in presets if p.preset in deps] - - graph = {p.preset: p.dependencies for p in presets} + if argv is None: + argv = sys.argv[1:] + argv = list(argv) + # 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: - order = list(graphlib.TopologicalSorter(graph).static_order()) - except graphlib.CycleError as e: - die(f"Preset dependency cycle detected: {' => '.join(e.args[1])}") - - return sorted(presets, key=lambda p: order.index(p.preset)) - - def finalize_default( - self, - setting: MkosiConfigSetting, - namespace: argparse.Namespace, - defaults: argparse.Namespace - ) -> None: - if setting.dest in namespace: - return - - for d in setting.default_factory_depends: - self.finalize_default(self.settings_lookup_by_dest[d], namespace, defaults) - - if setting.dest in defaults: - default = getattr(defaults, setting.dest) - elif setting.default_factory: - default = setting.default_factory(namespace) - elif setting.default is None: - default = setting.parse(None, None) - else: - default = setting.default - - setattr(namespace, setting.dest, default) - - def finalize_defaults(self, namespace: argparse.Namespace, defaults: argparse.Namespace) -> None: - for s in self.SETTINGS: - self.finalize_default(s, namespace, defaults) + v_i = argv.index(verb.name) + except ValueError: + continue - def parse(self, argv: Optional[Sequence[str]] = None) -> tuple[MkosiArgs, tuple[MkosiConfig, ...]]: - presets = [] - namespace = argparse.Namespace() - defaults = argparse.Namespace() + if v_i > 0 and argv[v_i - 1] != "--": + argv.insert(v_i, "--") + break + else: + argv += ["--", "build"] - if argv is None: - argv = sys.argv[1:] - argv = list(argv) + argparser = create_argument_parser() + argparser.parse_args(argv, namespace) - # 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 + args = load_args(namespace) - if v_i > 0 and argv[v_i - 1] != "--": - argv.insert(v_i, "--") - break - else: - argv += ["--", "build"] + if ARG_DEBUG.get(): + logging.getLogger().setLevel(logging.DEBUG) - argparser = self.create_argument_parser() - argparser.parse_args(argv, namespace) + if args.verb == Verb.help: + PagerHelpAction.__call__(None, argparser, namespace) # type: ignore - args = load_args(namespace) + if args.directory and not Path(args.directory).is_dir(): + die(f"{args.directory} is not a directory!") - if ARG_DEBUG.get(): - logging.getLogger().setLevel(logging.DEBUG) + if args.directory: + os.chdir(args.directory) - if args.verb == Verb.help: - PagerHelpAction.__call__(None, argparser, namespace) # type: ignore + if args.directory != "": + parse_config(Path("."), namespace, defaults) - if args.directory and not Path(args.directory).is_dir(): - die(f"{args.directory} is not a directory!") + if Path("mkosi.presets").exists(): + for p in Path("mkosi.presets").iterdir(): + if not p.is_dir() and not p.suffix == ".conf": + continue - if args.directory: - os.chdir(args.directory) + name = p.name.removesuffix(".conf") + if not name: + die(f"{p} is not a valid preset name") - if args.directory != "": - self.parse_config(Path("."), namespace, defaults) + ns_copy = copy.deepcopy(namespace) + defaults_copy = copy.deepcopy(defaults) - if Path("mkosi.presets").exists(): - for p in Path("mkosi.presets").iterdir(): - if not p.is_dir() and not p.suffix == ".conf": + with chdir(p if p.is_dir() else Path.cwd()): + if not parse_config(p if p.is_file() else Path("."), ns_copy, defaults_copy): continue - name = p.name.removesuffix(".conf") - if not name: - die(f"{p} is not a valid preset name") - - ns_copy = copy.deepcopy(namespace) - defaults_copy = copy.deepcopy(defaults) - - with chdir(p if p.is_dir() else Path.cwd()): - if not self.parse_config(p if p.is_file() else Path("."), ns_copy, defaults_copy): - continue - - setattr(ns_copy, "preset", name) - self.finalize_defaults(ns_copy, defaults_copy) - presets += [ns_copy] + setattr(ns_copy, "preset", name) + finalize_defaults(ns_copy, defaults_copy) + presets += [ns_copy] - if not presets: - setattr(namespace, "preset", None) - self.finalize_defaults(namespace, defaults) - presets = [namespace] + if not presets: + setattr(namespace, "preset", None) + finalize_defaults(namespace, defaults) + presets = [namespace] - if not presets: - die("No presets defined in mkosi.presets/") + if not presets: + die("No presets defined in mkosi.presets/") - # Manipulate some old settings to make them work with the new settings, for those typically used in - # infrastructure scripts rather than image-specific configuration. - self.backward_compat_stubs(namespace) + # Manipulate some old settings to make them work with the new settings, for those typically used in + # infrastructure scripts rather than image-specific configuration. + backward_compat_stubs(namespace) - presets = [load_config(ns) for ns in presets] - presets = self.resolve_deps(args, presets) + presets = [load_config(ns) for ns in presets] + presets = resolve_deps(args, presets) - return args, tuple(presets) + return args, tuple(presets) def load_credentials(args: argparse.Namespace) -> dict[str, str]: diff --git a/tests/test_parse_load_args.py b/tests/test_parse_load_args.py index 93c63a605..2463d9744 100644 --- a/tests/test_parse_load_args.py +++ b/tests/test_parse_load_args.py @@ -6,50 +6,46 @@ import operator import tempfile from pathlib import Path import textwrap -from typing import List, Optional +from typing import Optional import pytest -from mkosi.config import Compression, MkosiArgs, MkosiConfig, MkosiConfigParser, Verb +from mkosi.config import Compression, Verb, parse_config from mkosi.distributions import Distribution from mkosi.util import chdir -def parse(argv: Optional[List[str]] = None) -> tuple[MkosiArgs, tuple[MkosiConfig, ...]]: - return MkosiConfigParser().parse(argv) - - def test_parse_load_verb() -> None: with tempfile.TemporaryDirectory() as d, chdir(d): - assert parse(["build"])[0].verb == Verb.build - assert parse(["clean"])[0].verb == Verb.clean + assert parse_config(["build"])[0].verb == Verb.build + assert parse_config(["clean"])[0].verb == Verb.clean with pytest.raises(SystemExit): - parse(["help"]) - assert parse(["genkey"])[0].verb == Verb.genkey - assert parse(["bump"])[0].verb == Verb.bump - assert parse(["serve"])[0].verb == Verb.serve - assert parse(["build"])[0].verb == Verb.build - assert parse(["shell"])[0].verb == Verb.shell - assert parse(["boot"])[0].verb == Verb.boot - assert parse(["qemu"])[0].verb == Verb.qemu + parse_config(["help"]) + assert parse_config(["genkey"])[0].verb == Verb.genkey + assert parse_config(["bump"])[0].verb == Verb.bump + assert parse_config(["serve"])[0].verb == Verb.serve + assert parse_config(["build"])[0].verb == Verb.build + assert parse_config(["shell"])[0].verb == Verb.shell + assert parse_config(["boot"])[0].verb == Verb.boot + assert parse_config(["qemu"])[0].verb == Verb.qemu with pytest.raises(SystemExit): - parse(["invalid"]) + parse_config(["invalid"]) def test_os_distribution() -> None: with tempfile.TemporaryDirectory() as d, chdir(d): for dist in Distribution: - assert parse(["-d", dist.name])[1][0].distribution == dist + assert parse_config(["-d", dist.name])[1][0].distribution == dist with pytest.raises(tuple((argparse.ArgumentError, SystemExit))): - parse(["-d", "invalidDistro"]) + parse_config(["-d", "invalidDistro"]) with pytest.raises(tuple((argparse.ArgumentError, SystemExit))): - parse(["-d"]) + parse_config(["-d"]) for dist in Distribution: config = Path("mkosi.conf") config.write_text(f"[Distribution]\nDistribution={dist}") - assert parse([])[1][0].distribution == dist + assert parse_config([])[1][0].distribution == dist def test_parse_config_files_filter() -> None: @@ -60,12 +56,12 @@ def test_parse_config_files_filter() -> None: (confd / "10-file.conf").write_text("[Content]\nPackages=yes") (confd / "20-file.noconf").write_text("[Content]\nPackages=nope") - assert parse([])[1][0].packages == ["yes"] + assert parse_config([])[1][0].packages == ["yes"] def test_compression() -> None: with tempfile.TemporaryDirectory() as d, chdir(d): - assert parse(["--format", "disk", "--compress-output", "False"])[1][0].compress_output == Compression.none + assert parse_config(["--format", "disk", "--compress-output", "False"])[1][0].compress_output == Compression.none @pytest.mark.parametrize("dist1,dist2", itertools.combinations_with_replacement(Distribution, 2)) @@ -121,7 +117,7 @@ def test_match_distribution(dist1: Distribution, dist2: Distribution) -> None: ) ) - conf = parse([])[1][0] + conf = parse_config([])[1][0] assert "testpkg1" in conf.packages if dist1 == dist2: assert "testpkg2" in conf.packages @@ -186,7 +182,7 @@ def test_match_release(release1: int, release2: int) -> None: ) ) - conf = parse([])[1][0] + conf = parse_config([])[1][0] assert "testpkg1" in conf.packages if release1 == release2: assert "testpkg2" in conf.packages @@ -265,7 +261,7 @@ def test_match_imageid(image1: str, image2: str) -> None: ) ) - conf = parse([])[1][0] + conf = parse_config([])[1][0] assert "testpkg1" in conf.packages if image1 == image2: assert "testpkg2" in conf.packages @@ -343,7 +339,7 @@ def test_match_imageversion(op: str, version: str) -> None: ) ) - conf = parse([])[1][0] + conf = parse_config([])[1][0] assert ("testpkg1" in conf.packages) == opfunc(123, version) assert ("testpkg2" in conf.packages) == opfunc(123, version) assert "testpkg3" not in conf.packages @@ -365,7 +361,7 @@ def test_package_manager_tree(skel: Optional[Path], pkgmngr: Optional[Path]) -> if pkgmngr is not None: f.write(f"PackageManagerTrees={pkgmngr}\n") - conf = parse([])[1][0] + conf = parse_config([])[1][0] skel_expected = [(skel, None)] if skel is not None else [] pkgmngr_expected = [(pkgmngr, None)] if pkgmngr is not None else skel_expected