Skip to content

Commit f1b5259

Browse files
committed
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
1 parent 5a9d7f4 commit f1b5259

File tree

6 files changed

+336
-21
lines changed

6 files changed

+336
-21
lines changed

kiwi_boxed_plugin/cli.py

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# Copyright (c) 2024 SUSE LLC. All rights reserved.
2+
#
3+
# This file is part of kiwi-boxed-build.
4+
#
5+
# kiwi-boxed-build is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# kiwi-boxed-build is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with kiwi-boxed-build. If not, see <http://www.gnu.org/licenses/>
17+
#
18+
import typer
19+
import itertools
20+
from pathlib import Path
21+
from typing import (
22+
Annotated, Optional, List, Union, no_type_check
23+
)
24+
25+
typers = {
26+
'boxbuild': typer.Typer(
27+
add_completion=False, invoke_without_command=True
28+
)
29+
}
30+
31+
system = typers['boxbuild']
32+
33+
34+
@no_type_check
35+
@system.command(
36+
context_settings={
37+
'allow_extra_args': True,
38+
'ignore_unknown_options': True
39+
}
40+
)
41+
def kiwi(
42+
ctx: typer.Context
43+
):
44+
"""
45+
List of command parameters as supported by the kiwi-ng
46+
build command. The information given here is passed
47+
along to the kiwi-ng system build command running in
48+
the virtual machine or container.
49+
"""
50+
Cli = ctx.obj
51+
args = ctx.args
52+
for option in list(set(args)):
53+
if type(option) is not str or not option.startswith('-'):
54+
continue
55+
k: List[Union[str, List]] = [option]
56+
v = []
57+
indexes = [n for n, x in enumerate(args) if x == option]
58+
if len(indexes) > 1:
59+
for index in indexes:
60+
v.append(args[index + 1])
61+
for index in sorted(indexes, reverse=True):
62+
del args[index + 1]
63+
del args[index]
64+
k.append(v)
65+
args += k
66+
Cli.subcommand_args['boxbuild']['system_build'] = \
67+
dict(itertools.zip_longest(*[iter(args)] * 2))
68+
Cli.global_args['command'] = 'boxbuild'
69+
Cli.global_args['system'] = True
70+
Cli.cli_ok = True
71+
72+
73+
@system.callback(
74+
help='build a system image in a self contained VM or container',
75+
subcommand_metavar='kiwi [OPTIONS]'
76+
)
77+
def boxbuild(
78+
ctx: typer.Context,
79+
box: Annotated[
80+
Optional[str], typer.Option(
81+
help='<name> Name of the box to use for the build process.'
82+
)
83+
] = None,
84+
list_boxes: Annotated[
85+
Optional[bool], typer.Option(
86+
'--list-boxes',
87+
help='show available build boxes.'
88+
)
89+
] = False,
90+
box_memory: Annotated[
91+
Optional[str], typer.Option(
92+
help='<vmgb> Number of GBs to reserve as main memory '
93+
'for the virtual machine. By default 8GB will be used.'
94+
)
95+
] = None,
96+
box_console: Annotated[
97+
Optional[str], typer.Option(
98+
help='<ttyname> Name of console in the kernel settings '
99+
'for the virtual machine. By default set to hvc0.'
100+
)
101+
] = None,
102+
box_smp_cpus: Annotated[
103+
Optional[int], typer.Option(
104+
help='<number> Number of CPUs to use in the SMP setup. '
105+
'By default 4 CPUs will be used.'
106+
)
107+
] = 4,
108+
box_debug: Annotated[
109+
Optional[bool], typer.Option(
110+
'--box-debug',
111+
help='In debug mode the started virtual machine will be kept open.'
112+
)
113+
] = False,
114+
container: Annotated[
115+
Optional[bool], typer.Option(
116+
'--container',
117+
help='Build in container instead of a VM. Options related to '
118+
'building in a VM will have no effect.'
119+
)
120+
] = False,
121+
kiwi_version: Annotated[
122+
Optional[str], typer.Option(
123+
help='<version> Specify a KIWI version to use for '
124+
'the build. The referenced KIWI will be fetched from '
125+
'pip and replaces the box installed KIWI version. '
126+
'Note: If --no-snapshot is used in combination '
127+
'with this option, the change of the KIWI version will '
128+
'be permanently stored in the used box.'
129+
)
130+
] = None,
131+
shared_path: Annotated[
132+
Optional[Path], typer.Option(
133+
help='<path> Optional host path to share with the box. '
134+
'The same path as it is present on the host will also '
135+
'be available inside of the box during build time.'
136+
)
137+
] = None,
138+
no_update_check: Annotated[
139+
Optional[bool], typer.Option(
140+
'--no-update-check',
141+
help='Skip check for available box update. The option '
142+
'has no effect if the selected box does not yet exist '
143+
'on the host.'
144+
)
145+
] = False,
146+
no_snapshot: Annotated[
147+
Optional[bool], typer.Option(
148+
'--no-snapshot',
149+
help='Run box with snapshot mode switched off. This '
150+
'causes the box disk file to be modified by the build '
151+
'process and allows to keep a persistent package cache '
152+
'as part of the box. The option can be used to increase '
153+
'the build performance due to data stored in the box '
154+
'which does not have to be reloaded from the network. '
155+
'On the contrary this option invalidates the immutable '
156+
'box attribute and should be used with care. On update '
157+
'of the box all data stored will be wiped. To prevent '
158+
'this combine the option with the --no-update-check option.'
159+
)
160+
] = False,
161+
no_accel: Annotated[
162+
Optional[bool], typer.Option(
163+
'--no-accel',
164+
help='Run box without hardware acceleration. By default '
165+
'KVM acceleration is activated'
166+
)
167+
] = False,
168+
qemu_9p_sharing: Annotated[
169+
Optional[bool], typer.Option(
170+
'--9p-sharing',
171+
help='Select 9p backend to use for sharing data '
172+
'between the host and the box.'
173+
)
174+
] = False,
175+
virtiofs_sharing: Annotated[
176+
Optional[bool], typer.Option(
177+
'--virtiofs-sharing',
178+
help='Select virtiofsd backend to use for sharing data '
179+
'between the host and the box.'
180+
)
181+
] = False,
182+
sshfs_sharing: Annotated[
183+
Optional[bool], typer.Option(
184+
'--sshfs-sharing',
185+
help='Select sshfs backend to use for sharing data '
186+
'between the host and the box.'
187+
)
188+
] = False,
189+
ssh_key: Annotated[
190+
Optional[str], typer.Option(
191+
help='<name> Name of ssh key to authorize for '
192+
'connection. By default id_rsa is used.'
193+
)
194+
] = 'id_rsa',
195+
ssh_port: Annotated[
196+
Optional[int], typer.Option(
197+
help='<port> Port number to use to forward the '
198+
'guest SSH port to the host By default 10022 is used.'
199+
)
200+
] = 10022,
201+
x86_64: Annotated[
202+
Optional[bool], typer.Option(
203+
'--x86_64',
204+
help='Select box for the x86_64 architecture. If no '
205+
'architecture is selected the host architecture is '
206+
'used for selecting the box. The selected box '
207+
'architecture also specifies the target architecture '
208+
'for the image build with that box.'
209+
)
210+
] = False,
211+
aarch64: Annotated[
212+
Optional[bool], typer.Option(
213+
'--aarch64',
214+
help='Select box for the aarch64 architecture. If no '
215+
'architecture is selected the host architecture is '
216+
'used for selecting the box. The selected box '
217+
'architecture also specifies the target architecture '
218+
'for the image build with that box.'
219+
)
220+
] = False,
221+
machine: Annotated[
222+
Optional[str], typer.Option(
223+
help='<qemu_machine> Machine name used '
224+
'by QEMU. By default no specific value is used here '
225+
'and qemu selects its default machine type. For cross '
226+
'arch builds or for system architectures for which '
227+
'QEMU defines no default like for Arm, it is required '
228+
'to specify a machine name. If you do not care about '
229+
'reproducing the idiosyncrasies of a particular bit '
230+
'of hardware, the best option is to use the virt '
231+
'machine type.'
232+
)
233+
] = None,
234+
cpu: Annotated[
235+
Optional[str], typer.Option(
236+
help='<qemu_cpu> CPU type used by QEMU. By default '
237+
'the host CPU type is used which is only a good '
238+
'selection if the host and the selected box are from '
239+
'the same architecture. On cross arch builds it is '
240+
'required to specify the CPU emulation the box should use'
241+
)
242+
] = None
243+
):
244+
Cli = ctx.obj
245+
Cli.subcommand_args['boxbuild'] = {
246+
'--box': box,
247+
'--list-boxes': list_boxes,
248+
'--box-memory': box_memory,
249+
'--box-console': box_console,
250+
'--box-smp-cpus': f'{box_smp_cpus}',
251+
'--box-debug': box_debug,
252+
'--container': container,
253+
'--kiwi-version': kiwi_version,
254+
'--shared-path': shared_path,
255+
'--no-update-check': no_update_check,
256+
'--no-snapshot': no_snapshot,
257+
'--no-accel': no_accel,
258+
'--9p-sharing': qemu_9p_sharing,
259+
'--virtiofs-sharing': virtiofs_sharing,
260+
'--sshfs-sharing': sshfs_sharing,
261+
'--ssh-key': ssh_key,
262+
'--ssh-port': f'{ssh_port}',
263+
'--x86_64': x86_64,
264+
'--aarch64': aarch64,
265+
'--machine': machine,
266+
'--cpu': cpu,
267+
'help': False
268+
}
269+
Cli.global_args['command'] = 'boxbuild'
270+
Cli.global_args['system'] = True
271+
Cli.cli_ok = True

