From 551dad408a1055b54d7d3003a5484eca0654e9f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Sch=C3=A4fer?= Date: Fri, 14 Mar 2025 18:17:45 +0100 Subject: [PATCH] Move commandline interface to typer This commit moves the kiwi commandline and plugin interface to typer and drops the use of docopt. Typer is based on python type hints and allows for a more modern way to define Cli interfaces and auto completion, even for plugins. Also docopt is split into an old and a next generation variant and allows us to get rid of spec file hacks as well as documentation rendering hacks. --- Makefile | 16 +- completions/bash-completion | 86 ++ doc/source/commands/image_info.rst | 3 +- doc/source/commands/image_resize.rst | 3 +- doc/source/commands/kiwi.rst | 52 +- doc/source/commands/result_bundle.rst | 3 +- doc/source/commands/result_list.rst | 3 +- doc/source/commands/system_build.rst | 3 +- doc/source/commands/system_create.rst | 3 +- doc/source/commands/system_prepare.rst | 3 +- doc/source/commands/system_update.rst | 3 +- doc/source/conf.py | 17 - doc/source/contributing.rst | 11 +- .../contributing/kiwi_plugin_architecture.rst | 94 +- helper/completion_generator.py | 251 ---- helper/update_changelog.py | 329 ++--- kiwi/cli.py | 1094 ++++++++++++++--- kiwi/exceptions.py | 6 + kiwi/kiwi.py | 68 - kiwi/tasks/base.py | 3 - kiwi/tasks/image_info.py | 34 - kiwi/tasks/image_resize.py | 28 - kiwi/tasks/result_bundle.py | 43 - kiwi/tasks/result_list.py | 13 - kiwi/tasks/system_build.py | 102 -- kiwi/tasks/system_create.py | 23 - kiwi/tasks/system_prepare.py | 100 -- kiwi/tasks/system_update.py | 23 - package/python-kiwi-pkgbuild-template | 7 +- package/python-kiwi-spec-template | 18 +- pyproject.toml | 3 +- test/unit/cli_test.py | 213 +++- test/unit/tasks/base_test.py | 11 +- 33 files changed, 1459 insertions(+), 1210 deletions(-) create mode 100644 completions/bash-completion delete mode 100755 helper/completion_generator.py diff --git a/Makefile b/Makefile index fca82a11f62..7b8d2fd4a31 100644 --- a/Makefile +++ b/Makefile @@ -35,8 +35,8 @@ install: done # completion install -d -m 755 ${buildroot}usr/share/bash-completion/completions - $(python) helper/completion_generator.py \ - > ${buildroot}usr/share/bash-completion/completions/kiwi-ng + install -m 644 completions/bash-completion \ + ${buildroot}usr/share/bash-completion/completions/kiwi-ng # kiwi default configuration install -d -m 755 ${buildroot}etc install -m 644 kiwi.yml ${buildroot}etc/kiwi.yml @@ -80,16 +80,6 @@ valid: fi \ done -git_attributes: - # the following is required to update the $Format:%H$ git attribute - # for details on when this target is called see setup.py - git archive HEAD kiwi/version.py | tar -x - -clean_git_attributes: - # cleanup version.py to origin state - # for details on when this target is called see setup.py - git checkout kiwi/version.py - setup: poetry install --all-extras @@ -162,7 +152,7 @@ prepare_for_pypi: clean setup # ci-publish-to-pypi.yml github action poetry build --format=sdist -clean: clean_git_attributes +clean: rm -rf dist rm -rf doc/build rm -rf doc/dist diff --git a/completions/bash-completion b/completions/bash-completion new file mode 100644 index 00000000000..ef535711988 --- /dev/null +++ b/completions/bash-completion @@ -0,0 +1,86 @@ +#======================================== +# _kiwi +#---------------------------------------- +function setupCompletionLine { + local comp_line=$(echo $COMP_LINE | sed -e 's@kiwi-ng@kiwi@') + local result_comp_line + local prev_was_option=0 + for item in $comp_line; do + if [ $prev_was_option = 1 ];then + prev_was_option=0 + continue + fi + if [[ $item =~ -.* ]];then + prev_was_option=1 + continue + fi + result_comp_line="$result_comp_line $item" + done + echo $result_comp_line +} + +function _kiwi { + local cur prev opts + _get_comp_words_by_ref cur prev + local cmd=$(setupCompletionLine | awk -F ' ' '{ print $NF }') + for comp in $prev $cmd;do + case "$comp" in + "image") + __comp_reply "info resize" + return 0 + ;; + "result") + __comp_reply "bundle list" + return 0 + ;; + "system") + __comp_reply "build create prepare update" + return 0 + ;; + "build") + __comp_reply "--add-bootstrap-package --add-container-label --add-package --add-repo --add-repo-credentials --allow-existing-root --clear-cache --delete-package --description --help --ignore-repos --ignore-repos-used-for-build --set-container-derived-from --set-container-tag --set-release-version --set-repo --set-repo-credentials --set-type-attr= [] - kiwi-ng image info -h | --help + kiwi-ng image info --help kiwi-ng image info --description= [--resolve-package-list] [--list-profiles] @@ -18,7 +18,6 @@ SYNOPSIS [--ignore-repos] [--add-repo=...] [--print-xml|--print-yaml] - kiwi-ng image info help .. _db_image_info_desc: diff --git a/doc/source/commands/image_resize.rst b/doc/source/commands/image_resize.rst index aba9e05dee8..972ef3e4a70 100644 --- a/doc/source/commands/image_resize.rst +++ b/doc/source/commands/image_resize.rst @@ -12,10 +12,9 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng image resize -h | --help + kiwi-ng image resize --help kiwi-ng image resize --target-dir= --size= [--root=] - kiwi-ng image resize help .. _db_kiwi_image_resize_desc: diff --git a/doc/source/commands/kiwi.rst b/doc/source/commands/kiwi.rst index 68f2578ff21..7c238fd1ff2 100644 --- a/doc/source/commands/kiwi.rst +++ b/doc/source/commands/kiwi.rst @@ -8,47 +8,13 @@ SYNOPSIS .. code:: bash - kiwi-ng [global options] service [] - - kiwi-ng -h | --help - kiwi-ng [--profile=...] - [--setenv=...] - [--temp-dir=] - [--type=] - [--logfile=] - [--logsocket=] - [--loglevel=] - [--debug] - [--debug-run-scripts-in-screen] - [--color-output] - [--config=] - [--kiwi-file=] - image [...] - kiwi-ng [--logfile=] - [--logsocket=] - [--loglevel=] - [--debug] - [--debug-run-scripts-in-screen] - [--color-output] - [--config=] - result [...] - kiwi-ng [--profile=...] - [--setenv=...] - [--shared-cache-dir=] - [--temp-dir=] - [--target-arch=] - [--type=] - [--logfile=] - [--logsocket=] - [--loglevel=] - [--debug] - [--debug-run-scripts-in-screen] - [--color-output] - [--config=] - [--kiwi-file=] - system [...] - kiwi-ng -v | --version - kiwi-ng help + kiwi-ng --help | --version + + kiwi-ng [global options] image [...] + kiwi-ng [global options] result [...] + kiwi-ng [global options] system [...] + + kiwi-ng help [kiwi::COMMAND::SUBCOMMAND] .. _db_commands_kiwi_desc: @@ -97,8 +63,8 @@ GLOBAL OPTIONS --config= Use specified runtime configuration file. If not specified, the - runtime configuration is expected to be in the :file:`~/.config/kiwi/config.yml` - or :file:`/etc/kiwi.yml` files. + runtime configuration is expected to be in the + :file:`~/.config/kiwi/config.yml` or :file:`/etc/kiwi.yml` files. --debug diff --git a/doc/source/commands/result_bundle.rst b/doc/source/commands/result_bundle.rst index d7cc500ecd7..cd8ea1eaf99 100644 --- a/doc/source/commands/result_bundle.rst +++ b/doc/source/commands/result_bundle.rst @@ -10,13 +10,12 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng result bundle -h | --help + kiwi-ng result bundle --help kiwi-ng result bundle --target-dir= --id= --bundle-dir= [--bundle-format=] [--zsync_source=] [--package-as-rpm] [--no-compress] - kiwi-ng result bundle help .. _db_kiwi_result_bundle_desc: diff --git a/doc/source/commands/result_list.rst b/doc/source/commands/result_list.rst index 6c0be92e462..9048bb1871b 100644 --- a/doc/source/commands/result_list.rst +++ b/doc/source/commands/result_list.rst @@ -10,9 +10,8 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng result list -h | --help + kiwi-ng result list --help kiwi-ng result list --target-dir= - kiwi-ng result list help .. _db_kiwi_result_list_desc: diff --git a/doc/source/commands/system_build.rst b/doc/source/commands/system_build.rst index 131c2012c2b..7705885bfa4 100644 --- a/doc/source/commands/system_build.rst +++ b/doc/source/commands/system_build.rst @@ -12,7 +12,7 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng system build -h | --help + kiwi-ng system build --help kiwi-ng system build --description= --target-dir= [--allow-existing-root] [--clear-cache] @@ -31,7 +31,6 @@ SYNOPSIS [--set-type-attr=...] [--set-release-version=] [--signing-key=...] - kiwi-ng system build help .. _db_kiwi_system_build_desc: diff --git a/doc/source/commands/system_create.rst b/doc/source/commands/system_create.rst index ac7a71a9cc6..3405effe5a5 100644 --- a/doc/source/commands/system_create.rst +++ b/doc/source/commands/system_create.rst @@ -12,10 +12,9 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng system create -h | --help + kiwi-ng system create --help kiwi-ng system create --root= --target-dir= [--signing-key=...] - kiwi-ng system create help .. _db_kiwi_system_create_desc: diff --git a/doc/source/commands/system_prepare.rst b/doc/source/commands/system_prepare.rst index c5c1ee30a13..b363ebe3b3f 100644 --- a/doc/source/commands/system_prepare.rst +++ b/doc/source/commands/system_prepare.rst @@ -10,7 +10,7 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng system prepare -h | --help + kiwi-ng system prepare --help kiwi-ng system prepare --description= --root= [--allow-existing-root] [--clear-cache] @@ -29,7 +29,6 @@ SYNOPSIS [--set-type-attr=...] [--set-release-version=] [--signing-key=...] - kiwi-ng system prepare help .. _db_kiwi_system_prepare_desc: diff --git a/doc/source/commands/system_update.rst b/doc/source/commands/system_update.rst index 558ac8b2184..f63eacc9c13 100644 --- a/doc/source/commands/system_update.rst +++ b/doc/source/commands/system_update.rst @@ -10,11 +10,10 @@ SYNOPSIS kiwi-ng [global options] service [] - kiwi-ng system update -h | --help + kiwi-ng system update --help kiwi-ng system update --root= [--add-package=...] [--delete-package=...] - kiwi-ng system update help .. _db_kiwi_system_update_desc: diff --git a/doc/source/conf.py b/doc/source/conf.py index b659740e005..bfdd3077f43 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -35,22 +35,6 @@ 'sphinx_rtd_theme' ] -docopt_ignore = [ - 'kiwi.cli', - 'kiwi.tasks.system_build', - 'kiwi.tasks.system_prepare', - 'kiwi.tasks.system_update', - 'kiwi.tasks.system_create', - 'kiwi.tasks.result_list', - 'kiwi.tasks.result_bundle', - 'kiwi.tasks.image_resize', - 'kiwi.tasks.image_info' -] - -def remove_module_docstring(app, what, name, obj, options, lines): - if what == "module" and name in docopt_ignore: - del lines[:] - def prologReplace(app, docname, source): result = source[0] for key in app.config.prolog_replacements: @@ -60,7 +44,6 @@ def prologReplace(app, docname, source): def setup(app): app.add_config_value('prolog_replacements', {}, True) app.connect('source-read', prologReplace) - app.connect("autodoc-process-docstring", remove_module_docstring) prolog_replacements = { diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst index 9c79416c28d..ec544b02ace 100644 --- a/doc/source/contributing.rst +++ b/doc/source/contributing.rst @@ -62,12 +62,20 @@ Create a Python Virtual Development Environment ----------------------------------------------- The following commands initializes and activates a development -environment for Python 3: +environment for the current default Python version: .. code:: shell-session $ poetry install +.. note:: + + To create the python virtual env for another version of + Python, e.g. 3.11, call the following prior the poetry install + :: + + $ poetry env use python3.11 + The command above automatically creates the application script called :command:`kiwi-ng`, which allows you to run {kiwi} from the Python sources inside the virtual environment using Poetry: @@ -76,7 +84,6 @@ Python sources inside the virtual environment using Poetry: $ poetry run kiwi-ng --help - Running the Unit Tests ---------------------- diff --git a/doc/source/contributing/kiwi_plugin_architecture.rst b/doc/source/contributing/kiwi_plugin_architecture.rst index fbe0c4877fe..a64e85f0420 100644 --- a/doc/source/contributing/kiwi_plugin_architecture.rst +++ b/doc/source/contributing/kiwi_plugin_architecture.rst @@ -2,24 +2,27 @@ Plugin Architecture =================== Each command provided by {kiwi} is written as a task plugin under the -**kiwi.tasks** namespace. As a developer, you can extend {kiwi} with custom task -plugins, following the conventions below. +**kiwi.tasks** namespace. As a developer, you can extend the {kiwi} +system command space with custom task plugins, following the conventions +below. Naming conventions ------------------ Task plugin file name The file name of a task plugin must follow the pattern - :file:`_.py`. This allows you to invoke the task - with :command:`kiwi-ng service command ...` + :file:`system_.py`. This allows you to invoke the task + with :command:`kiwi-ng system command ...` Task plugin option handling - {kiwi} uses the docopt module to handle options. Each task plugin - must use docopt to allow option handling. + {kiwi} uses the typer module to handle options. Each task plugin + must use typer to allow option handling. The typer definition + must be provided in a file named :file:`cli.py` and must live in the + toplevel of the plugin python namespace. Task plugin class The implementation of the plugin must be a class that matches the naming - convention :class:`Task`. The class must inherit from the + convention :class:`SystemTask`. The class must inherit from the :class:`CliTask` base class. On the plugin startup, {kiwi} expects an implementation of the :file:`process` method. @@ -27,7 +30,7 @@ Task plugin entry point Registration of the plugin must be done in :file:`pyproject.toml` using the ``tool.poetry.plugins`` concept. - .. code:: python + .. code:: [tool.poetry] name = "kiwi_plugin" @@ -38,7 +41,7 @@ Task plugin entry point [tool.poetry.plugins] [tool.poetry.plugins."kiwi.tasks"] - service_command = "kiwi_plugin.tasks.service_command" + system_ = "kiwi__plugin.tasks.system_" Example plugin -------------- @@ -53,11 +56,10 @@ Example plugin 2. Create the entry point in :command:`pyproject.toml`. - Assuming we want to create the service named **relax** that has - the command **justdoit**, this is the required plugin - definition in :file:`pyproject.toml`: + Assuming we want to create the system command **justdoit**, this is + the following entry point definition in :file:`pyproject.toml`: - .. code:: python + .. code:: toml [tool.poetry] name = "kiwi_relax_plugin" @@ -68,34 +70,55 @@ Example plugin [tool.poetry.plugins] [tool.poetry.plugins."kiwi.tasks"] - relax_justdoit = "kiwi_relax_plugin.tasks.relax_justdoit" + system_justdoit = "kiwi_relax_plugin.tasks.system_justdoit" + +3. Create the typer cli interface in the file + :file:`kiwi_relax_plugin/cli.py` with the following + content: + + .. code:: python -3. Create the plugin code in the file - :file:`kiwi_relax_plugin/tasks/relax_justdoit.py` with the following + import typer + from typing import Annotated + + # typers variable must be provided for kiwi plugins + typers = { + 'justdoit': typer.Typer(add_completion=False) + } + + system = typers['justdoit'] + + @system.callback( + help='What is it good for' + invoke_without_command=True, + subcommand_metavar='' + ) + def justdoit( + ctx: typer.Context, + now: Annotated[str, typer.Option(help='For --now option')] + ): + Cli=ctx.obj + Cli.subcommand_args['justdoit'] = { + '--now': now, + 'help': False + } + Cli.global_args['command'] = 'justdoit' + Cli.global_args['system'] = True + Cli.cli_ok = True + +4. Create the plugin code in the file + :file:`kiwi_relax_plugin/tasks/system_justdoit.py` with the following content: .. code:: python - """ - usage: kiwi-ng relax justdoit -h | --help - kiwi-ng relax justdoit --now - - commands: - justdoit - time to relax - - options: - --now - right now. For more details about docopt - see: http://docopt.org - """ # These imports requires kiwi to be part of your environment # It can be either installed from pip into a virtual development # environment or from the distribution package manager from kiwi.tasks.base import CliTask from kiwi.help import Help - class RelaxJustdoitTask(CliTask): + class SystemJustdoitTask(CliTask): def process(self): self.manual = Help() if self.command_args.get('help') is True: @@ -105,12 +128,13 @@ Example plugin # installed by the plugin return self.manual.show('kiwi::relax::justdoit') - print( - 'https://genius.com/Frankie-goes-to-hollywood-relax-lyrics' - ) + if self.command_args.get('--now'): + print( + 'https://genius.com/Frankie-goes-to-hollywood-relax-lyrics' + ) -4. Test the plugin +5. Test the plugin .. code:: bash - $ poetry run kiwi-ng relax justdoit --now + $ poetry run kiwi-ng system justdoit --now diff --git a/helper/completion_generator.py b/helper/completion_generator.py deleted file mode 100755 index e04cefe8c68..00000000000 --- a/helper/completion_generator.py +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/python3 - -from textwrap import dedent - -import subprocess -import re - -import collections - - -class AutoVivification(dict): - def __getitem__(self, item): - try: - return dict.__getitem__(self, item) - except KeyError: - value = self[item] = type(self)() - return value - - -class AppHash: - def __init__(self): # noqa: C901 - tasks = [ - 'kiwi/cli.py', - 'kiwi/tasks/*.py' - ] - call_parm = ['bash', '-c', 'cat %s' % ' '.join(tasks)] - cmd = subprocess.Popen( - call_parm, stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) - begin = False - cur_path = '' - self.result = AutoVivification() - for line in cmd.communicate()[0].decode().split('\n'): - usage = re.search('^usage: (.*)', line) - if usage: - begin = True - if re.match('.*--help', line): - mod_line = re.sub('[\[\]\|]', '', usage.group(1)) - mod_line = re.sub('-h ', '', mod_line) - key_list = mod_line.split() - result_keys = self.validate(key_list) - cur_path = self.merge(result_keys, self.result) - else: - key_list = usage.group(1).split() - result_keys = self.validate(key_list) - cur_path = self.merge(result_keys, self.result) - elif begin: - if not line: - begin = False - else: - if re.match('.*--version', line): - mod_line = re.sub('[\[\]\|]', '', line) - mod_line = re.sub('-v ', '', mod_line) - key_list = mod_line.split() - result_keys = self.validate(key_list) - cur_path = self.merge(result_keys, self.result) - elif re.match('.*kiwi-ng --compat', line): - mod_line = re.sub('[\[\]\|]', '', line) - mod_line = re.sub('...', '', mod_line) - key_list = mod_line.split() - result_keys = self.validate(key_list) - cur_path = self.merge(result_keys, self.result) - elif re.match('.*kiwi-ng \[', line): - line = line.replace('[', '') - line = line.replace(']', '') - line = line.replace('|', '') - key_list = line.split() - key_list.pop(0) - for global_opt in key_list: - result_keys = self.validate( - ['kiwi-ng', global_opt] - ) - cur_path = self.merge(result_keys, self.result) - elif re.match(' \[', line): - line = line.replace('[', '') - line = line.replace(']', '') - line = line.replace('|', '') - key_list = line.split() - for global_opt in key_list: - result_keys = self.validate( - ['kiwi-ng', global_opt] - ) - cur_path = self.merge(result_keys, self.result) - elif re.match(' \[', line): - opt_val = re.search(' \[(--.*)', line) - global_opt = opt_val.group(1) - global_opt = global_opt.replace('[', '') - global_opt = global_opt.replace(']', '') - global_opt = global_opt.replace('|', '') - result_keys = self.validate( - ['kiwi-ng', global_opt] - ) - cur_path = self.merge(result_keys, self.result) - elif re.match('.*kiwi', line): - mandatory_options = re.search( - '(.*kiwi-ng.*?) (--.*)', line - ) - if mandatory_options: - line = mandatory_options.group(1) - key_list = line.split() - result_keys = self.validate(key_list) - cur_path = self.merge(result_keys, self.result) - if mandatory_options: - for option in mandatory_options.group(2).split(): - mod_line = cur_path + ' ' + option - key_list = mod_line.split() - result_keys = self.validate(key_list) - self.merge(result_keys, self.result) - else: - if 'kiwi-ng --' in cur_path: - cur_path = '' - for mod_line in line.strip().split('|'): - mod_line = cur_path + ' ' + mod_line - mod_line = re.sub('[\[\]]', '', mod_line) - mod_line = re.sub('-h ', '', mod_line) - key_list = mod_line.split() - result_keys = self.validate(key_list) - self.merge(result_keys, self.result) - - def merge(self, key_list, result): - raw_key_path = " ".join(key_list) - key_path = '' - for key in key_list: - key_path += '[\'' + key + '\']' - expression = 'self.result' + key_path - exec(expression) - return raw_key_path - - def validate(self, key_list): - result_keys = [] - for key in key_list: - option = re.search('^\[(--.*)=|^\[(.*)\]', key) - mandatory = re.search('^(--.*)=', key) - if option: - if option.group(1): - result_keys.append(option.group(1)) - elif mandatory: - if mandatory.group(1): - result_keys.append(mandatory.group(1)) - elif re.search('||...', key): - pass - else: - key = key.replace('<', '__') - key = key.replace('>', '__') - result_keys.append(key) - return result_keys - - -class AppTree: - def __init__(self): - self.completion = AppHash() - self.level_dict = {} - - def traverse(self, tree=None, level=0, origin=None): - if not tree: - tree = self.completion.result['kiwi-ng'] - if not origin: - origin = 'kiwi-ng' - for key in tree: - try: - if self.level_dict[str(level)]: - pass - except KeyError: - self.level_dict[str(level)] = {} - try: - if self.level_dict[str(level)][origin]: - pass - except KeyError: - self.level_dict[str(level)][origin] = [] - - if key not in self.level_dict[str(level)][origin]: - self.level_dict[str(level)][origin].append(key) - - if tree[key]: - self.traverse(tree[key], level + 1, key) - - -tree = AppTree() -tree.traverse() - -# helpful for debugging -# pp = pprint.PrettyPrinter(indent=4) -# pp.pprint(tree.completion.result) - -sorted_levels = collections.OrderedDict( - sorted(tree.level_dict.items()) -) - -print(dedent(''' -#======================================== -# _kiwi -#---------------------------------------- -function setupCompletionLine { - local comp_line=$(echo $COMP_LINE | sed -e 's@kiwi-ng@kiwi@') - local result_comp_line - local prev_was_option=0 - for item in $comp_line; do - if [ $prev_was_option = 1 ];then - prev_was_option=0 - continue - fi - if [[ $item =~ -.* ]];then - prev_was_option=1 - continue - fi - result_comp_line="$result_comp_line $item" - done - echo $result_comp_line -} - -function _kiwi { - local cur prev opts - _get_comp_words_by_ref cur prev - local cmd=$(setupCompletionLine | awk -F ' ' '{ print $NF }') -''').strip()) - -print(' for comp in $prev $cmd;do') -print(' case "$comp" in') -for level in sorted_levels: - if level == '0': - continue - for sub in sorted(sorted_levels[level]): - print(' "%s")' % (sub)) - print( - ' __comp_reply "{0}"'.format( - (" ".join(sorted(sorted_levels[level][sub]))) - ) - ) - print(' return 0') - print(' ;;') -print(' esac') -print(' done') -print( - ' __comp_reply "{0}"'.format( - (" ".join(sorted(sorted_levels['0']['kiwi-ng']))) - ) -) -print(' return 0') -print('}') -print(dedent(''' -#======================================== -# comp_reply -#---------------------------------------- -function __comp_reply { - word_list=$@ - COMPREPLY=($(compgen -W "$word_list" -- ${cur})) -} - -complete -F _kiwi -o default kiwi -complete -F _kiwi -o default kiwi-ng -''').strip()) diff --git a/helper/update_changelog.py b/helper/update_changelog.py index 2dda1b10e31..20819cc7ade 100755 --- a/helper/update_changelog.py +++ b/helper/update_changelog.py @@ -1,162 +1,191 @@ -#!/usr/bin/python3 -""" -usage: update_changelog (--since=|--file=) - [--utc] - [--fix] - -arguments: - --since= - changes since the latest entry in the reference file - --file= - changes from the given file - --utc - print date/time in UTC - --fix - lookup .fix files and apply them -""" -import docopt +#!/usr/bin/env python3 +import typer import os import glob import subprocess import sys from dateutil import parser from dateutil import tz +from typing import ( + Annotated, Optional +) +from pathlib import Path -# Commandline arguments -arguments = docopt.docopt(__doc__) - -# Latest date of the given reference file -date_reference = None - -# List of skipped commits older than date_reference -skip_list = [] - -# hash of git history log entries -log_data = {} - -# raw list of log lines from git history or reference file -log_lines = [] - -# Author and Date -log_author = None -log_date = None - -# changelog header line -log_start = '-' * 67 + os.linesep - -# date format for rpm changelog -date_format = '%a %b %d %T %Z %Y' - -# commit message -commit_message = [] - -# Open reference log file -reference_file = arguments['--since'] or arguments['--file'] - -# Custom fix files -fix_dict = {} -if arguments['--fix']: - for fix in glob.iglob(f'{os.path.dirname(reference_file)}/*.fix'): - sys.stderr.write(f'Reading fix: {fix}{os.linesep}') - with open(fix, 'r') as fixlog: - commit = fixlog.readline() - fix_dict[commit] = fixlog.read() - -if arguments['--since']: - # Read latest date from reference file - with open(reference_file, 'r') as gitlog: - # read commit and author - gitlog.readline() - gitlog.readline() - # read date - latest_date = gitlog.readline().replace('AuthorDate:', '').strip() - date_reference = parser.parse(latest_date) - - # Read git history since latest entry from reference file - process = subprocess.Popen( - [ - 'git', 'log', '--no-merges', '--format=fuller', - '--since="{0}"'.format(latest_date) - ], stdout=subprocess.PIPE - ) - from_git_log = True - for line in iter(process.stdout.readline, b''): - if line.startswith(b'commit'): - commit = line.decode() - if from_git_log and fix_dict.get(commit): - sys.stderr.write(f' Apply fix for: {commit}') - from_git_log = False + +def main( + since: Annotated[ + Optional[Path], typer.Option( + help='changes since the latest entry in the reference file' + ) + ] = None, + file: Annotated[ + Optional[Path], typer.Option( + help='changes from the given file' + ) + ] = None, + utc: Annotated[ + Optional[bool], typer.Option( + '--utc', + help='print date/time in UTC' + ) + ] = False, + fix: Annotated[ + Optional[bool], typer.Option( + '--fix', + help='lookup .fix files and apply them' + ) + ] = False +): + if not since and not file: + print('Either --since or --file must be specified') + sys.exit(1) + if since and file: + print('Only one of --since or --file must be specified') + sys.exit(1) + + # Commandline arguments + arguments = { + '--since': since, + '--file': file, + '--utc': utc, + '--fix': fix + } + + # Latest date of the given reference file + date_reference = None + + # List of skipped commits older than date_reference + skip_list = [] + + # hash of git history log entries + log_data = {} + + # raw list of log lines from git history or reference file + log_lines = [] + + # Author and Date + log_author = None + log_date = None + + # changelog header line + log_start = '-' * 67 + os.linesep + + # date format for rpm changelog + date_format = '%a %b %d %T %Z %Y' + + # commit message + commit_message = [] + + # Open reference log file + reference_file = arguments['--since'] or arguments['--file'] + + # Custom fix files + fix_dict = {} + if arguments['--fix']: + for fix in glob.iglob(f'{os.path.dirname(reference_file)}/*.fix'): + sys.stderr.write(f'Reading fix: {fix}{os.linesep}') + with open(fix, 'r') as fixlog: + commit = fixlog.readline() + fix_dict[commit] = fixlog.read() + + if arguments['--since']: + # Read latest date from reference file + with open(reference_file, 'r') as gitlog: + # read commit and author + gitlog.readline() + gitlog.readline() + # read date + latest_date = gitlog.readline().replace('AuthorDate:', '').strip() + date_reference = parser.parse(latest_date) + + # Read git history since latest entry from reference file + process = subprocess.Popen( + [ + 'git', 'log', '--no-merges', '--format=fuller', + '--since="{0}"'.format(latest_date) + ], stdout=subprocess.PIPE + ) + from_git_log = True + for line in iter(process.stdout.readline, b''): + if line.startswith(b'commit'): + commit = line.decode() + if from_git_log and fix_dict.get(commit): + sys.stderr.write(f' Apply fix for: {commit}') + from_git_log = False + log_lines.append(line) + for fix_line in fix_dict.get(commit).split(os.linesep): + log_lines.append(fix_line.encode()) + elif not from_git_log: + from_git_log = True + + if from_git_log: log_lines.append(line) - for fix_line in fix_dict.get(commit).split(os.linesep): - log_lines.append(fix_line.encode()) - elif not from_git_log: - from_git_log = True - - if from_git_log: - log_lines.append(line) -else: - with open(reference_file, 'rb') as gitlog: - log_lines = gitlog.readlines() - -# Iterate over log data and convert to changelog format -for line_data in log_lines: - line = line_data.decode(encoding='utf-8') - if line.startswith('commit'): - if commit_message: - commit_message.pop(0) - message_header = commit_message.pop(0).lstrip() - message_body = [] - for line in commit_message: - message_line = line.lstrip() - if not message_line: - message_body.append(os.linesep) - else: - message_body.append( - ' {0}{1}'.format(message_line, os.linesep) - ) - log_data[log_date] = ''.join( - [ - log_start, - '{0} - {1}{2}{2}'.format( - log_date.astimezone( - tz.UTC if arguments['--utc'] else tz.tzlocal() - ).strftime(date_format), log_author, os.linesep - ), - '- {0}{1}'.format( - message_header, os.linesep - ) - ] + message_body - ) - commit_message = [] - elif line.startswith('Author:'): - log_author = line.replace('Author:', '').strip() - elif line.startswith('AuthorDate:'): - log_date = parser.parse(line.replace('AuthorDate:', '').strip()) - elif line.startswith('Commit:'): - pass - elif line.startswith('CommitDate:'): - pass else: - commit_message.append(line.strip()) - -# print in changelog format on stdout -for author_date in reversed(sorted(log_data.keys())): - if date_reference: - if date_reference < author_date: - sys.stdout.write(log_data[author_date]) + with open(reference_file, 'rb') as gitlog: + log_lines = gitlog.readlines() + + # Iterate over log data and convert to changelog format + for line_data in log_lines: + line = line_data.decode(encoding='utf-8') + if line.startswith('commit'): + if commit_message: + commit_message.pop(0) + message_header = commit_message.pop(0).lstrip() + message_body = [] + for line in commit_message: + message_line = line.lstrip() + if not message_line: + message_body.append(os.linesep) + else: + message_body.append( + ' {0}{1}'.format(message_line, os.linesep) + ) + log_data[log_date] = ''.join( + [ + log_start, + '{0} - {1}{2}{2}'.format( + log_date.astimezone( + tz.UTC if arguments['--utc'] else tz.tzlocal() + ).strftime(date_format), log_author, os.linesep + ), + '- {0}{1}'.format( + message_header, os.linesep + ) + ] + message_body + ) + commit_message = [] + elif line.startswith('Author:'): + log_author = line.replace('Author:', '').strip() + elif line.startswith('AuthorDate:'): + log_date = parser.parse(line.replace('AuthorDate:', '').strip()) + elif line.startswith('Commit:'): + pass + elif line.startswith('CommitDate:'): + pass else: - skip_list.append(author_date) - else: - sys.stdout.write(log_data[author_date]) - -# print inconsistencies if any on stderr -if skip_list: - sys.stderr.write( - 'Reference Date: {0}{1}'.format(date_reference, os.linesep) - ) - for date in skip_list: + commit_message.append(line.strip()) + + # print in changelog format on stdout + for author_date in reversed(sorted(log_data.keys())): + if date_reference: + if date_reference < author_date: + sys.stdout.write(log_data[author_date]) + else: + skip_list.append(author_date) + else: + sys.stdout.write(log_data[author_date]) + + # print inconsistencies if any on stderr + if skip_list: sys.stderr.write( - ' + Skipped: {0}: past reference{1}'.format( - date, os.linesep - ) + 'Reference Date: {0}{1}'.format(date_reference, os.linesep) ) + for date in skip_list: + sys.stderr.write( + ' + Skipped: {0}: past reference{1}'.format( + date, os.linesep + ) + ) + +if __name__ == "__main__": + typer.run(main) diff --git a/kiwi/cli.py b/kiwi/cli.py index 88898e0c322..ef18484bb25 100644 --- a/kiwi/cli.py +++ b/kiwi/cli.py @@ -15,123 +15,25 @@ # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # -""" -usage: kiwi-ng -h | --help - kiwi-ng [--profile=...] - [--setenv=...] - [--temp-dir=] - [--target-arch=] - [--type=] - [--logfile=] - [--logsocket=] - [--loglevel=] - [--debug] - [--debug-run-scripts-in-screen] - [--color-output] - [--config=] - [--kiwi-file=] - image [...] - kiwi-ng [--logfile=] - [--logsocket=] - [--loglevel=] - [--debug] - [--debug-run-scripts-in-screen] - [--color-output] - [--config=] - result [...] - kiwi-ng [--profile=...] - [--setenv=...] - [--shared-cache-dir=] - [--temp-dir=] - [--target-arch=] - [--type=] - [--logfile=] - [--logsocket=] - [--loglevel=] - [--debug] - [--debug-run-scripts-in-screen] - [--color-output] - [--config=] - [--kiwi-file=] - system [...] - kiwi-ng -v | --version - kiwi-ng help - -global options: - --color-output - use colors for warning and error messages - --config= - use specified runtime configuration file. If - not specified the runtime configuration is looked - up at ~/.config/kiwi/config.yml or /etc/kiwi.yml - --logfile= - create a log file containing all log information including - debug information even if this was not requested by the - debug switch. The special call: '--logfile stdout' sends all - information to standard out instead of writing to a file - --logsocket= - send log data to the given Unix Domain socket in the same - format as with --logfile - --loglevel= - specify logging level as number. Details about the - available log levels can be found at: - https://docs.python.org/3/library/logging.html#logging-levels - Setting a log level causes all message >= level to be - displayed. - --debug - print debug information, same as: '--loglevel 10' - --debug-run-scripts-in-screen - run scripts called by kiwi in a screen session - -v --version - show program version - help - show manual page - -global options for services: image, system - --profile= - profile name, multiple profiles can be selected by passing - this option multiple times - --setenv= - export environment variable and its value into the caller - environment. This option can be specified multiple times - --shared-cache-dir= - specify an alternative shared cache directory. The directory - is shared via bind mount between the build host and image - root system and contains information about package repositories - and their cache and meta data. - --temp-dir= - specify an alternative base temporary directory. The - provided path is used as base directory to store temporary - files and directories. By default /var/tmp is used. - --type= - image build type. If not set the default XML specified - build type will be used - --kiwi-file= - Basename of kiwi file which contains the main image - configuration elements. If not specified kiwi searches for - a file named config.xml or a file matching *.kiwi - -global options for services: image, system - --target-arch= - set the image architecture. By default the host architecture is - used as the image architecture. If the specified architecture name - does not match the host architecture and is therefore requesting - a cross architecture image build, it's important to understand that - for this process to work a preparatory step to support the image - architecture and binary format on the building host is required - and not a responsibility of kiwi. -""" +import typer import logging import sys import os -from importlib.metadata import entry_points -from docopt import docopt +from unittest.mock import patch +from importlib.metadata import ( + entry_points, EntryPoint +) +from pathlib import Path +from typing import ( + Annotated, Dict, Optional, List +) # project from kiwi.exceptions import ( KiwiUnknownServiceName, KiwiCommandNotLoaded, - KiwiLoadCommandUndefined + KiwiLoadCommandUndefined, + KiwiLoadPluginError ) from kiwi.version import __version__ from kiwi.help import Help @@ -148,23 +50,862 @@ class Cli: application and implements methods to load further command plugins which itself provides their own command line interface """ + global_args: Dict = {} + subcommand_args: Dict = {} + plugins: Dict[str, typer.Typer] = {} + cli_ok = False + + # system + system = typer.Typer( + help='system command for building images. The system space ' + 'can also be extended by custom command plugins.' + ) + + # result + result = typer.Typer( + help='result command for listing image result information ' + 'and create result bundles.' + ) + + # image + image = typer.Typer( + help='image command for retrieving image information ' + 'prior building.' + ) + + cli = typer.Typer(add_completion=False) + cli.add_typer(system, name='system') + cli.add_typer(result, name='result') + cli.add_typer(image, name='image') + def __init__(self): - self.all_args = docopt( - __doc__, - version='KIWI (next generation) version ' + __version__, - options_first=True + Cli.cli_ok = False + exit_code = 1 + # load plugins... + Cli.plugins.update( + self.load_plugin_cli() + ) + # add plugin cli's if there are any loaded + for system_subcommand in sorted(Cli.plugins.keys()): + Cli.system.add_typer( + Cli.plugins[system_subcommand], name=system_subcommand, + context_settings={ + 'obj': Cli + } + ) + with patch('sys.exit') as sys_exit: + # This is unfortunately needed to integrate the + # typer interface with the former docopt based + # option handling to kiwi in a way that is not + # too intrusive. Usually typer quits after the + # invocation of the commmand function. But in case + # of kiwi we need the result of the typer processing + # to be used in the task classes such that typer + # should not exit from its operation. + Cli.cli() + (exit_code,) = sys_exit.call_args[0] + if not Cli.cli_ok: + sys.exit(exit_code) + + self.command_args = self.get_command_args( + raise_if_no_command=False ) - self.command_args = self.all_args[''] self.command_loaded = None - def show_and_exit_on_help_request(self): + def version(self, perform: bool): + if perform: + print(f'KIWI (next generation) version {__version__}') + raise typer.Exit(0) + + @staticmethod + @cli.callback() + def main( + color_output: Annotated[ + bool, typer.Option( + '--color-output', + help='use colors for warning and error messages' + ) + ] = False, + config: Annotated[ + Optional[Path], typer.Option( + help='use specified runtime configuration file. If ' + 'not specified the runtime configuration is looked ' + 'up at ~/.config/kiwi/config.yml or /etc/kiwi.yml' + ) + ] = None, + debug: Annotated[ + bool, typer.Option( + '--debug', + help='print debug information, same as: --loglevel 10' + ) + ] = False, + debug_run_scripts_in_screen: Annotated[ + bool, typer.Option( + '--debug-run-scripts-in-screen', + help='run scripts called by kiwi in a screen session' + ) + ] = False, + kiwi_file: Annotated[ + Optional[str], typer.Option( + help=' Basename of kiwi file which contains ' + 'the main image configuration elements. If not specified ' + 'kiwi searches for a file named config.xml or a file ' + 'matching *.kiwi' + ) + ] = None, + logfile: Annotated[ + Optional[Path], typer.Option( + help=' create a log file containing all log ' + 'information including debug information even if this ' + 'was not requested by the debug switch. The special ' + 'call: "--logfile stdout" sends all information to ' + 'standard out instead of writing to a file' + ) + ] = None, + logsocket: Annotated[ + Optional[Path], typer.Option( + help=' send log data to the given Unix ' + 'Domain socket in the same format as with --logfile' + ) + ] = None, + loglevel: Annotated[ + Optional[int], typer.Option( + help=' specify logging level as number. ' + 'Details about the available log levels can be found at: ' + 'https://docs.python.org/3/library/logging.html#logging-levels ' + 'Setting a log level causes all message >= level to be ' + 'displayed.' + ) + ] = None, + profile: Annotated[ + Optional[List[str]], typer.Option( + help=' Profile name, multiple profiles can be selected ' + 'by passing this option multiple times' + ) + ] = [], + setenv: Annotated[ + Optional[List[str]], typer.Option( + help=' export environment variable and its ' + 'value into the caller environment. This option can be ' + 'specified multiple times ' + ) + ] = [], + shared_cache_dir: Annotated[ + Optional[Path], typer.Option( + help=' An alternative shared cache directory. ' + 'The directory is shared via bind mount between the ' + 'build host and image root system and contains ' + 'information about package repositories and their ' + 'cache and meta data.' + ) + ] = Path(os.sep + Defaults.get_shared_cache_location()), + target_arch: Annotated[ + Optional[str], typer.Option( + help=' set the image architecture. By default the host ' + 'architecture is used as the image architecture. If the ' + 'specified architecture name does not match the host ' + 'architecture and is therefore requesting a cross ' + 'architecture image build, it is important to understand ' + 'that for this process to work a preparatory step to ' + 'support the image architecture and binary format on the ' + 'building host is required first.' + ) + ] = None, + temp_dir: Annotated[ + Optional[Path], typer.Option( + help=' An alternative base temporary directory. ' + 'The provided path is used as base directory to store ' + 'temporary files and directories.' + ) + ] = Path(Defaults.get_temp_location()), + type: Annotated[ + Optional[str], typer.Option( + help=' Image build type. If not set the ' + 'default XML specified build type will be used' + ) + ] = None, + version: Annotated[ + Optional[bool], typer.Option( + '--version', help='show program version', callback=version + ) + ] = None + ): + """ + KIWI - Appliance Builder + """ + Cli.global_args['--color-output'] = color_output + Cli.global_args['--config'] = config + Cli.global_args['--debug'] = debug + Cli.global_args['--debug-run-scripts-in-screen'] = \ + debug_run_scripts_in_screen + Cli.global_args['--kiwi-file'] = kiwi_file + Cli.global_args['--logfile'] = logfile + Cli.global_args['--loglevel'] = loglevel + Cli.global_args['--logsocket'] = logsocket + Cli.global_args['--profile'] = profile + Cli.global_args['--setenv'] = setenv + Cli.global_args['--shared-cache-dir'] = f'{shared_cache_dir}' + Cli.global_args['--target-arch'] = target_arch + Cli.global_args['--temp-dir'] = f'{temp_dir}' + Cli.global_args['--type'] = type + Cli.global_args['command'] = None + Cli.global_args['image'] = False + Cli.global_args['result'] = False + Cli.global_args['system'] = False + + @staticmethod + @cli.command(help='[kiwi::COMMAND:SUBCOMMAND]') + def help(command: Annotated[str, typer.Argument()] = 'kiwi'): + manual = Help() + manual.show(command) + + @staticmethod + @image.command() + def info( + description: Annotated[ + Path, typer.Option( + help=' The description must be a directory ' + 'containing a kiwi XML description and ' + 'optional metadata files' + ) + ], + resolve_package_list: Annotated[ + Optional[bool], typer.Option( + '--resolve-package-list', + help='solve package dependencies and return a ' + 'list of all packages including their attributes ' + 'e.g. size, shasum, etc...' + ) + ] = False, + list_profiles: Annotated[ + Optional[bool], typer.Option( + '--list-profiles', + help='list profiles available for the selected/default type' + ) + ] = False, + print_kiwi_env: Annotated[ + Optional[bool], typer.Option( + '--print-kiwi-env', + help='list profiles available for the selected/default type' + ) + ] = False, + ignore_repos: Annotated[ + Optional[bool], typer.Option( + '--ignore-repos', + help='ignore all repos from the XML configuration' + ) + ] = False, + add_repo: Annotated[ + Optional[List[str]], typer.Option( + help=' Add repository ' + 'with given source, type, alias and priority. The ' + 'option can be specified multiple times' + ) + ] = [], + print_xml: Annotated[ + Optional[bool], typer.Option( + '--print-xml', + help='print image description in XML' + ) + ] = False, + print_yaml: Annotated[ + Optional[bool], typer.Option( + '--print-yaml', + help='print image description in YAML' + ) + ] = False, + print_toml: Annotated[ + Optional[bool], typer.Option( + '--print-toml', + help='print image description in TOML' + ) + ] = False + ): + """ + Provide information about the specified image description + """ + Cli.subcommand_args['info'] = { + '--description': f'{description}', + '--resolve-package-list': resolve_package_list, + '--list-profiles': list_profiles, + '--print-kiwi-env': print_kiwi_env, + '--ignore-repos': ignore_repos, + '--add-repo': add_repo, + '--print-xml': print_xml, + '--print-yaml': print_yaml, + '--print-toml': print_toml, + 'help': False + } + Cli.global_args['info'] = True + Cli.global_args['command'] = 'info' + Cli.global_args['image'] = True + Cli.cli_ok = True + + @staticmethod + @image.command() + def resize( + target_dir: Annotated[ + Path, typer.Option( + help=' The target directory ' + 'to expect image build results' + ) + ], + size: Annotated[ + str, typer.Option( + help=' New size of the image. The value is either ' + 'a size in bytes or can be specified with m=MB ' + 'or g=GB. Example: 20g' + ) + ], + root: Annotated[ + Optional[Path], typer.Option( + help=' The path to the root directory, if not ' + 'specified kiwi searches the root directory ' + 'in build/image-root below the specified target ' + 'directory' + ) + ] = None + ): + """ + For disk based images, allow to resize the image to a new + disk geometry. The additional space is free and not in use + by the image. In order to make use of the additional free + space a repartition process is required like it is provided + by kiwi's oem boot code. Therefore the resize operation is + useful for oem image builds most of the time + """ + Cli.subcommand_args['resize'] = { + '--target-dir': f'{target_dir}', + '--size': size, + '--root': root, + 'help': False + } + Cli.global_args['resize'] = True + Cli.global_args['command'] = 'resize' + Cli.global_args['image'] = True + Cli.cli_ok = True + + @staticmethod + @result.command() + def list( + target_dir: Annotated[ + Path, typer.Option( + help='the target directory to expect image build results' + ) + ] + ): + """ + List result information from a previously built image + """ + Cli.subcommand_args['list'] = { + '--target-dir': target_dir, + 'help': False + } + Cli.global_args['list'] = True + Cli.global_args['command'] = 'list' + Cli.global_args['result'] = True + Cli.cli_ok = True + + @staticmethod + @result.command() + def bundle( + target_dir: Annotated[ + Path, typer.Option( + help=' The target directory to ' + 'expect image build results' + ) + ], + id: Annotated[ + str, typer.Option( + help=' The bundle id. A free form text ' + 'appended to the version information of the result ' + 'image filename' + ) + ], + bundle_dir: Annotated[ + str, typer.Option( + help=' Directory to store the bundle results' + ) + ], + bundle_format: Annotated[ + Optional[str], typer.Option( + help=' The bundle format to create the bundle. ' + 'If provided this setting will overwrite an eventually ' + 'provided bundle_format attribute from the main ' + 'image description' + ) + ] = None, + zsync_source: Annotated[ + Optional[str], typer.Option( + help=' Specify the download ' + 'location from which the bundle file(s) can be ' + 'fetched from. The information is effective if zsync ' + 'is used to sync the bundle. The zsync control file ' + 'is only created for those bundle files which are ' + 'marked for compression because in a kiwi build ' + 'only those are meaningful for a partial binary ' + 'file download. It is expected that all files from a ' + 'bundle are placed to the same download location' + ) + ] = None, + package_as_rpm: Annotated[ + Optional[bool], typer.Option( + '--package-as-rpm', + help='Take all result files and create an rpm package out of it' + ) + ] = False + ): + """ + Create result bundle from the image build results in the + specified target directory. Each result image will contain + the specified bundle identifier as part of its filename. + Uncompressed image files will also become xz compressed + and a sha sum will be created from every result image. + """ + Cli.subcommand_args['bundle'] = { + '--target-dir': target_dir, + '--id': id, + '--bundle-dir': bundle_dir, + '--bundle-format': bundle_format, + '--zsync-source': zsync_source, + '--package-as-rpm': package_as_rpm, + 'help': False + } + Cli.global_args['bundle'] = True + Cli.global_args['command'] = 'bundle' + Cli.global_args['result'] = True + Cli.cli_ok = True + + @staticmethod + @system.command() + def build( + description: Annotated[ + Path, typer.Option( + help=' The description must be a ' + 'directory containing a kiwi XML description ' + 'and optional metadata files' + ) + ], + target_dir: Annotated[ + Path, typer.Option( + help=' The target directory to ' + 'store the system image file(s)' + ) + ], + allow_existing_root: Annotated[ + Optional[bool], typer.Option( + '--allow-existing-root', + help='Allow to use an existing root directory ' + 'from an earlier build attempt. Use with caution ' + 'this could cause an inconsistent root tree if the ' + 'existing contents does not fit to the former ' + 'image type setup' + ) + ] = False, + clear_cache: Annotated[ + Optional[bool], typer.Option( + '--clear-cache', + help='Delete repository cache for each of the ' + 'used repositories before installing any package' + ) + ] = False, + ignore_repos: Annotated[ + Optional[bool], typer.Option( + '--ignore-repos', + help='Ignore all repos from the XML configuration' + ) + ] = False, + ignore_repos_used_for_build: Annotated[ + Optional[bool], typer.Option( + '--ignore-repos-used-for-build', + help='Ignore all repos from the XML configuration ' + 'except the ones marked as imageonly' + ) + ] = False, + set_repo: Annotated[ + Optional[str], typer.Option( + help=' Overwrite the first XML ' + 'listed repository source, type, alias, priority, ' + 'imageinclude(true|false), package_gpgcheck(true|false), ' + 'list of signing_keys enclosed in curly brackets delimited ' + 'by a colon, component list for debian based repos as string ' + 'delimited by a space, main distribution name for ' + 'debian based repos, repo_gpgcheck(true|false) and ' + 'repo_sourcetype(metalink|baseurl|mirrorlist)' + ) + ] = None, + set_repo_credentials: Annotated[ + Optional[str], typer.Option( + help=' ' + 'For repo sources of the form: uri://user:pass@location, ' + 'set the user and password connected to the set-repo ' + 'specification. If the provided value describes a ' + 'filename in the filesystem, the first line of that ' + 'file is read and used as credentials information.' + ) + ] = None, + add_repo: Annotated[ + Optional[List[str]], typer.Option( + help='Same as --set-repo, but it adds the repo to the ' + 'current list of repositories. The option can be specified ' + 'multiple times' + ) + ] = [], + add_repo_credentials: Annotated[ + Optional[List[str]], typer.Option( + help='Same as --set-repo-credentials, but The first ' + '--add-repo-credentials is connected with the first ' + '--add-repo specification and so on. The option can be ' + 'specified multiple times' + ) + ] = [], + add_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Install the given package name ' + 'The option can be specified multiple times' + ) + ] = [], + add_bootstrap_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Install the given package name as ' + 'part of the early bootstrap process. The option ' + 'can be specified multiple times' + ) + ] = [], + delete_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Delete the given package name ' + 'The option can be specified multiple times' + ) + ] = [], + set_container_derived_from: Annotated[ + Optional[str], typer.Option( + help=' Overwrite the source location of the ' + 'base container for the selected image type. ' + 'The setting is only effective if the configured ' + 'image type is setup with an initial derived_from reference' + ) + ] = None, + set_container_tag: Annotated[ + Optional[str], typer.Option( + help=' Overwrite the container tag in the ' + 'container configuration. The setting is only ' + 'effective if the container configuraiton provides ' + 'an initial tag value' + ) + ] = None, + add_container_label: Annotated[ + Optional[List[str]], typer.Option( + help=' Add a container label in the ' + 'container configuration metadata. The label with ' + 'the provided key-value pair will be overwritten ' + 'in case it was already defined in the XML description. ' + 'The option can be specified multiple times' + ) + ] = [], + set_type_attr: Annotated[ + Optional[List[str]], typer.Option( + help=' Overwrite/set the attribute ' + 'with the provided value in the selected build type ' + 'section. The option can be specified multiple times' + ) + ] = [], + set_release_version: Annotated[ + Optional[str], typer.Option( + help=' Overwrite/set the release-version ' + 'element in the selected build type preferences section' + ) + ] = None, + signing_key: Annotated[ + Optional[List[Path]], typer.Option( + help=' Includes the given key-file as a trusted ' + 'key for package manager validations. The option can be ' + 'specified multiple times' + ) + ] = [], + ): + """ + Build a system image from the specified description. The + build command combines the prepare and create commands. + """ + Cli.subcommand_args['build'] = { + '--description': f'{description}', + '--target-dir': f'{target_dir}', + '--allow-existing-root': allow_existing_root, + '--clear-cache': clear_cache, + '--ignore-repos': ignore_repos, + '--ignore-repos-used-for-build': ignore_repos_used_for_build, + '--set-repo': set_repo, + '--set-repo-credentials': set_repo_credentials, + '--add-repo': add_repo, + '--add-repo-credentials': add_repo_credentials, + '--add-package': add_package, + '--add-bootstrap-package': add_bootstrap_package, + '--delete-package': delete_package, + '--set-container-derived-from': set_container_derived_from, + '--set-container-tag': set_container_tag, + '--add-container-label': add_container_label, + '--set-type-attr': set_type_attr, + '--set-release-version': set_release_version, + '--signing-key': signing_key, + 'help': False + } + Cli.global_args['build'] = True + Cli.global_args['command'] = 'build' + Cli.global_args['system'] = True + Cli.cli_ok = True + + @staticmethod + @system.command() + def prepare( + description: Annotated[ + Path, typer.Option( + help=' The description must be a ' + 'directory containing a kiwi XML description ' + 'and optional metadata files' + ) + ], + root: Annotated[ + Path, typer.Option( + help=' The path to the new root ' + 'directory of the system ' + ) + ], + allow_existing_root: Annotated[ + Optional[bool], typer.Option( + '--allow-existing-root', + help='Allow to use an existing root directory ' + 'from an earlier build attempt. Use with caution ' + 'this could cause an inconsistent root tree if the ' + 'existing contents does not fit to the former ' + 'image type setup' + ) + ] = False, + clear_cache: Annotated[ + Optional[bool], typer.Option( + '--clear-cache', + help='Delete repository cache for each of the ' + 'used repositories before installing any package' + ) + ] = False, + ignore_repos: Annotated[ + Optional[bool], typer.Option( + '--ignore-repos', + help='Ignore all repos from the XML configuration' + ) + ] = False, + ignore_repos_used_for_build: Annotated[ + Optional[bool], typer.Option( + '--ignore-repos-used-for-build', + help='Ignore all repos from the XML configuration ' + 'except the ones marked as imageonly' + ) + ] = False, + set_repo: Annotated[ + Optional[str], typer.Option( + help=' Overwrite the first XML ' + 'listed repository source, type, alias, priority, ' + 'imageinclude(true|false), package_gpgcheck(true|false), ' + 'list of signing_keys enclosed in curly brackets delimited ' + 'by a colon, component list for debian based repos as string ' + 'delimited by a space, main distribution name for ' + 'debian based repos, repo_gpgcheck(true|false) and ' + 'repo_sourcetype(metalink|baseurl|mirrorlist)' + ) + ] = None, + set_repo_credentials: Annotated[ + Optional[str], typer.Option( + help=' ' + 'For repo sources of the form: uri://user:pass@location, ' + 'set the user and password connected to the set-repo ' + 'specification. If the provided value describes a ' + 'filename in the filesystem, the first line of that ' + 'file is read and used as credentials information.' + ) + ] = None, + add_repo: Annotated[ + Optional[List[str]], typer.Option( + help='Same as --set-repo, but it adds the repo to the ' + 'current list of repositories. The option can be specified ' + 'multiple times' + ) + ] = [], + add_repo_credentials: Annotated[ + Optional[List[str]], typer.Option( + help='Same as --set-repo-credentials, but The first ' + '--add-repo-credentials is connected with the first ' + '--add-repo specification and so on. The option can be ' + 'specified multiple times' + ) + ] = [], + add_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Install the given package name ' + 'The option can be specified multiple times' + ) + ] = [], + add_bootstrap_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Install the given package name as ' + 'part of the early bootstrap process. The option ' + 'can be specified multiple times' + ) + ] = [], + delete_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Delete the given package name ' + 'The option can be specified multiple times' + ) + ] = [], + set_container_derived_from: Annotated[ + Optional[str], typer.Option( + help=' Overwrite the source location of the ' + 'base container for the selected image type. ' + 'The setting is only effective if the configured ' + 'image type is setup with an initial derived_from reference' + ) + ] = None, + set_container_tag: Annotated[ + Optional[str], typer.Option( + help=' Overwrite the container tag in the ' + 'container configuration. The setting is only ' + 'effective if the container configuraiton provides ' + 'an initial tag value' + ) + ] = None, + add_container_label: Annotated[ + Optional[List[str]], typer.Option( + help=' Add a container label in the ' + 'container configuration metadata. The label with ' + 'the provided key-value pair will be overwritten ' + 'in case it was already defined in the XML description. ' + 'The option can be specified multiple times' + ) + ] = [], + set_type_attr: Annotated[ + Optional[List[str]], typer.Option( + help=' Overwrite/set the attribute ' + 'with the provided value in the selected build type ' + 'section. The option can be specified multiple times' + ) + ] = [], + set_release_version: Annotated[ + Optional[str], typer.Option( + help=' Overwrite/set the release-version ' + 'element in the selected build type preferences section' + ) + ] = None, + signing_key: Annotated[ + Optional[List[Path]], typer.Option( + help=' Includes the given key-file as a trusted ' + 'key for package manager validations. The option can be ' + 'specified multiple times' + ) + ] = [], + ): + """ + Prepare and install a new system root tree for chroot access. + """ + Cli.subcommand_args['prepare'] = { + '--description': f'{description}', + '--root': f'{root}', + '--allow-existing-root': allow_existing_root, + '--clear-cache': clear_cache, + '--ignore-repos': ignore_repos, + '--ignore-repos-used-for-build': ignore_repos_used_for_build, + '--set-repo': set_repo, + '--set-repo-credentials': set_repo_credentials, + '--add-repo': add_repo, + '--add-repo-credentials': add_repo_credentials, + '--add-package': add_package, + '--add-bootstrap-package': add_bootstrap_package, + '--delete-package': delete_package, + '--set-container-derived-from': set_container_derived_from, + '--set-container-tag': set_container_tag, + '--add-container-label': add_container_label, + '--set-type-attr': set_type_attr, + '--set-release-version': set_release_version, + '--signing-key': signing_key, + 'help': False + } + Cli.global_args['prepare'] = True + Cli.global_args['command'] = 'prepare' + Cli.global_args['system'] = True + Cli.cli_ok = True + + @staticmethod + @system.command() + def update( + root: Annotated[ + Path, typer.Option( + help=' The path to the root directory' + ) + ], + add_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Install the given package name ' + 'The option can be specified multiple times' + ) + ] = [], + delete_package: Annotated[ + Optional[List[str]], typer.Option( + help=' Delete the given package name ' + 'The option can be specified multiple times' + ) + ] = [] + ): + """ + Update root system with latest repository updates + and optionally allow to add or delete packages. + """ + Cli.subcommand_args['update'] = { + '--root': root, + '--add-package': add_package, + '--delete-package': delete_package + } + Cli.global_args['update'] = True + Cli.global_args['command'] = 'update' + Cli.global_args['system'] = True + Cli.cli_ok = True + + @staticmethod + @system.command() + def create( + root: Annotated[ + Path, typer.Option( + help=' The path to the root directory' + ) + ], + target_dir: Annotated[ + Path, typer.Option( + help=' The target directory to ' + 'store the system image file(s)' + ) + ], + signing_key: Annotated[ + Optional[List[Path]], typer.Option( + help=' Includes the given key-file as a trusted ' + 'key for package manager validations. The option can be ' + 'specified multiple times' + ) + ] = [], + ): """ - Execute man to show the selected manual page + Create an image from the specified root directory. """ - if self.all_args['help']: - manual = Help() - manual.show('kiwi') - sys.exit(0) + Cli.subcommand_args['create'] = { + '--root': root, + '--target-dir': target_dir, + '--signing-key': signing_key + } + Cli.global_args['create'] = True + Cli.global_args['command'] = 'create' + Cli.global_args['system'] = True + Cli.cli_ok = True def get_servicename(self): """ @@ -174,11 +915,11 @@ def get_servicename(self): :rtype: str """ - if self.all_args.get('image') is True: + if Cli.global_args.get('image') is True: return 'image' - elif self.all_args.get('system') is True: + elif Cli.global_args.get('system') is True: return 'system' - elif self.all_args.get('result') is True: + elif Cli.global_args.get('result') is True: return 'result' else: raise KiwiUnknownServiceName( @@ -192,11 +933,12 @@ def get_command(self): :return: command name :rtype: str """ - return self.all_args[''] + return Cli.global_args['command'] - def get_command_args(self): + def get_command_args(self, raise_if_no_command: bool = True) -> Dict: """ - Extract argument dict for selected command + Get argument dict for selected command + including global options :return: Contains dictionary of command arguments @@ -204,16 +946,26 @@ def get_command_args(self): .. code:: python { - '--command-option': 'value' + '--some-option-name': 'value' } :rtype: dict """ - return self._load_command_args() + result = Cli.global_args + command = self.get_command() + if Cli.subcommand_args.get(command): + result.update( + Cli.subcommand_args.get(command) or {} + ) + elif raise_if_no_command: + raise KiwiCommandNotLoaded( + f'{command} command not loaded' + ) + return result def get_global_args(self): """ - Extract argument dict for global arguments + Get argument dict for global arguments :return: Contains dictionary of global arguments @@ -221,25 +973,21 @@ def get_global_args(self): .. code:: python { - '--global-option': 'value' + '--some-global-option': 'value' } :rtype: dict """ result = {} - for arg, value in list(self.all_args.items()): - if not arg == '' and not arg == '': + for arg, value in list(Cli.global_args.items()): + if not arg == 'command': if arg == '--type' and value == 'vmx': log.warning( 'vmx type is now a subset of oem, --type set to oem' ) value = 'oem' - if arg == '--shared-cache-dir' and not value: - value = os.sep + Defaults.get_shared_cache_location() if arg == '--shared-cache-dir' and value: Defaults.set_shared_cache_location(value) - if arg == '--temp-dir' and not value: - value = Defaults.get_temp_location() if arg == '--temp-dir' and value: Defaults.set_temp_location(value) if arg == '--target-arch' and value: @@ -249,6 +997,45 @@ def get_global_args(self): result[arg] = value return result + def load_plugin_cli(self) -> Dict[str, typer.Typer]: + """ + Loads plugin command line interface + + The loading is based on the plugin entry point name + and requires the presence of a Typer based cli.py. + The implementation of cli.py in the plugin also + requires to provide a dict named typers. The matching + of an entry point to be a valid kiwi plugin requires + the entry point group to be set to kiwi.tasks and + the entry point value must container the _plugin + substring. + + :return: list of typer.Typer instances + + :rtype: list + """ + plugin_typers = {} + for entry in self._get_module_entries(): + if '_plugin' in entry.value: + module_name = entry.value.split('.')[0] + plugin_entry = EntryPoint( + name='cli', + value=f'{module_name}.cli', + group='kiwi.tasks' + ) + try: + plugin = plugin_entry.load() + plugin_typers.update(plugin.typers) + except ModuleNotFoundError: + # plugin gets skipped if it does not provide + # a typer based cli + pass + except Exception as issue: + raise KiwiLoadPluginError( + f'{plugin_entry}: {issue}' + ) + return plugin_typers + def load_command(self): """ Loads task class plugin according to service and command name @@ -258,14 +1045,8 @@ def load_command(self): :rtype: object """ discovered_tasks = {} - if sys.version_info >= (3, 12): - for entry in list(entry_points()): # pragma: no cover - if entry.group == 'kiwi.tasks': - discovered_tasks[entry.name] = entry.load() - else: # pragma: no cover - module_entries = dict.get(entry_points(), 'kiwi.tasks') - for entry in module_entries: - discovered_tasks[entry.name] = entry.load() + for entry in self._get_module_entries(): + discovered_tasks[entry.name] = entry.load() service = self.get_servicename() command = self.get_command() @@ -276,7 +1057,7 @@ def load_command(self): ) self.command_loaded = discovered_tasks.get( - service + '_' + command + f'{service}_{command}' ) if not self.command_loaded: prefix = 'usage:' @@ -294,13 +1075,16 @@ def load_command(self): ) return self.command_loaded - def _load_command_args(self): - try: - argv = [ - self.get_servicename(), self.get_command() - ] + self.command_args - return docopt(self.command_loaded.__doc__, argv=argv) - except Exception: - raise KiwiCommandNotLoaded( - f'{self.get_command()} command not loaded' - ) + def _get_module_entries(self) -> List[EntryPoint]: + """ + Lookup module entries matching the kiwi.tasks entry group + """ + entries: List[EntryPoint] = [] + if sys.version_info >= (3, 12): + for entry in list(entry_points()): # pragma: no cover + if entry.group == 'kiwi.tasks': + entries.append(entry) + else: # pragma: no cover + for entry in dict.get(entry_points(), 'kiwi.tasks') or {}: + entries.append(entry) + return entries diff --git a/kiwi/exceptions.py b/kiwi/exceptions.py index 7f91736d355..a07335296d9 100644 --- a/kiwi/exceptions.py +++ b/kiwi/exceptions.py @@ -387,6 +387,12 @@ class KiwiLoadCommandUndefined(KiwiError): """ +class KiwiLoadPluginError(KiwiError): + """ + Exception raised if loading the plugin has failed + """ + + class KiwiLogFileSetupFailed(KiwiError): """ Exception raised if the log file could not be created. diff --git a/kiwi/kiwi.py b/kiwi/kiwi.py index e9eea023135..ede1f4bb837 100644 --- a/kiwi/kiwi.py +++ b/kiwi/kiwi.py @@ -16,43 +16,15 @@ # along with kiwi. If not, see # import sys -import docopt import logging # project from kiwi.app import App from kiwi.exceptions import KiwiError -from kiwi.defaults import Defaults log = logging.getLogger('kiwi') -def extras(help_, version, options, doc): - """ - Overwritten method from docopt - - Shows our own usage message for -h|--help - - :param bool help_: indicate to show help - :param string version: version string - :param list options: - - list of option tuples - - .. code:: python - - [option(name='name', value='value')] - - :param string doc: docopt doc string - """ - if help_ and any((o.name in ('-h', '--help')) and o.value for o in options): - usage(doc.strip("\n")) - sys.exit(1) - if version and any(o.name == '--version' and o.value for o in options): - print(version) - sys.exit(0) - - def main(): """ kiwi - main application entry point @@ -63,7 +35,6 @@ def main(): which is handled as unexpected error including the python backtrace """ - docopt.__dict__['extras'] = extras try: App() except KiwiError as e: @@ -73,10 +44,6 @@ def main(): except KeyboardInterrupt: log.error('kiwi aborted by keyboard interrupt') sys.exit(1) - except docopt.DocoptExit as e: - # exception thrown by docopt, results in usage message - usage(e) - sys.exit(1) except SystemExit as e: # user exception, program aborted by user sys.exit(e) @@ -84,38 +51,3 @@ def main(): # exception we did no expect, show python backtrace log.error('Unexpected error:') raise - - -def usage(command_usage): - """ - Instead of the docopt way to show the usage information we - provide a kiwi specific usage information. The usage - data now always consists out of: - - 1. the generic call - kiwi-ng [global options] service [] - - 2. the command specific usage defined by the docopt string - short form by default, long form with -h | --help - - 3. the global options - - :param string command_usage: usage data - """ - with open(Defaults.project_file('cli.py'), 'r') as cli: - program_code = cli.readlines() - - global_options = '\n' - process_lines = False - for line in program_code: - if line.rstrip().startswith('global options'): - process_lines = True - if line.rstrip() == '"""': - process_lines = False - if process_lines: - global_options += format(line) - - print('usage: kiwi-ng [global options] service []\n') - print(format(command_usage).replace('usage:', ' ')) - if 'global options' not in format(command_usage): - print(format(global_options)) diff --git a/kiwi/tasks/base.py b/kiwi/tasks/base.py index 21a1ee92789..2bb0c1cbdd0 100644 --- a/kiwi/tasks/base.py +++ b/kiwi/tasks/base.py @@ -56,9 +56,6 @@ def __init__(self, should_perform_task_setup: bool = True) -> None: # initialize runtime checker self.runtime_checker: Optional[RuntimeChecker] = None - # help requested - self.cli.show_and_exit_on_help_request() - # load/import task module self.task = self.cli.load_command() diff --git a/kiwi/tasks/image_info.py b/kiwi/tasks/image_info.py index fe07838b7ad..4637faccd6c 100644 --- a/kiwi/tasks/image_info.py +++ b/kiwi/tasks/image_info.py @@ -15,40 +15,6 @@ # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # -""" -usage: kiwi-ng image info -h | --help - kiwi-ng image info --description= - [--resolve-package-list] - [--list-profiles] - [--print-kiwi-env] - [--ignore-repos] - [--add-repo=...] - [--print-xml|--print-yaml|--print-toml] - kiwi-ng image info help - -commands: - info - provide information about the specified image description - -options: - --add-repo= - add repository with given source, type, alias and priority - --description= - the description must be a directory containing a kiwi XML - description and optional metadata files - --ignore-repos - ignore all repos from the XML configuration - --resolve-package-list - solve package dependencies and return a list of all - packages including their attributes e.g size, - shasum, etc... - --list-profiles - list profiles available for the selected/default type - --print-kiwi-env - print kiwi profile environment variables - --print-xml|--print-yaml|--print-toml - print image description in specified format -""" import os # project diff --git a/kiwi/tasks/image_resize.py b/kiwi/tasks/image_resize.py index f21c01f794d..a83abc63122 100644 --- a/kiwi/tasks/image_resize.py +++ b/kiwi/tasks/image_resize.py @@ -15,34 +15,6 @@ # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # -""" -usage: kiwi-ng image resize -h | --help - kiwi-ng image resize --target-dir= --size= - [--root=] - kiwi-ng image resize help - -commands: - resize - for disk based images, allow to resize the image to a new - disk geometry. The additional space is free and not in use - by the image. In order to make use of the additional free - space a repartition process is required like it is provided - by kiwi's oem boot code. Therefore the resize operation is - useful for oem image builds most of the time - -options: - --root= - the path to the root directory, if not specified kiwi - searches the root directory in build/image-root below - the specified target directory - - --size= - new size of the image. The value is either a size in bytes - or can be specified with m=MB or g=GB. Example: 20g - - --target-dir= - the target directory to expect image build results -""" import os import logging diff --git a/kiwi/tasks/result_bundle.py b/kiwi/tasks/result_bundle.py index 89debcd85bb..0a740e7ab93 100644 --- a/kiwi/tasks/result_bundle.py +++ b/kiwi/tasks/result_bundle.py @@ -15,49 +15,6 @@ # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # -""" -usage: kiwi-ng result bundle -h | --help - kiwi-ng result bundle --target-dir= --id= --bundle-dir= - [--bundle-format=] - [--zsync-source=] - [--package-as-rpm] - [--no-compress] - kiwi-ng result bundle help - -commands: - bundle - create result bundle from the image build results in the - specified target directory. Each result image will contain - the specified bundle identifier as part of its filename. - Uncompressed image files will also become xz compressed - and a sha sum will be created from every result image. - -options: - --bundle-dir= - directory to store the bundle results - --id= - the bundle id. A free form text appended to the version - information of the result image filename - --target-dir= - the target directory to expect image build results - --zsync-source= - specify the download location from which the bundle file(s) - can be fetched from. The information is effective if zsync is - used to sync the bundle. The zsync control file is only created - for those bundle files which are marked for compression because - in a kiwi build only those are meaningful for a partial binary - file download. It is expected that all files from a bundle - are placed to the same download location - --package-as-rpm - Take all result files and create an rpm package out of it - --bundle-format= - specify the bundle format to create the bundle. - If provided this setting will overwrite an eventually - provided bundle_format attribute from the main - image description - --no-compress - Do not compress the result image file(s) -""" from collections import OrderedDict from textwrap import dedent from typing import List diff --git a/kiwi/tasks/result_list.py b/kiwi/tasks/result_list.py index cb43b17320c..7af2211f854 100644 --- a/kiwi/tasks/result_list.py +++ b/kiwi/tasks/result_list.py @@ -15,19 +15,6 @@ # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # -""" -usage: kiwi-ng result list -h | --help - kiwi-ng result list --target-dir= - kiwi-ng result list help - -commands: - list - list result information from a previous system command - -options: - --target-dir= - the target directory as it was used in a system command -""" import os import logging diff --git a/kiwi/tasks/system_build.py b/kiwi/tasks/system_build.py index a42aa463d7f..a367622784d 100644 --- a/kiwi/tasks/system_build.py +++ b/kiwi/tasks/system_build.py @@ -15,108 +15,6 @@ # You should have received a copy of the GNU General Public License # along with kiwi. If not, see # -""" -usage: kiwi-ng system build -h | --help - kiwi-ng system build --description= --target-dir= - [--allow-existing-root] - [--clear-cache] - [--ignore-repos] - [--ignore-repos-used-for-build] - [--set-repo=] - [--set-repo-credentials=] - [--add-repo=...] - [--add-repo-credentials=...] - [--add-package=...] - [--add-bootstrap-package=...] - [--delete-package=...] - [--set-container-derived-from=] - [--set-container-tag=] - [--add-container-label=