76db0706f3
This patch adds a new top-level schema node `_source_dir` which is used to track the top repo dir on auto-generated kas files. This node is generated by the kas menu plugin and provides the absolute path to the top repo dir at time of invoking the plugin. When later calling any other kas operation that performs a checkout, this node is evaluated and the top repo dir is set accordingly. This tracking is required when the build command is executed from another dir than the top repo dir, as the repo-dir cannot be computed by kas in this case (the .config.yaml file is in the KAS_WORK_DIR, not in the repo dir). Proposed-by: Peter Hoyes <Peter.Hoyes@arm.com> Signed-off-by: Felix Moessbauer <felix.moessbauer@siemens.com> [Jan: aligned format-changelog text, simplified load_config returning] Signed-off-by: Jan Kiszka <jan.kiszka@siemens.com>
515 lines
18 KiB
Python
515 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
|
|
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'
|
|
|
|
|
|
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 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]
|