From f1b5259b968bf8648e3ca9ebff49b2af4b2f7b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?= Date: Wed, 19 Mar 2025 21:44:53 +0100 Subject: [PATCH] Add typer cli interface In addition to docopt also support the typer commandline interface. This is relevant for kiwi versions that has moved to typer. OSInside/kiwi#2751 --- kiwi_boxed_plugin/cli.py | 271 ++++++++++++++++++ kiwi_boxed_plugin/tasks/system_boxbuild.py | 45 +-- .../python-kiwi_boxed_plugin-spec-template | 7 +- pyproject.toml | 2 + test/unit/.coveragerc | 2 + test/unit/tasks/system_boxbuild_test.py | 30 ++ 6 files changed, 336 insertions(+), 21 deletions(-) create mode 100644 kiwi_boxed_plugin/cli.py diff --git a/kiwi_boxed_plugin/cli.py b/kiwi_boxed_plugin/cli.py new file mode 100644 index 0000000..55b8687 --- /dev/null +++ b/kiwi_boxed_plugin/cli.py @@ -0,0 +1,271 @@ +# Copyright (c) 2024 SUSE LLC. All rights reserved. +# +# This file is part of kiwi-boxed-build. +# +# kiwi-boxed-build is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# kiwi-boxed-build is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with kiwi-boxed-build. If not, see +# +import typer +import itertools +from pathlib import Path +from typing import ( + Annotated, Optional, List, Union, no_type_check +) + +typers = { + 'boxbuild': typer.Typer( + add_completion=False, invoke_without_command=True + ) +} + +system = typers['boxbuild'] + + +@no_type_check +@system.command( + context_settings={ + 'allow_extra_args': True, + 'ignore_unknown_options': True + } +) +def kiwi( + ctx: typer.Context +): + """ + List of command parameters as supported by the kiwi-ng + build command. The information given here is passed + along to the kiwi-ng system build command running in + the virtual machine or container. + """ + Cli = ctx.obj + args = ctx.args + for option in list(set(args)): + if type(option) is not str or not option.startswith('-'): + continue + k: List[Union[str, List]] = [option] + v = [] + indexes = [n for n, x in enumerate(args) if x == option] + if len(indexes) > 1: + for index in indexes: + v.append(args[index + 1]) + for index in sorted(indexes, reverse=True): + del args[index + 1] + del args[index] + k.append(v) + args += k + Cli.subcommand_args['boxbuild']['system_build'] = \ + dict(itertools.zip_longest(*[iter(args)] * 2)) + Cli.global_args['command'] = 'boxbuild' + Cli.global_args['system'] = True + Cli.cli_ok = True + + +@system.callback( + help='build a system image in a self contained VM or container', + subcommand_metavar='kiwi [OPTIONS]' +) +def boxbuild( + ctx: typer.Context, + box: Annotated[ + Optional[str], typer.Option( + help=' Name of the box to use for the build process.' + ) + ] = None, + list_boxes: Annotated[ + Optional[bool], typer.Option( + '--list-boxes', + help='show available build boxes.' + ) + ] = False, + box_memory: Annotated[ + Optional[str], typer.Option( + help=' Number of GBs to reserve as main memory ' + 'for the virtual machine. By default 8GB will be used.' + ) + ] = None, + box_console: Annotated[ + Optional[str], typer.Option( + help=' Name of console in the kernel settings ' + 'for the virtual machine. By default set to hvc0.' + ) + ] = None, + box_smp_cpus: Annotated[ + Optional[int], typer.Option( + help=' Number of CPUs to use in the SMP setup. ' + 'By default 4 CPUs will be used.' + ) + ] = 4, + box_debug: Annotated[ + Optional[bool], typer.Option( + '--box-debug', + help='In debug mode the started virtual machine will be kept open.' + ) + ] = False, + container: Annotated[ + Optional[bool], typer.Option( + '--container', + help='Build in container instead of a VM. Options related to ' + 'building in a VM will have no effect.' + ) + ] = False, + kiwi_version: Annotated[ + Optional[str], typer.Option( + help=' Specify a KIWI version to use for ' + 'the build. The referenced KIWI will be fetched from ' + 'pip and replaces the box installed KIWI version. ' + 'Note: If --no-snapshot is used in combination ' + 'with this option, the change of the KIWI version will ' + 'be permanently stored in the used box.' + ) + ] = None, + shared_path: Annotated[ + Optional[Path], typer.Option( + help=' Optional host path to share with the box. ' + 'The same path as it is present on the host will also ' + 'be available inside of the box during build time.' + ) + ] = None, + no_update_check: Annotated[ + Optional[bool], typer.Option( + '--no-update-check', + help='Skip check for available box update. The option ' + 'has no effect if the selected box does not yet exist ' + 'on the host.' + ) + ] = False, + no_snapshot: Annotated[ + Optional[bool], typer.Option( + '--no-snapshot', + help='Run box with snapshot mode switched off. This ' + 'causes the box disk file to be modified by the build ' + 'process and allows to keep a persistent package cache ' + 'as part of the box. The option can be used to increase ' + 'the build performance due to data stored in the box ' + 'which does not have to be reloaded from the network. ' + 'On the contrary this option invalidates the immutable ' + 'box attribute and should be used with care. On update ' + 'of the box all data stored will be wiped. To prevent ' + 'this combine the option with the --no-update-check option.' + ) + ] = False, + no_accel: Annotated[ + Optional[bool], typer.Option( + '--no-accel', + help='Run box without hardware acceleration. By default ' + 'KVM acceleration is activated' + ) + ] = False, + qemu_9p_sharing: Annotated[ + Optional[bool], typer.Option( + '--9p-sharing', + help='Select 9p backend to use for sharing data ' + 'between the host and the box.' + ) + ] = False, + virtiofs_sharing: Annotated[ + Optional[bool], typer.Option( + '--virtiofs-sharing', + help='Select virtiofsd backend to use for sharing data ' + 'between the host and the box.' + ) + ] = False, + sshfs_sharing: Annotated[ + Optional[bool], typer.Option( + '--sshfs-sharing', + help='Select sshfs backend to use for sharing data ' + 'between the host and the box.' + ) + ] = False, + ssh_key: Annotated[ + Optional[str], typer.Option( + help=' Name of ssh key to authorize for ' + 'connection. By default id_rsa is used.' + ) + ] = 'id_rsa', + ssh_port: Annotated[ + Optional[int], typer.Option( + help=' Port number to use to forward the ' + 'guest SSH port to the host By default 10022 is used.' + ) + ] = 10022, + x86_64: Annotated[ + Optional[bool], typer.Option( + '--x86_64', + help='Select box for the x86_64 architecture. If no ' + 'architecture is selected the host architecture is ' + 'used for selecting the box. The selected box ' + 'architecture also specifies the target architecture ' + 'for the image build with that box.' + ) + ] = False, + aarch64: Annotated[ + Optional[bool], typer.Option( + '--aarch64', + help='Select box for the aarch64 architecture. If no ' + 'architecture is selected the host architecture is ' + 'used for selecting the box. The selected box ' + 'architecture also specifies the target architecture ' + 'for the image build with that box.' + ) + ] = False, + machine: Annotated[ + Optional[str], typer.Option( + help=' Machine name used ' + 'by QEMU. By default no specific value is used here ' + 'and qemu selects its default machine type. For cross ' + 'arch builds or for system architectures for which ' + 'QEMU defines no default like for Arm, it is required ' + 'to specify a machine name. If you do not care about ' + 'reproducing the idiosyncrasies of a particular bit ' + 'of hardware, the best option is to use the virt ' + 'machine type.' + ) + ] = None, + cpu: Annotated[ + Optional[str], typer.Option( + help=' CPU type used by QEMU. By default ' + 'the host CPU type is used which is only a good ' + 'selection if the host and the selected box are from ' + 'the same architecture. On cross arch builds it is ' + 'required to specify the CPU emulation the box should use' + ) + ] = None +): + Cli = ctx.obj + Cli.subcommand_args['boxbuild'] = { + '--box': box, + '--list-boxes': list_boxes, + '--box-memory': box_memory, + '--box-console': box_console, + '--box-smp-cpus': f'{box_smp_cpus}', + '--box-debug': box_debug, + '--container': container, + '--kiwi-version': kiwi_version, + '--shared-path': shared_path, + '--no-update-check': no_update_check, + '--no-snapshot': no_snapshot, + '--no-accel': no_accel, + '--9p-sharing': qemu_9p_sharing, + '--virtiofs-sharing': virtiofs_sharing, + '--sshfs-sharing': sshfs_sharing, + '--ssh-key': ssh_key, + '--ssh-port': f'{ssh_port}', + '--x86_64': x86_64, + '--aarch64': aarch64, + '--machine': machine, + '--cpu': cpu, + 'help': False + } + Cli.global_args['command'] = 'boxbuild' + Cli.global_args['system'] = True + Cli.cli_ok = True diff --git a/kiwi_boxed_plugin/tasks/system_boxbuild.py b/kiwi_boxed_plugin/tasks/system_boxbuild.py index e411fc7..18f162e 100644 --- a/kiwi_boxed_plugin/tasks/system_boxbuild.py +++ b/kiwi_boxed_plugin/tasks/system_boxbuild.py @@ -217,27 +217,32 @@ def process(self) -> None: ) def _validate_kiwi_build_command(self) -> List[str]: - # construct build command from given command line - kiwi_build_command = [ - 'system', 'build' - ] - kiwi_build_command += self.command_args.get( - '' - ) - if '--' in kiwi_build_command: - kiwi_build_command.remove('--') - # validate build command through docopt from the original - # kiwi.tasks.system_build docopt information - log.info( - 'Validating kiwi_build_command_args:{0} {1}'.format( - os.linesep, kiwi_build_command + if self.command_args.get(''): + # construct build command from docopt command line + kiwi_build_command = [ + 'system', 'build' + ] + kiwi_build_command += self.command_args.get( + '' ) - ) - validated_build_command = docopt( - kiwi.tasks.system_build.__doc__, - argv=kiwi_build_command - ) - # rebuild kiwi build command from validated docopt parser result + if '--' in kiwi_build_command: + kiwi_build_command.remove('--') + # validate build command through docopt from the original + # kiwi.tasks.system_build docopt information + log.info( + 'Validating kiwi_build_command_args:{0} {1}'.format( + os.linesep, kiwi_build_command + ) + ) + validated_build_command = docopt( + kiwi.tasks.system_build.__doc__, + argv=kiwi_build_command + ) + else: + # construct build command from typer command line + validated_build_command = self.command_args.get('system_build') + + # rebuild kiwi build command from validated parser result kiwi_build_command = [ 'system', 'build' ] diff --git a/package/python-kiwi_boxed_plugin-spec-template b/package/python-kiwi_boxed_plugin-spec-template index e505bf5..7db7b78 100644 --- a/package/python-kiwi_boxed_plugin-spec-template +++ b/package/python-kiwi_boxed_plugin-spec-template @@ -113,7 +113,12 @@ Requires: kiwi >= 9.21.21 Requires: python%{python3_pkgversion}-kiwi >= 9.21.21 %endif Requires: python%{python3_pkgversion} >= 3.9 -Requires: python%{python3_pkgversion}-docopt +%if ! (0%{?fedora} >= 41 || 0%{?rhel} >= 10) +Requires: python%{python3_pkgversion}-docopt >= 0.6.2 +%else +Requires: python%{python3_pkgversion}-docopt-ng +%endif +Recommends: python%{python3_pkgversion}-typer >= 0.9.0 Requires: python%{python3_pkgversion}-requests Requires: python%{python3_pkgversion}-progressbar2 %if 0%{?ubuntu} || 0%{?debian} diff --git a/pyproject.toml b/pyproject.toml index 5cc6a17..1db7016 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,8 @@ system_boxbuild = "kiwi_boxed_plugin.tasks.system_boxbuild" [tool.poetry.group.test] [tool.poetry.group.test.dependencies] +# for local plugin cli testing +typer = ">=0.9.0" # python unit testing framework pytest = ">=6.2.0" pytest-cov = "*" diff --git a/test/unit/.coveragerc b/test/unit/.coveragerc index 27bb669..b9ed032 100644 --- a/test/unit/.coveragerc +++ b/test/unit/.coveragerc @@ -1,7 +1,9 @@ [run] omit = */version.py + */cli.py [report] omit = */version.py + */cli.py diff --git a/test/unit/tasks/system_boxbuild_test.py b/test/unit/tasks/system_boxbuild_test.py index 9e35abb..09bafe2 100644 --- a/test/unit/tasks/system_boxbuild_test.py +++ b/test/unit/tasks/system_boxbuild_test.py @@ -92,6 +92,36 @@ def test_process_system_boxbuild_container(self, mock_BoxContainerBuild): ], False, None, None ) + @patch('kiwi_boxed_plugin.tasks.system_boxbuild.BoxBuild') + def test_process_system_boxbuild_typer_commandline(self, mock_BoxBuild): + self._init_command_args() + self.task.command_args[''] = None + self.task.command_args['system_build'] = { + '--description': 'foo', + '--target-dir': 'xxx', + '--allow-existing-root': True, + '--add-package': ['a', 'b'] + } + self.task.command_args['boxbuild'] = True + self.task.command_args['--box'] = 'universal' + box_build = Mock() + mock_BoxBuild.return_value = box_build + self.task.process() + mock_BoxBuild.assert_called_once_with( + boxname='universal', ram=None, console=None, smp=None, arch='', + machine=None, cpu='host', sharing_backend='9p', + ssh_key='id_rsa', ssh_port='22', accel=True + ) + box_build.run.assert_called_once_with( + [ + '--debug', '--type', 'oem', '--profile', 'foo', + 'system', 'build', + '--description', 'foo', '--target-dir', 'xxx', + '--allow-existing-root', + '--add-package', 'a', '--add-package', 'b' + ], True, True, False, None, None + ) + @patch('kiwi_boxed_plugin.tasks.system_boxbuild.BoxBuild') def test_process_system_boxbuild(self, mock_BoxBuild): self._init_command_args()