kiwi_boxed_plugin/tasks/system_boxbuild.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -217,27 +217,32 @@ def process(self) -> None:
217217
)
218218

219219
def _validate_kiwi_build_command(self) -> List[str]:
220-
# construct build command from given command line
221-
kiwi_build_command = [
222-
'system', 'build'
223-
]
224-
kiwi_build_command += self.command_args.get(
225-
'<kiwi_build_command_args>'
226-
)
227-
if '--' in kiwi_build_command:
228-
kiwi_build_command.remove('--')
229-
# validate build command through docopt from the original
230-
# kiwi.tasks.system_build docopt information
231-
log.info(
232-
'Validating kiwi_build_command_args:{0} {1}'.format(
233-
os.linesep, kiwi_build_command
220+
if self.command_args.get('<kiwi_build_command_args>'):
221+
# construct build command from docopt command line
222+
kiwi_build_command = [
223+
'system', 'build'
224+
]
225+
kiwi_build_command += self.command_args.get(
226+
'<kiwi_build_command_args>'
234227
)
235-
)
236-
validated_build_command = docopt(
237-
kiwi.tasks.system_build.__doc__,
238-
argv=kiwi_build_command
239-
)
240-
# rebuild kiwi build command from validated docopt parser result
228+
if '--' in kiwi_build_command:
229+
kiwi_build_command.remove('--')
230+
# validate build command through docopt from the original
231+
# kiwi.tasks.system_build docopt information
232+
log.info(
233+
'Validating kiwi_build_command_args:{0} {1}'.format(
234+
os.linesep, kiwi_build_command
235+
)
236+
)
237+
validated_build_command = docopt(
238+
kiwi.tasks.system_build.__doc__,
239+
argv=kiwi_build_command
240+
)
241+
else:
242+
# construct build command from typer command line
243+
validated_build_command = self.command_args.get('system_build')
244+
245+
# rebuild kiwi build command from validated parser result
241246
kiwi_build_command = [
242247
'system', 'build'
243248
]

package/python-kiwi_boxed_plugin-spec-template

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ Requires: kiwi >= 9.21.21
113113
Requires: python%{python3_pkgversion}-kiwi >= 9.21.21
114114
%endif
115115
Requires: python%{python3_pkgversion} >= 3.9
116-
Requires: python%{python3_pkgversion}-docopt
116+
%if ! (0%{?fedora} >= 41 || 0%{?rhel} >= 10)
117+
Requires: python%{python3_pkgversion}-docopt >= 0.6.2
118+
%else
119+
Requires: python%{python3_pkgversion}-docopt-ng
120+
%endif
121+
Recommends: python%{python3_pkgversion}-typer >= 0.9.0
117122
Requires: python%{python3_pkgversion}-requests
118123
Requires: python%{python3_pkgversion}-progressbar2
119124
%if 0%{?ubuntu} || 0%{?debian}

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ system_boxbuild = "kiwi_boxed_plugin.tasks.system_boxbuild"
5959

6060
[tool.poetry.group.test]
6161
[tool.poetry.group.test.dependencies]
62+
# for local plugin cli testing
63+
typer = ">=0.9.0"
6264
# python unit testing framework
6365
pytest = ">=6.2.0"
6466
pytest-cov = "*"

test/unit/.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
[run]
22
omit =
33
*/version.py
4+
*/cli.py
45

56
[report]
67
omit =
78
*/version.py
9+
*/cli.py

test/unit/tasks/system_boxbuild_test.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,36 @@ def test_process_system_boxbuild_container(self, mock_BoxContainerBuild):
9292
], False, None, None
9393
)
9494

