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()