When using the kas menu command, we need to track the repository location outside of the kas container as well. This is required to prepare the environment for a later kas-container build, as the repo path inside the container is different from the outside one. For that, the location of KAS_REPO_DIR is passed via an env-var into the container. There, this value is picked up by the menu plugin and written to the .config.yaml file as `_source_host_dir`. When running kas-container build, we check if the .config.yaml file contains this node and set the KAS_REPO_DIR accordingly. The schema is extended accordingly. Signed-off-by: Felix Moessbauer <felix.moessbauer@siemens.com> [Jan: rework _source_dir_host extraction to make it usable for menu as well] Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com>
521 lines
18 KiB
Python
521 lines
18 KiB
Python
# kas - setup tool for bitbake based projects
|
|
#
|
|
# Copyright (c) Siemens AG, 2021
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be
|
|
# included in all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
#
|
|
# Parts of this were based on kconfiglib, examples/menuconfig_example.py
|
|
#
|
|
# Copyright (c) 2011-2019, Ulf Magnusson <ulfalizer@gmail.com>
|
|
#
|
|
# Permission to use, copy, modify, and/or distribute this software for any
|
|
# purpose with or without fee is hereby granted, provided that the above
|
|
# copyright notice and this permission notice appear in all copies.
|
|
#
|
|
"""
|
|
This plugin implements the ``kas menu`` command.
|
|
|
|
When this command is executed, kas will open a configuration menu as
|
|
described by a Kconfig file. It processes any pre-existing configuration
|
|
file with saved settings, stores the final selections and invokes the build
|
|
plugin if requested by the user.
|
|
|
|
To make use of this plugin, a ``Kconfig`` file has to be provided. The
|
|
menu can define these types of configuration variables that the plugin
|
|
will translate into a kas configuration:
|
|
|
|
- kas configuration files that will be included when building the
|
|
generated configuration. Those are picked up from kconfig string
|
|
variables that have the name prefix ``KAS_INCLUDE_``.
|
|
|
|
- bitbake targets that shall be built via the generated configuration.
|
|
Those are picked up from kconfig string variables that have the name
|
|
prefix ``KAS_TARGET_``.
|
|
|
|
- The ``build_system`` that will used. The static kconfig string variable
|
|
``KAS_BUILD_SYSTEM`` defines this value which must be ``openembedded``,
|
|
``oe`` or ``isar`` is set.
|
|
|
|
- bitbake configuration variables that will be added to the
|
|
``local_conf_header`` section of the generated configuration. All other
|
|
active kconfig string, integer or hex variables are treated as such.
|
|
|
|
See https://www.kernel.org/doc/html/latest/kbuild/kconfig-language.html
|
|
for a complete documentation of the Kconfig language.
|
|
|
|
The menu plugin writes the selected configuration to a ``.config.yaml``
|
|
file in the kas work directory and also reads previous selection from such
|
|
a file if it exists. The ``.config.yaml`` both contains the selected
|
|
configuration in the ``menu_configuration`` key and also the effective
|
|
settings that can be used to invoke ``kas build`` or other kas commands.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
import pprint
|
|
import yaml
|
|
from kconfiglib import Kconfig, Symbol, Choice, KconfigError, \
|
|
expr_value, TYPE_TO_STR, MENU, COMMENT, STRING, BOOL, INT, HEX, UNKNOWN
|
|
from kas import __version__, __file_version__
|
|
from kas.context import create_global_context
|
|
from kas.config import CONFIG_YAML_FILE
|
|
from kas.repos import Repo
|
|
from kas.includehandler import load_config as load_config_yaml, \
|
|
SOURCE_DIR_OVERRIDE_KEY, SOURCE_DIR_HOST_OVERRIDE_KEY
|
|
from kas.plugins.build import Build
|
|
from kas.kasusererror import KasUserError
|
|
|
|
try:
|
|
from snack import SnackScreen, EntryWindow, ButtonChoiceWindow, \
|
|
ButtonBar, Listbox, GridFormHelp
|
|
newt_available = True
|
|
except ImportError:
|
|
newt_available = False # will be reported in run()
|
|
|
|
__license__ = 'MIT'
|
|
__copyright__ = \
|
|
'Copyright (c) 2011-2019, Ulf Magnusson <ulfalizer@gmail.com>\n' \
|
|
'Copyright (c) Siemens AG, 2021-2023'
|
|
|
|
SOURCE_DIR_HOST_ENV_KEY = '_KAS_REPO_DIR_HOST'
|
|
|
|
|
|
class VariableTypeError(KasUserError):
|
|
pass
|
|
|
|
|
|
class MissingModuleError(KasUserError):
|
|
pass
|
|
|
|
|
|
class KConfigLoadError(KasUserError):
|
|
"""
|
|
The KConfig file could not be found or is invalid
|
|
"""
|
|
pass
|
|
|
|
|
|
def check_sym_is_string(sym):
|
|
if sym.type != STRING:
|
|
raise VariableTypeError('Variable {} must be of string type'
|
|
.format(sym.name))
|
|
|
|
|
|
def str_representer(dumper, data):
|
|
style = '|' if len(data.splitlines()) > 1 else None
|
|
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style=style)
|
|
|
|
|
|
class Args:
|
|
pass
|
|
|
|
|
|
class Menu:
|
|
"""
|
|
This class implements the menu plugin for kas.
|
|
"""
|
|
|
|
name = 'menu'
|
|
helpmsg = (
|
|
'Provides a configuration menu and triggers the build of the choices.'
|
|
)
|
|
|
|
@classmethod
|
|
def setup_parser(cls, parser):
|
|
parser.add_argument('kconfig',
|
|
help='Kconfig file',
|
|
nargs='?', default='Kconfig')
|
|
|
|
def load_config(self, filename):
|
|
try:
|
|
self.orig_config, _ = load_config_yaml(filename)
|
|
except FileNotFoundError:
|
|
self.orig_config = {}
|
|
return
|
|
|
|
menu_configuration = self.orig_config.get('menu_configuration', {})
|
|
for symname in menu_configuration:
|
|
sym = self.kconf.syms.get(symname)
|
|
if not sym:
|
|
logging.warning(
|
|
'Ignoring unknown configuration variable %s in %s',
|
|
symname, filename)
|
|
continue
|
|
symvalue = menu_configuration[symname]
|
|
if sym.type == BOOL:
|
|
sym.set_value('y' if symvalue else 'n')
|
|
elif sym.type == INT:
|
|
sym.set_value(str(symvalue))
|
|
elif sym.type == HEX:
|
|
sym.set_value(str(hex(symvalue)))
|
|
else: # string
|
|
sym.set_value(symvalue)
|
|
|
|
def save_config(self, filename, top_repo_dir):
|
|
kas_includes = []
|
|
kas_targets = []
|
|
kas_build_system = None
|
|
kas_vars = {}
|
|
menu_configuration = {}
|
|
|
|
for symname in self.kconf.syms:
|
|
if symname == 'MODULES':
|
|
continue
|
|
|
|
sym = self.kconf.syms[symname]
|
|
symvalue = sym.str_value
|
|
|
|
if expr_value(sym.direct_dep) < 2:
|
|
continue
|
|
|
|
if sym.visibility == 2:
|
|
if sym.type == BOOL:
|
|
menu_configuration[symname] = symvalue == 'y'
|
|
elif sym.type == STRING:
|
|
menu_configuration[symname] = symvalue
|
|
elif sym.type == INT:
|
|
menu_configuration[symname] = int(symvalue)
|
|
elif sym.type == HEX:
|
|
menu_configuration[symname] = int(symvalue, 16)
|
|
else:
|
|
raise VariableTypeError(
|
|
'Configuration variable {} uses unsupported type'
|
|
.format(symname))
|
|
|
|
if symname.startswith('KAS_INCLUDE_'):
|
|
check_sym_is_string(sym)
|
|
if symvalue != '':
|
|
kas_includes.append(symvalue)
|
|
elif symname.startswith('KAS_TARGET_'):
|
|
check_sym_is_string(sym)
|
|
if symvalue != '':
|
|
kas_targets.append(symvalue)
|
|
elif symname == 'KAS_BUILD_SYSTEM':
|
|
check_sym_is_string(sym)
|
|
if symvalue != '':
|
|
kas_build_system = symvalue
|
|
elif sym.type in (STRING, INT, HEX):
|
|
kas_vars[symname] = symvalue
|
|
|
|
config = {
|
|
'header': {
|
|
'version': __file_version__,
|
|
'includes': kas_includes
|
|
},
|
|
'menu_configuration': menu_configuration,
|
|
SOURCE_DIR_OVERRIDE_KEY: top_repo_dir
|
|
}
|
|
|
|
if SOURCE_DIR_HOST_ENV_KEY in os.environ:
|
|
config[SOURCE_DIR_HOST_OVERRIDE_KEY] = \
|
|
os.environ[SOURCE_DIR_HOST_ENV_KEY]
|
|
if kas_build_system:
|
|
config['build_system'] = kas_build_system
|
|
if len(kas_targets) > 0:
|
|
config['target'] = kas_targets
|
|
if len(kas_vars) > 0:
|
|
config['local_conf_header'] = {
|
|
'__menu_config_vars': '\n'.join([
|
|
'{} = "{}"'.format(key, value)
|
|
for key, value in kas_vars.items()
|
|
])
|
|
}
|
|
|
|
logging.debug('Menu configuration:\n%s', pprint.pformat(config))
|
|
|
|
if config != self.orig_config:
|
|
logging.info('Saving configuration as %s', filename)
|
|
|
|
# format multi-line strings more nicely
|
|
yaml.add_representer(str, str_representer)
|
|
|
|
try:
|
|
os.rename(filename, filename + '.old')
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
with open(filename, 'w') as config_file:
|
|
config_file.write(
|
|
'#\n'
|
|
'# Automatically generated by kas {}\n'
|
|
'#\n'.format(__version__))
|
|
yaml.dump(config, config_file)
|
|
|
|
def dump_kconf_warnings(self):
|
|
if len(self.kconf.warnings) > 0:
|
|
logging.warning("\n".join(self.kconf.warnings))
|
|
self.kconf.warnings = []
|
|
|
|
def run(self, args):
|
|
if not newt_available:
|
|
raise MissingModuleError(
|
|
'Menu plugin requires \'python3-newt\' distribution package.')
|
|
|
|
ctx = create_global_context(args)
|
|
|
|
kconfig_file = os.path.abspath(args.kconfig)
|
|
try:
|
|
self.kconf = Kconfig(kconfig_file, warn_to_stderr=False)
|
|
except (KconfigError, FileNotFoundError) as err:
|
|
raise KConfigLoadError(str(err))
|
|
|
|
top_repo_path = Repo.get_root_path(os.path.dirname(kconfig_file))
|
|
config_filename = os.path.join(ctx.kas_work_dir, CONFIG_YAML_FILE)
|
|
|
|
self.load_config(config_filename)
|
|
self.dump_kconf_warnings()
|
|
|
|
menu = Menuconfig(self.kconf)
|
|
action = menu.show()
|
|
|
|
if action == 'exit':
|
|
return
|
|
|
|
self.save_config(config_filename, top_repo_path)
|
|
self.dump_kconf_warnings()
|
|
|
|
if action == 'build':
|
|
logging.debug('Starting build')
|
|
|
|
build_args = Args()
|
|
build_args.config = None
|
|
build_args.target = None
|
|
build_args.task = None
|
|
build_args.extra_bitbake_args = []
|
|
build_args.skip = None
|
|
|
|
Build().run(build_args)
|
|
|
|
|
|
class Menuconfig():
|
|
def __init__(self, kconf):
|
|
self.kconf = kconf
|
|
self.screen = None
|
|
|
|
@staticmethod
|
|
def value_str(sym):
|
|
"""
|
|
Returns the value part ("[*]", "(foo)" etc.) of a menu entry.
|
|
"""
|
|
if sym.type in (STRING, INT, HEX):
|
|
return "({})".format(sym.str_value)
|
|
|
|
# BOOL (TRISTATE not supported)
|
|
|
|
# The choice mode is an upper bound on the visibility of choice
|
|
# symbols, so we can check the choice symbols' own visibility to see
|
|
# if the choice is in y mode
|
|
if sym.choice and sym.visibility == 2:
|
|
return "(*)" if sym.choice.selection is sym else "( )"
|
|
|
|
tri_val_str = (" ", None, "*")[sym.tri_value]
|
|
|
|
if len(sym.assignable) == 1:
|
|
# Pinned to a single value
|
|
return "-{}-".format(tri_val_str)
|
|
|
|
if sym.type == BOOL:
|
|
return "[{}]".format(tri_val_str)
|
|
|
|
raise RuntimeError()
|
|
|
|
@staticmethod
|
|
def node_str(node, indent):
|
|
"""
|
|
Returns the complete menu entry text for a menu node, or "" for
|
|
invisible menu nodes. Invisible menu nodes are those that lack a prompt
|
|
or that do not have a satisfied prompt condition.
|
|
|
|
Example return value: "[*] Bool symbol (BOOL)"
|
|
|
|
The symbol name is printed in parentheses to the right of the prompt.
|
|
This is so that symbols can easily be referred to in the configuration
|
|
interface.
|
|
"""
|
|
if not node.prompt:
|
|
return ""
|
|
|
|
# Even for menu nodes for symbols and choices, it's wrong to check
|
|
# Symbol.visibility / Choice.visibility here. The reason is that a
|
|
# symbol (and a choice, in theory) can be defined in multiple
|
|
# locations, giving it multiple menu nodes, which do not necessarily
|
|
# all have the same prompt visibility. Symbol.visibility /
|
|
# Choice.visibility is calculated as the OR of the visibility of all
|
|
# the prompts.
|
|
prompt, prompt_cond = node.prompt
|
|
if not expr_value(prompt_cond):
|
|
return ""
|
|
|
|
if node.item == MENU:
|
|
return " " + indent * " " + prompt + " --->"
|
|
|
|
if type(node.item) == Choice:
|
|
return " " + indent * " " + prompt
|
|
|
|
if node.item == COMMENT:
|
|
return " " + indent * " " + "*** {} ***".format(prompt)
|
|
|
|
# Symbol
|
|
sym = node.item
|
|
|
|
if sym.type == UNKNOWN:
|
|
return ""
|
|
|
|
# {:3} sets the field width to three. Gives nice alignment for empty
|
|
# string values.
|
|
res = "{:3} {}{}".format(Menuconfig.value_str(sym), indent * " ",
|
|
prompt)
|
|
|
|
# Append a sub-menu arrow if menuconfig and enabled
|
|
if node.is_menuconfig:
|
|
res += " ---" + (">" if sym.tri_value > 0 else "-")
|
|
|
|
return res
|
|
|
|
@staticmethod
|
|
def menu_node_strings(node, indent):
|
|
items = []
|
|
|
|
while node:
|
|
string = Menuconfig.node_str(node, indent)
|
|
if string:
|
|
items.append((string, node))
|
|
|
|
if (node.list and node.item != MENU
|
|
and (type(node.item) == Choice or not node.is_menuconfig)):
|
|
items.extend(Menuconfig.menu_node_strings(node.list,
|
|
indent + 2))
|
|
|
|
node = node.next
|
|
|
|
return items
|
|
|
|
def show_menu(self, title, top_node):
|
|
selection = 0
|
|
|
|
while True:
|
|
items = Menuconfig.menu_node_strings(top_node, 0)
|
|
|
|
height = len(items)
|
|
scroll = 0
|
|
if height > self.screen.height - 13:
|
|
height = self.screen.height - 13
|
|
scroll = 1
|
|
|
|
buttons = [
|
|
('Save & Build', 'build', 'B'),
|
|
('Save & Exit', 'save', 'S'),
|
|
(' Help ', 'help', 'h'),
|
|
(' Exit ', 'exit', 'ESC')
|
|
]
|
|
buttonbar = ButtonBar(self.screen, buttons)
|
|
listbox = Listbox(height, scroll=scroll, returnExit=1)
|
|
count = 0
|
|
for string, _ in items:
|
|
listbox.append(string, count)
|
|
if (selection == count):
|
|
listbox.setCurrent(count)
|
|
count = count + 1
|
|
|
|
grid = GridFormHelp(self.screen, title, None, 1, 2)
|
|
grid.add(listbox, 0, 0, padding=(0, 0, 0, 1))
|
|
grid.add(buttonbar, 0, 1, growx=1)
|
|
grid.addHotKey(' ')
|
|
|
|
rc = grid.runOnce()
|
|
|
|
action = buttonbar.buttonPressed(rc)
|
|
if action and action != 'help':
|
|
return action
|
|
|
|
if count == 0:
|
|
continue
|
|
|
|
selection = listbox.current()
|
|
_, selected_node = items[selection]
|
|
sym = selected_node.item
|
|
|
|
if action == 'help':
|
|
prompt, _ = selected_node.prompt
|
|
if hasattr(selected_node, 'help') and selected_node.help:
|
|
help = selected_node.help
|
|
else:
|
|
help = 'No help available.'
|
|
ButtonChoiceWindow(
|
|
screen=self.screen,
|
|
title="Help on '{}'".format(prompt),
|
|
text=help,
|
|
width=60,
|
|
buttons=[' Ok '])
|
|
continue
|
|
|
|
show_submenu = False
|
|
|
|
if type(sym) == Symbol:
|
|
if rc == ' ':
|
|
if sym.type == BOOL:
|
|
sym.set_value('n' if sym.tri_value > 0 else 'y')
|
|
else:
|
|
if selected_node.is_menuconfig:
|
|
show_submenu = True
|
|
elif sym.type in (STRING, INT, HEX):
|
|
action, values = EntryWindow(
|
|
screen=self.screen,
|
|
title=sym.name,
|
|
text='Enter a %s value:' % TYPE_TO_STR[sym.type],
|
|
prompts=[('', sym.str_value)],
|
|
buttons=[(' Ok ', 'Ok'), ('Cancel', '', 'ESC')])
|
|
if action == 'Ok':
|
|
self.kconf.warnings = []
|
|
val = values[0]
|
|
if sym.type == HEX and not val.startswith('0x'):
|
|
val = '0x' + val
|
|
sym.set_value(val)
|
|
# only fetching triggers range check - how ugly...
|
|
sym.str_value
|
|
if len(self.kconf.warnings) > 0:
|
|
ButtonChoiceWindow(
|
|
screen=self.screen,
|
|
title="Invalid entry",
|
|
text="\n".join(self.kconf.warnings),
|
|
width=60,
|
|
buttons=[' Ok '])
|
|
self.kconf.warnings = []
|
|
elif selected_node.is_menuconfig and type(sym) != Choice:
|
|
show_submenu = True
|
|
|
|
if show_submenu:
|
|
submenu_title, _ = selected_node.prompt
|
|
action = self.show_menu(submenu_title,
|
|
selected_node.list)
|
|
if action != 'exit':
|
|
return action
|
|
|
|
def show(self):
|
|
self.screen = SnackScreen()
|
|
|
|
action = self.show_menu(self.kconf.mainmenu_text,
|
|
self.kconf.top_node.list)
|
|
|
|
self.screen.finish()
|
|
return action
|
|
|
|
|
|
__KAS_PLUGINS__ = [Menu]
|