95+
@patch('kiwi_boxed_plugin.tasks.system_boxbuild.BoxBuild')
96+
def test_process_system_boxbuild_typer_commandline(self, mock_BoxBuild):
97+
self._init_command_args()
98+
self.task.command_args['<kiwi_build_command_args>'] = None
99+
self.task.command_args['system_build'] = {
100+
'--description': 'foo',
101+
'--target-dir': 'xxx',
102+
'--allow-existing-root': True,
103+
'--add-package': ['a', 'b']
104+
}
105+
self.task.command_args['boxbuild'] = True
106+
self.task.command_args['--box'] = 'universal'
107+
box_build = Mock()
108+
mock_BoxBuild.return_value = box_build
109+
self.task.process()
110+
mock_BoxBuild.assert_called_once_with(
111+
boxname='universal', ram=None, console=None, smp=None, arch='',
112+
machine=None, cpu='host', sharing_backend='9p',
113+
ssh_key='id_rsa', ssh_port='22', accel=True
114+
)
115+
box_build.run.assert_called_once_with(
116+
[
117+
'--debug', '--type', 'oem', '--profile', 'foo',
118+
'system', 'build',
119+
'--description', 'foo', '--target-dir', 'xxx',
120+
'--allow-existing-root',
121+
'--add-package', 'a', '--add-package', 'b'
122+
], True, True, False, None, None
123+
)
124+
95125
@patch('kiwi_boxed_plugin.tasks.system_boxbuild.BoxBuild')
96126
def test_process_system_boxbuild(self, mock_BoxBuild):
97127
self._init_command_args()

0 commit comments

Comments
 (0)