Message ID | 20250118205226.349745-4-oss@braunwarth.dev |
---|---|
State | Changes Requested |
Headers | show |
Series | Replace old kconfig with Kconfiglib | expand |
Hi Daniel, Instead of copying / forking Kconfiglib, what would speak against a git submodule? Cheers, Mark On Sat, Jan 18, 2025 at 9:53 PM Daniel Braunwarth <oss@braunwarth.dev> wrote: > > From: Daniel Braunwarth <daniel@braunwarth.dev> > > Signed-off-by: Daniel Braunwarth <daniel@braunwarth.dev> > --- > scripts/Kconfiglib/alldefconfig.py | 27 + > scripts/Kconfiglib/allnoconfig.py | 45 + > scripts/Kconfiglib/allyesconfig.py | 56 + > scripts/Kconfiglib/defconfig.py | 43 + > scripts/Kconfiglib/genconfig.py | 154 + > scripts/Kconfiglib/guiconfig.py | 2324 +++++++++ > scripts/Kconfiglib/kconfiglib.py | 7160 +++++++++++++++++++++++++++ > scripts/Kconfiglib/listnewconfig.py | 76 + > scripts/Kconfiglib/menuconfig.py | 3282 ++++++++++++ > scripts/Kconfiglib/oldconfig.py | 246 + > scripts/Kconfiglib/olddefconfig.py | 28 + > scripts/Kconfiglib/savedefconfig.py | 49 + > scripts/Kconfiglib/setconfig.py | 92 + > 13 files changed, 13582 insertions(+) > create mode 100755 scripts/Kconfiglib/alldefconfig.py > create mode 100755 scripts/Kconfiglib/allnoconfig.py > create mode 100755 scripts/Kconfiglib/allyesconfig.py > create mode 100755 scripts/Kconfiglib/defconfig.py > create mode 100755 scripts/Kconfiglib/genconfig.py > create mode 100755 scripts/Kconfiglib/guiconfig.py > create mode 100644 scripts/Kconfiglib/kconfiglib.py > create mode 100755 scripts/Kconfiglib/listnewconfig.py > create mode 100755 scripts/Kconfiglib/menuconfig.py > create mode 100755 scripts/Kconfiglib/oldconfig.py > create mode 100755 scripts/Kconfiglib/olddefconfig.py > create mode 100755 scripts/Kconfiglib/savedefconfig.py > create mode 100755 scripts/Kconfiglib/setconfig.py > > diff --git a/scripts/Kconfiglib/alldefconfig.py b/scripts/Kconfiglib/alldefconfig.py > new file mode 100755 > index 0000000..56c4caa > --- /dev/null > +++ b/scripts/Kconfiglib/alldefconfig.py > @@ -0,0 +1,27 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Writes a configuration file where all symbols are set to their their default > +values. > + > +The default output filename is '.config'. A different filename can be passed in > +the KCONFIG_CONFIG environment variable. > + > +Usage for the Linux kernel: > + > + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/alldefconfig.py > +""" > +import kconfiglib > + > + > +def main(): > + kconf = kconfiglib.standard_kconfig(__doc__) > + kconf.load_allconfig("alldef.config") > + print(kconf.write_config()) > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/allnoconfig.py b/scripts/Kconfiglib/allnoconfig.py > new file mode 100755 > index 0000000..de90d8b > --- /dev/null > +++ b/scripts/Kconfiglib/allnoconfig.py > @@ -0,0 +1,45 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Writes a configuration file where as many symbols as possible are set to 'n'. > + > +The default output filename is '.config'. A different filename can be passed > +in the KCONFIG_CONFIG environment variable. > + > +Usage for the Linux kernel: > + > + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/allnoconfig.py > +""" > + > +# See examples/allnoconfig_walk.py for another way to implement this script > + > +import kconfiglib > + > + > +def main(): > + kconf = kconfiglib.standard_kconfig(__doc__) > + > + # Avoid warnings that would otherwise get printed by Kconfiglib for the > + # following: > + # > + # 1. Assigning a value to a symbol without a prompt, which never has any > + # effect > + # > + # 2. Assigning values invalid for the type (only bool/tristate symbols > + # accept 0/1/2, for n/m/y). The assignments will be ignored for other > + # symbol types, which is what we want. > + kconf.warn = False > + for sym in kconf.unique_defined_syms: > + sym.set_value(2 if sym.is_allnoconfig_y else 0) > + kconf.warn = True > + > + kconf.load_allconfig("allno.config") > + > + print(kconf.write_config()) > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/allyesconfig.py b/scripts/Kconfiglib/allyesconfig.py > new file mode 100755 > index 0000000..90eb9b8 > --- /dev/null > +++ b/scripts/Kconfiglib/allyesconfig.py > @@ -0,0 +1,56 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Writes a configuration file where as many symbols as possible are set to 'y'. > + > +The default output filename is '.config'. A different filename can be passed > +in the KCONFIG_CONFIG environment variable. > + > +Usage for the Linux kernel: > + > + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/allyesconfig.py > +""" > +import kconfiglib > + > + > +def main(): > + kconf = kconfiglib.standard_kconfig(__doc__) > + > + # See allnoconfig.py > + kconf.warn = False > + > + # Try to set all symbols to 'y'. Dependencies might truncate the value down > + # later, but this will at least give the highest possible value. > + # > + # Assigning 0/1/2 to non-bool/tristate symbols has no effect (int/hex > + # symbols still take a string, because they preserve formatting). > + for sym in kconf.unique_defined_syms: > + # Set choice symbols to 'm'. This value will be ignored for choices in > + # 'y' mode (the "normal" mode), which will instead just get their > + # default selection, but will set all symbols in m-mode choices to 'm', > + # which is as high as they can go. > + # > + # Here's a convoluted example of how you might get an m-mode choice > + # even during allyesconfig: > + # > + # choice > + # tristate "weird choice" > + # depends on m > + sym.set_value(1 if sym.choice else 2) > + > + # Set all choices to the highest possible mode > + for choice in kconf.unique_choices: > + choice.set_value(2) > + > + kconf.warn = True > + > + kconf.load_allconfig("allyes.config") > + > + print(kconf.write_config()) > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/defconfig.py b/scripts/Kconfiglib/defconfig.py > new file mode 100755 > index 0000000..b179273 > --- /dev/null > +++ b/scripts/Kconfiglib/defconfig.py > @@ -0,0 +1,43 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Reads a specified configuration file, then writes a new configuration file. > +This can be used to initialize the configuration from e.g. an arch-specific > +configuration file. This input configuration file would usually be a minimal > +configuration file, as generated by e.g. savedefconfig. > + > +The default output filename is '.config'. A different filename can be passed in > +the KCONFIG_CONFIG environment variable. > +""" > +import argparse > + > +import kconfiglib > + > + > +def main(): > + parser = argparse.ArgumentParser( > + formatter_class=argparse.RawDescriptionHelpFormatter, > + description=__doc__) > + > + parser.add_argument( > + "--kconfig", > + default="Kconfig", > + help="Top-level Kconfig file (default: Kconfig)") > + > + parser.add_argument( > + "config", > + metavar="CONFIGURATION", > + help="Input configuration file") > + > + args = parser.parse_args() > + > + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) > + print(kconf.load_config(args.config)) > + print(kconf.write_config()) > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/genconfig.py b/scripts/Kconfiglib/genconfig.py > new file mode 100755 > index 0000000..62f065b > --- /dev/null > +++ b/scripts/Kconfiglib/genconfig.py > @@ -0,0 +1,154 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Generates a header file with #defines from the configuration, matching the > +format of include/generated/autoconf.h in the Linux kernel. > + > +Optionally, also writes the configuration output as a .config file. See > +--config-out. > + > +The --sync-deps, --file-list, and --env-list options generate information that > +can be used to avoid needless rebuilds/reconfigurations. > + > +Before writing a header or configuration file, Kconfiglib compares the old > +contents of the file against the new contents. If there's no change, the write > +is skipped. This avoids updating file metadata like the modification time, and > +might save work depending on your build setup. > + > +By default, the configuration is generated from '.config'. A different > +configuration file can be passed in the KCONFIG_CONFIG environment variable. > + > +A custom header string can be inserted at the beginning of generated > +configuration and header files by setting the KCONFIG_CONFIG_HEADER and > +KCONFIG_AUTOHEADER_HEADER environment variables, respectively (this also works > +for other scripts). The string is not automatically made a comment (this is by > +design, to allow anything to be added), and no trailing newline is added, so > +add '/* */', '#', and newlines as appropriate. > + > +See https://www.gnu.org/software/make/manual/make.html#Multi_002dLine for a > +handy way to define multi-line variables in makefiles, for use with custom > +headers. Remember to export the variable to the environment. > +""" > +import argparse > +import os > +import sys > + > +import kconfiglib > + > + > +DEFAULT_SYNC_DEPS_PATH = "deps/" > + > + > +def main(): > + parser = argparse.ArgumentParser( > + formatter_class=argparse.RawDescriptionHelpFormatter, > + description=__doc__) > + > + parser.add_argument( > + "--header-path", > + metavar="HEADER_FILE", > + help=""" > +Path to write the generated header file to. If not specified, the path in the > +environment variable KCONFIG_AUTOHEADER is used if it is set, and 'config.h' > +otherwise. > +""") > + > + parser.add_argument( > + "--config-out", > + metavar="CONFIG_FILE", > + help=""" > +Write the configuration to CONFIG_FILE. This is useful if you include .config > +files in Makefiles, as the generated configuration file will be a full .config > +file even if .config is outdated. The generated configuration matches what > +olddefconfig would produce. If you use sync-deps, you can include > +deps/auto.conf instead. --config-out is meant for cases where incremental build > +information isn't needed. > +""") > + > + parser.add_argument( > + "--sync-deps", > + metavar="OUTPUT_DIR", > + nargs="?", > + const=DEFAULT_SYNC_DEPS_PATH, > + help=""" > +Enable generation of symbol dependency information for incremental builds, > +optionally specifying the output directory (default: {}). See the docstring of > +Kconfig.sync_deps() in Kconfiglib for more information. > +""".format(DEFAULT_SYNC_DEPS_PATH)) > + > + parser.add_argument( > + "--file-list", > + metavar="OUTPUT_FILE", > + help=""" > +Write a list of all Kconfig files to OUTPUT_FILE, with one file per line. The > +paths are relative to $srctree (or to the current directory if $srctree is > +unset). Files appear in the order they're 'source'd. > +""") > + > + parser.add_argument( > + "--env-list", > + metavar="OUTPUT_FILE", > + help=""" > +Write a list of all environment variables referenced in Kconfig files to > +OUTPUT_FILE, with one variable per line. Each line has the format NAME=VALUE. > +Only environment variables referenced with the preprocessor $(VAR) syntax are > +included, and not variables referenced with the older $VAR syntax (which is > +only supported for backwards compatibility). > +""") > + > + parser.add_argument( > + "kconfig", > + metavar="KCONFIG", > + nargs="?", > + default="Kconfig", > + help="Top-level Kconfig file (default: Kconfig)") > + > + args = parser.parse_args() > + > + > + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) > + kconf.load_config() > + > + if args.header_path is None: > + if "KCONFIG_AUTOHEADER" in os.environ: > + kconf.write_autoconf() > + else: > + # Kconfiglib defaults to include/generated/autoconf.h to be > + # compatible with the C tools. 'config.h' is used here instead for > + # backwards compatibility. It's probably a saner default for tools > + # as well. > + kconf.write_autoconf("config.h") > + else: > + kconf.write_autoconf(args.header_path) > + > + if args.config_out is not None: > + kconf.write_config(args.config_out, save_old=False) > + > + if args.sync_deps is not None: > + kconf.sync_deps(args.sync_deps) > + > + if args.file_list is not None: > + with _open_write(args.file_list) as f: > + for path in kconf.kconfig_filenames: > + f.write(path + "\n") > + > + if args.env_list is not None: > + with _open_write(args.env_list) as f: > + for env_var in kconf.env_vars: > + f.write("{}={}\n".format(env_var, os.environ[env_var])) > + > + > +def _open_write(path): > + # Python 2/3 compatibility. io.open() is available on both, but makes > + # write() expect 'unicode' strings on Python 2. > + > + if sys.version_info[0] < 3: > + return open(path, "w") > + return open(path, "w", encoding="utf-8") > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/guiconfig.py b/scripts/Kconfiglib/guiconfig.py > new file mode 100755 > index 0000000..7804fdc > --- /dev/null > +++ b/scripts/Kconfiglib/guiconfig.py > @@ -0,0 +1,2324 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Overview > +======== > + > +A Tkinter-based menuconfig implementation, based around a treeview control and > +a help display. The interface should feel familiar to people used to qconf > +('make xconfig'). Compatible with both Python 2 and Python 3. > + > +The display can be toggled between showing the full tree and showing just a > +single menu (like menuconfig.py). Only single-menu mode distinguishes between > +symbols defined with 'config' and symbols defined with 'menuconfig'. > + > +A show-all mode is available that shows invisible items in red. > + > +Supports both mouse and keyboard controls. The following keyboard shortcuts are > +available: > + > + Ctrl-S : Save configuration > + Ctrl-O : Open configuration > + Ctrl-A : Toggle show-all mode > + Ctrl-N : Toggle show-name mode > + Ctrl-M : Toggle single-menu mode > + Ctrl-F, /: Open jump-to dialog > + ESC : Close > + > +Running > +======= > + > +guiconfig.py can be run either as a standalone executable or by calling the > +menuconfig() function with an existing Kconfig instance. The second option is a > +bit inflexible in that it will still load and save .config, etc. > + > +When run in standalone mode, the top-level Kconfig file to load can be passed > +as a command-line argument. With no argument, it defaults to "Kconfig". > + > +The KCONFIG_CONFIG environment variable specifies the .config file to load (if > +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. > + > +When overwriting a configuration file, the old version is saved to > +<filename>.old (e.g. .config.old). > + > +$srctree is supported through Kconfiglib. > +""" > + > +# Note: There's some code duplication with menuconfig.py below, especially for > +# the help text. Maybe some of it could be moved into kconfiglib.py or a shared > +# helper script, but OTOH it's pretty nice to have things standalone and > +# customizable. > + > +import errno > +import os > +import sys > + > +_PY2 = sys.version_info[0] < 3 > + > +if _PY2: > + # Python 2 > + from Tkinter import * > + import ttk > + import tkFont as font > + import tkFileDialog as filedialog > + import tkMessageBox as messagebox > +else: > + # Python 3 > + from tkinter import * > + import tkinter.ttk as ttk > + import tkinter.font as font > + from tkinter import filedialog, messagebox > + > +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ > + BOOL, TRISTATE, STRING, INT, HEX, \ > + AND, OR, \ > + expr_str, expr_value, split_expr, \ > + standard_sc_expr_str, \ > + TRI_TO_STR, TYPE_TO_STR, \ > + standard_kconfig, standard_config_filename > + > + > +# If True, use GIF image data embedded in this file instead of separate GIF > +# files. See _load_images(). > +_USE_EMBEDDED_IMAGES = True > + > + > +# Help text for the jump-to dialog > +_JUMP_TO_HELP = """\ > +Type one or more strings/regexes and press Enter to list items that match all > +of them. Python's regex flavor is used (see the 're' module). Double-clicking > +an item will jump to it. Item values can be toggled directly within the dialog.\ > +""" > + > + > +def _main(): > + menuconfig(standard_kconfig(__doc__)) > + > + > +# Global variables used below: > +# > +# _root: > +# The Toplevel instance for the main window > +# > +# _tree: > +# The Treeview in the main window > +# > +# _jump_to_tree: > +# The Treeview in the jump-to dialog. None if the jump-to dialog isn't > +# open. Doubles as a flag. > +# > +# _jump_to_matches: > +# List of Nodes shown in the jump-to dialog > +# > +# _menupath: > +# The Label that shows the menu path of the selected item > +# > +# _backbutton: > +# The button shown in single-menu mode for jumping to the parent menu > +# > +# _status_label: > +# Label with status text shown at the bottom of the main window > +# ("Modified", "Saved to ...", etc.) > +# > +# _id_to_node: > +# We can't use Node objects directly as Treeview item IDs, so we use their > +# id()s instead. This dictionary maps Node id()s back to Nodes. (The keys > +# are actually str(id(node)), just to simplify lookups.) > +# > +# _cur_menu: > +# The current menu. Ignored outside single-menu mode. > +# > +# _show_all_var/_show_name_var/_single_menu_var: > +# Tkinter Variable instances bound to the corresponding checkboxes > +# > +# _show_all/_single_menu: > +# Plain Python bools that track _show_all_var and _single_menu_var, to > +# speed up and simplify things a bit > +# > +# _conf_filename: > +# File to save the configuration to > +# > +# _minconf_filename: > +# File to save minimal configurations to > +# > +# _conf_changed: > +# True if the configuration has been changed. If False, we don't bother > +# showing the save-and-quit dialog. > +# > +# We reset this to False whenever the configuration is saved. > +# > +# _*_img: > +# PhotoImage instances for images > + > + > +def menuconfig(kconf): > + """ > + Launches the configuration interface, returning after the user exits. > + > + kconf: > + Kconfig instance to be configured > + """ > + global _kconf > + global _conf_filename > + global _minconf_filename > + global _jump_to_tree > + global _cur_menu > + > + _kconf = kconf > + > + _jump_to_tree = None > + > + _create_id_to_node() > + > + _create_ui() > + > + # Filename to save configuration to > + _conf_filename = standard_config_filename() > + > + # Load existing configuration and check if it's outdated > + _set_conf_changed(_load_config()) > + > + # Filename to save minimal configuration to > + _minconf_filename = "defconfig" > + > + # Current menu in single-menu mode > + _cur_menu = _kconf.top_node > + > + # Any visible items in the top menu? > + if not _shown_menu_nodes(kconf.top_node): > + # Nothing visible. Start in show-all mode and try again. > + _show_all_var.set(True) > + if not _shown_menu_nodes(kconf.top_node): > + # Give up and show an error. It's nice to be able to assume that > + # the tree is non-empty in the rest of the code. > + _root.wait_visibility() > + messagebox.showerror( > + "Error", > + "Empty configuration -- nothing to configure.\n\n" > + "Check that environment variables are set properly.") > + _root.destroy() > + return > + > + # Build the initial tree > + _update_tree() > + > + # Select the first item and focus the Treeview, so that keyboard controls > + # work immediately > + _select(_tree, _tree.get_children()[0]) > + _tree.focus_set() > + > + # Make geometry information available for centering the window. This > + # indirectly creates the window, so hide it so that it's never shown at the > + # old location. > + _root.withdraw() > + _root.update_idletasks() > + > + # Center the window > + _root.geometry("+{}+{}".format( > + (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2, > + (_root.winfo_screenheight() - _root.winfo_reqheight())//2)) > + > + # Show it > + _root.deiconify() > + > + # Prevent the window from being automatically resized. Otherwise, it > + # changes size when scrollbars appear/disappear before the user has > + # manually resized it. > + _root.geometry(_root.geometry()) > + > + _root.mainloop() > + > + > +def _load_config(): > + # Loads any existing .config file. See the Kconfig.load_config() docstring. > + # > + # Returns True if .config is missing or outdated. We always prompt for > + # saving the configuration in that case. > + > + print(_kconf.load_config()) > + if not os.path.exists(_conf_filename): > + # No .config > + return True > + > + return _needs_save() > + > + > +def _needs_save(): > + # Returns True if a just-loaded .config file is outdated (would get > + # modified when saving) > + > + if _kconf.missing_syms: > + # Assignments to undefined symbols in the .config > + return True > + > + for sym in _kconf.unique_defined_syms: > + if sym.user_value is None: > + if sym.config_string: > + # Unwritten symbol > + return True > + elif sym.orig_type in (BOOL, TRISTATE): > + if sym.tri_value != sym.user_value: > + # Written bool/tristate symbol, new value > + return True > + elif sym.str_value != sym.user_value: > + # Written string/int/hex symbol, new value > + return True > + > + # No need to prompt for save > + return False > + > + > +def _create_id_to_node(): > + global _id_to_node > + > + _id_to_node = {str(id(node)): node for node in _kconf.node_iter()} > + > + > +def _create_ui(): > + # Creates the main window UI > + > + global _root > + global _tree > + > + # Create the root window. This initializes Tkinter and makes e.g. > + # PhotoImage available, so do it early. > + _root = Tk() > + > + _load_images() > + _init_misc_ui() > + _fix_treeview_issues() > + > + _create_top_widgets() > + # Create the pane with the Kconfig tree and description text > + panedwindow, _tree = _create_kconfig_tree_and_desc(_root) > + panedwindow.grid(column=0, row=1, sticky="nsew") > + _create_status_bar() > + > + _root.columnconfigure(0, weight=1) > + # Only the pane with the Kconfig tree and description grows vertically > + _root.rowconfigure(1, weight=1) > + > + # Start with show-name disabled > + _do_showname() > + > + _tree.bind("<Left>", _tree_left_key) > + _tree.bind("<Right>", _tree_right_key) > + # Note: Binding this for the jump-to tree as well would cause issues due to > + # the Tk bug mentioned in _tree_open() > + _tree.bind("<<TreeviewOpen>>", _tree_open) > + # add=True to avoid overriding the description text update > + _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True) > + > + _root.bind("<Control-s>", _save) > + _root.bind("<Control-o>", _open) > + _root.bind("<Control-a>", _toggle_showall) > + _root.bind("<Control-n>", _toggle_showname) > + _root.bind("<Control-m>", _toggle_tree_mode) > + _root.bind("<Control-f>", _jump_to_dialog) > + _root.bind("/", _jump_to_dialog) > + _root.bind("<Escape>", _on_quit) > + > + > +def _load_images(): > + # Loads GIF images, creating the global _*_img PhotoImage variables. > + # Base64-encoded images embedded in this script are used if > + # _USE_EMBEDDED_IMAGES is True, and separate image files in the same > + # directory as the script otherwise. > + # > + # Using a global variable indirectly prevents the image from being > + # garbage-collected. Passing an image to a Tkinter function isn't enough to > + # keep it alive. > + > + def load_image(name, data): > + var_name = "_{}_img".format(name) > + > + if _USE_EMBEDDED_IMAGES: > + globals()[var_name] = PhotoImage(data=data, format="gif") > + else: > + globals()[var_name] = PhotoImage( > + file=os.path.join(os.path.dirname(__file__), name + ".gif"), > + format="gif") > + > + # Note: Base64 data can be put on the clipboard with > + # $ base64 -w0 foo.gif | xclip > + > + load_image("icon", "R0lGODlhMAAwAPEDAAAAAADQAO7u7v///yH5BAUKAAMALAAAAAAwADAAAAL/nI+gy+2Pokyv2jazuZxryQjiSJZmyXxHeLbumH6sEATvW8OLNtf5bfLZRLFITzgEipDJ4mYxYv6A0ubuqYhWk66tVTE4enHer7jcKvt0LLUw6P45lvEprT6c0+v7OBuqhYdHohcoqIbSAHc4ljhDwrh1UlgSydRCWWlp5wiYZvmSuSh4IzrqV6p4cwhkCsmY+nhK6uJ6t1mrOhuJqfu6+WYiCiwl7HtLjNSZZZis/MeM7NY3TaRKS40ooDeoiVqIultsrav92bi9c3a5KkkOsOJZpSS99m4k/0zPng4Gks9JSbB+8DIcoQfnjwpZCHv5W+ip4aQrKrB0uOikYhiMCBw1/uPoQUMBADs=") > + load_image("n_bool", "R0lGODdhEAAQAPAAAAgICP///ywAAAAAEAAQAAACIISPacHtvp5kcb5qG85hZ2+BkyiRF8BBaEqtrKkqslEAADs=") > + load_image("y_bool", "R0lGODdhEAAQAPEAAAgICADQAP///wAAACwAAAAAEAAQAAACMoSPacLtvlh4YrIYsst2cV19AvaVF9CUXBNJJoum7ymrsKuCnhiupIWjSSjAFuWhSCIKADs=") > + load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=") > + load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=") > + load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7") > + load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=") > + load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==") > + load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==") > + load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==") > + load_image("y_locked", "R0lGODlhEAAQAPD/AAAAAADQACH5BAUKAAIALAAAAAAQABAAAAIylC8AyKzNgnlCtoDTwvZwrHydIYpQmR3KWq4uK74IOnp0HQPmnD3cOVlUIAgKsShkFAAAOw==") > + load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7") > + load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=") > + load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=") > + > + > +def _fix_treeview_issues(): > + # Fixes some Treeview issues > + > + global _treeview_rowheight > + > + style = ttk.Style() > + > + # The treeview rowheight isn't adjusted automatically on high-DPI displays, > + # so do it ourselves. The font will probably always be TkDefaultFont, but > + # play it safe and look it up. > + > + _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \ > + .metrics("linespace") + 2 > + > + style.configure("Treeview", rowheight=_treeview_rowheight) > + > + # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae, > + # which breaks tag background colors > + > + for option in "foreground", "background": > + # Filter out any styles starting with ("!disabled", "!selected", ...). > + # style.map() returns an empty list for missing options, so this should > + # be future-safe. > + style.map( > + "Treeview", > + **{option: [elm for elm in style.map("Treeview", query_opt=option) > + if elm[:2] != ("!disabled", "!selected")]}) > + > + > +def _init_misc_ui(): > + # Does misc. UI initialization, like setting the title, icon, and theme > + > + _root.title(_kconf.mainmenu_text) > + # iconphoto() isn't available in Python 2's Tkinter > + _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img) > + # Reducing the width of the window to 1 pixel makes it move around, at > + # least on GNOME. Prevent weird stuff like that. > + _root.minsize(128, 128) > + _root.protocol("WM_DELETE_WINDOW", _on_quit) > + > + # Use the 'clam' theme on *nix if it's available. It looks nicer than the > + # 'default' theme. > + if _root.tk.call("tk", "windowingsystem") == "x11": > + style = ttk.Style() > + if "clam" in style.theme_names(): > + style.theme_use("clam") > + > + > +def _create_top_widgets(): > + # Creates the controls above the Kconfig tree in the main window > + > + global _show_all_var > + global _show_name_var > + global _single_menu_var > + global _menupath > + global _backbutton > + > + topframe = ttk.Frame(_root) > + topframe.grid(column=0, row=0, sticky="ew") > + > + ttk.Button(topframe, text="Save", command=_save) \ > + .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c") > + > + ttk.Button(topframe, text="Save as...", command=_save_as) \ > + .grid(column=1, row=0, sticky="ew") > + > + ttk.Button(topframe, text="Save minimal (advanced)...", > + command=_save_minimal) \ > + .grid(column=2, row=0, sticky="ew", padx=".05c") > + > + ttk.Button(topframe, text="Open...", command=_open) \ > + .grid(column=3, row=0) > + > + ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \ > + .grid(column=4, row=0, padx=".05c") > + > + _show_name_var = BooleanVar() > + ttk.Checkbutton(topframe, text="Show name", command=_do_showname, > + variable=_show_name_var) \ > + .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c", > + ipady=".2c") > + > + _show_all_var = BooleanVar() > + ttk.Checkbutton(topframe, text="Show all", command=_do_showall, > + variable=_show_all_var) \ > + .grid(column=1, row=1, sticky="nsew", pady="0 .05c") > + > + # Allow the show-all and single-menu status to be queried via plain global > + # Python variables, which is faster and simpler > + > + def show_all_updated(*_): > + global _show_all > + _show_all = _show_all_var.get() > + > + _trace_write(_show_all_var, show_all_updated) > + _show_all_var.set(False) > + > + _single_menu_var = BooleanVar() > + ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode, > + variable=_single_menu_var) \ > + .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c") > + > + _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu, > + state="disabled") > + _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c") > + > + def tree_mode_updated(*_): > + global _single_menu > + _single_menu = _single_menu_var.get() > + > + if _single_menu: > + _backbutton.grid() > + else: > + _backbutton.grid_remove() > + > + _trace_write(_single_menu_var, tree_mode_updated) > + _single_menu_var.set(False) > + > + # Column to the right of the buttons that the menu path extends into, so > + # that it can grow wider than the buttons > + topframe.columnconfigure(5, weight=1) > + > + _menupath = ttk.Label(topframe) > + _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c", > + pady="0 .05c") > + > + > +def _create_kconfig_tree_and_desc(parent): > + # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text > + # that shows a description of the selected node. Returns a tuple with the > + # Panedwindow and the Treeview. This code is shared between the main window > + # and the jump-to dialog. > + > + panedwindow = ttk.Panedwindow(parent, orient=VERTICAL) > + > + tree_frame, tree = _create_kconfig_tree(panedwindow) > + desc_frame, desc = _create_kconfig_desc(panedwindow) > + > + panedwindow.add(tree_frame, weight=1) > + panedwindow.add(desc_frame) > + > + def tree_select(_): > + # The Text widget does not allow editing the text in its disabled > + # state. We need to temporarily enable it. > + desc["state"] = "normal" > + > + sel = tree.selection() > + if not sel: > + desc.delete("1.0", "end") > + desc["state"] = "disabled" > + return > + > + # Text.replace() is not available in Python 2's Tkinter > + desc.delete("1.0", "end") > + desc.insert("end", _info_str(_id_to_node[sel[0]])) > + > + desc["state"] = "disabled" > + > + tree.bind("<<TreeviewSelect>>", tree_select) > + tree.bind("<1>", _tree_click) > + tree.bind("<Double-1>", _tree_double_click) > + tree.bind("<Return>", _tree_enter) > + tree.bind("<KP_Enter>", _tree_enter) > + tree.bind("<space>", _tree_toggle) > + tree.bind("n", _tree_set_val(0)) > + tree.bind("m", _tree_set_val(1)) > + tree.bind("y", _tree_set_val(2)) > + > + return panedwindow, tree > + > + > +def _create_kconfig_tree(parent): > + # Creates a Treeview for showing Kconfig nodes > + > + frame = ttk.Frame(parent) > + > + tree = ttk.Treeview(frame, selectmode="browse", height=20, > + columns=("name",)) > + tree.heading("#0", text="Option", anchor="w") > + tree.heading("name", text="Name", anchor="w") > + > + tree.tag_configure("n-bool", image=_n_bool_img) > + tree.tag_configure("y-bool", image=_y_bool_img) > + tree.tag_configure("m-tri", image=_m_tri_img) > + tree.tag_configure("n-tri", image=_n_tri_img) > + tree.tag_configure("m-tri", image=_m_tri_img) > + tree.tag_configure("y-tri", image=_y_tri_img) > + tree.tag_configure("m-my", image=_m_my_img) > + tree.tag_configure("y-my", image=_y_my_img) > + tree.tag_configure("n-locked", image=_n_locked_img) > + tree.tag_configure("m-locked", image=_m_locked_img) > + tree.tag_configure("y-locked", image=_y_locked_img) > + tree.tag_configure("not-selected", image=_not_selected_img) > + tree.tag_configure("selected", image=_selected_img) > + tree.tag_configure("edit", image=_edit_img) > + tree.tag_configure("invisible", foreground="red") > + > + tree.grid(column=0, row=0, sticky="nsew") > + > + _add_vscrollbar(frame, tree) > + > + frame.columnconfigure(0, weight=1) > + frame.rowconfigure(0, weight=1) > + > + # Create items for all menu nodes. These can be detached/moved later. > + # Micro-optimize this a bit. > + insert = tree.insert > + id_ = id > + Symbol_ = Symbol > + for node in _kconf.node_iter(): > + item = node.item > + insert("", "end", iid=id_(node), > + values=item.name if item.__class__ is Symbol_ else "") > + > + return frame, tree > + > + > +def _create_kconfig_desc(parent): > + # Creates a Text for showing the description of the selected Kconfig node > + > + frame = ttk.Frame(parent) > + > + desc = Text(frame, height=12, wrap="none", borderwidth=0, > + state="disabled") > + desc.grid(column=0, row=0, sticky="nsew") > + > + # Work around not being to Ctrl-C/V text from a disabled Text widget, with a > + # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only > + desc.bind("<1>", lambda _: desc.focus_set()) > + > + _add_vscrollbar(frame, desc) > + > + frame.columnconfigure(0, weight=1) > + frame.rowconfigure(0, weight=1) > + > + return frame, desc > + > + > +def _add_vscrollbar(parent, widget): > + # Adds a vertical scrollbar to 'widget' that's only shown as needed > + > + vscrollbar = ttk.Scrollbar(parent, orient="vertical", > + command=widget.yview) > + vscrollbar.grid(column=1, row=0, sticky="ns") > + > + def yscrollcommand(first, last): > + # Only show the scrollbar when needed. 'first' and 'last' are > + # strings. > + if float(first) <= 0.0 and float(last) >= 1.0: > + vscrollbar.grid_remove() > + else: > + vscrollbar.grid() > + > + vscrollbar.set(first, last) > + > + widget["yscrollcommand"] = yscrollcommand > + > + > +def _create_status_bar(): > + # Creates the status bar at the bottom of the main window > + > + global _status_label > + > + _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0") > + _status_label.grid(column=0, row=3, sticky="ew") > + > + > +def _set_status(s): > + # Sets the text in the status bar to 's' > + > + _status_label["text"] = s > + > + > +def _set_conf_changed(changed): > + # Updates the status re. whether there are unsaved changes > + > + global _conf_changed > + > + _conf_changed = changed > + if changed: > + _set_status("Modified") > + > + > +def _update_tree(): > + # Updates the Kconfig tree in the main window by first detaching all nodes > + # and then updating and reattaching them. The tree structure might have > + # changed. > + > + # If a selected/focused item is detached and later reattached, it stays > + # selected/focused. That can give multiple selections even though > + # selectmode=browse. Save and later restore the selection and focus as a > + # workaround. > + old_selection = _tree.selection() > + old_focus = _tree.focus() > + > + # Detach all tree items before re-stringing them. This is relatively fast, > + # luckily. > + _tree.detach(*_id_to_node.keys()) > + > + if _single_menu: > + _build_menu_tree() > + else: > + _build_full_tree(_kconf.top_node) > + > + _tree.selection_set(old_selection) > + _tree.focus(old_focus) > + > + > +def _build_full_tree(menu): > + # Updates the tree starting from menu.list, in full-tree mode. To speed > + # things up, only open menus are updated. The menu-at-a-time logic here is > + # to deal with invisible items that can show up outside show-all mode (see > + # _shown_full_nodes()). > + > + for node in _shown_full_nodes(menu): > + _add_to_tree(node, _kconf.top_node) > + > + # _shown_full_nodes() includes nodes from menus rooted at symbols, so > + # we only need to check "real" menus/choices here > + if node.list and not isinstance(node.item, Symbol): > + if _tree.item(id(node), "open"): > + _build_full_tree(node) > + else: > + # We're just probing here, so _shown_menu_nodes() will work > + # fine, and might be a bit faster > + shown = _shown_menu_nodes(node) > + if shown: > + # Dummy element to make the open/closed toggle appear > + _tree.move(id(shown[0]), id(shown[0].parent), "end") > + > + > +def _shown_full_nodes(menu): > + # Returns the list of menu nodes shown in 'menu' (a menu node for a menu) > + # for full-tree mode. A tricky detail is that invisible items need to be > + # shown if they have visible children. > + > + def rec(node): > + res = [] > + > + while node: > + if _visible(node) or _show_all: > + res.append(node) > + if node.list and isinstance(node.item, Symbol): > + # Nodes from menu created from dependencies > + res += rec(node.list) > + > + elif node.list and isinstance(node.item, Symbol): > + # Show invisible symbols (defined with either 'config' and > + # 'menuconfig') if they have visible children. This can happen > + # for an m/y-valued symbol with an optional prompt > + # ('prompt "foo" is COND') that is currently disabled. > + shown_children = rec(node.list) > + if shown_children: > + res.append(node) > + res += shown_children > + > + node = node.next > + > + return res > + > + return rec(menu.list) > + > + > +def _build_menu_tree(): > + # Updates the tree in single-menu mode. See _build_full_tree() as well. > + > + for node in _shown_menu_nodes(_cur_menu): > + _add_to_tree(node, _cur_menu) > + > + > +def _shown_menu_nodes(menu): > + # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't > + # include children of symbols defined with 'menuconfig'. > + > + def rec(node): > + res = [] > + > + while node: > + if _visible(node) or _show_all: > + res.append(node) > + if node.list and not node.is_menuconfig: > + res += rec(node.list) > + > + elif node.list and isinstance(node.item, Symbol): > + shown_children = rec(node.list) > + if shown_children: > + # Invisible item with visible children > + res.append(node) > + if not node.is_menuconfig: > + res += shown_children > + > + node = node.next > + > + return res > + > + return rec(menu.list) > + > + > +def _visible(node): > + # Returns True if the node should appear in the menu (outside show-all > + # mode) > + > + return node.prompt and expr_value(node.prompt[1]) and not \ > + (node.item == MENU and not expr_value(node.visibility)) > + > + > +def _add_to_tree(node, top): > + # Adds 'node' to the tree, at the end of its menu. We rely on going through > + # the nodes linearly to get the correct order. 'top' holds the menu that > + # corresponds to the top-level menu, and can vary in single-menu mode. > + > + parent = node.parent > + _tree.move(id(node), "" if parent is top else id(parent), "end") > + _tree.item( > + id(node), > + text=_node_str(node), > + # The _show_all test avoids showing invisible items in red outside > + # show-all mode, which could look confusing/broken. Invisible symbols > + # are shown outside show-all mode if an invisible symbol has visible > + # children in an implicit menu. > + tags=_img_tag(node) if _visible(node) or not _show_all else > + _img_tag(node) + " invisible") > + > + > +def _node_str(node): > + # Returns the string shown to the right of the image (if any) for the node > + > + if node.prompt: > + if node.item == COMMENT: > + s = "*** {} ***".format(node.prompt[0]) > + else: > + s = node.prompt[0] > + > + if isinstance(node.item, Symbol): > + sym = node.item > + > + # Print "(NEW)" next to symbols without a user value (from e.g. a > + # .config), but skip it for choice symbols in choices in y mode, > + # and for symbols of UNKNOWN type (which generate a warning though) > + if sym.user_value is None and sym.type and not \ > + (sym.choice and sym.choice.tri_value == 2): > + > + s += " (NEW)" > + > + elif isinstance(node.item, Symbol): > + # Symbol without prompt (can show up in show-all) > + s = "<{}>".format(node.item.name) > + > + else: > + # Choice without prompt. Use standard_sc_expr_str() so that it shows up > + # as '<choice (name if any)>'. > + s = standard_sc_expr_str(node.item) > + > + > + if isinstance(node.item, Symbol): > + sym = node.item > + if sym.orig_type == STRING: > + s += ": " + sym.str_value > + elif sym.orig_type in (INT, HEX): > + s = "({}) {}".format(sym.str_value, s) > + > + elif isinstance(node.item, Choice) and node.item.tri_value == 2: > + # Print the prompt of the selected symbol after the choice for > + # choices in y mode > + sym = node.item.selection > + if sym: > + for sym_node in sym.nodes: > + # Use the prompt used at this choice location, in case the > + # choice symbol is defined in multiple locations > + if sym_node.parent is node and sym_node.prompt: > + s += " ({})".format(sym_node.prompt[0]) > + break > + else: > + # If the symbol isn't defined at this choice location, then > + # just use whatever prompt we can find for it > + for sym_node in sym.nodes: > + if sym_node.prompt: > + s += " ({})".format(sym_node.prompt[0]) > + break > + > + # In single-menu mode, print "--->" next to nodes that have menus that can > + # potentially be entered. Print "----" if the menu is empty. We don't allow > + # those to be entered. > + if _single_menu and node.is_menuconfig: > + s += " --->" if _shown_menu_nodes(node) else " ----" > + > + return s > + > + > +def _img_tag(node): > + # Returns the tag for the image that should be shown next to 'node', or the > + # empty string if it shouldn't have an image > + > + item = node.item > + > + if item in (MENU, COMMENT) or not item.orig_type: > + return "" > + > + if item.orig_type in (STRING, INT, HEX): > + return "edit" > + > + # BOOL or TRISTATE > + > + if _is_y_mode_choice_sym(item): > + # Choice symbol in y-mode choice > + return "selected" if item.choice.selection is item else "not-selected" > + > + if len(item.assignable) <= 1: > + # Pinned to a single value > + return "" if isinstance(item, Choice) else item.str_value + "-locked" > + > + if item.type == BOOL: > + return item.str_value + "-bool" > + > + # item.type == TRISTATE > + if item.assignable == (1, 2): > + return item.str_value + "-my" > + return item.str_value + "-tri" > + > + > +def _is_y_mode_choice_sym(item): > + # 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 > + return isinstance(item, Symbol) and item.choice and item.visibility == 2 > + > + > +def _tree_click(event): > + # Click on the Kconfig Treeview > + > + tree = event.widget > + if tree.identify_element(event.x, event.y) == "image": > + item = tree.identify_row(event.y) > + # Select the item before possibly popping up a dialog for > + # string/int/hex items, so that its help is visible > + _select(tree, item) > + _change_node(_id_to_node[item], tree.winfo_toplevel()) > + return "break" > + > + > +def _tree_double_click(event): > + # Double-click on the Kconfig treeview > + > + # Do an extra check to avoid weirdness when double-clicking in the tree > + # heading area > + if not _in_heading(event): > + return _tree_enter(event) > + > + > +def _in_heading(event): > + # Returns True if 'event' took place in the tree heading > + > + tree = event.widget > + return hasattr(tree, "identify_region") and \ > + tree.identify_region(event.x, event.y) in ("heading", "separator") > + > + > +def _tree_enter(event): > + # Enter press or double-click within the Kconfig treeview. Prefer to > + # open/close/enter menus, but toggle the value if that's not possible. > + > + tree = event.widget > + sel = tree.focus() > + if sel: > + node = _id_to_node[sel] > + > + if tree.get_children(sel): > + _tree_toggle_open(sel) > + elif _single_menu_mode_menu(node, tree): > + _enter_menu_and_select_first(node) > + else: > + _change_node(node, tree.winfo_toplevel()) > + > + return "break" > + > + > +def _tree_toggle(event): > + # Space press within the Kconfig treeview. Prefer to toggle the value, but > + # open/close/enter the menu if that's not possible. > + > + tree = event.widget > + sel = tree.focus() > + if sel: > + node = _id_to_node[sel] > + > + if _changeable(node): > + _change_node(node, tree.winfo_toplevel()) > + elif _single_menu_mode_menu(node, tree): > + _enter_menu_and_select_first(node) > + elif tree.get_children(sel): > + _tree_toggle_open(sel) > + > + return "break" > + > + > +def _tree_left_key(_): > + # Left arrow key press within the Kconfig treeview > + > + if _single_menu: > + # Leave the current menu in single-menu mode > + _leave_menu() > + return "break" > + > + # Otherwise, default action > + > + > +def _tree_right_key(_): > + # Right arrow key press within the Kconfig treeview > + > + sel = _tree.focus() > + if sel: > + node = _id_to_node[sel] > + # If the node can be entered in single-menu mode, do it > + if _single_menu_mode_menu(node, _tree): > + _enter_menu_and_select_first(node) > + return "break" > + > + # Otherwise, default action > + > + > +def _single_menu_mode_menu(node, tree): > + # Returns True if single-menu mode is on and 'node' is an (interface) > + # menu that can be entered > + > + return _single_menu and tree is _tree and node.is_menuconfig and \ > + _shown_menu_nodes(node) > + > + > +def _changeable(node): > + # Returns True if 'node' is a Symbol/Choice whose value can be changed > + > + sc = node.item > + > + if not isinstance(sc, (Symbol, Choice)): > + return False > + > + # This will hit for invisible symbols, which appear in show-all mode and > + # when an invisible symbol has visible children (which can happen e.g. for > + # symbols with optional prompts) > + if not (node.prompt and expr_value(node.prompt[1])): > + return False > + > + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ > + or _is_y_mode_choice_sym(sc) > + > + > +def _tree_toggle_open(item): > + # Opens/closes the Treeview item 'item' > + > + if _tree.item(item, "open"): > + _tree.item(item, open=False) > + else: > + node = _id_to_node[item] > + if not isinstance(node.item, Symbol): > + # Can only get here in full-tree mode > + _build_full_tree(node) > + _tree.item(item, open=True) > + > + > +def _tree_set_val(tri_val): > + def tree_set_val(event): > + # n/m/y press within the Kconfig treeview > + > + # Sets the value of the currently selected item to 'tri_val', if that > + # value can be assigned > + > + sel = event.widget.focus() > + if sel: > + sc = _id_to_node[sel].item > + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: > + _set_val(sc, tri_val) > + > + return tree_set_val > + > + > +def _tree_open(_): > + # Lazily populates the Kconfig tree when menus are opened in full-tree mode > + > + if _single_menu: > + # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e > + # ("ttk::treeview open/closed indicators can be toggled while hidden"). > + # Clicking on the hidden indicator will call _build_full_tree() in > + # single-menu mode otherwise. > + return > + > + node = _id_to_node[_tree.focus()] > + # _shown_full_nodes() includes nodes from menus rooted at symbols, so we > + # only need to check "real" menus and choices here > + if not isinstance(node.item, Symbol): > + _build_full_tree(node) > + > + > +def _update_menu_path(_): > + # Updates the displayed menu path when nodes are selected in the Kconfig > + # treeview > + > + sel = _tree.selection() > + _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else "" > + > + > +def _item_row(item): > + # Returns the row number 'item' appears on within the Kconfig treeview, > + # starting from the top of the tree. Used to preserve scrolling. > + # > + # ttkTreeview.c in the Tk sources defines a RowNumber() function that does > + # the same thing, but it's not exposed. > + > + row = 0 > + > + while True: > + prev = _tree.prev(item) > + if prev: > + item = prev > + row += _n_rows(item) > + else: > + item = _tree.parent(item) > + if not item: > + return row > + row += 1 > + > + > +def _n_rows(item): > + # _item_row() helper. Returns the number of rows occupied by 'item' and # > + # its children. > + > + rows = 1 > + > + if _tree.item(item, "open"): > + for child in _tree.get_children(item): > + rows += _n_rows(child) > + > + return rows > + > + > +def _attached(item): > + # Heuristic for checking if a Treeview item is attached. Doesn't seem to be > + # good APIs for this. Might fail for super-obscure cases with tiny trees, > + # but you'd just get a small scroll mess-up. > + > + return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item)) > + > + > +def _change_node(node, parent): > + # Toggles/changes the value of 'node'. 'parent' is the parent window > + # (either the main window or the jump-to dialog), in case we need to pop up > + # a dialog. > + > + if not _changeable(node): > + return > + > + # sc = symbol/choice > + sc = node.item > + > + if sc.type in (INT, HEX, STRING): > + s = _set_val_dialog(node, parent) > + > + # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib > + # can't deal with. UTF-8-encode the string to work around it. > + if _PY2 and isinstance(s, unicode): > + s = s.encode("utf-8", "ignore") > + > + if s is not None: > + _set_val(sc, s) > + > + elif len(sc.assignable) == 1: > + # Handles choice symbols for choices in y mode, which are a special > + # case: .assignable can be (2,) while .tri_value is 0. > + _set_val(sc, sc.assignable[0]) > + > + else: > + # Set the symbol to the value after the current value in > + # sc.assignable, with wrapping > + val_index = sc.assignable.index(sc.tri_value) > + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) > + > + > +def _set_val(sc, val): > + # Wrapper around Symbol/Choice.set_value() for updating the menu state and > + # _conf_changed > + > + # Use the string representation of tristate values. This makes the format > + # consistent for all symbol types. > + if val in TRI_TO_STR: > + val = TRI_TO_STR[val] > + > + if val != sc.str_value: > + sc.set_value(val) > + _set_conf_changed(True) > + > + # Update the tree and try to preserve the scroll. Do a cheaper variant > + # than in the show-all case, that might mess up the scroll slightly in > + # rare cases, but is fast and flicker-free. > + > + stayput = _loc_ref_item() # Item to preserve scroll for > + old_row = _item_row(stayput) > + > + _update_tree() > + > + # If the reference item disappeared (can happen if the change was done > + # from the jump-to dialog), then avoid messing with the scroll and hope > + # for the best > + if _attached(stayput): > + _tree.yview_scroll(_item_row(stayput) - old_row, "units") > + > + if _jump_to_tree: > + _update_jump_to_display() > + > + > +def _set_val_dialog(node, parent): > + # Pops up a dialog for setting the value of the string/int/hex > + # symbol at node 'node'. 'parent' is the parent window. > + > + def ok(_=None): > + # No 'nonlocal' in Python 2 > + global _entry_res > + > + s = entry.get() > + if sym.type == HEX and not s.startswith(("0x", "0X")): > + s = "0x" + s > + > + if _check_valid(dialog, entry, sym, s): > + _entry_res = s > + dialog.destroy() > + > + def cancel(_=None): > + global _entry_res > + _entry_res = None > + dialog.destroy() > + > + sym = node.item > + > + dialog = Toplevel(parent) > + dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type])) > + dialog.resizable(False, False) > + dialog.transient(parent) > + dialog.protocol("WM_DELETE_WINDOW", cancel) > + > + ttk.Label(dialog, text=node.prompt[0] + ":") \ > + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c", > + pady=".2c .05c") > + > + entry = ttk.Entry(dialog, width=30) > + # Start with the previous value in the editbox, selected > + entry.insert(0, sym.str_value) > + entry.selection_range(0, "end") > + entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c") > + entry.focus_set() > + > + range_info = _range_info(sym) > + if range_info: > + ttk.Label(dialog, text=range_info) \ > + .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c", > + pady=".2c 0") > + > + ttk.Button(dialog, text="OK", command=ok) \ > + .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c", > + pady=".4c") > + > + ttk.Button(dialog, text="Cancel", command=cancel) \ > + .grid(column=1, row=4 if range_info else 3, padx="0 .3c") > + > + # Give all horizontal space to the grid cell with the OK button, so that > + # Cancel moves to the right > + dialog.columnconfigure(0, weight=1) > + > + _center_on_root(dialog) > + > + # Hack to scroll the entry so that the end of the text is shown, from > + # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail. > + # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff > + def scroll_entry(_): > + _root.update_idletasks() > + entry.unbind("<Expose>") > + entry.xview_moveto(1) > + entry.bind("<Expose>", scroll_entry) > + > + # The dialog must be visible before we can grab the input > + dialog.wait_visibility() > + dialog.grab_set() > + > + dialog.bind("<Return>", ok) > + dialog.bind("<KP_Enter>", ok) > + dialog.bind("<Escape>", cancel) > + > + # Wait for the user to be done with the dialog > + parent.wait_window(dialog) > + > + # Regrab the input in the parent > + parent.grab_set() > + > + return _entry_res > + > + > +def _center_on_root(dialog): > + # Centers 'dialog' on the root window. It often ends up at some bad place > + # like the top-left corner of the screen otherwise. See the menuconfig() > + # function, which has similar logic. > + > + dialog.withdraw() > + _root.update_idletasks() > + > + dialog_width = dialog.winfo_reqwidth() > + dialog_height = dialog.winfo_reqheight() > + > + screen_width = _root.winfo_screenwidth() > + screen_height = _root.winfo_screenheight() > + > + x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2 > + y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2 > + > + # Clamp so that no part of the dialog is outside the screen > + if x + dialog_width > screen_width: > + x = screen_width - dialog_width > + elif x < 0: > + x = 0 > + if y + dialog_height > screen_height: > + y = screen_height - dialog_height > + elif y < 0: > + y = 0 > + > + dialog.geometry("+{}+{}".format(x, y)) > + > + dialog.deiconify() > + > + > +def _check_valid(dialog, entry, sym, s): > + # Returns True if the string 's' is a well-formed value for 'sym'. > + # Otherwise, pops up an error and returns False. > + > + if sym.type not in (INT, HEX): > + # Anything goes for non-int/hex symbols > + return True > + > + base = 10 if sym.type == INT else 16 > + try: > + int(s, base) > + except ValueError: > + messagebox.showerror( > + "Bad value", > + "'{}' is a malformed {} value".format( > + s, TYPE_TO_STR[sym.type]), > + parent=dialog) > + entry.focus_set() > + return False > + > + for low_sym, high_sym, cond in sym.ranges: > + if expr_value(cond): > + low_s = low_sym.str_value > + high_s = high_sym.str_value > + > + if not int(low_s, base) <= int(s, base) <= int(high_s, base): > + messagebox.showerror( > + "Value out of range", > + "{} is outside the range {}-{}".format(s, low_s, high_s), > + parent=dialog) > + entry.focus_set() > + return False > + > + break > + > + return True > + > + > +def _range_info(sym): > + # Returns a string with information about the valid range for the symbol > + # 'sym', or None if 'sym' doesn't have a range > + > + if sym.type in (INT, HEX): > + for low, high, cond in sym.ranges: > + if expr_value(cond): > + return "Range: {}-{}".format(low.str_value, high.str_value) > + > + return None > + > + > +def _save(_=None): > + # Tries to save the configuration > + > + if _try_save(_kconf.write_config, _conf_filename, "configuration"): > + _set_conf_changed(False) > + > + _tree.focus_set() > + > + > +def _save_as(): > + # Pops up a dialog for saving the configuration to a specific location > + > + global _conf_filename > + > + filename = _conf_filename > + while True: > + filename = filedialog.asksaveasfilename( > + title="Save configuration as", > + initialdir=os.path.dirname(filename), > + initialfile=os.path.basename(filename), > + parent=_root) > + > + if not filename: > + break > + > + if _try_save(_kconf.write_config, filename, "configuration"): > + _conf_filename = filename > + break > + > + _tree.focus_set() > + > + > +def _save_minimal(): > + # Pops up a dialog for saving a minimal configuration (defconfig) to a > + # specific location > + > + global _minconf_filename > + > + filename = _minconf_filename > + while True: > + filename = filedialog.asksaveasfilename( > + title="Save minimal configuration as", > + initialdir=os.path.dirname(filename), > + initialfile=os.path.basename(filename), > + parent=_root) > + > + if not filename: > + break > + > + if _try_save(_kconf.write_min_config, filename, > + "minimal configuration"): > + > + _minconf_filename = filename > + break > + > + _tree.focus_set() > + > + > +def _open(_=None): > + # Pops up a dialog for loading a configuration > + > + global _conf_filename > + > + if _conf_changed and \ > + not messagebox.askokcancel( > + "Unsaved changes", > + "You have unsaved changes. Load new configuration anyway?"): > + > + return > + > + filename = _conf_filename > + while True: > + filename = filedialog.askopenfilename( > + title="Open configuration", > + initialdir=os.path.dirname(filename), > + initialfile=os.path.basename(filename), > + parent=_root) > + > + if not filename: > + break > + > + if _try_load(filename): > + # Maybe something fancier could be done here later to try to > + # preserve the scroll > + > + _conf_filename = filename > + _set_conf_changed(_needs_save()) > + > + if _single_menu and not _shown_menu_nodes(_cur_menu): > + # Turn on show-all if we're in single-menu mode and would end > + # up with an empty menu > + _show_all_var.set(True) > + > + _update_tree() > + > + break > + > + _tree.focus_set() > + > + > +def _toggle_showname(_): > + # Toggles show-name mode on/off > + > + _show_name_var.set(not _show_name_var.get()) > + _do_showname() > + > + > +def _do_showname(): > + # Updates the UI for the current show-name setting > + > + # Columns do not automatically shrink/expand, so we have to update > + # column widths ourselves > + > + tree_width = _tree.winfo_width() > + > + if _show_name_var.get(): > + _tree["displaycolumns"] = ("name",) > + _tree["show"] = "tree headings" > + name_width = tree_width//3 > + _tree.column("#0", width=max(tree_width - name_width, 1)) > + _tree.column("name", width=name_width) > + else: > + _tree["displaycolumns"] = () > + _tree["show"] = "tree" > + _tree.column("#0", width=tree_width) > + > + _tree.focus_set() > + > + > +def _toggle_showall(_): > + # Toggles show-all mode on/off > + > + _show_all_var.set(not _show_all) > + _do_showall() > + > + > +def _do_showall(): > + # Updates the UI for the current show-all setting > + > + # Don't allow turning off show-all if we'd end up with no visible nodes > + if _nothing_shown(): > + _show_all_var.set(True) > + return > + > + # Save scroll information. old_scroll can end up negative here, if the > + # reference item isn't shown (only invisible items on the screen, and > + # show-all being turned off). > + > + stayput = _vis_loc_ref_item() > + # Probe the middle of the first row, to play it safe. identify_row(0) seems > + # to return the row before the top row. > + old_scroll = _item_row(stayput) - \ > + _item_row(_tree.identify_row(_treeview_rowheight//2)) > + > + _update_tree() > + > + if _show_all: > + # Deep magic: Unless we call update_idletasks(), the scroll adjustment > + # below is restricted to the height of the old tree, instead of the > + # height of the new tree. Since the tree with show-all on is guaranteed > + # to be taller, and we want the maximum range, we only call it when > + # turning show-all on. > + # > + # Strictly speaking, something similar ought to be done when changing > + # symbol values, but it causes annoying flicker, and in 99% of cases > + # things work anyway there (with usually minor scroll mess-ups in the > + # 1% case). > + _root.update_idletasks() > + > + # Restore scroll > + _tree.yview(_item_row(stayput) - old_scroll) > + > + _tree.focus_set() > + > + > +def _nothing_shown(): > + # _do_showall() helper. Returns True if no nodes would get > + # shown with the current show-all setting. Also handles the > + # (obscure) case when there are no visible nodes in the entire > + # tree, meaning guiconfig was automatically started in > + # show-all mode, which mustn't be turned off. > + > + return not _shown_menu_nodes( > + _cur_menu if _single_menu else _kconf.top_node) > + > + > +def _toggle_tree_mode(_): > + # Toggles single-menu mode on/off > + > + _single_menu_var.set(not _single_menu) > + _do_tree_mode() > + > + > +def _do_tree_mode(): > + # Updates the UI for the current tree mode (full-tree or single-menu) > + > + loc_ref_node = _id_to_node[_loc_ref_item()] > + > + if not _single_menu: > + # _jump_to() -> _enter_menu() already updates the tree, but > + # _jump_to() -> load_parents() doesn't, because it isn't always needed. > + # We always need to update the tree here, e.g. to add/remove "--->". > + _update_tree() > + > + _jump_to(loc_ref_node) > + _tree.focus_set() > + > + > +def _enter_menu_and_select_first(menu): > + # Enters the menu 'menu' and selects the first item. Used in single-menu > + # mode. > + > + _enter_menu(menu) > + _select(_tree, _tree.get_children()[0]) > + > + > +def _enter_menu(menu): > + # Enters the menu 'menu'. Used in single-menu mode. > + > + global _cur_menu > + > + _cur_menu = menu > + _update_tree() > + > + _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal" > + > + > +def _leave_menu(): > + # Leaves the current menu. Used in single-menu mode. > + > + global _cur_menu > + > + if _cur_menu is not _kconf.top_node: > + old_menu = _cur_menu > + > + _cur_menu = _parent_menu(_cur_menu) > + _update_tree() > + > + _select(_tree, id(old_menu)) > + > + if _cur_menu is _kconf.top_node: > + _backbutton["state"] = "disabled" > + > + _tree.focus_set() > + > + > +def _select(tree, item): > + # Selects, focuses, and see()s 'item' in 'tree' > + > + tree.selection_set(item) > + tree.focus(item) > + tree.see(item) > + > + > +def _loc_ref_item(): > + # Returns a Treeview item that can serve as a reference for the current > + # scroll location. We try to make this item stay on the same row on the > + # screen when updating the tree. > + > + # If the selected item is visible, use that > + sel = _tree.selection() > + if sel and _tree.bbox(sel[0]): > + return sel[0] > + > + # Otherwise, use the middle item on the screen. If it doesn't exist, the > + # tree is probably really small, so use the first item in the entire tree. > + return _tree.identify_row(_tree.winfo_height()//2) or \ > + _tree.get_children()[0] > + > + > +def _vis_loc_ref_item(): > + # Like _loc_ref_item(), but finds a visible item around the reference item. > + # Used when changing show-all mode, where non-visible (red) items will > + # disappear. > + > + item = _loc_ref_item() > + > + vis_before = _vis_before(item) > + if vis_before and _tree.bbox(vis_before): > + return vis_before > + > + vis_after = _vis_after(item) > + if vis_after and _tree.bbox(vis_after): > + return vis_after > + > + return vis_before or vis_after > + > + > +def _vis_before(item): > + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, > + # searching backwards from 'item'. > + > + while item: > + if not _tree.tag_has("invisible", item): > + return item > + > + prev = _tree.prev(item) > + item = prev if prev else _tree.parent(item) > + > + return None > + > + > +def _vis_after(item): > + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, > + # searching forwards from 'item'. > + > + while item: > + if not _tree.tag_has("invisible", item): > + return item > + > + next = _tree.next(item) > + if next: > + item = next > + else: > + item = _tree.parent(item) > + if not item: > + break > + item = _tree.next(item) > + > + return None > + > + > +def _on_quit(_=None): > + # Called when the user wants to exit > + > + if not _conf_changed: > + _quit("No changes to save (for '{}')".format(_conf_filename)) > + return > + > + while True: > + ync = messagebox.askyesnocancel("Quit", "Save changes?") > + if ync is None: > + return > + > + if not ync: > + _quit("Configuration ({}) was not saved".format(_conf_filename)) > + return > + > + if _try_save(_kconf.write_config, _conf_filename, "configuration"): > + # _try_save() already prints the "Configuration saved to ..." > + # message > + _quit() > + return > + > + > +def _quit(msg=None): > + # Quits the application > + > + # Do not call sys.exit() here, in case we're being run from a script > + _root.destroy() > + if msg: > + print(msg) > + > + > +def _try_save(save_fn, filename, description): > + # Tries to save a configuration file. Pops up an error and returns False on > + # failure. > + # > + # save_fn: > + # Function to call with 'filename' to save the file > + # > + # description: > + # String describing the thing being saved > + > + try: > + # save_fn() returns a message to print > + msg = save_fn(filename) > + _set_status(msg) > + print(msg) > + return True > + except EnvironmentError as e: > + messagebox.showerror( > + "Error saving " + description, > + "Error saving {} to '{}': {} (errno: {})" > + .format(description, e.filename, e.strerror, > + errno.errorcode[e.errno])) > + return False > + > + > +def _try_load(filename): > + # Tries to load a configuration file. Pops up an error and returns False on > + # failure. > + # > + # filename: > + # Configuration file to load > + > + try: > + msg = _kconf.load_config(filename) > + _set_status(msg) > + print(msg) > + return True > + except EnvironmentError as e: > + messagebox.showerror( > + "Error loading configuration", > + "Error loading '{}': {} (errno: {})" > + .format(filename, e.strerror, errno.errorcode[e.errno])) > + return False > + > + > +def _jump_to_dialog(_=None): > + # Pops up a dialog for jumping directly to a particular node. Symbol values > + # can also be changed within the dialog. > + # > + # Note: There's nothing preventing this from doing an incremental search > + # like menuconfig.py does, but currently it's a bit jerky for large Kconfig > + # trees, at least when inputting the beginning of the search string. We'd > + # need to somehow only update the tree items that are shown in the Treeview > + # to fix it. > + > + global _jump_to_tree > + > + def search(_=None): > + _update_jump_to_matches(msglabel, entry.get()) > + > + def jump_to_selected(event=None): > + # Jumps to the selected node and closes the dialog > + > + # Ignore double clicks on the image and in the heading area > + if event and (tree.identify_element(event.x, event.y) == "image" or > + _in_heading(event)): > + return > + > + sel = tree.selection() > + if not sel: > + return > + > + node = _id_to_node[sel[0]] > + > + if node not in _shown_menu_nodes(_parent_menu(node)): > + _show_all_var.set(True) > + if not _single_menu: > + # See comment in _do_tree_mode() > + _update_tree() > + > + _jump_to(node) > + > + dialog.destroy() > + > + def tree_select(_): > + jumpto_button["state"] = "normal" if tree.selection() else "disabled" > + > + > + dialog = Toplevel(_root) > + dialog.geometry("+{}+{}".format( > + _root.winfo_rootx() + 50, _root.winfo_rooty() + 50)) > + dialog.title("Jump to symbol/choice/menu/comment") > + dialog.minsize(128, 128) # See _create_ui() > + dialog.transient(_root) > + > + ttk.Label(dialog, text=_JUMP_TO_HELP) \ > + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c", > + pady=".1c") > + > + entry = ttk.Entry(dialog) > + entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c") > + entry.focus_set() > + > + entry.bind("<Return>", search) > + entry.bind("<KP_Enter>", search) > + > + ttk.Button(dialog, text="Search", command=search) \ > + .grid(column=1, row=1, padx="0 .1c", pady="0 .1c") > + > + msglabel = ttk.Label(dialog) > + msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c") > + > + panedwindow, tree = _create_kconfig_tree_and_desc(dialog) > + panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew") > + > + # Clear tree > + tree.set_children("") > + > + _jump_to_tree = tree > + > + jumpto_button = ttk.Button(dialog, text="Jump to selected item", > + state="disabled", command=jump_to_selected) > + jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c") > + > + dialog.columnconfigure(0, weight=1) > + # Only the pane with the Kconfig tree and description grows vertically > + dialog.rowconfigure(3, weight=1) > + > + # See the menuconfig() function > + _root.update_idletasks() > + dialog.geometry(dialog.geometry()) > + > + # The dialog must be visible before we can grab the input > + dialog.wait_visibility() > + dialog.grab_set() > + > + tree.bind("<Double-1>", jump_to_selected) > + tree.bind("<Return>", jump_to_selected) > + tree.bind("<KP_Enter>", jump_to_selected) > + # add=True to avoid overriding the description text update > + tree.bind("<<TreeviewSelect>>", tree_select, add=True) > + > + dialog.bind("<Escape>", lambda _: dialog.destroy()) > + > + # Wait for the user to be done with the dialog > + _root.wait_window(dialog) > + > + _jump_to_tree = None > + > + _tree.focus_set() > + > + > +def _update_jump_to_matches(msglabel, search_string): > + # Searches for nodes matching the search string and updates > + # _jump_to_matches. Puts a message in 'msglabel' if there are no matches, > + # or regex errors. > + > + global _jump_to_matches > + > + _jump_to_tree.selection_set(()) > + > + try: > + # We could use re.IGNORECASE here instead of lower(), but this is > + # faster for regexes like '.*debug$' (though the '.*' is redundant > + # there). Those probably have bad interactions with re.search(), which > + # matches anywhere in the string. > + regex_searches = [re.compile(regex).search > + for regex in search_string.lower().split()] > + except re.error as e: > + msg = "Bad regular expression" > + # re.error.msg was added in Python 3.5 > + if hasattr(e, "msg"): > + msg += ": " + e.msg > + msglabel["text"] = msg > + # Clear tree > + _jump_to_tree.set_children("") > + return > + > + _jump_to_matches = [] > + add_match = _jump_to_matches.append > + > + for node in _sorted_sc_nodes(): > + # Symbol/choice > + sc = node.item > + > + for search in regex_searches: > + # Both the name and the prompt might be missing, since > + # we're searching both symbols and choices > + > + # Does the regex match either the symbol name or the > + # prompt (if any)? > + if not (sc.name and search(sc.name.lower()) or > + node.prompt and search(node.prompt[0].lower())): > + > + # Give up on the first regex that doesn't match, to > + # speed things up a bit when multiple regexes are > + # entered > + break > + > + else: > + add_match(node) > + > + # Search menus and comments > + > + for node in _sorted_menu_comment_nodes(): > + for search in regex_searches: > + if not search(node.prompt[0].lower()): > + break > + else: > + add_match(node) > + > + msglabel["text"] = "" if _jump_to_matches else "No matches" > + > + _update_jump_to_display() > + > + if _jump_to_matches: > + item = id(_jump_to_matches[0]) > + _jump_to_tree.selection_set(item) > + _jump_to_tree.focus(item) > + > + > +def _update_jump_to_display(): > + # Updates the images and text for the items in _jump_to_matches, and sets > + # them as the items of _jump_to_tree > + > + # Micro-optimize a bit > + item = _jump_to_tree.item > + id_ = id > + node_str = _node_str > + img_tag = _img_tag > + visible = _visible > + for node in _jump_to_matches: > + item(id_(node), > + text=node_str(node), > + tags=img_tag(node) if visible(node) else > + img_tag(node) + " invisible") > + > + _jump_to_tree.set_children("", *map(id, _jump_to_matches)) > + > + > +def _jump_to(node): > + # Jumps directly to 'node' and selects it > + > + if _single_menu: > + _enter_menu(_parent_menu(node)) > + else: > + _load_parents(node) > + > + _select(_tree, id(node)) > + > + > +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing > +# to the same list. This avoids a global. > +def _sorted_sc_nodes(cached_nodes=[]): > + # Returns a sorted list of symbol and choice nodes to search. The symbol > + # nodes appear first, sorted by name, and then the choice nodes, sorted by > + # prompt and (secondarily) name. > + > + if not cached_nodes: > + # Add symbol nodes > + for sym in sorted(_kconf.unique_defined_syms, > + key=lambda sym: sym.name): > + # += is in-place for lists > + cached_nodes += sym.nodes > + > + # Add choice nodes > + > + choices = sorted(_kconf.unique_choices, > + key=lambda choice: choice.name or "") > + > + cached_nodes += sorted( > + [node for choice in choices for node in choice.nodes], > + key=lambda node: node.prompt[0] if node.prompt else "") > + > + return cached_nodes > + > + > +def _sorted_menu_comment_nodes(cached_nodes=[]): > + # Returns a list of menu and comment nodes to search, sorted by prompt, > + # with the menus first > + > + if not cached_nodes: > + def prompt_text(mc): > + return mc.prompt[0] > + > + cached_nodes += sorted(_kconf.menus, key=prompt_text) > + cached_nodes += sorted(_kconf.comments, key=prompt_text) > + > + return cached_nodes > + > + > +def _load_parents(node): > + # Menus are lazily populated as they're opened in full-tree mode, but > + # jumping to an item needs its parent menus to be populated. This function > + # populates 'node's parents. > + > + # Get all parents leading up to 'node', sorted with the root first > + parents = [] > + cur = node.parent > + while cur is not _kconf.top_node: > + parents.append(cur) > + cur = cur.parent > + parents.reverse() > + > + for i, parent in enumerate(parents): > + if not _tree.item(id(parent), "open"): > + # Found a closed menu. Populate it and all the remaining menus > + # leading up to 'node'. > + for parent in parents[i:]: > + # We only need to populate "real" menus/choices. Implicit menus > + # are populated when their parents menus are entered. > + if not isinstance(parent.item, Symbol): > + _build_full_tree(parent) > + return > + > + > +def _parent_menu(node): > + # Returns the menu node of the menu that contains 'node'. In addition to > + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. > + # "Menu" here means a menu in the interface. > + > + menu = node.parent > + while not menu.is_menuconfig: > + menu = menu.parent > + return menu > + > + > +def _trace_write(var, fn): > + # Makes fn() be called whenever the Tkinter Variable 'var' changes value > + > + # trace_variable() is deprecated according to the docstring, > + # which recommends trace_add() > + if hasattr(var, "trace_add"): > + var.trace_add("write", fn) > + else: > + var.trace_variable("w", fn) > + > + > +def _info_str(node): > + # Returns information about the menu node 'node' as a string. > + # > + # The helper functions are responsible for adding newlines. This allows > + # them to return "" if they don't want to add any output. > + > + if isinstance(node.item, Symbol): > + sym = node.item > + > + return ( > + _name_info(sym) + > + _help_info(sym) + > + _direct_dep_info(sym) + > + _defaults_info(sym) + > + _select_imply_info(sym) + > + _kconfig_def_info(sym) > + ) > + > + if isinstance(node.item, Choice): > + choice = node.item > + > + return ( > + _name_info(choice) + > + _help_info(choice) + > + 'Mode: {}\n\n'.format(choice.str_value) + > + _choice_syms_info(choice) + > + _direct_dep_info(choice) + > + _defaults_info(choice) + > + _kconfig_def_info(choice) > + ) > + > + # node.item in (MENU, COMMENT) > + return _kconfig_def_info(node) > + > + > +def _name_info(sc): > + # Returns a string with the name of the symbol/choice. Choices are shown as > + # <choice (name if any)>. > + > + return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n" > + > + > +def _value_info(sym): > + # Returns a string showing 'sym's value > + > + # Only put quotes around the value for string symbols > + return "Value: {}\n".format( > + '"{}"'.format(sym.str_value) > + if sym.orig_type == STRING > + else sym.str_value) > + > + > +def _choice_syms_info(choice): > + # Returns a string listing the choice symbols in 'choice'. Adds > + # "(selected)" next to the selected one. > + > + s = "Choice symbols:\n" > + > + for sym in choice.syms: > + s += " - " + sym.name > + if sym is choice.selection: > + s += " (selected)" > + s += "\n" > + > + return s + "\n" > + > + > +def _help_info(sc): > + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). > + # Symbols and choices defined in multiple locations can have multiple help > + # texts. > + > + s = "" > + > + for node in sc.nodes: > + if node.help is not None: > + s += node.help + "\n\n" > + > + return s > + > + > +def _direct_dep_info(sc): > + # Returns a string describing the direct dependencies of 'sc' (Symbol or > + # Choice). The direct dependencies are the OR of the dependencies from each > + # definition location. The dependencies at each definition location come > + # from 'depends on' and dependencies inherited from parent items. > + > + return "" if sc.direct_dep is _kconf.y else \ > + 'Direct dependencies (={}):\n{}\n' \ > + .format(TRI_TO_STR[expr_value(sc.direct_dep)], > + _split_expr_info(sc.direct_dep, 2)) > + > + > +def _defaults_info(sc): > + # Returns a string describing the defaults of 'sc' (Symbol or Choice) > + > + if not sc.defaults: > + return "" > + > + s = "Default" > + if len(sc.defaults) > 1: > + s += "s" > + s += ":\n" > + > + for val, cond in sc.orig_defaults: > + s += " - " > + if isinstance(sc, Symbol): > + s += _expr_str(val) > + > + # Skip the tristate value hint if the expression is just a single > + # symbol. _expr_str() already shows its value as a string. > + # > + # This also avoids showing the tristate value for string/int/hex > + # defaults, which wouldn't make any sense. > + if isinstance(val, tuple): > + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) > + else: > + # Don't print the value next to the symbol name for choice > + # defaults, as it looks a bit confusing > + s += val.name > + s += "\n" > + > + if cond is not _kconf.y: > + s += " Condition (={}):\n{}" \ > + .format(TRI_TO_STR[expr_value(cond)], > + _split_expr_info(cond, 4)) > + > + return s + "\n" > + > + > +def _split_expr_info(expr, indent): > + # Returns a string with 'expr' split into its top-level && or || operands, > + # with one operand per line, together with the operand's value. This is > + # usually enough to get something readable for long expressions. A fancier > + # recursive thingy would be possible too. > + # > + # indent: > + # Number of leading spaces to add before the split expression. > + > + if len(split_expr(expr, AND)) > 1: > + split_op = AND > + op_str = "&&" > + else: > + split_op = OR > + op_str = "||" > + > + s = "" > + for i, term in enumerate(split_expr(expr, split_op)): > + s += "{}{} {}".format(indent*" ", > + " " if i == 0 else op_str, > + _expr_str(term)) > + > + # Don't bother showing the value hint if the expression is just a > + # single symbol. _expr_str() already shows its value. > + if isinstance(term, tuple): > + s += " (={})".format(TRI_TO_STR[expr_value(term)]) > + > + s += "\n" > + > + return s > + > + > +def _select_imply_info(sym): > + # Returns a string with information about which symbols 'select' or 'imply' > + # 'sym'. The selecting/implying symbols are grouped according to which > + # value they select/imply 'sym' to (n/m/y). > + > + def sis(expr, val, title): > + # sis = selects/implies > + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] > + if not sis: > + return "" > + > + res = title > + for si in sis: > + res += " - {}\n".format(split_expr(si, AND)[0].name) > + return res + "\n" > + > + s = "" > + > + if sym.rev_dep is not _kconf.n: > + s += sis(sym.rev_dep, 2, > + "Symbols currently y-selecting this symbol:\n") > + s += sis(sym.rev_dep, 1, > + "Symbols currently m-selecting this symbol:\n") > + s += sis(sym.rev_dep, 0, > + "Symbols currently n-selecting this symbol (no effect):\n") > + > + if sym.weak_rev_dep is not _kconf.n: > + s += sis(sym.weak_rev_dep, 2, > + "Symbols currently y-implying this symbol:\n") > + s += sis(sym.weak_rev_dep, 1, > + "Symbols currently m-implying this symbol:\n") > + s += sis(sym.weak_rev_dep, 0, > + "Symbols currently n-implying this symbol (no effect):\n") > + > + return s > + > + > +def _kconfig_def_info(item): > + # Returns a string with the definition of 'item' in Kconfig syntax, > + # together with the definition location(s) and their include and menu paths > + > + nodes = [item] if isinstance(item, MenuNode) else item.nodes > + > + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ > + .format("s" if len(nodes) > 1 else "") > + s += (len(s) - 1)*"=" > + > + for node in nodes: > + s += "\n\n" \ > + "At {}:{}\n" \ > + "{}" \ > + "Menu path: {}\n\n" \ > + "{}" \ > + .format(node.filename, node.linenr, > + _include_path_info(node), > + _menu_path_info(node), > + node.custom_str(_name_and_val_str)) > + > + return s > + > + > +def _include_path_info(node): > + if not node.include_path: > + # In the top-level Kconfig file > + return "" > + > + return "Included via {}\n".format( > + " -> ".join("{}:{}".format(filename, linenr) > + for filename, linenr in node.include_path)) > + > + > +def _menu_path_info(node): > + # Returns a string describing the menu path leading up to 'node' > + > + path = "" > + > + while node.parent is not _kconf.top_node: > + node = node.parent > + > + # Promptless choices might appear among the parents. Use > + # standard_sc_expr_str() for them, so that they show up as > + # '<choice (name if any)>'. > + path = " -> " + (node.prompt[0] if node.prompt else > + standard_sc_expr_str(node.item)) + path > + > + return "(Top)" + path > + > + > +def _name_and_val_str(sc): > + # Custom symbol/choice printer that shows symbol values after symbols > + > + # Show the values of non-constant (non-quoted) symbols that don't look like > + # numbers. Things like 123 are actually symbol references, and only work as > + # expected due to undefined symbols getting their name as their value. > + # Showing the symbol value for those isn't helpful though. > + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): > + if not sc.nodes: > + # Undefined symbol reference > + return "{}(undefined/n)".format(sc.name) > + > + return '{}(={})'.format(sc.name, sc.str_value) > + > + # For other items, use the standard format > + return standard_sc_expr_str(sc) > + > + > +def _expr_str(expr): > + # Custom expression printer that shows symbol values > + return expr_str(expr, _name_and_val_str) > + > + > +def _is_num(name): > + # Heuristic to see if a symbol name looks like a number, for nicer output > + # when printing expressions. Things like 16 are actually symbol names, only > + # they get their name as their value when the symbol is undefined. > + > + try: > + int(name) > + except ValueError: > + if not name.startswith(("0x", "0X")): > + return False > + > + try: > + int(name, 16) > + except ValueError: > + return False > + > + return True > + > + > +if __name__ == "__main__": > + _main() > diff --git a/scripts/Kconfiglib/kconfiglib.py b/scripts/Kconfiglib/kconfiglib.py > new file mode 100644 > index 0000000..a50312e > --- /dev/null > +++ b/scripts/Kconfiglib/kconfiglib.py > @@ -0,0 +1,7160 @@ > +# Copyright (c) 2011-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Overview > +======== > + > +Kconfiglib is a Python 2/3 library for scripting and extracting information > +from Kconfig (https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt) > +configuration systems. > + > +See the homepage at https://github.com/zephyrproject-rtos/Kconfiglib for a longer > +overview. > + > +Since Kconfiglib 12.0.0, the library version is available in > +kconfiglib.VERSION, which is a (<major>, <minor>, <patch>) tuple, e.g. > +(12, 0, 0). > + > + > +Using Kconfiglib on the Linux kernel with the Makefile targets > +============================================================== > + > +For the Linux kernel, a handy interface is provided by the > +scripts/kconfig/Makefile patch, which can be applied with either 'git am' or > +the 'patch' utility: > + > + $ wget -qO- https://raw.githubusercontent.com/zephyrproject-rtos/Kconfiglib/master/makefile.patch | git am > + $ wget -qO- https://raw.githubusercontent.com/zephyrproject-rtos/Kconfiglib/master/makefile.patch | patch -p1 > + > +Warning: Not passing -p1 to patch will cause the wrong file to be patched. > + > +Please tell me if the patch does not apply. It should be trivial to apply > +manually, as it's just a block of text that needs to be inserted near the other > +*conf: targets in scripts/kconfig/Makefile. > + > +Look further down for a motivation for the Makefile patch and for instructions > +on how you can use Kconfiglib without it. > + > +If you do not wish to install Kconfiglib via pip, the Makefile patch is set up > +so that you can also just clone Kconfiglib into the kernel root: > + > + $ git clone git://github.com/zephyrproject-rtos/Kconfiglib.git > + $ git am Kconfiglib/makefile.patch (or 'patch -p1 < Kconfiglib/makefile.patch') > + > +Warning: The directory name Kconfiglib/ is significant in this case, because > +it's added to PYTHONPATH by the new targets in makefile.patch. > + > +The targets added by the Makefile patch are described in the following > +sections. > + > + > +make kmenuconfig > +---------------- > + > +This target runs the curses menuconfig interface with Python 3. As of > +Kconfiglib 12.2.0, both Python 2 and Python 3 are supported (previously, only > +Python 3 was supported, so this was a backport). > + > + > +make guiconfig > +-------------- > + > +This target runs the Tkinter menuconfig interface. Both Python 2 and Python 3 > +are supported. To change the Python interpreter used, pass > +PYTHONCMD=<executable> to 'make'. The default is 'python'. > + > + > +make [ARCH=<arch>] iscriptconfig > +-------------------------------- > + > +This target gives an interactive Python prompt where a Kconfig instance has > +been preloaded and is available in 'kconf'. To change the Python interpreter > +used, pass PYTHONCMD=<executable> to 'make'. The default is 'python'. > + > +To get a feel for the API, try evaluating and printing the symbols in > +kconf.defined_syms, and explore the MenuNode menu tree starting at > +kconf.top_node by following 'next' and 'list' pointers. > + > +The item contained in a menu node is found in MenuNode.item (note that this can > +be one of the constants kconfiglib.MENU and kconfiglib.COMMENT), and all > +symbols and choices have a 'nodes' attribute containing their menu nodes > +(usually only one). Printing a menu node will print its item, in Kconfig > +format. > + > +If you want to look up a symbol by name, use the kconf.syms dictionary. > + > + > +make scriptconfig SCRIPT=<script> [SCRIPT_ARG=<arg>] > +---------------------------------------------------- > + > +This target runs the Python script given by the SCRIPT parameter on the > +configuration. sys.argv[1] holds the name of the top-level Kconfig file > +(currently always "Kconfig" in practice), and sys.argv[2] holds the SCRIPT_ARG > +argument, if given. > + > +See the examples/ subdirectory for example scripts. > + > + > +make dumpvarsconfig > +------------------- > + > +This target prints a list of all environment variables referenced from the > +Kconfig files, together with their values. See the > +Kconfiglib/examples/dumpvars.py script. > + > +Only environment variables that are referenced via the Kconfig preprocessor > +$(FOO) syntax are included. The preprocessor was added in Linux 4.18. > + > + > +Using Kconfiglib without the Makefile targets > +============================================= > + > +The make targets are only needed to pick up environment variables exported from > +the Kbuild makefiles and referenced inside Kconfig files, via e.g. > +'source "arch/$(SRCARCH)/Kconfig" and commands run via '$(shell,...)'. > + > +These variables are referenced as of writing (Linux 4.18), together with sample > +values: > + > + srctree (.) > + ARCH (x86) > + SRCARCH (x86) > + KERNELVERSION (4.18.0) > + CC (gcc) > + HOSTCC (gcc) > + HOSTCXX (g++) > + CC_VERSION_TEXT (gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0) > + > +Older kernels only reference ARCH, SRCARCH, and KERNELVERSION. > + > +If your kernel is recent enough (4.18+), you can get a list of referenced > +environment variables via 'make dumpvarsconfig' (see above). Note that this > +command is added by the Makefile patch. > + > +To run Kconfiglib without the Makefile patch, set the environment variables > +manually: > + > + $ srctree=. ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` ... python(3) > + >>> import kconfiglib > + >>> kconf = kconfiglib.Kconfig() # filename defaults to "Kconfig" > + > +Search the top-level Makefile for "Additional ARCH settings" to see other > +possibilities for ARCH and SRCARCH. > + > + > +Intro to symbol values > +====================== > + > +Kconfiglib has the same assignment semantics as the C implementation. > + > +Any symbol can be assigned a value by the user (via Kconfig.load_config() or > +Symbol.set_value()), but this user value is only respected if the symbol is > +visible, which corresponds to it (currently) being visible in the menuconfig > +interface. > + > +For symbols with prompts, the visibility of the symbol is determined by the > +condition on the prompt. Symbols without prompts are never visible, so setting > +a user value on them is pointless. A warning will be printed by default if > +Symbol.set_value() is called on a promptless symbol. Assignments to promptless > +symbols are normal within a .config file, so no similar warning will be printed > +by load_config(). > + > +Dependencies from parents and 'if'/'depends on' are propagated to properties, > +including prompts, so these two configurations are logically equivalent: > + > +(1) > + > + menu "menu" > + depends on A > + > + if B > + > + config FOO > + tristate "foo" if D > + default y > + depends on C > + > + endif > + > + endmenu > + > +(2) > + > + menu "menu" > + depends on A > + > + config FOO > + tristate "foo" if A && B && C && D > + default y if A && B && C > + > + endmenu > + > +In this example, A && B && C && D (the prompt condition) needs to be non-n for > +FOO to be visible (assignable). If its value is m, the symbol can only be > +assigned the value m: The visibility sets an upper bound on the value that can > +be assigned by the user, and any higher user value will be truncated down. > + > +'default' properties are independent of the visibility, though a 'default' will > +often get the same condition as the prompt due to dependency propagation. > +'default' properties are used if the symbol is not visible or has no user > +value. > + > +Symbols with no user value (or that have a user value but are not visible) and > +no (active) 'default' default to n for bool/tristate symbols, and to the empty > +string for other symbol types. > + > +'select' works similarly to symbol visibility, but sets a lower bound on the > +value of the symbol. The lower bound is determined by the value of the > +select*ing* symbol. 'select' does not respect visibility, so non-visible > +symbols can be forced to a particular (minimum) value by a select as well. > + > +For non-bool/tristate symbols, it only matters whether the visibility is n or > +non-n: m visibility acts the same as y visibility. > + > +Conditions on 'default' and 'select' work in mostly intuitive ways. If the > +condition is n, the 'default' or 'select' is disabled. If it is m, the > +'default' or 'select' value (the value of the selecting symbol) is truncated > +down to m. > + > +When writing a configuration with Kconfig.write_config(), only symbols that are > +visible, have an (active) default, or are selected will get written out (note > +that this includes all symbols that would accept user values). Kconfiglib > +matches the .config format produced by the C implementations down to the > +character. This eases testing. > + > +For a visible bool/tristate symbol FOO with value n, this line is written to > +.config: > + > + # CONFIG_FOO is not set > + > +The point is to remember the user n selection (which might differ from the > +default value the symbol would get), while at the same sticking to the rule > +that undefined corresponds to n (.config uses Makefile format, making the line > +above a comment). When the .config file is read back in, this line will be > +treated the same as the following assignment: > + > + CONFIG_FOO=n > + > +In Kconfiglib, the set of (currently) assignable values for a bool/tristate > +symbol appear in Symbol.assignable. For other symbol types, just check if > +sym.visibility is non-0 (non-n) to see whether the user value will have an > +effect. > + > + > +Intro to the menu tree > +====================== > + > +The menu structure, as seen in e.g. menuconfig, is represented by a tree of > +MenuNode objects. The top node of the configuration corresponds to an implicit > +top-level menu, the title of which is shown at the top in the standard > +menuconfig interface. (The title is also available in Kconfig.mainmenu_text in > +Kconfiglib.) > + > +The top node is found in Kconfig.top_node. From there, you can visit child menu > +nodes by following the 'list' pointer, and any following menu nodes by > +following the 'next' pointer. Usually, a non-None 'list' pointer indicates a > +menu or Choice, but menu nodes for symbols can sometimes have a non-None 'list' > +pointer too due to submenus created implicitly from dependencies. > + > +MenuNode.item is either a Symbol or a Choice object, or one of the constants > +MENU and COMMENT. The prompt of the menu node can be found in MenuNode.prompt, > +which also holds the title for menus and comments. For Symbol and Choice, > +MenuNode.help holds the help text (if any, otherwise None). > + > +Most symbols will only have a single menu node. A symbol defined in multiple > +locations will have one menu node for each location. The list of menu nodes for > +a Symbol or Choice can be found in the Symbol/Choice.nodes attribute. > + > +Note that prompts and help texts for symbols and choices are stored in their > +menu node(s) rather than in the Symbol or Choice objects themselves. This makes > +it possible to define a symbol in multiple locations with a different prompt or > +help text in each location. To get the help text or prompt for a symbol with a > +single menu node, do sym.nodes[0].help and sym.nodes[0].prompt, respectively. > +The prompt is a (text, condition) tuple, where condition determines the > +visibility (see 'Intro to expressions' below). > + > +This organization mirrors the C implementation. MenuNode is called > +'struct menu' there, but I thought "menu" was a confusing name. > + > +It is possible to give a Choice a name and define it in multiple locations, > +hence why Choice.nodes is also a list. > + > +As a convenience, the properties added at a particular definition location are > +available on the MenuNode itself, in e.g. MenuNode.defaults. This is helpful > +when generating documentation, so that symbols/choices defined in multiple > +locations can be shown with the correct properties at each location. > + > + > +Intro to expressions > +==================== > + > +Expressions can be evaluated with the expr_value() function and printed with > +the expr_str() function (these are used internally as well). Evaluating an > +expression always yields a tristate value, where n, m, and y are represented as > +0, 1, and 2, respectively. > + > +The following table should help you figure out how expressions are represented. > +A, B, C, ... are symbols (Symbol instances), NOT is the kconfiglib.NOT > +constant, etc. > + > +Expression Representation > +---------- -------------- > +A A > +"A" A (constant symbol) > +!A (NOT, A) > +A && B (AND, A, B) > +A && B && C (AND, A, (AND, B, C)) > +A || B (OR, A, B) > +A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) > +A = B (EQUAL, A, B) > +A != "foo" (UNEQUAL, A, foo (constant symbol)) > +A && B = C && D (AND, A, (AND, (EQUAL, B, C), D)) > +n Kconfig.n (constant symbol) > +m Kconfig.m (constant symbol) > +y Kconfig.y (constant symbol) > +"y" Kconfig.y (constant symbol) > + > +Strings like "foo" in 'default "foo"' or 'depends on SYM = "foo"' are > +represented as constant symbols, so the only values that appear in expressions > +are symbols***. This mirrors the C implementation. > + > +***For choice symbols, the parent Choice will appear in expressions as well, > +but it's usually invisible as the value interfaces of Symbol and Choice are > +identical. This mirrors the C implementation and makes different choice modes > +"just work". > + > +Manual evaluation examples: > + > + - The value of A && B is min(A.tri_value, B.tri_value) > + > + - The value of A || B is max(A.tri_value, B.tri_value) > + > + - The value of !A is 2 - A.tri_value > + > + - The value of A = B is 2 (y) if A.str_value == B.str_value, and 0 (n) > + otherwise. Note that str_value is used here instead of tri_value. > + > + For constant (as well as undefined) symbols, str_value matches the name of > + the symbol. This mirrors the C implementation and explains why > + 'depends on SYM = "foo"' above works as expected. > + > +n/m/y are automatically converted to the corresponding constant symbols > +"n"/"m"/"y" (Kconfig.n/m/y) during parsing. > + > +Kconfig.const_syms is a dictionary like Kconfig.syms but for constant symbols. > + > +If a condition is missing (e.g., <cond> when the 'if <cond>' is removed from > +'default A if <cond>'), it is actually Kconfig.y. The standard __str__() > +functions just avoid printing 'if y' conditions to give cleaner output. > + > + > +Kconfig extensions > +================== > + > +Kconfiglib includes a couple of Kconfig extensions: > + > +'source' with relative path > +--------------------------- > + > +The 'rsource' statement sources Kconfig files with a path relative to directory > +of the Kconfig file containing the 'rsource' statement, instead of relative to > +the project root. > + > +Consider following directory tree: > + > + Project > + +--Kconfig > + | > + +--src > + +--Kconfig > + | > + +--SubSystem1 > + +--Kconfig > + | > + +--ModuleA > + +--Kconfig > + > +In this example, assume that src/SubSystem1/Kconfig wants to source > +src/SubSystem1/ModuleA/Kconfig. > + > +With 'source', this statement would be used: > + > + source "src/SubSystem1/ModuleA/Kconfig" > + > +With 'rsource', this turns into > + > + rsource "ModuleA/Kconfig" > + > +If an absolute path is given to 'rsource', it acts the same as 'source'. > + > +'rsource' can be used to create "position-independent" Kconfig trees that can > +be moved around freely. > + > + > +Globbing 'source' > +----------------- > + > +'source' and 'rsource' accept glob patterns, sourcing all matching Kconfig > +files. They require at least one matching file, raising a KconfigError > +otherwise. > + > +For example, the following statement might source sub1/foofoofoo and > +sub2/foobarfoo: > + > + source "sub[12]/foo*foo" > + > +The glob patterns accepted are the same as for the standard glob.glob() > +function. > + > +Two additional statements are provided for cases where it's acceptable for a > +pattern to match no files: 'osource' and 'orsource' (the o is for "optional"). > + > +For example, the following statements will be no-ops if neither "foo" nor any > +files matching "bar*" exist: > + > + osource "foo" > + osource "bar*" > + > +'orsource' does a relative optional source. > + > +'source' and 'osource' are analogous to 'include' and '-include' in Make. > + > + > +Generalized def_* keywords > +-------------------------- > + > +def_int, def_hex, and def_string are available in addition to def_bool and > +def_tristate, allowing int, hex, and string symbols to be given a type and a > +default at the same time. > + > + > +Extra optional warnings > +----------------------- > + > +Some optional warnings can be controlled via environment variables: > + > + - KCONFIG_WARN_UNDEF: If set to 'y', warnings will be generated for all > + references to undefined symbols within Kconfig files. The only gotcha is > + that all hex literals must be prefixed with "0x" or "0X", to make it > + possible to distinguish them from symbol references. > + > + Some projects (e.g. the Linux kernel) use multiple Kconfig trees with many > + shared Kconfig files, leading to some safe undefined symbol references. > + KCONFIG_WARN_UNDEF is useful in projects that only have a single Kconfig > + tree though. > + > + KCONFIG_STRICT is an older alias for this environment variable, supported > + for backwards compatibility. > + > + - KCONFIG_WARN_UNDEF_ASSIGN: If set to 'y', warnings will be generated for > + all assignments to undefined symbols within .config files. By default, no > + such warnings are generated. > + > + This warning can also be enabled/disabled via the Kconfig.warn_assign_undef > + variable. > + > + > +Preprocessor user functions defined in Python > +--------------------------------------------- > + > +Preprocessor functions can be defined in Python, which makes it simple to > +integrate information from existing Python tools into Kconfig (e.g. to have > +Kconfig symbols depend on hardware information stored in some other format). > + > +Putting a Python module named kconfigfunctions(.py) anywhere in sys.path will > +cause it to be imported by Kconfiglib (in Kconfig.__init__()). Note that > +sys.path can be customized via PYTHONPATH, and includes the directory of the > +module being run by default, as well as installation directories. > + > +If the KCONFIG_FUNCTIONS environment variable is set, it gives a different > +module name to use instead of 'kconfigfunctions'. > + > +The imported module is expected to define a global dictionary named 'functions' > +that maps function names to Python functions, as follows: > + > + def my_fn(kconf, name, arg_1, arg_2, ...): > + # kconf: > + # Kconfig instance > + # > + # name: > + # Name of the user-defined function ("my-fn"). Think argv[0]. > + # > + # arg_1, arg_2, ...: > + # Arguments passed to the function from Kconfig (strings) > + # > + # Returns a string to be substituted as the result of calling the > + # function > + ... > + > + def my_other_fn(kconf, name, arg_1, arg_2, ...): > + ... > + > + functions = { > + "my-fn": (my_fn, <min.args>, <max.args>/None), > + "my-other-fn": (my_other_fn, <min.args>, <max.args>/None), > + ... > + } > + > + ... > + > +<min.args> and <max.args> are the minimum and maximum number of arguments > +expected by the function (excluding the implicit 'name' argument). If > +<max.args> is None, there is no upper limit to the number of arguments. Passing > +an invalid number of arguments will generate a KconfigError exception. > + > +Functions can access the current parsing location as kconf.filename/linenr. > +Accessing other fields of the Kconfig object is not safe. See the warning > +below. > + > +Keep in mind that for a variable defined like 'foo = $(fn)', 'fn' will be > +called only when 'foo' is expanded. If 'fn' uses the parsing location and the > +intent is to use the location of the assignment, you want 'foo := $(fn)' > +instead, which calls the function immediately. > + > +Once defined, user functions can be called from Kconfig in the same way as > +other preprocessor functions: > + > + config FOO > + ... > + depends on $(my-fn,arg1,arg2) > + > +If my_fn() returns "n", this will result in > + > + config FOO > + ... > + depends on n > + > +Warning > +******* > + > +User-defined preprocessor functions are called as they're encountered at parse > +time, before all Kconfig files have been processed, and before the menu tree > +has been finalized. There are no guarantees that accessing Kconfig symbols or > +the menu tree via the 'kconf' parameter will work, and it could potentially > +lead to a crash. > + > +Preferably, user-defined functions should be stateless. > + > + > +Feedback > +======== > + > +For bug reports, suggestions, and questions, please open a ticket on the GitHub > +page. > +""" > +import errno > +import importlib > +import os > +import re > +import sys > + > +# Get rid of some attribute lookups. These are obvious in context. > +from glob import iglob > +from os.path import dirname, exists, expandvars, islink, join, realpath > + > + > +VERSION = (14, 1, 0) > + > + > +# File layout: > +# > +# Public classes > +# Public functions > +# Internal functions > +# Global constants > + > +# Line length: 79 columns > + > + > +# > +# Public classes > +# > + > + > +class Kconfig(object): > + """ > + Represents a Kconfig configuration, e.g. for x86 or ARM. This is the set of > + symbols, choices, and menu nodes appearing in the configuration. Creating > + any number of Kconfig objects (including for different architectures) is > + safe. Kconfiglib doesn't keep any global state. > + > + The following attributes are available. They should be treated as > + read-only, and some are implemented through @property magic. > + > + syms: > + A dictionary with all symbols in the configuration, indexed by name. Also > + includes all symbols that are referenced in expressions but never > + defined, except for constant (quoted) symbols. > + > + Undefined symbols can be recognized by Symbol.nodes being empty -- see > + the 'Intro to the menu tree' section in the module docstring. > + > + const_syms: > + A dictionary like 'syms' for constant (quoted) symbols > + > + named_choices: > + A dictionary like 'syms' for named choices (choice FOO) > + > + defined_syms: > + A list with all defined symbols, in the same order as they appear in the > + Kconfig files. Symbols defined in multiple locations appear multiple > + times. > + > + Note: You probably want to use 'unique_defined_syms' instead. This > + attribute is mostly maintained for backwards compatibility. > + > + unique_defined_syms: > + A list like 'defined_syms', but with duplicates removed. Just the first > + instance is kept for symbols defined in multiple locations. Kconfig order > + is preserved otherwise. > + > + Using this attribute instead of 'defined_syms' can save work, and > + automatically gives reasonable behavior when writing configuration output > + (symbols defined in multiple locations only generate output once, while > + still preserving Kconfig order for readability). > + > + choices: > + A list with all choices, in the same order as they appear in the Kconfig > + files. > + > + Note: You probably want to use 'unique_choices' instead. This attribute > + is mostly maintained for backwards compatibility. > + > + unique_choices: > + Analogous to 'unique_defined_syms', for choices. Named choices can have > + multiple definition locations. > + > + menus: > + A list with all menus, in the same order as they appear in the Kconfig > + files > + > + comments: > + A list with all comments, in the same order as they appear in the Kconfig > + files > + > + kconfig_filenames: > + A list with the filenames of all Kconfig files included in the > + configuration, relative to $srctree (or relative to the current directory > + if $srctree isn't set), except absolute paths (e.g. > + 'source "/foo/Kconfig"') are kept as-is. > + > + The files are listed in the order they are source'd, starting with the > + top-level Kconfig file. If a file is source'd multiple times, it will > + appear multiple times. Use set() to get unique filenames. > + > + Note that Kconfig.sync_deps() already indirectly catches any file > + modifications that change configuration output. > + > + env_vars: > + A set() with the names of all environment variables referenced in the > + Kconfig files. > + > + Only environment variables referenced with the preprocessor $(FOO) syntax > + will be registered. The older $FOO syntax is only supported for backwards > + compatibility. > + > + Also note that $(FOO) won't be registered unless the environment variable > + $FOO is actually set. If it isn't, $(FOO) is an expansion of an unset > + preprocessor variable (which gives the empty string). > + > + Another gotcha is that environment variables referenced in the values of > + recursively expanded preprocessor variables (those defined with =) will > + only be registered if the variable is actually used (expanded) somewhere. > + > + The note from the 'kconfig_filenames' documentation applies here too. > + > + n/m/y: > + The predefined constant symbols n/m/y. Also available in const_syms. > + > + modules: > + The Symbol instance for the modules symbol. Currently hardcoded to > + MODULES, which is backwards compatible. Kconfiglib will warn if > + 'option modules' is set on some other symbol. Tell me if you need proper > + 'option modules' support. > + > + 'modules' is never None. If the MODULES symbol is not explicitly defined, > + its tri_value will be 0 (n), as expected. > + > + A simple way to enable modules is to do 'kconf.modules.set_value(2)' > + (provided the MODULES symbol is defined and visible). Modules are > + disabled by default in the kernel Kconfig files as of writing, though > + nearly all defconfig files enable them (with 'CONFIG_MODULES=y'). > + > + defconfig_list: > + The Symbol instance for the 'option defconfig_list' symbol, or None if no > + defconfig_list symbol exists. The defconfig filename derived from this > + symbol can be found in Kconfig.defconfig_filename. > + > + defconfig_filename: > + The filename given by the defconfig_list symbol. This is taken from the > + first 'default' with a satisfied condition where the specified file > + exists (can be opened for reading). If a defconfig file foo/defconfig is > + not found and $srctree was set when the Kconfig was created, > + $srctree/foo/defconfig is looked up as well. > + > + 'defconfig_filename' is None if either no defconfig_list symbol exists, > + or if the defconfig_list symbol has no 'default' with a satisfied > + condition that specifies a file that exists. > + > + Gotcha: scripts/kconfig/Makefile might pass --defconfig=<defconfig> to > + scripts/kconfig/conf when running e.g. 'make defconfig'. This option > + overrides the defconfig_list symbol, meaning defconfig_filename might not > + always match what 'make defconfig' would use. > + > + top_node: > + The menu node (see the MenuNode class) of the implicit top-level menu. > + Acts as the root of the menu tree. > + > + mainmenu_text: > + The prompt (title) of the top menu (top_node). Defaults to "Main menu". > + Can be changed with the 'mainmenu' statement (see kconfig-language.txt). > + > + variables: > + A dictionary with all preprocessor variables, indexed by name. See the > + Variable class. > + > + warn: > + Set this variable to True/False to enable/disable warnings. See > + Kconfig.__init__(). > + > + When 'warn' is False, the values of the other warning-related variables > + are ignored. > + > + This variable as well as the other warn* variables can be read to check > + the current warning settings. > + > + warn_to_stderr: > + Set this variable to True/False to enable/disable warnings on stderr. See > + Kconfig.__init__(). > + > + warn_assign_undef: > + Set this variable to True to generate warnings for assignments to > + undefined symbols in configuration files. > + > + This variable is False by default unless the KCONFIG_WARN_UNDEF_ASSIGN > + environment variable was set to 'y' when the Kconfig instance was > + created. > + > + warn_assign_override: > + Set this variable to True to generate warnings for multiple assignments > + to the same symbol in configuration files, where the assignments set > + different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where the > + last value would get used). > + > + This variable is True by default. Disabling it might be useful when > + merging configurations. > + > + warn_assign_redun: > + Like warn_assign_override, but for multiple assignments setting a symbol > + to the same value. > + > + This variable is True by default. Disabling it might be useful when > + merging configurations. > + > + warnings: > + A list of strings containing all warnings that have been generated, for > + cases where more flexibility is needed. > + > + See the 'warn_to_stderr' parameter to Kconfig.__init__() and the > + Kconfig.warn_to_stderr variable as well. Note that warnings still get > + added to Kconfig.warnings when 'warn_to_stderr' is True. > + > + Just as for warnings printed to stderr, only warnings that are enabled > + will get added to Kconfig.warnings. See the various Kconfig.warn* > + variables. > + > + missing_syms: > + A list with (name, value) tuples for all assignments to undefined symbols > + within the most recently loaded .config file(s). 'name' is the symbol > + name without the 'CONFIG_' prefix. 'value' is a string that gives the > + right-hand side of the assignment verbatim. > + > + See Kconfig.load_config() as well. > + > + srctree: > + The value the $srctree environment variable had when the Kconfig instance > + was created, or the empty string if $srctree wasn't set. This gives nice > + behavior with os.path.join(), which treats "" as the current directory, > + without adding "./". > + > + Kconfig files are looked up relative to $srctree (unless absolute paths > + are used), and .config files are looked up relative to $srctree if they > + are not found in the current directory. This is used to support > + out-of-tree builds. The C tools use this environment variable in the same > + way. > + > + Changing $srctree after creating the Kconfig instance has no effect. Only > + the value when the configuration is loaded matters. This avoids surprises > + if multiple configurations are loaded with different values for $srctree. > + > + config_prefix: > + The value the CONFIG_ environment variable had when the Kconfig instance > + was created, or "CONFIG_" if CONFIG_ wasn't set. This is the prefix used > + (and expected) on symbol names in .config files and C headers. Used in > + the same way in the C tools. > + > + config_header: > + The value the KCONFIG_CONFIG_HEADER environment variable had when the > + Kconfig instance was created, or the empty string if > + KCONFIG_CONFIG_HEADER wasn't set. This string is inserted verbatim at the > + beginning of configuration files. See write_config(). > + > + header_header: > + The value the KCONFIG_AUTOHEADER_HEADER environment variable had when the > + Kconfig instance was created, or the empty string if > + KCONFIG_AUTOHEADER_HEADER wasn't set. This string is inserted verbatim at > + the beginning of header files. See write_autoconf(). > + > + filename/linenr: > + The current parsing location, for use in Python preprocessor functions. > + See the module docstring. > + """ > + __slots__ = ( > + "_encoding", > + "_functions", > + "_set_match", > + "_srctree_prefix", > + "_unset_match", > + "_warn_assign_no_prompt", > + "choices", > + "comments", > + "config_header", > + "config_prefix", > + "const_syms", > + "defconfig_list", > + "defined_syms", > + "env_vars", > + "header_header", > + "kconfig_filenames", > + "m", > + "menus", > + "missing_syms", > + "modules", > + "n", > + "named_choices", > + "srctree", > + "syms", > + "top_node", > + "unique_choices", > + "unique_defined_syms", > + "variables", > + "warn", > + "warn_assign_override", > + "warn_assign_redun", > + "warn_assign_undef", > + "warn_to_stderr", > + "warnings", > + "y", > + > + # Parsing-related > + "_parsing_kconfigs", > + "_readline", > + "filename", > + "linenr", > + "_include_path", > + "_filestack", > + "_line", > + "_tokens", > + "_tokens_i", > + "_reuse_tokens", > + ) > + > + # > + # Public interface > + # > + > + def __init__(self, filename="Kconfig", warn=True, warn_to_stderr=True, > + encoding="utf-8", suppress_traceback=False): > + """ > + Creates a new Kconfig object by parsing Kconfig files. > + Note that Kconfig files are not the same as .config files (which store > + configuration symbol values). > + > + See the module docstring for some environment variables that influence > + default warning settings (KCONFIG_WARN_UNDEF and > + KCONFIG_WARN_UNDEF_ASSIGN). > + > + Raises KconfigError on syntax/semantic errors, and OSError or (possibly > + a subclass of) IOError on IO errors ('errno', 'strerror', and > + 'filename' are available). Note that IOError is an alias for OSError on > + Python 3, so it's enough to catch OSError there. If you need Python 2/3 > + compatibility, it's easiest to catch EnvironmentError, which is a > + common base class of OSError/IOError on Python 2 and an alias for > + OSError on Python 3. > + > + filename (default: "Kconfig"): > + The Kconfig file to load. For the Linux kernel, you'll want "Kconfig" > + from the top-level directory, as environment variables will make sure > + the right Kconfig is included from there (arch/$SRCARCH/Kconfig as of > + writing). > + > + If $srctree is set, 'filename' will be looked up relative to it. > + $srctree is also used to look up source'd files within Kconfig files. > + See the class documentation. > + > + If you are using Kconfiglib via 'make scriptconfig', the filename of > + the base base Kconfig file will be in sys.argv[1]. It's currently > + always "Kconfig" in practice. > + > + warn (default: True): > + True if warnings related to this configuration should be generated. > + This can be changed later by setting Kconfig.warn to True/False. It > + is provided as a constructor argument since warnings might be > + generated during parsing. > + > + See the other Kconfig.warn_* variables as well, which enable or > + suppress certain warnings when warnings are enabled. > + > + All generated warnings are added to the Kconfig.warnings list. See > + the class documentation. > + > + warn_to_stderr (default: True): > + True if warnings should be printed to stderr in addition to being > + added to Kconfig.warnings. > + > + This can be changed later by setting Kconfig.warn_to_stderr to > + True/False. > + > + encoding (default: "utf-8"): > + The encoding to use when reading and writing files, and when decoding > + output from commands run via $(shell). If None, the encoding > + specified in the current locale will be used. > + > + The "utf-8" default avoids exceptions on systems that are configured > + to use the C locale, which implies an ASCII encoding. > + > + This parameter has no effect on Python 2, due to implementation > + issues (regular strings turning into Unicode strings, which are > + distinct in Python 2). Python 2 doesn't decode regular strings > + anyway. > + > + Related PEP: https://www.python.org/dev/peps/pep-0538/ > + > + suppress_traceback (default: False): > + Helper for tools. When True, any EnvironmentError or KconfigError > + generated during parsing is caught, the exception message is printed > + to stderr together with the command name, and sys.exit(1) is called > + (which generates SystemExit). > + > + This hides the Python traceback for "expected" errors like syntax > + errors in Kconfig files. > + > + Other exceptions besides EnvironmentError and KconfigError are still > + propagated when suppress_traceback is True. > + """ > + try: > + self._init(filename, warn, warn_to_stderr, encoding) > + except (EnvironmentError, KconfigError) as e: > + if suppress_traceback: > + cmd = sys.argv[0] # Empty string if missing > + if cmd: > + cmd += ": " > + # Some long exception messages have extra newlines for better > + # formatting when reported as an unhandled exception. Strip > + # them here. > + sys.exit(cmd + str(e).strip()) > + raise > + > + def _init(self, filename, warn, warn_to_stderr, encoding): > + # See __init__() > + > + self._encoding = encoding > + > + self.srctree = os.getenv("srctree", "") > + # A prefix we can reliably strip from glob() results to get a filename > + # relative to $srctree. relpath() can cause issues for symlinks, > + # because it assumes symlink/../foo is the same as foo/. > + self._srctree_prefix = realpath(self.srctree) + os.sep > + > + self.warn = warn > + self.warn_to_stderr = warn_to_stderr > + self.warn_assign_undef = os.getenv("KCONFIG_WARN_UNDEF_ASSIGN") == "y" > + self.warn_assign_override = True > + self.warn_assign_redun = True > + self._warn_assign_no_prompt = True > + > + self.warnings = [] > + > + self.config_prefix = os.getenv("CONFIG_", "CONFIG_") > + # Regular expressions for parsing .config files > + self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)") > + self._unset_match = _re_match(r"# {}([^ ]+) is not set".format( > + self.config_prefix)) > + > + self.config_header = os.getenv("KCONFIG_CONFIG_HEADER", "") > + self.header_header = os.getenv("KCONFIG_AUTOHEADER_HEADER", "") > + > + self.syms = {} > + self.const_syms = {} > + self.defined_syms = [] > + self.missing_syms = [] > + self.named_choices = {} > + self.choices = [] > + self.menus = [] > + self.comments = [] > + > + for nmy in "n", "m", "y": > + sym = Symbol() > + sym.kconfig = self > + sym.name = nmy > + sym.is_constant = True > + sym.orig_type = TRISTATE > + sym._cached_tri_val = STR_TO_TRI[nmy] > + > + self.const_syms[nmy] = sym > + > + self.n = self.const_syms["n"] > + self.m = self.const_syms["m"] > + self.y = self.const_syms["y"] > + > + # Make n/m/y well-formed symbols > + for nmy in "n", "m", "y": > + sym = self.const_syms[nmy] > + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n > + > + # Maps preprocessor variables names to Variable instances > + self.variables = {} > + > + # Predefined preprocessor functions, with min/max number of arguments > + self._functions = { > + "info": (_info_fn, 1, 1), > + "error-if": (_error_if_fn, 2, 2), > + "filename": (_filename_fn, 0, 0), > + "lineno": (_lineno_fn, 0, 0), > + "shell": (_shell_fn, 1, 1), > + "warning-if": (_warning_if_fn, 2, 2), > + } > + > + # Add any user-defined preprocessor functions > + try: > + self._functions.update( > + importlib.import_module( > + os.getenv("KCONFIG_FUNCTIONS", "kconfigfunctions") > + ).functions) > + except ImportError: > + pass > + > + # This determines whether previously unseen symbols are registered. > + # They shouldn't be if we parse expressions after parsing, as part of > + # Kconfig.eval_string(). > + self._parsing_kconfigs = True > + > + self.modules = self._lookup_sym("MODULES") > + self.defconfig_list = None > + > + self.top_node = MenuNode() > + self.top_node.kconfig = self > + self.top_node.item = MENU > + self.top_node.is_menuconfig = True > + self.top_node.visibility = self.y > + self.top_node.prompt = ("Main menu", self.y) > + self.top_node.parent = None > + self.top_node.dep = self.y > + self.top_node.filename = filename > + self.top_node.linenr = 1 > + self.top_node.include_path = () > + > + # Parse the Kconfig files > + > + # Not used internally. Provided as a convenience. > + self.kconfig_filenames = [filename] > + self.env_vars = set() > + > + # Keeps track of the location in the parent Kconfig files. Kconfig > + # files usually source other Kconfig files. See _enter_file(). > + self._filestack = [] > + self._include_path = () > + > + # The current parsing location > + self.filename = filename > + self.linenr = 0 > + > + # Used to avoid retokenizing lines when we discover that they're not > + # part of the construct currently being parsed. This is kinda like an > + # unget operation. > + self._reuse_tokens = False > + > + # Open the top-level Kconfig file. Store the readline() method directly > + # as a small optimization. > + self._readline = self._open(join(self.srctree, filename), "r").readline > + > + try: > + # Parse the Kconfig files. Returns the last node, which we > + # terminate with '.next = None'. > + self._parse_block(None, self.top_node, self.top_node).next = None > + self.top_node.list = self.top_node.next > + self.top_node.next = None > + except UnicodeDecodeError as e: > + _decoding_error(e, self.filename) > + > + # Close the top-level Kconfig file. __self__ fetches the 'file' object > + # for the method. > + self._readline.__self__.close() > + > + self._parsing_kconfigs = False > + > + # Do various menu tree post-processing > + self._finalize_node(self.top_node, self.y) > + > + self.unique_defined_syms = _ordered_unique(self.defined_syms) > + self.unique_choices = _ordered_unique(self.choices) > + > + # Do sanity checks. Some of these depend on everything being finalized. > + self._check_sym_sanity() > + self._check_choice_sanity() > + > + # KCONFIG_STRICT is an older alias for KCONFIG_WARN_UNDEF, supported > + # for backwards compatibility > + if os.getenv("KCONFIG_WARN_UNDEF") == "y" or \ > + os.getenv("KCONFIG_STRICT") == "y": > + > + self._check_undef_syms() > + > + # Build Symbol._dependents for all symbols and choices > + self._build_dep() > + > + # Check for dependency loops > + check_dep_loop_sym = _check_dep_loop_sym # Micro-optimization > + for sym in self.unique_defined_syms: > + check_dep_loop_sym(sym, False) > + > + # Add extra dependencies from choices to choice symbols that get > + # awkward during dependency loop detection > + self._add_choice_deps() > + > + @property > + def mainmenu_text(self): > + """ > + See the class documentation. > + """ > + return self.top_node.prompt[0] > + > + @property > + def defconfig_filename(self): > + """ > + See the class documentation. > + """ > + if self.defconfig_list: > + for filename, cond in self.defconfig_list.defaults: > + if expr_value(cond): > + try: > + with self._open_config(filename.str_value) as f: > + return f.name > + except EnvironmentError: > + continue > + > + return None > + > + def load_config(self, filename=None, replace=True, verbose=None): > + """ > + Loads symbol values from a file in the .config format. Equivalent to > + calling Symbol.set_value() to set each of the values. > + > + "# CONFIG_FOO is not set" within a .config file sets the user value of > + FOO to n. The C tools work the same way. > + > + For each symbol, the Symbol.user_value attribute holds the value the > + symbol was assigned in the .config file (if any). The user value might > + differ from Symbol.str/tri_value if there are unsatisfied dependencies. > + > + Calling this function also updates the Kconfig.missing_syms attribute > + with a list of all assignments to undefined symbols within the > + configuration file. Kconfig.missing_syms is cleared if 'replace' is > + True, and appended to otherwise. See the documentation for > + Kconfig.missing_syms as well. > + > + See the Kconfig.__init__() docstring for raised exceptions > + (OSError/IOError). KconfigError is never raised here. > + > + filename (default: None): > + Path to load configuration from (a string). Respects $srctree if set > + (see the class documentation). > + > + If 'filename' is None (the default), the configuration file to load > + (if any) is calculated automatically, giving the behavior you'd > + usually want: > + > + 1. If the KCONFIG_CONFIG environment variable is set, it gives the > + path to the configuration file to load. Otherwise, ".config" is > + used. See standard_config_filename(). > + > + 2. If the path from (1.) doesn't exist, the configuration file > + given by kconf.defconfig_filename is loaded instead, which is > + derived from the 'option defconfig_list' symbol. > + > + 3. If (1.) and (2.) fail to find a configuration file to load, no > + configuration file is loaded, and symbols retain their current > + values (e.g., their default values). This is not an error. > + > + See the return value as well. > + > + replace (default: True): > + If True, all existing user values will be cleared before loading the > + .config. Pass False to merge configurations. > + > + verbose (default: None): > + Limited backwards compatibility to prevent crashes. A warning is > + printed if anything but None is passed. > + > + Prior to Kconfiglib 12.0.0, this option enabled printing of messages > + to stdout when 'filename' was None. A message is (always) returned > + now instead, which is more flexible. > + > + Will probably be removed in some future version. > + > + Returns a string with a message saying which file got loaded (or > + possibly that no file got loaded, when 'filename' is None). This is > + meant to reduce boilerplate in tools, which can do e.g. > + print(kconf.load_config()). The returned message distinguishes between > + loading (replace == True) and merging (replace == False). > + """ > + if verbose is not None: > + _warn_verbose_deprecated("load_config") > + > + msg = None > + if filename is None: > + filename = standard_config_filename() > + if not exists(filename) and \ > + not exists(join(self.srctree, filename)): > + defconfig = self.defconfig_filename > + if defconfig is None: > + return "Using default symbol values (no '{}')" \ > + .format(filename) > + > + msg = " default configuration '{}' (no '{}')" \ > + .format(defconfig, filename) > + filename = defconfig > + > + if not msg: > + msg = " configuration '{}'".format(filename) > + > + # Disable the warning about assigning to symbols without prompts. This > + # is normal and expected within a .config file. > + self._warn_assign_no_prompt = False > + > + # This stub only exists to make sure _warn_assign_no_prompt gets > + # reenabled > + try: > + self._load_config(filename, replace) > + except UnicodeDecodeError as e: > + _decoding_error(e, filename) > + finally: > + self._warn_assign_no_prompt = True > + > + return ("Loaded" if replace else "Merged") + msg > + > + def _load_config(self, filename, replace): > + with self._open_config(filename) as f: > + if replace: > + self.missing_syms = [] > + > + # If we're replacing the configuration, keep track of which > + # symbols and choices got set so that we can unset the rest > + # later. This avoids invalidating everything and is faster. > + # Another benefit is that invalidation must be rock solid for > + # it to work, making it a good test. > + > + for sym in self.unique_defined_syms: > + sym._was_set = False > + > + for choice in self.unique_choices: > + choice._was_set = False > + > + # Small optimizations > + set_match = self._set_match > + unset_match = self._unset_match > + get_sym = self.syms.get > + > + for linenr, line in enumerate(f, 1): > + # The C tools ignore trailing whitespace > + line = line.rstrip() > + > + match = set_match(line) > + if match: > + name, val = match.groups() > + sym = get_sym(name) > + if not sym or not sym.nodes: > + self._undef_assign(name, val, filename, linenr) > + continue > + > + if sym.orig_type in _BOOL_TRISTATE: > + # The C implementation only checks the first character > + # to the right of '=', for whatever reason > + if not (sym.orig_type is BOOL > + and val.startswith(("y", "n")) or > + sym.orig_type is TRISTATE > + and val.startswith(("y", "m", "n"))): > + self._warn("'{}' is not a valid value for the {} " > + "symbol {}. Assignment ignored." > + .format(val, TYPE_TO_STR[sym.orig_type], > + sym.name_and_loc), > + filename, linenr) > + continue > + > + val = val[0] > + > + if sym.choice and val != "n": > + # During .config loading, we infer the mode of the > + # choice from the kind of values that are assigned > + # to the choice symbols > + > + prev_mode = sym.choice.user_value > + if prev_mode is not None and \ > + TRI_TO_STR[prev_mode] != val: > + > + self._warn("both m and y assigned to symbols " > + "within the same choice", > + filename, linenr) > + > + # Set the choice's mode > + sym.choice.set_value(val) > + > + elif sym.orig_type is STRING: > + match = _conf_string_match(val) > + if not match: > + self._warn("malformed string literal in " > + "assignment to {}. Assignment ignored." > + .format(sym.name_and_loc), > + filename, linenr) > + continue > + > + val = unescape(match.group(1)) > + > + else: > + match = unset_match(line) > + if not match: > + # Print a warning for lines that match neither > + # set_match() nor unset_match() and that are not blank > + # lines or comments. 'line' has already been > + # rstrip()'d, so blank lines show up as "" here. > + if line and not line.lstrip().startswith("#"): > + self._warn("ignoring malformed line '{}'" > + .format(line), > + filename, linenr) > + > + continue > + > + name = match.group(1) > + sym = get_sym(name) > + if not sym or not sym.nodes: > + self._undef_assign(name, "n", filename, linenr) > + continue > + > + if sym.orig_type not in _BOOL_TRISTATE: > + continue > + > + val = "n" > + > + # Done parsing the assignment. Set the value. > + > + if sym._was_set: > + self._assigned_twice(sym, val, filename, linenr) > + > + sym.set_value(val) > + > + if replace: > + # If we're replacing the configuration, unset the symbols that > + # didn't get set > + > + for sym in self.unique_defined_syms: > + if not sym._was_set: > + sym.unset_value() > + > + for choice in self.unique_choices: > + if not choice._was_set: > + choice.unset_value() > + > + def _undef_assign(self, name, val, filename, linenr): > + # Called for assignments to undefined symbols during .config loading > + > + self.missing_syms.append((name, val)) > + if self.warn_assign_undef: > + self._warn( > + "attempt to assign the value '{}' to the undefined symbol {}" > + .format(val, name), filename, linenr) > + > + def _assigned_twice(self, sym, new_val, filename, linenr): > + # Called when a symbol is assigned more than once in a .config file > + > + # Use strings for bool/tristate user values in the warning > + if sym.orig_type in _BOOL_TRISTATE: > + user_val = TRI_TO_STR[sym.user_value] > + else: > + user_val = sym.user_value > + > + msg = '{} set more than once. Old value "{}", new value "{}".'.format( > + sym.name_and_loc, user_val, new_val) > + > + if user_val == new_val: > + if self.warn_assign_redun: > + self._warn(msg, filename, linenr) > + elif self.warn_assign_override: > + self._warn(msg, filename, linenr) > + > + def load_allconfig(self, filename): > + """ > + Helper for all*config. Loads (merges) the configuration file specified > + by KCONFIG_ALLCONFIG, if any. See Documentation/kbuild/kconfig.txt in > + the Linux kernel. > + > + Disables warnings for duplicated assignments within configuration files > + for the duration of the call > + (kconf.warn_assign_override/warn_assign_redun = False), and restores > + the previous warning settings at the end. The KCONFIG_ALLCONFIG > + configuration file is expected to override symbols. > + > + Exits with sys.exit() (which raises a SystemExit exception) and prints > + an error to stderr if KCONFIG_ALLCONFIG is set but the configuration > + file can't be opened. > + > + filename: > + Command-specific configuration filename - "allyes.config", > + "allno.config", etc. > + """ > + load_allconfig(self, filename) > + > + def write_autoconf(self, filename=None, header=None): > + r""" > + Writes out symbol values as a C header file, matching the format used > + by include/generated/autoconf.h in the kernel. > + > + The ordering of the #defines matches the one generated by > + write_config(). The order in the C implementation depends on the hash > + table implementation as of writing, and so won't match. > + > + If 'filename' exists and its contents is identical to what would get > + written out, it is left untouched. This avoids updating file metadata > + like the modification time and possibly triggering redundant work in > + build tools. > + > + filename (default: None): > + Path to write header to. > + > + If None (the default), the path in the environment variable > + KCONFIG_AUTOHEADER is used if set, and "include/generated/autoconf.h" > + otherwise. This is compatible with the C tools. > + > + header (default: None): > + Text inserted verbatim at the beginning of the file. You would > + usually want it enclosed in '/* */' to make it a C comment, and > + include a trailing newline. > + > + If None (the default), the value of the environment variable > + KCONFIG_AUTOHEADER_HEADER had when the Kconfig instance was created > + will be used if it was set, and no header otherwise. See the > + Kconfig.header_header attribute. > + > + Returns a string with a message saying that the header got saved, or > + that there were no changes to it. This is meant to reduce boilerplate > + in tools, which can do e.g. print(kconf.write_autoconf()). > + """ > + if filename is None: > + filename = os.getenv("KCONFIG_AUTOHEADER", > + "include/generated/autoconf.h") > + > + if self._write_if_changed(filename, self._autoconf_contents(header)): > + return "Kconfig header saved to '{}'".format(filename) > + return "No change to Kconfig header in '{}'".format(filename) > + > + def _autoconf_contents(self, header): > + # write_autoconf() helper. Returns the contents to write as a string, > + # with 'header' or KCONFIG_AUTOHEADER_HEADER at the beginning. > + > + if header is None: > + header = self.header_header > + > + chunks = [header] # "".join()ed later > + add = chunks.append > + > + for sym in self.unique_defined_syms: > + # _write_to_conf is determined when the value is calculated. This > + # is a hidden function call due to property magic. > + # > + # Note: In client code, you can check if sym.config_string is empty > + # instead, to avoid accessing the internal _write_to_conf variable > + # (though it's likely to keep working). > + val = sym.str_value > + if not sym._write_to_conf: > + continue > + > + if sym.orig_type in _BOOL_TRISTATE: > + if val == "y": > + add("#define {}{} 1\n" > + .format(self.config_prefix, sym.name)) > + elif val == "m": > + add("#define {}{}_MODULE 1\n" > + .format(self.config_prefix, sym.name)) > + > + elif sym.orig_type is STRING: > + add('#define {}{} "{}"\n' > + .format(self.config_prefix, sym.name, escape(val))) > + > + else: # sym.orig_type in _INT_HEX: > + if sym.orig_type is HEX and \ > + not val.startswith(("0x", "0X")): > + val = "0x" + val > + > + add("#define {}{} {}\n" > + .format(self.config_prefix, sym.name, val)) > + > + return "".join(chunks) > + > + def write_config(self, filename=None, header=None, save_old=True, > + verbose=None): > + r""" > + Writes out symbol values in the .config format. The format matches the > + C implementation, including ordering. > + > + Symbols appear in the same order in generated .config files as they do > + in the Kconfig files. For symbols defined in multiple locations, a > + single assignment is written out corresponding to the first location > + where the symbol is defined. > + > + See the 'Intro to symbol values' section in the module docstring to > + understand which symbols get written out. > + > + If 'filename' exists and its contents is identical to what would get > + written out, it is left untouched. This avoids updating file metadata > + like the modification time and possibly triggering redundant work in > + build tools. > + > + See the Kconfig.__init__() docstring for raised exceptions > + (OSError/IOError). KconfigError is never raised here. > + > + filename (default: None): > + Path to write configuration to (a string). > + > + If None (the default), the path in the environment variable > + KCONFIG_CONFIG is used if set, and ".config" otherwise. See > + standard_config_filename(). > + > + header (default: None): > + Text inserted verbatim at the beginning of the file. You would > + usually want each line to start with '#' to make it a comment, and > + include a trailing newline. > + > + if None (the default), the value of the environment variable > + KCONFIG_CONFIG_HEADER had when the Kconfig instance was created will > + be used if it was set, and no header otherwise. See the > + Kconfig.config_header attribute. > + > + save_old (default: True): > + If True and <filename> already exists, a copy of it will be saved to > + <filename>.old in the same directory before the new configuration is > + written. > + > + Errors are silently ignored if <filename>.old cannot be written (e.g. > + due to being a directory, or <filename> being something like > + /dev/null). > + > + verbose (default: None): > + Limited backwards compatibility to prevent crashes. A warning is > + printed if anything but None is passed. > + > + Prior to Kconfiglib 12.0.0, this option enabled printing of messages > + to stdout when 'filename' was None. A message is (always) returned > + now instead, which is more flexible. > + > + Will probably be removed in some future version. > + > + Returns a string with a message saying which file got saved. This is > + meant to reduce boilerplate in tools, which can do e.g. > + print(kconf.write_config()). > + """ > + if verbose is not None: > + _warn_verbose_deprecated("write_config") > + > + if filename is None: > + filename = standard_config_filename() > + > + contents = self._config_contents(header) > + if self._contents_eq(filename, contents): > + return "No change to configuration in '{}'".format(filename) > + > + if save_old: > + _save_old(filename) > + > + with self._open(filename, "w") as f: > + f.write(contents) > + > + return "Configuration saved to '{}'".format(filename) > + > + def _config_contents(self, header): > + # write_config() helper. Returns the contents to write as a string, > + # with 'header' or KCONFIG_CONFIG_HEADER at the beginning. > + # > + # More memory friendly would be to 'yield' the strings and > + # "".join(_config_contents()), but it was a bit slower on my system. > + > + # node_iter() was used here before commit 3aea9f7 ("Add '# end of > + # <menu>' after menus in .config"). Those comments get tricky to > + # implement with it. > + > + for sym in self.unique_defined_syms: > + sym._visited = False > + > + if header is None: > + header = self.config_header > + > + chunks = [header] # "".join()ed later > + add = chunks.append > + > + # Did we just print an '# end of ...' comment? > + after_end_comment = False > + > + node = self.top_node > + while 1: > + # Jump to the next node with an iterative tree walk > + if node.list: > + node = node.list > + elif node.next: > + node = node.next > + else: > + while node.parent: > + node = node.parent > + > + # Add a comment when leaving visible menus > + if node.item is MENU and expr_value(node.dep) and \ > + expr_value(node.visibility) and \ > + node is not self.top_node: > + add("# end of {}\n".format(node.prompt[0])) > + after_end_comment = True > + > + if node.next: > + node = node.next > + break > + else: > + # No more nodes > + return "".join(chunks) > + > + # Generate configuration output for the node > + > + item = node.item > + > + if item.__class__ is Symbol: > + if item._visited: > + continue > + item._visited = True > + > + conf_string = item.config_string > + if not conf_string: > + continue > + > + if after_end_comment: > + # Add a blank line before the first symbol printed after an > + # '# end of ...' comment > + after_end_comment = False > + add("\n") > + add(conf_string) > + > + elif expr_value(node.dep) and \ > + ((item is MENU and expr_value(node.visibility)) or > + item is COMMENT): > + > + add("\n#\n# {}\n#\n".format(node.prompt[0])) > + after_end_comment = False > + > + def write_min_config(self, filename, header=None): > + """ > + Writes out a "minimal" configuration file, omitting symbols whose value > + matches their default value. The format matches the one produced by > + 'make savedefconfig'. > + > + The resulting configuration file is incomplete, but a complete > + configuration can be derived from it by loading it. Minimal > + configuration files can serve as a more manageable configuration format > + compared to a "full" .config file, especially when configurations files > + are merged or edited by hand. > + > + See the Kconfig.__init__() docstring for raised exceptions > + (OSError/IOError). KconfigError is never raised here. > + > + filename: > + Path to write minimal configuration to. > + > + header (default: None): > + Text inserted verbatim at the beginning of the file. You would > + usually want each line to start with '#' to make it a comment, and > + include a final terminating newline. > + > + if None (the default), the value of the environment variable > + KCONFIG_CONFIG_HEADER had when the Kconfig instance was created will > + be used if it was set, and no header otherwise. See the > + Kconfig.config_header attribute. > + > + Returns a string with a message saying the minimal configuration got > + saved, or that there were no changes to it. This is meant to reduce > + boilerplate in tools, which can do e.g. > + print(kconf.write_min_config()). > + """ > + if self._write_if_changed(filename, self._min_config_contents(header)): > + return "Minimal configuration saved to '{}'".format(filename) > + return "No change to minimal configuration in '{}'".format(filename) > + > + def _min_config_contents(self, header): > + # write_min_config() helper. Returns the contents to write as a string, > + # with 'header' or KCONFIG_CONFIG_HEADER at the beginning. > + > + if header is None: > + header = self.config_header > + > + chunks = [header] # "".join()ed later > + add = chunks.append > + > + for sym in self.unique_defined_syms: > + # Skip symbols that cannot be changed. Only check > + # non-choice symbols, as selects don't affect choice > + # symbols. > + if not sym.choice and \ > + sym.visibility <= expr_value(sym.rev_dep): > + continue > + > + # Skip symbols whose value matches their default > + if sym.str_value == sym._str_default(): > + continue > + > + # Skip symbols that would be selected by default in a > + # choice, unless the choice is optional or the symbol type > + # isn't bool (it might be possible to set the choice mode > + # to n or the symbol to m in those cases). > + if sym.choice and \ > + not sym.choice.is_optional and \ > + sym.choice._selection_from_defaults() is sym and \ > + sym.orig_type is BOOL and \ > + sym.tri_value == 2: > + continue > + > + add(sym.config_string) > + > + return "".join(chunks) > + > + def sync_deps(self, path): > + """ > + Creates or updates a directory structure that can be used to avoid > + doing a full rebuild whenever the configuration is changed, mirroring > + include/config/ in the kernel. > + > + This function is intended to be called during each build, before > + compiling source files that depend on configuration symbols. > + > + See the Kconfig.__init__() docstring for raised exceptions > + (OSError/IOError). KconfigError is never raised here. > + > + path: > + Path to directory > + > + sync_deps(path) does the following: > + > + 1. If the directory <path> does not exist, it is created. > + > + 2. If <path>/auto.conf exists, old symbol values are loaded from it, > + which are then compared against the current symbol values. If a > + symbol has changed value (would generate different output in > + autoconf.h compared to before), the change is signaled by > + touch'ing a file corresponding to the symbol. > + > + The first time sync_deps() is run on a directory, <path>/auto.conf > + won't exist, and no old symbol values will be available. This > + logically has the same effect as updating the entire > + configuration. > + > + The path to a symbol's file is calculated from the symbol's name > + by replacing all '_' with '/' and appending '.h'. For example, the > + symbol FOO_BAR_BAZ gets the file <path>/foo/bar/baz.h, and FOO > + gets the file <path>/foo.h. > + > + This scheme matches the C tools. The point is to avoid having a > + single directory with a huge number of files, which the underlying > + filesystem might not handle well. > + > + 3. A new auto.conf with the current symbol values is written, to keep > + track of them for the next build. > + > + If auto.conf exists and its contents is identical to what would > + get written out, it is left untouched. This avoids updating file > + metadata like the modification time and possibly triggering > + redundant work in build tools. > + > + > + The last piece of the puzzle is knowing what symbols each source file > + depends on. Knowing that, dependencies can be added from source files > + to the files corresponding to the symbols they depends on. The source > + file will then get recompiled (only) when the symbol value changes > + (provided sync_deps() is run first during each build). > + > + The tool in the kernel that extracts symbol dependencies from source > + files is scripts/basic/fixdep.c. Missing symbol files also correspond > + to "not changed", which fixdep deals with by using the $(wildcard) Make > + function when adding symbol prerequisites to source files. > + > + In case you need a different scheme for your project, the sync_deps() > + implementation can be used as a template. > + """ > + if not exists(path): > + os.mkdir(path, 0o755) > + > + # Load old values from auto.conf, if any > + self._load_old_vals(path) > + > + for sym in self.unique_defined_syms: > + # _write_to_conf is determined when the value is calculated. This > + # is a hidden function call due to property magic. > + # > + # Note: In client code, you can check if sym.config_string is empty > + # instead, to avoid accessing the internal _write_to_conf variable > + # (though it's likely to keep working). > + val = sym.str_value > + > + # n tristate values do not get written to auto.conf and autoconf.h, > + # making a missing symbol logically equivalent to n > + > + if sym._write_to_conf: > + if sym._old_val is None and \ > + sym.orig_type in _BOOL_TRISTATE and \ > + val == "n": > + # No old value (the symbol was missing or n), new value n. > + # No change. > + continue > + > + if val == sym._old_val: > + # New value matches old. No change. > + continue > + > + elif sym._old_val is None: > + # The symbol wouldn't appear in autoconf.h (because > + # _write_to_conf is false), and it wouldn't have appeared in > + # autoconf.h previously either (because it didn't appear in > + # auto.conf). No change. > + continue > + > + # 'sym' has a new value. Flag it. > + _touch_dep_file(path, sym.name) > + > + # Remember the current values as the "new old" values. > + # > + # This call could go anywhere after the call to _load_old_vals(), but > + # putting it last means _sync_deps() can be safely rerun if it fails > + # before this point. > + self._write_old_vals(path) > + > + def _load_old_vals(self, path): > + # Loads old symbol values from auto.conf into a dedicated > + # Symbol._old_val field. Mirrors load_config(). > + # > + # The extra field could be avoided with some trickery involving dumping > + # symbol values and restoring them later, but this is simpler and > + # faster. The C tools also use a dedicated field for this purpose. > + > + for sym in self.unique_defined_syms: > + sym._old_val = None > + > + try: > + auto_conf = self._open(join(path, "auto.conf"), "r") > + except EnvironmentError as e: > + if e.errno == errno.ENOENT: > + # No old values > + return > + raise > + > + with auto_conf as f: > + for line in f: > + match = self._set_match(line) > + if not match: > + # We only expect CONFIG_FOO=... (and possibly a header > + # comment) in auto.conf > + continue > + > + name, val = match.groups() > + if name in self.syms: > + sym = self.syms[name] > + > + if sym.orig_type is STRING: > + match = _conf_string_match(val) > + if not match: > + continue > + val = unescape(match.group(1)) > + > + self.syms[name]._old_val = val > + else: > + # Flag that the symbol no longer exists, in > + # case something still depends on it > + _touch_dep_file(path, name) > + > + def _write_old_vals(self, path): > + # Helper for writing auto.conf. Basically just a simplified > + # write_config() that doesn't write any comments (including > + # '# CONFIG_FOO is not set' comments). The format matches the C > + # implementation, though the ordering is arbitrary there (depends on > + # the hash table implementation). > + # > + # A separate helper function is neater than complicating write_config() > + # by passing a flag to it, plus we only need to look at symbols here. > + > + self._write_if_changed( > + os.path.join(path, "auto.conf"), > + self._old_vals_contents()) > + > + def _old_vals_contents(self): > + # _write_old_vals() helper. Returns the contents to write as a string. > + > + # Temporary list instead of generator makes this a bit faster > + return "".join([ > + sym.config_string for sym in self.unique_defined_syms > + if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value) > + ]) > + > + def node_iter(self, unique_syms=False): > + """ > + Returns a generator for iterating through all MenuNode's in the Kconfig > + tree. The iteration is done in Kconfig definition order (each node is > + visited before its children, and the children of a node are visited > + before the next node). > + > + The Kconfig.top_node menu node is skipped. It contains an implicit menu > + that holds the top-level items. > + > + As an example, the following code will produce a list equal to > + Kconfig.defined_syms: > + > + defined_syms = [node.item for node in kconf.node_iter() > + if isinstance(node.item, Symbol)] > + > + unique_syms (default: False): > + If True, only the first MenuNode will be included for symbols defined > + in multiple locations. > + > + Using kconf.node_iter(True) in the example above would give a list > + equal to unique_defined_syms. > + """ > + if unique_syms: > + for sym in self.unique_defined_syms: > + sym._visited = False > + > + node = self.top_node > + while 1: > + # Jump to the next node with an iterative tree walk > + if node.list: > + node = node.list > + elif node.next: > + node = node.next > + else: > + while node.parent: > + node = node.parent > + if node.next: > + node = node.next > + break > + else: > + # No more nodes > + return > + > + if unique_syms and node.item.__class__ is Symbol: > + if node.item._visited: > + continue > + node.item._visited = True > + > + yield node > + > + def eval_string(self, s): > + """ > + Returns the tristate value of the expression 's', represented as 0, 1, > + and 2 for n, m, and y, respectively. Raises KconfigError on syntax > + errors. Warns if undefined symbols are referenced. > + > + As an example, if FOO and BAR are tristate symbols at least one of > + which has the value y, then eval_string("y && (FOO || BAR)") returns > + 2 (y). > + > + To get the string value of non-bool/tristate symbols, use > + Symbol.str_value. eval_string() always returns a tristate value, and > + all non-bool/tristate symbols have the tristate value 0 (n). > + > + The expression parsing is consistent with how parsing works for > + conditional ('if ...') expressions in the configuration, and matches > + the C implementation. m is rewritten to 'm && MODULES', so > + eval_string("m") will return 0 (n) unless modules are enabled. > + """ > + # The parser is optimized to be fast when parsing Kconfig files (where > + # an expression can never appear at the beginning of a line). We have > + # to monkey-patch things a bit here to reuse it. > + > + self.filename = None > + > + self._tokens = self._tokenize("if " + s) > + # Strip "if " to avoid giving confusing error messages > + self._line = s > + self._tokens_i = 1 # Skip the 'if' token > + > + return expr_value(self._expect_expr_and_eol()) > + > + def unset_values(self): > + """ > + Removes any user values from all symbols, as if Kconfig.load_config() > + or Symbol.set_value() had never been called. > + """ > + self._warn_assign_no_prompt = False > + try: > + # set_value() already rejects undefined symbols, and they don't > + # need to be invalidated (because their value never changes), so we > + # can just iterate over defined symbols > + for sym in self.unique_defined_syms: > + sym.unset_value() > + > + for choice in self.unique_choices: > + choice.unset_value() > + finally: > + self._warn_assign_no_prompt = True > + > + def enable_warnings(self): > + """ > + Do 'Kconfig.warn = True' instead. Maintained for backwards > + compatibility. > + """ > + self.warn = True > + > + def disable_warnings(self): > + """ > + Do 'Kconfig.warn = False' instead. Maintained for backwards > + compatibility. > + """ > + self.warn = False > + > + def enable_stderr_warnings(self): > + """ > + Do 'Kconfig.warn_to_stderr = True' instead. Maintained for backwards > + compatibility. > + """ > + self.warn_to_stderr = True > + > + def disable_stderr_warnings(self): > + """ > + Do 'Kconfig.warn_to_stderr = False' instead. Maintained for backwards > + compatibility. > + """ > + self.warn_to_stderr = False > + > + def enable_undef_warnings(self): > + """ > + Do 'Kconfig.warn_assign_undef = True' instead. Maintained for backwards > + compatibility. > + """ > + self.warn_assign_undef = True > + > + def disable_undef_warnings(self): > + """ > + Do 'Kconfig.warn_assign_undef = False' instead. Maintained for > + backwards compatibility. > + """ > + self.warn_assign_undef = False > + > + def enable_override_warnings(self): > + """ > + Do 'Kconfig.warn_assign_override = True' instead. Maintained for > + backwards compatibility. > + """ > + self.warn_assign_override = True > + > + def disable_override_warnings(self): > + """ > + Do 'Kconfig.warn_assign_override = False' instead. Maintained for > + backwards compatibility. > + """ > + self.warn_assign_override = False > + > + def enable_redun_warnings(self): > + """ > + Do 'Kconfig.warn_assign_redun = True' instead. Maintained for backwards > + compatibility. > + """ > + self.warn_assign_redun = True > + > + def disable_redun_warnings(self): > + """ > + Do 'Kconfig.warn_assign_redun = False' instead. Maintained for > + backwards compatibility. > + """ > + self.warn_assign_redun = False > + > + def __repr__(self): > + """ > + Returns a string with information about the Kconfig object when it is > + evaluated on e.g. the interactive Python prompt. > + """ > + def status(flag): > + return "enabled" if flag else "disabled" > + > + return "<{}>".format(", ".join(( > + "configuration with {} symbols".format(len(self.syms)), > + 'main menu prompt "{}"'.format(self.mainmenu_text), > + "srctree is current directory" if not self.srctree else > + 'srctree "{}"'.format(self.srctree), > + 'config symbol prefix "{}"'.format(self.config_prefix), > + "warnings " + status(self.warn), > + "printing of warnings to stderr " + status(self.warn_to_stderr), > + "undef. symbol assignment warnings " + > + status(self.warn_assign_undef), > + "overriding symbol assignment warnings " + > + status(self.warn_assign_override), > + "redundant symbol assignment warnings " + > + status(self.warn_assign_redun) > + ))) > + > + # > + # Private methods > + # > + > + > + # > + # File reading > + # > + > + def _open_config(self, filename): > + # Opens a .config file. First tries to open 'filename', then > + # '$srctree/filename' if $srctree was set when the configuration was > + # loaded. > + > + try: > + return self._open(filename, "r") > + except EnvironmentError as e: > + # This will try opening the same file twice if $srctree is unset, > + # but it's not a big deal > + try: > + return self._open(join(self.srctree, filename), "r") > + except EnvironmentError as e2: > + # This is needed for Python 3, because e2 is deleted after > + # the try block: > + # > + # https://docs.python.org/3/reference/compound_stmts.html#the-try-statement > + e = e2 > + > + raise _KconfigIOError( > + e, "Could not open '{}' ({}: {}). Check that the $srctree " > + "environment variable ({}) is set correctly." > + .format(filename, errno.errorcode[e.errno], e.strerror, > + "set to '{}'".format(self.srctree) if self.srctree > + else "unset or blank")) > + > + def _enter_file(self, filename): > + # Jumps to the beginning of a sourced Kconfig file, saving the previous > + # position and file object. > + # > + # filename: > + # Absolute path to file > + > + # Path relative to $srctree, stored in e.g. self.filename (which makes > + # it indirectly show up in MenuNode.filename). Equals 'filename' for > + # absolute paths passed to 'source'. > + if filename.startswith(self._srctree_prefix): > + # Relative path (or a redundant absolute path to within $srctree, > + # but it's probably fine to reduce those too) > + rel_filename = filename[len(self._srctree_prefix):] > + else: > + # Absolute path > + rel_filename = filename > + > + self.kconfig_filenames.append(rel_filename) > + > + # The parent Kconfig files are represented as a list of > + # (<include path>, <Python 'file' object for Kconfig file>) tuples. > + # > + # <include path> is immutable and holds a *tuple* of > + # (<filename>, <linenr>) tuples, giving the locations of the 'source' > + # statements in the parent Kconfig files. The current include path is > + # also available in Kconfig._include_path. > + # > + # The point of this redundant setup is to allow Kconfig._include_path > + # to be assigned directly to MenuNode.include_path without having to > + # copy it, sharing it wherever possible. > + > + # Save include path and 'file' object (via its 'readline' function) > + # before entering the file > + self._filestack.append((self._include_path, self._readline)) > + > + # _include_path is a tuple, so this rebinds the variable instead of > + # doing in-place modification > + self._include_path += ((self.filename, self.linenr),) > + > + # Check for recursive 'source' > + for name, _ in self._include_path: > + if name == rel_filename: > + raise KconfigError( > + "\n{}:{}: recursive 'source' of '{}' detected. Check that " > + "environment variables are set correctly.\n" > + "Include path:\n{}" > + .format(self.filename, self.linenr, rel_filename, > + "\n".join("{}:{}".format(name, linenr) > + for name, linenr in self._include_path))) > + > + try: > + self._readline = self._open(filename, "r").readline > + except EnvironmentError as e: > + # We already know that the file exists > + raise _KconfigIOError( > + e, "{}:{}: Could not open '{}' (in '{}') ({}: {})" > + .format(self.filename, self.linenr, filename, > + self._line.strip(), > + errno.errorcode[e.errno], e.strerror)) > + > + self.filename = rel_filename > + self.linenr = 0 > + > + def _leave_file(self): > + # Returns from a Kconfig file to the file that sourced it. See > + # _enter_file(). > + > + # Restore location from parent Kconfig file > + self.filename, self.linenr = self._include_path[-1] > + # Restore include path and 'file' object > + self._readline.__self__.close() # __self__ fetches the 'file' object > + self._include_path, self._readline = self._filestack.pop() > + > + def _next_line(self): > + # Fetches and tokenizes the next line from the current Kconfig file. > + # Returns False at EOF and True otherwise. > + > + # We might already have tokens from parsing a line and discovering that > + # it's part of a different construct > + if self._reuse_tokens: > + self._reuse_tokens = False > + # self._tokens_i is known to be 1 here, because _parse_props() > + # leaves it like that when it can't recognize a line (or parses a > + # help text) > + return True > + > + # readline() returns '' over and over at EOF, which we rely on for help > + # texts at the end of files (see _line_after_help()) > + line = self._readline() > + if not line: > + return False > + self.linenr += 1 > + > + # Handle line joining > + while line.endswith("\\\n"): > + line = line[:-2] + self._readline() > + self.linenr += 1 > + > + self._tokens = self._tokenize(line) > + # Initialize to 1 instead of 0 to factor out code from _parse_block() > + # and _parse_props(). They immediately fetch self._tokens[0]. > + self._tokens_i = 1 > + > + return True > + > + def _line_after_help(self, line): > + # Tokenizes a line after a help text. This case is special in that the > + # line has already been fetched (to discover that it isn't part of the > + # help text). > + # > + # An earlier version used a _saved_line variable instead that was > + # checked in _next_line(). This special-casing gets rid of it and makes > + # _reuse_tokens alone sufficient to handle unget. > + > + # Handle line joining > + while line.endswith("\\\n"): > + line = line[:-2] + self._readline() > + self.linenr += 1 > + > + self._tokens = self._tokenize(line) > + self._reuse_tokens = True > + > + def _write_if_changed(self, filename, contents): > + # Writes 'contents' into 'filename', but only if it differs from the > + # current contents of the file. > + # > + # Another variant would be write a temporary file on the same > + # filesystem, compare the files, and rename() the temporary file if it > + # differs, but it breaks stuff like write_config("/dev/null"), which is > + # used out there to force evaluation-related warnings to be generated. > + # This simple version is pretty failsafe and portable. > + # > + # Returns True if the file has changed and is updated, and False > + # otherwise. > + > + if self._contents_eq(filename, contents): > + return False > + with self._open(filename, "w") as f: > + f.write(contents) > + return True > + > + def _contents_eq(self, filename, contents): > + # Returns True if the contents of 'filename' is 'contents' (a string), > + # and False otherwise (including if 'filename' can't be opened/read) > + > + try: > + with self._open(filename, "r") as f: > + # Robust re. things like encoding and line endings (mmap() > + # trickery isn't) > + return f.read(len(contents) + 1) == contents > + except EnvironmentError: > + # If the error here would prevent writing the file as well, we'll > + # notice it later > + return False > + > + # > + # Tokenization > + # > + > + def _lookup_sym(self, name): > + # Fetches the symbol 'name' from the symbol table, creating and > + # registering it if it does not exist. If '_parsing_kconfigs' is False, > + # it means we're in eval_string(), and new symbols won't be registered. > + > + if name in self.syms: > + return self.syms[name] > + > + sym = Symbol() > + sym.kconfig = self > + sym.name = name > + sym.is_constant = False > + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n > + > + if self._parsing_kconfigs: > + self.syms[name] = sym > + else: > + self._warn("no symbol {} in configuration".format(name)) > + > + return sym > + > + def _lookup_const_sym(self, name): > + # Like _lookup_sym(), for constant (quoted) symbols > + > + if name in self.const_syms: > + return self.const_syms[name] > + > + sym = Symbol() > + sym.kconfig = self > + sym.name = name > + sym.is_constant = True > + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n > + > + if self._parsing_kconfigs: > + self.const_syms[name] = sym > + > + return sym > + > + def _tokenize(self, s): > + # Parses 's', returning a None-terminated list of tokens. Registers any > + # new symbols encountered with _lookup(_const)_sym(). > + # > + # Tries to be reasonably speedy by processing chunks of text via > + # regexes and string operations where possible. This is the biggest > + # hotspot during parsing. > + # > + # It might be possible to rewrite this to 'yield' tokens instead, > + # working across multiple lines. Lookback and compatibility with old > + # janky versions of the C tools complicate things though. > + > + self._line = s # Used for error reporting > + > + # Initial token on the line > + match = _command_match(s) > + if not match: > + if s.isspace() or s.lstrip().startswith("#"): > + return (None,) > + self._parse_error("unknown token at start of line") > + > + # Tricky implementation detail: While parsing a token, 'token' refers > + # to the previous token. See _STRING_LEX for why this is needed. > + token = _get_keyword(match.group(1)) > + if not token: > + # Backwards compatibility with old versions of the C tools, which > + # (accidentally) accepted stuff like "--help--" and "-help---". > + # This was fixed in the C tools by commit c2264564 ("kconfig: warn > + # of unhandled characters in Kconfig commands"), committed in July > + # 2015, but it seems people still run Kconfiglib on older kernels. > + if s.strip(" \t\n-") == "help": > + return (_T_HELP, None) > + > + # If the first token is not a keyword (and not a weird help token), > + # we have a preprocessor variable assignment (or a bare macro on a > + # line) > + self._parse_assignment(s) > + return (None,) > + > + tokens = [token] > + # The current index in the string being tokenized > + i = match.end() > + > + # Main tokenization loop (for tokens past the first one) > + while i < len(s): > + # Test for an identifier/keyword first. This is the most common > + # case. > + match = _id_keyword_match(s, i) > + if match: > + # We have an identifier or keyword > + > + # Check what it is. lookup_sym() will take care of allocating > + # new symbols for us the first time we see them. Note that > + # 'token' still refers to the previous token. > + > + name = match.group(1) > + keyword = _get_keyword(name) > + if keyword: > + # It's a keyword > + token = keyword > + # Jump past it > + i = match.end() > + > + elif token not in _STRING_LEX: > + # It's a non-const symbol, except we translate n, m, and y > + # into the corresponding constant symbols, like the C > + # implementation > + > + if "$" in name: > + # Macro expansion within symbol name > + name, s, i = self._expand_name(s, i) > + else: > + i = match.end() > + > + token = self.const_syms[name] if name in STR_TO_TRI else \ > + self._lookup_sym(name) > + > + else: > + # It's a case of missing quotes. For example, the > + # following is accepted: > + # > + # menu unquoted_title > + # > + # config A > + # tristate unquoted_prompt > + # > + # endmenu > + # > + # Named choices ('choice FOO') also end up here. > + > + if token is not _T_CHOICE: > + self._warn("style: quotes recommended around '{}' in '{}'" > + .format(name, self._line.strip()), > + self.filename, self.linenr) > + > + token = name > + i = match.end() > + > + else: > + # Neither a keyword nor a non-const symbol > + > + # We always strip whitespace after tokens, so it is safe to > + # assume that s[i] is the start of a token here. > + c = s[i] > + > + if c in "\"'": > + if "$" not in s and "\\" not in s: > + # Fast path for lines without $ and \. Find the > + # matching quote. > + end_i = s.find(c, i + 1) + 1 > + if not end_i: > + self._parse_error("unterminated string") > + val = s[i + 1:end_i - 1] > + i = end_i > + else: > + # Slow path > + s, end_i = self._expand_str(s, i) > + > + # os.path.expandvars() and the $UNAME_RELEASE replace() > + # is a backwards compatibility hack, which should be > + # reasonably safe as expandvars() leaves references to > + # undefined env. vars. as is. > + # > + # The preprocessor functionality changed how > + # environment variables are referenced, to $(FOO). > + val = expandvars(s[i + 1:end_i - 1] > + .replace("$UNAME_RELEASE", > + _UNAME_RELEASE)) > + > + i = end_i > + > + # This is the only place where we don't survive with a > + # single token of lookback: 'option env="FOO"' does not > + # refer to a constant symbol named "FOO". > + token = \ > + val if token in _STRING_LEX or tokens[0] is _T_OPTION \ > + else self._lookup_const_sym(val) > + > + elif s.startswith("&&", i): > + token = _T_AND > + i += 2 > + > + elif s.startswith("||", i): > + token = _T_OR > + i += 2 > + > + elif c == "=": > + token = _T_EQUAL > + i += 1 > + > + elif s.startswith("!=", i): > + token = _T_UNEQUAL > + i += 2 > + > + elif c == "!": > + token = _T_NOT > + i += 1 > + > + elif c == "(": > + token = _T_OPEN_PAREN > + i += 1 > + > + elif c == ")": > + token = _T_CLOSE_PAREN > + i += 1 > + > + elif c == "#": > + break > + > + > + # Very rare > + > + elif s.startswith("<=", i): > + token = _T_LESS_EQUAL > + i += 2 > + > + elif c == "<": > + token = _T_LESS > + i += 1 > + > + elif s.startswith(">=", i): > + token = _T_GREATER_EQUAL > + i += 2 > + > + elif c == ">": > + token = _T_GREATER > + i += 1 > + > + > + else: > + self._parse_error("unknown tokens in line") > + > + > + # Skip trailing whitespace > + while i < len(s) and s[i].isspace(): > + i += 1 > + > + > + # Add the token > + tokens.append(token) > + > + # None-terminating the token list makes token fetching simpler/faster > + tokens.append(None) > + > + return tokens > + > + # Helpers for syntax checking and token fetching. See the > + # 'Intro to expressions' section for what a constant symbol is. > + # > + # More of these could be added, but the single-use cases are inlined as an > + # optimization. > + > + def _expect_sym(self): > + token = self._tokens[self._tokens_i] > + self._tokens_i += 1 > + > + if token.__class__ is not Symbol: > + self._parse_error("expected symbol") > + > + return token > + > + def _expect_nonconst_sym(self): > + # Used for 'select' and 'imply' only. We know the token indices. > + > + token = self._tokens[1] > + self._tokens_i = 2 > + > + if token.__class__ is not Symbol or token.is_constant: > + self._parse_error("expected nonconstant symbol") > + > + return token > + > + def _expect_str_and_eol(self): > + token = self._tokens[self._tokens_i] > + self._tokens_i += 1 > + > + if token.__class__ is not str: > + self._parse_error("expected string") > + > + if self._tokens[self._tokens_i] is not None: > + self._trailing_tokens_error() > + > + return token > + > + def _expect_expr_and_eol(self): > + expr = self._parse_expr(True) > + > + if self._tokens[self._tokens_i] is not None: > + self._trailing_tokens_error() > + > + return expr > + > + def _check_token(self, token): > + # If the next token is 'token', removes it and returns True > + > + if self._tokens[self._tokens_i] is token: > + self._tokens_i += 1 > + return True > + return False > + > + # > + # Preprocessor logic > + # > + > + def _parse_assignment(self, s): > + # Parses a preprocessor variable assignment, registering the variable > + # if it doesn't already exist. Also takes care of bare macros on lines > + # (which are allowed, and can be useful for their side effects). > + > + # Expand any macros in the left-hand side of the assignment (the > + # variable name) > + s = s.lstrip() > + i = 0 > + while 1: > + i = _assignment_lhs_fragment_match(s, i).end() > + if s.startswith("$(", i): > + s, i = self._expand_macro(s, i, ()) > + else: > + break > + > + if s.isspace(): > + # We also accept a bare macro on a line (e.g. > + # $(warning-if,$(foo),ops)), provided it expands to a blank string > + return > + > + # Assigned variable > + name = s[:i] > + > + > + # Extract assignment operator (=, :=, or +=) and value > + rhs_match = _assignment_rhs_match(s, i) > + if not rhs_match: > + self._parse_error("syntax error") > + > + op, val = rhs_match.groups() > + > + > + if name in self.variables: > + # Already seen variable > + var = self.variables[name] > + else: > + # New variable > + var = Variable() > + var.kconfig = self > + var.name = name > + var._n_expansions = 0 > + self.variables[name] = var > + > + # += acts like = on undefined variables (defines a recursive > + # variable) > + if op == "+=": > + op = "=" > + > + if op == "=": > + var.is_recursive = True > + var.value = val > + elif op == ":=": > + var.is_recursive = False > + var.value = self._expand_whole(val, ()) > + else: # op == "+=" > + # += does immediate expansion if the variable was last set > + # with := > + var.value += " " + (val if var.is_recursive else > + self._expand_whole(val, ())) > + > + def _expand_whole(self, s, args): > + # Expands preprocessor macros in all of 's'. Used whenever we don't > + # have to worry about delimiters. See _expand_macro() re. the 'args' > + # parameter. > + # > + # Returns the expanded string. > + > + i = 0 > + while 1: > + i = s.find("$(", i) > + if i == -1: > + break > + s, i = self._expand_macro(s, i, args) > + return s > + > + def _expand_name(self, s, i): > + # Expands a symbol name starting at index 'i' in 's'. > + # > + # Returns the expanded name, the expanded 's' (including the part > + # before the name), and the index of the first character in the next > + # token after the name. > + > + s, end_i = self._expand_name_iter(s, i) > + name = s[i:end_i] > + # isspace() is False for empty strings > + if not name.strip(): > + # Avoid creating a Kconfig symbol with a blank name. It's almost > + # guaranteed to be an error. > + self._parse_error("macro expanded to blank string") > + > + # Skip trailing whitespace > + while end_i < len(s) and s[end_i].isspace(): > + end_i += 1 > + > + return name, s, end_i > + > + def _expand_name_iter(self, s, i): > + # Expands a symbol name starting at index 'i' in 's'. > + # > + # Returns the expanded 's' (including the part before the name) and the > + # index of the first character after the expanded name in 's'. > + > + while 1: > + match = _name_special_search(s, i) > + > + if match.group() != "$(": > + return (s, match.start()) > + s, i = self._expand_macro(s, match.start(), ()) > + > + def _expand_str(self, s, i): > + # Expands a quoted string starting at index 'i' in 's'. Handles both > + # backslash escapes and macro expansion. > + # > + # Returns the expanded 's' (including the part before the string) and > + # the index of the first character after the expanded string in 's'. > + > + quote = s[i] > + i += 1 # Skip over initial "/' > + while 1: > + match = _string_special_search(s, i) > + if not match: > + self._parse_error("unterminated string") > + > + > + if match.group() == quote: > + # Found the end of the string > + return (s, match.end()) > + > + elif match.group() == "\\": > + # Replace '\x' with 'x'. 'i' ends up pointing to the character > + # after 'x', which allows macros to be canceled with '\$(foo)'. > + i = match.end() > + s = s[:match.start()] + s[i:] > + > + elif match.group() == "$(": > + # A macro call within the string > + s, i = self._expand_macro(s, match.start(), ()) > + > + else: > + # A ' quote within " quotes or vice versa > + i += 1 > + > + def _expand_macro(self, s, i, args): > + # Expands a macro starting at index 'i' in 's'. If this macro resulted > + # from the expansion of another macro, 'args' holds the arguments > + # passed to that macro. > + # > + # Returns the expanded 's' (including the part before the macro) and > + # the index of the first character after the expanded macro in 's'. > + > + res = s[:i] > + i += 2 # Skip over "$(" > + > + arg_start = i # Start of current macro argument > + new_args = [] # Arguments of this macro call > + nesting = 0 # Current parentheses nesting level > + > + while 1: > + match = _macro_special_search(s, i) > + if not match: > + self._parse_error("missing end parenthesis in macro expansion") > + > + > + if match.group() == "(": > + nesting += 1 > + i = match.end() > + > + elif match.group() == ")": > + if nesting: > + nesting -= 1 > + i = match.end() > + continue > + > + # Found the end of the macro > + > + new_args.append(s[arg_start:match.start()]) > + > + # $(1) is replaced by the first argument to the function, etc., > + # provided at least that many arguments were passed > + > + try: > + # Does the macro look like an integer, with a corresponding > + # argument? If so, expand it to the value of the argument. > + res += args[int(new_args[0])] > + except (ValueError, IndexError): > + # Regular variables are just functions without arguments, > + # and also go through the function value path > + res += self._fn_val(new_args) > + > + return (res + s[match.end():], len(res)) > + > + elif match.group() == ",": > + i = match.end() > + if nesting: > + continue > + > + # Found the end of a macro argument > + new_args.append(s[arg_start:match.start()]) > + arg_start = i > + > + else: # match.group() == "$(" > + # A nested macro call within the macro > + s, i = self._expand_macro(s, match.start(), args) > + > + def _fn_val(self, args): > + # Returns the result of calling the function args[0] with the arguments > + # args[1..len(args)-1]. Plain variables are treated as functions > + # without arguments. > + > + fn = args[0] > + > + if fn in self.variables: > + var = self.variables[fn] > + > + if len(args) == 1: > + # Plain variable > + if var._n_expansions: > + self._parse_error("Preprocessor variable {} recursively " > + "references itself".format(var.name)) > + elif var._n_expansions > 100: > + # Allow functions to call themselves, but guess that functions > + # that are overly recursive are stuck > + self._parse_error("Preprocessor function {} seems stuck " > + "in infinite recursion".format(var.name)) > + > + var._n_expansions += 1 > + res = self._expand_whole(self.variables[fn].value, args) > + var._n_expansions -= 1 > + return res > + > + if fn in self._functions: > + # Built-in or user-defined function > + > + py_fn, min_arg, max_arg = self._functions[fn] > + > + if len(args) - 1 < min_arg or \ > + (max_arg is not None and len(args) - 1 > max_arg): > + > + if min_arg == max_arg: > + expected_args = min_arg > + elif max_arg is None: > + expected_args = "{} or more".format(min_arg) > + else: > + expected_args = "{}-{}".format(min_arg, max_arg) > + > + raise KconfigError("{}:{}: bad number of arguments in call " > + "to {}, expected {}, got {}" > + .format(self.filename, self.linenr, fn, > + expected_args, len(args) - 1)) > + > + return py_fn(self, *args) > + > + # Environment variables are tried last > + if fn in os.environ: > + self.env_vars.add(fn) > + return os.environ[fn] > + > + return "" > + > + # > + # Parsing > + # > + > + def _make_and(self, e1, e2): > + # Constructs an AND (&&) expression. Performs trivial simplification. > + > + if e1 is self.y: > + return e2 > + > + if e2 is self.y: > + return e1 > + > + if e1 is self.n or e2 is self.n: > + return self.n > + > + return (AND, e1, e2) > + > + def _make_or(self, e1, e2): > + # Constructs an OR (||) expression. Performs trivial simplification. > + > + if e1 is self.n: > + return e2 > + > + if e2 is self.n: > + return e1 > + > + if e1 is self.y or e2 is self.y: > + return self.y > + > + return (OR, e1, e2) > + > + def _parse_block(self, end_token, parent, prev): > + # Parses a block, which is the contents of either a file or an if, > + # menu, or choice statement. > + # > + # end_token: > + # The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. > + # None for files. > + # > + # parent: > + # The parent menu node, corresponding to a menu, Choice, or 'if'. > + # 'if's are flattened after parsing. > + # > + # prev: > + # The previous menu node. New nodes will be added after this one (by > + # modifying 'next' pointers). > + # > + # 'prev' is reused to parse a list of child menu nodes (for a menu or > + # Choice): After parsing the children, the 'next' pointer is assigned > + # to the 'list' pointer to "tilt up" the children above the node. > + # > + # Returns the final menu node in the block (or 'prev' if the block is > + # empty). This allows chaining. > + > + while self._next_line(): > + t0 = self._tokens[0] > + > + if t0 is _T_CONFIG or t0 is _T_MENUCONFIG: > + # The tokenizer allocates Symbol objects for us > + sym = self._tokens[1] > + > + if sym.__class__ is not Symbol or sym.is_constant: > + self._parse_error("missing or bad symbol name") > + > + if self._tokens[2] is not None: > + self._trailing_tokens_error() > + > + self.defined_syms.append(sym) > + > + node = MenuNode() > + node.kconfig = self > + node.item = sym > + node.is_menuconfig = (t0 is _T_MENUCONFIG) > + node.prompt = node.help = node.list = None > + node.parent = parent > + node.filename = self.filename > + node.linenr = self.linenr > + node.include_path = self._include_path > + > + sym.nodes.append(node) > + > + self._parse_props(node) > + > + if node.is_menuconfig and not node.prompt: > + self._warn("the menuconfig symbol {} has no prompt" > + .format(sym.name_and_loc)) > + > + # Equivalent to > + # > + # prev.next = node > + # prev = node > + # > + # due to tricky Python semantics. The order matters. > + prev.next = prev = node > + > + elif t0 is None: > + # Blank line > + continue > + > + elif t0 in _SOURCE_TOKENS: > + pattern = self._expect_str_and_eol() > + > + if t0 in _REL_SOURCE_TOKENS: > + # Relative source > + pattern = join(dirname(self.filename), pattern) > + > + # - glob() doesn't support globbing relative to a directory, so > + # we need to prepend $srctree to 'pattern'. Use join() > + # instead of '+' so that an absolute path in 'pattern' is > + # preserved. > + # > + # - Sort the glob results to ensure a consistent ordering of > + # Kconfig symbols, which indirectly ensures a consistent > + # ordering in e.g. .config files > + filenames = sorted(iglob(join(self._srctree_prefix, pattern))) > + > + if not filenames and t0 in _OBL_SOURCE_TOKENS: > + raise KconfigError( > + "{}:{}: '{}' not found (in '{}'). Check that " > + "environment variables are set correctly (e.g. " > + "$srctree, which is {}). Also note that unset " > + "environment variables expand to the empty string." > + .format(self.filename, self.linenr, pattern, > + self._line.strip(), > + "set to '{}'".format(self.srctree) > + if self.srctree else "unset or blank")) > + > + for filename in filenames: > + self._enter_file(filename) > + prev = self._parse_block(None, parent, prev) > + self._leave_file() > + > + elif t0 is end_token: > + # Reached the end of the block. Terminate the final node and > + # return it. > + > + if self._tokens[1] is not None: > + self._trailing_tokens_error() > + > + prev.next = None > + return prev > + > + elif t0 is _T_IF: > + node = MenuNode() > + node.item = node.prompt = None > + node.parent = parent > + node.dep = self._expect_expr_and_eol() > + > + self._parse_block(_T_ENDIF, node, node) > + node.list = node.next > + > + prev.next = prev = node > + > + elif t0 is _T_MENU: > + node = MenuNode() > + node.kconfig = self > + node.item = t0 # _T_MENU == MENU > + node.is_menuconfig = True > + node.prompt = (self._expect_str_and_eol(), self.y) > + node.visibility = self.y > + node.parent = parent > + node.filename = self.filename > + node.linenr = self.linenr > + node.include_path = self._include_path > + > + self.menus.append(node) > + > + self._parse_props(node) > + self._parse_block(_T_ENDMENU, node, node) > + node.list = node.next > + > + prev.next = prev = node > + > + elif t0 is _T_COMMENT: > + node = MenuNode() > + node.kconfig = self > + node.item = t0 # _T_COMMENT == COMMENT > + node.is_menuconfig = False > + node.prompt = (self._expect_str_and_eol(), self.y) > + node.list = None > + node.parent = parent > + node.filename = self.filename > + node.linenr = self.linenr > + node.include_path = self._include_path > + > + self.comments.append(node) > + > + self._parse_props(node) > + > + prev.next = prev = node > + > + elif t0 is _T_CHOICE: > + if self._tokens[1] is None: > + choice = Choice() > + choice.direct_dep = self.n > + else: > + # Named choice > + name = self._expect_str_and_eol() > + choice = self.named_choices.get(name) > + if not choice: > + choice = Choice() > + choice.name = name > + choice.direct_dep = self.n > + self.named_choices[name] = choice > + > + self.choices.append(choice) > + > + node = MenuNode() > + node.kconfig = choice.kconfig = self > + node.item = choice > + node.is_menuconfig = True > + node.prompt = node.help = None > + node.parent = parent > + node.filename = self.filename > + node.linenr = self.linenr > + node.include_path = self._include_path > + > + choice.nodes.append(node) > + > + self._parse_props(node) > + self._parse_block(_T_ENDCHOICE, node, node) > + node.list = node.next > + > + prev.next = prev = node > + > + elif t0 is _T_MAINMENU: > + self.top_node.prompt = (self._expect_str_and_eol(), self.y) > + > + else: > + # A valid endchoice/endif/endmenu is caught by the 'end_token' > + # check above > + self._parse_error( > + "no corresponding 'choice'" if t0 is _T_ENDCHOICE else > + "no corresponding 'if'" if t0 is _T_ENDIF else > + "no corresponding 'menu'" if t0 is _T_ENDMENU else > + "unrecognized construct") > + > + # End of file reached. Return the last node. > + > + if end_token: > + raise KconfigError( > + "error: expected '{}' at end of '{}'" > + .format("endchoice" if end_token is _T_ENDCHOICE else > + "endif" if end_token is _T_ENDIF else > + "endmenu", > + self.filename)) > + > + return prev > + > + def _parse_cond(self): > + # Parses an optional 'if <expr>' construct and returns the parsed > + # <expr>, or self.y if the next token is not _T_IF > + > + expr = self._parse_expr(True) if self._check_token(_T_IF) else self.y > + > + if self._tokens[self._tokens_i] is not None: > + self._trailing_tokens_error() > + > + return expr > + > + def _parse_props(self, node): > + # Parses and adds properties to the MenuNode 'node' (type, 'prompt', > + # 'default's, etc.) Properties are later copied up to symbols and > + # choices in a separate pass after parsing, in e.g. > + # _add_props_to_sym(). > + # > + # An older version of this code added properties directly to symbols > + # and choices instead of to their menu nodes (and handled dependency > + # propagation simultaneously), but that loses information on where a > + # property is added when a symbol or choice is defined in multiple > + # locations. Some Kconfig configuration systems rely heavily on such > + # symbols, and better docs can be generated by keeping track of where > + # properties are added. > + # > + # node: > + # The menu node we're parsing properties on > + > + # Dependencies from 'depends on'. Will get propagated to the properties > + # below. > + node.dep = self.y > + > + while self._next_line(): > + t0 = self._tokens[0] > + > + if t0 in _TYPE_TOKENS: > + # Relies on '_T_BOOL is BOOL', etc., to save a conversion > + self._set_type(node.item, t0) > + if self._tokens[1] is not None: > + self._parse_prompt(node) > + > + elif t0 is _T_DEPENDS: > + if not self._check_token(_T_ON): > + self._parse_error("expected 'on' after 'depends'") > + > + node.dep = self._make_and(node.dep, > + self._expect_expr_and_eol()) > + > + elif t0 is _T_HELP: > + self._parse_help(node) > + > + elif t0 is _T_SELECT: > + if node.item.__class__ is not Symbol: > + self._parse_error("only symbols can select") > + > + node.selects.append((self._expect_nonconst_sym(), > + self._parse_cond())) > + > + elif t0 is None: > + # Blank line > + continue > + > + elif t0 is _T_DEFAULT: > + node.defaults.append((self._parse_expr(False), > + self._parse_cond())) > + > + elif t0 in _DEF_TOKEN_TO_TYPE: > + self._set_type(node.item, _DEF_TOKEN_TO_TYPE[t0]) > + node.defaults.append((self._parse_expr(False), > + self._parse_cond())) > + > + elif t0 is _T_PROMPT: > + self._parse_prompt(node) > + > + elif t0 is _T_RANGE: > + node.ranges.append((self._expect_sym(), self._expect_sym(), > + self._parse_cond())) > + > + elif t0 is _T_IMPLY: > + if node.item.__class__ is not Symbol: > + self._parse_error("only symbols can imply") > + > + node.implies.append((self._expect_nonconst_sym(), > + self._parse_cond())) > + > + elif t0 is _T_VISIBLE: > + if not self._check_token(_T_IF): > + self._parse_error("expected 'if' after 'visible'") > + > + node.visibility = self._make_and(node.visibility, > + self._expect_expr_and_eol()) > + > + elif t0 is _T_OPTION: > + if self._check_token(_T_ENV): > + if not self._check_token(_T_EQUAL): > + self._parse_error("expected '=' after 'env'") > + > + env_var = self._expect_str_and_eol() > + node.item.env_var = env_var > + > + if env_var in os.environ: > + node.defaults.append( > + (self._lookup_const_sym(os.environ[env_var]), > + self.y)) > + else: > + self._warn("{1} has 'option env=\"{0}\"', " > + "but the environment variable {0} is not " > + "set".format(node.item.name, env_var), > + self.filename, self.linenr) > + > + if env_var != node.item.name: > + self._warn("Kconfiglib expands environment variables " > + "in strings directly, meaning you do not " > + "need 'option env=...' \"bounce\" symbols. " > + "For compatibility with the C tools, " > + "rename {} to {} (so that the symbol name " > + "matches the environment variable name)." > + .format(node.item.name, env_var), > + self.filename, self.linenr) > + > + elif self._check_token(_T_DEFCONFIG_LIST): > + if not self.defconfig_list: > + self.defconfig_list = node.item > + else: > + self._warn("'option defconfig_list' set on multiple " > + "symbols ({0} and {1}). Only {0} will be " > + "used.".format(self.defconfig_list.name, > + node.item.name), > + self.filename, self.linenr) > + > + elif self._check_token(_T_MODULES): > + # To reduce warning spam, only warn if 'option modules' is > + # set on some symbol that isn't MODULES, which should be > + # safe. I haven't run into any projects that make use > + # modules besides the kernel yet, and there it's likely to > + # keep being called "MODULES". > + if node.item is not self.modules: > + self._warn("the 'modules' option is not supported. " > + "Let me know if this is a problem for you, " > + "as it wouldn't be that hard to implement. " > + "Note that modules are supported -- " > + "Kconfiglib just assumes the symbol name " > + "MODULES, like older versions of the C " > + "implementation did when 'option modules' " > + "wasn't used.", > + self.filename, self.linenr) > + > + elif self._check_token(_T_ALLNOCONFIG_Y): > + if node.item.__class__ is not Symbol: > + self._parse_error("the 'allnoconfig_y' option is only " > + "valid for symbols") > + > + node.item.is_allnoconfig_y = True > + > + else: > + self._parse_error("unrecognized option") > + > + elif t0 is _T_OPTIONAL: > + if node.item.__class__ is not Choice: > + self._parse_error('"optional" is only valid for choices') > + > + node.item.is_optional = True > + > + else: > + # Reuse the tokens for the non-property line later > + self._reuse_tokens = True > + return > + > + def _set_type(self, sc, new_type): > + # Sets the type of 'sc' (symbol or choice) to 'new_type' > + > + # UNKNOWN is falsy > + if sc.orig_type and sc.orig_type is not new_type: > + self._warn("{} defined with multiple types, {} will be used" > + .format(sc.name_and_loc, TYPE_TO_STR[new_type])) > + > + sc.orig_type = new_type > + > + def _parse_prompt(self, node): > + # 'prompt' properties override each other within a single definition of > + # a symbol, but additional prompts can be added by defining the symbol > + # multiple times > + > + if node.prompt: > + self._warn(node.item.name_and_loc + > + " defined with multiple prompts in single location") > + > + prompt = self._tokens[1] > + self._tokens_i = 2 > + > + if prompt.__class__ is not str: > + self._parse_error("expected prompt string") > + > + if prompt != prompt.strip(): > + self._warn(node.item.name_and_loc + > + " has leading or trailing whitespace in its prompt") > + > + # This avoid issues for e.g. reStructuredText documentation, where > + # '*prompt *' is invalid > + prompt = prompt.strip() > + > + node.prompt = (prompt, self._parse_cond()) > + > + def _parse_help(self, node): > + if node.help is not None: > + self._warn(node.item.name_and_loc + " defined with more than " > + "one help text -- only the last one will be used") > + > + # Micro-optimization. This code is pretty hot. > + readline = self._readline > + > + # Find first non-blank (not all-space) line and get its > + # indentation > + > + while 1: > + line = readline() > + self.linenr += 1 > + if not line: > + self._empty_help(node, line) > + return > + if not line.isspace(): > + break > + > + len_ = len # Micro-optimization > + > + # Use a separate 'expline' variable here and below to avoid stomping on > + # any tabs people might've put deliberately into the first line after > + # the help text > + expline = line.expandtabs() > + indent = len_(expline) - len_(expline.lstrip()) > + if not indent: > + self._empty_help(node, line) > + return > + > + # The help text goes on till the first non-blank line with less indent > + # than the first line > + > + # Add the first line > + lines = [expline[indent:]] > + add_line = lines.append # Micro-optimization > + > + while 1: > + line = readline() > + if line.isspace(): > + # No need to preserve the exact whitespace in these > + add_line("\n") > + elif not line: > + # End of file > + break > + else: > + expline = line.expandtabs() > + if len_(expline) - len_(expline.lstrip()) < indent: > + break > + add_line(expline[indent:]) > + > + self.linenr += len_(lines) > + node.help = "".join(lines).rstrip() > + if line: > + self._line_after_help(line) > + > + def _empty_help(self, node, line): > + self._warn(node.item.name_and_loc + > + " has 'help' but empty help text") > + node.help = "" > + if line: > + self._line_after_help(line) > + > + def _parse_expr(self, transform_m): > + # Parses an expression from the tokens in Kconfig._tokens using a > + # simple top-down approach. See the module docstring for the expression > + # format. > + # > + # transform_m: > + # True if m should be rewritten to m && MODULES. See the > + # Kconfig.eval_string() documentation. > + > + # Grammar: > + # > + # expr: and_expr ['||' expr] > + # and_expr: factor ['&&' and_expr] > + # factor: <symbol> ['='/'!='/'<'/... <symbol>] > + # '!' factor > + # '(' expr ')' > + # > + # It helps to think of the 'expr: and_expr' case as a single-operand OR > + # (no ||), and of the 'and_expr: factor' case as a single-operand AND > + # (no &&). Parsing code is always a bit tricky. > + > + # Mind dump: parse_factor() and two nested loops for OR and AND would > + # work as well. The straightforward implementation there gives a > + # (op, (op, (op, A, B), C), D) parse for A op B op C op D. Representing > + # expressions as (op, [list of operands]) instead goes nicely with that > + # version, but is wasteful for short expressions and complicates > + # expression evaluation and other code that works on expressions (more > + # complicated code likely offsets any performance gain from less > + # recursion too). If we also try to optimize the list representation by > + # merging lists when possible (e.g. when ANDing two AND expressions), > + # we end up allocating a ton of lists instead of reusing expressions, > + # which is bad. > + > + and_expr = self._parse_and_expr(transform_m) > + > + # Return 'and_expr' directly if we have a "single-operand" OR. > + # Otherwise, parse the expression on the right and make an OR node. > + # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). > + return and_expr if not self._check_token(_T_OR) else \ > + (OR, and_expr, self._parse_expr(transform_m)) > + > + def _parse_and_expr(self, transform_m): > + factor = self._parse_factor(transform_m) > + > + # Return 'factor' directly if we have a "single-operand" AND. > + # Otherwise, parse the right operand and make an AND node. This turns > + # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). > + return factor if not self._check_token(_T_AND) else \ > + (AND, factor, self._parse_and_expr(transform_m)) > + > + def _parse_factor(self, transform_m): > + token = self._tokens[self._tokens_i] > + self._tokens_i += 1 > + > + if token.__class__ is Symbol: > + # Plain symbol or relation > + > + if self._tokens[self._tokens_i] not in _RELATIONS: > + # Plain symbol > + > + # For conditional expressions ('depends on <expr>', > + # '... if <expr>', etc.), m is rewritten to m && MODULES. > + if transform_m and token is self.m: > + return (AND, self.m, self.modules) > + > + return token > + > + # Relation > + # > + # _T_EQUAL, _T_UNEQUAL, etc., deliberately have the same values as > + # EQUAL, UNEQUAL, etc., so we can just use the token directly > + self._tokens_i += 1 > + return (self._tokens[self._tokens_i - 1], token, > + self._expect_sym()) > + > + if token is _T_NOT: > + # token == _T_NOT == NOT > + return (token, self._parse_factor(transform_m)) > + > + if token is _T_OPEN_PAREN: > + expr_parse = self._parse_expr(transform_m) > + if self._check_token(_T_CLOSE_PAREN): > + return expr_parse > + > + self._parse_error("malformed expression") > + > + # > + # Caching and invalidation > + # > + > + def _build_dep(self): > + # Populates the Symbol/Choice._dependents sets, which contain all other > + # items (symbols and choices) that immediately depend on the item in > + # the sense that changing the value of the item might affect the value > + # of the dependent items. This is used for caching/invalidation. > + # > + # The calculated sets might be larger than necessary as we don't do any > + # complex analysis of the expressions. > + > + depend_on = _depend_on # Micro-optimization > + > + # Only calculate _dependents for defined symbols. Constant and > + # undefined symbols could theoretically be selected/implied, but it > + # wouldn't change their value, so it's not a true dependency. > + for sym in self.unique_defined_syms: > + # Symbols depend on the following: > + > + # The prompt conditions > + for node in sym.nodes: > + if node.prompt: > + depend_on(sym, node.prompt[1]) > + > + # The default values and their conditions > + for value, cond in sym.defaults: > + depend_on(sym, value) > + depend_on(sym, cond) > + > + # The reverse and weak reverse dependencies > + depend_on(sym, sym.rev_dep) > + depend_on(sym, sym.weak_rev_dep) > + > + # The ranges along with their conditions > + for low, high, cond in sym.ranges: > + depend_on(sym, low) > + depend_on(sym, high) > + depend_on(sym, cond) > + > + # The direct dependencies. This is usually redundant, as the direct > + # dependencies get propagated to properties, but it's needed to get > + # invalidation solid for 'imply', which only checks the direct > + # dependencies (even if there are no properties to propagate it > + # to). > + depend_on(sym, sym.direct_dep) > + > + # In addition to the above, choice symbols depend on the choice > + # they're in, but that's handled automatically since the Choice is > + # propagated to the conditions of the properties before > + # _build_dep() runs. > + > + for choice in self.unique_choices: > + # Choices depend on the following: > + > + # The prompt conditions > + for node in choice.nodes: > + if node.prompt: > + depend_on(choice, node.prompt[1]) > + > + # The default symbol conditions > + for _, cond in choice.defaults: > + depend_on(choice, cond) > + > + def _add_choice_deps(self): > + # Choices also depend on the choice symbols themselves, because the > + # y-mode selection of the choice might change if a choice symbol's > + # visibility changes. > + # > + # We add these dependencies separately after dependency loop detection. > + # The invalidation algorithm can handle the resulting > + # <choice symbol> <-> <choice> dependency loops, but they make loop > + # detection awkward. > + > + for choice in self.unique_choices: > + for sym in choice.syms: > + sym._dependents.add(choice) > + > + def _invalidate_all(self): > + # Undefined symbols never change value and don't need to be > + # invalidated, so we can just iterate over defined symbols. > + # Invalidating constant symbols would break things horribly. > + for sym in self.unique_defined_syms: > + sym._invalidate() > + > + for choice in self.unique_choices: > + choice._invalidate() > + > + # > + # Post-parsing menu tree processing, including dependency propagation and > + # implicit submenu creation > + # > + > + def _finalize_node(self, node, visible_if): > + # Finalizes a menu node and its children: > + # > + # - Copies properties from menu nodes up to their contained > + # symbols/choices > + # > + # - Propagates dependencies from parent to child nodes > + # > + # - Creates implicit menus (see kconfig-language.txt) > + # > + # - Removes 'if' nodes > + # > + # - Sets 'choice' types and registers choice symbols > + # > + # menu_finalize() in the C implementation is similar. > + # > + # node: > + # The menu node to finalize. This node and its children will have > + # been finalized when the function returns, and any implicit menus > + # will have been created. > + # > + # visible_if: > + # Dependencies from 'visible if' on parent menus. These are added to > + # the prompts of symbols and choices. > + > + if node.item.__class__ is Symbol: > + # Copy defaults, ranges, selects, and implies to the Symbol > + self._add_props_to_sym(node) > + > + # Find any items that should go in an implicit menu rooted at the > + # symbol > + cur = node > + while cur.next and _auto_menu_dep(node, cur.next): > + # This makes implicit submenu creation work recursively, with > + # implicit menus inside implicit menus > + self._finalize_node(cur.next, visible_if) > + cur = cur.next > + cur.parent = node > + > + if cur is not node: > + # Found symbols that should go in an implicit submenu. Tilt > + # them up above us. > + node.list = node.next > + node.next = cur.next > + cur.next = None > + > + elif node.list: > + # The menu node is a choice, menu, or if. Finalize each child node. > + > + if node.item is MENU: > + visible_if = self._make_and(visible_if, node.visibility) > + > + # Propagate the menu node's dependencies to each child menu node. > + # > + # This needs to go before the recursive _finalize_node() call so > + # that implicit submenu creation can look ahead at dependencies. > + self._propagate_deps(node, visible_if) > + > + # Finalize the children > + cur = node.list > + while cur: > + self._finalize_node(cur, visible_if) > + cur = cur.next > + > + if node.list: > + # node's children have been individually finalized. Do final steps > + # to finalize this "level" in the menu tree. > + _flatten(node.list) > + _remove_ifs(node) > + > + # Empty choices (node.list None) are possible, so this needs to go > + # outside > + if node.item.__class__ is Choice: > + # Add the node's non-node-specific properties to the choice, like > + # _add_props_to_sym() does > + choice = node.item > + choice.direct_dep = self._make_or(choice.direct_dep, node.dep) > + choice.defaults += node.defaults > + > + _finalize_choice(node) > + > + def _propagate_deps(self, node, visible_if): > + # Propagates 'node's dependencies to its child menu nodes > + > + # If the parent node holds a Choice, we use the Choice itself as the > + # parent dependency. This makes sense as the value (mode) of the choice > + # limits the visibility of the contained choice symbols. The C > + # implementation works the same way. > + # > + # Due to the similar interface, Choice works as a drop-in replacement > + # for Symbol here. > + basedep = node.item if node.item.__class__ is Choice else node.dep > + > + cur = node.list > + while cur: > + dep = cur.dep = self._make_and(cur.dep, basedep) > + > + if cur.item.__class__ in _SYMBOL_CHOICE: > + # Propagate 'visible if' and dependencies to the prompt > + if cur.prompt: > + cur.prompt = (cur.prompt[0], > + self._make_and( > + cur.prompt[1], > + self._make_and(visible_if, dep))) > + > + # Propagate dependencies to defaults > + if cur.defaults: > + cur.defaults = [(default, self._make_and(cond, dep)) > + for default, cond in cur.defaults] > + > + # Propagate dependencies to ranges > + if cur.ranges: > + cur.ranges = [(low, high, self._make_and(cond, dep)) > + for low, high, cond in cur.ranges] > + > + # Propagate dependencies to selects > + if cur.selects: > + cur.selects = [(target, self._make_and(cond, dep)) > + for target, cond in cur.selects] > + > + # Propagate dependencies to implies > + if cur.implies: > + cur.implies = [(target, self._make_and(cond, dep)) > + for target, cond in cur.implies] > + > + elif cur.prompt: # Not a symbol/choice > + # Propagate dependencies to the prompt. 'visible if' is only > + # propagated to symbols/choices. > + cur.prompt = (cur.prompt[0], > + self._make_and(cur.prompt[1], dep)) > + > + cur = cur.next > + > + def _add_props_to_sym(self, node): > + # Copies properties from the menu node 'node' up to its contained > + # symbol, and adds (weak) reverse dependencies to selected/implied > + # symbols. > + # > + # This can't be rolled into _propagate_deps(), because that function > + # traverses the menu tree roughly breadth-first, meaning properties on > + # symbols defined in multiple locations could end up in the wrong > + # order. > + > + sym = node.item > + > + # See the Symbol class docstring > + sym.direct_dep = self._make_or(sym.direct_dep, node.dep) > + > + sym.defaults += node.defaults > + sym.ranges += node.ranges > + sym.selects += node.selects > + sym.implies += node.implies > + > + # Modify the reverse dependencies of the selected symbol > + for target, cond in node.selects: > + target.rev_dep = self._make_or( > + target.rev_dep, > + self._make_and(sym, cond)) > + > + # Modify the weak reverse dependencies of the implied > + # symbol > + for target, cond in node.implies: > + target.weak_rev_dep = self._make_or( > + target.weak_rev_dep, > + self._make_and(sym, cond)) > + > + # > + # Misc. > + # > + > + def _check_sym_sanity(self): > + # Checks various symbol properties that are handiest to check after > + # parsing. Only generates errors and warnings. > + > + def num_ok(sym, type_): > + # Returns True if the (possibly constant) symbol 'sym' is valid as a value > + # for a symbol of type type_ (INT or HEX) > + > + # 'not sym.nodes' implies a constant or undefined symbol, e.g. a plain > + # "123" > + if not sym.nodes: > + return _is_base_n(sym.name, _TYPE_TO_BASE[type_]) > + > + return sym.orig_type is type_ > + > + for sym in self.unique_defined_syms: > + if sym.orig_type in _BOOL_TRISTATE: > + # A helper function could be factored out here, but keep it > + # speedy/straightforward > + > + for target_sym, _ in sym.selects: > + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: > + self._warn("{} selects the {} symbol {}, which is not " > + "bool or tristate" > + .format(sym.name_and_loc, > + TYPE_TO_STR[target_sym.orig_type], > + target_sym.name_and_loc)) > + > + for target_sym, _ in sym.implies: > + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: > + self._warn("{} implies the {} symbol {}, which is not " > + "bool or tristate" > + .format(sym.name_and_loc, > + TYPE_TO_STR[target_sym.orig_type], > + target_sym.name_and_loc)) > + > + elif sym.orig_type: # STRING/INT/HEX > + for default, _ in sym.defaults: > + if default.__class__ is not Symbol: > + raise KconfigError( > + "the {} symbol {} has a malformed default {} -- " > + "expected a single symbol" > + .format(TYPE_TO_STR[sym.orig_type], > + sym.name_and_loc, expr_str(default))) > + > + if sym.orig_type is STRING: > + if not default.is_constant and not default.nodes and \ > + not default.name.isupper(): > + # 'default foo' on a string symbol could be either a symbol > + # reference or someone leaving out the quotes. Guess that > + # the quotes were left out if 'foo' isn't all-uppercase > + # (and no symbol named 'foo' exists). > + self._warn("style: quotes recommended around " > + "default value for string symbol " > + + sym.name_and_loc) > + > + elif not num_ok(default, sym.orig_type): # INT/HEX > + self._warn("the {0} symbol {1} has a non-{0} default {2}" > + .format(TYPE_TO_STR[sym.orig_type], > + sym.name_and_loc, > + default.name_and_loc)) > + > + if sym.selects or sym.implies: > + self._warn("the {} symbol {} has selects or implies" > + .format(TYPE_TO_STR[sym.orig_type], > + sym.name_and_loc)) > + > + else: # UNKNOWN > + self._warn("{} defined without a type" > + .format(sym.name_and_loc)) > + > + > + if sym.ranges: > + if sym.orig_type not in _INT_HEX: > + self._warn( > + "the {} symbol {} has ranges, but is not int or hex" > + .format(TYPE_TO_STR[sym.orig_type], > + sym.name_and_loc)) > + else: > + for low, high, _ in sym.ranges: > + if not num_ok(low, sym.orig_type) or \ > + not num_ok(high, sym.orig_type): > + > + self._warn("the {0} symbol {1} has a non-{0} " > + "range [{2}, {3}]" > + .format(TYPE_TO_STR[sym.orig_type], > + sym.name_and_loc, > + low.name_and_loc, > + high.name_and_loc)) > + > + def _check_choice_sanity(self): > + # Checks various choice properties that are handiest to check after > + # parsing. Only generates errors and warnings. > + > + def warn_select_imply(sym, expr, expr_type): > + msg = "the choice symbol {} is {} by the following symbols, but " \ > + "select/imply has no effect on choice symbols" \ > + .format(sym.name_and_loc, expr_type) > + > + # si = select/imply > + for si in split_expr(expr, OR): > + msg += "\n - " + split_expr(si, AND)[0].name_and_loc > + > + self._warn(msg) > + > + for choice in self.unique_choices: > + if choice.orig_type not in _BOOL_TRISTATE: > + self._warn("{} defined with type {}" > + .format(choice.name_and_loc, > + TYPE_TO_STR[choice.orig_type])) > + > + for node in choice.nodes: > + if node.prompt: > + break > + else: > + self._warn(choice.name_and_loc + " defined without a prompt") > + > + for default, _ in choice.defaults: > + if default.__class__ is not Symbol: > + raise KconfigError( > + "{} has a malformed default {}" > + .format(choice.name_and_loc, expr_str(default))) > + > + if default.choice is not choice: > + self._warn("the default selection {} of {} is not " > + "contained in the choice" > + .format(default.name_and_loc, > + choice.name_and_loc)) > + > + for sym in choice.syms: > + if sym.defaults: > + self._warn("default on the choice symbol {} will have " > + "no effect, as defaults do not affect choice " > + "symbols".format(sym.name_and_loc)) > + > + if sym.rev_dep is not sym.kconfig.n: > + warn_select_imply(sym, sym.rev_dep, "selected") > + > + if sym.weak_rev_dep is not sym.kconfig.n: > + warn_select_imply(sym, sym.weak_rev_dep, "implied") > + > + for node in sym.nodes: > + if node.parent.item is choice: > + if not node.prompt: > + self._warn("the choice symbol {} has no prompt" > + .format(sym.name_and_loc)) > + > + elif node.prompt: > + self._warn("the choice symbol {} is defined with a " > + "prompt outside the choice" > + .format(sym.name_and_loc)) > + > + def _parse_error(self, msg): > + raise KconfigError("{}error: couldn't parse '{}': {}".format( > + "" if self.filename is None else > + "{}:{}: ".format(self.filename, self.linenr), > + self._line.strip(), msg)) > + > + def _trailing_tokens_error(self): > + self._parse_error("extra tokens at end of line") > + > + def _open(self, filename, mode): > + # open() wrapper: > + # > + # - Enable universal newlines mode on Python 2 to ease > + # interoperability between Linux and Windows. It's already the > + # default on Python 3. > + # > + # The "U" flag would currently work for both Python 2 and 3, but it's > + # deprecated on Python 3, so play it future-safe. > + # > + # io.open() defaults to universal newlines on Python 2 (and is an > + # alias for open() on Python 3), but it returns 'unicode' strings and > + # slows things down: > + # > + # Parsing x86 Kconfigs on Python 2 > + # > + # with open(..., "rU"): > + # > + # real 0m0.930s > + # user 0m0.905s > + # sys 0m0.025s > + # > + # with io.open(): > + # > + # real 0m1.069s > + # user 0m1.040s > + # sys 0m0.029s > + # > + # There's no appreciable performance difference between "r" and > + # "rU" for parsing performance on Python 2. > + # > + # - For Python 3, force the encoding. Forcing the encoding on Python 2 > + # turns strings into Unicode strings, which gets messy. Python 2 > + # doesn't decode regular strings anyway. > + return open(filename, "rU" if mode == "r" else mode) if _IS_PY2 else \ > + open(filename, mode, encoding=self._encoding) > + > + def _check_undef_syms(self): > + # Prints warnings for all references to undefined symbols within the > + # Kconfig files > + > + def is_num(s): > + # Returns True if the string 's' looks like a number. > + # > + # Internally, all operands in Kconfig are symbols, only undefined symbols > + # (which numbers usually are) get their name as their value. > + # > + # Only hex numbers that start with 0x/0X are classified as numbers. > + # Otherwise, symbols whose names happen to contain only the letters A-F > + # would trigger false positives. > + > + try: > + int(s) > + except ValueError: > + if not s.startswith(("0x", "0X")): > + return False > + > + try: > + int(s, 16) > + except ValueError: > + return False > + > + return True > + > + for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)(): > + # - sym.nodes empty means the symbol is undefined (has no > + # definition locations) > + # > + # - Due to Kconfig internals, numbers show up as undefined Kconfig > + # symbols, but shouldn't be flagged > + # > + # - The MODULES symbol always exists > + if not sym.nodes and not is_num(sym.name) and \ > + sym.name != "MODULES": > + > + msg = "undefined symbol {}:".format(sym.name) > + for node in self.node_iter(): > + if sym in node.referenced: > + msg += "\n\n- Referenced at {}:{}:\n\n{}" \ > + .format(node.filename, node.linenr, node) > + self._warn(msg) > + > + def _warn(self, msg, filename=None, linenr=None): > + # For printing general warnings > + > + if not self.warn: > + return > + > + msg = "warning: " + msg > + if filename is not None: > + msg = "{}:{}: {}".format(filename, linenr, msg) > + > + self.warnings.append(msg) > + if self.warn_to_stderr: > + sys.stderr.write(msg + "\n") > + > + > +class Symbol(object): > + """ > + Represents a configuration symbol: > + > + (menu)config FOO > + ... > + > + The following attributes are available. They should be viewed as read-only, > + and some are implemented through @property magic (but are still efficient > + to access due to internal caching). > + > + Note: Prompts, help texts, and locations are stored in the Symbol's > + MenuNode(s) rather than in the Symbol itself. Check the MenuNode class and > + the Symbol.nodes attribute. This organization matches the C tools. > + > + name: > + The name of the symbol, e.g. "FOO" for 'config FOO'. > + > + type: > + The type of the symbol. One of BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN. > + UNKNOWN is for undefined symbols, (non-special) constant symbols, and > + symbols defined without a type. > + > + When running without modules (MODULES having the value n), TRISTATE > + symbols magically change type to BOOL. This also happens for symbols > + within choices in "y" mode. This matches the C tools, and makes sense for > + menuconfig-like functionality. > + > + orig_type: > + The type as given in the Kconfig file, without any magic applied. Used > + when printing the symbol. > + > + tri_value: > + The tristate value of the symbol as an integer. One of 0, 1, 2, > + representing n, m, y. Always 0 (n) for non-bool/tristate symbols. > + > + This is the symbol value that's used outside of relation expressions > + (A, !A, A && B, A || B). > + > + str_value: > + The value of the symbol as a string. Gives the value for string/int/hex > + symbols. For bool/tristate symbols, gives "n", "m", or "y". > + > + This is the symbol value that's used in relational expressions > + (A = B, A != B, etc.) > + > + Gotcha: For int/hex symbols, the exact format of the value is often > + preserved (e.g. when writing a .config file), hence why you can't get it > + directly as an int. Do int(int_sym.str_value) or > + int(hex_sym.str_value, 16) to get the integer value. > + > + user_value: > + The user value of the symbol. None if no user value has been assigned > + (via Kconfig.load_config() or Symbol.set_value()). > + > + Holds 0, 1, or 2 for bool/tristate symbols, and a string for the other > + symbol types. > + > + WARNING: Do not assign directly to this. It will break things. Use > + Symbol.set_value(). > + > + assignable: > + A tuple containing the tristate user values that can currently be > + assigned to the symbol (that would be respected), ordered from lowest (0, > + representing n) to highest (2, representing y). This corresponds to the > + selections available in the menuconfig interface. The set of assignable > + values is calculated from the symbol's visibility and selects/implies. > + > + Returns the empty set for non-bool/tristate symbols and for symbols with > + visibility n. The other possible values are (0, 2), (0, 1, 2), (1, 2), > + (1,), and (2,). A (1,) or (2,) result means the symbol is visible but > + "locked" to m or y through a select, perhaps in combination with the > + visibility. menuconfig represents this as -M- and -*-, respectively. > + > + For string/hex/int symbols, check if Symbol.visibility is non-0 (non-n) > + instead to determine if the value can be changed. > + > + Some handy 'assignable' idioms: > + > + # Is 'sym' an assignable (visible) bool/tristate symbol? > + if sym.assignable: > + # What's the highest value it can be assigned? [-1] in Python > + # gives the last element. > + sym_high = sym.assignable[-1] > + > + # The lowest? > + sym_low = sym.assignable[0] > + > + # Can the symbol be set to at least m? > + if sym.assignable[-1] >= 1: > + ... > + > + # Can the symbol be set to m? > + if 1 in sym.assignable: > + ... > + > + visibility: > + The visibility of the symbol. One of 0, 1, 2, representing n, m, y. See > + the module documentation for an overview of symbol values and visibility. > + > + config_string: > + The .config assignment string that would get written out for the symbol > + by Kconfig.write_config(). Returns the empty string if no .config > + assignment would get written out. > + > + In general, visible symbols, symbols with (active) defaults, and selected > + symbols get written out. This includes all non-n-valued bool/tristate > + symbols, and all visible string/int/hex symbols. > + > + Symbols with the (no longer needed) 'option env=...' option generate no > + configuration output, and neither does the special > + 'option defconfig_list' symbol. > + > + Tip: This field is useful when generating custom configuration output, > + even for non-.config-like formats. To write just the symbols that would > + get written out to .config files, do this: > + > + if sym.config_string: > + *Write symbol, e.g. by looking sym.str_value* > + > + This is a superset of the symbols written out by write_autoconf(). > + That function skips all n-valued symbols. > + > + There usually won't be any great harm in just writing all symbols either, > + though you might get some special symbols and possibly some "redundant" > + n-valued symbol entries in there. > + > + name_and_loc: > + Holds a string like > + > + "MY_SYMBOL (defined at foo/Kconfig:12, bar/Kconfig:14)" > + > + , giving the name of the symbol and its definition location(s). > + > + If the symbol is undefined, the location is given as "(undefined)". > + > + nodes: > + A list of MenuNodes for this symbol. Will contain a single MenuNode for > + most symbols. Undefined and constant symbols have an empty nodes list. > + Symbols defined in multiple locations get one node for each location. > + > + choice: > + Holds the parent Choice for choice symbols, and None for non-choice > + symbols. Doubles as a flag for whether a symbol is a choice symbol. > + > + defaults: > + List of (default, cond) tuples for the symbol's 'default' properties. For > + example, 'default A && B if C || D' is represented as > + ((AND, A, B), (OR, C, D)). If no condition was given, 'cond' is > + self.kconfig.y. > + > + Note that 'depends on' and parent dependencies are propagated to > + 'default' conditions. > + > + selects: > + List of (symbol, cond) tuples for the symbol's 'select' properties. For > + example, 'select A if B && C' is represented as (A, (AND, B, C)). If no > + condition was given, 'cond' is self.kconfig.y. > + > + Note that 'depends on' and parent dependencies are propagated to 'select' > + conditions. > + > + implies: > + Like 'selects', for imply. > + > + ranges: > + List of (low, high, cond) tuples for the symbol's 'range' properties. For > + example, 'range 1 2 if A' is represented as (1, 2, A). If there is no > + condition, 'cond' is self.kconfig.y. > + > + Note that 'depends on' and parent dependencies are propagated to 'range' > + conditions. > + > + Gotcha: 1 and 2 above will be represented as (undefined) Symbols rather > + than plain integers. Undefined symbols get their name as their string > + value, so this works out. The C tools work the same way. > + > + orig_defaults: > + orig_selects: > + orig_implies: > + orig_ranges: > + See the corresponding attributes on the MenuNode class. > + > + rev_dep: > + Reverse dependency expression from other symbols selecting this symbol. > + Multiple selections get ORed together. A condition on a select is ANDed > + with the selecting symbol. > + > + For example, if A has 'select FOO' and B has 'select FOO if C', then > + FOO's rev_dep will be (OR, A, (AND, B, C)). > + > + weak_rev_dep: > + Like rev_dep, for imply. > + > + direct_dep: > + The direct ('depends on') dependencies for the symbol, or self.kconfig.y > + if there are no direct dependencies. > + > + This attribute includes any dependencies from surrounding menus and ifs. > + Those get propagated to the direct dependencies, and the resulting direct > + dependencies in turn get propagated to the conditions of all properties. > + > + If the symbol is defined in multiple locations, the dependencies from the > + different locations get ORed together. > + > + referenced: > + A set() with all symbols and choices referenced in the properties and > + property conditions of the symbol. > + > + Also includes dependencies from surrounding menus and ifs, because those > + get propagated to the symbol (see the 'Intro to symbol values' section in > + the module docstring). > + > + Choices appear in the dependencies of choice symbols. > + > + For the following definitions, only B and not C appears in A's > + 'referenced'. To get transitive references, you'll have to recursively > + expand 'references' until no new items appear. > + > + config A > + bool > + depends on B > + > + config B > + bool > + depends on C > + > + config C > + bool > + > + See the Symbol.direct_dep attribute if you're only interested in the > + direct dependencies of the symbol (its 'depends on'). You can extract the > + symbols in it with the global expr_items() function. > + > + env_var: > + If the Symbol has an 'option env="FOO"' option, this contains the name > + ("FOO") of the environment variable. None for symbols without no > + 'option env'. > + > + 'option env="FOO"' acts like a 'default' property whose value is the > + value of $FOO. > + > + Symbols with 'option env' are never written out to .config files, even if > + they are visible. env_var corresponds to a flag called SYMBOL_AUTO in the > + C implementation. > + > + is_allnoconfig_y: > + True if the symbol has 'option allnoconfig_y' set on it. This has no > + effect internally (except when printing symbols), but can be checked by > + scripts. > + > + is_constant: > + True if the symbol is a constant (quoted) symbol. > + > + kconfig: > + The Kconfig instance this symbol is from. > + """ > + __slots__ = ( > + "_cached_assignable", > + "_cached_str_val", > + "_cached_tri_val", > + "_cached_vis", > + "_dependents", > + "_old_val", > + "_visited", > + "_was_set", > + "_write_to_conf", > + "choice", > + "defaults", > + "direct_dep", > + "env_var", > + "implies", > + "is_allnoconfig_y", > + "is_constant", > + "kconfig", > + "name", > + "nodes", > + "orig_type", > + "ranges", > + "rev_dep", > + "selects", > + "user_value", > + "weak_rev_dep", > + ) > + > + # > + # Public interface > + # > + > + @property > + def type(self): > + """ > + See the class documentation. > + """ > + if self.orig_type is TRISTATE and \ > + (self.choice and self.choice.tri_value == 2 or > + not self.kconfig.modules.tri_value): > + > + return BOOL > + > + return self.orig_type > + > + @property > + def str_value(self): > + """ > + See the class documentation. > + """ > + if self._cached_str_val is not None: > + return self._cached_str_val > + > + if self.orig_type in _BOOL_TRISTATE: > + # Also calculates the visibility, so invalidation safe > + self._cached_str_val = TRI_TO_STR[self.tri_value] > + return self._cached_str_val > + > + # As a quirk of Kconfig, undefined symbols get their name as their > + # string value. This is why things like "FOO = bar" work for seeing if > + # FOO has the value "bar". > + if not self.orig_type: # UNKNOWN > + self._cached_str_val = self.name > + return self.name > + > + val = "" > + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden > + # function call (property magic) > + vis = self.visibility > + > + self._write_to_conf = (vis != 0) > + > + if self.orig_type in _INT_HEX: > + # The C implementation checks the user value against the range in a > + # separate code path (post-processing after loading a .config). > + # Checking all values here instead makes more sense for us. It > + # requires that we check for a range first. > + > + base = _TYPE_TO_BASE[self.orig_type] > + > + # Check if a range is in effect > + for low_expr, high_expr, cond in self.ranges: > + if expr_value(cond): > + has_active_range = True > + > + # The zeros are from the C implementation running strtoll() > + # on empty strings > + low = int(low_expr.str_value, base) if \ > + _is_base_n(low_expr.str_value, base) else 0 > + high = int(high_expr.str_value, base) if \ > + _is_base_n(high_expr.str_value, base) else 0 > + > + break > + else: > + has_active_range = False > + > + # Defaults are used if the symbol is invisible, lacks a user value, > + # or has an out-of-range user value > + use_defaults = True > + > + if vis and self.user_value: > + user_val = int(self.user_value, base) > + if has_active_range and not low <= user_val <= high: > + num2str = str if base == 10 else hex > + self.kconfig._warn( > + "user value {} on the {} symbol {} ignored due to " > + "being outside the active range ([{}, {}]) -- falling " > + "back on defaults" > + .format(num2str(user_val), TYPE_TO_STR[self.orig_type], > + self.name_and_loc, > + num2str(low), num2str(high))) > + else: > + # If the user value is well-formed and satisfies range > + # contraints, it is stored in exactly the same form as > + # specified in the assignment (with or without "0x", etc.) > + val = self.user_value > + use_defaults = False > + > + if use_defaults: > + # No user value or invalid user value. Look at defaults. > + > + # Used to implement the warning below > + has_default = False > + > + for sym, cond in self.defaults: > + if expr_value(cond): > + has_default = self._write_to_conf = True > + > + val = sym.str_value > + > + if _is_base_n(val, base): > + val_num = int(val, base) > + else: > + val_num = 0 # strtoll() on empty string > + > + break > + else: > + val_num = 0 # strtoll() on empty string > + > + # This clamping procedure runs even if there's no default > + if has_active_range: > + clamp = None > + if val_num < low: > + clamp = low > + elif val_num > high: > + clamp = high > + > + if clamp is not None: > + # The value is rewritten to a standard form if it is > + # clamped > + val = str(clamp) \ > + if self.orig_type is INT else \ > + hex(clamp) > + > + if has_default: > + num2str = str if base == 10 else hex > + self.kconfig._warn( > + "default value {} on {} clamped to {} due to " > + "being outside the active range ([{}, {}])" > + .format(val_num, self.name_and_loc, > + num2str(clamp), num2str(low), > + num2str(high))) > + > + elif self.orig_type is STRING: > + if vis and self.user_value is not None: > + # If the symbol is visible and has a user value, use that > + val = self.user_value > + else: > + # Otherwise, look at defaults > + for sym, cond in self.defaults: > + if expr_value(cond): > + val = sym.str_value > + self._write_to_conf = True > + break > + > + # env_var corresponds to SYMBOL_AUTO in the C implementation, and is > + # also set on the defconfig_list symbol there. Test for the > + # defconfig_list symbol explicitly instead here, to avoid a nonsensical > + # env_var setting and the defconfig_list symbol being printed > + # incorrectly. This code is pretty cold anyway. > + if self.env_var is not None or self is self.kconfig.defconfig_list: > + self._write_to_conf = False > + > + self._cached_str_val = val > + return val > + > + @property > + def tri_value(self): > + """ > + See the class documentation. > + """ > + if self._cached_tri_val is not None: > + return self._cached_tri_val > + > + if self.orig_type not in _BOOL_TRISTATE: > + if self.orig_type: # != UNKNOWN > + # Would take some work to give the location here > + self.kconfig._warn( > + "The {} symbol {} is being evaluated in a logical context " > + "somewhere. It will always evaluate to n." > + .format(TYPE_TO_STR[self.orig_type], self.name_and_loc)) > + > + self._cached_tri_val = 0 > + return 0 > + > + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden > + # function call (property magic) > + vis = self.visibility > + self._write_to_conf = (vis != 0) > + > + val = 0 > + > + if not self.choice: > + # Non-choice symbol > + > + if vis and self.user_value is not None: > + # If the symbol is visible and has a user value, use that > + val = min(self.user_value, vis) > + > + else: > + # Otherwise, look at defaults and weak reverse dependencies > + # (implies) > + > + for default, cond in self.defaults: > + dep_val = expr_value(cond) > + if dep_val: > + val = min(expr_value(default), dep_val) > + if val: > + self._write_to_conf = True > + break > + > + # Weak reverse dependencies are only considered if our > + # direct dependencies are met > + dep_val = expr_value(self.weak_rev_dep) > + if dep_val and expr_value(self.direct_dep): > + val = max(dep_val, val) > + self._write_to_conf = True > + > + # Reverse (select-related) dependencies take precedence > + dep_val = expr_value(self.rev_dep) > + if dep_val: > + if expr_value(self.direct_dep) < dep_val: > + self._warn_select_unsatisfied_deps() > + > + val = max(dep_val, val) > + self._write_to_conf = True > + > + # m is promoted to y for (1) bool symbols and (2) symbols with a > + # weak_rev_dep (from imply) of y > + if val == 1 and \ > + (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): > + val = 2 > + > + elif vis == 2: > + # Visible choice symbol in y-mode choice. The choice mode limits > + # the visibility of choice symbols, so it's sufficient to just > + # check the visibility of the choice symbols themselves. > + val = 2 if self.choice.selection is self else 0 > + > + elif vis and self.user_value: > + # Visible choice symbol in m-mode choice, with set non-0 user value > + val = 1 > + > + self._cached_tri_val = val > + return val > + > + @property > + def assignable(self): > + """ > + See the class documentation. > + """ > + if self._cached_assignable is None: > + self._cached_assignable = self._assignable() > + return self._cached_assignable > + > + @property > + def visibility(self): > + """ > + See the class documentation. > + """ > + if self._cached_vis is None: > + self._cached_vis = _visibility(self) > + return self._cached_vis > + > + @property > + def config_string(self): > + """ > + See the class documentation. > + """ > + # _write_to_conf is determined when the value is calculated. This is a > + # hidden function call due to property magic. > + val = self.str_value > + if not self._write_to_conf: > + return "" > + > + if self.orig_type in _BOOL_TRISTATE: > + return "{}{}={}\n" \ > + .format(self.kconfig.config_prefix, self.name, val) \ > + if val != "n" else \ > + "# {}{} is not set\n" \ > + .format(self.kconfig.config_prefix, self.name) > + > + if self.orig_type in _INT_HEX: > + return "{}{}={}\n" \ > + .format(self.kconfig.config_prefix, self.name, val) > + > + # sym.orig_type is STRING > + return '{}{}="{}"\n' \ > + .format(self.kconfig.config_prefix, self.name, escape(val)) > + > + @property > + def name_and_loc(self): > + """ > + See the class documentation. > + """ > + return self.name + " " + _locs(self) > + > + def set_value(self, value): > + """ > + Sets the user value of the symbol. > + > + Equal in effect to assigning the value to the symbol within a .config > + file. For bool and tristate symbols, use the 'assignable' attribute to > + check which values can currently be assigned. Setting values outside > + 'assignable' will cause Symbol.user_value to differ from > + Symbol.str/tri_value (be truncated down or up). > + > + Setting a choice symbol to 2 (y) sets Choice.user_selection to the > + choice symbol in addition to setting Symbol.user_value. > + Choice.user_selection is considered when the choice is in y mode (the > + "normal" mode). > + > + Other symbols that depend (possibly indirectly) on this symbol are > + automatically recalculated to reflect the assigned value. > + > + value: > + The user value to give to the symbol. For bool and tristate symbols, > + n/m/y can be specified either as 0/1/2 (the usual format for tristate > + values in Kconfiglib) or as one of the strings "n", "m", or "y". For > + other symbol types, pass a string. > + > + Note that the value for an int/hex symbol is passed as a string, e.g. > + "123" or "0x0123". The format of this string is preserved in the > + output. > + > + Values that are invalid for the type (such as "foo" or 1 (m) for a > + BOOL or "0x123" for an INT) are ignored and won't be stored in > + Symbol.user_value. Kconfiglib will print a warning by default for > + invalid assignments, and set_value() will return False. > + > + Returns True if the value is valid for the type of the symbol, and > + False otherwise. This only looks at the form of the value. For BOOL and > + TRISTATE symbols, check the Symbol.assignable attribute to see what > + values are currently in range and would actually be reflected in the > + value of the symbol. For other symbol types, check whether the > + visibility is non-n. > + """ > + if self.orig_type in _BOOL_TRISTATE and value in STR_TO_TRI: > + value = STR_TO_TRI[value] > + > + # If the new user value matches the old, nothing changes, and we can > + # avoid invalidating cached values. > + # > + # This optimization is skipped for choice symbols: Setting a choice > + # symbol's user value to y might change the state of the choice, so it > + # wouldn't be safe (symbol user values always match the values set in a > + # .config file or via set_value(), and are never implicitly updated). > + if value == self.user_value and not self.choice: > + self._was_set = True > + return True > + > + # Check if the value is valid for our type > + if not (self.orig_type is BOOL and value in (2, 0) or > + self.orig_type is TRISTATE and value in TRI_TO_STR or > + value.__class__ is str and > + (self.orig_type is STRING or > + self.orig_type is INT and _is_base_n(value, 10) or > + self.orig_type is HEX and _is_base_n(value, 16) > + and int(value, 16) >= 0)): > + > + # Display tristate values as n, m, y in the warning > + self.kconfig._warn( > + "the value {} is invalid for {}, which has type {} -- " > + "assignment ignored" > + .format(TRI_TO_STR[value] if value in TRI_TO_STR else > + "'{}'".format(value), > + self.name_and_loc, TYPE_TO_STR[self.orig_type])) > + > + return False > + > + self.user_value = value > + self._was_set = True > + > + if self.choice and value == 2: > + # Setting a choice symbol to y makes it the user selection of the > + # choice. Like for symbol user values, the user selection is not > + # guaranteed to match the actual selection of the choice, as > + # dependencies come into play. > + self.choice.user_selection = self > + self.choice._was_set = True > + self.choice._rec_invalidate() > + else: > + self._rec_invalidate_if_has_prompt() > + > + return True > + > + def unset_value(self): > + """ > + Removes any user value from the symbol, as if the symbol had never > + gotten a user value via Kconfig.load_config() or Symbol.set_value(). > + """ > + if self.user_value is not None: > + self.user_value = None > + self._rec_invalidate_if_has_prompt() > + > + @property > + def referenced(self): > + """ > + See the class documentation. > + """ > + return {item for node in self.nodes for item in node.referenced} > + > + @property > + def orig_defaults(self): > + """ > + See the class documentation. > + """ > + return [d for node in self.nodes for d in node.orig_defaults] > + > + @property > + def orig_selects(self): > + """ > + See the class documentation. > + """ > + return [s for node in self.nodes for s in node.orig_selects] > + > + @property > + def orig_implies(self): > + """ > + See the class documentation. > + """ > + return [i for node in self.nodes for i in node.orig_implies] > + > + @property > + def orig_ranges(self): > + """ > + See the class documentation. > + """ > + return [r for node in self.nodes for r in node.orig_ranges] > + > + def __repr__(self): > + """ > + Returns a string with information about the symbol (including its name, > + value, visibility, and location(s)) when it is evaluated on e.g. the > + interactive Python prompt. > + """ > + fields = ["symbol " + self.name, TYPE_TO_STR[self.type]] > + add = fields.append > + > + for node in self.nodes: > + if node.prompt: > + add('"{}"'.format(node.prompt[0])) > + > + # Only add quotes for non-bool/tristate symbols > + add("value " + (self.str_value if self.orig_type in _BOOL_TRISTATE > + else '"{}"'.format(self.str_value))) > + > + if not self.is_constant: > + # These aren't helpful to show for constant symbols > + > + if self.user_value is not None: > + # Only add quotes for non-bool/tristate symbols > + add("user value " + (TRI_TO_STR[self.user_value] > + if self.orig_type in _BOOL_TRISTATE > + else '"{}"'.format(self.user_value))) > + > + add("visibility " + TRI_TO_STR[self.visibility]) > + > + if self.choice: > + add("choice symbol") > + > + if self.is_allnoconfig_y: > + add("allnoconfig_y") > + > + if self is self.kconfig.defconfig_list: > + add("is the defconfig_list symbol") > + > + if self.env_var is not None: > + add("from environment variable " + self.env_var) > + > + if self is self.kconfig.modules: > + add("is the modules symbol") > + > + add("direct deps " + TRI_TO_STR[expr_value(self.direct_dep)]) > + > + if self.nodes: > + for node in self.nodes: > + add("{}:{}".format(node.filename, node.linenr)) > + else: > + add("constant" if self.is_constant else "undefined") > + > + return "<{}>".format(", ".join(fields)) > + > + def __str__(self): > + """ > + Returns a string representation of the symbol when it is printed. > + Matches the Kconfig format, with any parent dependencies propagated to > + the 'depends on' condition. > + > + The string is constructed by joining the strings returned by > + MenuNode.__str__() for each of the symbol's menu nodes, so symbols > + defined in multiple locations will return a string with all > + definitions. > + > + The returned string does not end in a newline. An empty string is > + returned for undefined and constant symbols. > + """ > + return self.custom_str(standard_sc_expr_str) > + > + def custom_str(self, sc_expr_str_fn): > + """ > + Works like Symbol.__str__(), but allows a custom format to be used for > + all symbol/choice references. See expr_str(). > + """ > + return "\n\n".join(node.custom_str(sc_expr_str_fn) > + for node in self.nodes) > + > + # > + # Private methods > + # > + > + def __init__(self): > + """ > + Symbol constructor -- not intended to be called directly by Kconfiglib > + clients. > + """ > + # These attributes are always set on the instance from outside and > + # don't need defaults: > + # kconfig > + # direct_dep > + # is_constant > + # name > + # rev_dep > + # weak_rev_dep > + > + # - UNKNOWN == 0 > + # - _visited is used during tree iteration and dep. loop detection > + self.orig_type = self._visited = 0 > + > + self.nodes = [] > + > + self.defaults = [] > + self.selects = [] > + self.implies = [] > + self.ranges = [] > + > + self.user_value = \ > + self.choice = \ > + self.env_var = \ > + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ > + self._cached_assignable = None > + > + # _write_to_conf is calculated along with the value. If True, the > + # Symbol gets a .config entry. > + > + self.is_allnoconfig_y = \ > + self._was_set = \ > + self._write_to_conf = False > + > + # See Kconfig._build_dep() > + self._dependents = set() > + > + def _assignable(self): > + # Worker function for the 'assignable' attribute > + > + if self.orig_type not in _BOOL_TRISTATE: > + return () > + > + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden > + # function call (property magic) > + vis = self.visibility > + if not vis: > + return () > + > + rev_dep_val = expr_value(self.rev_dep) > + > + if vis == 2: > + if self.choice: > + return (2,) > + > + if not rev_dep_val: > + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: > + return (0, 2) > + return (0, 1, 2) > + > + if rev_dep_val == 2: > + return (2,) > + > + # rev_dep_val == 1 > + > + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: > + return (2,) > + return (1, 2) > + > + # vis == 1 > + > + # Must be a tristate here, because bool m visibility gets promoted to y > + > + if not rev_dep_val: > + return (0, 1) if expr_value(self.weak_rev_dep) != 2 else (0, 2) > + > + if rev_dep_val == 2: > + return (2,) > + > + # vis == rev_dep_val == 1 > + > + return (1,) > + > + def _invalidate(self): > + # Marks the symbol as needing to be recalculated > + > + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ > + self._cached_assignable = None > + > + def _rec_invalidate(self): > + # Invalidates the symbol and all items that (possibly) depend on it > + > + if self is self.kconfig.modules: > + # Invalidating MODULES has wide-ranging effects > + self.kconfig._invalidate_all() > + else: > + self._invalidate() > + > + for item in self._dependents: > + # _cached_vis doubles as a flag that tells us whether 'item' > + # has cached values, because it's calculated as a side effect > + # of calculating all other (non-constant) cached values. > + # > + # If item._cached_vis is None, it means there can't be cached > + # values on other items that depend on 'item', because if there > + # were, some value on 'item' would have been calculated and > + # item._cached_vis set as a side effect. It's therefore safe to > + # stop the invalidation at symbols with _cached_vis None. > + # > + # This approach massively speeds up scripts that set a lot of > + # values, vs simply invalidating all possibly dependent symbols > + # (even when you already have a list of all the dependent > + # symbols, because some symbols get huge dependency trees). > + # > + # This gracefully handles dependency loops too, which is nice > + # for choices, where the choice depends on the choice symbols > + # and vice versa. > + if item._cached_vis is not None: > + item._rec_invalidate() > + > + def _rec_invalidate_if_has_prompt(self): > + # Invalidates the symbol and its dependent symbols, but only if the > + # symbol has a prompt. User values never have an effect on promptless > + # symbols, so we skip invalidation for them as an optimization. > + # > + # This also prevents constant (quoted) symbols from being invalidated > + # if set_value() is called on them, which would make them lose their > + # value and break things. > + # > + # Prints a warning if the symbol has no prompt. In some contexts (e.g. > + # when loading a .config files) assignments to promptless symbols are > + # normal and expected, so the warning can be disabled. > + > + for node in self.nodes: > + if node.prompt: > + self._rec_invalidate() > + return > + > + if self.kconfig._warn_assign_no_prompt: > + self.kconfig._warn(self.name_and_loc + " has no prompt, meaning " > + "user values have no effect on it") > + > + def _str_default(self): > + # write_min_config() helper function. Returns the value the symbol > + # would get from defaults if it didn't have a user value. Uses exactly > + # the same algorithm as the C implementation (though a bit cleaned up), > + # for compatibility. > + > + if self.orig_type in _BOOL_TRISTATE: > + val = 0 > + > + # Defaults, selects, and implies do not affect choice symbols > + if not self.choice: > + for default, cond in self.defaults: > + cond_val = expr_value(cond) > + if cond_val: > + val = min(expr_value(default), cond_val) > + break > + > + val = max(expr_value(self.rev_dep), > + expr_value(self.weak_rev_dep), > + val) > + > + # Transpose mod to yes if type is bool (possibly due to modules > + # being disabled) > + if val == 1 and self.type is BOOL: > + val = 2 > + > + return TRI_TO_STR[val] > + > + if self.orig_type: # STRING/INT/HEX > + for default, cond in self.defaults: > + if expr_value(cond): > + return default.str_value > + > + return "" > + > + def _warn_select_unsatisfied_deps(self): > + # Helper for printing an informative warning when a symbol with > + # unsatisfied direct dependencies (dependencies from 'depends on', ifs, > + # and menus) is selected by some other symbol. Also warn if a symbol > + # whose direct dependencies evaluate to m is selected to y. > + > + msg = "{} has direct dependencies {} with value {}, but is " \ > + "currently being {}-selected by the following symbols:" \ > + .format(self.name_and_loc, expr_str(self.direct_dep), > + TRI_TO_STR[expr_value(self.direct_dep)], > + TRI_TO_STR[expr_value(self.rev_dep)]) > + > + # The reverse dependencies from each select are ORed together > + for select in split_expr(self.rev_dep, OR): > + if expr_value(select) <= expr_value(self.direct_dep): > + # Only include selects that exceed the direct dependencies > + continue > + > + # - 'select A if B' turns into A && B > + # - 'select A' just turns into A > + # > + # In both cases, we can split on AND and pick the first operand > + selecting_sym = split_expr(select, AND)[0] > + > + msg += "\n - {}, with value {}, direct dependencies {} " \ > + "(value: {})" \ > + .format(selecting_sym.name_and_loc, > + selecting_sym.str_value, > + expr_str(selecting_sym.direct_dep), > + TRI_TO_STR[expr_value(selecting_sym.direct_dep)]) > + > + if select.__class__ is tuple: > + msg += ", and select condition {} (value: {})" \ > + .format(expr_str(select[2]), > + TRI_TO_STR[expr_value(select[2])]) > + > + self.kconfig._warn(msg) > + > + > +class Choice(object): > + """ > + Represents a choice statement: > + > + choice > + ... > + endchoice > + > + The following attributes are available on Choice instances. They should be > + treated as read-only, and some are implemented through @property magic (but > + are still efficient to access due to internal caching). > + > + Note: Prompts, help texts, and locations are stored in the Choice's > + MenuNode(s) rather than in the Choice itself. Check the MenuNode class and > + the Choice.nodes attribute. This organization matches the C tools. > + > + name: > + The name of the choice, e.g. "FOO" for 'choice FOO', or None if the > + Choice has no name. > + > + type: > + The type of the choice. One of BOOL, TRISTATE, UNKNOWN. UNKNOWN is for > + choices defined without a type where none of the contained symbols have a > + type either (otherwise the choice inherits the type of the first symbol > + defined with a type). > + > + When running without modules (CONFIG_MODULES=n), TRISTATE choices > + magically change type to BOOL. This matches the C tools, and makes sense > + for menuconfig-like functionality. > + > + orig_type: > + The type as given in the Kconfig file, without any magic applied. Used > + when printing the choice. > + > + tri_value: > + The tristate value (mode) of the choice. A choice can be in one of three > + modes: > + > + 0 (n) - The choice is disabled and no symbols can be selected. For > + visible choices, this mode is only possible for choices with > + the 'optional' flag set (see kconfig-language.txt). > + > + 1 (m) - Any number of choice symbols can be set to m, the rest will > + be n. > + > + 2 (y) - One symbol will be y, the rest n. > + > + Only tristate choices can be in m mode. The visibility of the choice is > + an upper bound on the mode, and the mode in turn is an upper bound on the > + visibility of the choice symbols. > + > + To change the mode, use Choice.set_value(). > + > + Implementation note: > + The C tools internally represent choices as a type of symbol, with > + special-casing in many code paths. This is why there is a lot of > + similarity to Symbol. The value (mode) of a choice is really just a > + normal symbol value, and an implicit reverse dependency forces its > + lower bound to m for visible non-optional choices (the reverse > + dependency is 'm && <visibility>'). > + > + Symbols within choices get the choice propagated as a dependency to > + their properties. This turns the mode of the choice into an upper bound > + on e.g. the visibility of choice symbols, and explains the gotcha > + related to printing choice symbols mentioned in the module docstring. > + > + Kconfiglib uses a separate Choice class only because it makes the code > + and interface less confusing (especially in a user-facing interface). > + Corresponding attributes have the same name in the Symbol and Choice > + classes, for consistency and compatibility. > + > + str_value: > + Like choice.tri_value, but gives the value as one of the strings > + "n", "m", or "y" > + > + user_value: > + The value (mode) selected by the user through Choice.set_value(). Either > + 0, 1, or 2, or None if the user hasn't selected a mode. See > + Symbol.user_value. > + > + WARNING: Do not assign directly to this. It will break things. Use > + Choice.set_value() instead. > + > + assignable: > + See the symbol class documentation. Gives the assignable values (modes). > + > + selection: > + The Symbol instance of the currently selected symbol. None if the Choice > + is not in y mode or has no selected symbol (due to unsatisfied > + dependencies on choice symbols). > + > + WARNING: Do not assign directly to this. It will break things. Call > + sym.set_value(2) on the choice symbol you want to select instead. > + > + user_selection: > + The symbol selected by the user (by setting it to y). Ignored if the > + choice is not in y mode, but still remembered so that the choice "snaps > + back" to the user selection if the mode is changed back to y. This might > + differ from 'selection' due to unsatisfied dependencies. > + > + WARNING: Do not assign directly to this. It will break things. Call > + sym.set_value(2) on the choice symbol to be selected instead. > + > + visibility: > + See the Symbol class documentation. Acts on the value (mode). > + > + name_and_loc: > + Holds a string like > + > + "<choice MY_CHOICE> (defined at foo/Kconfig:12)" > + > + , giving the name of the choice and its definition location(s). If the > + choice has no name (isn't defined with 'choice MY_CHOICE'), then it will > + be shown as "<choice>" before the list of locations (always a single one > + in that case). > + > + syms: > + List of symbols contained in the choice. > + > + Obscure gotcha: If a symbol depends on the previous symbol within a > + choice so that an implicit menu is created, it won't be a choice symbol, > + and won't be included in 'syms'. > + > + nodes: > + A list of MenuNodes for this choice. In practice, the list will probably > + always contain a single MenuNode, but it is possible to give a choice a > + name and define it in multiple locations. > + > + defaults: > + List of (symbol, cond) tuples for the choice's 'defaults' properties. For > + example, 'default A if B && C' is represented as (A, (AND, B, C)). If > + there is no condition, 'cond' is self.kconfig.y. > + > + Note that 'depends on' and parent dependencies are propagated to > + 'default' conditions. > + > + orig_defaults: > + See the corresponding attribute on the MenuNode class. > + > + direct_dep: > + See Symbol.direct_dep. > + > + referenced: > + A set() with all symbols referenced in the properties and property > + conditions of the choice. > + > + Also includes dependencies from surrounding menus and ifs, because those > + get propagated to the choice (see the 'Intro to symbol values' section in > + the module docstring). > + > + is_optional: > + True if the choice has the 'optional' flag set on it and can be in > + n mode. > + > + kconfig: > + The Kconfig instance this choice is from. > + """ > + __slots__ = ( > + "_cached_assignable", > + "_cached_selection", > + "_cached_vis", > + "_dependents", > + "_visited", > + "_was_set", > + "defaults", > + "direct_dep", > + "is_constant", > + "is_optional", > + "kconfig", > + "name", > + "nodes", > + "orig_type", > + "syms", > + "user_selection", > + "user_value", > + ) > + > + # > + # Public interface > + # > + > + @property > + def type(self): > + """ > + Returns the type of the choice. See Symbol.type. > + """ > + if self.orig_type is TRISTATE and not self.kconfig.modules.tri_value: > + return BOOL > + return self.orig_type > + > + @property > + def str_value(self): > + """ > + See the class documentation. > + """ > + return TRI_TO_STR[self.tri_value] > + > + @property > + def tri_value(self): > + """ > + See the class documentation. > + """ > + # This emulates a reverse dependency of 'm && visibility' for > + # non-optional choices, which is how the C implementation does it > + > + val = 0 if self.is_optional else 1 > + > + if self.user_value is not None: > + val = max(val, self.user_value) > + > + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden > + # function call (property magic) > + val = min(val, self.visibility) > + > + # Promote m to y for boolean choices > + return 2 if val == 1 and self.type is BOOL else val > + > + @property > + def assignable(self): > + """ > + See the class documentation. > + """ > + if self._cached_assignable is None: > + self._cached_assignable = self._assignable() > + return self._cached_assignable > + > + @property > + def visibility(self): > + """ > + See the class documentation. > + """ > + if self._cached_vis is None: > + self._cached_vis = _visibility(self) > + return self._cached_vis > + > + @property > + def name_and_loc(self): > + """ > + See the class documentation. > + """ > + # Reuse the expression format, which is '<choice (name, if any)>'. > + return standard_sc_expr_str(self) + " " + _locs(self) > + > + @property > + def selection(self): > + """ > + See the class documentation. > + """ > + if self._cached_selection is _NO_CACHED_SELECTION: > + self._cached_selection = self._selection() > + return self._cached_selection > + > + def set_value(self, value): > + """ > + Sets the user value (mode) of the choice. Like for Symbol.set_value(), > + the visibility might truncate the value. Choices without the 'optional' > + attribute (is_optional) can never be in n mode, but 0/"n" is still > + accepted since it's not a malformed value (though it will have no > + effect). > + > + Returns True if the value is valid for the type of the choice, and > + False otherwise. This only looks at the form of the value. Check the > + Choice.assignable attribute to see what values are currently in range > + and would actually be reflected in the mode of the choice. > + """ > + if value in STR_TO_TRI: > + value = STR_TO_TRI[value] > + > + if value == self.user_value: > + # We know the value must be valid if it was successfully set > + # previously > + self._was_set = True > + return True > + > + if not (self.orig_type is BOOL and value in (2, 0) or > + self.orig_type is TRISTATE and value in TRI_TO_STR): > + > + # Display tristate values as n, m, y in the warning > + self.kconfig._warn( > + "the value {} is invalid for {}, which has type {} -- " > + "assignment ignored" > + .format(TRI_TO_STR[value] if value in TRI_TO_STR else > + "'{}'".format(value), > + self.name_and_loc, TYPE_TO_STR[self.orig_type])) > + > + return False > + > + self.user_value = value > + self._was_set = True > + self._rec_invalidate() > + > + return True > + > + def unset_value(self): > + """ > + Resets the user value (mode) and user selection of the Choice, as if > + the user had never touched the mode or any of the choice symbols. > + """ > + if self.user_value is not None or self.user_selection: > + self.user_value = self.user_selection = None > + self._rec_invalidate() > + > + @property > + def referenced(self): > + """ > + See the class documentation. > + """ > + return {item for node in self.nodes for item in node.referenced} > + > + @property > + def orig_defaults(self): > + """ > + See the class documentation. > + """ > + return [d for node in self.nodes for d in node.orig_defaults] > + > + def __repr__(self): > + """ > + Returns a string with information about the choice when it is evaluated > + on e.g. the interactive Python prompt. > + """ > + fields = ["choice " + self.name if self.name else "choice", > + TYPE_TO_STR[self.type]] > + add = fields.append > + > + for node in self.nodes: > + if node.prompt: > + add('"{}"'.format(node.prompt[0])) > + > + add("mode " + self.str_value) > + > + if self.user_value is not None: > + add('user mode {}'.format(TRI_TO_STR[self.user_value])) > + > + if self.selection: > + add("{} selected".format(self.selection.name)) > + > + if self.user_selection: > + user_sel_str = "{} selected by user" \ > + .format(self.user_selection.name) > + > + if self.selection is not self.user_selection: > + user_sel_str += " (overridden)" > + > + add(user_sel_str) > + > + add("visibility " + TRI_TO_STR[self.visibility]) > + > + if self.is_optional: > + add("optional") > + > + for node in self.nodes: > + add("{}:{}".format(node.filename, node.linenr)) > + > + return "<{}>".format(", ".join(fields)) > + > + def __str__(self): > + """ > + Returns a string representation of the choice when it is printed. > + Matches the Kconfig format (though without the contained choice > + symbols), with any parent dependencies propagated to the 'depends on' > + condition. > + > + The returned string does not end in a newline. > + > + See Symbol.__str__() as well. > + """ > + return self.custom_str(standard_sc_expr_str) > + > + def custom_str(self, sc_expr_str_fn): > + """ > + Works like Choice.__str__(), but allows a custom format to be used for > + all symbol/choice references. See expr_str(). > + """ > + return "\n\n".join(node.custom_str(sc_expr_str_fn) > + for node in self.nodes) > + > + # > + # Private methods > + # > + > + def __init__(self): > + """ > + Choice constructor -- not intended to be called directly by Kconfiglib > + clients. > + """ > + # These attributes are always set on the instance from outside and > + # don't need defaults: > + # direct_dep > + # kconfig > + > + # - UNKNOWN == 0 > + # - _visited is used during dep. loop detection > + self.orig_type = self._visited = 0 > + > + self.nodes = [] > + > + self.syms = [] > + self.defaults = [] > + > + self.name = \ > + self.user_value = self.user_selection = \ > + self._cached_vis = self._cached_assignable = None > + > + self._cached_selection = _NO_CACHED_SELECTION > + > + # is_constant is checked by _depend_on(). Just set it to avoid having > + # to special-case choices. > + self.is_constant = self.is_optional = False > + > + # See Kconfig._build_dep() > + self._dependents = set() > + > + def _assignable(self): > + # Worker function for the 'assignable' attribute > + > + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden > + # function call (property magic) > + vis = self.visibility > + > + if not vis: > + return () > + > + if vis == 2: > + if not self.is_optional: > + return (2,) if self.type is BOOL else (1, 2) > + return (0, 2) if self.type is BOOL else (0, 1, 2) > + > + # vis == 1 > + > + return (0, 1) if self.is_optional else (1,) > + > + def _selection(self): > + # Worker function for the 'selection' attribute > + > + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden > + # function call (property magic) > + if self.tri_value != 2: > + # Not in y mode, so no selection > + return None > + > + # Use the user selection if it's visible > + if self.user_selection and self.user_selection.visibility: > + return self.user_selection > + > + # Otherwise, check if we have a default > + return self._selection_from_defaults() > + > + def _selection_from_defaults(self): > + # Check if we have a default > + for sym, cond in self.defaults: > + # The default symbol must be visible too > + if expr_value(cond) and sym.visibility: > + return sym > + > + # Otherwise, pick the first visible symbol, if any > + for sym in self.syms: > + if sym.visibility: > + return sym > + > + # Couldn't find a selection > + return None > + > + def _invalidate(self): > + self._cached_vis = self._cached_assignable = None > + self._cached_selection = _NO_CACHED_SELECTION > + > + def _rec_invalidate(self): > + # See Symbol._rec_invalidate() > + > + self._invalidate() > + > + for item in self._dependents: > + if item._cached_vis is not None: > + item._rec_invalidate() > + > + > +class MenuNode(object): > + """ > + Represents a menu node in the configuration. This corresponds to an entry > + in e.g. the 'make menuconfig' interface, though non-visible choices, menus, > + and comments also get menu nodes. If a symbol or choice is defined in > + multiple locations, it gets one menu node for each location. > + > + The top-level menu node, corresponding to the implicit top-level menu, is > + available in Kconfig.top_node. > + > + The menu nodes for a Symbol or Choice can be found in the > + Symbol/Choice.nodes attribute. Menus and comments are represented as plain > + menu nodes, with their text stored in the prompt attribute (prompt[0]). > + This mirrors the C implementation. > + > + The following attributes are available on MenuNode instances. They should > + be viewed as read-only. > + > + item: > + Either a Symbol, a Choice, or one of the constants MENU and COMMENT. > + Menus and comments are represented as plain menu nodes. Ifs are collapsed > + (matching the C implementation) and do not appear in the final menu tree. > + > + next: > + The following menu node. None if there is no following node. > + > + list: > + The first child menu node. None if there are no children. > + > + Choices and menus naturally have children, but Symbols can also have > + children because of menus created automatically from dependencies (see > + kconfig-language.txt). > + > + parent: > + The parent menu node. None if there is no parent. > + > + prompt: > + A (string, cond) tuple with the prompt for the menu node and its > + conditional expression (which is self.kconfig.y if there is no > + condition). None if there is no prompt. > + > + For symbols and choices, the prompt is stored in the MenuNode rather than > + the Symbol or Choice instance. For menus and comments, the prompt holds > + the text. > + > + defaults: > + The 'default' properties for this particular menu node. See > + symbol.defaults. > + > + When evaluating defaults, you should use Symbol/Choice.defaults instead, > + as it include properties from all menu nodes (a symbol/choice can have > + multiple definition locations/menu nodes). MenuNode.defaults is meant for > + documentation generation. > + > + selects: > + Like MenuNode.defaults, for selects. > + > + implies: > + Like MenuNode.defaults, for implies. > + > + ranges: > + Like MenuNode.defaults, for ranges. > + > + orig_prompt: > + orig_defaults: > + orig_selects: > + orig_implies: > + orig_ranges: > + These work the like the corresponding attributes without orig_*, but omit > + any dependencies propagated from 'depends on' and surrounding 'if's (the > + direct dependencies, stored in MenuNode.dep). > + > + One use for this is generating less cluttered documentation, by only > + showing the direct dependencies in one place. > + > + help: > + The help text for the menu node for Symbols and Choices. None if there is > + no help text. Always stored in the node rather than the Symbol or Choice. > + It is possible to have a separate help text at each location if a symbol > + is defined in multiple locations. > + > + Trailing whitespace (including a final newline) is stripped from the help > + text. This was not the case before Kconfiglib 10.21.0, where the format > + was undocumented. > + > + dep: > + The direct ('depends on') dependencies for the menu node, or > + self.kconfig.y if there are no direct dependencies. > + > + This attribute includes any dependencies from surrounding menus and ifs. > + Those get propagated to the direct dependencies, and the resulting direct > + dependencies in turn get propagated to the conditions of all properties. > + > + If a symbol or choice is defined in multiple locations, only the > + properties defined at a particular location get the corresponding > + MenuNode.dep dependencies propagated to them. > + > + visibility: > + The 'visible if' dependencies for the menu node (which must represent a > + menu), or self.kconfig.y if there are no 'visible if' dependencies. > + 'visible if' dependencies are recursively propagated to the prompts of > + symbols and choices within the menu. > + > + referenced: > + A set() with all symbols and choices referenced in the properties and > + property conditions of the menu node. > + > + Also includes dependencies inherited from surrounding menus and ifs. > + Choices appear in the dependencies of choice symbols. > + > + is_menuconfig: > + Set to True if the children of the menu node should be displayed in a > + separate menu. This is the case for the following items: > + > + - Menus (node.item == MENU) > + > + - Choices > + > + - Symbols defined with the 'menuconfig' keyword. The children come from > + implicitly created submenus, and should be displayed in a separate > + menu rather than being indented. > + > + 'is_menuconfig' is just a hint on how to display the menu node. It's > + ignored internally by Kconfiglib, except when printing symbols. > + > + filename/linenr: > + The location where the menu node appears. The filename is relative to > + $srctree (or to the current directory if $srctree isn't set), except > + absolute paths are used for paths outside $srctree. > + > + include_path: > + A tuple of (filename, linenr) tuples, giving the locations of the > + 'source' statements via which the Kconfig file containing this menu node > + was included. The first element is the location of the 'source' statement > + in the top-level Kconfig file passed to Kconfig.__init__(), etc. > + > + Note that the Kconfig file of the menu node itself isn't included. Check > + 'filename' and 'linenr' for that. > + > + kconfig: > + The Kconfig instance the menu node is from. > + """ > + __slots__ = ( > + "dep", > + "filename", > + "help", > + "include_path", > + "is_menuconfig", > + "item", > + "kconfig", > + "linenr", > + "list", > + "next", > + "parent", > + "prompt", > + "visibility", > + > + # Properties > + "defaults", > + "selects", > + "implies", > + "ranges", > + ) > + > + def __init__(self): > + # Properties defined on this particular menu node. A local 'depends on' > + # only applies to these, in case a symbol is defined in multiple > + # locations. > + self.defaults = [] > + self.selects = [] > + self.implies = [] > + self.ranges = [] > + > + @property > + def orig_prompt(self): > + """ > + See the class documentation. > + """ > + if not self.prompt: > + return None > + return (self.prompt[0], self._strip_dep(self.prompt[1])) > + > + @property > + def orig_defaults(self): > + """ > + See the class documentation. > + """ > + return [(default, self._strip_dep(cond)) > + for default, cond in self.defaults] > + > + @property > + def orig_selects(self): > + """ > + See the class documentation. > + """ > + return [(select, self._strip_dep(cond)) > + for select, cond in self.selects] > + > + @property > + def orig_implies(self): > + """ > + See the class documentation. > + """ > + return [(imply, self._strip_dep(cond)) > + for imply, cond in self.implies] > + > + @property > + def orig_ranges(self): > + """ > + See the class documentation. > + """ > + return [(low, high, self._strip_dep(cond)) > + for low, high, cond in self.ranges] > + > + @property > + def referenced(self): > + """ > + See the class documentation. > + """ > + # self.dep is included to catch dependencies from a lone 'depends on' > + # when there are no properties to propagate it to > + res = expr_items(self.dep) > + > + if self.prompt: > + res |= expr_items(self.prompt[1]) > + > + if self.item is MENU: > + res |= expr_items(self.visibility) > + > + for value, cond in self.defaults: > + res |= expr_items(value) > + res |= expr_items(cond) > + > + for value, cond in self.selects: > + res.add(value) > + res |= expr_items(cond) > + > + for value, cond in self.implies: > + res.add(value) > + res |= expr_items(cond) > + > + for low, high, cond in self.ranges: > + res.add(low) > + res.add(high) > + res |= expr_items(cond) > + > + return res > + > + def __repr__(self): > + """ > + Returns a string with information about the menu node when it is > + evaluated on e.g. the interactive Python prompt. > + """ > + fields = [] > + add = fields.append > + > + if self.item.__class__ is Symbol: > + add("menu node for symbol " + self.item.name) > + > + elif self.item.__class__ is Choice: > + s = "menu node for choice" > + if self.item.name is not None: > + s += " " + self.item.name > + add(s) > + > + elif self.item is MENU: > + add("menu node for menu") > + > + else: # self.item is COMMENT > + add("menu node for comment") > + > + if self.prompt: > + add('prompt "{}" (visibility {})'.format( > + self.prompt[0], TRI_TO_STR[expr_value(self.prompt[1])])) > + > + if self.item.__class__ is Symbol and self.is_menuconfig: > + add("is menuconfig") > + > + add("deps " + TRI_TO_STR[expr_value(self.dep)]) > + > + if self.item is MENU: > + add("'visible if' deps " + TRI_TO_STR[expr_value(self.visibility)]) > + > + if self.item.__class__ in _SYMBOL_CHOICE and self.help is not None: > + add("has help") > + > + if self.list: > + add("has child") > + > + if self.next: > + add("has next") > + > + add("{}:{}".format(self.filename, self.linenr)) > + > + return "<{}>".format(", ".join(fields)) > + > + def __str__(self): > + """ > + Returns a string representation of the menu node. Matches the Kconfig > + format, with any parent dependencies propagated to the 'depends on' > + condition. > + > + The output could (almost) be fed back into a Kconfig parser to redefine > + the object associated with the menu node. See the module documentation > + for a gotcha related to choice symbols. > + > + For symbols and choices with multiple menu nodes (multiple definition > + locations), properties that aren't associated with a particular menu > + node are shown on all menu nodes ('option env=...', 'optional' for > + choices, etc.). > + > + The returned string does not end in a newline. > + """ > + return self.custom_str(standard_sc_expr_str) > + > + def custom_str(self, sc_expr_str_fn): > + """ > + Works like MenuNode.__str__(), but allows a custom format to be used > + for all symbol/choice references. See expr_str(). > + """ > + return self._menu_comment_node_str(sc_expr_str_fn) \ > + if self.item in _MENU_COMMENT else \ > + self._sym_choice_node_str(sc_expr_str_fn) > + > + def _menu_comment_node_str(self, sc_expr_str_fn): > + s = '{} "{}"'.format("menu" if self.item is MENU else "comment", > + self.prompt[0]) > + > + if self.dep is not self.kconfig.y: > + s += "\n\tdepends on {}".format(expr_str(self.dep, sc_expr_str_fn)) > + > + if self.item is MENU and self.visibility is not self.kconfig.y: > + s += "\n\tvisible if {}".format(expr_str(self.visibility, > + sc_expr_str_fn)) > + > + return s > + > + def _sym_choice_node_str(self, sc_expr_str_fn): > + def indent_add(s): > + lines.append("\t" + s) > + > + def indent_add_cond(s, cond): > + if cond is not self.kconfig.y: > + s += " if " + expr_str(cond, sc_expr_str_fn) > + indent_add(s) > + > + sc = self.item > + > + if sc.__class__ is Symbol: > + lines = [("menuconfig " if self.is_menuconfig else "config ") > + + sc.name] > + else: > + lines = ["choice " + sc.name if sc.name else "choice"] > + > + if sc.orig_type and not self.prompt: # sc.orig_type != UNKNOWN > + # If there's a prompt, we'll use the '<type> "prompt"' shorthand > + # instead > + indent_add(TYPE_TO_STR[sc.orig_type]) > + > + if self.prompt: > + if sc.orig_type: > + prefix = TYPE_TO_STR[sc.orig_type] > + else: > + # Symbol defined without a type (which generates a warning) > + prefix = "prompt" > + > + indent_add_cond(prefix + ' "{}"'.format(escape(self.prompt[0])), > + self.orig_prompt[1]) > + > + if sc.__class__ is Symbol: > + if sc.is_allnoconfig_y: > + indent_add("option allnoconfig_y") > + > + if sc is sc.kconfig.defconfig_list: > + indent_add("option defconfig_list") > + > + if sc.env_var is not None: > + indent_add('option env="{}"'.format(sc.env_var)) > + > + if sc is sc.kconfig.modules: > + indent_add("option modules") > + > + for low, high, cond in self.orig_ranges: > + indent_add_cond( > + "range {} {}".format(sc_expr_str_fn(low), > + sc_expr_str_fn(high)), > + cond) > + > + for default, cond in self.orig_defaults: > + indent_add_cond("default " + expr_str(default, sc_expr_str_fn), > + cond) > + > + if sc.__class__ is Choice and sc.is_optional: > + indent_add("optional") > + > + if sc.__class__ is Symbol: > + for select, cond in self.orig_selects: > + indent_add_cond("select " + sc_expr_str_fn(select), cond) > + > + for imply, cond in self.orig_implies: > + indent_add_cond("imply " + sc_expr_str_fn(imply), cond) > + > + if self.dep is not sc.kconfig.y: > + indent_add("depends on " + expr_str(self.dep, sc_expr_str_fn)) > + > + if self.help is not None: > + indent_add("help") > + for line in self.help.splitlines(): > + indent_add(" " + line) > + > + return "\n".join(lines) > + > + def _strip_dep(self, expr): > + # Helper function for removing MenuNode.dep from 'expr'. Uses two > + # pieces of internal knowledge: (1) Expressions are reused rather than > + # copied, and (2) the direct dependencies always appear at the end. > + > + # ... if dep -> ... if y > + if self.dep is expr: > + return self.kconfig.y > + > + # (AND, X, dep) -> X > + if expr.__class__ is tuple and expr[0] is AND and expr[2] is self.dep: > + return expr[1] > + > + return expr > + > + > +class Variable(object): > + """ > + Represents a preprocessor variable/function. > + > + The following attributes are available: > + > + name: > + The name of the variable. > + > + value: > + The unexpanded value of the variable. > + > + expanded_value: > + The expanded value of the variable. For simple variables (those defined > + with :=), this will equal 'value'. Accessing this property will raise a > + KconfigError if the expansion seems to be stuck in a loop. > + > + Accessing this field is the same as calling expanded_value_w_args() with > + no arguments. I hadn't considered function arguments when adding it. It > + is retained for backwards compatibility though. > + > + is_recursive: > + True if the variable is recursive (defined with =). > + """ > + __slots__ = ( > + "_n_expansions", > + "is_recursive", > + "kconfig", > + "name", > + "value", > + ) > + > + @property > + def expanded_value(self): > + """ > + See the class documentation. > + """ > + return self.expanded_value_w_args() > + > + def expanded_value_w_args(self, *args): > + """ > + Returns the expanded value of the variable/function. Any arguments > + passed will be substituted for $(1), $(2), etc. > + > + Raises a KconfigError if the expansion seems to be stuck in a loop. > + """ > + return self.kconfig._fn_val((self.name,) + args) > + > + def __repr__(self): > + return "<variable {}, {}, value '{}'>" \ > + .format(self.name, > + "recursive" if self.is_recursive else "immediate", > + self.value) > + > + > +class KconfigError(Exception): > + """ > + Exception raised for Kconfig-related errors. > + > + KconfigError and KconfigSyntaxError are the same class. The > + KconfigSyntaxError alias is only maintained for backwards compatibility. > + """ > + > +KconfigSyntaxError = KconfigError # Backwards compatibility > + > + > +class InternalError(Exception): > + "Never raised. Kept around for backwards compatibility." > + > + > +# Workaround: > +# > +# If 'errno' and 'strerror' are set on IOError, then __str__() always returns > +# "[Errno <errno>] <strerror>", ignoring any custom message passed to the > +# constructor. By defining our own subclass, we can use a custom message while > +# also providing 'errno', 'strerror', and 'filename' to scripts. > +class _KconfigIOError(IOError): > + def __init__(self, ioerror, msg): > + self.msg = msg > + super(_KconfigIOError, self).__init__( > + ioerror.errno, ioerror.strerror, ioerror.filename) > + > + def __str__(self): > + return self.msg > + > + > +# > +# Public functions > +# > + > + > +def expr_value(expr): > + """ > + Evaluates the expression 'expr' to a tristate value. Returns 0 (n), 1 (m), > + or 2 (y). > + > + 'expr' must be an already-parsed expression from a Symbol, Choice, or > + MenuNode property. To evaluate an expression represented as a string, use > + Kconfig.eval_string(). > + > + Passing subexpressions of expressions to this function works as expected. > + """ > + if expr.__class__ is not tuple: > + return expr.tri_value > + > + if expr[0] is AND: > + v1 = expr_value(expr[1]) > + # Short-circuit the n case as an optimization (~5% faster > + # allnoconfig.py and allyesconfig.py, as of writing) > + return 0 if not v1 else min(v1, expr_value(expr[2])) > + > + if expr[0] is OR: > + v1 = expr_value(expr[1]) > + # Short-circuit the y case as an optimization > + return 2 if v1 == 2 else max(v1, expr_value(expr[2])) > + > + if expr[0] is NOT: > + return 2 - expr_value(expr[1]) > + > + # Relation > + # > + # Implements <, <=, >, >= comparisons as well. These were added to > + # kconfig in 31847b67 (kconfig: allow use of relations other than > + # (in)equality). > + > + rel, v1, v2 = expr > + > + # If both operands are strings... > + if v1.orig_type is STRING and v2.orig_type is STRING: > + # ...then compare them lexicographically > + comp = _strcmp(v1.str_value, v2.str_value) > + else: > + # Otherwise, try to compare them as numbers > + try: > + comp = _sym_to_num(v1) - _sym_to_num(v2) > + except ValueError: > + # Fall back on a lexicographic comparison if the operands don't > + # parse as numbers > + comp = _strcmp(v1.str_value, v2.str_value) > + > + return 2*(comp == 0 if rel is EQUAL else > + comp != 0 if rel is UNEQUAL else > + comp < 0 if rel is LESS else > + comp <= 0 if rel is LESS_EQUAL else > + comp > 0 if rel is GREATER else > + comp >= 0) > + > + > +def standard_sc_expr_str(sc): > + """ > + Standard symbol/choice printing function. Uses plain Kconfig syntax, and > + displays choices as <choice> (or <choice NAME>, for named choices). > + > + See expr_str(). > + """ > + if sc.__class__ is Symbol: > + if sc.is_constant and sc.name not in STR_TO_TRI: > + return '"{}"'.format(escape(sc.name)) > + return sc.name > + > + return "<choice {}>".format(sc.name) if sc.name else "<choice>" > + > + > +def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str): > + """ > + Returns the string representation of the expression 'expr', as in a Kconfig > + file. > + > + Passing subexpressions of expressions to this function works as expected. > + > + sc_expr_str_fn (default: standard_sc_expr_str): > + This function is called for every symbol/choice (hence "sc") appearing in > + the expression, with the symbol/choice as the argument. It is expected to > + return a string to be used for the symbol/choice. > + > + This can be used e.g. to turn symbols/choices into links when generating > + documentation, or for printing the value of each symbol/choice after it. > + > + Note that quoted values are represented as constants symbols > + (Symbol.is_constant == True). > + """ > + if expr.__class__ is not tuple: > + return sc_expr_str_fn(expr) > + > + if expr[0] is AND: > + return "{} && {}".format(_parenthesize(expr[1], OR, sc_expr_str_fn), > + _parenthesize(expr[2], OR, sc_expr_str_fn)) > + > + if expr[0] is OR: > + # This turns A && B || C && D into "(A && B) || (C && D)", which is > + # redundant, but more readable > + return "{} || {}".format(_parenthesize(expr[1], AND, sc_expr_str_fn), > + _parenthesize(expr[2], AND, sc_expr_str_fn)) > + > + if expr[0] is NOT: > + if expr[1].__class__ is tuple: > + return "!({})".format(expr_str(expr[1], sc_expr_str_fn)) > + return "!" + sc_expr_str_fn(expr[1]) # Symbol > + > + # Relation > + # > + # Relation operands are always symbols (quoted strings are constant > + # symbols) > + return "{} {} {}".format(sc_expr_str_fn(expr[1]), REL_TO_STR[expr[0]], > + sc_expr_str_fn(expr[2])) > + > + > +def expr_items(expr): > + """ > + Returns a set() of all items (symbols and choices) that appear in the > + expression 'expr'. > + > + Passing subexpressions of expressions to this function works as expected. > + """ > + res = set() > + > + def rec(subexpr): > + if subexpr.__class__ is tuple: > + # AND, OR, NOT, or relation > + > + rec(subexpr[1]) > + > + # NOTs only have a single operand > + if subexpr[0] is not NOT: > + rec(subexpr[2]) > + > + else: > + # Symbol or choice > + res.add(subexpr) > + > + rec(expr) > + return res > + > + > +def split_expr(expr, op): > + """ > + Returns a list containing the top-level AND or OR operands in the > + expression 'expr', in the same (left-to-right) order as they appear in > + the expression. > + > + This can be handy e.g. for splitting (weak) reverse dependencies > + from 'select' and 'imply' into individual selects/implies. > + > + op: > + Either AND to get AND operands, or OR to get OR operands. > + > + (Having this as an operand might be more future-safe than having two > + hardcoded functions.) > + > + > + Pseudo-code examples: > + > + split_expr( A , OR ) -> [A] > + split_expr( A && B , OR ) -> [A && B] > + split_expr( A || B , OR ) -> [A, B] > + split_expr( A || B , AND ) -> [A || B] > + split_expr( A || B || (C && D) , OR ) -> [A, B, C && D] > + > + # Second || is not at the top level > + split_expr( A || (B && (C || D)) , OR ) -> [A, B && (C || D)] > + > + # Parentheses don't matter as long as we stay at the top level (don't > + # encounter any non-'op' nodes) > + split_expr( (A || B) || C , OR ) -> [A, B, C] > + split_expr( A || (B || C) , OR ) -> [A, B, C] > + """ > + res = [] > + > + def rec(subexpr): > + if subexpr.__class__ is tuple and subexpr[0] is op: > + rec(subexpr[1]) > + rec(subexpr[2]) > + else: > + res.append(subexpr) > + > + rec(expr) > + return res > + > + > +def escape(s): > + r""" > + Escapes the string 's' in the same fashion as is done for display in > + Kconfig format and when writing strings to a .config file. " and \ are > + replaced by \" and \\, respectively. > + """ > + # \ must be escaped before " to avoid double escaping > + return s.replace("\\", r"\\").replace('"', r'\"') > + > + > +def unescape(s): > + r""" > + Unescapes the string 's'. \ followed by any character is replaced with just > + that character. Used internally when reading .config files. > + """ > + return _unescape_sub(r"\1", s) > + > +# unescape() helper > +_unescape_sub = re.compile(r"\\(.)").sub > + > + > +def standard_kconfig(description=None): > + """ > + Argument parsing helper for tools that take a single optional Kconfig file > + argument (default: Kconfig). Returns the Kconfig instance for the parsed > + configuration. Uses argparse internally. > + > + Exits with sys.exit() (which raises SystemExit) on errors. > + > + description (default: None): > + The 'description' passed to argparse.ArgumentParser(). > + argparse.RawDescriptionHelpFormatter is used, so formatting is preserved. > + """ > + import argparse > + > + parser = argparse.ArgumentParser( > + formatter_class=argparse.RawDescriptionHelpFormatter, > + description=description) > + > + parser.add_argument( > + "kconfig", > + metavar="KCONFIG", > + default="Kconfig", > + nargs="?", > + help="Top-level Kconfig file (default: Kconfig)") > + > + return Kconfig(parser.parse_args().kconfig, suppress_traceback=True) > + > + > +def standard_config_filename(): > + """ > + Helper for tools. Returns the value of KCONFIG_CONFIG (which specifies the > + .config file to load/save) if it is set, and ".config" otherwise. > + > + Calling load_config() with filename=None might give the behavior you want, > + without having to use this function. > + """ > + return os.getenv("KCONFIG_CONFIG", ".config") > + > + > +def load_allconfig(kconf, filename): > + """ > + Use Kconfig.load_allconfig() instead, which was added in Kconfiglib 13.4.0. > + Supported for backwards compatibility. Might be removed at some point after > + a long period of deprecation warnings. > + """ > + allconfig = os.getenv("KCONFIG_ALLCONFIG") > + if allconfig is None: > + return > + > + def std_msg(e): > + # "Upcasts" a _KconfigIOError to an IOError, removing the custom > + # __str__() message. The standard message is better here. > + # > + # This might also convert an OSError to an IOError in obscure cases, > + # but it's probably not a big deal. The distinction is shaky (see > + # PEP-3151). > + return IOError(e.errno, e.strerror, e.filename) > + > + old_warn_assign_override = kconf.warn_assign_override > + old_warn_assign_redun = kconf.warn_assign_redun > + kconf.warn_assign_override = kconf.warn_assign_redun = False > + > + if allconfig in ("", "1"): > + try: > + print(kconf.load_config(filename, False)) > + except EnvironmentError as e1: > + try: > + print(kconf.load_config("all.config", False)) > + except EnvironmentError as e2: > + sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} " > + "nor all.config could be opened: {}, {}" > + .format(filename, std_msg(e1), std_msg(e2))) > + else: > + try: > + print(kconf.load_config(allconfig, False)) > + except EnvironmentError as e: > + sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which " > + "could not be opened: {}" > + .format(allconfig, std_msg(e))) > + > + kconf.warn_assign_override = old_warn_assign_override > + kconf.warn_assign_redun = old_warn_assign_redun > + > + > +# > +# Internal functions > +# > + > + > +def _visibility(sc): > + # Symbols and Choices have a "visibility" that acts as an upper bound on > + # the values a user can set for them, corresponding to the visibility in > + # e.g. 'make menuconfig'. This function calculates the visibility for the > + # Symbol or Choice 'sc' -- the logic is nearly identical. > + > + vis = 0 > + > + for node in sc.nodes: > + if node.prompt: > + vis = max(vis, expr_value(node.prompt[1])) > + > + if sc.__class__ is Symbol and sc.choice: > + if sc.choice.orig_type is TRISTATE and \ > + sc.orig_type is not TRISTATE and sc.choice.tri_value != 2: > + # Non-tristate choice symbols are only visible in y mode > + return 0 > + > + if sc.orig_type is TRISTATE and vis == 1 and sc.choice.tri_value == 2: > + # Choice symbols with m visibility are not visible in y mode > + return 0 > + > + # Promote m to y if we're dealing with a non-tristate (possibly due to > + # modules being disabled) > + if vis == 1 and sc.type is not TRISTATE: > + return 2 > + > + return vis > + > + > +def _depend_on(sc, expr): > + # Adds 'sc' (symbol or choice) as a "dependee" to all symbols in 'expr'. > + # Constant symbols in 'expr' are skipped as they can never change value > + # anyway. > + > + if expr.__class__ is tuple: > + # AND, OR, NOT, or relation > + > + _depend_on(sc, expr[1]) > + > + # NOTs only have a single operand > + if expr[0] is not NOT: > + _depend_on(sc, expr[2]) > + > + elif not expr.is_constant: > + # Non-constant symbol, or choice > + expr._dependents.add(sc) > + > + > +def _parenthesize(expr, type_, sc_expr_str_fn): > + # expr_str() helper. Adds parentheses around expressions of type 'type_'. > + > + if expr.__class__ is tuple and expr[0] is type_: > + return "({})".format(expr_str(expr, sc_expr_str_fn)) > + return expr_str(expr, sc_expr_str_fn) > + > + > +def _ordered_unique(lst): > + # Returns 'lst' with any duplicates removed, preserving order. This hacky > + # version seems to be a common idiom. It relies on short-circuit evaluation > + # and set.add() returning None, which is falsy. > + > + seen = set() > + seen_add = seen.add > + return [x for x in lst if x not in seen and not seen_add(x)] > + > + > +def _is_base_n(s, n): > + try: > + int(s, n) > + return True > + except ValueError: > + return False > + > + > +def _strcmp(s1, s2): > + # strcmp()-alike that returns -1, 0, or 1 > + > + return (s1 > s2) - (s1 < s2) > + > + > +def _sym_to_num(sym): > + # expr_value() helper for converting a symbol to a number. Raises > + # ValueError for symbols that can't be converted. > + > + # For BOOL and TRISTATE, n/m/y count as 0/1/2. This mirrors 9059a3493ef > + # ("kconfig: fix relational operators for bool and tristate symbols") in > + # the C implementation. > + return sym.tri_value if sym.orig_type in _BOOL_TRISTATE else \ > + int(sym.str_value, _TYPE_TO_BASE[sym.orig_type]) > + > + > +def _touch_dep_file(path, sym_name): > + # If sym_name is MY_SYM_NAME, touches my/sym/name.h. See the sync_deps() > + # docstring. > + > + sym_path = path + os.sep + sym_name.lower().replace("_", os.sep) + ".h" > + sym_path_dir = dirname(sym_path) > + if not exists(sym_path_dir): > + os.makedirs(sym_path_dir, 0o755) > + > + # A kind of truncating touch, mirroring the C tools > + os.close(os.open( > + sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)) > + > + > +def _save_old(path): > + # See write_config() > + > + def copy(src, dst): > + # Import as needed, to save some startup time > + import shutil > + shutil.copyfile(src, dst) > + > + if islink(path): > + # Preserve symlinks > + copy_fn = copy > + elif hasattr(os, "replace"): > + # Python 3 (3.3+) only. Best choice when available, because it > + # removes <filename>.old on both *nix and Windows. > + copy_fn = os.replace > + elif os.name == "posix": > + # Removes <filename>.old on POSIX systems > + copy_fn = os.rename > + else: > + # Fall back on copying > + copy_fn = copy > + > + try: > + copy_fn(path, path + ".old") > + except Exception: > + # Ignore errors from 'path' missing as well as other errors. > + # <filename>.old file is usually more of a nice-to-have, and not worth > + # erroring out over e.g. if <filename>.old happens to be a directory or > + # <filename> is something like /dev/null. > + pass > + > + > +def _locs(sc): > + # Symbol/Choice.name_and_loc helper. Returns the "(defined at ...)" part of > + # the string. 'sc' is a Symbol or Choice. > + > + if sc.nodes: > + return "(defined at {})".format( > + ", ".join("{0.filename}:{0.linenr}".format(node) > + for node in sc.nodes)) > + > + return "(undefined)" > + > + > +# Menu manipulation > + > + > +def _expr_depends_on(expr, sym): > + # Reimplementation of expr_depends_symbol() from mconf.c. Used to determine > + # if a submenu should be implicitly created. This also influences which > + # items inside choice statements are considered choice items. > + > + if expr.__class__ is not tuple: > + return expr is sym > + > + if expr[0] in _EQUAL_UNEQUAL: > + # Check for one of the following: > + # sym = m/y, m/y = sym, sym != n, n != sym > + > + left, right = expr[1:] > + > + if right is sym: > + left, right = right, left > + elif left is not sym: > + return False > + > + return (expr[0] is EQUAL and right is sym.kconfig.m or > + right is sym.kconfig.y) or \ > + (expr[0] is UNEQUAL and right is sym.kconfig.n) > + > + return expr[0] is AND and \ > + (_expr_depends_on(expr[1], sym) or > + _expr_depends_on(expr[2], sym)) > + > + > +def _auto_menu_dep(node1, node2): > + # Returns True if node2 has an "automatic menu dependency" on node1. If > + # node2 has a prompt, we check its condition. Otherwise, we look directly > + # at node2.dep. > + > + return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep, > + node1.item) > + > + > +def _flatten(node): > + # "Flattens" menu nodes without prompts (e.g. 'if' nodes and non-visible > + # symbols with children from automatic menu creation) so that their > + # children appear after them instead. This gives a clean menu structure > + # with no unexpected "jumps" in the indentation. > + # > + # Do not flatten promptless choices (which can appear "legitimately" if a > + # named choice is defined in multiple locations to add on symbols). It > + # looks confusing, and the menuconfig already shows all choice symbols if > + # you enter the choice at some location with a prompt. > + > + while node: > + if node.list and not node.prompt and \ > + node.item.__class__ is not Choice: > + > + last_node = node.list > + while 1: > + last_node.parent = node.parent > + if not last_node.next: > + break > + last_node = last_node.next > + > + last_node.next = node.next > + node.next = node.list > + node.list = None > + > + node = node.next > + > + > +def _remove_ifs(node): > + # Removes 'if' nodes (which can be recognized by MenuNode.item being None), > + # which are assumed to already have been flattened. The C implementation > + # doesn't bother to do this, but we expose the menu tree directly, and it > + # makes it nicer to work with. > + > + cur = node.list > + while cur and not cur.item: > + cur = cur.next > + > + node.list = cur > + > + while cur: > + next = cur.next > + while next and not next.item: > + next = next.next > + > + # Equivalent to > + # > + # cur.next = next > + # cur = next > + # > + # due to tricky Python semantics. The order matters. > + cur.next = cur = next > + > + > +def _finalize_choice(node): > + # Finalizes a choice, marking each symbol whose menu node has the choice as > + # the parent as a choice symbol, and automatically determining types if not > + # specified. > + > + choice = node.item > + > + cur = node.list > + while cur: > + if cur.item.__class__ is Symbol: > + cur.item.choice = choice > + choice.syms.append(cur.item) > + cur = cur.next > + > + # If no type is specified for the choice, its type is that of > + # the first choice item with a specified type > + if not choice.orig_type: > + for item in choice.syms: > + if item.orig_type: > + choice.orig_type = item.orig_type > + break > + > + # Each choice item of UNKNOWN type gets the type of the choice > + for sym in choice.syms: > + if not sym.orig_type: > + sym.orig_type = choice.orig_type > + > + > +def _check_dep_loop_sym(sym, ignore_choice): > + # Detects dependency loops using depth-first search on the dependency graph > + # (which is calculated earlier in Kconfig._build_dep()). > + # > + # Algorithm: > + # > + # 1. Symbols/choices start out with _visited = 0, meaning unvisited. > + # > + # 2. When a symbol/choice is first visited, _visited is set to 1, meaning > + # "visited, potentially part of a dependency loop". The recursive > + # search then continues from the symbol/choice. > + # > + # 3. If we run into a symbol/choice X with _visited already set to 1, > + # there's a dependency loop. The loop is found on the call stack by > + # recording symbols while returning ("on the way back") until X is seen > + # again. > + # > + # 4. Once a symbol/choice and all its dependencies (or dependents in this > + # case) have been checked recursively without detecting any loops, its > + # _visited is set to 2, meaning "visited, not part of a dependency > + # loop". > + # > + # This saves work if we run into the symbol/choice again in later calls > + # to _check_dep_loop_sym(). We just return immediately. > + # > + # Choices complicate things, as every choice symbol depends on every other > + # choice symbol in a sense. When a choice is "entered" via a choice symbol > + # X, we visit all choice symbols from the choice except X, and prevent > + # immediately revisiting the choice with a flag (ignore_choice). > + # > + # Maybe there's a better way to handle this (different flags or the > + # like...) > + > + if not sym._visited: > + # sym._visited == 0, unvisited > + > + sym._visited = 1 > + > + for dep in sym._dependents: > + # Choices show up in Symbol._dependents when the choice has the > + # symbol in a 'prompt' or 'default' condition (e.g. > + # 'default ... if SYM'). > + # > + # Since we aren't entering the choice via a choice symbol, all > + # choice symbols need to be checked, hence the None. > + loop = _check_dep_loop_choice(dep, None) \ > + if dep.__class__ is Choice \ > + else _check_dep_loop_sym(dep, False) > + > + if loop: > + # Dependency loop found > + return _found_dep_loop(loop, sym) > + > + if sym.choice and not ignore_choice: > + loop = _check_dep_loop_choice(sym.choice, sym) > + if loop: > + # Dependency loop found > + return _found_dep_loop(loop, sym) > + > + # The symbol is not part of a dependency loop > + sym._visited = 2 > + > + # No dependency loop found > + return None > + > + if sym._visited == 2: > + # The symbol was checked earlier and is already known to not be part of > + # a dependency loop > + return None > + > + # sym._visited == 1, found a dependency loop. Return the symbol as the > + # first element in it. > + return (sym,) > + > + > +def _check_dep_loop_choice(choice, skip): > + if not choice._visited: > + # choice._visited == 0, unvisited > + > + choice._visited = 1 > + > + # Check for loops involving choice symbols. If we came here via a > + # choice symbol, skip that one, as we'd get a false positive > + # '<sym FOO> -> <choice> -> <sym FOO>' loop otherwise. > + for sym in choice.syms: > + if sym is not skip: > + # Prevent the choice from being immediately re-entered via the > + # "is a choice symbol" path by passing True > + loop = _check_dep_loop_sym(sym, True) > + if loop: > + # Dependency loop found > + return _found_dep_loop(loop, choice) > + > + # The choice is not part of a dependency loop > + choice._visited = 2 > + > + # No dependency loop found > + return None > + > + if choice._visited == 2: > + # The choice was checked earlier and is already known to not be part of > + # a dependency loop > + return None > + > + # choice._visited == 1, found a dependency loop. Return the choice as the > + # first element in it. > + return (choice,) > + > + > +def _found_dep_loop(loop, cur): > + # Called "on the way back" when we know we have a loop > + > + # Is the symbol/choice 'cur' where the loop started? > + if cur is not loop[0]: > + # Nope, it's just a part of the loop > + return loop + (cur,) > + > + # Yep, we have the entire loop. Throw an exception that shows it. > + > + msg = "\nDependency loop\n" \ > + "===============\n\n" > + > + for item in loop: > + if item is not loop[0]: > + msg += "...depends on " > + if item.__class__ is Symbol and item.choice: > + msg += "the choice symbol " > + > + msg += "{}, with definition...\n\n{}\n\n" \ > + .format(item.name_and_loc, item) > + > + # Small wart: Since we reuse the already calculated > + # Symbol/Choice._dependents sets for recursive dependency detection, we > + # lose information on whether a dependency came from a 'select'/'imply' > + # condition or e.g. a 'depends on'. > + # > + # This might cause selecting symbols to "disappear". For example, > + # a symbol B having 'select A if C' gives a direct dependency from A to > + # C, since it corresponds to a reverse dependency of B && C. > + # > + # Always print reverse dependencies for symbols that have them to make > + # sure information isn't lost. I wonder if there's some neat way to > + # improve this. > + > + if item.__class__ is Symbol: > + if item.rev_dep is not item.kconfig.n: > + msg += "(select-related dependencies: {})\n\n" \ > + .format(expr_str(item.rev_dep)) > + > + if item.weak_rev_dep is not item.kconfig.n: > + msg += "(imply-related dependencies: {})\n\n" \ > + .format(expr_str(item.rev_dep)) > + > + msg += "...depends again on " + loop[0].name_and_loc > + > + raise KconfigError(msg) > + > + > +def _decoding_error(e, filename, macro_linenr=None): > + # Gives the filename and context for UnicodeDecodeError's, which are a pain > + # to debug otherwise. 'e' is the UnicodeDecodeError object. > + # > + # If the decoding error is for the output of a $(shell,...) command, > + # macro_linenr holds the line number where it was run (the exact line > + # number isn't available for decoding errors in files). > + > + raise KconfigError( > + "\n" > + "Malformed {} in {}\n" > + "Context: {}\n" > + "Problematic data: {}\n" > + "Reason: {}".format( > + e.encoding, > + "'{}'".format(filename) if macro_linenr is None else > + "output from macro at {}:{}".format(filename, macro_linenr), > + e.object[max(e.start - 40, 0):e.end + 40], > + e.object[e.start:e.end], > + e.reason)) > + > + > +def _warn_verbose_deprecated(fn_name): > + sys.stderr.write( > + "Deprecation warning: {0}()'s 'verbose' argument has no effect. Since " > + "Kconfiglib 12.0.0, the message is returned from {0}() instead, " > + "and is always generated. Do e.g. print(kconf.{0}()) if you want to " > + "want to show a message like \"Loaded configuration '.config'\" on " > + "stdout. The old API required ugly hacks to reuse messages in " > + "configuration interfaces.\n".format(fn_name)) > + > + > +# Predefined preprocessor functions > + > + > +def _filename_fn(kconf, _): > + return kconf.filename > + > + > +def _lineno_fn(kconf, _): > + return str(kconf.linenr) > + > + > +def _info_fn(kconf, _, msg): > + print("{}:{}: {}".format(kconf.filename, kconf.linenr, msg)) > + > + return "" > + > + > +def _warning_if_fn(kconf, _, cond, msg): > + if cond == "y": > + kconf._warn(msg, kconf.filename, kconf.linenr) > + > + return "" > + > + > +def _error_if_fn(kconf, _, cond, msg): > + if cond == "y": > + raise KconfigError("{}:{}: {}".format( > + kconf.filename, kconf.linenr, msg)) > + > + return "" > + > + > +def _shell_fn(kconf, _, command): > + import subprocess # Only import as needed, to save some startup time > + > + stdout, stderr = subprocess.Popen( > + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE > + ).communicate() > + > + if not _IS_PY2: > + try: > + stdout = stdout.decode(kconf._encoding) > + stderr = stderr.decode(kconf._encoding) > + except UnicodeDecodeError as e: > + _decoding_error(e, kconf.filename, kconf.linenr) > + > + if stderr: > + kconf._warn("'{}' wrote to stderr: {}".format( > + command, "\n".join(stderr.splitlines())), > + kconf.filename, kconf.linenr) > + > + # Universal newlines with splitlines() (to prevent e.g. stray \r's in > + # command output on Windows), trailing newline removal, and > + # newline-to-space conversion. > + # > + # On Python 3 versions before 3.6, it's not possible to specify the > + # encoding when passing universal_newlines=True to Popen() (the 'encoding' > + # parameter was added in 3.6), so we do this manual version instead. > + return "\n".join(stdout.splitlines()).rstrip("\n").replace("\n", " ") > + > +# > +# Global constants > +# > + > +TRI_TO_STR = { > + 0: "n", > + 1: "m", > + 2: "y", > +} > + > +STR_TO_TRI = { > + "n": 0, > + "m": 1, > + "y": 2, > +} > + > +# Constant representing that there's no cached choice selection. This is > +# distinct from a cached None (no selection). Any object that's not None or a > +# Symbol will do. We test this with 'is'. > +_NO_CACHED_SELECTION = 0 > + > +# Are we running on Python 2? > +_IS_PY2 = sys.version_info[0] < 3 > + > +try: > + _UNAME_RELEASE = os.uname()[2] > +except AttributeError: > + # Only import as needed, to save some startup time > + import platform > + _UNAME_RELEASE = platform.uname()[2] > + > +# The token and type constants below are safe to test with 'is', which is a bit > +# faster (~30% faster on my machine, and a few % faster for total parsing > +# time), even without assuming Python's small integer optimization (which > +# caches small integer objects). The constants end up pointing to unique > +# integer objects, and since we consistently refer to them via the names below, > +# we always get the same object. > +# > +# Client code should use == though. > + > +# Tokens, with values 1, 2, ... . Avoiding 0 simplifies some checks by making > +# all tokens except empty strings truthy. > +( > + _T_ALLNOCONFIG_Y, > + _T_AND, > + _T_BOOL, > + _T_CHOICE, > + _T_CLOSE_PAREN, > + _T_COMMENT, > + _T_CONFIG, > + _T_DEFAULT, > + _T_DEFCONFIG_LIST, > + _T_DEF_BOOL, > + _T_DEF_HEX, > + _T_DEF_INT, > + _T_DEF_STRING, > + _T_DEF_TRISTATE, > + _T_DEPENDS, > + _T_ENDCHOICE, > + _T_ENDIF, > + _T_ENDMENU, > + _T_ENV, > + _T_EQUAL, > + _T_GREATER, > + _T_GREATER_EQUAL, > + _T_HELP, > + _T_HEX, > + _T_IF, > + _T_IMPLY, > + _T_INT, > + _T_LESS, > + _T_LESS_EQUAL, > + _T_MAINMENU, > + _T_MENU, > + _T_MENUCONFIG, > + _T_MODULES, > + _T_NOT, > + _T_ON, > + _T_OPEN_PAREN, > + _T_OPTION, > + _T_OPTIONAL, > + _T_OR, > + _T_ORSOURCE, > + _T_OSOURCE, > + _T_PROMPT, > + _T_RANGE, > + _T_RSOURCE, > + _T_SELECT, > + _T_SOURCE, > + _T_STRING, > + _T_TRISTATE, > + _T_UNEQUAL, > + _T_VISIBLE, > +) = range(1, 51) > + > +# Keyword to token map, with the get() method assigned directly as a small > +# optimization > +_get_keyword = { > + "---help---": _T_HELP, > + "allnoconfig_y": _T_ALLNOCONFIG_Y, > + "bool": _T_BOOL, > + "boolean": _T_BOOL, > + "choice": _T_CHOICE, > + "comment": _T_COMMENT, > + "config": _T_CONFIG, > + "def_bool": _T_DEF_BOOL, > + "def_hex": _T_DEF_HEX, > + "def_int": _T_DEF_INT, > + "def_string": _T_DEF_STRING, > + "def_tristate": _T_DEF_TRISTATE, > + "default": _T_DEFAULT, > + "defconfig_list": _T_DEFCONFIG_LIST, > + "depends": _T_DEPENDS, > + "endchoice": _T_ENDCHOICE, > + "endif": _T_ENDIF, > + "endmenu": _T_ENDMENU, > + "env": _T_ENV, > + "grsource": _T_ORSOURCE, # Backwards compatibility > + "gsource": _T_OSOURCE, # Backwards compatibility > + "help": _T_HELP, > + "hex": _T_HEX, > + "if": _T_IF, > + "imply": _T_IMPLY, > + "int": _T_INT, > + "mainmenu": _T_MAINMENU, > + "menu": _T_MENU, > + "menuconfig": _T_MENUCONFIG, > + "modules": _T_MODULES, > + "on": _T_ON, > + "option": _T_OPTION, > + "optional": _T_OPTIONAL, > + "orsource": _T_ORSOURCE, > + "osource": _T_OSOURCE, > + "prompt": _T_PROMPT, > + "range": _T_RANGE, > + "rsource": _T_RSOURCE, > + "select": _T_SELECT, > + "source": _T_SOURCE, > + "string": _T_STRING, > + "tristate": _T_TRISTATE, > + "visible": _T_VISIBLE, > +}.get > + > +# The constants below match the value of the corresponding tokens to remove the > +# need for conversion > + > +# Node types > +MENU = _T_MENU > +COMMENT = _T_COMMENT > + > +# Expression types > +AND = _T_AND > +OR = _T_OR > +NOT = _T_NOT > +EQUAL = _T_EQUAL > +UNEQUAL = _T_UNEQUAL > +LESS = _T_LESS > +LESS_EQUAL = _T_LESS_EQUAL > +GREATER = _T_GREATER > +GREATER_EQUAL = _T_GREATER_EQUAL > + > +REL_TO_STR = { > + EQUAL: "=", > + UNEQUAL: "!=", > + LESS: "<", > + LESS_EQUAL: "<=", > + GREATER: ">", > + GREATER_EQUAL: ">=", > +} > + > +# Symbol/choice types. UNKNOWN is 0 (falsy) to simplify some checks. > +# Client code shouldn't rely on it though, as it was non-zero in > +# older versions. > +UNKNOWN = 0 > +BOOL = _T_BOOL > +TRISTATE = _T_TRISTATE > +STRING = _T_STRING > +INT = _T_INT > +HEX = _T_HEX > + > +TYPE_TO_STR = { > + UNKNOWN: "unknown", > + BOOL: "bool", > + TRISTATE: "tristate", > + STRING: "string", > + INT: "int", > + HEX: "hex", > +} > + > +# Used in comparisons. 0 means the base is inferred from the format of the > +# string. > +_TYPE_TO_BASE = { > + HEX: 16, > + INT: 10, > + STRING: 0, > + UNKNOWN: 0, > +} > + > +# def_bool -> BOOL, etc. > +_DEF_TOKEN_TO_TYPE = { > + _T_DEF_BOOL: BOOL, > + _T_DEF_HEX: HEX, > + _T_DEF_INT: INT, > + _T_DEF_STRING: STRING, > + _T_DEF_TRISTATE: TRISTATE, > +} > + > +# Tokens after which strings are expected. This is used to tell strings from > +# constant symbol references during tokenization, both of which are enclosed in > +# quotes. > +# > +# Identifier-like lexemes ("missing quotes") are also treated as strings after > +# these tokens. _T_CHOICE is included to avoid symbols being registered for > +# named choices. > +_STRING_LEX = frozenset({ > + _T_BOOL, > + _T_CHOICE, > + _T_COMMENT, > + _T_HEX, > + _T_INT, > + _T_MAINMENU, > + _T_MENU, > + _T_ORSOURCE, > + _T_OSOURCE, > + _T_PROMPT, > + _T_RSOURCE, > + _T_SOURCE, > + _T_STRING, > + _T_TRISTATE, > +}) > + > +# Various sets for quick membership tests. Gives a single global lookup and > +# avoids creating temporary dicts/tuples. > + > +_TYPE_TOKENS = frozenset({ > + _T_BOOL, > + _T_TRISTATE, > + _T_INT, > + _T_HEX, > + _T_STRING, > +}) > + > +_SOURCE_TOKENS = frozenset({ > + _T_SOURCE, > + _T_RSOURCE, > + _T_OSOURCE, > + _T_ORSOURCE, > +}) > + > +_REL_SOURCE_TOKENS = frozenset({ > + _T_RSOURCE, > + _T_ORSOURCE, > +}) > + > +# Obligatory (non-optional) sources > +_OBL_SOURCE_TOKENS = frozenset({ > + _T_SOURCE, > + _T_RSOURCE, > +}) > + > +_BOOL_TRISTATE = frozenset({ > + BOOL, > + TRISTATE, > +}) > + > +_BOOL_TRISTATE_UNKNOWN = frozenset({ > + BOOL, > + TRISTATE, > + UNKNOWN, > +}) > + > +_INT_HEX = frozenset({ > + INT, > + HEX, > +}) > + > +_SYMBOL_CHOICE = frozenset({ > + Symbol, > + Choice, > +}) > + > +_MENU_COMMENT = frozenset({ > + MENU, > + COMMENT, > +}) > + > +_EQUAL_UNEQUAL = frozenset({ > + EQUAL, > + UNEQUAL, > +}) > + > +_RELATIONS = frozenset({ > + EQUAL, > + UNEQUAL, > + LESS, > + LESS_EQUAL, > + GREATER, > + GREATER_EQUAL, > +}) > + > +# Helper functions for getting compiled regular expressions, with the needed > +# matching function returned directly as a small optimization. > +# > +# Use ASCII regex matching on Python 3. It's already the default on Python 2. > + > + > +def _re_match(regex): > + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).match > + > + > +def _re_search(regex): > + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).search > + > + > +# Various regular expressions used during parsing > + > +# The initial token on a line. Also eats leading and trailing whitespace, so > +# that we can jump straight to the next token (or to the end of the line if > +# there is only one token). > +# > +# This regex will also fail to match for empty lines and comment lines. > +# > +# '$' is included to detect preprocessor variable assignments with macro > +# expansions in the left-hand side. > +_command_match = _re_match(r"\s*([A-Za-z0-9_$-]+)\s*") > + > +# An identifier/keyword after the first token. Also eats trailing whitespace. > +# '$' is included to detect identifiers containing macro expansions. > +_id_keyword_match = _re_match(r"([A-Za-z0-9_$/.-]+)\s*") > + > +# A fragment in the left-hand side of a preprocessor variable assignment. These > +# are the portions between macro expansions ($(foo)). Macros are supported in > +# the LHS (variable name). > +_assignment_lhs_fragment_match = _re_match("[A-Za-z0-9_-]*") > + > +# The assignment operator and value (right-hand side) in a preprocessor > +# variable assignment > +_assignment_rhs_match = _re_match(r"\s*(=|:=|\+=)\s*(.*)") > + > +# Special characters/strings while expanding a macro ('(', ')', ',', and '$(') > +_macro_special_search = _re_search(r"\(|\)|,|\$\(") > + > +# Special characters/strings while expanding a string (quotes, '\', and '$(') > +_string_special_search = _re_search(r'"|\'|\\|\$\(') > + > +# Special characters/strings while expanding a symbol name. Also includes > +# end-of-line, in case the macro is the last thing on the line. > +_name_special_search = _re_search(r'[^A-Za-z0-9_$/.-]|\$\(|$') > + > +# A valid right-hand side for an assignment to a string symbol in a .config > +# file, including escaped characters. Extracts the contents. > +_conf_string_match = _re_match(r'"((?:[^\\"]|\\.)*)"') > diff --git a/scripts/Kconfiglib/listnewconfig.py b/scripts/Kconfiglib/listnewconfig.py > new file mode 100755 > index 0000000..8276de1 > --- /dev/null > +++ b/scripts/Kconfiglib/listnewconfig.py > @@ -0,0 +1,76 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Lists all user-modifiable symbols that are not given a value in the > +configuration file. Usually, these are new symbols that have been added to the > +Kconfig files. > + > +The default configuration filename is '.config'. A different filename can be > +passed in the KCONFIG_CONFIG environment variable. > +""" > +from __future__ import print_function > + > +import argparse > +import sys > + > +from kconfiglib import Kconfig, BOOL, TRISTATE, INT, HEX, STRING, TRI_TO_STR > + > + > +def main(): > + parser = argparse.ArgumentParser( > + formatter_class=argparse.RawDescriptionHelpFormatter, > + description=__doc__) > + > + parser.add_argument( > + "--show-help", "-l", > + action="store_true", > + help="Show any help texts as well") > + > + parser.add_argument( > + "kconfig", > + metavar="KCONFIG", > + nargs="?", > + default="Kconfig", > + help="Top-level Kconfig file (default: Kconfig)") > + > + args = parser.parse_args() > + > + kconf = Kconfig(args.kconfig, suppress_traceback=True) > + # Make it possible to filter this message out > + print(kconf.load_config(), file=sys.stderr) > + > + for sym in kconf.unique_defined_syms: > + # Only show symbols that can be toggled. Choice symbols are a special > + # case in that sym.assignable will be (2,) (length 1) for visible > + # symbols in choices in y mode, but they can still be toggled by > + # selecting some other symbol. > + if sym.user_value is None and \ > + (len(sym.assignable) > 1 or > + (sym.visibility and (sym.orig_type in (INT, HEX, STRING) or > + sym.choice))): > + > + # Don't reuse the 'config_string' format for bool/tristate symbols, > + # to show n-valued symbols as 'CONFIG_FOO=n' instead of > + # '# CONFIG_FOO is not set'. This matches the C tools. > + if sym.orig_type in (BOOL, TRISTATE): > + s = "{}{}={}\n".format(kconf.config_prefix, sym.name, > + TRI_TO_STR[sym.tri_value]) > + else: > + s = sym.config_string > + > + print(s, end="") > + if args.show_help: > + for node in sym.nodes: > + if node.help is not None: > + # Indent by two spaces. textwrap.indent() is not > + # available in Python 2 (it's 3.3+). > + print("\n".join(" " + line > + for line in node.help.split("\n"))) > + break > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/menuconfig.py b/scripts/Kconfiglib/menuconfig.py > new file mode 100755 > index 0000000..b595a69 > --- /dev/null > +++ b/scripts/Kconfiglib/menuconfig.py > @@ -0,0 +1,3282 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Overview > +======== > + > +A curses-based Python 2/3 menuconfig implementation. The interface should feel > +familiar to people used to mconf ('make menuconfig'). > + > +Supports the same keys as mconf, and also supports a set of keybindings > +inspired by Vi: > + > + J/K : Down/Up > + L : Enter menu/Toggle item > + H : Leave menu > + Ctrl-D/U: Page Down/Page Up > + G/End : Jump to end of list > + g/Home : Jump to beginning of list > + > +[Space] toggles values if possible, and enters menus otherwise. [Enter] works > +the other way around. > + > +The mconf feature where pressing a key jumps to a menu entry with that > +character in it in the current menu isn't supported. A jump-to feature for > +jumping directly to any symbol (including invisible symbols), choice, menu or > +comment (as in a Kconfig 'comment "Foo"') is available instead. > + > +A few different modes are available: > + > + F: Toggle show-help mode, which shows the help text of the currently selected > + item in the window at the bottom of the menu display. This is handy when > + browsing through options. > + > + C: Toggle show-name mode, which shows the symbol name before each symbol menu > + entry > + > + A: Toggle show-all mode, which shows all items, including currently invisible > + items and items that lack a prompt. Invisible items are drawn in a different > + style to make them stand out. > + > + > +Running > +======= > + > +menuconfig.py can be run either as a standalone executable or by calling the > +menuconfig() function with an existing Kconfig instance. The second option is a > +bit inflexible in that it will still load and save .config, etc. > + > +When run in standalone mode, the top-level Kconfig file to load can be passed > +as a command-line argument. With no argument, it defaults to "Kconfig". > + > +The KCONFIG_CONFIG environment variable specifies the .config file to load (if > +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. > + > +When overwriting a configuration file, the old version is saved to > +<filename>.old (e.g. .config.old). > + > +$srctree is supported through Kconfiglib. > + > + > +Color schemes > +============= > + > +It is possible to customize the color scheme by setting the MENUCONFIG_STYLE > +environment variable. For example, setting it to 'aquatic' will enable an > +alternative, less yellow, more 'make menuconfig'-like color scheme, contributed > +by Mitja Horvat (pinkfluid). > + > +This is the current list of built-in styles: > + - default classic Kconfiglib theme with a yellow accent > + - monochrome colorless theme (uses only bold and standout) attributes, > + this style is used if the terminal doesn't support colors > + - aquatic blue-tinted style loosely resembling the lxdialog theme > + > +It is possible to customize the current style by changing colors of UI > +elements on the screen. This is the list of elements that can be stylized: > + > + - path Top row in the main display, with the menu path > + - separator Separator lines between windows. Also used for the top line > + in the symbol information display. > + - list List of items, e.g. the main display > + - selection Style for the selected item > + - inv-list Like list, but for invisible items. Used in show-all mode. > + - inv-selection Like selection, but for invisible items. Used in show-all > + mode. > + - help Help text windows at the bottom of various fullscreen > + dialogs > + - show-help Window showing the help text in show-help mode > + - frame Frame around dialog boxes > + - body Body of dialog boxes > + - edit Edit box in pop-up dialogs > + - jump-edit Edit box in jump-to dialog > + - text Symbol information text > + > +The color definition is a comma separated list of attributes: > + > + - fg:COLOR Set the foreground/background colors. COLOR can be one of > + * or * the basic 16 colors (black, red, green, yellow, blue, > + - bg:COLOR magenta, cyan, white and brighter versions, for example, > + brightred). On terminals that support more than 8 colors, > + you can also directly put in a color number, e.g. fg:123 > + (hexadecimal and octal constants are accepted as well). > + Colors outside the range -1..curses.COLORS-1 (which is > + terminal-dependent) are ignored (with a warning). The COLOR > + can be also specified using a RGB value in the HTML > + notation, for example #RRGGBB. If the terminal supports > + color changing, the color is rendered accurately. > + Otherwise, the visually nearest color is used. > + > + If the background or foreground color of an element is not > + specified, it defaults to -1, representing the default > + terminal foreground or background color. > + > + Note: On some terminals a bright version of the color > + implies bold. > + - bold Use bold text > + - underline Use underline text > + - standout Standout text attribute (reverse color) > + > +More often than not, some UI elements share the same color definition. In such > +cases the right value may specify an UI element from which the color definition > +will be copied. For example, "separator=help" will apply the current color > +definition for "help" to "separator". > + > +A keyword without the '=' is assumed to be a style template. The template name > +is looked up in the built-in styles list and the style definition is expanded > +in-place. With this, built-in styles can be used as basis for new styles. > + > +For example, take the aquatic theme and give it a red selection bar: > + > +MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red" > + > +If there's an error in the style definition or if a missing style is assigned > +to, the assignment will be ignored, along with a warning being printed on > +stderr. > + > +The 'default' theme is always implicitly parsed first, so the following two > +settings have the same effect: > + > + MENUCONFIG_STYLE="selection=fg:white,bg:red" > + MENUCONFIG_STYLE="default selection=fg:white,bg:red" > + > +If the terminal doesn't support colors, the 'monochrome' theme is used, and > +MENUCONFIG_STYLE is ignored. The assumption is that the environment is broken > +somehow, and that the important thing is to get something usable. > + > + > +Other features > +============== > + > + - Seamless terminal resizing > + > + - No dependencies on *nix, as the 'curses' module is in the Python standard > + library > + > + - Unicode text entry > + > + - Improved information screen compared to mconf: > + > + * Expressions are split up by their top-level &&/|| operands to improve > + readability > + > + * Undefined symbols in expressions are pointed out > + > + * Menus and comments have information displays > + > + * Kconfig definitions are printed > + > + * The include path is shown, listing the locations of the 'source' > + statements that included the Kconfig file of the symbol (or other > + item) > + > + > +Limitations > +=========== > + > +Doesn't work out of the box on Windows, but can be made to work with > + > + pip install windows-curses > + > +See the https://github.com/zephyrproject-rtos/windows-curses repository. > +""" > +from __future__ import print_function > + > +import os > +import sys > + > +_IS_WINDOWS = os.name == "nt" # Are we running on Windows? > + > +try: > + import curses > +except ImportError as e: > + if not _IS_WINDOWS: > + raise > + sys.exit("""\ > +menuconfig failed to import the standard Python 'curses' library. Try > +installing a package like windows-curses > +(https://github.com/zephyrproject-rtos/windows-curses) by running this command > +in cmd.exe: > + > + pip install windows-curses > + > +Starting with Kconfiglib 13.0.0, windows-curses is no longer automatically > +installed when installing Kconfiglib via pip on Windows (because it breaks > +installation on MSYS2). > + > +Exception: > +{}: {}""".format(type(e).__name__, e)) > + > +import errno > +import locale > +import re > +import textwrap > + > +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ > + BOOL, TRISTATE, STRING, INT, HEX, \ > + AND, OR, \ > + expr_str, expr_value, split_expr, \ > + standard_sc_expr_str, \ > + TRI_TO_STR, TYPE_TO_STR, \ > + standard_kconfig, standard_config_filename > + > + > +# > +# Configuration variables > +# > + > +# If True, try to change LC_CTYPE to a UTF-8 locale if it is set to the C > +# locale (which implies ASCII). This fixes curses Unicode I/O issues on systems > +# with bad defaults. ncurses configures itself from the locale settings. > +# > +# Related PEP: https://www.python.org/dev/peps/pep-0538/ > +_CHANGE_C_LC_CTYPE_TO_UTF8 = True > + > +# How many steps an implicit submenu will be indented. Implicit submenus are > +# created when an item depends on the symbol before it. Note that symbols > +# defined with 'menuconfig' create a separate menu instead of indenting. > +_SUBMENU_INDENT = 4 > + > +# Number of steps for Page Up/Down to jump > +_PG_JUMP = 6 > + > +# Height of the help window in show-help mode > +_SHOW_HELP_HEIGHT = 8 > + > +# How far the cursor needs to be from the edge of the window before it starts > +# to scroll. Used for the main menu display, the information display, the > +# search display, and for text boxes. > +_SCROLL_OFFSET = 5 > + > +# Minimum width of dialogs that ask for text input > +_INPUT_DIALOG_MIN_WIDTH = 30 > + > +# Number of arrows pointing up/down to draw when a window is scrolled > +_N_SCROLL_ARROWS = 14 > + > +# Lines of help text shown at the bottom of the "main" display > +_MAIN_HELP_LINES = """ > +[Space/Enter] Toggle/enter [ESC] Leave menu [S] Save > +[O] Load [?] Symbol info [/] Jump to symbol > +[F] Toggle show-help mode [C] Toggle show-name mode [A] Toggle show-all mode > +[Q] Quit (prompts for save) [D] Save minimal config (advanced) > +"""[1:-1].split("\n") > + > +# Lines of help text shown at the bottom of the information dialog > +_INFO_HELP_LINES = """ > +[ESC/q] Return to menu [/] Jump to symbol > +"""[1:-1].split("\n") > + > +# Lines of help text shown at the bottom of the search dialog > +_JUMP_TO_HELP_LINES = """ > +Type text to narrow the search. Regexes are supported (via Python's 're' > +module). The up/down cursor keys step in the list. [Enter] jumps to the > +selected symbol. [ESC] aborts the search. Type multiple space-separated > +strings/regexes to find entries that match all of them. Type Ctrl-F to > +view the help of the selected item without leaving the dialog. > +"""[1:-1].split("\n") > + > +# > +# Styling > +# > + > +_STYLES = { > + "default": """ > + path=fg:black,bg:white,bold > + separator=fg:black,bg:yellow,bold > + list=fg:black,bg:white > + selection=fg:white,bg:blue,bold > + inv-list=fg:red,bg:white > + inv-selection=fg:red,bg:blue > + help=path > + show-help=list > + frame=fg:black,bg:yellow,bold > + body=fg:white,bg:black > + edit=fg:white,bg:blue > + jump-edit=edit > + text=list > + """, > + > + # This style is forced on terminals that do no support colors > + "monochrome": """ > + path=bold > + separator=bold,standout > + list= > + selection=bold,standout > + inv-list=bold > + inv-selection=bold,standout > + help=bold > + show-help= > + frame=bold,standout > + body= > + edit=standout > + jump-edit= > + text= > + """, > + > + # Blue-tinted style loosely resembling lxdialog > + "aquatic": """ > + path=fg:white,bg:blue > + separator=fg:white,bg:cyan > + help=path > + frame=fg:white,bg:cyan > + body=fg:white,bg:blue > + edit=fg:black,bg:white > + """ > +} > + > +_NAMED_COLORS = { > + # Basic colors > + "black": curses.COLOR_BLACK, > + "red": curses.COLOR_RED, > + "green": curses.COLOR_GREEN, > + "yellow": curses.COLOR_YELLOW, > + "blue": curses.COLOR_BLUE, > + "magenta": curses.COLOR_MAGENTA, > + "cyan": curses.COLOR_CYAN, > + "white": curses.COLOR_WHITE, > + > + # Bright versions > + "brightblack": curses.COLOR_BLACK + 8, > + "brightred": curses.COLOR_RED + 8, > + "brightgreen": curses.COLOR_GREEN + 8, > + "brightyellow": curses.COLOR_YELLOW + 8, > + "brightblue": curses.COLOR_BLUE + 8, > + "brightmagenta": curses.COLOR_MAGENTA + 8, > + "brightcyan": curses.COLOR_CYAN + 8, > + "brightwhite": curses.COLOR_WHITE + 8, > + > + # Aliases > + "purple": curses.COLOR_MAGENTA, > + "brightpurple": curses.COLOR_MAGENTA + 8, > +} > + > + > +def _rgb_to_6cube(rgb): > + # Converts an 888 RGB color to a 3-tuple (nice in that it's hashable) > + # representing the closest xterm 256-color 6x6x6 color cube color. > + # > + # The xterm 256-color extension uses a RGB color palette with components in > + # the range 0-5 (a 6x6x6 cube). The catch is that the mapping is nonlinear. > + # Index 0 in the 6x6x6 cube is mapped to 0, index 1 to 95, then 135, 175, > + # etc., in increments of 40. See the links below: > + # > + # https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg > + # https://github.com/tmux/tmux/blob/master/colour.c > + > + # 48 is the middle ground between 0 and 95. > + return tuple(0 if x < 48 else int(round(max(1, (x - 55)/40))) for x in rgb) > + > + > +def _6cube_to_rgb(r6g6b6): > + # Returns the 888 RGB color for a 666 xterm color cube index > + > + return tuple(0 if x == 0 else 40*x + 55 for x in r6g6b6) > + > + > +def _rgb_to_gray(rgb): > + # Converts an 888 RGB color to the index of an xterm 256-color grayscale > + # color with approx. the same perceived brightness > + > + # Calculate the luminance (gray intensity) of the color. See > + # https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color > + # and > + # https://www.w3.org/TR/AERT/#color-contrast > + luma = 0.299*rgb[0] + 0.587*rgb[1] + 0.114*rgb[2] > + > + # Closest index in the grayscale palette, which starts at RGB 0x080808, > + # with stepping 0x0A0A0A > + index = int(round((luma - 8)/10)) > + > + # Clamp the index to 0-23, corresponding to 232-255 > + return max(0, min(index, 23)) > + > + > +def _gray_to_rgb(index): > + # Convert a grayscale index to its closet single RGB component > + > + return 3*(10*index + 8,) # Returns a 3-tuple > + > + > +# Obscure Python: We never pass a value for rgb2index, and it keeps pointing to > +# the same dict. This avoids a global. > +def _alloc_rgb(rgb, rgb2index={}): > + # Initialize a new entry in the xterm palette to the given RGB color, > + # returning its index. If the color has already been initialized, the index > + # of the existing entry is returned. > + # > + # ncurses is palette-based, so we need to overwrite palette entries to make > + # new colors. > + # > + # The colors from 0 to 15 are user-defined, and there's no way to query > + # their RGB values, so we better leave them untouched. Also leave any > + # hypothetical colors above 255 untouched (though we're unlikely to > + # allocate that many colors anyway). > + > + if rgb in rgb2index: > + return rgb2index[rgb] > + > + # Many terminals allow the user to customize the first 16 colors. Avoid > + # changing their values. > + color_index = 16 + len(rgb2index) > + if color_index >= 256: > + _warn("Unable to allocate new RGB color ", rgb, ". Too many colors " > + "allocated.") > + return 0 > + > + # Map each RGB component from the range 0-255 to the range 0-1000, which is > + # what curses uses > + curses.init_color(color_index, *(int(round(1000*x/255)) for x in rgb)) > + rgb2index[rgb] = color_index > + > + return color_index > + > + > +def _color_from_num(num): > + # Returns the index of a color that looks like color 'num' in the xterm > + # 256-color palette (but that might not be 'num', if we're redefining > + # colors) > + > + # - _alloc_rgb() won't touch the first 16 colors or any (hypothetical) > + # colors above 255, so we can always return them as-is > + # > + # - If the terminal doesn't support changing color definitions, or if > + # curses.COLORS < 256, _alloc_rgb() won't touch any color, and all colors > + # can be returned as-is > + if num < 16 or num > 255 or not curses.can_change_color() or \ > + curses.COLORS < 256: > + return num > + > + # _alloc_rgb() might redefine colors, so emulate the xterm 256-color > + # palette by allocating new colors instead of returning color numbers > + # directly > + > + if num < 232: > + num -= 16 > + return _alloc_rgb(_6cube_to_rgb(((num//36)%6, (num//6)%6, num%6))) > + > + return _alloc_rgb(_gray_to_rgb(num - 232)) > + > + > +def _color_from_rgb(rgb): > + # Returns the index of a color matching the 888 RGB color 'rgb'. The > + # returned color might be an ~exact match or an approximation, depending on > + # terminal capabilities. > + > + # Calculates the Euclidean distance between two RGB colors > + def dist(r1, r2): return sum((x - y)**2 for x, y in zip(r1, r2)) > + > + if curses.COLORS >= 256: > + # Assume we're dealing with xterm's 256-color extension > + > + if curses.can_change_color(): > + # Best case -- the terminal supports changing palette entries via > + # curses.init_color(). Initialize an unused palette entry and > + # return it. > + return _alloc_rgb(rgb) > + > + # Second best case -- pick between the xterm 256-color extension colors > + > + # Closest 6-cube "color" color > + c6 = _rgb_to_6cube(rgb) > + # Closest gray color > + gray = _rgb_to_gray(rgb) > + > + if dist(rgb, _6cube_to_rgb(c6)) < dist(rgb, _gray_to_rgb(gray)): > + # Use the "color" color from the 6x6x6 color palette. Calculate the > + # color number from the 6-cube index triplet. > + return 16 + 36*c6[0] + 6*c6[1] + c6[2] > + > + # Use the color from the gray palette > + return 232 + gray > + > + # Terminal not in xterm 256-color mode. This is probably the best we can > + # do, or is it? Submit patches. :) > + min_dist = float('inf') > + best = -1 > + for color in range(curses.COLORS): > + # ncurses uses the range 0..1000. Scale that down to 0..255. > + d = dist(rgb, tuple(int(round(255*c/1000)) > + for c in curses.color_content(color))) > + if d < min_dist: > + min_dist = d > + best = color > + > + return best > + > + > +def _parse_style(style_str, parsing_default): > + # Parses a string with '<element>=<style>' assignments. Anything not > + # containing '=' is assumed to be a reference to a built-in style, which is > + # treated as if all the assignments from the style were inserted at that > + # point in the string. > + # > + # The parsing_default flag is set to True when we're implicitly parsing the > + # 'default'/'monochrome' style, to prevent warnings. > + > + for sline in style_str.split(): > + # Words without a "=" character represents a style template > + if "=" in sline: > + key, data = sline.split("=", 1) > + > + # The 'default' style template is assumed to define all keys. We > + # run _style_to_curses() for non-existing keys as well, so that we > + # print warnings for errors to the right of '=' for those too. > + if key not in _style and not parsing_default: > + _warn("Ignoring non-existent style", key) > + > + # If data is a reference to another key, copy its style > + if data in _style: > + _style[key] = _style[data] > + else: > + _style[key] = _style_to_curses(data) > + > + elif sline in _STYLES: > + # Recursively parse style template. Ignore styles that don't exist, > + # for backwards/forwards compatibility. > + _parse_style(_STYLES[sline], parsing_default) > + > + else: > + _warn("Ignoring non-existent style template", sline) > + > +# Dictionary mapping element types to the curses attributes used to display > +# them > +_style = {} > + > + > +def _style_to_curses(style_def): > + # Parses a style definition string (<element>=<style>), returning > + # a (fg_color, bg_color, attributes) tuple. > + > + def parse_color(color_def): > + color_def = color_def.split(":", 1)[1] > + > + # HTML format, #RRGGBB > + if re.match("#[A-Fa-f0-9]{6}", color_def): > + return _color_from_rgb(( > + int(color_def[1:3], 16), > + int(color_def[3:5], 16), > + int(color_def[5:7], 16))) > + > + if color_def in _NAMED_COLORS: > + color_num = _color_from_num(_NAMED_COLORS[color_def]) > + else: > + try: > + color_num = _color_from_num(int(color_def, 0)) > + except ValueError: > + _warn("Ignoring color", color_def, "that's neither " > + "predefined nor a number") > + return -1 > + > + if not -1 <= color_num < curses.COLORS: > + _warn("Ignoring color {}, which is outside the range " > + "-1..curses.COLORS-1 (-1..{})" > + .format(color_def, curses.COLORS - 1)) > + return -1 > + > + return color_num > + > + fg_color = -1 > + bg_color = -1 > + attrs = 0 > + > + if style_def: > + for field in style_def.split(","): > + if field.startswith("fg:"): > + fg_color = parse_color(field) > + elif field.startswith("bg:"): > + bg_color = parse_color(field) > + elif field == "bold": > + # A_BOLD tends to produce faint and hard-to-read text on the > + # Windows console, especially with the old color scheme, before > + # the introduction of > + # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/ > + attrs |= curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD > + elif field == "standout": > + attrs |= curses.A_STANDOUT > + elif field == "underline": > + attrs |= curses.A_UNDERLINE > + else: > + _warn("Ignoring unknown style attribute", field) > + > + return _style_attr(fg_color, bg_color, attrs) > + > + > +def _init_styles(): > + if curses.has_colors(): > + try: > + curses.use_default_colors() > + except curses.error: > + # Ignore errors on funky terminals that support colors but not > + # using default colors. Worst it can do is break transparency and > + # the like. Ran across this with the MSYS2/winpty setup in > + # https://github.com/msys2/MINGW-packages/issues/5823, though there > + # seems to be a lot of general brokenness there. > + pass > + > + # Use the 'default' theme as the base, and add any user-defined style > + # settings from the environment > + _parse_style("default", True) > + if "MENUCONFIG_STYLE" in os.environ: > + _parse_style(os.environ["MENUCONFIG_STYLE"], False) > + else: > + # Force the 'monochrome' theme if the terminal doesn't support colors. > + # MENUCONFIG_STYLE is likely to mess things up here (though any colors > + # would be ignored), so ignore it. > + _parse_style("monochrome", True) > + > + > +# color_attribs holds the color pairs we've already created, indexed by a > +# (<foreground color>, <background color>) tuple. > +# > +# Obscure Python: We never pass a value for color_attribs, and it keeps > +# pointing to the same dict. This avoids a global. > +def _style_attr(fg_color, bg_color, attribs, color_attribs={}): > + # Returns an attribute with the specified foreground and background color > + # and the attributes in 'attribs'. Reuses color pairs already created if > + # possible, and creates a new color pair otherwise. > + # > + # Returns 'attribs' if colors aren't supported. > + > + if not curses.has_colors(): > + return attribs > + > + if (fg_color, bg_color) not in color_attribs: > + # Create new color pair. Color pair number 0 is hardcoded and cannot be > + # changed, hence the +1s. > + curses.init_pair(len(color_attribs) + 1, fg_color, bg_color) > + color_attribs[(fg_color, bg_color)] = \ > + curses.color_pair(len(color_attribs) + 1) > + > + return color_attribs[(fg_color, bg_color)] | attribs > + > + > +# > +# Main application > +# > + > + > +def _main(): > + menuconfig(standard_kconfig(__doc__)) > + > + > +def menuconfig(kconf): > + """ > + Launches the configuration interface, returning after the user exits. > + > + kconf: > + Kconfig instance to be configured > + """ > + global _kconf > + global _conf_filename > + global _conf_changed > + global _minconf_filename > + global _show_all > + > + _kconf = kconf > + > + # Filename to save configuration to > + _conf_filename = standard_config_filename() > + > + # Load existing configuration and set _conf_changed True if it is outdated > + _conf_changed = _load_config() > + > + # Filename to save minimal configuration to > + _minconf_filename = "defconfig" > + > + # Any visible items in the top menu? > + _show_all = False > + if not _shown_nodes(kconf.top_node): > + # Nothing visible. Start in show-all mode and try again. > + _show_all = True > + if not _shown_nodes(kconf.top_node): > + # Give up. The implementation relies on always having a selected > + # node. > + print("Empty configuration -- nothing to configure.\n" > + "Check that environment variables are set properly.") > + return > + > + # Disable warnings. They get mangled in curses mode, and we deal with > + # errors ourselves. > + kconf.warn = False > + > + try: > + # Make curses use the locale settings specified in the environment > + locale.setlocale(locale.LC_ALL, "") > + except locale.Error: > + # fall back to the default locale > + locale.setlocale(locale.LC_ALL, "C") > + > + # Try to fix Unicode issues on systems with bad defaults > + if _CHANGE_C_LC_CTYPE_TO_UTF8: > + _change_c_lc_ctype_to_utf8() > + > + # Get rid of the delay between pressing ESC and jumping to the parent menu, > + # unless the user has set ESCDELAY (see ncurses(3)). This makes the UI much > + # smoother to work with. > + # > + # Note: This is strictly pretty iffy, since escape codes for e.g. cursor > + # keys start with ESC, but I've never seen it cause problems in practice > + # (probably because it's unlikely that the escape code for a key would get > + # split up across read()s, at least with a terminal emulator). Please > + # report if you run into issues. Some suitable small default value could be > + # used here instead in that case. Maybe it's silly to not put in the > + # smallest imperceptible delay here already, though I don't like guessing. > + # > + # (From a quick glance at the ncurses source code, ESCDELAY might only be > + # relevant for mouse events there, so maybe escapes are assumed to arrive > + # in one piece already...) > + os.environ.setdefault("ESCDELAY", "0") > + > + # Enter curses mode. _menuconfig() returns a string to print on exit, after > + # curses has been de-initialized. > + print(curses.wrapper(_menuconfig)) > + > + > +def _load_config(): > + # Loads any existing .config file. See the Kconfig.load_config() docstring. > + # > + # Returns True if .config is missing or outdated. We always prompt for > + # saving the configuration in that case. > + > + print(_kconf.load_config()) > + if not os.path.exists(_conf_filename): > + # No .config > + return True > + > + return _needs_save() > + > + > +def _needs_save(): > + # Returns True if a just-loaded .config file is outdated (would get > + # modified when saving) > + > + if _kconf.missing_syms: > + # Assignments to undefined symbols in the .config > + return True > + > + for sym in _kconf.unique_defined_syms: > + if sym.user_value is None: > + if sym.config_string: > + # Unwritten symbol > + return True > + elif sym.orig_type in (BOOL, TRISTATE): > + if sym.tri_value != sym.user_value: > + # Written bool/tristate symbol, new value > + return True > + elif sym.str_value != sym.user_value: > + # Written string/int/hex symbol, new value > + return True > + > + # No need to prompt for save > + return False > + > + > +# Global variables used below: > +# > +# _stdscr: > +# stdscr from curses > +# > +# _cur_menu: > +# Menu node of the menu (or menuconfig symbol, or choice) currently being > +# shown > +# > +# _shown: > +# List of items in _cur_menu that are shown (ignoring scrolling). In > +# show-all mode, this list contains all items in _cur_menu. Otherwise, it > +# contains just the visible items. > +# > +# _sel_node_i: > +# Index in _shown of the currently selected node > +# > +# _menu_scroll: > +# Index in _shown of the top row of the main display > +# > +# _parent_screen_rows: > +# List/stack of the row numbers that the selections in the parent menus > +# appeared on. This is used to prevent the scrolling from jumping around > +# when going in and out of menus. > +# > +# _show_help/_show_name/_show_all: > +# If True, the corresponding mode is on. See the module docstring. > +# > +# _conf_filename: > +# File to save the configuration to > +# > +# _minconf_filename: > +# File to save minimal configurations to > +# > +# _conf_changed: > +# True if the configuration has been changed. If False, we don't bother > +# showing the save-and-quit dialog. > +# > +# We reset this to False whenever the configuration is saved explicitly > +# from the save dialog. > + > + > +def _menuconfig(stdscr): > + # Logic for the main display, with the list of symbols, etc. > + > + global _stdscr > + global _conf_filename > + global _conf_changed > + global _minconf_filename > + global _show_help > + global _show_name > + > + _stdscr = stdscr > + > + _init() > + > + while True: > + _draw_main() > + curses.doupdate() > + > + > + c = _getch_compat(_menu_win) > + > + if c == curses.KEY_RESIZE: > + _resize_main() > + > + elif c in (curses.KEY_DOWN, "j", "J"): > + _select_next_menu_entry() > + > + elif c in (curses.KEY_UP, "k", "K"): > + _select_prev_menu_entry() > + > + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D > + # Keep it simple. This way we get sane behavior for small windows, > + # etc., for free. > + for _ in range(_PG_JUMP): > + _select_next_menu_entry() > + > + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U > + for _ in range(_PG_JUMP): > + _select_prev_menu_entry() > + > + elif c in (curses.KEY_END, "G"): > + _select_last_menu_entry() > + > + elif c in (curses.KEY_HOME, "g"): > + _select_first_menu_entry() > + > + elif c == " ": > + # Toggle the node if possible > + sel_node = _shown[_sel_node_i] > + if not _change_node(sel_node): > + _enter_menu(sel_node) > + > + elif c in (curses.KEY_RIGHT, "\n", "l", "L"): > + # Enter the node if possible > + sel_node = _shown[_sel_node_i] > + if not _enter_menu(sel_node): > + _change_node(sel_node) > + > + elif c in ("n", "N"): > + _set_sel_node_tri_val(0) > + > + elif c in ("m", "M"): > + _set_sel_node_tri_val(1) > + > + elif c in ("y", "Y"): > + _set_sel_node_tri_val(2) > + > + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, > + "\x1B", "h", "H"): # \x1B = ESC > + > + if c == "\x1B" and _cur_menu is _kconf.top_node: > + res = _quit_dialog() > + if res: > + return res > + else: > + _leave_menu() > + > + elif c in ("o", "O"): > + _load_dialog() > + > + elif c in ("s", "S"): > + filename = _save_dialog(_kconf.write_config, _conf_filename, > + "configuration") > + if filename: > + _conf_filename = filename > + _conf_changed = False > + > + elif c in ("d", "D"): > + filename = _save_dialog(_kconf.write_min_config, _minconf_filename, > + "minimal configuration") > + if filename: > + _minconf_filename = filename > + > + elif c == "/": > + _jump_to_dialog() > + # The terminal might have been resized while the fullscreen jump-to > + # dialog was open > + _resize_main() > + > + elif c == "?": > + _info_dialog(_shown[_sel_node_i], False) > + # The terminal might have been resized while the fullscreen info > + # dialog was open > + _resize_main() > + > + elif c in ("f", "F"): > + _show_help = not _show_help > + _set_style(_help_win, "show-help" if _show_help else "help") > + _resize_main() > + > + elif c in ("c", "C"): > + _show_name = not _show_name > + > + elif c in ("a", "A"): > + _toggle_show_all() > + > + elif c in ("q", "Q"): > + res = _quit_dialog() > + if res: > + return res > + > + > +def _quit_dialog(): > + if not _conf_changed: > + return "No changes to save (for '{}')".format(_conf_filename) > + > + while True: > + c = _key_dialog( > + "Quit", > + " Save configuration?\n" > + "\n" > + "(Y)es (N)o (C)ancel", > + "ync") > + > + if c is None or c == "c": > + return None > + > + if c == "y": > + # Returns a message to print > + msg = _try_save(_kconf.write_config, _conf_filename, "configuration") > + if msg: > + return msg > + > + elif c == "n": > + return "Configuration ({}) was not saved".format(_conf_filename) > + > + > +def _init(): > + # Initializes the main display with the list of symbols, etc. Also does > + # misc. global initialization that needs to happen after initializing > + # curses. > + > + global _ERASE_CHAR > + > + global _path_win > + global _top_sep_win > + global _menu_win > + global _bot_sep_win > + global _help_win > + > + global _parent_screen_rows > + global _cur_menu > + global _shown > + global _sel_node_i > + global _menu_scroll > + > + global _show_help > + global _show_name > + > + # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes > + # backspace work with TERM=vt100. That makes it likely to work in sane > + # environments. > + _ERASE_CHAR = curses.erasechar() > + if sys.version_info[0] >= 3: > + # erasechar() returns a one-byte bytes object on Python 3. This sets > + # _ERASE_CHAR to a blank string if it can't be decoded, which should be > + # harmless. > + _ERASE_CHAR = _ERASE_CHAR.decode("utf-8", "ignore") > + > + _init_styles() > + > + # Hide the cursor > + _safe_curs_set(0) > + > + # Initialize windows > + > + # Top row, with menu path > + _path_win = _styled_win("path") > + > + # Separator below menu path, with title and arrows pointing up > + _top_sep_win = _styled_win("separator") > + > + # List of menu entries with symbols, etc. > + _menu_win = _styled_win("list") > + _menu_win.keypad(True) > + > + # Row below menu list, with arrows pointing down > + _bot_sep_win = _styled_win("separator") > + > + # Help window with keys at the bottom. Shows help texts in show-help mode. > + _help_win = _styled_win("help") > + > + # The rows we'd like the nodes in the parent menus to appear on. This > + # prevents the scroll from jumping around when going in and out of menus. > + _parent_screen_rows = [] > + > + # Initial state > + > + _cur_menu = _kconf.top_node > + _shown = _shown_nodes(_cur_menu) > + _sel_node_i = _menu_scroll = 0 > + > + _show_help = _show_name = False > + > + # Give windows their initial size > + _resize_main() > + > + > +def _resize_main(): > + # Resizes the main display, with the list of symbols, etc., to fill the > + # terminal > + > + global _menu_scroll > + > + screen_height, screen_width = _stdscr.getmaxyx() > + > + _path_win.resize(1, screen_width) > + _top_sep_win.resize(1, screen_width) > + _bot_sep_win.resize(1, screen_width) > + > + help_win_height = _SHOW_HELP_HEIGHT if _show_help else \ > + len(_MAIN_HELP_LINES) > + > + menu_win_height = screen_height - help_win_height - 3 > + > + if menu_win_height >= 1: > + _menu_win.resize(menu_win_height, screen_width) > + _help_win.resize(help_win_height, screen_width) > + > + _top_sep_win.mvwin(1, 0) > + _menu_win.mvwin(2, 0) > + _bot_sep_win.mvwin(2 + menu_win_height, 0) > + _help_win.mvwin(2 + menu_win_height + 1, 0) > + else: > + # Degenerate case. Give up on nice rendering and just prevent errors. > + > + menu_win_height = 1 > + > + _menu_win.resize(1, screen_width) > + _help_win.resize(1, screen_width) > + > + for win in _top_sep_win, _menu_win, _bot_sep_win, _help_win: > + win.mvwin(0, 0) > + > + # Adjust the scroll so that the selected node is still within the window, > + # if needed > + if _sel_node_i - _menu_scroll >= menu_win_height: > + _menu_scroll = _sel_node_i - menu_win_height + 1 > + > + > +def _height(win): > + # Returns the height of 'win' > + > + return win.getmaxyx()[0] > + > + > +def _width(win): > + # Returns the width of 'win' > + > + return win.getmaxyx()[1] > + > + > +def _enter_menu(menu): > + # Makes 'menu' the currently displayed menu. In addition to actual 'menu's, > + # "menu" here includes choices and symbols defined with the 'menuconfig' > + # keyword. > + # > + # Returns False if 'menu' can't be entered. > + > + global _cur_menu > + global _shown > + global _sel_node_i > + global _menu_scroll > + > + if not menu.is_menuconfig: > + return False # Not a menu > + > + shown_sub = _shown_nodes(menu) > + # Never enter empty menus. We depend on having a current node. > + if not shown_sub: > + return False > + > + # Remember where the current node appears on the screen, so we can try > + # to get it to appear in the same place when we leave the menu > + _parent_screen_rows.append(_sel_node_i - _menu_scroll) > + > + # Jump into menu > + _cur_menu = menu > + _shown = shown_sub > + _sel_node_i = _menu_scroll = 0 > + > + if isinstance(menu.item, Choice): > + _select_selected_choice_sym() > + > + return True > + > + > +def _select_selected_choice_sym(): > + # Puts the cursor on the currently selected (y-valued) choice symbol, if > + # any. Does nothing if if the choice has no selection (is not visible/in y > + # mode). > + > + global _sel_node_i > + > + choice = _cur_menu.item > + if choice.selection: > + # Search through all menu nodes to handle choice symbols being defined > + # in multiple locations > + for node in choice.selection.nodes: > + if node in _shown: > + _sel_node_i = _shown.index(node) > + _center_vertically() > + return > + > + > +def _jump_to(node): > + # Jumps directly to the menu node 'node' > + > + global _cur_menu > + global _shown > + global _sel_node_i > + global _menu_scroll > + global _show_all > + global _parent_screen_rows > + > + # Clear remembered menu locations. We might not even have been in the > + # parent menus before. > + _parent_screen_rows = [] > + > + old_show_all = _show_all > + jump_into = (isinstance(node.item, Choice) or node.item == MENU) and \ > + node.list > + > + # If we're jumping to a non-empty choice or menu, jump to the first entry > + # in it instead of jumping to its menu node > + if jump_into: > + _cur_menu = node > + node = node.list > + else: > + _cur_menu = _parent_menu(node) > + > + _shown = _shown_nodes(_cur_menu) > + if node not in _shown: > + # The node wouldn't be shown. Turn on show-all to show it. > + _show_all = True > + _shown = _shown_nodes(_cur_menu) > + > + _sel_node_i = _shown.index(node) > + > + if jump_into and not old_show_all and _show_all: > + # If we're jumping into a choice or menu and were forced to turn on > + # show-all because the first entry wasn't visible, try turning it off. > + # That will land us at the first visible node if there are visible > + # nodes, and is a no-op otherwise. > + _toggle_show_all() > + > + _center_vertically() > + > + # If we're jumping to a non-empty choice, jump to the selected symbol, if > + # any > + if jump_into and isinstance(_cur_menu.item, Choice): > + _select_selected_choice_sym() > + > + > +def _leave_menu(): > + # Jumps to the parent menu of the current menu. Does nothing if we're in > + # the top menu. > + > + global _cur_menu > + global _shown > + global _sel_node_i > + global _menu_scroll > + > + if _cur_menu is _kconf.top_node: > + return > + > + # Jump to parent menu > + parent = _parent_menu(_cur_menu) > + _shown = _shown_nodes(parent) > + _sel_node_i = _shown.index(_cur_menu) > + _cur_menu = parent > + > + # Try to make the menu entry appear on the same row on the screen as it did > + # before we entered the menu. > + > + if _parent_screen_rows: > + # The terminal might have shrunk since we were last in the parent menu > + screen_row = min(_parent_screen_rows.pop(), _height(_menu_win) - 1) > + _menu_scroll = max(_sel_node_i - screen_row, 0) > + else: > + # No saved parent menu locations, meaning we jumped directly to some > + # node earlier > + _center_vertically() > + > + > +def _select_next_menu_entry(): > + # Selects the menu entry after the current one, adjusting the scroll if > + # necessary. Does nothing if we're already at the last menu entry. > + > + global _sel_node_i > + global _menu_scroll > + > + if _sel_node_i < len(_shown) - 1: > + # Jump to the next node > + _sel_node_i += 1 > + > + # If the new node is sufficiently close to the edge of the menu window > + # (as determined by _SCROLL_OFFSET), increase the scroll by one. This > + # gives nice and non-jumpy behavior even when > + # _SCROLL_OFFSET >= _height(_menu_win). > + if _sel_node_i >= _menu_scroll + _height(_menu_win) - _SCROLL_OFFSET \ > + and _menu_scroll < _max_scroll(_shown, _menu_win): > + > + _menu_scroll += 1 > + > + > +def _select_prev_menu_entry(): > + # Selects the menu entry before the current one, adjusting the scroll if > + # necessary. Does nothing if we're already at the first menu entry. > + > + global _sel_node_i > + global _menu_scroll > + > + if _sel_node_i > 0: > + # Jump to the previous node > + _sel_node_i -= 1 > + > + # See _select_next_menu_entry() > + if _sel_node_i < _menu_scroll + _SCROLL_OFFSET: > + _menu_scroll = max(_menu_scroll - 1, 0) > + > + > +def _select_last_menu_entry(): > + # Selects the last menu entry in the current menu > + > + global _sel_node_i > + global _menu_scroll > + > + _sel_node_i = len(_shown) - 1 > + _menu_scroll = _max_scroll(_shown, _menu_win) > + > + > +def _select_first_menu_entry(): > + # Selects the first menu entry in the current menu > + > + global _sel_node_i > + global _menu_scroll > + > + _sel_node_i = _menu_scroll = 0 > + > + > +def _toggle_show_all(): > + # Toggles show-all mode on/off. If turning it off would give no visible > + # items in the current menu, it is left on. > + > + global _show_all > + global _shown > + global _sel_node_i > + global _menu_scroll > + > + # Row on the screen the cursor is on. Preferably we want the same row to > + # stay highlighted. > + old_row = _sel_node_i - _menu_scroll > + > + _show_all = not _show_all > + # List of new nodes to be shown after toggling _show_all > + new_shown = _shown_nodes(_cur_menu) > + > + # Find a good node to select. The selected node might disappear if show-all > + # mode is turned off. > + > + # Select the previously selected node itself if it is still visible. If > + # there are visible nodes before it, select the closest one. > + for node in _shown[_sel_node_i::-1]: > + if node in new_shown: > + _sel_node_i = new_shown.index(node) > + break > + else: > + # No visible nodes before the previously selected node. Select the > + # closest visible node after it instead. > + for node in _shown[_sel_node_i + 1:]: > + if node in new_shown: > + _sel_node_i = new_shown.index(node) > + break > + else: > + # No visible nodes at all, meaning show-all was turned off inside > + # an invisible menu. Don't allow that, as the implementation relies > + # on always having a selected node. > + _show_all = True > + return > + > + _shown = new_shown > + > + # Try to make the cursor stay on the same row in the menu window. This > + # might be impossible if too many nodes have disappeared above the node. > + _menu_scroll = max(_sel_node_i - old_row, 0) > + > + > +def _center_vertically(): > + # Centers the selected node vertically, if possible > + > + global _menu_scroll > + > + _menu_scroll = min(max(_sel_node_i - _height(_menu_win)//2, 0), > + _max_scroll(_shown, _menu_win)) > + > + > +def _draw_main(): > + # Draws the "main" display, with the list of symbols, the header, and the > + # footer. > + # > + # This could be optimized to only update the windows that have actually > + # changed, but keep it simple for now and let curses sort it out. > + > + term_width = _width(_stdscr) > + > + # > + # Update the separator row below the menu path > + # > + > + _top_sep_win.erase() > + > + # Draw arrows pointing up if the symbol window is scrolled down. Draw them > + # before drawing the title, so the title ends up on top for small windows. > + if _menu_scroll > 0: > + _safe_hline(_top_sep_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) > + > + # Add the 'mainmenu' text as the title, centered at the top > + _safe_addstr(_top_sep_win, > + 0, max((term_width - len(_kconf.mainmenu_text))//2, 0), > + _kconf.mainmenu_text) > + > + _top_sep_win.noutrefresh() > + > + # Note: The menu path at the top is deliberately updated last. See below. > + > + # > + # Update the symbol window > + # > + > + _menu_win.erase() > + > + # Draw the _shown nodes starting from index _menu_scroll up to either as > + # many as fit in the window, or to the end of _shown > + for i in range(_menu_scroll, > + min(_menu_scroll + _height(_menu_win), len(_shown))): > + > + node = _shown[i] > + > + # The 'not _show_all' test avoids showing invisible items in red > + # outside show-all mode, which could look confusing/broken. Invisible > + # symbols show up outside show-all mode if an invisible symbol has > + # visible children in an implicit (indented) menu. > + if _visible(node) or not _show_all: > + style = _style["selection" if i == _sel_node_i else "list"] > + else: > + style = _style["inv-selection" if i == _sel_node_i else "inv-list"] > + > + _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style) > + > + _menu_win.noutrefresh() > + > + # > + # Update the bottom separator window > + # > + > + _bot_sep_win.erase() > + > + # Draw arrows pointing down if the symbol window is scrolled up > + if _menu_scroll < _max_scroll(_shown, _menu_win): > + _safe_hline(_bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) > + > + # Indicate when show-name/show-help/show-all mode is enabled > + enabled_modes = [] > + if _show_help: > + enabled_modes.append("show-help (toggle with [F])") > + if _show_name: > + enabled_modes.append("show-name") > + if _show_all: > + enabled_modes.append("show-all") > + if enabled_modes: > + s = " and ".join(enabled_modes) + " mode enabled" > + _safe_addstr(_bot_sep_win, 0, max(term_width - len(s) - 2, 0), s) > + > + _bot_sep_win.noutrefresh() > + > + # > + # Update the help window, which shows either key bindings or help texts > + # > + > + _help_win.erase() > + > + if _show_help: > + node = _shown[_sel_node_i] > + if isinstance(node.item, (Symbol, Choice)) and node.help: > + help_lines = textwrap.wrap(node.help, _width(_help_win)) > + for i in range(min(_height(_help_win), len(help_lines))): > + _safe_addstr(_help_win, i, 0, help_lines[i]) > + else: > + _safe_addstr(_help_win, 0, 0, "(no help)") > + else: > + for i, line in enumerate(_MAIN_HELP_LINES): > + _safe_addstr(_help_win, i, 0, line) > + > + _help_win.noutrefresh() > + > + # > + # Update the top row with the menu path. > + # > + # Doing this last leaves the cursor on the top row, which avoids some minor > + # annoying jumpiness in gnome-terminal when reducing the height of the > + # terminal. It seems to happen whenever the row with the cursor on it > + # disappears. > + # > + > + _path_win.erase() > + > + # Draw the menu path ("(Top) -> Menu -> Submenu -> ...") > + > + menu_prompts = [] > + > + menu = _cur_menu > + while menu is not _kconf.top_node: > + # Promptless choices can be entered in show-all mode. Use > + # standard_sc_expr_str() for them, so they show up as > + # '<choice (name if any)>'. > + menu_prompts.append(menu.prompt[0] if menu.prompt else > + standard_sc_expr_str(menu.item)) > + menu = menu.parent > + menu_prompts.append("(Top)") > + menu_prompts.reverse() > + > + # Hack: We can't put ACS_RARROW directly in the string. Temporarily > + # represent it with NULL. > + menu_path_str = " \0 ".join(menu_prompts) > + > + # Scroll the menu path to the right if needed to make the current menu's > + # title visible > + if len(menu_path_str) > term_width: > + menu_path_str = menu_path_str[len(menu_path_str) - term_width:] > + > + # Print the path with the arrows reinserted > + split_path = menu_path_str.split("\0") > + _safe_addstr(_path_win, split_path[0]) > + for s in split_path[1:]: > + _safe_addch(_path_win, curses.ACS_RARROW) > + _safe_addstr(_path_win, s) > + > + _path_win.noutrefresh() > + > + > +def _parent_menu(node): > + # Returns the menu node of the menu that contains 'node'. In addition to > + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. > + # "Menu" here means a menu in the interface. > + > + menu = node.parent > + while not menu.is_menuconfig: > + menu = menu.parent > + return menu > + > + > +def _shown_nodes(menu): > + # Returns the list of menu nodes from 'menu' (see _parent_menu()) that > + # would be shown when entering it > + > + def rec(node): > + res = [] > + > + while node: > + if _visible(node) or _show_all: > + res.append(node) > + if node.list and not node.is_menuconfig: > + # Nodes from implicit menu created from dependencies. Will > + # be shown indented. Note that is_menuconfig is True for > + # menus and choices as well as 'menuconfig' symbols. > + res += rec(node.list) > + > + elif node.list and isinstance(node.item, Symbol): > + # Show invisible symbols if they have visible children. This > + # can happen for an m/y-valued symbol with an optional prompt > + # ('prompt "foo" is COND') that is currently disabled. Note > + # that it applies to both 'config' and 'menuconfig' symbols. > + shown_children = rec(node.list) > + if shown_children: > + res.append(node) > + if not node.is_menuconfig: > + res += shown_children > + > + node = node.next > + > + return res > + > + if isinstance(menu.item, Choice): > + # For named choices defined in multiple locations, entering the choice > + # at a particular menu node would normally only show the choice symbols > + # defined there (because that's what the MenuNode tree looks like). > + # > + # That might look confusing, and makes extending choices by defining > + # them in multiple locations less useful. Instead, gather all the child > + # menu nodes for all the choices whenever a choice is entered. That > + # makes all choice symbols visible at all locations. > + # > + # Choices can contain non-symbol items (people do all sorts of weird > + # stuff with them), hence the generality here. We really need to > + # preserve the menu tree at each choice location. > + # > + # Note: Named choices are pretty broken in the C tools, and this is > + # super obscure, so you probably won't find much that relies on this. > + # This whole 'if' could be deleted if you don't care about defining > + # choices in multiple locations to add symbols (which will still work, > + # just with things being displayed in a way that might be unexpected). > + > + # Do some additional work to avoid listing choice symbols twice if all > + # or part of the choice is copied in multiple locations (e.g. by > + # including some Kconfig file multiple times). We give the prompts at > + # the current location precedence. > + seen_syms = {node.item for node in rec(menu.list) > + if isinstance(node.item, Symbol)} > + res = [] > + for choice_node in menu.item.nodes: > + for node in rec(choice_node.list): > + # 'choice_node is menu' checks if we're dealing with the > + # current location > + if node.item not in seen_syms or choice_node is menu: > + res.append(node) > + if isinstance(node.item, Symbol): > + seen_syms.add(node.item) > + return res > + > + return rec(menu.list) > + > + > +def _visible(node): > + # Returns True if the node should appear in the menu (outside show-all > + # mode) > + > + return node.prompt and expr_value(node.prompt[1]) and not \ > + (node.item == MENU and not expr_value(node.visibility)) > + > + > +def _change_node(node): > + # Changes the value of the menu node 'node' if it is a symbol. Bools and > + # tristates are toggled, while other symbol types pop up a text entry > + # dialog. > + # > + # Returns False if the value of 'node' can't be changed. > + > + if not _changeable(node): > + return False > + > + # sc = symbol/choice > + sc = node.item > + > + if sc.orig_type in (INT, HEX, STRING): > + s = sc.str_value > + > + while True: > + s = _input_dialog( > + "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.orig_type]), > + s, _range_info(sc)) > + > + if s is None: > + break > + > + if sc.orig_type in (INT, HEX): > + s = s.strip() > + > + # 'make menuconfig' does this too. Hex values not starting with > + # '0x' are accepted when loading .config files though. > + if sc.orig_type == HEX and not s.startswith(("0x", "0X")): > + s = "0x" + s > + > + if _check_valid(sc, s): > + _set_val(sc, s) > + break > + > + elif len(sc.assignable) == 1: > + # Handles choice symbols for choices in y mode, which are a special > + # case: .assignable can be (2,) while .tri_value is 0. > + _set_val(sc, sc.assignable[0]) > + > + else: > + # Set the symbol to the value after the current value in > + # sc.assignable, with wrapping > + val_index = sc.assignable.index(sc.tri_value) > + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) > + > + > + if _is_y_mode_choice_sym(sc) and not node.list: > + # Immediately jump to the parent menu after making a choice selection, > + # like 'make menuconfig' does, except if the menu node has children > + # (which can happen if a symbol 'depends on' a choice symbol that > + # immediately precedes it). > + _leave_menu() > + > + > + return True > + > + > +def _changeable(node): > + # Returns True if the value if 'node' can be changed > + > + sc = node.item > + > + if not isinstance(sc, (Symbol, Choice)): > + return False > + > + # This will hit for invisible symbols, which appear in show-all mode and > + # when an invisible symbol has visible children (which can happen e.g. for > + # symbols with optional prompts) > + if not (node.prompt and expr_value(node.prompt[1])): > + return False > + > + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ > + or _is_y_mode_choice_sym(sc) > + > + > +def _set_sel_node_tri_val(tri_val): > + # Sets the value of the currently selected menu entry to 'tri_val', if that > + # value can be assigned > + > + sc = _shown[_sel_node_i].item > + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: > + _set_val(sc, tri_val) > + > + > +def _set_val(sc, val): > + # Wrapper around Symbol/Choice.set_value() for updating the menu state and > + # _conf_changed > + > + global _conf_changed > + > + # Use the string representation of tristate values. This makes the format > + # consistent for all symbol types. > + if val in TRI_TO_STR: > + val = TRI_TO_STR[val] > + > + if val != sc.str_value: > + sc.set_value(val) > + _conf_changed = True > + > + # Changing the value of the symbol might have changed what items in the > + # current menu are visible. Recalculate the state. > + _update_menu() > + > + > +def _update_menu(): > + # Updates the current menu after the value of a symbol or choice has been > + # changed. Changing a value might change which items in the menu are > + # visible. > + # > + # If possible, preserves the location of the cursor on the screen when > + # items are added/removed above the selected item. > + > + global _shown > + global _sel_node_i > + global _menu_scroll > + > + # Row on the screen the cursor was on > + old_row = _sel_node_i - _menu_scroll > + > + sel_node = _shown[_sel_node_i] > + > + # New visible nodes > + _shown = _shown_nodes(_cur_menu) > + > + # New index of selected node > + _sel_node_i = _shown.index(sel_node) > + > + # Try to make the cursor stay on the same row in the menu window. This > + # might be impossible if too many nodes have disappeared above the node. > + _menu_scroll = max(_sel_node_i - old_row, 0) > + > + > +def _input_dialog(title, initial_text, info_text=None): > + # Pops up a dialog that prompts the user for a string > + # > + # title: > + # Title to display at the top of the dialog window's border > + # > + # initial_text: > + # Initial text to prefill the input field with > + # > + # info_text: > + # String to show next to the input field. If None, just the input field > + # is shown. > + > + win = _styled_win("body") > + win.keypad(True) > + > + info_lines = info_text.split("\n") if info_text else [] > + > + # Give the input dialog its initial size > + _resize_input_dialog(win, title, info_lines) > + > + _safe_curs_set(2) > + > + # Input field text > + s = initial_text > + > + # Cursor position > + i = len(initial_text) > + > + def edit_width(): > + return _width(win) - 4 > + > + # Horizontal scroll offset > + hscroll = max(i - edit_width() + 1, 0) > + > + while True: > + # Draw the "main" display with the menu, etc., so that resizing still > + # works properly. This is like a stack of windows, only hardcoded for > + # now. > + _draw_main() > + _draw_input_dialog(win, title, info_lines, s, i, hscroll) > + curses.doupdate() > + > + > + c = _getch_compat(win) > + > + if c == curses.KEY_RESIZE: > + # Resize the main display too. The dialog floats above it. > + _resize_main() > + _resize_input_dialog(win, title, info_lines) > + > + elif c == "\n": > + _safe_curs_set(0) > + return s > + > + elif c == "\x1B": # \x1B = ESC > + _safe_curs_set(0) > + return None > + > + else: > + s, i, hscroll = _edit_text(c, s, i, hscroll, edit_width()) > + > + > +def _resize_input_dialog(win, title, info_lines): > + # Resizes the input dialog to a size appropriate for the terminal size > + > + screen_height, screen_width = _stdscr.getmaxyx() > + > + win_height = 5 > + if info_lines: > + win_height += len(info_lines) + 1 > + win_height = min(win_height, screen_height) > + > + win_width = max(_INPUT_DIALOG_MIN_WIDTH, > + len(title) + 4, > + *(len(line) + 4 for line in info_lines)) > + win_width = min(win_width, screen_width) > + > + win.resize(win_height, win_width) > + win.mvwin((screen_height - win_height)//2, > + (screen_width - win_width)//2) > + > + > +def _draw_input_dialog(win, title, info_lines, s, i, hscroll): > + edit_width = _width(win) - 4 > + > + win.erase() > + > + # Note: Perhaps having a separate window for the input field would be nicer > + visible_s = s[hscroll:hscroll + edit_width] > + _safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)), > + _style["edit"]) > + > + for linenr, line in enumerate(info_lines): > + _safe_addstr(win, 4 + linenr, 2, line) > + > + # Draw the frame last so that it overwrites the body text for small windows > + _draw_frame(win, title) > + > + _safe_move(win, 2, 2 + i - hscroll) > + > + win.noutrefresh() > + > + > +def _load_dialog(): > + # Dialog for loading a new configuration > + > + global _conf_changed > + global _conf_filename > + global _show_all > + > + if _conf_changed: > + c = _key_dialog( > + "Load", > + "You have unsaved changes. Load new\n" > + "configuration anyway?\n" > + "\n" > + " (O)K (C)ancel", > + "oc") > + > + if c is None or c == "c": > + return > + > + filename = _conf_filename > + while True: > + filename = _input_dialog("File to load", filename, _load_save_info()) > + if filename is None: > + return > + > + filename = os.path.expanduser(filename) > + > + if _try_load(filename): > + _conf_filename = filename > + _conf_changed = _needs_save() > + > + # Turn on show-all mode if the selected node is not visible after > + # loading the new configuration. _shown still holds the old state. > + if _shown[_sel_node_i] not in _shown_nodes(_cur_menu): > + _show_all = True > + > + _update_menu() > + > + # The message dialog indirectly updates the menu display, so _msg() > + # must be called after the new state has been initialized > + _msg("Success", "Loaded " + filename) > + return > + > + > +def _try_load(filename): > + # Tries to load a configuration file. Pops up an error and returns False on > + # failure. > + # > + # filename: > + # Configuration file to load > + > + try: > + _kconf.load_config(filename) > + return True > + except EnvironmentError as e: > + _error("Error loading '{}'\n\n{} (errno: {})" > + .format(filename, e.strerror, errno.errorcode[e.errno])) > + return False > + > + > +def _save_dialog(save_fn, default_filename, description): > + # Dialog for saving the current configuration > + # > + # save_fn: > + # Function to call with 'filename' to save the file > + # > + # default_filename: > + # Prefilled filename in the input field > + # > + # description: > + # String describing the thing being saved > + # > + # Return value: > + # The path to the saved file, or None if no file was saved > + > + filename = default_filename > + while True: > + filename = _input_dialog("Filename to save {} to".format(description), > + filename, _load_save_info()) > + if filename is None: > + return None > + > + filename = os.path.expanduser(filename) > + > + msg = _try_save(save_fn, filename, description) > + if msg: > + _msg("Success", msg) > + return filename > + > + > +def _try_save(save_fn, filename, description): > + # Tries to save a configuration file. Returns a message to print on > + # success. > + # > + # save_fn: > + # Function to call with 'filename' to save the file > + # > + # description: > + # String describing the thing being saved > + # > + # Return value: > + # A message to print on success, and None on failure > + > + try: > + # save_fn() returns a message to print > + return save_fn(filename) > + except EnvironmentError as e: > + _error("Error saving {} to '{}'\n\n{} (errno: {})" > + .format(description, e.filename, e.strerror, > + errno.errorcode[e.errno])) > + return None > + > + > +def _key_dialog(title, text, keys): > + # Pops up a dialog that can be closed by pressing a key > + # > + # title: > + # Title to display at the top of the dialog window's border > + # > + # text: > + # Text to show in the dialog > + # > + # keys: > + # List of keys that will close the dialog. Other keys (besides ESC) are > + # ignored. The caller is responsible for providing a hint about which > + # keys can be pressed in 'text'. > + # > + # Return value: > + # The key that was pressed to close the dialog. Uppercase characters are > + # converted to lowercase. ESC will always close the dialog, and returns > + # None. > + > + win = _styled_win("body") > + win.keypad(True) > + > + _resize_key_dialog(win, text) > + > + while True: > + # See _input_dialog() > + _draw_main() > + _draw_key_dialog(win, title, text) > + curses.doupdate() > + > + > + c = _getch_compat(win) > + > + if c == curses.KEY_RESIZE: > + # Resize the main display too. The dialog floats above it. > + _resize_main() > + _resize_key_dialog(win, text) > + > + elif c == "\x1B": # \x1B = ESC > + return None > + > + elif isinstance(c, str): > + c = c.lower() > + if c in keys: > + return c > + > + > +def _resize_key_dialog(win, text): > + # Resizes the key dialog to a size appropriate for the terminal size > + > + screen_height, screen_width = _stdscr.getmaxyx() > + > + lines = text.split("\n") > + > + win_height = min(len(lines) + 4, screen_height) > + win_width = min(max(len(line) for line in lines) + 4, screen_width) > + > + win.resize(win_height, win_width) > + win.mvwin((screen_height - win_height)//2, > + (screen_width - win_width)//2) > + > + > +def _draw_key_dialog(win, title, text): > + win.erase() > + > + for i, line in enumerate(text.split("\n")): > + _safe_addstr(win, 2 + i, 2, line) > + > + # Draw the frame last so that it overwrites the body text for small windows > + _draw_frame(win, title) > + > + win.noutrefresh() > + > + > +def _draw_frame(win, title): > + # Draw a frame around the inner edges of 'win', with 'title' at the top > + > + win_height, win_width = win.getmaxyx() > + > + win.attron(_style["frame"]) > + > + # Draw top/bottom edge > + _safe_hline(win, 0, 0, " ", win_width) > + _safe_hline(win, win_height - 1, 0, " ", win_width) > + > + # Draw left/right edge > + _safe_vline(win, 0, 0, " ", win_height) > + _safe_vline(win, 0, win_width - 1, " ", win_height) > + > + # Draw title > + _safe_addstr(win, 0, max((win_width - len(title))//2, 0), title) > + > + win.attroff(_style["frame"]) > + > + > +def _jump_to_dialog(): > + # Implements the jump-to dialog, where symbols can be looked up via > + # incremental search and jumped to. > + # > + # Returns True if the user jumped to a symbol, and False if the dialog was > + # canceled. > + > + s = "" # Search text > + prev_s = None # Previous search text > + s_i = 0 # Search text cursor position > + hscroll = 0 # Horizontal scroll offset > + > + sel_node_i = 0 # Index of selected row > + scroll = 0 # Index in 'matches' of the top row of the list > + > + # Edit box at the top > + edit_box = _styled_win("jump-edit") > + edit_box.keypad(True) > + > + # List of matches > + matches_win = _styled_win("list") > + > + # Bottom separator, with arrows pointing down > + bot_sep_win = _styled_win("separator") > + > + # Help window with instructions at the bottom > + help_win = _styled_win("help") > + > + # Give windows their initial size > + _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, > + sel_node_i, scroll) > + > + _safe_curs_set(2) > + > + # Logic duplication with _select_{next,prev}_menu_entry(), except we do a > + # functional variant that returns the new (sel_node_i, scroll) values to > + # avoid 'nonlocal'. TODO: Can this be factored out in some nice way? > + > + def select_next_match(): > + if sel_node_i == len(matches) - 1: > + return sel_node_i, scroll > + > + if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \ > + and scroll < _max_scroll(matches, matches_win): > + > + return sel_node_i + 1, scroll + 1 > + > + return sel_node_i + 1, scroll > + > + def select_prev_match(): > + if sel_node_i == 0: > + return sel_node_i, scroll > + > + if sel_node_i - 1 < scroll + _SCROLL_OFFSET: > + return sel_node_i - 1, max(scroll - 1, 0) > + > + return sel_node_i - 1, scroll > + > + while True: > + if s != prev_s: > + # The search text changed. Find new matching nodes. > + > + prev_s = s > + > + try: > + # We could use re.IGNORECASE here instead of lower(), but this > + # is noticeably less jerky while inputting regexes like > + # '.*debug$' (though the '.*' is redundant there). Those > + # probably have bad interactions with re.search(), which > + # matches anywhere in the string. > + # > + # It's not horrible either way. Just a bit smoother. > + regex_searches = [re.compile(regex).search > + for regex in s.lower().split()] > + > + # No exception thrown, so the regexes are okay > + bad_re = None > + > + # List of matching nodes > + matches = [] > + add_match = matches.append > + > + # Search symbols and choices > + > + for node in _sorted_sc_nodes(): > + # Symbol/choice > + sc = node.item > + > + for search in regex_searches: > + # Both the name and the prompt might be missing, since > + # we're searching both symbols and choices > + > + # Does the regex match either the symbol name or the > + # prompt (if any)? > + if not (sc.name and search(sc.name.lower()) or > + node.prompt and search(node.prompt[0].lower())): > + > + # Give up on the first regex that doesn't match, to > + # speed things up a bit when multiple regexes are > + # entered > + break > + > + else: > + add_match(node) > + > + # Search menus and comments > + > + for node in _sorted_menu_comment_nodes(): > + for search in regex_searches: > + if not search(node.prompt[0].lower()): > + break > + else: > + add_match(node) > + > + except re.error as e: > + # Bad regex. Remember the error message so we can show it. > + bad_re = "Bad regular expression" > + # re.error.msg was added in Python 3.5 > + if hasattr(e, "msg"): > + bad_re += ": " + e.msg > + > + matches = [] > + > + # Reset scroll and jump to the top of the list of matches > + sel_node_i = scroll = 0 > + > + _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, > + s, s_i, hscroll, > + bad_re, matches, sel_node_i, scroll) > + curses.doupdate() > + > + > + c = _getch_compat(edit_box) > + > + if c == "\n": > + if matches: > + _jump_to(matches[sel_node_i]) > + _safe_curs_set(0) > + return True > + > + elif c == "\x1B": # \x1B = ESC > + _safe_curs_set(0) > + return False > + > + elif c == curses.KEY_RESIZE: > + # We adjust the scroll so that the selected node stays visible in > + # the list when the terminal is resized, hence the 'scroll' > + # assignment > + scroll = _resize_jump_to_dialog( > + edit_box, matches_win, bot_sep_win, help_win, > + sel_node_i, scroll) > + > + elif c == "\x06": # \x06 = Ctrl-F > + if matches: > + _safe_curs_set(0) > + _info_dialog(matches[sel_node_i], True) > + _safe_curs_set(2) > + > + scroll = _resize_jump_to_dialog( > + edit_box, matches_win, bot_sep_win, help_win, > + sel_node_i, scroll) > + > + elif c == curses.KEY_DOWN: > + sel_node_i, scroll = select_next_match() > + > + elif c == curses.KEY_UP: > + sel_node_i, scroll = select_prev_match() > + > + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D > + # Keep it simple. This way we get sane behavior for small windows, > + # etc., for free. > + for _ in range(_PG_JUMP): > + sel_node_i, scroll = select_next_match() > + > + # Page Up (no Ctrl-U, as it's already used by the edit box) > + elif c == curses.KEY_PPAGE: > + for _ in range(_PG_JUMP): > + sel_node_i, scroll = select_prev_match() > + > + elif c == curses.KEY_END: > + sel_node_i = len(matches) - 1 > + scroll = _max_scroll(matches, matches_win) > + > + elif c == curses.KEY_HOME: > + sel_node_i = scroll = 0 > + > + else: > + s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, > + _width(edit_box) - 2) > + > + > +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing > +# to the same list. This avoids a global. > +def _sorted_sc_nodes(cached_nodes=[]): > + # Returns a sorted list of symbol and choice nodes to search. The symbol > + # nodes appear first, sorted by name, and then the choice nodes, sorted by > + # prompt and (secondarily) name. > + > + if not cached_nodes: > + # Add symbol nodes > + for sym in sorted(_kconf.unique_defined_syms, > + key=lambda sym: sym.name): > + # += is in-place for lists > + cached_nodes += sym.nodes > + > + # Add choice nodes > + > + choices = sorted(_kconf.unique_choices, > + key=lambda choice: choice.name or "") > + > + cached_nodes += sorted( > + [node for choice in choices for node in choice.nodes], > + key=lambda node: node.prompt[0] if node.prompt else "") > + > + return cached_nodes > + > + > +def _sorted_menu_comment_nodes(cached_nodes=[]): > + # Returns a list of menu and comment nodes to search, sorted by prompt, > + # with the menus first > + > + if not cached_nodes: > + def prompt_text(mc): > + return mc.prompt[0] > + > + cached_nodes += sorted(_kconf.menus, key=prompt_text) > + cached_nodes += sorted(_kconf.comments, key=prompt_text) > + > + return cached_nodes > + > + > +def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, > + sel_node_i, scroll): > + # Resizes the jump-to dialog to fill the terminal. > + # > + # Returns the new scroll index. We adjust the scroll if needed so that the > + # selected node stays visible. > + > + screen_height, screen_width = _stdscr.getmaxyx() > + > + bot_sep_win.resize(1, screen_width) > + > + help_win_height = len(_JUMP_TO_HELP_LINES) > + matches_win_height = screen_height - help_win_height - 4 > + > + if matches_win_height >= 1: > + edit_box.resize(3, screen_width) > + matches_win.resize(matches_win_height, screen_width) > + help_win.resize(help_win_height, screen_width) > + > + matches_win.mvwin(3, 0) > + bot_sep_win.mvwin(3 + matches_win_height, 0) > + help_win.mvwin(3 + matches_win_height + 1, 0) > + else: > + # Degenerate case. Give up on nice rendering and just prevent errors. > + > + matches_win_height = 1 > + > + edit_box.resize(screen_height, screen_width) > + matches_win.resize(1, screen_width) > + help_win.resize(1, screen_width) > + > + for win in matches_win, bot_sep_win, help_win: > + win.mvwin(0, 0) > + > + # Adjust the scroll so that the selected row is still within the window, if > + # needed > + if sel_node_i - scroll >= matches_win_height: > + return sel_node_i - matches_win_height + 1 > + return scroll > + > + > +def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, > + s, s_i, hscroll, > + bad_re, matches, sel_node_i, scroll): > + > + edit_width = _width(edit_box) - 2 > + > + # > + # Update list of matches > + # > + > + matches_win.erase() > + > + if matches: > + for i in range(scroll, > + min(scroll + _height(matches_win), len(matches))): > + > + node = matches[i] > + > + if isinstance(node.item, (Symbol, Choice)): > + node_str = _name_and_val_str(node.item) > + if node.prompt: > + node_str += ' "{}"'.format(node.prompt[0]) > + elif node.item == MENU: > + node_str = 'menu "{}"'.format(node.prompt[0]) > + else: # node.item == COMMENT > + node_str = 'comment "{}"'.format(node.prompt[0]) > + > + _safe_addstr(matches_win, i - scroll, 0, node_str, > + _style["selection" if i == sel_node_i else "list"]) > + > + else: > + # bad_re holds the error message from the re.error exception on errors > + _safe_addstr(matches_win, 0, 0, bad_re or "No matches") > + > + matches_win.noutrefresh() > + > + # > + # Update bottom separator line > + # > + > + bot_sep_win.erase() > + > + # Draw arrows pointing down if the symbol list is scrolled up > + if scroll < _max_scroll(matches, matches_win): > + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) > + > + bot_sep_win.noutrefresh() > + > + # > + # Update help window at bottom > + # > + > + help_win.erase() > + > + for i, line in enumerate(_JUMP_TO_HELP_LINES): > + _safe_addstr(help_win, i, 0, line) > + > + help_win.noutrefresh() > + > + # > + # Update edit box. We do this last since it makes it handy to position the > + # cursor. > + # > + > + edit_box.erase() > + > + _draw_frame(edit_box, "Jump to symbol/choice/menu/comment") > + > + # Draw arrows pointing up if the symbol list is scrolled down > + if scroll > 0: > + # TODO: Bit ugly that _style["frame"] is repeated here > + _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS, > + _style["frame"]) > + > + visible_s = s[hscroll:hscroll + edit_width] > + _safe_addstr(edit_box, 1, 1, visible_s) > + > + _safe_move(edit_box, 1, 1 + s_i - hscroll) > + > + edit_box.noutrefresh() > + > + > +def _info_dialog(node, from_jump_to_dialog): > + # Shows a fullscreen window with information about 'node'. > + # > + # If 'from_jump_to_dialog' is True, the information dialog was opened from > + # within the jump-to-dialog. In this case, we make '/' from within the > + # information dialog just return, to avoid a confusing recursive invocation > + # of the jump-to-dialog. > + > + # Top row, with title and arrows point up > + top_line_win = _styled_win("separator") > + > + # Text display > + text_win = _styled_win("text") > + text_win.keypad(True) > + > + # Bottom separator, with arrows pointing down > + bot_sep_win = _styled_win("separator") > + > + # Help window with keys at the bottom > + help_win = _styled_win("help") > + > + # Give windows their initial size > + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) > + > + > + # Get lines of help text > + lines = _info_str(node).split("\n") > + > + # Index of first row in 'lines' to show > + scroll = 0 > + > + while True: > + _draw_info_dialog(node, lines, scroll, top_line_win, text_win, > + bot_sep_win, help_win) > + curses.doupdate() > + > + > + c = _getch_compat(text_win) > + > + if c == curses.KEY_RESIZE: > + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) > + > + elif c in (curses.KEY_DOWN, "j", "J"): > + if scroll < _max_scroll(lines, text_win): > + scroll += 1 > + > + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D > + scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win)) > + > + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U > + scroll = max(scroll - _PG_JUMP, 0) > + > + elif c in (curses.KEY_END, "G"): > + scroll = _max_scroll(lines, text_win) > + > + elif c in (curses.KEY_HOME, "g"): > + scroll = 0 > + > + elif c in (curses.KEY_UP, "k", "K"): > + if scroll > 0: > + scroll -= 1 > + > + elif c == "/": > + # Support starting a search from within the information dialog > + > + if from_jump_to_dialog: > + return # Avoid recursion > + > + if _jump_to_dialog(): > + return # Jumped to a symbol. Cancel the information dialog. > + > + # Stay in the information dialog if the jump-to dialog was > + # canceled. Resize it in case the terminal was resized while the > + # fullscreen jump-to dialog was open. > + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) > + > + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, > + "\x1B", # \x1B = ESC > + "q", "Q", "h", "H"): > + > + return > + > + > +def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): > + # Resizes the info dialog to fill the terminal > + > + screen_height, screen_width = _stdscr.getmaxyx() > + > + top_line_win.resize(1, screen_width) > + bot_sep_win.resize(1, screen_width) > + > + help_win_height = len(_INFO_HELP_LINES) > + text_win_height = screen_height - help_win_height - 2 > + > + if text_win_height >= 1: > + text_win.resize(text_win_height, screen_width) > + help_win.resize(help_win_height, screen_width) > + > + text_win.mvwin(1, 0) > + bot_sep_win.mvwin(1 + text_win_height, 0) > + help_win.mvwin(1 + text_win_height + 1, 0) > + else: > + # Degenerate case. Give up on nice rendering and just prevent errors. > + > + text_win.resize(1, screen_width) > + help_win.resize(1, screen_width) > + > + for win in text_win, bot_sep_win, help_win: > + win.mvwin(0, 0) > + > + > +def _draw_info_dialog(node, lines, scroll, top_line_win, text_win, > + bot_sep_win, help_win): > + > + text_win_height, text_win_width = text_win.getmaxyx() > + > + # Note: The top row is deliberately updated last. See _draw_main(). > + > + # > + # Update text display > + # > + > + text_win.erase() > + > + for i, line in enumerate(lines[scroll:scroll + text_win_height]): > + _safe_addstr(text_win, i, 0, line) > + > + text_win.noutrefresh() > + > + # > + # Update bottom separator line > + # > + > + bot_sep_win.erase() > + > + # Draw arrows pointing down if the symbol window is scrolled up > + if scroll < _max_scroll(lines, text_win): > + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) > + > + bot_sep_win.noutrefresh() > + > + # > + # Update help window at bottom > + # > + > + help_win.erase() > + > + for i, line in enumerate(_INFO_HELP_LINES): > + _safe_addstr(help_win, i, 0, line) > + > + help_win.noutrefresh() > + > + # > + # Update top row > + # > + > + top_line_win.erase() > + > + # Draw arrows pointing up if the information window is scrolled down. Draw > + # them before drawing the title, so the title ends up on top for small > + # windows. > + if scroll > 0: > + _safe_hline(top_line_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) > + > + title = ("Symbol" if isinstance(node.item, Symbol) else > + "Choice" if isinstance(node.item, Choice) else > + "Menu" if node.item == MENU else > + "Comment") + " information" > + _safe_addstr(top_line_win, 0, max((text_win_width - len(title))//2, 0), > + title) > + > + top_line_win.noutrefresh() > + > + > +def _info_str(node): > + # Returns information about the menu node 'node' as a string. > + # > + # The helper functions are responsible for adding newlines. This allows > + # them to return "" if they don't want to add any output. > + > + if isinstance(node.item, Symbol): > + sym = node.item > + > + return ( > + _name_info(sym) + > + _prompt_info(sym) + > + "Type: {}\n".format(TYPE_TO_STR[sym.type]) + > + _value_info(sym) + > + _help_info(sym) + > + _direct_dep_info(sym) + > + _defaults_info(sym) + > + _select_imply_info(sym) + > + _kconfig_def_info(sym) > + ) > + > + if isinstance(node.item, Choice): > + choice = node.item > + > + return ( > + _name_info(choice) + > + _prompt_info(choice) + > + "Type: {}\n".format(TYPE_TO_STR[choice.type]) + > + 'Mode: {}\n'.format(choice.str_value) + > + _help_info(choice) + > + _choice_syms_info(choice) + > + _direct_dep_info(choice) + > + _defaults_info(choice) + > + _kconfig_def_info(choice) > + ) > + > + return _kconfig_def_info(node) # node.item in (MENU, COMMENT) > + > + > +def _name_info(sc): > + # Returns a string with the name of the symbol/choice. Names are optional > + # for choices. > + > + return "Name: {}\n".format(sc.name) if sc.name else "" > + > + > +def _prompt_info(sc): > + # Returns a string listing the prompts of 'sc' (Symbol or Choice) > + > + s = "" > + > + for node in sc.nodes: > + if node.prompt: > + s += "Prompt: {}\n".format(node.prompt[0]) > + > + return s > + > + > +def _value_info(sym): > + # Returns a string showing 'sym's value > + > + # Only put quotes around the value for string symbols > + return "Value: {}\n".format( > + '"{}"'.format(sym.str_value) > + if sym.orig_type == STRING > + else sym.str_value) > + > + > +def _choice_syms_info(choice): > + # Returns a string listing the choice symbols in 'choice'. Adds > + # "(selected)" next to the selected one. > + > + s = "Choice symbols:\n" > + > + for sym in choice.syms: > + s += " - " + sym.name > + if sym is choice.selection: > + s += " (selected)" > + s += "\n" > + > + return s + "\n" > + > + > +def _help_info(sc): > + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). > + # Symbols and choices defined in multiple locations can have multiple help > + # texts. > + > + s = "\n" > + > + for node in sc.nodes: > + if node.help is not None: > + s += "Help:\n\n{}\n\n".format(_indent(node.help, 2)) > + > + return s > + > + > +def _direct_dep_info(sc): > + # Returns a string describing the direct dependencies of 'sc' (Symbol or > + # Choice). The direct dependencies are the OR of the dependencies from each > + # definition location. The dependencies at each definition location come > + # from 'depends on' and dependencies inherited from parent items. > + > + return "" if sc.direct_dep is _kconf.y else \ > + 'Direct dependencies (={}):\n{}\n' \ > + .format(TRI_TO_STR[expr_value(sc.direct_dep)], > + _split_expr_info(sc.direct_dep, 2)) > + > + > +def _defaults_info(sc): > + # Returns a string describing the defaults of 'sc' (Symbol or Choice) > + > + if not sc.defaults: > + return "" > + > + s = "Default" > + if len(sc.defaults) > 1: > + s += "s" > + s += ":\n" > + > + for val, cond in sc.orig_defaults: > + s += " - " > + if isinstance(sc, Symbol): > + s += _expr_str(val) > + > + # Skip the tristate value hint if the expression is just a single > + # symbol. _expr_str() already shows its value as a string. > + # > + # This also avoids showing the tristate value for string/int/hex > + # defaults, which wouldn't make any sense. > + if isinstance(val, tuple): > + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) > + else: > + # Don't print the value next to the symbol name for choice > + # defaults, as it looks a bit confusing > + s += val.name > + s += "\n" > + > + if cond is not _kconf.y: > + s += " Condition (={}):\n{}" \ > + .format(TRI_TO_STR[expr_value(cond)], > + _split_expr_info(cond, 4)) > + > + return s + "\n" > + > + > +def _split_expr_info(expr, indent): > + # Returns a string with 'expr' split into its top-level && or || operands, > + # with one operand per line, together with the operand's value. This is > + # usually enough to get something readable for long expressions. A fancier > + # recursive thingy would be possible too. > + # > + # indent: > + # Number of leading spaces to add before the split expression. > + > + if len(split_expr(expr, AND)) > 1: > + split_op = AND > + op_str = "&&" > + else: > + split_op = OR > + op_str = "||" > + > + s = "" > + for i, term in enumerate(split_expr(expr, split_op)): > + s += "{}{} {}".format(indent*" ", > + " " if i == 0 else op_str, > + _expr_str(term)) > + > + # Don't bother showing the value hint if the expression is just a > + # single symbol. _expr_str() already shows its value. > + if isinstance(term, tuple): > + s += " (={})".format(TRI_TO_STR[expr_value(term)]) > + > + s += "\n" > + > + return s > + > + > +def _select_imply_info(sym): > + # Returns a string with information about which symbols 'select' or 'imply' > + # 'sym'. The selecting/implying symbols are grouped according to which > + # value they select/imply 'sym' to (n/m/y). > + > + def sis(expr, val, title): > + # sis = selects/implies > + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] > + if not sis: > + return "" > + > + res = title > + for si in sis: > + res += " - {}\n".format(split_expr(si, AND)[0].name) > + return res + "\n" > + > + s = "" > + > + if sym.rev_dep is not _kconf.n: > + s += sis(sym.rev_dep, 2, > + "Symbols currently y-selecting this symbol:\n") > + s += sis(sym.rev_dep, 1, > + "Symbols currently m-selecting this symbol:\n") > + s += sis(sym.rev_dep, 0, > + "Symbols currently n-selecting this symbol (no effect):\n") > + > + if sym.weak_rev_dep is not _kconf.n: > + s += sis(sym.weak_rev_dep, 2, > + "Symbols currently y-implying this symbol:\n") > + s += sis(sym.weak_rev_dep, 1, > + "Symbols currently m-implying this symbol:\n") > + s += sis(sym.weak_rev_dep, 0, > + "Symbols currently n-implying this symbol (no effect):\n") > + > + return s > + > + > +def _kconfig_def_info(item): > + # Returns a string with the definition of 'item' in Kconfig syntax, > + # together with the definition location(s) and their include and menu paths > + > + nodes = [item] if isinstance(item, MenuNode) else item.nodes > + > + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ > + .format("s" if len(nodes) > 1 else "") > + s += (len(s) - 1)*"=" > + > + for node in nodes: > + s += "\n\n" \ > + "At {}:{}\n" \ > + "{}" \ > + "Menu path: {}\n\n" \ > + "{}" \ > + .format(node.filename, node.linenr, > + _include_path_info(node), > + _menu_path_info(node), > + _indent(node.custom_str(_name_and_val_str), 2)) > + > + return s > + > + > +def _include_path_info(node): > + if not node.include_path: > + # In the top-level Kconfig file > + return "" > + > + return "Included via {}\n".format( > + " -> ".join("{}:{}".format(filename, linenr) > + for filename, linenr in node.include_path)) > + > + > +def _menu_path_info(node): > + # Returns a string describing the menu path leading up to 'node' > + > + path = "" > + > + while node.parent is not _kconf.top_node: > + node = node.parent > + > + # Promptless choices might appear among the parents. Use > + # standard_sc_expr_str() for them, so that they show up as > + # '<choice (name if any)>'. > + path = " -> " + (node.prompt[0] if node.prompt else > + standard_sc_expr_str(node.item)) + path > + > + return "(Top)" + path > + > + > +def _indent(s, n): > + # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not > + # available in Python 2 (it's 3.3+). > + > + return "\n".join(n*" " + line for line in s.split("\n")) > + > + > +def _name_and_val_str(sc): > + # Custom symbol/choice printer that shows symbol values after symbols > + > + # Show the values of non-constant (non-quoted) symbols that don't look like > + # numbers. Things like 123 are actually symbol references, and only work as > + # expected due to undefined symbols getting their name as their value. > + # Showing the symbol value for those isn't helpful though. > + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): > + if not sc.nodes: > + # Undefined symbol reference > + return "{}(undefined/n)".format(sc.name) > + > + return '{}(={})'.format(sc.name, sc.str_value) > + > + # For other items, use the standard format > + return standard_sc_expr_str(sc) > + > + > +def _expr_str(expr): > + # Custom expression printer that shows symbol values > + return expr_str(expr, _name_and_val_str) > + > + > +def _styled_win(style): > + # Returns a new curses window with style 'style' and space as the fill > + # character. The initial dimensions are (1, 1), so the window needs to be > + # sized and positioned separately. > + > + win = curses.newwin(1, 1) > + _set_style(win, style) > + return win > + > + > +def _set_style(win, style): > + # Changes the style of an existing window > + > + win.bkgdset(" ", _style[style]) > + > + > +def _max_scroll(lst, win): > + # Assuming 'lst' is a list of items to be displayed in 'win', > + # returns the maximum number of steps 'win' can be scrolled down. > + # We stop scrolling when the bottom item is visible. > + > + return max(0, len(lst) - _height(win)) > + > + > +def _edit_text(c, s, i, hscroll, width): > + # Implements text editing commands for edit boxes. Takes a character (which > + # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns > + # the new state after the character has been processed. > + # > + # c: > + # Character from user > + # > + # s: > + # Current contents of string > + # > + # i: > + # Current cursor index in string > + # > + # hscroll: > + # Index in s of the leftmost character in the edit box, for horizontal > + # scrolling > + # > + # width: > + # Width in characters of the edit box > + # > + # Return value: > + # An (s, i, hscroll) tuple for the new state > + > + if c == curses.KEY_LEFT: > + if i > 0: > + i -= 1 > + > + elif c == curses.KEY_RIGHT: > + if i < len(s): > + i += 1 > + > + elif c in (curses.KEY_HOME, "\x01"): # \x01 = CTRL-A > + i = 0 > + > + elif c in (curses.KEY_END, "\x05"): # \x05 = CTRL-E > + i = len(s) > + > + elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR): > + if i > 0: > + s = s[:i-1] + s[i:] > + i -= 1 > + > + elif c == curses.KEY_DC: > + s = s[:i] + s[i+1:] > + > + elif c == "\x17": # \x17 = CTRL-W > + # The \W removes characters like ',' one at a time > + new_i = re.search(r"(?:\w*|\W)\s*$", s[:i]).start() > + s = s[:new_i] + s[i:] > + i = new_i > + > + elif c == "\x0B": # \x0B = CTRL-K > + s = s[:i] > + > + elif c == "\x15": # \x15 = CTRL-U > + s = s[i:] > + i = 0 > + > + elif isinstance(c, str): > + # Insert character > + s = s[:i] + c + s[i:] > + i += 1 > + > + # Adjust the horizontal scroll so that the cursor never touches the left or > + # right edges of the edit box, except when it's at the beginning or the end > + # of the string > + if i < hscroll + _SCROLL_OFFSET: > + hscroll = max(i - _SCROLL_OFFSET, 0) > + elif i >= hscroll + width - _SCROLL_OFFSET: > + max_scroll = max(len(s) - width + 1, 0) > + hscroll = min(i - width + _SCROLL_OFFSET + 1, max_scroll) > + > + return s, i, hscroll > + > + > +def _load_save_info(): > + # Returns an information string for load/save dialog boxes > + > + return "(Relative to {})\n\nRefer to your home directory with ~" \ > + .format(os.path.join(os.getcwd(), "")) > + > + > +def _msg(title, text): > + # Pops up a message dialog that can be dismissed with Space/Enter/ESC > + > + _key_dialog(title, text, " \n") > + > + > +def _error(text): > + # Pops up an error dialog that can be dismissed with Space/Enter/ESC > + > + _msg("Error", text) > + > + > +def _node_str(node): > + # Returns the complete menu entry text for a menu node. > + # > + # Example return value: "[*] Support for X" > + > + # Calculate the indent to print the item with by checking how many levels > + # above it the closest 'menuconfig' item is (this includes menus and > + # choices as well as menuconfig symbols) > + indent = 0 > + parent = node.parent > + while not parent.is_menuconfig: > + indent += _SUBMENU_INDENT > + parent = parent.parent > + > + # This approach gives nice alignment for empty string symbols ("() Foo") > + s = "{:{}}".format(_value_str(node), 3 + indent) > + > + if _should_show_name(node): > + if isinstance(node.item, Symbol): > + s += " <{}>".format(node.item.name) > + else: > + # For choices, use standard_sc_expr_str(). That way they show up as > + # '<choice (name if any)>'. > + s += " " + standard_sc_expr_str(node.item) > + > + if node.prompt: > + if node.item == COMMENT: > + s += " *** {} ***".format(node.prompt[0]) > + else: > + s += " " + node.prompt[0] > + > + if isinstance(node.item, Symbol): > + sym = node.item > + > + # Print "(NEW)" next to symbols without a user value (from e.g. a > + # .config), but skip it for choice symbols in choices in y mode, > + # and for symbols of UNKNOWN type (which generate a warning though) > + if sym.user_value is None and sym.orig_type and \ > + not (sym.choice and sym.choice.tri_value == 2): > + > + s += " (NEW)" > + > + if isinstance(node.item, Choice) and node.item.tri_value == 2: > + # Print the prompt of the selected symbol after the choice for > + # choices in y mode > + sym = node.item.selection > + if sym: > + for sym_node in sym.nodes: > + # Use the prompt used at this choice location, in case the > + # choice symbol is defined in multiple locations > + if sym_node.parent is node and sym_node.prompt: > + s += " ({})".format(sym_node.prompt[0]) > + break > + else: > + # If the symbol isn't defined at this choice location, then > + # just use whatever prompt we can find for it > + for sym_node in sym.nodes: > + if sym_node.prompt: > + s += " ({})".format(sym_node.prompt[0]) > + break > + > + # Print "--->" next to nodes that have menus that can potentially be > + # entered. Print "----" if the menu is empty. We don't allow those to be > + # entered. > + if node.is_menuconfig: > + s += " --->" if _shown_nodes(node) else " ----" > + > + return s > + > + > +def _should_show_name(node): > + # Returns True if 'node' is a symbol or choice whose name should shown (if > + # any, as names are optional for choices) > + > + # The 'not node.prompt' case only hits in show-all mode, for promptless > + # symbols and choices > + return not node.prompt or \ > + (_show_name and isinstance(node.item, (Symbol, Choice))) > + > + > +def _value_str(node): > + # Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu node > + > + item = node.item > + > + if item in (MENU, COMMENT): > + return "" > + > + # Wouldn't normally happen, and generates a warning > + if not item.orig_type: > + return "" > + > + if item.orig_type in (STRING, INT, HEX): > + return "({})".format(item.str_value) > + > + # BOOL or TRISTATE > + > + if _is_y_mode_choice_sym(item): > + return "(X)" if item.choice.selection is item else "( )" > + > + tri_val_str = (" ", "M", "*")[item.tri_value] > + > + if len(item.assignable) <= 1: > + # Pinned to a single value > + return "" if isinstance(item, Choice) else "-{}-".format(tri_val_str) > + > + if item.type == BOOL: > + return "[{}]".format(tri_val_str) > + > + # item.type == TRISTATE > + if item.assignable == (1, 2): > + return "{{{}}}".format(tri_val_str) # {M}/{*} > + return "<{}>".format(tri_val_str) > + > + > +def _is_y_mode_choice_sym(item): > + # 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 > + return isinstance(item, Symbol) and item.choice and item.visibility == 2 > + > + > +def _check_valid(sym, s): > + # Returns True if the string 's' is a well-formed value for 'sym'. > + # Otherwise, displays an error and returns False. > + > + if sym.orig_type not in (INT, HEX): > + return True # Anything goes for non-int/hex symbols > + > + base = 10 if sym.orig_type == INT else 16 > + try: > + int(s, base) > + except ValueError: > + _error("'{}' is a malformed {} value" > + .format(s, TYPE_TO_STR[sym.orig_type])) > + return False > + > + for low_sym, high_sym, cond in sym.ranges: > + if expr_value(cond): > + low_s = low_sym.str_value > + high_s = high_sym.str_value > + > + if not int(low_s, base) <= int(s, base) <= int(high_s, base): > + _error("{} is outside the range {}-{}" > + .format(s, low_s, high_s)) > + return False > + > + break > + > + return True > + > + > +def _range_info(sym): > + # Returns a string with information about the valid range for the symbol > + # 'sym', or None if 'sym' doesn't have a range > + > + if sym.orig_type in (INT, HEX): > + for low, high, cond in sym.ranges: > + if expr_value(cond): > + return "Range: {}-{}".format(low.str_value, high.str_value) > + > + return None > + > + > +def _is_num(name): > + # Heuristic to see if a symbol name looks like a number, for nicer output > + # when printing expressions. Things like 16 are actually symbol names, only > + # they get their name as their value when the symbol is undefined. > + > + try: > + int(name) > + except ValueError: > + if not name.startswith(("0x", "0X")): > + return False > + > + try: > + int(name, 16) > + except ValueError: > + return False > + > + return True > + > + > +def _getch_compat(win): > + # Uses get_wch() if available (Python 3.3+) and getch() otherwise. > + # > + # Also falls back on getch() if get_wch() raises curses.error, to work > + # around an issue when resizing the terminal on at least macOS Catalina. > + # See https://github.com/ulfalizer/Kconfiglib/issues/84. > + # > + # Also handles a PDCurses resizing quirk. > + > + try: > + c = win.get_wch() > + except (AttributeError, curses.error): > + c = win.getch() > + if 0 <= c <= 255: > + c = chr(c) > + > + # Decent resizing behavior on PDCurses requires calling resize_term(0, 0) > + # after receiving KEY_RESIZE, while ncurses (usually) handles terminal > + # resizing automatically in get(_w)ch() (see the end of the > + # resizeterm(3NCURSES) man page). > + # > + # resize_term(0, 0) reliably fails and does nothing on ncurses, so this > + # hack gives ncurses/PDCurses compatibility for resizing. I don't know > + # whether it would cause trouble for other implementations. > + if c == curses.KEY_RESIZE: > + try: > + curses.resize_term(0, 0) > + except curses.error: > + pass > + > + return c > + > + > +def _warn(*args): > + # Temporarily returns from curses to shell mode and prints a warning to > + # stderr. The warning would get lost in curses mode. > + curses.endwin() > + print("menuconfig warning: ", end="", file=sys.stderr) > + print(*args, file=sys.stderr) > + curses.doupdate() > + > + > +# Ignore exceptions from some functions that might fail, e.g. for small > +# windows. They usually do reasonable things anyway. > + > + > +def _safe_curs_set(visibility): > + try: > + curses.curs_set(visibility) > + except curses.error: > + pass > + > + > +def _safe_addstr(win, *args): > + # Clip the line to avoid wrapping to the next line, which looks glitchy. > + # addchstr() would do it for us, but it's not available in the 'curses' > + # module. > + > + attr = None > + if isinstance(args[0], str): > + y, x = win.getyx() > + s = args[0] > + if len(args) == 2: > + attr = args[1] > + else: > + y, x, s = args[:3] > + if len(args) == 4: > + attr = args[3] > + > + maxlen = _width(win) - x > + s = s.expandtabs() > + > + try: > + # The 'curses' module uses wattr_set() internally if you pass 'attr', > + # overwriting the background style, so setting 'attr' to 0 in the first > + # case won't do the right thing > + if attr is None: > + win.addnstr(y, x, s, maxlen) > + else: > + win.addnstr(y, x, s, maxlen, attr) > + except curses.error: > + pass > + > + > +def _safe_addch(win, *args): > + try: > + win.addch(*args) > + except curses.error: > + pass > + > + > +def _safe_hline(win, *args): > + try: > + win.hline(*args) > + except curses.error: > + pass > + > + > +def _safe_vline(win, *args): > + try: > + win.vline(*args) > + except curses.error: > + pass > + > + > +def _safe_move(win, *args): > + try: > + win.move(*args) > + except curses.error: > + pass > + > + > +def _change_c_lc_ctype_to_utf8(): > + # See _CHANGE_C_LC_CTYPE_TO_UTF8 > + > + if _IS_WINDOWS: > + # Windows rarely has issues here, and the PEP 538 implementation avoids > + # changing the locale on it. None of the UTF-8 locales below were > + # supported from some quick testing either. Play it safe. > + return > + > + def try_set_locale(loc): > + try: > + locale.setlocale(locale.LC_CTYPE, loc) > + return True > + except locale.Error: > + return False > + > + # Is LC_CTYPE set to the C locale? > + if locale.setlocale(locale.LC_CTYPE) == "C": > + # This list was taken from the PEP 538 implementation in the CPython > + # code, in Python/pylifecycle.c > + for loc in "C.UTF-8", "C.utf8", "UTF-8": > + if try_set_locale(loc): > + # LC_CTYPE successfully changed > + return > + > + > +if __name__ == "__main__": > + _main() > diff --git a/scripts/Kconfiglib/oldconfig.py b/scripts/Kconfiglib/oldconfig.py > new file mode 100755 > index 0000000..53434b2 > --- /dev/null > +++ b/scripts/Kconfiglib/oldconfig.py > @@ -0,0 +1,246 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Implements oldconfig functionality. > + > + 1. Loads existing .config > + 2. Prompts for the value of all modifiable symbols/choices that > + aren't already set in the .config > + 3. Writes an updated .config > + > +The default input/output filename is '.config'. A different filename can be > +passed in the KCONFIG_CONFIG environment variable. > + > +When overwriting a configuration file, the old version is saved to > +<filename>.old (e.g. .config.old). > + > +Entering '?' displays the help text of the symbol/choice, if any. > + > +Unlike 'make oldconfig', this script doesn't print menu titles and comments, > +but gives Kconfig definition locations. Printing menus and comments would be > +pretty easy to add: Look at the parents of each item, and print all menu > +prompts and comments unless they have already been printed (assuming you want > +to skip "irrelevant" menus). > +""" > +from __future__ import print_function > + > +import sys > + > +from kconfiglib import Symbol, Choice, BOOL, TRISTATE, HEX, standard_kconfig > + > + > +# Python 2/3 compatibility hack > +if sys.version_info[0] < 3: > + input = raw_input > + > + > +def _main(): > + # Earlier symbols in Kconfig files might depend on later symbols and become > + # visible if their values change. This flag is set to True if the value of > + # any symbol changes, in which case we rerun the oldconfig to check for new > + # visible symbols. > + global conf_changed > + > + kconf = standard_kconfig(__doc__) > + print(kconf.load_config()) > + > + while True: > + conf_changed = False > + > + for node in kconf.node_iter(): > + oldconfig(node) > + > + if not conf_changed: > + break > + > + print(kconf.write_config()) > + > + > +def oldconfig(node): > + """ > + Prompts the user for a value if node.item is a visible symbol/choice with > + no user value. > + """ > + # See main() > + global conf_changed > + > + # Only symbols and choices can be configured > + if not isinstance(node.item, (Symbol, Choice)): > + return > + > + # Skip symbols and choices that aren't visible > + if not node.item.visibility: > + return > + > + # Skip symbols and choices that don't have a prompt (at this location) > + if not node.prompt: > + return > + > + if isinstance(node.item, Symbol): > + sym = node.item > + > + # Skip symbols that already have a user value > + if sym.user_value is not None: > + return > + > + # Skip symbols that can only have a single value, due to selects > + if len(sym.assignable) == 1: > + return > + > + # Skip symbols in choices in y mode. We ask once for the entire choice > + # instead. > + if sym.choice and sym.choice.tri_value == 2: > + return > + > + # Loop until the user enters a valid value or enters a blank string > + # (for the default value) > + while True: > + val = input("{} ({}) [{}] ".format( > + node.prompt[0], _name_and_loc_str(sym), > + _default_value_str(sym))) > + > + if val == "?": > + _print_help(node) > + continue > + > + # Substitute a blank string with the default value the symbol > + # would get > + if not val: > + val = sym.str_value > + > + # Automatically add a "0x" prefix for hex symbols, like the > + # menuconfig interface does. This isn't done when loading .config > + # files, hence why set_value() doesn't do it automatically. > + if sym.type == HEX and not val.startswith(("0x", "0X")): > + val = "0x" + val > + > + old_str_val = sym.str_value > + > + # Kconfiglib itself will print a warning here if the value > + # is invalid, so we don't need to bother > + if sym.set_value(val): > + # Valid value input. We're done with this node. > + > + if sym.str_value != old_str_val: > + conf_changed = True > + > + return > + > + else: > + choice = node.item > + > + # Skip choices that already have a visible user selection... > + if choice.user_selection and choice.user_selection.visibility == 2: > + # ...unless there are new visible symbols in the choice. (We know > + # they have y (2) visibility in that case, because m-visible > + # symbols get demoted to n-visibility in y-mode choices, and the > + # user-selected symbol had visibility y.) > + for sym in choice.syms: > + if sym is not choice.user_selection and sym.visibility and \ > + sym.user_value is None: > + # New visible symbols in the choice > + break > + else: > + # No new visible symbols in the choice > + return > + > + # Get a list of available selections. The mode of the choice limits > + # the visibility of the choice value symbols, so this will indirectly > + # skip choices in n and m mode. > + options = [sym for sym in choice.syms if sym.visibility == 2] > + > + if not options: > + # No y-visible choice value symbols > + return > + > + # Loop until the user enters a valid selection or a blank string (for > + # the default selection) > + while True: > + print("{} ({})".format(node.prompt[0], _name_and_loc_str(choice))) > + > + for i, sym in enumerate(options, 1): > + print("{} {}. {} ({})".format( > + ">" if sym is choice.selection else " ", > + i, > + # Assume people don't define choice symbols with multiple > + # prompts. That generates a warning anyway. > + sym.nodes[0].prompt[0], > + sym.name)) > + > + sel_index = input("choice[1-{}]: ".format(len(options))) > + > + if sel_index == "?": > + _print_help(node) > + continue > + > + # Pick the default selection if the string is blank > + if not sel_index: > + choice.selection.set_value(2) > + break > + > + try: > + sel_index = int(sel_index) > + except ValueError: > + print("Bad index", file=sys.stderr) > + continue > + > + if not 1 <= sel_index <= len(options): > + print("Bad index", file=sys.stderr) > + continue > + > + # Valid selection > + > + if options[sel_index - 1].tri_value != 2: > + conf_changed = True > + > + options[sel_index - 1].set_value(2) > + break > + > + # Give all of the non-selected visible choice symbols the user value n. > + # This makes it so that the choice is no longer considered new once we > + # do additional passes, if the reason that it was considered new was > + # that it had new visible choice symbols. > + # > + # Only giving visible choice symbols the user value n means we will > + # prompt for the choice again if later user selections make more new > + # choice symbols visible, which is correct. > + for sym in choice.syms: > + if sym is not choice.user_selection and sym.visibility: > + sym.set_value(0) > + > + > +def _name_and_loc_str(sc): > + # Helper for printing the name of the symbol/choice 'sc' along with the > + # location(s) in the Kconfig files where it is defined. Unnamed choices > + # return "choice" instead of the name. > + > + return "{}, defined at {}".format( > + sc.name or "choice", > + ", ".join("{}:{}".format(node.filename, node.linenr) > + for node in sc.nodes)) > + > + > +def _print_help(node): > + print("\n" + (node.help or "No help text\n")) > + > + > +def _default_value_str(sym): > + # Returns the "m/M/y" string in e.g. > + # > + # TRISTATE_SYM prompt (TRISTATE_SYM, defined at Kconfig:9) [n/M/y]: > + # > + # For string/int/hex, returns the default value as-is. > + > + if sym.type in (BOOL, TRISTATE): > + return "/".join(("NMY" if sym.tri_value == tri else "nmy")[tri] > + for tri in sym.assignable) > + > + # string/int/hex > + return sym.str_value > + > + > +if __name__ == "__main__": > + _main() > diff --git a/scripts/Kconfiglib/olddefconfig.py b/scripts/Kconfiglib/olddefconfig.py > new file mode 100755 > index 0000000..2dadfb4 > --- /dev/null > +++ b/scripts/Kconfiglib/olddefconfig.py > @@ -0,0 +1,28 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2018-2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Updates an old .config file or creates a new one, by filling in default values > +for all new symbols. This is the same as picking the default selection for all > +symbols in oldconfig, or entering the menuconfig interface and immediately > +saving. > + > +The default input/output filename is '.config'. A different filename can be > +passed in the KCONFIG_CONFIG environment variable. > + > +When overwriting a configuration file, the old version is saved to > +<filename>.old (e.g. .config.old). > +""" > +import kconfiglib > + > + > +def main(): > + kconf = kconfiglib.standard_kconfig(__doc__) > + print(kconf.load_config()) > + print(kconf.write_config()) > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/savedefconfig.py b/scripts/Kconfiglib/savedefconfig.py > new file mode 100755 > index 0000000..0f36bde > --- /dev/null > +++ b/scripts/Kconfiglib/savedefconfig.py > @@ -0,0 +1,49 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Saves a minimal configuration file that only lists symbols that differ in value > +from their defaults. Loading such a configuration file is equivalent to loading > +the "full" configuration file. > + > +Minimal configuration files are handy to start from when editing configuration > +files by hand. > + > +The default input configuration file is '.config'. A different input filename > +can be passed in the KCONFIG_CONFIG environment variable. > + > +Note: Minimal configurations can also be generated from within the menuconfig > +interface. > +""" > +import argparse > + > +import kconfiglib > + > + > +def main(): > + parser = argparse.ArgumentParser( > + formatter_class=argparse.RawDescriptionHelpFormatter, > + description=__doc__) > + > + parser.add_argument( > + "--kconfig", > + default="Kconfig", > + help="Top-level Kconfig file (default: Kconfig)") > + > + parser.add_argument( > + "--out", > + metavar="MINIMAL_CONFIGURATION", > + default="defconfig", > + help="Output filename for minimal configuration (default: defconfig)") > + > + args = parser.parse_args() > + > + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) > + print(kconf.load_config()) > + print(kconf.write_min_config(args.out)) > + > + > +if __name__ == "__main__": > + main() > diff --git a/scripts/Kconfiglib/setconfig.py b/scripts/Kconfiglib/setconfig.py > new file mode 100755 > index 0000000..f9cf5cd > --- /dev/null > +++ b/scripts/Kconfiglib/setconfig.py > @@ -0,0 +1,92 @@ > +#!/usr/bin/env python3 > + > +# Copyright (c) 2019, Ulf Magnusson > +# SPDX-License-Identifier: ISC > + > +""" > +Simple utility for setting configuration values from the command line. > + > +Sample usage: > + > + $ setconfig FOO_SUPPORT=y BAR_BITS=8 > + > +Note: Symbol names should not be prefixed with 'CONFIG_'. > + > +The exit status on errors is 1. > + > +The default input/output configuration file is '.config'. A different filename > +can be passed in the KCONFIG_CONFIG environment variable. > + > +When overwriting a configuration file, the old version is saved to > +<filename>.old (e.g. .config.old). > +""" > +import argparse > +import sys > + > +import kconfiglib > + > + > +def main(): > + parser = argparse.ArgumentParser( > + formatter_class=argparse.RawDescriptionHelpFormatter, > + description=__doc__) > + > + parser.add_argument( > + "--kconfig", > + default="Kconfig", > + help="Top-level Kconfig file (default: Kconfig)") > + > + parser.add_argument( > + "--no-check-exists", > + dest="check_exists", > + action="store_false", > + help="Ignore assignments to non-existent symbols instead of erroring " > + "out") > + > + parser.add_argument( > + "--no-check-value", > + dest="check_value", > + action="store_false", > + help="Ignore assignments that didn't \"take\" (where the symbol got a " > + "different value, e.g. due to unsatisfied dependencies) instead " > + "of erroring out") > + > + parser.add_argument( > + "assignments", > + metavar="ASSIGNMENT", > + nargs="*", > + help="A 'NAME=value' assignment") > + > + args = parser.parse_args() > + > + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) > + print(kconf.load_config()) > + > + for arg in args.assignments: > + if "=" not in arg: > + sys.exit("error: no '=' in assignment: '{}'".format(arg)) > + name, value = arg.split("=", 1) > + > + if name not in kconf.syms: > + if not args.check_exists: > + continue > + sys.exit("error: no symbol '{}' in configuration".format(name)) > + > + sym = kconf.syms[name] > + > + if not sym.set_value(value): > + sys.exit("error: '{}' is an invalid value for the {} symbol {}" > + .format(value, kconfiglib.TYPE_TO_STR[sym.orig_type], > + name)) > + > + if args.check_value and sym.str_value != value: > + sys.exit("error: {} was assigned the value '{}', but got the " > + "value '{}'. Check the symbol's dependencies, and make " > + "sure that it has a prompt." > + .format(name, value, sym.str_value)) > + > + print(kconf.write_config()) > + > + > +if __name__ == "__main__": > + main() > -- > 2.45.2 > > -- > You received this message because you are subscribed to the Google Groups "swupdate" group. > To unsubscribe from this group and stop receiving emails from it, send an email to swupdate+unsubscribe@googlegroups.com. > To view this discussion visit https://groups.google.com/d/msgid/swupdate/20250118205226.349745-4-oss%40braunwarth.dev.
Hi Mark, On 19.01.25 17:09, Mark Jonas wrote: > Hi Daniel, > > Instead of copying / forking Kconfiglib, what would speak against a > git submodule? > No, we wouldn't. Submodule is a nightmare used together with other tools like kas, and I want thew project is selfcontained. Regards, Stefano > Cheers, > Mark > > On Sat, Jan 18, 2025 at 9:53 PM Daniel Braunwarth <oss@braunwarth.dev> wrote: >> >> From: Daniel Braunwarth <daniel@braunwarth.dev> >> >> Signed-off-by: Daniel Braunwarth <daniel@braunwarth.dev> >> --- >> scripts/Kconfiglib/alldefconfig.py | 27 + >> scripts/Kconfiglib/allnoconfig.py | 45 + >> scripts/Kconfiglib/allyesconfig.py | 56 + >> scripts/Kconfiglib/defconfig.py | 43 + >> scripts/Kconfiglib/genconfig.py | 154 + >> scripts/Kconfiglib/guiconfig.py | 2324 +++++++++ >> scripts/Kconfiglib/kconfiglib.py | 7160 +++++++++++++++++++++++++++ >> scripts/Kconfiglib/listnewconfig.py | 76 + >> scripts/Kconfiglib/menuconfig.py | 3282 ++++++++++++ >> scripts/Kconfiglib/oldconfig.py | 246 + >> scripts/Kconfiglib/olddefconfig.py | 28 + >> scripts/Kconfiglib/savedefconfig.py | 49 + >> scripts/Kconfiglib/setconfig.py | 92 + >> 13 files changed, 13582 insertions(+) >> create mode 100755 scripts/Kconfiglib/alldefconfig.py >> create mode 100755 scripts/Kconfiglib/allnoconfig.py >> create mode 100755 scripts/Kconfiglib/allyesconfig.py >> create mode 100755 scripts/Kconfiglib/defconfig.py >> create mode 100755 scripts/Kconfiglib/genconfig.py >> create mode 100755 scripts/Kconfiglib/guiconfig.py >> create mode 100644 scripts/Kconfiglib/kconfiglib.py >> create mode 100755 scripts/Kconfiglib/listnewconfig.py >> create mode 100755 scripts/Kconfiglib/menuconfig.py >> create mode 100755 scripts/Kconfiglib/oldconfig.py >> create mode 100755 scripts/Kconfiglib/olddefconfig.py >> create mode 100755 scripts/Kconfiglib/savedefconfig.py >> create mode 100755 scripts/Kconfiglib/setconfig.py >> >> diff --git a/scripts/Kconfiglib/alldefconfig.py b/scripts/Kconfiglib/alldefconfig.py >> new file mode 100755 >> index 0000000..56c4caa >> --- /dev/null >> +++ b/scripts/Kconfiglib/alldefconfig.py >> @@ -0,0 +1,27 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Writes a configuration file where all symbols are set to their their default >> +values. >> + >> +The default output filename is '.config'. A different filename can be passed in >> +the KCONFIG_CONFIG environment variable. >> + >> +Usage for the Linux kernel: >> + >> + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/alldefconfig.py >> +""" >> +import kconfiglib >> + >> + >> +def main(): >> + kconf = kconfiglib.standard_kconfig(__doc__) >> + kconf.load_allconfig("alldef.config") >> + print(kconf.write_config()) >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/allnoconfig.py b/scripts/Kconfiglib/allnoconfig.py >> new file mode 100755 >> index 0000000..de90d8b >> --- /dev/null >> +++ b/scripts/Kconfiglib/allnoconfig.py >> @@ -0,0 +1,45 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Writes a configuration file where as many symbols as possible are set to 'n'. >> + >> +The default output filename is '.config'. A different filename can be passed >> +in the KCONFIG_CONFIG environment variable. >> + >> +Usage for the Linux kernel: >> + >> + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/allnoconfig.py >> +""" >> + >> +# See examples/allnoconfig_walk.py for another way to implement this script >> + >> +import kconfiglib >> + >> + >> +def main(): >> + kconf = kconfiglib.standard_kconfig(__doc__) >> + >> + # Avoid warnings that would otherwise get printed by Kconfiglib for the >> + # following: >> + # >> + # 1. Assigning a value to a symbol without a prompt, which never has any >> + # effect >> + # >> + # 2. Assigning values invalid for the type (only bool/tristate symbols >> + # accept 0/1/2, for n/m/y). The assignments will be ignored for other >> + # symbol types, which is what we want. >> + kconf.warn = False >> + for sym in kconf.unique_defined_syms: >> + sym.set_value(2 if sym.is_allnoconfig_y else 0) >> + kconf.warn = True >> + >> + kconf.load_allconfig("allno.config") >> + >> + print(kconf.write_config()) >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/allyesconfig.py b/scripts/Kconfiglib/allyesconfig.py >> new file mode 100755 >> index 0000000..90eb9b8 >> --- /dev/null >> +++ b/scripts/Kconfiglib/allyesconfig.py >> @@ -0,0 +1,56 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Writes a configuration file where as many symbols as possible are set to 'y'. >> + >> +The default output filename is '.config'. A different filename can be passed >> +in the KCONFIG_CONFIG environment variable. >> + >> +Usage for the Linux kernel: >> + >> + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/allyesconfig.py >> +""" >> +import kconfiglib >> + >> + >> +def main(): >> + kconf = kconfiglib.standard_kconfig(__doc__) >> + >> + # See allnoconfig.py >> + kconf.warn = False >> + >> + # Try to set all symbols to 'y'. Dependencies might truncate the value down >> + # later, but this will at least give the highest possible value. >> + # >> + # Assigning 0/1/2 to non-bool/tristate symbols has no effect (int/hex >> + # symbols still take a string, because they preserve formatting). >> + for sym in kconf.unique_defined_syms: >> + # Set choice symbols to 'm'. This value will be ignored for choices in >> + # 'y' mode (the "normal" mode), which will instead just get their >> + # default selection, but will set all symbols in m-mode choices to 'm', >> + # which is as high as they can go. >> + # >> + # Here's a convoluted example of how you might get an m-mode choice >> + # even during allyesconfig: >> + # >> + # choice >> + # tristate "weird choice" >> + # depends on m >> + sym.set_value(1 if sym.choice else 2) >> + >> + # Set all choices to the highest possible mode >> + for choice in kconf.unique_choices: >> + choice.set_value(2) >> + >> + kconf.warn = True >> + >> + kconf.load_allconfig("allyes.config") >> + >> + print(kconf.write_config()) >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/defconfig.py b/scripts/Kconfiglib/defconfig.py >> new file mode 100755 >> index 0000000..b179273 >> --- /dev/null >> +++ b/scripts/Kconfiglib/defconfig.py >> @@ -0,0 +1,43 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Reads a specified configuration file, then writes a new configuration file. >> +This can be used to initialize the configuration from e.g. an arch-specific >> +configuration file. This input configuration file would usually be a minimal >> +configuration file, as generated by e.g. savedefconfig. >> + >> +The default output filename is '.config'. A different filename can be passed in >> +the KCONFIG_CONFIG environment variable. >> +""" >> +import argparse >> + >> +import kconfiglib >> + >> + >> +def main(): >> + parser = argparse.ArgumentParser( >> + formatter_class=argparse.RawDescriptionHelpFormatter, >> + description=__doc__) >> + >> + parser.add_argument( >> + "--kconfig", >> + default="Kconfig", >> + help="Top-level Kconfig file (default: Kconfig)") >> + >> + parser.add_argument( >> + "config", >> + metavar="CONFIGURATION", >> + help="Input configuration file") >> + >> + args = parser.parse_args() >> + >> + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) >> + print(kconf.load_config(args.config)) >> + print(kconf.write_config()) >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/genconfig.py b/scripts/Kconfiglib/genconfig.py >> new file mode 100755 >> index 0000000..62f065b >> --- /dev/null >> +++ b/scripts/Kconfiglib/genconfig.py >> @@ -0,0 +1,154 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Generates a header file with #defines from the configuration, matching the >> +format of include/generated/autoconf.h in the Linux kernel. >> + >> +Optionally, also writes the configuration output as a .config file. See >> +--config-out. >> + >> +The --sync-deps, --file-list, and --env-list options generate information that >> +can be used to avoid needless rebuilds/reconfigurations. >> + >> +Before writing a header or configuration file, Kconfiglib compares the old >> +contents of the file against the new contents. If there's no change, the write >> +is skipped. This avoids updating file metadata like the modification time, and >> +might save work depending on your build setup. >> + >> +By default, the configuration is generated from '.config'. A different >> +configuration file can be passed in the KCONFIG_CONFIG environment variable. >> + >> +A custom header string can be inserted at the beginning of generated >> +configuration and header files by setting the KCONFIG_CONFIG_HEADER and >> +KCONFIG_AUTOHEADER_HEADER environment variables, respectively (this also works >> +for other scripts). The string is not automatically made a comment (this is by >> +design, to allow anything to be added), and no trailing newline is added, so >> +add '/* */', '#', and newlines as appropriate. >> + >> +See https://www.gnu.org/software/make/manual/make.html#Multi_002dLine for a >> +handy way to define multi-line variables in makefiles, for use with custom >> +headers. Remember to export the variable to the environment. >> +""" >> +import argparse >> +import os >> +import sys >> + >> +import kconfiglib >> + >> + >> +DEFAULT_SYNC_DEPS_PATH = "deps/" >> + >> + >> +def main(): >> + parser = argparse.ArgumentParser( >> + formatter_class=argparse.RawDescriptionHelpFormatter, >> + description=__doc__) >> + >> + parser.add_argument( >> + "--header-path", >> + metavar="HEADER_FILE", >> + help=""" >> +Path to write the generated header file to. If not specified, the path in the >> +environment variable KCONFIG_AUTOHEADER is used if it is set, and 'config.h' >> +otherwise. >> +""") >> + >> + parser.add_argument( >> + "--config-out", >> + metavar="CONFIG_FILE", >> + help=""" >> +Write the configuration to CONFIG_FILE. This is useful if you include .config >> +files in Makefiles, as the generated configuration file will be a full .config >> +file even if .config is outdated. The generated configuration matches what >> +olddefconfig would produce. If you use sync-deps, you can include >> +deps/auto.conf instead. --config-out is meant for cases where incremental build >> +information isn't needed. >> +""") >> + >> + parser.add_argument( >> + "--sync-deps", >> + metavar="OUTPUT_DIR", >> + nargs="?", >> + const=DEFAULT_SYNC_DEPS_PATH, >> + help=""" >> +Enable generation of symbol dependency information for incremental builds, >> +optionally specifying the output directory (default: {}). See the docstring of >> +Kconfig.sync_deps() in Kconfiglib for more information. >> +""".format(DEFAULT_SYNC_DEPS_PATH)) >> + >> + parser.add_argument( >> + "--file-list", >> + metavar="OUTPUT_FILE", >> + help=""" >> +Write a list of all Kconfig files to OUTPUT_FILE, with one file per line. The >> +paths are relative to $srctree (or to the current directory if $srctree is >> +unset). Files appear in the order they're 'source'd. >> +""") >> + >> + parser.add_argument( >> + "--env-list", >> + metavar="OUTPUT_FILE", >> + help=""" >> +Write a list of all environment variables referenced in Kconfig files to >> +OUTPUT_FILE, with one variable per line. Each line has the format NAME=VALUE. >> +Only environment variables referenced with the preprocessor $(VAR) syntax are >> +included, and not variables referenced with the older $VAR syntax (which is >> +only supported for backwards compatibility). >> +""") >> + >> + parser.add_argument( >> + "kconfig", >> + metavar="KCONFIG", >> + nargs="?", >> + default="Kconfig", >> + help="Top-level Kconfig file (default: Kconfig)") >> + >> + args = parser.parse_args() >> + >> + >> + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) >> + kconf.load_config() >> + >> + if args.header_path is None: >> + if "KCONFIG_AUTOHEADER" in os.environ: >> + kconf.write_autoconf() >> + else: >> + # Kconfiglib defaults to include/generated/autoconf.h to be >> + # compatible with the C tools. 'config.h' is used here instead for >> + # backwards compatibility. It's probably a saner default for tools >> + # as well. >> + kconf.write_autoconf("config.h") >> + else: >> + kconf.write_autoconf(args.header_path) >> + >> + if args.config_out is not None: >> + kconf.write_config(args.config_out, save_old=False) >> + >> + if args.sync_deps is not None: >> + kconf.sync_deps(args.sync_deps) >> + >> + if args.file_list is not None: >> + with _open_write(args.file_list) as f: >> + for path in kconf.kconfig_filenames: >> + f.write(path + "\n") >> + >> + if args.env_list is not None: >> + with _open_write(args.env_list) as f: >> + for env_var in kconf.env_vars: >> + f.write("{}={}\n".format(env_var, os.environ[env_var])) >> + >> + >> +def _open_write(path): >> + # Python 2/3 compatibility. io.open() is available on both, but makes >> + # write() expect 'unicode' strings on Python 2. >> + >> + if sys.version_info[0] < 3: >> + return open(path, "w") >> + return open(path, "w", encoding="utf-8") >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/guiconfig.py b/scripts/Kconfiglib/guiconfig.py >> new file mode 100755 >> index 0000000..7804fdc >> --- /dev/null >> +++ b/scripts/Kconfiglib/guiconfig.py >> @@ -0,0 +1,2324 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Overview >> +======== >> + >> +A Tkinter-based menuconfig implementation, based around a treeview control and >> +a help display. The interface should feel familiar to people used to qconf >> +('make xconfig'). Compatible with both Python 2 and Python 3. >> + >> +The display can be toggled between showing the full tree and showing just a >> +single menu (like menuconfig.py). Only single-menu mode distinguishes between >> +symbols defined with 'config' and symbols defined with 'menuconfig'. >> + >> +A show-all mode is available that shows invisible items in red. >> + >> +Supports both mouse and keyboard controls. The following keyboard shortcuts are >> +available: >> + >> + Ctrl-S : Save configuration >> + Ctrl-O : Open configuration >> + Ctrl-A : Toggle show-all mode >> + Ctrl-N : Toggle show-name mode >> + Ctrl-M : Toggle single-menu mode >> + Ctrl-F, /: Open jump-to dialog >> + ESC : Close >> + >> +Running >> +======= >> + >> +guiconfig.py can be run either as a standalone executable or by calling the >> +menuconfig() function with an existing Kconfig instance. The second option is a >> +bit inflexible in that it will still load and save .config, etc. >> + >> +When run in standalone mode, the top-level Kconfig file to load can be passed >> +as a command-line argument. With no argument, it defaults to "Kconfig". >> + >> +The KCONFIG_CONFIG environment variable specifies the .config file to load (if >> +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. >> + >> +When overwriting a configuration file, the old version is saved to >> +<filename>.old (e.g. .config.old). >> + >> +$srctree is supported through Kconfiglib. >> +""" >> + >> +# Note: There's some code duplication with menuconfig.py below, especially for >> +# the help text. Maybe some of it could be moved into kconfiglib.py or a shared >> +# helper script, but OTOH it's pretty nice to have things standalone and >> +# customizable. >> + >> +import errno >> +import os >> +import sys >> + >> +_PY2 = sys.version_info[0] < 3 >> + >> +if _PY2: >> + # Python 2 >> + from Tkinter import * >> + import ttk >> + import tkFont as font >> + import tkFileDialog as filedialog >> + import tkMessageBox as messagebox >> +else: >> + # Python 3 >> + from tkinter import * >> + import tkinter.ttk as ttk >> + import tkinter.font as font >> + from tkinter import filedialog, messagebox >> + >> +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ >> + BOOL, TRISTATE, STRING, INT, HEX, \ >> + AND, OR, \ >> + expr_str, expr_value, split_expr, \ >> + standard_sc_expr_str, \ >> + TRI_TO_STR, TYPE_TO_STR, \ >> + standard_kconfig, standard_config_filename >> + >> + >> +# If True, use GIF image data embedded in this file instead of separate GIF >> +# files. See _load_images(). >> +_USE_EMBEDDED_IMAGES = True >> + >> + >> +# Help text for the jump-to dialog >> +_JUMP_TO_HELP = """\ >> +Type one or more strings/regexes and press Enter to list items that match all >> +of them. Python's regex flavor is used (see the 're' module). Double-clicking >> +an item will jump to it. Item values can be toggled directly within the dialog.\ >> +""" >> + >> + >> +def _main(): >> + menuconfig(standard_kconfig(__doc__)) >> + >> + >> +# Global variables used below: >> +# >> +# _root: >> +# The Toplevel instance for the main window >> +# >> +# _tree: >> +# The Treeview in the main window >> +# >> +# _jump_to_tree: >> +# The Treeview in the jump-to dialog. None if the jump-to dialog isn't >> +# open. Doubles as a flag. >> +# >> +# _jump_to_matches: >> +# List of Nodes shown in the jump-to dialog >> +# >> +# _menupath: >> +# The Label that shows the menu path of the selected item >> +# >> +# _backbutton: >> +# The button shown in single-menu mode for jumping to the parent menu >> +# >> +# _status_label: >> +# Label with status text shown at the bottom of the main window >> +# ("Modified", "Saved to ...", etc.) >> +# >> +# _id_to_node: >> +# We can't use Node objects directly as Treeview item IDs, so we use their >> +# id()s instead. This dictionary maps Node id()s back to Nodes. (The keys >> +# are actually str(id(node)), just to simplify lookups.) >> +# >> +# _cur_menu: >> +# The current menu. Ignored outside single-menu mode. >> +# >> +# _show_all_var/_show_name_var/_single_menu_var: >> +# Tkinter Variable instances bound to the corresponding checkboxes >> +# >> +# _show_all/_single_menu: >> +# Plain Python bools that track _show_all_var and _single_menu_var, to >> +# speed up and simplify things a bit >> +# >> +# _conf_filename: >> +# File to save the configuration to >> +# >> +# _minconf_filename: >> +# File to save minimal configurations to >> +# >> +# _conf_changed: >> +# True if the configuration has been changed. If False, we don't bother >> +# showing the save-and-quit dialog. >> +# >> +# We reset this to False whenever the configuration is saved. >> +# >> +# _*_img: >> +# PhotoImage instances for images >> + >> + >> +def menuconfig(kconf): >> + """ >> + Launches the configuration interface, returning after the user exits. >> + >> + kconf: >> + Kconfig instance to be configured >> + """ >> + global _kconf >> + global _conf_filename >> + global _minconf_filename >> + global _jump_to_tree >> + global _cur_menu >> + >> + _kconf = kconf >> + >> + _jump_to_tree = None >> + >> + _create_id_to_node() >> + >> + _create_ui() >> + >> + # Filename to save configuration to >> + _conf_filename = standard_config_filename() >> + >> + # Load existing configuration and check if it's outdated >> + _set_conf_changed(_load_config()) >> + >> + # Filename to save minimal configuration to >> + _minconf_filename = "defconfig" >> + >> + # Current menu in single-menu mode >> + _cur_menu = _kconf.top_node >> + >> + # Any visible items in the top menu? >> + if not _shown_menu_nodes(kconf.top_node): >> + # Nothing visible. Start in show-all mode and try again. >> + _show_all_var.set(True) >> + if not _shown_menu_nodes(kconf.top_node): >> + # Give up and show an error. It's nice to be able to assume that >> + # the tree is non-empty in the rest of the code. >> + _root.wait_visibility() >> + messagebox.showerror( >> + "Error", >> + "Empty configuration -- nothing to configure.\n\n" >> + "Check that environment variables are set properly.") >> + _root.destroy() >> + return >> + >> + # Build the initial tree >> + _update_tree() >> + >> + # Select the first item and focus the Treeview, so that keyboard controls >> + # work immediately >> + _select(_tree, _tree.get_children()[0]) >> + _tree.focus_set() >> + >> + # Make geometry information available for centering the window. This >> + # indirectly creates the window, so hide it so that it's never shown at the >> + # old location. >> + _root.withdraw() >> + _root.update_idletasks() >> + >> + # Center the window >> + _root.geometry("+{}+{}".format( >> + (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2, >> + (_root.winfo_screenheight() - _root.winfo_reqheight())//2)) >> + >> + # Show it >> + _root.deiconify() >> + >> + # Prevent the window from being automatically resized. Otherwise, it >> + # changes size when scrollbars appear/disappear before the user has >> + # manually resized it. >> + _root.geometry(_root.geometry()) >> + >> + _root.mainloop() >> + >> + >> +def _load_config(): >> + # Loads any existing .config file. See the Kconfig.load_config() docstring. >> + # >> + # Returns True if .config is missing or outdated. We always prompt for >> + # saving the configuration in that case. >> + >> + print(_kconf.load_config()) >> + if not os.path.exists(_conf_filename): >> + # No .config >> + return True >> + >> + return _needs_save() >> + >> + >> +def _needs_save(): >> + # Returns True if a just-loaded .config file is outdated (would get >> + # modified when saving) >> + >> + if _kconf.missing_syms: >> + # Assignments to undefined symbols in the .config >> + return True >> + >> + for sym in _kconf.unique_defined_syms: >> + if sym.user_value is None: >> + if sym.config_string: >> + # Unwritten symbol >> + return True >> + elif sym.orig_type in (BOOL, TRISTATE): >> + if sym.tri_value != sym.user_value: >> + # Written bool/tristate symbol, new value >> + return True >> + elif sym.str_value != sym.user_value: >> + # Written string/int/hex symbol, new value >> + return True >> + >> + # No need to prompt for save >> + return False >> + >> + >> +def _create_id_to_node(): >> + global _id_to_node >> + >> + _id_to_node = {str(id(node)): node for node in _kconf.node_iter()} >> + >> + >> +def _create_ui(): >> + # Creates the main window UI >> + >> + global _root >> + global _tree >> + >> + # Create the root window. This initializes Tkinter and makes e.g. >> + # PhotoImage available, so do it early. >> + _root = Tk() >> + >> + _load_images() >> + _init_misc_ui() >> + _fix_treeview_issues() >> + >> + _create_top_widgets() >> + # Create the pane with the Kconfig tree and description text >> + panedwindow, _tree = _create_kconfig_tree_and_desc(_root) >> + panedwindow.grid(column=0, row=1, sticky="nsew") >> + _create_status_bar() >> + >> + _root.columnconfigure(0, weight=1) >> + # Only the pane with the Kconfig tree and description grows vertically >> + _root.rowconfigure(1, weight=1) >> + >> + # Start with show-name disabled >> + _do_showname() >> + >> + _tree.bind("<Left>", _tree_left_key) >> + _tree.bind("<Right>", _tree_right_key) >> + # Note: Binding this for the jump-to tree as well would cause issues due to >> + # the Tk bug mentioned in _tree_open() >> + _tree.bind("<<TreeviewOpen>>", _tree_open) >> + # add=True to avoid overriding the description text update >> + _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True) >> + >> + _root.bind("<Control-s>", _save) >> + _root.bind("<Control-o>", _open) >> + _root.bind("<Control-a>", _toggle_showall) >> + _root.bind("<Control-n>", _toggle_showname) >> + _root.bind("<Control-m>", _toggle_tree_mode) >> + _root.bind("<Control-f>", _jump_to_dialog) >> + _root.bind("/", _jump_to_dialog) >> + _root.bind("<Escape>", _on_quit) >> + >> + >> +def _load_images(): >> + # Loads GIF images, creating the global _*_img PhotoImage variables. >> + # Base64-encoded images embedded in this script are used if >> + # _USE_EMBEDDED_IMAGES is True, and separate image files in the same >> + # directory as the script otherwise. >> + # >> + # Using a global variable indirectly prevents the image from being >> + # garbage-collected. Passing an image to a Tkinter function isn't enough to >> + # keep it alive. >> + >> + def load_image(name, data): >> + var_name = "_{}_img".format(name) >> + >> + if _USE_EMBEDDED_IMAGES: >> + globals()[var_name] = PhotoImage(data=data, format="gif") >> + else: >> + globals()[var_name] = PhotoImage( >> + file=os.path.join(os.path.dirname(__file__), name + ".gif"), >> + format="gif") >> + >> + # Note: Base64 data can be put on the clipboard with >> + # $ base64 -w0 foo.gif | xclip >> + >> + load_image("icon", "R0lGODlhMAAwAPEDAAAAAADQAO7u7v///yH5BAUKAAMALAAAAAAwADAAAAL/nI+gy+2Pokyv2jazuZxryQjiSJZmyXxHeLbumH6sEATvW8OLNtf5bfLZRLFITzgEipDJ4mYxYv6A0ubuqYhWk66tVTE4enHer7jcKvt0LLUw6P45lvEprT6c0+v7OBuqhYdHohcoqIbSAHc4ljhDwrh1UlgSydRCWWlp5wiYZvmSuSh4IzrqV6p4cwhkCsmY+nhK6uJ6t1mrOhuJqfu6+WYiCiwl7HtLjNSZZZis/MeM7NY3TaRKS40ooDeoiVqIultsrav92bi9c3a5KkkOsOJZpSS99m4k/0zPng4Gks9JSbB+8DIcoQfnjwpZCHv5W+ip4aQrKrB0uOikYhiMCBw1/uPoQUMBADs=") >> + load_image("n_bool", "R0lGODdhEAAQAPAAAAgICP///ywAAAAAEAAQAAACIISPacHtvp5kcb5qG85hZ2+BkyiRF8BBaEqtrKkqslEAADs=") >> + load_image("y_bool", "R0lGODdhEAAQAPEAAAgICADQAP///wAAACwAAAAAEAAQAAACMoSPacLtvlh4YrIYsst2cV19AvaVF9CUXBNJJoum7ymrsKuCnhiupIWjSSjAFuWhSCIKADs=") >> + load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=") >> + load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=") >> + load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7") >> + load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=") >> + load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==") >> + load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==") >> + load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==") >> + load_image("y_locked", "R0lGODlhEAAQAPD/AAAAAADQACH5BAUKAAIALAAAAAAQABAAAAIylC8AyKzNgnlCtoDTwvZwrHydIYpQmR3KWq4uK74IOnp0HQPmnD3cOVlUIAgKsShkFAAAOw==") >> + load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7") >> + load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=") >> + load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=") >> + >> + >> +def _fix_treeview_issues(): >> + # Fixes some Treeview issues >> + >> + global _treeview_rowheight >> + >> + style = ttk.Style() >> + >> + # The treeview rowheight isn't adjusted automatically on high-DPI displays, >> + # so do it ourselves. The font will probably always be TkDefaultFont, but >> + # play it safe and look it up. >> + >> + _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \ >> + .metrics("linespace") + 2 >> + >> + style.configure("Treeview", rowheight=_treeview_rowheight) >> + >> + # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae, >> + # which breaks tag background colors >> + >> + for option in "foreground", "background": >> + # Filter out any styles starting with ("!disabled", "!selected", ...). >> + # style.map() returns an empty list for missing options, so this should >> + # be future-safe. >> + style.map( >> + "Treeview", >> + **{option: [elm for elm in style.map("Treeview", query_opt=option) >> + if elm[:2] != ("!disabled", "!selected")]}) >> + >> + >> +def _init_misc_ui(): >> + # Does misc. UI initialization, like setting the title, icon, and theme >> + >> + _root.title(_kconf.mainmenu_text) >> + # iconphoto() isn't available in Python 2's Tkinter >> + _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img) >> + # Reducing the width of the window to 1 pixel makes it move around, at >> + # least on GNOME. Prevent weird stuff like that. >> + _root.minsize(128, 128) >> + _root.protocol("WM_DELETE_WINDOW", _on_quit) >> + >> + # Use the 'clam' theme on *nix if it's available. It looks nicer than the >> + # 'default' theme. >> + if _root.tk.call("tk", "windowingsystem") == "x11": >> + style = ttk.Style() >> + if "clam" in style.theme_names(): >> + style.theme_use("clam") >> + >> + >> +def _create_top_widgets(): >> + # Creates the controls above the Kconfig tree in the main window >> + >> + global _show_all_var >> + global _show_name_var >> + global _single_menu_var >> + global _menupath >> + global _backbutton >> + >> + topframe = ttk.Frame(_root) >> + topframe.grid(column=0, row=0, sticky="ew") >> + >> + ttk.Button(topframe, text="Save", command=_save) \ >> + .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c") >> + >> + ttk.Button(topframe, text="Save as...", command=_save_as) \ >> + .grid(column=1, row=0, sticky="ew") >> + >> + ttk.Button(topframe, text="Save minimal (advanced)...", >> + command=_save_minimal) \ >> + .grid(column=2, row=0, sticky="ew", padx=".05c") >> + >> + ttk.Button(topframe, text="Open...", command=_open) \ >> + .grid(column=3, row=0) >> + >> + ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \ >> + .grid(column=4, row=0, padx=".05c") >> + >> + _show_name_var = BooleanVar() >> + ttk.Checkbutton(topframe, text="Show name", command=_do_showname, >> + variable=_show_name_var) \ >> + .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c", >> + ipady=".2c") >> + >> + _show_all_var = BooleanVar() >> + ttk.Checkbutton(topframe, text="Show all", command=_do_showall, >> + variable=_show_all_var) \ >> + .grid(column=1, row=1, sticky="nsew", pady="0 .05c") >> + >> + # Allow the show-all and single-menu status to be queried via plain global >> + # Python variables, which is faster and simpler >> + >> + def show_all_updated(*_): >> + global _show_all >> + _show_all = _show_all_var.get() >> + >> + _trace_write(_show_all_var, show_all_updated) >> + _show_all_var.set(False) >> + >> + _single_menu_var = BooleanVar() >> + ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode, >> + variable=_single_menu_var) \ >> + .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c") >> + >> + _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu, >> + state="disabled") >> + _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c") >> + >> + def tree_mode_updated(*_): >> + global _single_menu >> + _single_menu = _single_menu_var.get() >> + >> + if _single_menu: >> + _backbutton.grid() >> + else: >> + _backbutton.grid_remove() >> + >> + _trace_write(_single_menu_var, tree_mode_updated) >> + _single_menu_var.set(False) >> + >> + # Column to the right of the buttons that the menu path extends into, so >> + # that it can grow wider than the buttons >> + topframe.columnconfigure(5, weight=1) >> + >> + _menupath = ttk.Label(topframe) >> + _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c", >> + pady="0 .05c") >> + >> + >> +def _create_kconfig_tree_and_desc(parent): >> + # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text >> + # that shows a description of the selected node. Returns a tuple with the >> + # Panedwindow and the Treeview. This code is shared between the main window >> + # and the jump-to dialog. >> + >> + panedwindow = ttk.Panedwindow(parent, orient=VERTICAL) >> + >> + tree_frame, tree = _create_kconfig_tree(panedwindow) >> + desc_frame, desc = _create_kconfig_desc(panedwindow) >> + >> + panedwindow.add(tree_frame, weight=1) >> + panedwindow.add(desc_frame) >> + >> + def tree_select(_): >> + # The Text widget does not allow editing the text in its disabled >> + # state. We need to temporarily enable it. >> + desc["state"] = "normal" >> + >> + sel = tree.selection() >> + if not sel: >> + desc.delete("1.0", "end") >> + desc["state"] = "disabled" >> + return >> + >> + # Text.replace() is not available in Python 2's Tkinter >> + desc.delete("1.0", "end") >> + desc.insert("end", _info_str(_id_to_node[sel[0]])) >> + >> + desc["state"] = "disabled" >> + >> + tree.bind("<<TreeviewSelect>>", tree_select) >> + tree.bind("<1>", _tree_click) >> + tree.bind("<Double-1>", _tree_double_click) >> + tree.bind("<Return>", _tree_enter) >> + tree.bind("<KP_Enter>", _tree_enter) >> + tree.bind("<space>", _tree_toggle) >> + tree.bind("n", _tree_set_val(0)) >> + tree.bind("m", _tree_set_val(1)) >> + tree.bind("y", _tree_set_val(2)) >> + >> + return panedwindow, tree >> + >> + >> +def _create_kconfig_tree(parent): >> + # Creates a Treeview for showing Kconfig nodes >> + >> + frame = ttk.Frame(parent) >> + >> + tree = ttk.Treeview(frame, selectmode="browse", height=20, >> + columns=("name",)) >> + tree.heading("#0", text="Option", anchor="w") >> + tree.heading("name", text="Name", anchor="w") >> + >> + tree.tag_configure("n-bool", image=_n_bool_img) >> + tree.tag_configure("y-bool", image=_y_bool_img) >> + tree.tag_configure("m-tri", image=_m_tri_img) >> + tree.tag_configure("n-tri", image=_n_tri_img) >> + tree.tag_configure("m-tri", image=_m_tri_img) >> + tree.tag_configure("y-tri", image=_y_tri_img) >> + tree.tag_configure("m-my", image=_m_my_img) >> + tree.tag_configure("y-my", image=_y_my_img) >> + tree.tag_configure("n-locked", image=_n_locked_img) >> + tree.tag_configure("m-locked", image=_m_locked_img) >> + tree.tag_configure("y-locked", image=_y_locked_img) >> + tree.tag_configure("not-selected", image=_not_selected_img) >> + tree.tag_configure("selected", image=_selected_img) >> + tree.tag_configure("edit", image=_edit_img) >> + tree.tag_configure("invisible", foreground="red") >> + >> + tree.grid(column=0, row=0, sticky="nsew") >> + >> + _add_vscrollbar(frame, tree) >> + >> + frame.columnconfigure(0, weight=1) >> + frame.rowconfigure(0, weight=1) >> + >> + # Create items for all menu nodes. These can be detached/moved later. >> + # Micro-optimize this a bit. >> + insert = tree.insert >> + id_ = id >> + Symbol_ = Symbol >> + for node in _kconf.node_iter(): >> + item = node.item >> + insert("", "end", iid=id_(node), >> + values=item.name if item.__class__ is Symbol_ else "") >> + >> + return frame, tree >> + >> + >> +def _create_kconfig_desc(parent): >> + # Creates a Text for showing the description of the selected Kconfig node >> + >> + frame = ttk.Frame(parent) >> + >> + desc = Text(frame, height=12, wrap="none", borderwidth=0, >> + state="disabled") >> + desc.grid(column=0, row=0, sticky="nsew") >> + >> + # Work around not being to Ctrl-C/V text from a disabled Text widget, with a >> + # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only >> + desc.bind("<1>", lambda _: desc.focus_set()) >> + >> + _add_vscrollbar(frame, desc) >> + >> + frame.columnconfigure(0, weight=1) >> + frame.rowconfigure(0, weight=1) >> + >> + return frame, desc >> + >> + >> +def _add_vscrollbar(parent, widget): >> + # Adds a vertical scrollbar to 'widget' that's only shown as needed >> + >> + vscrollbar = ttk.Scrollbar(parent, orient="vertical", >> + command=widget.yview) >> + vscrollbar.grid(column=1, row=0, sticky="ns") >> + >> + def yscrollcommand(first, last): >> + # Only show the scrollbar when needed. 'first' and 'last' are >> + # strings. >> + if float(first) <= 0.0 and float(last) >= 1.0: >> + vscrollbar.grid_remove() >> + else: >> + vscrollbar.grid() >> + >> + vscrollbar.set(first, last) >> + >> + widget["yscrollcommand"] = yscrollcommand >> + >> + >> +def _create_status_bar(): >> + # Creates the status bar at the bottom of the main window >> + >> + global _status_label >> + >> + _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0") >> + _status_label.grid(column=0, row=3, sticky="ew") >> + >> + >> +def _set_status(s): >> + # Sets the text in the status bar to 's' >> + >> + _status_label["text"] = s >> + >> + >> +def _set_conf_changed(changed): >> + # Updates the status re. whether there are unsaved changes >> + >> + global _conf_changed >> + >> + _conf_changed = changed >> + if changed: >> + _set_status("Modified") >> + >> + >> +def _update_tree(): >> + # Updates the Kconfig tree in the main window by first detaching all nodes >> + # and then updating and reattaching them. The tree structure might have >> + # changed. >> + >> + # If a selected/focused item is detached and later reattached, it stays >> + # selected/focused. That can give multiple selections even though >> + # selectmode=browse. Save and later restore the selection and focus as a >> + # workaround. >> + old_selection = _tree.selection() >> + old_focus = _tree.focus() >> + >> + # Detach all tree items before re-stringing them. This is relatively fast, >> + # luckily. >> + _tree.detach(*_id_to_node.keys()) >> + >> + if _single_menu: >> + _build_menu_tree() >> + else: >> + _build_full_tree(_kconf.top_node) >> + >> + _tree.selection_set(old_selection) >> + _tree.focus(old_focus) >> + >> + >> +def _build_full_tree(menu): >> + # Updates the tree starting from menu.list, in full-tree mode. To speed >> + # things up, only open menus are updated. The menu-at-a-time logic here is >> + # to deal with invisible items that can show up outside show-all mode (see >> + # _shown_full_nodes()). >> + >> + for node in _shown_full_nodes(menu): >> + _add_to_tree(node, _kconf.top_node) >> + >> + # _shown_full_nodes() includes nodes from menus rooted at symbols, so >> + # we only need to check "real" menus/choices here >> + if node.list and not isinstance(node.item, Symbol): >> + if _tree.item(id(node), "open"): >> + _build_full_tree(node) >> + else: >> + # We're just probing here, so _shown_menu_nodes() will work >> + # fine, and might be a bit faster >> + shown = _shown_menu_nodes(node) >> + if shown: >> + # Dummy element to make the open/closed toggle appear >> + _tree.move(id(shown[0]), id(shown[0].parent), "end") >> + >> + >> +def _shown_full_nodes(menu): >> + # Returns the list of menu nodes shown in 'menu' (a menu node for a menu) >> + # for full-tree mode. A tricky detail is that invisible items need to be >> + # shown if they have visible children. >> + >> + def rec(node): >> + res = [] >> + >> + while node: >> + if _visible(node) or _show_all: >> + res.append(node) >> + if node.list and isinstance(node.item, Symbol): >> + # Nodes from menu created from dependencies >> + res += rec(node.list) >> + >> + elif node.list and isinstance(node.item, Symbol): >> + # Show invisible symbols (defined with either 'config' and >> + # 'menuconfig') if they have visible children. This can happen >> + # for an m/y-valued symbol with an optional prompt >> + # ('prompt "foo" is COND') that is currently disabled. >> + shown_children = rec(node.list) >> + if shown_children: >> + res.append(node) >> + res += shown_children >> + >> + node = node.next >> + >> + return res >> + >> + return rec(menu.list) >> + >> + >> +def _build_menu_tree(): >> + # Updates the tree in single-menu mode. See _build_full_tree() as well. >> + >> + for node in _shown_menu_nodes(_cur_menu): >> + _add_to_tree(node, _cur_menu) >> + >> + >> +def _shown_menu_nodes(menu): >> + # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't >> + # include children of symbols defined with 'menuconfig'. >> + >> + def rec(node): >> + res = [] >> + >> + while node: >> + if _visible(node) or _show_all: >> + res.append(node) >> + if node.list and not node.is_menuconfig: >> + res += rec(node.list) >> + >> + elif node.list and isinstance(node.item, Symbol): >> + shown_children = rec(node.list) >> + if shown_children: >> + # Invisible item with visible children >> + res.append(node) >> + if not node.is_menuconfig: >> + res += shown_children >> + >> + node = node.next >> + >> + return res >> + >> + return rec(menu.list) >> + >> + >> +def _visible(node): >> + # Returns True if the node should appear in the menu (outside show-all >> + # mode) >> + >> + return node.prompt and expr_value(node.prompt[1]) and not \ >> + (node.item == MENU and not expr_value(node.visibility)) >> + >> + >> +def _add_to_tree(node, top): >> + # Adds 'node' to the tree, at the end of its menu. We rely on going through >> + # the nodes linearly to get the correct order. 'top' holds the menu that >> + # corresponds to the top-level menu, and can vary in single-menu mode. >> + >> + parent = node.parent >> + _tree.move(id(node), "" if parent is top else id(parent), "end") >> + _tree.item( >> + id(node), >> + text=_node_str(node), >> + # The _show_all test avoids showing invisible items in red outside >> + # show-all mode, which could look confusing/broken. Invisible symbols >> + # are shown outside show-all mode if an invisible symbol has visible >> + # children in an implicit menu. >> + tags=_img_tag(node) if _visible(node) or not _show_all else >> + _img_tag(node) + " invisible") >> + >> + >> +def _node_str(node): >> + # Returns the string shown to the right of the image (if any) for the node >> + >> + if node.prompt: >> + if node.item == COMMENT: >> + s = "*** {} ***".format(node.prompt[0]) >> + else: >> + s = node.prompt[0] >> + >> + if isinstance(node.item, Symbol): >> + sym = node.item >> + >> + # Print "(NEW)" next to symbols without a user value (from e.g. a >> + # .config), but skip it for choice symbols in choices in y mode, >> + # and for symbols of UNKNOWN type (which generate a warning though) >> + if sym.user_value is None and sym.type and not \ >> + (sym.choice and sym.choice.tri_value == 2): >> + >> + s += " (NEW)" >> + >> + elif isinstance(node.item, Symbol): >> + # Symbol without prompt (can show up in show-all) >> + s = "<{}>".format(node.item.name) >> + >> + else: >> + # Choice without prompt. Use standard_sc_expr_str() so that it shows up >> + # as '<choice (name if any)>'. >> + s = standard_sc_expr_str(node.item) >> + >> + >> + if isinstance(node.item, Symbol): >> + sym = node.item >> + if sym.orig_type == STRING: >> + s += ": " + sym.str_value >> + elif sym.orig_type in (INT, HEX): >> + s = "({}) {}".format(sym.str_value, s) >> + >> + elif isinstance(node.item, Choice) and node.item.tri_value == 2: >> + # Print the prompt of the selected symbol after the choice for >> + # choices in y mode >> + sym = node.item.selection >> + if sym: >> + for sym_node in sym.nodes: >> + # Use the prompt used at this choice location, in case the >> + # choice symbol is defined in multiple locations >> + if sym_node.parent is node and sym_node.prompt: >> + s += " ({})".format(sym_node.prompt[0]) >> + break >> + else: >> + # If the symbol isn't defined at this choice location, then >> + # just use whatever prompt we can find for it >> + for sym_node in sym.nodes: >> + if sym_node.prompt: >> + s += " ({})".format(sym_node.prompt[0]) >> + break >> + >> + # In single-menu mode, print "--->" next to nodes that have menus that can >> + # potentially be entered. Print "----" if the menu is empty. We don't allow >> + # those to be entered. >> + if _single_menu and node.is_menuconfig: >> + s += " --->" if _shown_menu_nodes(node) else " ----" >> + >> + return s >> + >> + >> +def _img_tag(node): >> + # Returns the tag for the image that should be shown next to 'node', or the >> + # empty string if it shouldn't have an image >> + >> + item = node.item >> + >> + if item in (MENU, COMMENT) or not item.orig_type: >> + return "" >> + >> + if item.orig_type in (STRING, INT, HEX): >> + return "edit" >> + >> + # BOOL or TRISTATE >> + >> + if _is_y_mode_choice_sym(item): >> + # Choice symbol in y-mode choice >> + return "selected" if item.choice.selection is item else "not-selected" >> + >> + if len(item.assignable) <= 1: >> + # Pinned to a single value >> + return "" if isinstance(item, Choice) else item.str_value + "-locked" >> + >> + if item.type == BOOL: >> + return item.str_value + "-bool" >> + >> + # item.type == TRISTATE >> + if item.assignable == (1, 2): >> + return item.str_value + "-my" >> + return item.str_value + "-tri" >> + >> + >> +def _is_y_mode_choice_sym(item): >> + # 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 >> + return isinstance(item, Symbol) and item.choice and item.visibility == 2 >> + >> + >> +def _tree_click(event): >> + # Click on the Kconfig Treeview >> + >> + tree = event.widget >> + if tree.identify_element(event.x, event.y) == "image": >> + item = tree.identify_row(event.y) >> + # Select the item before possibly popping up a dialog for >> + # string/int/hex items, so that its help is visible >> + _select(tree, item) >> + _change_node(_id_to_node[item], tree.winfo_toplevel()) >> + return "break" >> + >> + >> +def _tree_double_click(event): >> + # Double-click on the Kconfig treeview >> + >> + # Do an extra check to avoid weirdness when double-clicking in the tree >> + # heading area >> + if not _in_heading(event): >> + return _tree_enter(event) >> + >> + >> +def _in_heading(event): >> + # Returns True if 'event' took place in the tree heading >> + >> + tree = event.widget >> + return hasattr(tree, "identify_region") and \ >> + tree.identify_region(event.x, event.y) in ("heading", "separator") >> + >> + >> +def _tree_enter(event): >> + # Enter press or double-click within the Kconfig treeview. Prefer to >> + # open/close/enter menus, but toggle the value if that's not possible. >> + >> + tree = event.widget >> + sel = tree.focus() >> + if sel: >> + node = _id_to_node[sel] >> + >> + if tree.get_children(sel): >> + _tree_toggle_open(sel) >> + elif _single_menu_mode_menu(node, tree): >> + _enter_menu_and_select_first(node) >> + else: >> + _change_node(node, tree.winfo_toplevel()) >> + >> + return "break" >> + >> + >> +def _tree_toggle(event): >> + # Space press within the Kconfig treeview. Prefer to toggle the value, but >> + # open/close/enter the menu if that's not possible. >> + >> + tree = event.widget >> + sel = tree.focus() >> + if sel: >> + node = _id_to_node[sel] >> + >> + if _changeable(node): >> + _change_node(node, tree.winfo_toplevel()) >> + elif _single_menu_mode_menu(node, tree): >> + _enter_menu_and_select_first(node) >> + elif tree.get_children(sel): >> + _tree_toggle_open(sel) >> + >> + return "break" >> + >> + >> +def _tree_left_key(_): >> + # Left arrow key press within the Kconfig treeview >> + >> + if _single_menu: >> + # Leave the current menu in single-menu mode >> + _leave_menu() >> + return "break" >> + >> + # Otherwise, default action >> + >> + >> +def _tree_right_key(_): >> + # Right arrow key press within the Kconfig treeview >> + >> + sel = _tree.focus() >> + if sel: >> + node = _id_to_node[sel] >> + # If the node can be entered in single-menu mode, do it >> + if _single_menu_mode_menu(node, _tree): >> + _enter_menu_and_select_first(node) >> + return "break" >> + >> + # Otherwise, default action >> + >> + >> +def _single_menu_mode_menu(node, tree): >> + # Returns True if single-menu mode is on and 'node' is an (interface) >> + # menu that can be entered >> + >> + return _single_menu and tree is _tree and node.is_menuconfig and \ >> + _shown_menu_nodes(node) >> + >> + >> +def _changeable(node): >> + # Returns True if 'node' is a Symbol/Choice whose value can be changed >> + >> + sc = node.item >> + >> + if not isinstance(sc, (Symbol, Choice)): >> + return False >> + >> + # This will hit for invisible symbols, which appear in show-all mode and >> + # when an invisible symbol has visible children (which can happen e.g. for >> + # symbols with optional prompts) >> + if not (node.prompt and expr_value(node.prompt[1])): >> + return False >> + >> + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ >> + or _is_y_mode_choice_sym(sc) >> + >> + >> +def _tree_toggle_open(item): >> + # Opens/closes the Treeview item 'item' >> + >> + if _tree.item(item, "open"): >> + _tree.item(item, open=False) >> + else: >> + node = _id_to_node[item] >> + if not isinstance(node.item, Symbol): >> + # Can only get here in full-tree mode >> + _build_full_tree(node) >> + _tree.item(item, open=True) >> + >> + >> +def _tree_set_val(tri_val): >> + def tree_set_val(event): >> + # n/m/y press within the Kconfig treeview >> + >> + # Sets the value of the currently selected item to 'tri_val', if that >> + # value can be assigned >> + >> + sel = event.widget.focus() >> + if sel: >> + sc = _id_to_node[sel].item >> + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: >> + _set_val(sc, tri_val) >> + >> + return tree_set_val >> + >> + >> +def _tree_open(_): >> + # Lazily populates the Kconfig tree when menus are opened in full-tree mode >> + >> + if _single_menu: >> + # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e >> + # ("ttk::treeview open/closed indicators can be toggled while hidden"). >> + # Clicking on the hidden indicator will call _build_full_tree() in >> + # single-menu mode otherwise. >> + return >> + >> + node = _id_to_node[_tree.focus()] >> + # _shown_full_nodes() includes nodes from menus rooted at symbols, so we >> + # only need to check "real" menus and choices here >> + if not isinstance(node.item, Symbol): >> + _build_full_tree(node) >> + >> + >> +def _update_menu_path(_): >> + # Updates the displayed menu path when nodes are selected in the Kconfig >> + # treeview >> + >> + sel = _tree.selection() >> + _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else "" >> + >> + >> +def _item_row(item): >> + # Returns the row number 'item' appears on within the Kconfig treeview, >> + # starting from the top of the tree. Used to preserve scrolling. >> + # >> + # ttkTreeview.c in the Tk sources defines a RowNumber() function that does >> + # the same thing, but it's not exposed. >> + >> + row = 0 >> + >> + while True: >> + prev = _tree.prev(item) >> + if prev: >> + item = prev >> + row += _n_rows(item) >> + else: >> + item = _tree.parent(item) >> + if not item: >> + return row >> + row += 1 >> + >> + >> +def _n_rows(item): >> + # _item_row() helper. Returns the number of rows occupied by 'item' and # >> + # its children. >> + >> + rows = 1 >> + >> + if _tree.item(item, "open"): >> + for child in _tree.get_children(item): >> + rows += _n_rows(child) >> + >> + return rows >> + >> + >> +def _attached(item): >> + # Heuristic for checking if a Treeview item is attached. Doesn't seem to be >> + # good APIs for this. Might fail for super-obscure cases with tiny trees, >> + # but you'd just get a small scroll mess-up. >> + >> + return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item)) >> + >> + >> +def _change_node(node, parent): >> + # Toggles/changes the value of 'node'. 'parent' is the parent window >> + # (either the main window or the jump-to dialog), in case we need to pop up >> + # a dialog. >> + >> + if not _changeable(node): >> + return >> + >> + # sc = symbol/choice >> + sc = node.item >> + >> + if sc.type in (INT, HEX, STRING): >> + s = _set_val_dialog(node, parent) >> + >> + # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib >> + # can't deal with. UTF-8-encode the string to work around it. >> + if _PY2 and isinstance(s, unicode): >> + s = s.encode("utf-8", "ignore") >> + >> + if s is not None: >> + _set_val(sc, s) >> + >> + elif len(sc.assignable) == 1: >> + # Handles choice symbols for choices in y mode, which are a special >> + # case: .assignable can be (2,) while .tri_value is 0. >> + _set_val(sc, sc.assignable[0]) >> + >> + else: >> + # Set the symbol to the value after the current value in >> + # sc.assignable, with wrapping >> + val_index = sc.assignable.index(sc.tri_value) >> + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) >> + >> + >> +def _set_val(sc, val): >> + # Wrapper around Symbol/Choice.set_value() for updating the menu state and >> + # _conf_changed >> + >> + # Use the string representation of tristate values. This makes the format >> + # consistent for all symbol types. >> + if val in TRI_TO_STR: >> + val = TRI_TO_STR[val] >> + >> + if val != sc.str_value: >> + sc.set_value(val) >> + _set_conf_changed(True) >> + >> + # Update the tree and try to preserve the scroll. Do a cheaper variant >> + # than in the show-all case, that might mess up the scroll slightly in >> + # rare cases, but is fast and flicker-free. >> + >> + stayput = _loc_ref_item() # Item to preserve scroll for >> + old_row = _item_row(stayput) >> + >> + _update_tree() >> + >> + # If the reference item disappeared (can happen if the change was done >> + # from the jump-to dialog), then avoid messing with the scroll and hope >> + # for the best >> + if _attached(stayput): >> + _tree.yview_scroll(_item_row(stayput) - old_row, "units") >> + >> + if _jump_to_tree: >> + _update_jump_to_display() >> + >> + >> +def _set_val_dialog(node, parent): >> + # Pops up a dialog for setting the value of the string/int/hex >> + # symbol at node 'node'. 'parent' is the parent window. >> + >> + def ok(_=None): >> + # No 'nonlocal' in Python 2 >> + global _entry_res >> + >> + s = entry.get() >> + if sym.type == HEX and not s.startswith(("0x", "0X")): >> + s = "0x" + s >> + >> + if _check_valid(dialog, entry, sym, s): >> + _entry_res = s >> + dialog.destroy() >> + >> + def cancel(_=None): >> + global _entry_res >> + _entry_res = None >> + dialog.destroy() >> + >> + sym = node.item >> + >> + dialog = Toplevel(parent) >> + dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type])) >> + dialog.resizable(False, False) >> + dialog.transient(parent) >> + dialog.protocol("WM_DELETE_WINDOW", cancel) >> + >> + ttk.Label(dialog, text=node.prompt[0] + ":") \ >> + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c", >> + pady=".2c .05c") >> + >> + entry = ttk.Entry(dialog, width=30) >> + # Start with the previous value in the editbox, selected >> + entry.insert(0, sym.str_value) >> + entry.selection_range(0, "end") >> + entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c") >> + entry.focus_set() >> + >> + range_info = _range_info(sym) >> + if range_info: >> + ttk.Label(dialog, text=range_info) \ >> + .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c", >> + pady=".2c 0") >> + >> + ttk.Button(dialog, text="OK", command=ok) \ >> + .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c", >> + pady=".4c") >> + >> + ttk.Button(dialog, text="Cancel", command=cancel) \ >> + .grid(column=1, row=4 if range_info else 3, padx="0 .3c") >> + >> + # Give all horizontal space to the grid cell with the OK button, so that >> + # Cancel moves to the right >> + dialog.columnconfigure(0, weight=1) >> + >> + _center_on_root(dialog) >> + >> + # Hack to scroll the entry so that the end of the text is shown, from >> + # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail. >> + # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff >> + def scroll_entry(_): >> + _root.update_idletasks() >> + entry.unbind("<Expose>") >> + entry.xview_moveto(1) >> + entry.bind("<Expose>", scroll_entry) >> + >> + # The dialog must be visible before we can grab the input >> + dialog.wait_visibility() >> + dialog.grab_set() >> + >> + dialog.bind("<Return>", ok) >> + dialog.bind("<KP_Enter>", ok) >> + dialog.bind("<Escape>", cancel) >> + >> + # Wait for the user to be done with the dialog >> + parent.wait_window(dialog) >> + >> + # Regrab the input in the parent >> + parent.grab_set() >> + >> + return _entry_res >> + >> + >> +def _center_on_root(dialog): >> + # Centers 'dialog' on the root window. It often ends up at some bad place >> + # like the top-left corner of the screen otherwise. See the menuconfig() >> + # function, which has similar logic. >> + >> + dialog.withdraw() >> + _root.update_idletasks() >> + >> + dialog_width = dialog.winfo_reqwidth() >> + dialog_height = dialog.winfo_reqheight() >> + >> + screen_width = _root.winfo_screenwidth() >> + screen_height = _root.winfo_screenheight() >> + >> + x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2 >> + y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2 >> + >> + # Clamp so that no part of the dialog is outside the screen >> + if x + dialog_width > screen_width: >> + x = screen_width - dialog_width >> + elif x < 0: >> + x = 0 >> + if y + dialog_height > screen_height: >> + y = screen_height - dialog_height >> + elif y < 0: >> + y = 0 >> + >> + dialog.geometry("+{}+{}".format(x, y)) >> + >> + dialog.deiconify() >> + >> + >> +def _check_valid(dialog, entry, sym, s): >> + # Returns True if the string 's' is a well-formed value for 'sym'. >> + # Otherwise, pops up an error and returns False. >> + >> + if sym.type not in (INT, HEX): >> + # Anything goes for non-int/hex symbols >> + return True >> + >> + base = 10 if sym.type == INT else 16 >> + try: >> + int(s, base) >> + except ValueError: >> + messagebox.showerror( >> + "Bad value", >> + "'{}' is a malformed {} value".format( >> + s, TYPE_TO_STR[sym.type]), >> + parent=dialog) >> + entry.focus_set() >> + return False >> + >> + for low_sym, high_sym, cond in sym.ranges: >> + if expr_value(cond): >> + low_s = low_sym.str_value >> + high_s = high_sym.str_value >> + >> + if not int(low_s, base) <= int(s, base) <= int(high_s, base): >> + messagebox.showerror( >> + "Value out of range", >> + "{} is outside the range {}-{}".format(s, low_s, high_s), >> + parent=dialog) >> + entry.focus_set() >> + return False >> + >> + break >> + >> + return True >> + >> + >> +def _range_info(sym): >> + # Returns a string with information about the valid range for the symbol >> + # 'sym', or None if 'sym' doesn't have a range >> + >> + if sym.type in (INT, HEX): >> + for low, high, cond in sym.ranges: >> + if expr_value(cond): >> + return "Range: {}-{}".format(low.str_value, high.str_value) >> + >> + return None >> + >> + >> +def _save(_=None): >> + # Tries to save the configuration >> + >> + if _try_save(_kconf.write_config, _conf_filename, "configuration"): >> + _set_conf_changed(False) >> + >> + _tree.focus_set() >> + >> + >> +def _save_as(): >> + # Pops up a dialog for saving the configuration to a specific location >> + >> + global _conf_filename >> + >> + filename = _conf_filename >> + while True: >> + filename = filedialog.asksaveasfilename( >> + title="Save configuration as", >> + initialdir=os.path.dirname(filename), >> + initialfile=os.path.basename(filename), >> + parent=_root) >> + >> + if not filename: >> + break >> + >> + if _try_save(_kconf.write_config, filename, "configuration"): >> + _conf_filename = filename >> + break >> + >> + _tree.focus_set() >> + >> + >> +def _save_minimal(): >> + # Pops up a dialog for saving a minimal configuration (defconfig) to a >> + # specific location >> + >> + global _minconf_filename >> + >> + filename = _minconf_filename >> + while True: >> + filename = filedialog.asksaveasfilename( >> + title="Save minimal configuration as", >> + initialdir=os.path.dirname(filename), >> + initialfile=os.path.basename(filename), >> + parent=_root) >> + >> + if not filename: >> + break >> + >> + if _try_save(_kconf.write_min_config, filename, >> + "minimal configuration"): >> + >> + _minconf_filename = filename >> + break >> + >> + _tree.focus_set() >> + >> + >> +def _open(_=None): >> + # Pops up a dialog for loading a configuration >> + >> + global _conf_filename >> + >> + if _conf_changed and \ >> + not messagebox.askokcancel( >> + "Unsaved changes", >> + "You have unsaved changes. Load new configuration anyway?"): >> + >> + return >> + >> + filename = _conf_filename >> + while True: >> + filename = filedialog.askopenfilename( >> + title="Open configuration", >> + initialdir=os.path.dirname(filename), >> + initialfile=os.path.basename(filename), >> + parent=_root) >> + >> + if not filename: >> + break >> + >> + if _try_load(filename): >> + # Maybe something fancier could be done here later to try to >> + # preserve the scroll >> + >> + _conf_filename = filename >> + _set_conf_changed(_needs_save()) >> + >> + if _single_menu and not _shown_menu_nodes(_cur_menu): >> + # Turn on show-all if we're in single-menu mode and would end >> + # up with an empty menu >> + _show_all_var.set(True) >> + >> + _update_tree() >> + >> + break >> + >> + _tree.focus_set() >> + >> + >> +def _toggle_showname(_): >> + # Toggles show-name mode on/off >> + >> + _show_name_var.set(not _show_name_var.get()) >> + _do_showname() >> + >> + >> +def _do_showname(): >> + # Updates the UI for the current show-name setting >> + >> + # Columns do not automatically shrink/expand, so we have to update >> + # column widths ourselves >> + >> + tree_width = _tree.winfo_width() >> + >> + if _show_name_var.get(): >> + _tree["displaycolumns"] = ("name",) >> + _tree["show"] = "tree headings" >> + name_width = tree_width//3 >> + _tree.column("#0", width=max(tree_width - name_width, 1)) >> + _tree.column("name", width=name_width) >> + else: >> + _tree["displaycolumns"] = () >> + _tree["show"] = "tree" >> + _tree.column("#0", width=tree_width) >> + >> + _tree.focus_set() >> + >> + >> +def _toggle_showall(_): >> + # Toggles show-all mode on/off >> + >> + _show_all_var.set(not _show_all) >> + _do_showall() >> + >> + >> +def _do_showall(): >> + # Updates the UI for the current show-all setting >> + >> + # Don't allow turning off show-all if we'd end up with no visible nodes >> + if _nothing_shown(): >> + _show_all_var.set(True) >> + return >> + >> + # Save scroll information. old_scroll can end up negative here, if the >> + # reference item isn't shown (only invisible items on the screen, and >> + # show-all being turned off). >> + >> + stayput = _vis_loc_ref_item() >> + # Probe the middle of the first row, to play it safe. identify_row(0) seems >> + # to return the row before the top row. >> + old_scroll = _item_row(stayput) - \ >> + _item_row(_tree.identify_row(_treeview_rowheight//2)) >> + >> + _update_tree() >> + >> + if _show_all: >> + # Deep magic: Unless we call update_idletasks(), the scroll adjustment >> + # below is restricted to the height of the old tree, instead of the >> + # height of the new tree. Since the tree with show-all on is guaranteed >> + # to be taller, and we want the maximum range, we only call it when >> + # turning show-all on. >> + # >> + # Strictly speaking, something similar ought to be done when changing >> + # symbol values, but it causes annoying flicker, and in 99% of cases >> + # things work anyway there (with usually minor scroll mess-ups in the >> + # 1% case). >> + _root.update_idletasks() >> + >> + # Restore scroll >> + _tree.yview(_item_row(stayput) - old_scroll) >> + >> + _tree.focus_set() >> + >> + >> +def _nothing_shown(): >> + # _do_showall() helper. Returns True if no nodes would get >> + # shown with the current show-all setting. Also handles the >> + # (obscure) case when there are no visible nodes in the entire >> + # tree, meaning guiconfig was automatically started in >> + # show-all mode, which mustn't be turned off. >> + >> + return not _shown_menu_nodes( >> + _cur_menu if _single_menu else _kconf.top_node) >> + >> + >> +def _toggle_tree_mode(_): >> + # Toggles single-menu mode on/off >> + >> + _single_menu_var.set(not _single_menu) >> + _do_tree_mode() >> + >> + >> +def _do_tree_mode(): >> + # Updates the UI for the current tree mode (full-tree or single-menu) >> + >> + loc_ref_node = _id_to_node[_loc_ref_item()] >> + >> + if not _single_menu: >> + # _jump_to() -> _enter_menu() already updates the tree, but >> + # _jump_to() -> load_parents() doesn't, because it isn't always needed. >> + # We always need to update the tree here, e.g. to add/remove "--->". >> + _update_tree() >> + >> + _jump_to(loc_ref_node) >> + _tree.focus_set() >> + >> + >> +def _enter_menu_and_select_first(menu): >> + # Enters the menu 'menu' and selects the first item. Used in single-menu >> + # mode. >> + >> + _enter_menu(menu) >> + _select(_tree, _tree.get_children()[0]) >> + >> + >> +def _enter_menu(menu): >> + # Enters the menu 'menu'. Used in single-menu mode. >> + >> + global _cur_menu >> + >> + _cur_menu = menu >> + _update_tree() >> + >> + _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal" >> + >> + >> +def _leave_menu(): >> + # Leaves the current menu. Used in single-menu mode. >> + >> + global _cur_menu >> + >> + if _cur_menu is not _kconf.top_node: >> + old_menu = _cur_menu >> + >> + _cur_menu = _parent_menu(_cur_menu) >> + _update_tree() >> + >> + _select(_tree, id(old_menu)) >> + >> + if _cur_menu is _kconf.top_node: >> + _backbutton["state"] = "disabled" >> + >> + _tree.focus_set() >> + >> + >> +def _select(tree, item): >> + # Selects, focuses, and see()s 'item' in 'tree' >> + >> + tree.selection_set(item) >> + tree.focus(item) >> + tree.see(item) >> + >> + >> +def _loc_ref_item(): >> + # Returns a Treeview item that can serve as a reference for the current >> + # scroll location. We try to make this item stay on the same row on the >> + # screen when updating the tree. >> + >> + # If the selected item is visible, use that >> + sel = _tree.selection() >> + if sel and _tree.bbox(sel[0]): >> + return sel[0] >> + >> + # Otherwise, use the middle item on the screen. If it doesn't exist, the >> + # tree is probably really small, so use the first item in the entire tree. >> + return _tree.identify_row(_tree.winfo_height()//2) or \ >> + _tree.get_children()[0] >> + >> + >> +def _vis_loc_ref_item(): >> + # Like _loc_ref_item(), but finds a visible item around the reference item. >> + # Used when changing show-all mode, where non-visible (red) items will >> + # disappear. >> + >> + item = _loc_ref_item() >> + >> + vis_before = _vis_before(item) >> + if vis_before and _tree.bbox(vis_before): >> + return vis_before >> + >> + vis_after = _vis_after(item) >> + if vis_after and _tree.bbox(vis_after): >> + return vis_after >> + >> + return vis_before or vis_after >> + >> + >> +def _vis_before(item): >> + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, >> + # searching backwards from 'item'. >> + >> + while item: >> + if not _tree.tag_has("invisible", item): >> + return item >> + >> + prev = _tree.prev(item) >> + item = prev if prev else _tree.parent(item) >> + >> + return None >> + >> + >> +def _vis_after(item): >> + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, >> + # searching forwards from 'item'. >> + >> + while item: >> + if not _tree.tag_has("invisible", item): >> + return item >> + >> + next = _tree.next(item) >> + if next: >> + item = next >> + else: >> + item = _tree.parent(item) >> + if not item: >> + break >> + item = _tree.next(item) >> + >> + return None >> + >> + >> +def _on_quit(_=None): >> + # Called when the user wants to exit >> + >> + if not _conf_changed: >> + _quit("No changes to save (for '{}')".format(_conf_filename)) >> + return >> + >> + while True: >> + ync = messagebox.askyesnocancel("Quit", "Save changes?") >> + if ync is None: >> + return >> + >> + if not ync: >> + _quit("Configuration ({}) was not saved".format(_conf_filename)) >> + return >> + >> + if _try_save(_kconf.write_config, _conf_filename, "configuration"): >> + # _try_save() already prints the "Configuration saved to ..." >> + # message >> + _quit() >> + return >> + >> + >> +def _quit(msg=None): >> + # Quits the application >> + >> + # Do not call sys.exit() here, in case we're being run from a script >> + _root.destroy() >> + if msg: >> + print(msg) >> + >> + >> +def _try_save(save_fn, filename, description): >> + # Tries to save a configuration file. Pops up an error and returns False on >> + # failure. >> + # >> + # save_fn: >> + # Function to call with 'filename' to save the file >> + # >> + # description: >> + # String describing the thing being saved >> + >> + try: >> + # save_fn() returns a message to print >> + msg = save_fn(filename) >> + _set_status(msg) >> + print(msg) >> + return True >> + except EnvironmentError as e: >> + messagebox.showerror( >> + "Error saving " + description, >> + "Error saving {} to '{}': {} (errno: {})" >> + .format(description, e.filename, e.strerror, >> + errno.errorcode[e.errno])) >> + return False >> + >> + >> +def _try_load(filename): >> + # Tries to load a configuration file. Pops up an error and returns False on >> + # failure. >> + # >> + # filename: >> + # Configuration file to load >> + >> + try: >> + msg = _kconf.load_config(filename) >> + _set_status(msg) >> + print(msg) >> + return True >> + except EnvironmentError as e: >> + messagebox.showerror( >> + "Error loading configuration", >> + "Error loading '{}': {} (errno: {})" >> + .format(filename, e.strerror, errno.errorcode[e.errno])) >> + return False >> + >> + >> +def _jump_to_dialog(_=None): >> + # Pops up a dialog for jumping directly to a particular node. Symbol values >> + # can also be changed within the dialog. >> + # >> + # Note: There's nothing preventing this from doing an incremental search >> + # like menuconfig.py does, but currently it's a bit jerky for large Kconfig >> + # trees, at least when inputting the beginning of the search string. We'd >> + # need to somehow only update the tree items that are shown in the Treeview >> + # to fix it. >> + >> + global _jump_to_tree >> + >> + def search(_=None): >> + _update_jump_to_matches(msglabel, entry.get()) >> + >> + def jump_to_selected(event=None): >> + # Jumps to the selected node and closes the dialog >> + >> + # Ignore double clicks on the image and in the heading area >> + if event and (tree.identify_element(event.x, event.y) == "image" or >> + _in_heading(event)): >> + return >> + >> + sel = tree.selection() >> + if not sel: >> + return >> + >> + node = _id_to_node[sel[0]] >> + >> + if node not in _shown_menu_nodes(_parent_menu(node)): >> + _show_all_var.set(True) >> + if not _single_menu: >> + # See comment in _do_tree_mode() >> + _update_tree() >> + >> + _jump_to(node) >> + >> + dialog.destroy() >> + >> + def tree_select(_): >> + jumpto_button["state"] = "normal" if tree.selection() else "disabled" >> + >> + >> + dialog = Toplevel(_root) >> + dialog.geometry("+{}+{}".format( >> + _root.winfo_rootx() + 50, _root.winfo_rooty() + 50)) >> + dialog.title("Jump to symbol/choice/menu/comment") >> + dialog.minsize(128, 128) # See _create_ui() >> + dialog.transient(_root) >> + >> + ttk.Label(dialog, text=_JUMP_TO_HELP) \ >> + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c", >> + pady=".1c") >> + >> + entry = ttk.Entry(dialog) >> + entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c") >> + entry.focus_set() >> + >> + entry.bind("<Return>", search) >> + entry.bind("<KP_Enter>", search) >> + >> + ttk.Button(dialog, text="Search", command=search) \ >> + .grid(column=1, row=1, padx="0 .1c", pady="0 .1c") >> + >> + msglabel = ttk.Label(dialog) >> + msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c") >> + >> + panedwindow, tree = _create_kconfig_tree_and_desc(dialog) >> + panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew") >> + >> + # Clear tree >> + tree.set_children("") >> + >> + _jump_to_tree = tree >> + >> + jumpto_button = ttk.Button(dialog, text="Jump to selected item", >> + state="disabled", command=jump_to_selected) >> + jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c") >> + >> + dialog.columnconfigure(0, weight=1) >> + # Only the pane with the Kconfig tree and description grows vertically >> + dialog.rowconfigure(3, weight=1) >> + >> + # See the menuconfig() function >> + _root.update_idletasks() >> + dialog.geometry(dialog.geometry()) >> + >> + # The dialog must be visible before we can grab the input >> + dialog.wait_visibility() >> + dialog.grab_set() >> + >> + tree.bind("<Double-1>", jump_to_selected) >> + tree.bind("<Return>", jump_to_selected) >> + tree.bind("<KP_Enter>", jump_to_selected) >> + # add=True to avoid overriding the description text update >> + tree.bind("<<TreeviewSelect>>", tree_select, add=True) >> + >> + dialog.bind("<Escape>", lambda _: dialog.destroy()) >> + >> + # Wait for the user to be done with the dialog >> + _root.wait_window(dialog) >> + >> + _jump_to_tree = None >> + >> + _tree.focus_set() >> + >> + >> +def _update_jump_to_matches(msglabel, search_string): >> + # Searches for nodes matching the search string and updates >> + # _jump_to_matches. Puts a message in 'msglabel' if there are no matches, >> + # or regex errors. >> + >> + global _jump_to_matches >> + >> + _jump_to_tree.selection_set(()) >> + >> + try: >> + # We could use re.IGNORECASE here instead of lower(), but this is >> + # faster for regexes like '.*debug$' (though the '.*' is redundant >> + # there). Those probably have bad interactions with re.search(), which >> + # matches anywhere in the string. >> + regex_searches = [re.compile(regex).search >> + for regex in search_string.lower().split()] >> + except re.error as e: >> + msg = "Bad regular expression" >> + # re.error.msg was added in Python 3.5 >> + if hasattr(e, "msg"): >> + msg += ": " + e.msg >> + msglabel["text"] = msg >> + # Clear tree >> + _jump_to_tree.set_children("") >> + return >> + >> + _jump_to_matches = [] >> + add_match = _jump_to_matches.append >> + >> + for node in _sorted_sc_nodes(): >> + # Symbol/choice >> + sc = node.item >> + >> + for search in regex_searches: >> + # Both the name and the prompt might be missing, since >> + # we're searching both symbols and choices >> + >> + # Does the regex match either the symbol name or the >> + # prompt (if any)? >> + if not (sc.name and search(sc.name.lower()) or >> + node.prompt and search(node.prompt[0].lower())): >> + >> + # Give up on the first regex that doesn't match, to >> + # speed things up a bit when multiple regexes are >> + # entered >> + break >> + >> + else: >> + add_match(node) >> + >> + # Search menus and comments >> + >> + for node in _sorted_menu_comment_nodes(): >> + for search in regex_searches: >> + if not search(node.prompt[0].lower()): >> + break >> + else: >> + add_match(node) >> + >> + msglabel["text"] = "" if _jump_to_matches else "No matches" >> + >> + _update_jump_to_display() >> + >> + if _jump_to_matches: >> + item = id(_jump_to_matches[0]) >> + _jump_to_tree.selection_set(item) >> + _jump_to_tree.focus(item) >> + >> + >> +def _update_jump_to_display(): >> + # Updates the images and text for the items in _jump_to_matches, and sets >> + # them as the items of _jump_to_tree >> + >> + # Micro-optimize a bit >> + item = _jump_to_tree.item >> + id_ = id >> + node_str = _node_str >> + img_tag = _img_tag >> + visible = _visible >> + for node in _jump_to_matches: >> + item(id_(node), >> + text=node_str(node), >> + tags=img_tag(node) if visible(node) else >> + img_tag(node) + " invisible") >> + >> + _jump_to_tree.set_children("", *map(id, _jump_to_matches)) >> + >> + >> +def _jump_to(node): >> + # Jumps directly to 'node' and selects it >> + >> + if _single_menu: >> + _enter_menu(_parent_menu(node)) >> + else: >> + _load_parents(node) >> + >> + _select(_tree, id(node)) >> + >> + >> +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing >> +# to the same list. This avoids a global. >> +def _sorted_sc_nodes(cached_nodes=[]): >> + # Returns a sorted list of symbol and choice nodes to search. The symbol >> + # nodes appear first, sorted by name, and then the choice nodes, sorted by >> + # prompt and (secondarily) name. >> + >> + if not cached_nodes: >> + # Add symbol nodes >> + for sym in sorted(_kconf.unique_defined_syms, >> + key=lambda sym: sym.name): >> + # += is in-place for lists >> + cached_nodes += sym.nodes >> + >> + # Add choice nodes >> + >> + choices = sorted(_kconf.unique_choices, >> + key=lambda choice: choice.name or "") >> + >> + cached_nodes += sorted( >> + [node for choice in choices for node in choice.nodes], >> + key=lambda node: node.prompt[0] if node.prompt else "") >> + >> + return cached_nodes >> + >> + >> +def _sorted_menu_comment_nodes(cached_nodes=[]): >> + # Returns a list of menu and comment nodes to search, sorted by prompt, >> + # with the menus first >> + >> + if not cached_nodes: >> + def prompt_text(mc): >> + return mc.prompt[0] >> + >> + cached_nodes += sorted(_kconf.menus, key=prompt_text) >> + cached_nodes += sorted(_kconf.comments, key=prompt_text) >> + >> + return cached_nodes >> + >> + >> +def _load_parents(node): >> + # Menus are lazily populated as they're opened in full-tree mode, but >> + # jumping to an item needs its parent menus to be populated. This function >> + # populates 'node's parents. >> + >> + # Get all parents leading up to 'node', sorted with the root first >> + parents = [] >> + cur = node.parent >> + while cur is not _kconf.top_node: >> + parents.append(cur) >> + cur = cur.parent >> + parents.reverse() >> + >> + for i, parent in enumerate(parents): >> + if not _tree.item(id(parent), "open"): >> + # Found a closed menu. Populate it and all the remaining menus >> + # leading up to 'node'. >> + for parent in parents[i:]: >> + # We only need to populate "real" menus/choices. Implicit menus >> + # are populated when their parents menus are entered. >> + if not isinstance(parent.item, Symbol): >> + _build_full_tree(parent) >> + return >> + >> + >> +def _parent_menu(node): >> + # Returns the menu node of the menu that contains 'node'. In addition to >> + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. >> + # "Menu" here means a menu in the interface. >> + >> + menu = node.parent >> + while not menu.is_menuconfig: >> + menu = menu.parent >> + return menu >> + >> + >> +def _trace_write(var, fn): >> + # Makes fn() be called whenever the Tkinter Variable 'var' changes value >> + >> + # trace_variable() is deprecated according to the docstring, >> + # which recommends trace_add() >> + if hasattr(var, "trace_add"): >> + var.trace_add("write", fn) >> + else: >> + var.trace_variable("w", fn) >> + >> + >> +def _info_str(node): >> + # Returns information about the menu node 'node' as a string. >> + # >> + # The helper functions are responsible for adding newlines. This allows >> + # them to return "" if they don't want to add any output. >> + >> + if isinstance(node.item, Symbol): >> + sym = node.item >> + >> + return ( >> + _name_info(sym) + >> + _help_info(sym) + >> + _direct_dep_info(sym) + >> + _defaults_info(sym) + >> + _select_imply_info(sym) + >> + _kconfig_def_info(sym) >> + ) >> + >> + if isinstance(node.item, Choice): >> + choice = node.item >> + >> + return ( >> + _name_info(choice) + >> + _help_info(choice) + >> + 'Mode: {}\n\n'.format(choice.str_value) + >> + _choice_syms_info(choice) + >> + _direct_dep_info(choice) + >> + _defaults_info(choice) + >> + _kconfig_def_info(choice) >> + ) >> + >> + # node.item in (MENU, COMMENT) >> + return _kconfig_def_info(node) >> + >> + >> +def _name_info(sc): >> + # Returns a string with the name of the symbol/choice. Choices are shown as >> + # <choice (name if any)>. >> + >> + return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n" >> + >> + >> +def _value_info(sym): >> + # Returns a string showing 'sym's value >> + >> + # Only put quotes around the value for string symbols >> + return "Value: {}\n".format( >> + '"{}"'.format(sym.str_value) >> + if sym.orig_type == STRING >> + else sym.str_value) >> + >> + >> +def _choice_syms_info(choice): >> + # Returns a string listing the choice symbols in 'choice'. Adds >> + # "(selected)" next to the selected one. >> + >> + s = "Choice symbols:\n" >> + >> + for sym in choice.syms: >> + s += " - " + sym.name >> + if sym is choice.selection: >> + s += " (selected)" >> + s += "\n" >> + >> + return s + "\n" >> + >> + >> +def _help_info(sc): >> + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). >> + # Symbols and choices defined in multiple locations can have multiple help >> + # texts. >> + >> + s = "" >> + >> + for node in sc.nodes: >> + if node.help is not None: >> + s += node.help + "\n\n" >> + >> + return s >> + >> + >> +def _direct_dep_info(sc): >> + # Returns a string describing the direct dependencies of 'sc' (Symbol or >> + # Choice). The direct dependencies are the OR of the dependencies from each >> + # definition location. The dependencies at each definition location come >> + # from 'depends on' and dependencies inherited from parent items. >> + >> + return "" if sc.direct_dep is _kconf.y else \ >> + 'Direct dependencies (={}):\n{}\n' \ >> + .format(TRI_TO_STR[expr_value(sc.direct_dep)], >> + _split_expr_info(sc.direct_dep, 2)) >> + >> + >> +def _defaults_info(sc): >> + # Returns a string describing the defaults of 'sc' (Symbol or Choice) >> + >> + if not sc.defaults: >> + return "" >> + >> + s = "Default" >> + if len(sc.defaults) > 1: >> + s += "s" >> + s += ":\n" >> + >> + for val, cond in sc.orig_defaults: >> + s += " - " >> + if isinstance(sc, Symbol): >> + s += _expr_str(val) >> + >> + # Skip the tristate value hint if the expression is just a single >> + # symbol. _expr_str() already shows its value as a string. >> + # >> + # This also avoids showing the tristate value for string/int/hex >> + # defaults, which wouldn't make any sense. >> + if isinstance(val, tuple): >> + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) >> + else: >> + # Don't print the value next to the symbol name for choice >> + # defaults, as it looks a bit confusing >> + s += val.name >> + s += "\n" >> + >> + if cond is not _kconf.y: >> + s += " Condition (={}):\n{}" \ >> + .format(TRI_TO_STR[expr_value(cond)], >> + _split_expr_info(cond, 4)) >> + >> + return s + "\n" >> + >> + >> +def _split_expr_info(expr, indent): >> + # Returns a string with 'expr' split into its top-level && or || operands, >> + # with one operand per line, together with the operand's value. This is >> + # usually enough to get something readable for long expressions. A fancier >> + # recursive thingy would be possible too. >> + # >> + # indent: >> + # Number of leading spaces to add before the split expression. >> + >> + if len(split_expr(expr, AND)) > 1: >> + split_op = AND >> + op_str = "&&" >> + else: >> + split_op = OR >> + op_str = "||" >> + >> + s = "" >> + for i, term in enumerate(split_expr(expr, split_op)): >> + s += "{}{} {}".format(indent*" ", >> + " " if i == 0 else op_str, >> + _expr_str(term)) >> + >> + # Don't bother showing the value hint if the expression is just a >> + # single symbol. _expr_str() already shows its value. >> + if isinstance(term, tuple): >> + s += " (={})".format(TRI_TO_STR[expr_value(term)]) >> + >> + s += "\n" >> + >> + return s >> + >> + >> +def _select_imply_info(sym): >> + # Returns a string with information about which symbols 'select' or 'imply' >> + # 'sym'. The selecting/implying symbols are grouped according to which >> + # value they select/imply 'sym' to (n/m/y). >> + >> + def sis(expr, val, title): >> + # sis = selects/implies >> + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] >> + if not sis: >> + return "" >> + >> + res = title >> + for si in sis: >> + res += " - {}\n".format(split_expr(si, AND)[0].name) >> + return res + "\n" >> + >> + s = "" >> + >> + if sym.rev_dep is not _kconf.n: >> + s += sis(sym.rev_dep, 2, >> + "Symbols currently y-selecting this symbol:\n") >> + s += sis(sym.rev_dep, 1, >> + "Symbols currently m-selecting this symbol:\n") >> + s += sis(sym.rev_dep, 0, >> + "Symbols currently n-selecting this symbol (no effect):\n") >> + >> + if sym.weak_rev_dep is not _kconf.n: >> + s += sis(sym.weak_rev_dep, 2, >> + "Symbols currently y-implying this symbol:\n") >> + s += sis(sym.weak_rev_dep, 1, >> + "Symbols currently m-implying this symbol:\n") >> + s += sis(sym.weak_rev_dep, 0, >> + "Symbols currently n-implying this symbol (no effect):\n") >> + >> + return s >> + >> + >> +def _kconfig_def_info(item): >> + # Returns a string with the definition of 'item' in Kconfig syntax, >> + # together with the definition location(s) and their include and menu paths >> + >> + nodes = [item] if isinstance(item, MenuNode) else item.nodes >> + >> + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ >> + .format("s" if len(nodes) > 1 else "") >> + s += (len(s) - 1)*"=" >> + >> + for node in nodes: >> + s += "\n\n" \ >> + "At {}:{}\n" \ >> + "{}" \ >> + "Menu path: {}\n\n" \ >> + "{}" \ >> + .format(node.filename, node.linenr, >> + _include_path_info(node), >> + _menu_path_info(node), >> + node.custom_str(_name_and_val_str)) >> + >> + return s >> + >> + >> +def _include_path_info(node): >> + if not node.include_path: >> + # In the top-level Kconfig file >> + return "" >> + >> + return "Included via {}\n".format( >> + " -> ".join("{}:{}".format(filename, linenr) >> + for filename, linenr in node.include_path)) >> + >> + >> +def _menu_path_info(node): >> + # Returns a string describing the menu path leading up to 'node' >> + >> + path = "" >> + >> + while node.parent is not _kconf.top_node: >> + node = node.parent >> + >> + # Promptless choices might appear among the parents. Use >> + # standard_sc_expr_str() for them, so that they show up as >> + # '<choice (name if any)>'. >> + path = " -> " + (node.prompt[0] if node.prompt else >> + standard_sc_expr_str(node.item)) + path >> + >> + return "(Top)" + path >> + >> + >> +def _name_and_val_str(sc): >> + # Custom symbol/choice printer that shows symbol values after symbols >> + >> + # Show the values of non-constant (non-quoted) symbols that don't look like >> + # numbers. Things like 123 are actually symbol references, and only work as >> + # expected due to undefined symbols getting their name as their value. >> + # Showing the symbol value for those isn't helpful though. >> + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): >> + if not sc.nodes: >> + # Undefined symbol reference >> + return "{}(undefined/n)".format(sc.name) >> + >> + return '{}(={})'.format(sc.name, sc.str_value) >> + >> + # For other items, use the standard format >> + return standard_sc_expr_str(sc) >> + >> + >> +def _expr_str(expr): >> + # Custom expression printer that shows symbol values >> + return expr_str(expr, _name_and_val_str) >> + >> + >> +def _is_num(name): >> + # Heuristic to see if a symbol name looks like a number, for nicer output >> + # when printing expressions. Things like 16 are actually symbol names, only >> + # they get their name as their value when the symbol is undefined. >> + >> + try: >> + int(name) >> + except ValueError: >> + if not name.startswith(("0x", "0X")): >> + return False >> + >> + try: >> + int(name, 16) >> + except ValueError: >> + return False >> + >> + return True >> + >> + >> +if __name__ == "__main__": >> + _main() >> diff --git a/scripts/Kconfiglib/kconfiglib.py b/scripts/Kconfiglib/kconfiglib.py >> new file mode 100644 >> index 0000000..a50312e >> --- /dev/null >> +++ b/scripts/Kconfiglib/kconfiglib.py >> @@ -0,0 +1,7160 @@ >> +# Copyright (c) 2011-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Overview >> +======== >> + >> +Kconfiglib is a Python 2/3 library for scripting and extracting information >> +from Kconfig (https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt) >> +configuration systems. >> + >> +See the homepage at https://github.com/zephyrproject-rtos/Kconfiglib for a longer >> +overview. >> + >> +Since Kconfiglib 12.0.0, the library version is available in >> +kconfiglib.VERSION, which is a (<major>, <minor>, <patch>) tuple, e.g. >> +(12, 0, 0). >> + >> + >> +Using Kconfiglib on the Linux kernel with the Makefile targets >> +============================================================== >> + >> +For the Linux kernel, a handy interface is provided by the >> +scripts/kconfig/Makefile patch, which can be applied with either 'git am' or >> +the 'patch' utility: >> + >> + $ wget -qO- https://raw.githubusercontent.com/zephyrproject-rtos/Kconfiglib/master/makefile.patch | git am >> + $ wget -qO- https://raw.githubusercontent.com/zephyrproject-rtos/Kconfiglib/master/makefile.patch | patch -p1 >> + >> +Warning: Not passing -p1 to patch will cause the wrong file to be patched. >> + >> +Please tell me if the patch does not apply. It should be trivial to apply >> +manually, as it's just a block of text that needs to be inserted near the other >> +*conf: targets in scripts/kconfig/Makefile. >> + >> +Look further down for a motivation for the Makefile patch and for instructions >> +on how you can use Kconfiglib without it. >> + >> +If you do not wish to install Kconfiglib via pip, the Makefile patch is set up >> +so that you can also just clone Kconfiglib into the kernel root: >> + >> + $ git clone git://github.com/zephyrproject-rtos/Kconfiglib.git >> + $ git am Kconfiglib/makefile.patch (or 'patch -p1 < Kconfiglib/makefile.patch') >> + >> +Warning: The directory name Kconfiglib/ is significant in this case, because >> +it's added to PYTHONPATH by the new targets in makefile.patch. >> + >> +The targets added by the Makefile patch are described in the following >> +sections. >> + >> + >> +make kmenuconfig >> +---------------- >> + >> +This target runs the curses menuconfig interface with Python 3. As of >> +Kconfiglib 12.2.0, both Python 2 and Python 3 are supported (previously, only >> +Python 3 was supported, so this was a backport). >> + >> + >> +make guiconfig >> +-------------- >> + >> +This target runs the Tkinter menuconfig interface. Both Python 2 and Python 3 >> +are supported. To change the Python interpreter used, pass >> +PYTHONCMD=<executable> to 'make'. The default is 'python'. >> + >> + >> +make [ARCH=<arch>] iscriptconfig >> +-------------------------------- >> + >> +This target gives an interactive Python prompt where a Kconfig instance has >> +been preloaded and is available in 'kconf'. To change the Python interpreter >> +used, pass PYTHONCMD=<executable> to 'make'. The default is 'python'. >> + >> +To get a feel for the API, try evaluating and printing the symbols in >> +kconf.defined_syms, and explore the MenuNode menu tree starting at >> +kconf.top_node by following 'next' and 'list' pointers. >> + >> +The item contained in a menu node is found in MenuNode.item (note that this can >> +be one of the constants kconfiglib.MENU and kconfiglib.COMMENT), and all >> +symbols and choices have a 'nodes' attribute containing their menu nodes >> +(usually only one). Printing a menu node will print its item, in Kconfig >> +format. >> + >> +If you want to look up a symbol by name, use the kconf.syms dictionary. >> + >> + >> +make scriptconfig SCRIPT=<script> [SCRIPT_ARG=<arg>] >> +---------------------------------------------------- >> + >> +This target runs the Python script given by the SCRIPT parameter on the >> +configuration. sys.argv[1] holds the name of the top-level Kconfig file >> +(currently always "Kconfig" in practice), and sys.argv[2] holds the SCRIPT_ARG >> +argument, if given. >> + >> +See the examples/ subdirectory for example scripts. >> + >> + >> +make dumpvarsconfig >> +------------------- >> + >> +This target prints a list of all environment variables referenced from the >> +Kconfig files, together with their values. See the >> +Kconfiglib/examples/dumpvars.py script. >> + >> +Only environment variables that are referenced via the Kconfig preprocessor >> +$(FOO) syntax are included. The preprocessor was added in Linux 4.18. >> + >> + >> +Using Kconfiglib without the Makefile targets >> +============================================= >> + >> +The make targets are only needed to pick up environment variables exported from >> +the Kbuild makefiles and referenced inside Kconfig files, via e.g. >> +'source "arch/$(SRCARCH)/Kconfig" and commands run via '$(shell,...)'. >> + >> +These variables are referenced as of writing (Linux 4.18), together with sample >> +values: >> + >> + srctree (.) >> + ARCH (x86) >> + SRCARCH (x86) >> + KERNELVERSION (4.18.0) >> + CC (gcc) >> + HOSTCC (gcc) >> + HOSTCXX (g++) >> + CC_VERSION_TEXT (gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0) >> + >> +Older kernels only reference ARCH, SRCARCH, and KERNELVERSION. >> + >> +If your kernel is recent enough (4.18+), you can get a list of referenced >> +environment variables via 'make dumpvarsconfig' (see above). Note that this >> +command is added by the Makefile patch. >> + >> +To run Kconfiglib without the Makefile patch, set the environment variables >> +manually: >> + >> + $ srctree=. ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` ... python(3) >> + >>> import kconfiglib >> + >>> kconf = kconfiglib.Kconfig() # filename defaults to "Kconfig" >> + >> +Search the top-level Makefile for "Additional ARCH settings" to see other >> +possibilities for ARCH and SRCARCH. >> + >> + >> +Intro to symbol values >> +====================== >> + >> +Kconfiglib has the same assignment semantics as the C implementation. >> + >> +Any symbol can be assigned a value by the user (via Kconfig.load_config() or >> +Symbol.set_value()), but this user value is only respected if the symbol is >> +visible, which corresponds to it (currently) being visible in the menuconfig >> +interface. >> + >> +For symbols with prompts, the visibility of the symbol is determined by the >> +condition on the prompt. Symbols without prompts are never visible, so setting >> +a user value on them is pointless. A warning will be printed by default if >> +Symbol.set_value() is called on a promptless symbol. Assignments to promptless >> +symbols are normal within a .config file, so no similar warning will be printed >> +by load_config(). >> + >> +Dependencies from parents and 'if'/'depends on' are propagated to properties, >> +including prompts, so these two configurations are logically equivalent: >> + >> +(1) >> + >> + menu "menu" >> + depends on A >> + >> + if B >> + >> + config FOO >> + tristate "foo" if D >> + default y >> + depends on C >> + >> + endif >> + >> + endmenu >> + >> +(2) >> + >> + menu "menu" >> + depends on A >> + >> + config FOO >> + tristate "foo" if A && B && C && D >> + default y if A && B && C >> + >> + endmenu >> + >> +In this example, A && B && C && D (the prompt condition) needs to be non-n for >> +FOO to be visible (assignable). If its value is m, the symbol can only be >> +assigned the value m: The visibility sets an upper bound on the value that can >> +be assigned by the user, and any higher user value will be truncated down. >> + >> +'default' properties are independent of the visibility, though a 'default' will >> +often get the same condition as the prompt due to dependency propagation. >> +'default' properties are used if the symbol is not visible or has no user >> +value. >> + >> +Symbols with no user value (or that have a user value but are not visible) and >> +no (active) 'default' default to n for bool/tristate symbols, and to the empty >> +string for other symbol types. >> + >> +'select' works similarly to symbol visibility, but sets a lower bound on the >> +value of the symbol. The lower bound is determined by the value of the >> +select*ing* symbol. 'select' does not respect visibility, so non-visible >> +symbols can be forced to a particular (minimum) value by a select as well. >> + >> +For non-bool/tristate symbols, it only matters whether the visibility is n or >> +non-n: m visibility acts the same as y visibility. >> + >> +Conditions on 'default' and 'select' work in mostly intuitive ways. If the >> +condition is n, the 'default' or 'select' is disabled. If it is m, the >> +'default' or 'select' value (the value of the selecting symbol) is truncated >> +down to m. >> + >> +When writing a configuration with Kconfig.write_config(), only symbols that are >> +visible, have an (active) default, or are selected will get written out (note >> +that this includes all symbols that would accept user values). Kconfiglib >> +matches the .config format produced by the C implementations down to the >> +character. This eases testing. >> + >> +For a visible bool/tristate symbol FOO with value n, this line is written to >> +.config: >> + >> + # CONFIG_FOO is not set >> + >> +The point is to remember the user n selection (which might differ from the >> +default value the symbol would get), while at the same sticking to the rule >> +that undefined corresponds to n (.config uses Makefile format, making the line >> +above a comment). When the .config file is read back in, this line will be >> +treated the same as the following assignment: >> + >> + CONFIG_FOO=n >> + >> +In Kconfiglib, the set of (currently) assignable values for a bool/tristate >> +symbol appear in Symbol.assignable. For other symbol types, just check if >> +sym.visibility is non-0 (non-n) to see whether the user value will have an >> +effect. >> + >> + >> +Intro to the menu tree >> +====================== >> + >> +The menu structure, as seen in e.g. menuconfig, is represented by a tree of >> +MenuNode objects. The top node of the configuration corresponds to an implicit >> +top-level menu, the title of which is shown at the top in the standard >> +menuconfig interface. (The title is also available in Kconfig.mainmenu_text in >> +Kconfiglib.) >> + >> +The top node is found in Kconfig.top_node. From there, you can visit child menu >> +nodes by following the 'list' pointer, and any following menu nodes by >> +following the 'next' pointer. Usually, a non-None 'list' pointer indicates a >> +menu or Choice, but menu nodes for symbols can sometimes have a non-None 'list' >> +pointer too due to submenus created implicitly from dependencies. >> + >> +MenuNode.item is either a Symbol or a Choice object, or one of the constants >> +MENU and COMMENT. The prompt of the menu node can be found in MenuNode.prompt, >> +which also holds the title for menus and comments. For Symbol and Choice, >> +MenuNode.help holds the help text (if any, otherwise None). >> + >> +Most symbols will only have a single menu node. A symbol defined in multiple >> +locations will have one menu node for each location. The list of menu nodes for >> +a Symbol or Choice can be found in the Symbol/Choice.nodes attribute. >> + >> +Note that prompts and help texts for symbols and choices are stored in their >> +menu node(s) rather than in the Symbol or Choice objects themselves. This makes >> +it possible to define a symbol in multiple locations with a different prompt or >> +help text in each location. To get the help text or prompt for a symbol with a >> +single menu node, do sym.nodes[0].help and sym.nodes[0].prompt, respectively. >> +The prompt is a (text, condition) tuple, where condition determines the >> +visibility (see 'Intro to expressions' below). >> + >> +This organization mirrors the C implementation. MenuNode is called >> +'struct menu' there, but I thought "menu" was a confusing name. >> + >> +It is possible to give a Choice a name and define it in multiple locations, >> +hence why Choice.nodes is also a list. >> + >> +As a convenience, the properties added at a particular definition location are >> +available on the MenuNode itself, in e.g. MenuNode.defaults. This is helpful >> +when generating documentation, so that symbols/choices defined in multiple >> +locations can be shown with the correct properties at each location. >> + >> + >> +Intro to expressions >> +==================== >> + >> +Expressions can be evaluated with the expr_value() function and printed with >> +the expr_str() function (these are used internally as well). Evaluating an >> +expression always yields a tristate value, where n, m, and y are represented as >> +0, 1, and 2, respectively. >> + >> +The following table should help you figure out how expressions are represented. >> +A, B, C, ... are symbols (Symbol instances), NOT is the kconfiglib.NOT >> +constant, etc. >> + >> +Expression Representation >> +---------- -------------- >> +A A >> +"A" A (constant symbol) >> +!A (NOT, A) >> +A && B (AND, A, B) >> +A && B && C (AND, A, (AND, B, C)) >> +A || B (OR, A, B) >> +A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) >> +A = B (EQUAL, A, B) >> +A != "foo" (UNEQUAL, A, foo (constant symbol)) >> +A && B = C && D (AND, A, (AND, (EQUAL, B, C), D)) >> +n Kconfig.n (constant symbol) >> +m Kconfig.m (constant symbol) >> +y Kconfig.y (constant symbol) >> +"y" Kconfig.y (constant symbol) >> + >> +Strings like "foo" in 'default "foo"' or 'depends on SYM = "foo"' are >> +represented as constant symbols, so the only values that appear in expressions >> +are symbols***. This mirrors the C implementation. >> + >> +***For choice symbols, the parent Choice will appear in expressions as well, >> +but it's usually invisible as the value interfaces of Symbol and Choice are >> +identical. This mirrors the C implementation and makes different choice modes >> +"just work". >> + >> +Manual evaluation examples: >> + >> + - The value of A && B is min(A.tri_value, B.tri_value) >> + >> + - The value of A || B is max(A.tri_value, B.tri_value) >> + >> + - The value of !A is 2 - A.tri_value >> + >> + - The value of A = B is 2 (y) if A.str_value == B.str_value, and 0 (n) >> + otherwise. Note that str_value is used here instead of tri_value. >> + >> + For constant (as well as undefined) symbols, str_value matches the name of >> + the symbol. This mirrors the C implementation and explains why >> + 'depends on SYM = "foo"' above works as expected. >> + >> +n/m/y are automatically converted to the corresponding constant symbols >> +"n"/"m"/"y" (Kconfig.n/m/y) during parsing. >> + >> +Kconfig.const_syms is a dictionary like Kconfig.syms but for constant symbols. >> + >> +If a condition is missing (e.g., <cond> when the 'if <cond>' is removed from >> +'default A if <cond>'), it is actually Kconfig.y. The standard __str__() >> +functions just avoid printing 'if y' conditions to give cleaner output. >> + >> + >> +Kconfig extensions >> +================== >> + >> +Kconfiglib includes a couple of Kconfig extensions: >> + >> +'source' with relative path >> +--------------------------- >> + >> +The 'rsource' statement sources Kconfig files with a path relative to directory >> +of the Kconfig file containing the 'rsource' statement, instead of relative to >> +the project root. >> + >> +Consider following directory tree: >> + >> + Project >> + +--Kconfig >> + | >> + +--src >> + +--Kconfig >> + | >> + +--SubSystem1 >> + +--Kconfig >> + | >> + +--ModuleA >> + +--Kconfig >> + >> +In this example, assume that src/SubSystem1/Kconfig wants to source >> +src/SubSystem1/ModuleA/Kconfig. >> + >> +With 'source', this statement would be used: >> + >> + source "src/SubSystem1/ModuleA/Kconfig" >> + >> +With 'rsource', this turns into >> + >> + rsource "ModuleA/Kconfig" >> + >> +If an absolute path is given to 'rsource', it acts the same as 'source'. >> + >> +'rsource' can be used to create "position-independent" Kconfig trees that can >> +be moved around freely. >> + >> + >> +Globbing 'source' >> +----------------- >> + >> +'source' and 'rsource' accept glob patterns, sourcing all matching Kconfig >> +files. They require at least one matching file, raising a KconfigError >> +otherwise. >> + >> +For example, the following statement might source sub1/foofoofoo and >> +sub2/foobarfoo: >> + >> + source "sub[12]/foo*foo" >> + >> +The glob patterns accepted are the same as for the standard glob.glob() >> +function. >> + >> +Two additional statements are provided for cases where it's acceptable for a >> +pattern to match no files: 'osource' and 'orsource' (the o is for "optional"). >> + >> +For example, the following statements will be no-ops if neither "foo" nor any >> +files matching "bar*" exist: >> + >> + osource "foo" >> + osource "bar*" >> + >> +'orsource' does a relative optional source. >> + >> +'source' and 'osource' are analogous to 'include' and '-include' in Make. >> + >> + >> +Generalized def_* keywords >> +-------------------------- >> + >> +def_int, def_hex, and def_string are available in addition to def_bool and >> +def_tristate, allowing int, hex, and string symbols to be given a type and a >> +default at the same time. >> + >> + >> +Extra optional warnings >> +----------------------- >> + >> +Some optional warnings can be controlled via environment variables: >> + >> + - KCONFIG_WARN_UNDEF: If set to 'y', warnings will be generated for all >> + references to undefined symbols within Kconfig files. The only gotcha is >> + that all hex literals must be prefixed with "0x" or "0X", to make it >> + possible to distinguish them from symbol references. >> + >> + Some projects (e.g. the Linux kernel) use multiple Kconfig trees with many >> + shared Kconfig files, leading to some safe undefined symbol references. >> + KCONFIG_WARN_UNDEF is useful in projects that only have a single Kconfig >> + tree though. >> + >> + KCONFIG_STRICT is an older alias for this environment variable, supported >> + for backwards compatibility. >> + >> + - KCONFIG_WARN_UNDEF_ASSIGN: If set to 'y', warnings will be generated for >> + all assignments to undefined symbols within .config files. By default, no >> + such warnings are generated. >> + >> + This warning can also be enabled/disabled via the Kconfig.warn_assign_undef >> + variable. >> + >> + >> +Preprocessor user functions defined in Python >> +--------------------------------------------- >> + >> +Preprocessor functions can be defined in Python, which makes it simple to >> +integrate information from existing Python tools into Kconfig (e.g. to have >> +Kconfig symbols depend on hardware information stored in some other format). >> + >> +Putting a Python module named kconfigfunctions(.py) anywhere in sys.path will >> +cause it to be imported by Kconfiglib (in Kconfig.__init__()). Note that >> +sys.path can be customized via PYTHONPATH, and includes the directory of the >> +module being run by default, as well as installation directories. >> + >> +If the KCONFIG_FUNCTIONS environment variable is set, it gives a different >> +module name to use instead of 'kconfigfunctions'. >> + >> +The imported module is expected to define a global dictionary named 'functions' >> +that maps function names to Python functions, as follows: >> + >> + def my_fn(kconf, name, arg_1, arg_2, ...): >> + # kconf: >> + # Kconfig instance >> + # >> + # name: >> + # Name of the user-defined function ("my-fn"). Think argv[0]. >> + # >> + # arg_1, arg_2, ...: >> + # Arguments passed to the function from Kconfig (strings) >> + # >> + # Returns a string to be substituted as the result of calling the >> + # function >> + ... >> + >> + def my_other_fn(kconf, name, arg_1, arg_2, ...): >> + ... >> + >> + functions = { >> + "my-fn": (my_fn, <min.args>, <max.args>/None), >> + "my-other-fn": (my_other_fn, <min.args>, <max.args>/None), >> + ... >> + } >> + >> + ... >> + >> +<min.args> and <max.args> are the minimum and maximum number of arguments >> +expected by the function (excluding the implicit 'name' argument). If >> +<max.args> is None, there is no upper limit to the number of arguments. Passing >> +an invalid number of arguments will generate a KconfigError exception. >> + >> +Functions can access the current parsing location as kconf.filename/linenr. >> +Accessing other fields of the Kconfig object is not safe. See the warning >> +below. >> + >> +Keep in mind that for a variable defined like 'foo = $(fn)', 'fn' will be >> +called only when 'foo' is expanded. If 'fn' uses the parsing location and the >> +intent is to use the location of the assignment, you want 'foo := $(fn)' >> +instead, which calls the function immediately. >> + >> +Once defined, user functions can be called from Kconfig in the same way as >> +other preprocessor functions: >> + >> + config FOO >> + ... >> + depends on $(my-fn,arg1,arg2) >> + >> +If my_fn() returns "n", this will result in >> + >> + config FOO >> + ... >> + depends on n >> + >> +Warning >> +******* >> + >> +User-defined preprocessor functions are called as they're encountered at parse >> +time, before all Kconfig files have been processed, and before the menu tree >> +has been finalized. There are no guarantees that accessing Kconfig symbols or >> +the menu tree via the 'kconf' parameter will work, and it could potentially >> +lead to a crash. >> + >> +Preferably, user-defined functions should be stateless. >> + >> + >> +Feedback >> +======== >> + >> +For bug reports, suggestions, and questions, please open a ticket on the GitHub >> +page. >> +""" >> +import errno >> +import importlib >> +import os >> +import re >> +import sys >> + >> +# Get rid of some attribute lookups. These are obvious in context. >> +from glob import iglob >> +from os.path import dirname, exists, expandvars, islink, join, realpath >> + >> + >> +VERSION = (14, 1, 0) >> + >> + >> +# File layout: >> +# >> +# Public classes >> +# Public functions >> +# Internal functions >> +# Global constants >> + >> +# Line length: 79 columns >> + >> + >> +# >> +# Public classes >> +# >> + >> + >> +class Kconfig(object): >> + """ >> + Represents a Kconfig configuration, e.g. for x86 or ARM. This is the set of >> + symbols, choices, and menu nodes appearing in the configuration. Creating >> + any number of Kconfig objects (including for different architectures) is >> + safe. Kconfiglib doesn't keep any global state. >> + >> + The following attributes are available. They should be treated as >> + read-only, and some are implemented through @property magic. >> + >> + syms: >> + A dictionary with all symbols in the configuration, indexed by name. Also >> + includes all symbols that are referenced in expressions but never >> + defined, except for constant (quoted) symbols. >> + >> + Undefined symbols can be recognized by Symbol.nodes being empty -- see >> + the 'Intro to the menu tree' section in the module docstring. >> + >> + const_syms: >> + A dictionary like 'syms' for constant (quoted) symbols >> + >> + named_choices: >> + A dictionary like 'syms' for named choices (choice FOO) >> + >> + defined_syms: >> + A list with all defined symbols, in the same order as they appear in the >> + Kconfig files. Symbols defined in multiple locations appear multiple >> + times. >> + >> + Note: You probably want to use 'unique_defined_syms' instead. This >> + attribute is mostly maintained for backwards compatibility. >> + >> + unique_defined_syms: >> + A list like 'defined_syms', but with duplicates removed. Just the first >> + instance is kept for symbols defined in multiple locations. Kconfig order >> + is preserved otherwise. >> + >> + Using this attribute instead of 'defined_syms' can save work, and >> + automatically gives reasonable behavior when writing configuration output >> + (symbols defined in multiple locations only generate output once, while >> + still preserving Kconfig order for readability). >> + >> + choices: >> + A list with all choices, in the same order as they appear in the Kconfig >> + files. >> + >> + Note: You probably want to use 'unique_choices' instead. This attribute >> + is mostly maintained for backwards compatibility. >> + >> + unique_choices: >> + Analogous to 'unique_defined_syms', for choices. Named choices can have >> + multiple definition locations. >> + >> + menus: >> + A list with all menus, in the same order as they appear in the Kconfig >> + files >> + >> + comments: >> + A list with all comments, in the same order as they appear in the Kconfig >> + files >> + >> + kconfig_filenames: >> + A list with the filenames of all Kconfig files included in the >> + configuration, relative to $srctree (or relative to the current directory >> + if $srctree isn't set), except absolute paths (e.g. >> + 'source "/foo/Kconfig"') are kept as-is. >> + >> + The files are listed in the order they are source'd, starting with the >> + top-level Kconfig file. If a file is source'd multiple times, it will >> + appear multiple times. Use set() to get unique filenames. >> + >> + Note that Kconfig.sync_deps() already indirectly catches any file >> + modifications that change configuration output. >> + >> + env_vars: >> + A set() with the names of all environment variables referenced in the >> + Kconfig files. >> + >> + Only environment variables referenced with the preprocessor $(FOO) syntax >> + will be registered. The older $FOO syntax is only supported for backwards >> + compatibility. >> + >> + Also note that $(FOO) won't be registered unless the environment variable >> + $FOO is actually set. If it isn't, $(FOO) is an expansion of an unset >> + preprocessor variable (which gives the empty string). >> + >> + Another gotcha is that environment variables referenced in the values of >> + recursively expanded preprocessor variables (those defined with =) will >> + only be registered if the variable is actually used (expanded) somewhere. >> + >> + The note from the 'kconfig_filenames' documentation applies here too. >> + >> + n/m/y: >> + The predefined constant symbols n/m/y. Also available in const_syms. >> + >> + modules: >> + The Symbol instance for the modules symbol. Currently hardcoded to >> + MODULES, which is backwards compatible. Kconfiglib will warn if >> + 'option modules' is set on some other symbol. Tell me if you need proper >> + 'option modules' support. >> + >> + 'modules' is never None. If the MODULES symbol is not explicitly defined, >> + its tri_value will be 0 (n), as expected. >> + >> + A simple way to enable modules is to do 'kconf.modules.set_value(2)' >> + (provided the MODULES symbol is defined and visible). Modules are >> + disabled by default in the kernel Kconfig files as of writing, though >> + nearly all defconfig files enable them (with 'CONFIG_MODULES=y'). >> + >> + defconfig_list: >> + The Symbol instance for the 'option defconfig_list' symbol, or None if no >> + defconfig_list symbol exists. The defconfig filename derived from this >> + symbol can be found in Kconfig.defconfig_filename. >> + >> + defconfig_filename: >> + The filename given by the defconfig_list symbol. This is taken from the >> + first 'default' with a satisfied condition where the specified file >> + exists (can be opened for reading). If a defconfig file foo/defconfig is >> + not found and $srctree was set when the Kconfig was created, >> + $srctree/foo/defconfig is looked up as well. >> + >> + 'defconfig_filename' is None if either no defconfig_list symbol exists, >> + or if the defconfig_list symbol has no 'default' with a satisfied >> + condition that specifies a file that exists. >> + >> + Gotcha: scripts/kconfig/Makefile might pass --defconfig=<defconfig> to >> + scripts/kconfig/conf when running e.g. 'make defconfig'. This option >> + overrides the defconfig_list symbol, meaning defconfig_filename might not >> + always match what 'make defconfig' would use. >> + >> + top_node: >> + The menu node (see the MenuNode class) of the implicit top-level menu. >> + Acts as the root of the menu tree. >> + >> + mainmenu_text: >> + The prompt (title) of the top menu (top_node). Defaults to "Main menu". >> + Can be changed with the 'mainmenu' statement (see kconfig-language.txt). >> + >> + variables: >> + A dictionary with all preprocessor variables, indexed by name. See the >> + Variable class. >> + >> + warn: >> + Set this variable to True/False to enable/disable warnings. See >> + Kconfig.__init__(). >> + >> + When 'warn' is False, the values of the other warning-related variables >> + are ignored. >> + >> + This variable as well as the other warn* variables can be read to check >> + the current warning settings. >> + >> + warn_to_stderr: >> + Set this variable to True/False to enable/disable warnings on stderr. See >> + Kconfig.__init__(). >> + >> + warn_assign_undef: >> + Set this variable to True to generate warnings for assignments to >> + undefined symbols in configuration files. >> + >> + This variable is False by default unless the KCONFIG_WARN_UNDEF_ASSIGN >> + environment variable was set to 'y' when the Kconfig instance was >> + created. >> + >> + warn_assign_override: >> + Set this variable to True to generate warnings for multiple assignments >> + to the same symbol in configuration files, where the assignments set >> + different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where the >> + last value would get used). >> + >> + This variable is True by default. Disabling it might be useful when >> + merging configurations. >> + >> + warn_assign_redun: >> + Like warn_assign_override, but for multiple assignments setting a symbol >> + to the same value. >> + >> + This variable is True by default. Disabling it might be useful when >> + merging configurations. >> + >> + warnings: >> + A list of strings containing all warnings that have been generated, for >> + cases where more flexibility is needed. >> + >> + See the 'warn_to_stderr' parameter to Kconfig.__init__() and the >> + Kconfig.warn_to_stderr variable as well. Note that warnings still get >> + added to Kconfig.warnings when 'warn_to_stderr' is True. >> + >> + Just as for warnings printed to stderr, only warnings that are enabled >> + will get added to Kconfig.warnings. See the various Kconfig.warn* >> + variables. >> + >> + missing_syms: >> + A list with (name, value) tuples for all assignments to undefined symbols >> + within the most recently loaded .config file(s). 'name' is the symbol >> + name without the 'CONFIG_' prefix. 'value' is a string that gives the >> + right-hand side of the assignment verbatim. >> + >> + See Kconfig.load_config() as well. >> + >> + srctree: >> + The value the $srctree environment variable had when the Kconfig instance >> + was created, or the empty string if $srctree wasn't set. This gives nice >> + behavior with os.path.join(), which treats "" as the current directory, >> + without adding "./". >> + >> + Kconfig files are looked up relative to $srctree (unless absolute paths >> + are used), and .config files are looked up relative to $srctree if they >> + are not found in the current directory. This is used to support >> + out-of-tree builds. The C tools use this environment variable in the same >> + way. >> + >> + Changing $srctree after creating the Kconfig instance has no effect. Only >> + the value when the configuration is loaded matters. This avoids surprises >> + if multiple configurations are loaded with different values for $srctree. >> + >> + config_prefix: >> + The value the CONFIG_ environment variable had when the Kconfig instance >> + was created, or "CONFIG_" if CONFIG_ wasn't set. This is the prefix used >> + (and expected) on symbol names in .config files and C headers. Used in >> + the same way in the C tools. >> + >> + config_header: >> + The value the KCONFIG_CONFIG_HEADER environment variable had when the >> + Kconfig instance was created, or the empty string if >> + KCONFIG_CONFIG_HEADER wasn't set. This string is inserted verbatim at the >> + beginning of configuration files. See write_config(). >> + >> + header_header: >> + The value the KCONFIG_AUTOHEADER_HEADER environment variable had when the >> + Kconfig instance was created, or the empty string if >> + KCONFIG_AUTOHEADER_HEADER wasn't set. This string is inserted verbatim at >> + the beginning of header files. See write_autoconf(). >> + >> + filename/linenr: >> + The current parsing location, for use in Python preprocessor functions. >> + See the module docstring. >> + """ >> + __slots__ = ( >> + "_encoding", >> + "_functions", >> + "_set_match", >> + "_srctree_prefix", >> + "_unset_match", >> + "_warn_assign_no_prompt", >> + "choices", >> + "comments", >> + "config_header", >> + "config_prefix", >> + "const_syms", >> + "defconfig_list", >> + "defined_syms", >> + "env_vars", >> + "header_header", >> + "kconfig_filenames", >> + "m", >> + "menus", >> + "missing_syms", >> + "modules", >> + "n", >> + "named_choices", >> + "srctree", >> + "syms", >> + "top_node", >> + "unique_choices", >> + "unique_defined_syms", >> + "variables", >> + "warn", >> + "warn_assign_override", >> + "warn_assign_redun", >> + "warn_assign_undef", >> + "warn_to_stderr", >> + "warnings", >> + "y", >> + >> + # Parsing-related >> + "_parsing_kconfigs", >> + "_readline", >> + "filename", >> + "linenr", >> + "_include_path", >> + "_filestack", >> + "_line", >> + "_tokens", >> + "_tokens_i", >> + "_reuse_tokens", >> + ) >> + >> + # >> + # Public interface >> + # >> + >> + def __init__(self, filename="Kconfig", warn=True, warn_to_stderr=True, >> + encoding="utf-8", suppress_traceback=False): >> + """ >> + Creates a new Kconfig object by parsing Kconfig files. >> + Note that Kconfig files are not the same as .config files (which store >> + configuration symbol values). >> + >> + See the module docstring for some environment variables that influence >> + default warning settings (KCONFIG_WARN_UNDEF and >> + KCONFIG_WARN_UNDEF_ASSIGN). >> + >> + Raises KconfigError on syntax/semantic errors, and OSError or (possibly >> + a subclass of) IOError on IO errors ('errno', 'strerror', and >> + 'filename' are available). Note that IOError is an alias for OSError on >> + Python 3, so it's enough to catch OSError there. If you need Python 2/3 >> + compatibility, it's easiest to catch EnvironmentError, which is a >> + common base class of OSError/IOError on Python 2 and an alias for >> + OSError on Python 3. >> + >> + filename (default: "Kconfig"): >> + The Kconfig file to load. For the Linux kernel, you'll want "Kconfig" >> + from the top-level directory, as environment variables will make sure >> + the right Kconfig is included from there (arch/$SRCARCH/Kconfig as of >> + writing). >> + >> + If $srctree is set, 'filename' will be looked up relative to it. >> + $srctree is also used to look up source'd files within Kconfig files. >> + See the class documentation. >> + >> + If you are using Kconfiglib via 'make scriptconfig', the filename of >> + the base base Kconfig file will be in sys.argv[1]. It's currently >> + always "Kconfig" in practice. >> + >> + warn (default: True): >> + True if warnings related to this configuration should be generated. >> + This can be changed later by setting Kconfig.warn to True/False. It >> + is provided as a constructor argument since warnings might be >> + generated during parsing. >> + >> + See the other Kconfig.warn_* variables as well, which enable or >> + suppress certain warnings when warnings are enabled. >> + >> + All generated warnings are added to the Kconfig.warnings list. See >> + the class documentation. >> + >> + warn_to_stderr (default: True): >> + True if warnings should be printed to stderr in addition to being >> + added to Kconfig.warnings. >> + >> + This can be changed later by setting Kconfig.warn_to_stderr to >> + True/False. >> + >> + encoding (default: "utf-8"): >> + The encoding to use when reading and writing files, and when decoding >> + output from commands run via $(shell). If None, the encoding >> + specified in the current locale will be used. >> + >> + The "utf-8" default avoids exceptions on systems that are configured >> + to use the C locale, which implies an ASCII encoding. >> + >> + This parameter has no effect on Python 2, due to implementation >> + issues (regular strings turning into Unicode strings, which are >> + distinct in Python 2). Python 2 doesn't decode regular strings >> + anyway. >> + >> + Related PEP: https://www.python.org/dev/peps/pep-0538/ >> + >> + suppress_traceback (default: False): >> + Helper for tools. When True, any EnvironmentError or KconfigError >> + generated during parsing is caught, the exception message is printed >> + to stderr together with the command name, and sys.exit(1) is called >> + (which generates SystemExit). >> + >> + This hides the Python traceback for "expected" errors like syntax >> + errors in Kconfig files. >> + >> + Other exceptions besides EnvironmentError and KconfigError are still >> + propagated when suppress_traceback is True. >> + """ >> + try: >> + self._init(filename, warn, warn_to_stderr, encoding) >> + except (EnvironmentError, KconfigError) as e: >> + if suppress_traceback: >> + cmd = sys.argv[0] # Empty string if missing >> + if cmd: >> + cmd += ": " >> + # Some long exception messages have extra newlines for better >> + # formatting when reported as an unhandled exception. Strip >> + # them here. >> + sys.exit(cmd + str(e).strip()) >> + raise >> + >> + def _init(self, filename, warn, warn_to_stderr, encoding): >> + # See __init__() >> + >> + self._encoding = encoding >> + >> + self.srctree = os.getenv("srctree", "") >> + # A prefix we can reliably strip from glob() results to get a filename >> + # relative to $srctree. relpath() can cause issues for symlinks, >> + # because it assumes symlink/../foo is the same as foo/. >> + self._srctree_prefix = realpath(self.srctree) + os.sep >> + >> + self.warn = warn >> + self.warn_to_stderr = warn_to_stderr >> + self.warn_assign_undef = os.getenv("KCONFIG_WARN_UNDEF_ASSIGN") == "y" >> + self.warn_assign_override = True >> + self.warn_assign_redun = True >> + self._warn_assign_no_prompt = True >> + >> + self.warnings = [] >> + >> + self.config_prefix = os.getenv("CONFIG_", "CONFIG_") >> + # Regular expressions for parsing .config files >> + self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)") >> + self._unset_match = _re_match(r"# {}([^ ]+) is not set".format( >> + self.config_prefix)) >> + >> + self.config_header = os.getenv("KCONFIG_CONFIG_HEADER", "") >> + self.header_header = os.getenv("KCONFIG_AUTOHEADER_HEADER", "") >> + >> + self.syms = {} >> + self.const_syms = {} >> + self.defined_syms = [] >> + self.missing_syms = [] >> + self.named_choices = {} >> + self.choices = [] >> + self.menus = [] >> + self.comments = [] >> + >> + for nmy in "n", "m", "y": >> + sym = Symbol() >> + sym.kconfig = self >> + sym.name = nmy >> + sym.is_constant = True >> + sym.orig_type = TRISTATE >> + sym._cached_tri_val = STR_TO_TRI[nmy] >> + >> + self.const_syms[nmy] = sym >> + >> + self.n = self.const_syms["n"] >> + self.m = self.const_syms["m"] >> + self.y = self.const_syms["y"] >> + >> + # Make n/m/y well-formed symbols >> + for nmy in "n", "m", "y": >> + sym = self.const_syms[nmy] >> + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n >> + >> + # Maps preprocessor variables names to Variable instances >> + self.variables = {} >> + >> + # Predefined preprocessor functions, with min/max number of arguments >> + self._functions = { >> + "info": (_info_fn, 1, 1), >> + "error-if": (_error_if_fn, 2, 2), >> + "filename": (_filename_fn, 0, 0), >> + "lineno": (_lineno_fn, 0, 0), >> + "shell": (_shell_fn, 1, 1), >> + "warning-if": (_warning_if_fn, 2, 2), >> + } >> + >> + # Add any user-defined preprocessor functions >> + try: >> + self._functions.update( >> + importlib.import_module( >> + os.getenv("KCONFIG_FUNCTIONS", "kconfigfunctions") >> + ).functions) >> + except ImportError: >> + pass >> + >> + # This determines whether previously unseen symbols are registered. >> + # They shouldn't be if we parse expressions after parsing, as part of >> + # Kconfig.eval_string(). >> + self._parsing_kconfigs = True >> + >> + self.modules = self._lookup_sym("MODULES") >> + self.defconfig_list = None >> + >> + self.top_node = MenuNode() >> + self.top_node.kconfig = self >> + self.top_node.item = MENU >> + self.top_node.is_menuconfig = True >> + self.top_node.visibility = self.y >> + self.top_node.prompt = ("Main menu", self.y) >> + self.top_node.parent = None >> + self.top_node.dep = self.y >> + self.top_node.filename = filename >> + self.top_node.linenr = 1 >> + self.top_node.include_path = () >> + >> + # Parse the Kconfig files >> + >> + # Not used internally. Provided as a convenience. >> + self.kconfig_filenames = [filename] >> + self.env_vars = set() >> + >> + # Keeps track of the location in the parent Kconfig files. Kconfig >> + # files usually source other Kconfig files. See _enter_file(). >> + self._filestack = [] >> + self._include_path = () >> + >> + # The current parsing location >> + self.filename = filename >> + self.linenr = 0 >> + >> + # Used to avoid retokenizing lines when we discover that they're not >> + # part of the construct currently being parsed. This is kinda like an >> + # unget operation. >> + self._reuse_tokens = False >> + >> + # Open the top-level Kconfig file. Store the readline() method directly >> + # as a small optimization. >> + self._readline = self._open(join(self.srctree, filename), "r").readline >> + >> + try: >> + # Parse the Kconfig files. Returns the last node, which we >> + # terminate with '.next = None'. >> + self._parse_block(None, self.top_node, self.top_node).next = None >> + self.top_node.list = self.top_node.next >> + self.top_node.next = None >> + except UnicodeDecodeError as e: >> + _decoding_error(e, self.filename) >> + >> + # Close the top-level Kconfig file. __self__ fetches the 'file' object >> + # for the method. >> + self._readline.__self__.close() >> + >> + self._parsing_kconfigs = False >> + >> + # Do various menu tree post-processing >> + self._finalize_node(self.top_node, self.y) >> + >> + self.unique_defined_syms = _ordered_unique(self.defined_syms) >> + self.unique_choices = _ordered_unique(self.choices) >> + >> + # Do sanity checks. Some of these depend on everything being finalized. >> + self._check_sym_sanity() >> + self._check_choice_sanity() >> + >> + # KCONFIG_STRICT is an older alias for KCONFIG_WARN_UNDEF, supported >> + # for backwards compatibility >> + if os.getenv("KCONFIG_WARN_UNDEF") == "y" or \ >> + os.getenv("KCONFIG_STRICT") == "y": >> + >> + self._check_undef_syms() >> + >> + # Build Symbol._dependents for all symbols and choices >> + self._build_dep() >> + >> + # Check for dependency loops >> + check_dep_loop_sym = _check_dep_loop_sym # Micro-optimization >> + for sym in self.unique_defined_syms: >> + check_dep_loop_sym(sym, False) >> + >> + # Add extra dependencies from choices to choice symbols that get >> + # awkward during dependency loop detection >> + self._add_choice_deps() >> + >> + @property >> + def mainmenu_text(self): >> + """ >> + See the class documentation. >> + """ >> + return self.top_node.prompt[0] >> + >> + @property >> + def defconfig_filename(self): >> + """ >> + See the class documentation. >> + """ >> + if self.defconfig_list: >> + for filename, cond in self.defconfig_list.defaults: >> + if expr_value(cond): >> + try: >> + with self._open_config(filename.str_value) as f: >> + return f.name >> + except EnvironmentError: >> + continue >> + >> + return None >> + >> + def load_config(self, filename=None, replace=True, verbose=None): >> + """ >> + Loads symbol values from a file in the .config format. Equivalent to >> + calling Symbol.set_value() to set each of the values. >> + >> + "# CONFIG_FOO is not set" within a .config file sets the user value of >> + FOO to n. The C tools work the same way. >> + >> + For each symbol, the Symbol.user_value attribute holds the value the >> + symbol was assigned in the .config file (if any). The user value might >> + differ from Symbol.str/tri_value if there are unsatisfied dependencies. >> + >> + Calling this function also updates the Kconfig.missing_syms attribute >> + with a list of all assignments to undefined symbols within the >> + configuration file. Kconfig.missing_syms is cleared if 'replace' is >> + True, and appended to otherwise. See the documentation for >> + Kconfig.missing_syms as well. >> + >> + See the Kconfig.__init__() docstring for raised exceptions >> + (OSError/IOError). KconfigError is never raised here. >> + >> + filename (default: None): >> + Path to load configuration from (a string). Respects $srctree if set >> + (see the class documentation). >> + >> + If 'filename' is None (the default), the configuration file to load >> + (if any) is calculated automatically, giving the behavior you'd >> + usually want: >> + >> + 1. If the KCONFIG_CONFIG environment variable is set, it gives the >> + path to the configuration file to load. Otherwise, ".config" is >> + used. See standard_config_filename(). >> + >> + 2. If the path from (1.) doesn't exist, the configuration file >> + given by kconf.defconfig_filename is loaded instead, which is >> + derived from the 'option defconfig_list' symbol. >> + >> + 3. If (1.) and (2.) fail to find a configuration file to load, no >> + configuration file is loaded, and symbols retain their current >> + values (e.g., their default values). This is not an error. >> + >> + See the return value as well. >> + >> + replace (default: True): >> + If True, all existing user values will be cleared before loading the >> + .config. Pass False to merge configurations. >> + >> + verbose (default: None): >> + Limited backwards compatibility to prevent crashes. A warning is >> + printed if anything but None is passed. >> + >> + Prior to Kconfiglib 12.0.0, this option enabled printing of messages >> + to stdout when 'filename' was None. A message is (always) returned >> + now instead, which is more flexible. >> + >> + Will probably be removed in some future version. >> + >> + Returns a string with a message saying which file got loaded (or >> + possibly that no file got loaded, when 'filename' is None). This is >> + meant to reduce boilerplate in tools, which can do e.g. >> + print(kconf.load_config()). The returned message distinguishes between >> + loading (replace == True) and merging (replace == False). >> + """ >> + if verbose is not None: >> + _warn_verbose_deprecated("load_config") >> + >> + msg = None >> + if filename is None: >> + filename = standard_config_filename() >> + if not exists(filename) and \ >> + not exists(join(self.srctree, filename)): >> + defconfig = self.defconfig_filename >> + if defconfig is None: >> + return "Using default symbol values (no '{}')" \ >> + .format(filename) >> + >> + msg = " default configuration '{}' (no '{}')" \ >> + .format(defconfig, filename) >> + filename = defconfig >> + >> + if not msg: >> + msg = " configuration '{}'".format(filename) >> + >> + # Disable the warning about assigning to symbols without prompts. This >> + # is normal and expected within a .config file. >> + self._warn_assign_no_prompt = False >> + >> + # This stub only exists to make sure _warn_assign_no_prompt gets >> + # reenabled >> + try: >> + self._load_config(filename, replace) >> + except UnicodeDecodeError as e: >> + _decoding_error(e, filename) >> + finally: >> + self._warn_assign_no_prompt = True >> + >> + return ("Loaded" if replace else "Merged") + msg >> + >> + def _load_config(self, filename, replace): >> + with self._open_config(filename) as f: >> + if replace: >> + self.missing_syms = [] >> + >> + # If we're replacing the configuration, keep track of which >> + # symbols and choices got set so that we can unset the rest >> + # later. This avoids invalidating everything and is faster. >> + # Another benefit is that invalidation must be rock solid for >> + # it to work, making it a good test. >> + >> + for sym in self.unique_defined_syms: >> + sym._was_set = False >> + >> + for choice in self.unique_choices: >> + choice._was_set = False >> + >> + # Small optimizations >> + set_match = self._set_match >> + unset_match = self._unset_match >> + get_sym = self.syms.get >> + >> + for linenr, line in enumerate(f, 1): >> + # The C tools ignore trailing whitespace >> + line = line.rstrip() >> + >> + match = set_match(line) >> + if match: >> + name, val = match.groups() >> + sym = get_sym(name) >> + if not sym or not sym.nodes: >> + self._undef_assign(name, val, filename, linenr) >> + continue >> + >> + if sym.orig_type in _BOOL_TRISTATE: >> + # The C implementation only checks the first character >> + # to the right of '=', for whatever reason >> + if not (sym.orig_type is BOOL >> + and val.startswith(("y", "n")) or >> + sym.orig_type is TRISTATE >> + and val.startswith(("y", "m", "n"))): >> + self._warn("'{}' is not a valid value for the {} " >> + "symbol {}. Assignment ignored." >> + .format(val, TYPE_TO_STR[sym.orig_type], >> + sym.name_and_loc), >> + filename, linenr) >> + continue >> + >> + val = val[0] >> + >> + if sym.choice and val != "n": >> + # During .config loading, we infer the mode of the >> + # choice from the kind of values that are assigned >> + # to the choice symbols >> + >> + prev_mode = sym.choice.user_value >> + if prev_mode is not None and \ >> + TRI_TO_STR[prev_mode] != val: >> + >> + self._warn("both m and y assigned to symbols " >> + "within the same choice", >> + filename, linenr) >> + >> + # Set the choice's mode >> + sym.choice.set_value(val) >> + >> + elif sym.orig_type is STRING: >> + match = _conf_string_match(val) >> + if not match: >> + self._warn("malformed string literal in " >> + "assignment to {}. Assignment ignored." >> + .format(sym.name_and_loc), >> + filename, linenr) >> + continue >> + >> + val = unescape(match.group(1)) >> + >> + else: >> + match = unset_match(line) >> + if not match: >> + # Print a warning for lines that match neither >> + # set_match() nor unset_match() and that are not blank >> + # lines or comments. 'line' has already been >> + # rstrip()'d, so blank lines show up as "" here. >> + if line and not line.lstrip().startswith("#"): >> + self._warn("ignoring malformed line '{}'" >> + .format(line), >> + filename, linenr) >> + >> + continue >> + >> + name = match.group(1) >> + sym = get_sym(name) >> + if not sym or not sym.nodes: >> + self._undef_assign(name, "n", filename, linenr) >> + continue >> + >> + if sym.orig_type not in _BOOL_TRISTATE: >> + continue >> + >> + val = "n" >> + >> + # Done parsing the assignment. Set the value. >> + >> + if sym._was_set: >> + self._assigned_twice(sym, val, filename, linenr) >> + >> + sym.set_value(val) >> + >> + if replace: >> + # If we're replacing the configuration, unset the symbols that >> + # didn't get set >> + >> + for sym in self.unique_defined_syms: >> + if not sym._was_set: >> + sym.unset_value() >> + >> + for choice in self.unique_choices: >> + if not choice._was_set: >> + choice.unset_value() >> + >> + def _undef_assign(self, name, val, filename, linenr): >> + # Called for assignments to undefined symbols during .config loading >> + >> + self.missing_syms.append((name, val)) >> + if self.warn_assign_undef: >> + self._warn( >> + "attempt to assign the value '{}' to the undefined symbol {}" >> + .format(val, name), filename, linenr) >> + >> + def _assigned_twice(self, sym, new_val, filename, linenr): >> + # Called when a symbol is assigned more than once in a .config file >> + >> + # Use strings for bool/tristate user values in the warning >> + if sym.orig_type in _BOOL_TRISTATE: >> + user_val = TRI_TO_STR[sym.user_value] >> + else: >> + user_val = sym.user_value >> + >> + msg = '{} set more than once. Old value "{}", new value "{}".'.format( >> + sym.name_and_loc, user_val, new_val) >> + >> + if user_val == new_val: >> + if self.warn_assign_redun: >> + self._warn(msg, filename, linenr) >> + elif self.warn_assign_override: >> + self._warn(msg, filename, linenr) >> + >> + def load_allconfig(self, filename): >> + """ >> + Helper for all*config. Loads (merges) the configuration file specified >> + by KCONFIG_ALLCONFIG, if any. See Documentation/kbuild/kconfig.txt in >> + the Linux kernel. >> + >> + Disables warnings for duplicated assignments within configuration files >> + for the duration of the call >> + (kconf.warn_assign_override/warn_assign_redun = False), and restores >> + the previous warning settings at the end. The KCONFIG_ALLCONFIG >> + configuration file is expected to override symbols. >> + >> + Exits with sys.exit() (which raises a SystemExit exception) and prints >> + an error to stderr if KCONFIG_ALLCONFIG is set but the configuration >> + file can't be opened. >> + >> + filename: >> + Command-specific configuration filename - "allyes.config", >> + "allno.config", etc. >> + """ >> + load_allconfig(self, filename) >> + >> + def write_autoconf(self, filename=None, header=None): >> + r""" >> + Writes out symbol values as a C header file, matching the format used >> + by include/generated/autoconf.h in the kernel. >> + >> + The ordering of the #defines matches the one generated by >> + write_config(). The order in the C implementation depends on the hash >> + table implementation as of writing, and so won't match. >> + >> + If 'filename' exists and its contents is identical to what would get >> + written out, it is left untouched. This avoids updating file metadata >> + like the modification time and possibly triggering redundant work in >> + build tools. >> + >> + filename (default: None): >> + Path to write header to. >> + >> + If None (the default), the path in the environment variable >> + KCONFIG_AUTOHEADER is used if set, and "include/generated/autoconf.h" >> + otherwise. This is compatible with the C tools. >> + >> + header (default: None): >> + Text inserted verbatim at the beginning of the file. You would >> + usually want it enclosed in '/* */' to make it a C comment, and >> + include a trailing newline. >> + >> + If None (the default), the value of the environment variable >> + KCONFIG_AUTOHEADER_HEADER had when the Kconfig instance was created >> + will be used if it was set, and no header otherwise. See the >> + Kconfig.header_header attribute. >> + >> + Returns a string with a message saying that the header got saved, or >> + that there were no changes to it. This is meant to reduce boilerplate >> + in tools, which can do e.g. print(kconf.write_autoconf()). >> + """ >> + if filename is None: >> + filename = os.getenv("KCONFIG_AUTOHEADER", >> + "include/generated/autoconf.h") >> + >> + if self._write_if_changed(filename, self._autoconf_contents(header)): >> + return "Kconfig header saved to '{}'".format(filename) >> + return "No change to Kconfig header in '{}'".format(filename) >> + >> + def _autoconf_contents(self, header): >> + # write_autoconf() helper. Returns the contents to write as a string, >> + # with 'header' or KCONFIG_AUTOHEADER_HEADER at the beginning. >> + >> + if header is None: >> + header = self.header_header >> + >> + chunks = [header] # "".join()ed later >> + add = chunks.append >> + >> + for sym in self.unique_defined_syms: >> + # _write_to_conf is determined when the value is calculated. This >> + # is a hidden function call due to property magic. >> + # >> + # Note: In client code, you can check if sym.config_string is empty >> + # instead, to avoid accessing the internal _write_to_conf variable >> + # (though it's likely to keep working). >> + val = sym.str_value >> + if not sym._write_to_conf: >> + continue >> + >> + if sym.orig_type in _BOOL_TRISTATE: >> + if val == "y": >> + add("#define {}{} 1\n" >> + .format(self.config_prefix, sym.name)) >> + elif val == "m": >> + add("#define {}{}_MODULE 1\n" >> + .format(self.config_prefix, sym.name)) >> + >> + elif sym.orig_type is STRING: >> + add('#define {}{} "{}"\n' >> + .format(self.config_prefix, sym.name, escape(val))) >> + >> + else: # sym.orig_type in _INT_HEX: >> + if sym.orig_type is HEX and \ >> + not val.startswith(("0x", "0X")): >> + val = "0x" + val >> + >> + add("#define {}{} {}\n" >> + .format(self.config_prefix, sym.name, val)) >> + >> + return "".join(chunks) >> + >> + def write_config(self, filename=None, header=None, save_old=True, >> + verbose=None): >> + r""" >> + Writes out symbol values in the .config format. The format matches the >> + C implementation, including ordering. >> + >> + Symbols appear in the same order in generated .config files as they do >> + in the Kconfig files. For symbols defined in multiple locations, a >> + single assignment is written out corresponding to the first location >> + where the symbol is defined. >> + >> + See the 'Intro to symbol values' section in the module docstring to >> + understand which symbols get written out. >> + >> + If 'filename' exists and its contents is identical to what would get >> + written out, it is left untouched. This avoids updating file metadata >> + like the modification time and possibly triggering redundant work in >> + build tools. >> + >> + See the Kconfig.__init__() docstring for raised exceptions >> + (OSError/IOError). KconfigError is never raised here. >> + >> + filename (default: None): >> + Path to write configuration to (a string). >> + >> + If None (the default), the path in the environment variable >> + KCONFIG_CONFIG is used if set, and ".config" otherwise. See >> + standard_config_filename(). >> + >> + header (default: None): >> + Text inserted verbatim at the beginning of the file. You would >> + usually want each line to start with '#' to make it a comment, and >> + include a trailing newline. >> + >> + if None (the default), the value of the environment variable >> + KCONFIG_CONFIG_HEADER had when the Kconfig instance was created will >> + be used if it was set, and no header otherwise. See the >> + Kconfig.config_header attribute. >> + >> + save_old (default: True): >> + If True and <filename> already exists, a copy of it will be saved to >> + <filename>.old in the same directory before the new configuration is >> + written. >> + >> + Errors are silently ignored if <filename>.old cannot be written (e.g. >> + due to being a directory, or <filename> being something like >> + /dev/null). >> + >> + verbose (default: None): >> + Limited backwards compatibility to prevent crashes. A warning is >> + printed if anything but None is passed. >> + >> + Prior to Kconfiglib 12.0.0, this option enabled printing of messages >> + to stdout when 'filename' was None. A message is (always) returned >> + now instead, which is more flexible. >> + >> + Will probably be removed in some future version. >> + >> + Returns a string with a message saying which file got saved. This is >> + meant to reduce boilerplate in tools, which can do e.g. >> + print(kconf.write_config()). >> + """ >> + if verbose is not None: >> + _warn_verbose_deprecated("write_config") >> + >> + if filename is None: >> + filename = standard_config_filename() >> + >> + contents = self._config_contents(header) >> + if self._contents_eq(filename, contents): >> + return "No change to configuration in '{}'".format(filename) >> + >> + if save_old: >> + _save_old(filename) >> + >> + with self._open(filename, "w") as f: >> + f.write(contents) >> + >> + return "Configuration saved to '{}'".format(filename) >> + >> + def _config_contents(self, header): >> + # write_config() helper. Returns the contents to write as a string, >> + # with 'header' or KCONFIG_CONFIG_HEADER at the beginning. >> + # >> + # More memory friendly would be to 'yield' the strings and >> + # "".join(_config_contents()), but it was a bit slower on my system. >> + >> + # node_iter() was used here before commit 3aea9f7 ("Add '# end of >> + # <menu>' after menus in .config"). Those comments get tricky to >> + # implement with it. >> + >> + for sym in self.unique_defined_syms: >> + sym._visited = False >> + >> + if header is None: >> + header = self.config_header >> + >> + chunks = [header] # "".join()ed later >> + add = chunks.append >> + >> + # Did we just print an '# end of ...' comment? >> + after_end_comment = False >> + >> + node = self.top_node >> + while 1: >> + # Jump to the next node with an iterative tree walk >> + if node.list: >> + node = node.list >> + elif node.next: >> + node = node.next >> + else: >> + while node.parent: >> + node = node.parent >> + >> + # Add a comment when leaving visible menus >> + if node.item is MENU and expr_value(node.dep) and \ >> + expr_value(node.visibility) and \ >> + node is not self.top_node: >> + add("# end of {}\n".format(node.prompt[0])) >> + after_end_comment = True >> + >> + if node.next: >> + node = node.next >> + break >> + else: >> + # No more nodes >> + return "".join(chunks) >> + >> + # Generate configuration output for the node >> + >> + item = node.item >> + >> + if item.__class__ is Symbol: >> + if item._visited: >> + continue >> + item._visited = True >> + >> + conf_string = item.config_string >> + if not conf_string: >> + continue >> + >> + if after_end_comment: >> + # Add a blank line before the first symbol printed after an >> + # '# end of ...' comment >> + after_end_comment = False >> + add("\n") >> + add(conf_string) >> + >> + elif expr_value(node.dep) and \ >> + ((item is MENU and expr_value(node.visibility)) or >> + item is COMMENT): >> + >> + add("\n#\n# {}\n#\n".format(node.prompt[0])) >> + after_end_comment = False >> + >> + def write_min_config(self, filename, header=None): >> + """ >> + Writes out a "minimal" configuration file, omitting symbols whose value >> + matches their default value. The format matches the one produced by >> + 'make savedefconfig'. >> + >> + The resulting configuration file is incomplete, but a complete >> + configuration can be derived from it by loading it. Minimal >> + configuration files can serve as a more manageable configuration format >> + compared to a "full" .config file, especially when configurations files >> + are merged or edited by hand. >> + >> + See the Kconfig.__init__() docstring for raised exceptions >> + (OSError/IOError). KconfigError is never raised here. >> + >> + filename: >> + Path to write minimal configuration to. >> + >> + header (default: None): >> + Text inserted verbatim at the beginning of the file. You would >> + usually want each line to start with '#' to make it a comment, and >> + include a final terminating newline. >> + >> + if None (the default), the value of the environment variable >> + KCONFIG_CONFIG_HEADER had when the Kconfig instance was created will >> + be used if it was set, and no header otherwise. See the >> + Kconfig.config_header attribute. >> + >> + Returns a string with a message saying the minimal configuration got >> + saved, or that there were no changes to it. This is meant to reduce >> + boilerplate in tools, which can do e.g. >> + print(kconf.write_min_config()). >> + """ >> + if self._write_if_changed(filename, self._min_config_contents(header)): >> + return "Minimal configuration saved to '{}'".format(filename) >> + return "No change to minimal configuration in '{}'".format(filename) >> + >> + def _min_config_contents(self, header): >> + # write_min_config() helper. Returns the contents to write as a string, >> + # with 'header' or KCONFIG_CONFIG_HEADER at the beginning. >> + >> + if header is None: >> + header = self.config_header >> + >> + chunks = [header] # "".join()ed later >> + add = chunks.append >> + >> + for sym in self.unique_defined_syms: >> + # Skip symbols that cannot be changed. Only check >> + # non-choice symbols, as selects don't affect choice >> + # symbols. >> + if not sym.choice and \ >> + sym.visibility <= expr_value(sym.rev_dep): >> + continue >> + >> + # Skip symbols whose value matches their default >> + if sym.str_value == sym._str_default(): >> + continue >> + >> + # Skip symbols that would be selected by default in a >> + # choice, unless the choice is optional or the symbol type >> + # isn't bool (it might be possible to set the choice mode >> + # to n or the symbol to m in those cases). >> + if sym.choice and \ >> + not sym.choice.is_optional and \ >> + sym.choice._selection_from_defaults() is sym and \ >> + sym.orig_type is BOOL and \ >> + sym.tri_value == 2: >> + continue >> + >> + add(sym.config_string) >> + >> + return "".join(chunks) >> + >> + def sync_deps(self, path): >> + """ >> + Creates or updates a directory structure that can be used to avoid >> + doing a full rebuild whenever the configuration is changed, mirroring >> + include/config/ in the kernel. >> + >> + This function is intended to be called during each build, before >> + compiling source files that depend on configuration symbols. >> + >> + See the Kconfig.__init__() docstring for raised exceptions >> + (OSError/IOError). KconfigError is never raised here. >> + >> + path: >> + Path to directory >> + >> + sync_deps(path) does the following: >> + >> + 1. If the directory <path> does not exist, it is created. >> + >> + 2. If <path>/auto.conf exists, old symbol values are loaded from it, >> + which are then compared against the current symbol values. If a >> + symbol has changed value (would generate different output in >> + autoconf.h compared to before), the change is signaled by >> + touch'ing a file corresponding to the symbol. >> + >> + The first time sync_deps() is run on a directory, <path>/auto.conf >> + won't exist, and no old symbol values will be available. This >> + logically has the same effect as updating the entire >> + configuration. >> + >> + The path to a symbol's file is calculated from the symbol's name >> + by replacing all '_' with '/' and appending '.h'. For example, the >> + symbol FOO_BAR_BAZ gets the file <path>/foo/bar/baz.h, and FOO >> + gets the file <path>/foo.h. >> + >> + This scheme matches the C tools. The point is to avoid having a >> + single directory with a huge number of files, which the underlying >> + filesystem might not handle well. >> + >> + 3. A new auto.conf with the current symbol values is written, to keep >> + track of them for the next build. >> + >> + If auto.conf exists and its contents is identical to what would >> + get written out, it is left untouched. This avoids updating file >> + metadata like the modification time and possibly triggering >> + redundant work in build tools. >> + >> + >> + The last piece of the puzzle is knowing what symbols each source file >> + depends on. Knowing that, dependencies can be added from source files >> + to the files corresponding to the symbols they depends on. The source >> + file will then get recompiled (only) when the symbol value changes >> + (provided sync_deps() is run first during each build). >> + >> + The tool in the kernel that extracts symbol dependencies from source >> + files is scripts/basic/fixdep.c. Missing symbol files also correspond >> + to "not changed", which fixdep deals with by using the $(wildcard) Make >> + function when adding symbol prerequisites to source files. >> + >> + In case you need a different scheme for your project, the sync_deps() >> + implementation can be used as a template. >> + """ >> + if not exists(path): >> + os.mkdir(path, 0o755) >> + >> + # Load old values from auto.conf, if any >> + self._load_old_vals(path) >> + >> + for sym in self.unique_defined_syms: >> + # _write_to_conf is determined when the value is calculated. This >> + # is a hidden function call due to property magic. >> + # >> + # Note: In client code, you can check if sym.config_string is empty >> + # instead, to avoid accessing the internal _write_to_conf variable >> + # (though it's likely to keep working). >> + val = sym.str_value >> + >> + # n tristate values do not get written to auto.conf and autoconf.h, >> + # making a missing symbol logically equivalent to n >> + >> + if sym._write_to_conf: >> + if sym._old_val is None and \ >> + sym.orig_type in _BOOL_TRISTATE and \ >> + val == "n": >> + # No old value (the symbol was missing or n), new value n. >> + # No change. >> + continue >> + >> + if val == sym._old_val: >> + # New value matches old. No change. >> + continue >> + >> + elif sym._old_val is None: >> + # The symbol wouldn't appear in autoconf.h (because >> + # _write_to_conf is false), and it wouldn't have appeared in >> + # autoconf.h previously either (because it didn't appear in >> + # auto.conf). No change. >> + continue >> + >> + # 'sym' has a new value. Flag it. >> + _touch_dep_file(path, sym.name) >> + >> + # Remember the current values as the "new old" values. >> + # >> + # This call could go anywhere after the call to _load_old_vals(), but >> + # putting it last means _sync_deps() can be safely rerun if it fails >> + # before this point. >> + self._write_old_vals(path) >> + >> + def _load_old_vals(self, path): >> + # Loads old symbol values from auto.conf into a dedicated >> + # Symbol._old_val field. Mirrors load_config(). >> + # >> + # The extra field could be avoided with some trickery involving dumping >> + # symbol values and restoring them later, but this is simpler and >> + # faster. The C tools also use a dedicated field for this purpose. >> + >> + for sym in self.unique_defined_syms: >> + sym._old_val = None >> + >> + try: >> + auto_conf = self._open(join(path, "auto.conf"), "r") >> + except EnvironmentError as e: >> + if e.errno == errno.ENOENT: >> + # No old values >> + return >> + raise >> + >> + with auto_conf as f: >> + for line in f: >> + match = self._set_match(line) >> + if not match: >> + # We only expect CONFIG_FOO=... (and possibly a header >> + # comment) in auto.conf >> + continue >> + >> + name, val = match.groups() >> + if name in self.syms: >> + sym = self.syms[name] >> + >> + if sym.orig_type is STRING: >> + match = _conf_string_match(val) >> + if not match: >> + continue >> + val = unescape(match.group(1)) >> + >> + self.syms[name]._old_val = val >> + else: >> + # Flag that the symbol no longer exists, in >> + # case something still depends on it >> + _touch_dep_file(path, name) >> + >> + def _write_old_vals(self, path): >> + # Helper for writing auto.conf. Basically just a simplified >> + # write_config() that doesn't write any comments (including >> + # '# CONFIG_FOO is not set' comments). The format matches the C >> + # implementation, though the ordering is arbitrary there (depends on >> + # the hash table implementation). >> + # >> + # A separate helper function is neater than complicating write_config() >> + # by passing a flag to it, plus we only need to look at symbols here. >> + >> + self._write_if_changed( >> + os.path.join(path, "auto.conf"), >> + self._old_vals_contents()) >> + >> + def _old_vals_contents(self): >> + # _write_old_vals() helper. Returns the contents to write as a string. >> + >> + # Temporary list instead of generator makes this a bit faster >> + return "".join([ >> + sym.config_string for sym in self.unique_defined_syms >> + if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value) >> + ]) >> + >> + def node_iter(self, unique_syms=False): >> + """ >> + Returns a generator for iterating through all MenuNode's in the Kconfig >> + tree. The iteration is done in Kconfig definition order (each node is >> + visited before its children, and the children of a node are visited >> + before the next node). >> + >> + The Kconfig.top_node menu node is skipped. It contains an implicit menu >> + that holds the top-level items. >> + >> + As an example, the following code will produce a list equal to >> + Kconfig.defined_syms: >> + >> + defined_syms = [node.item for node in kconf.node_iter() >> + if isinstance(node.item, Symbol)] >> + >> + unique_syms (default: False): >> + If True, only the first MenuNode will be included for symbols defined >> + in multiple locations. >> + >> + Using kconf.node_iter(True) in the example above would give a list >> + equal to unique_defined_syms. >> + """ >> + if unique_syms: >> + for sym in self.unique_defined_syms: >> + sym._visited = False >> + >> + node = self.top_node >> + while 1: >> + # Jump to the next node with an iterative tree walk >> + if node.list: >> + node = node.list >> + elif node.next: >> + node = node.next >> + else: >> + while node.parent: >> + node = node.parent >> + if node.next: >> + node = node.next >> + break >> + else: >> + # No more nodes >> + return >> + >> + if unique_syms and node.item.__class__ is Symbol: >> + if node.item._visited: >> + continue >> + node.item._visited = True >> + >> + yield node >> + >> + def eval_string(self, s): >> + """ >> + Returns the tristate value of the expression 's', represented as 0, 1, >> + and 2 for n, m, and y, respectively. Raises KconfigError on syntax >> + errors. Warns if undefined symbols are referenced. >> + >> + As an example, if FOO and BAR are tristate symbols at least one of >> + which has the value y, then eval_string("y && (FOO || BAR)") returns >> + 2 (y). >> + >> + To get the string value of non-bool/tristate symbols, use >> + Symbol.str_value. eval_string() always returns a tristate value, and >> + all non-bool/tristate symbols have the tristate value 0 (n). >> + >> + The expression parsing is consistent with how parsing works for >> + conditional ('if ...') expressions in the configuration, and matches >> + the C implementation. m is rewritten to 'm && MODULES', so >> + eval_string("m") will return 0 (n) unless modules are enabled. >> + """ >> + # The parser is optimized to be fast when parsing Kconfig files (where >> + # an expression can never appear at the beginning of a line). We have >> + # to monkey-patch things a bit here to reuse it. >> + >> + self.filename = None >> + >> + self._tokens = self._tokenize("if " + s) >> + # Strip "if " to avoid giving confusing error messages >> + self._line = s >> + self._tokens_i = 1 # Skip the 'if' token >> + >> + return expr_value(self._expect_expr_and_eol()) >> + >> + def unset_values(self): >> + """ >> + Removes any user values from all symbols, as if Kconfig.load_config() >> + or Symbol.set_value() had never been called. >> + """ >> + self._warn_assign_no_prompt = False >> + try: >> + # set_value() already rejects undefined symbols, and they don't >> + # need to be invalidated (because their value never changes), so we >> + # can just iterate over defined symbols >> + for sym in self.unique_defined_syms: >> + sym.unset_value() >> + >> + for choice in self.unique_choices: >> + choice.unset_value() >> + finally: >> + self._warn_assign_no_prompt = True >> + >> + def enable_warnings(self): >> + """ >> + Do 'Kconfig.warn = True' instead. Maintained for backwards >> + compatibility. >> + """ >> + self.warn = True >> + >> + def disable_warnings(self): >> + """ >> + Do 'Kconfig.warn = False' instead. Maintained for backwards >> + compatibility. >> + """ >> + self.warn = False >> + >> + def enable_stderr_warnings(self): >> + """ >> + Do 'Kconfig.warn_to_stderr = True' instead. Maintained for backwards >> + compatibility. >> + """ >> + self.warn_to_stderr = True >> + >> + def disable_stderr_warnings(self): >> + """ >> + Do 'Kconfig.warn_to_stderr = False' instead. Maintained for backwards >> + compatibility. >> + """ >> + self.warn_to_stderr = False >> + >> + def enable_undef_warnings(self): >> + """ >> + Do 'Kconfig.warn_assign_undef = True' instead. Maintained for backwards >> + compatibility. >> + """ >> + self.warn_assign_undef = True >> + >> + def disable_undef_warnings(self): >> + """ >> + Do 'Kconfig.warn_assign_undef = False' instead. Maintained for >> + backwards compatibility. >> + """ >> + self.warn_assign_undef = False >> + >> + def enable_override_warnings(self): >> + """ >> + Do 'Kconfig.warn_assign_override = True' instead. Maintained for >> + backwards compatibility. >> + """ >> + self.warn_assign_override = True >> + >> + def disable_override_warnings(self): >> + """ >> + Do 'Kconfig.warn_assign_override = False' instead. Maintained for >> + backwards compatibility. >> + """ >> + self.warn_assign_override = False >> + >> + def enable_redun_warnings(self): >> + """ >> + Do 'Kconfig.warn_assign_redun = True' instead. Maintained for backwards >> + compatibility. >> + """ >> + self.warn_assign_redun = True >> + >> + def disable_redun_warnings(self): >> + """ >> + Do 'Kconfig.warn_assign_redun = False' instead. Maintained for >> + backwards compatibility. >> + """ >> + self.warn_assign_redun = False >> + >> + def __repr__(self): >> + """ >> + Returns a string with information about the Kconfig object when it is >> + evaluated on e.g. the interactive Python prompt. >> + """ >> + def status(flag): >> + return "enabled" if flag else "disabled" >> + >> + return "<{}>".format(", ".join(( >> + "configuration with {} symbols".format(len(self.syms)), >> + 'main menu prompt "{}"'.format(self.mainmenu_text), >> + "srctree is current directory" if not self.srctree else >> + 'srctree "{}"'.format(self.srctree), >> + 'config symbol prefix "{}"'.format(self.config_prefix), >> + "warnings " + status(self.warn), >> + "printing of warnings to stderr " + status(self.warn_to_stderr), >> + "undef. symbol assignment warnings " + >> + status(self.warn_assign_undef), >> + "overriding symbol assignment warnings " + >> + status(self.warn_assign_override), >> + "redundant symbol assignment warnings " + >> + status(self.warn_assign_redun) >> + ))) >> + >> + # >> + # Private methods >> + # >> + >> + >> + # >> + # File reading >> + # >> + >> + def _open_config(self, filename): >> + # Opens a .config file. First tries to open 'filename', then >> + # '$srctree/filename' if $srctree was set when the configuration was >> + # loaded. >> + >> + try: >> + return self._open(filename, "r") >> + except EnvironmentError as e: >> + # This will try opening the same file twice if $srctree is unset, >> + # but it's not a big deal >> + try: >> + return self._open(join(self.srctree, filename), "r") >> + except EnvironmentError as e2: >> + # This is needed for Python 3, because e2 is deleted after >> + # the try block: >> + # >> + # https://docs.python.org/3/reference/compound_stmts.html#the-try-statement >> + e = e2 >> + >> + raise _KconfigIOError( >> + e, "Could not open '{}' ({}: {}). Check that the $srctree " >> + "environment variable ({}) is set correctly." >> + .format(filename, errno.errorcode[e.errno], e.strerror, >> + "set to '{}'".format(self.srctree) if self.srctree >> + else "unset or blank")) >> + >> + def _enter_file(self, filename): >> + # Jumps to the beginning of a sourced Kconfig file, saving the previous >> + # position and file object. >> + # >> + # filename: >> + # Absolute path to file >> + >> + # Path relative to $srctree, stored in e.g. self.filename (which makes >> + # it indirectly show up in MenuNode.filename). Equals 'filename' for >> + # absolute paths passed to 'source'. >> + if filename.startswith(self._srctree_prefix): >> + # Relative path (or a redundant absolute path to within $srctree, >> + # but it's probably fine to reduce those too) >> + rel_filename = filename[len(self._srctree_prefix):] >> + else: >> + # Absolute path >> + rel_filename = filename >> + >> + self.kconfig_filenames.append(rel_filename) >> + >> + # The parent Kconfig files are represented as a list of >> + # (<include path>, <Python 'file' object for Kconfig file>) tuples. >> + # >> + # <include path> is immutable and holds a *tuple* of >> + # (<filename>, <linenr>) tuples, giving the locations of the 'source' >> + # statements in the parent Kconfig files. The current include path is >> + # also available in Kconfig._include_path. >> + # >> + # The point of this redundant setup is to allow Kconfig._include_path >> + # to be assigned directly to MenuNode.include_path without having to >> + # copy it, sharing it wherever possible. >> + >> + # Save include path and 'file' object (via its 'readline' function) >> + # before entering the file >> + self._filestack.append((self._include_path, self._readline)) >> + >> + # _include_path is a tuple, so this rebinds the variable instead of >> + # doing in-place modification >> + self._include_path += ((self.filename, self.linenr),) >> + >> + # Check for recursive 'source' >> + for name, _ in self._include_path: >> + if name == rel_filename: >> + raise KconfigError( >> + "\n{}:{}: recursive 'source' of '{}' detected. Check that " >> + "environment variables are set correctly.\n" >> + "Include path:\n{}" >> + .format(self.filename, self.linenr, rel_filename, >> + "\n".join("{}:{}".format(name, linenr) >> + for name, linenr in self._include_path))) >> + >> + try: >> + self._readline = self._open(filename, "r").readline >> + except EnvironmentError as e: >> + # We already know that the file exists >> + raise _KconfigIOError( >> + e, "{}:{}: Could not open '{}' (in '{}') ({}: {})" >> + .format(self.filename, self.linenr, filename, >> + self._line.strip(), >> + errno.errorcode[e.errno], e.strerror)) >> + >> + self.filename = rel_filename >> + self.linenr = 0 >> + >> + def _leave_file(self): >> + # Returns from a Kconfig file to the file that sourced it. See >> + # _enter_file(). >> + >> + # Restore location from parent Kconfig file >> + self.filename, self.linenr = self._include_path[-1] >> + # Restore include path and 'file' object >> + self._readline.__self__.close() # __self__ fetches the 'file' object >> + self._include_path, self._readline = self._filestack.pop() >> + >> + def _next_line(self): >> + # Fetches and tokenizes the next line from the current Kconfig file. >> + # Returns False at EOF and True otherwise. >> + >> + # We might already have tokens from parsing a line and discovering that >> + # it's part of a different construct >> + if self._reuse_tokens: >> + self._reuse_tokens = False >> + # self._tokens_i is known to be 1 here, because _parse_props() >> + # leaves it like that when it can't recognize a line (or parses a >> + # help text) >> + return True >> + >> + # readline() returns '' over and over at EOF, which we rely on for help >> + # texts at the end of files (see _line_after_help()) >> + line = self._readline() >> + if not line: >> + return False >> + self.linenr += 1 >> + >> + # Handle line joining >> + while line.endswith("\\\n"): >> + line = line[:-2] + self._readline() >> + self.linenr += 1 >> + >> + self._tokens = self._tokenize(line) >> + # Initialize to 1 instead of 0 to factor out code from _parse_block() >> + # and _parse_props(). They immediately fetch self._tokens[0]. >> + self._tokens_i = 1 >> + >> + return True >> + >> + def _line_after_help(self, line): >> + # Tokenizes a line after a help text. This case is special in that the >> + # line has already been fetched (to discover that it isn't part of the >> + # help text). >> + # >> + # An earlier version used a _saved_line variable instead that was >> + # checked in _next_line(). This special-casing gets rid of it and makes >> + # _reuse_tokens alone sufficient to handle unget. >> + >> + # Handle line joining >> + while line.endswith("\\\n"): >> + line = line[:-2] + self._readline() >> + self.linenr += 1 >> + >> + self._tokens = self._tokenize(line) >> + self._reuse_tokens = True >> + >> + def _write_if_changed(self, filename, contents): >> + # Writes 'contents' into 'filename', but only if it differs from the >> + # current contents of the file. >> + # >> + # Another variant would be write a temporary file on the same >> + # filesystem, compare the files, and rename() the temporary file if it >> + # differs, but it breaks stuff like write_config("/dev/null"), which is >> + # used out there to force evaluation-related warnings to be generated. >> + # This simple version is pretty failsafe and portable. >> + # >> + # Returns True if the file has changed and is updated, and False >> + # otherwise. >> + >> + if self._contents_eq(filename, contents): >> + return False >> + with self._open(filename, "w") as f: >> + f.write(contents) >> + return True >> + >> + def _contents_eq(self, filename, contents): >> + # Returns True if the contents of 'filename' is 'contents' (a string), >> + # and False otherwise (including if 'filename' can't be opened/read) >> + >> + try: >> + with self._open(filename, "r") as f: >> + # Robust re. things like encoding and line endings (mmap() >> + # trickery isn't) >> + return f.read(len(contents) + 1) == contents >> + except EnvironmentError: >> + # If the error here would prevent writing the file as well, we'll >> + # notice it later >> + return False >> + >> + # >> + # Tokenization >> + # >> + >> + def _lookup_sym(self, name): >> + # Fetches the symbol 'name' from the symbol table, creating and >> + # registering it if it does not exist. If '_parsing_kconfigs' is False, >> + # it means we're in eval_string(), and new symbols won't be registered. >> + >> + if name in self.syms: >> + return self.syms[name] >> + >> + sym = Symbol() >> + sym.kconfig = self >> + sym.name = name >> + sym.is_constant = False >> + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n >> + >> + if self._parsing_kconfigs: >> + self.syms[name] = sym >> + else: >> + self._warn("no symbol {} in configuration".format(name)) >> + >> + return sym >> + >> + def _lookup_const_sym(self, name): >> + # Like _lookup_sym(), for constant (quoted) symbols >> + >> + if name in self.const_syms: >> + return self.const_syms[name] >> + >> + sym = Symbol() >> + sym.kconfig = self >> + sym.name = name >> + sym.is_constant = True >> + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n >> + >> + if self._parsing_kconfigs: >> + self.const_syms[name] = sym >> + >> + return sym >> + >> + def _tokenize(self, s): >> + # Parses 's', returning a None-terminated list of tokens. Registers any >> + # new symbols encountered with _lookup(_const)_sym(). >> + # >> + # Tries to be reasonably speedy by processing chunks of text via >> + # regexes and string operations where possible. This is the biggest >> + # hotspot during parsing. >> + # >> + # It might be possible to rewrite this to 'yield' tokens instead, >> + # working across multiple lines. Lookback and compatibility with old >> + # janky versions of the C tools complicate things though. >> + >> + self._line = s # Used for error reporting >> + >> + # Initial token on the line >> + match = _command_match(s) >> + if not match: >> + if s.isspace() or s.lstrip().startswith("#"): >> + return (None,) >> + self._parse_error("unknown token at start of line") >> + >> + # Tricky implementation detail: While parsing a token, 'token' refers >> + # to the previous token. See _STRING_LEX for why this is needed. >> + token = _get_keyword(match.group(1)) >> + if not token: >> + # Backwards compatibility with old versions of the C tools, which >> + # (accidentally) accepted stuff like "--help--" and "-help---". >> + # This was fixed in the C tools by commit c2264564 ("kconfig: warn >> + # of unhandled characters in Kconfig commands"), committed in July >> + # 2015, but it seems people still run Kconfiglib on older kernels. >> + if s.strip(" \t\n-") == "help": >> + return (_T_HELP, None) >> + >> + # If the first token is not a keyword (and not a weird help token), >> + # we have a preprocessor variable assignment (or a bare macro on a >> + # line) >> + self._parse_assignment(s) >> + return (None,) >> + >> + tokens = [token] >> + # The current index in the string being tokenized >> + i = match.end() >> + >> + # Main tokenization loop (for tokens past the first one) >> + while i < len(s): >> + # Test for an identifier/keyword first. This is the most common >> + # case. >> + match = _id_keyword_match(s, i) >> + if match: >> + # We have an identifier or keyword >> + >> + # Check what it is. lookup_sym() will take care of allocating >> + # new symbols for us the first time we see them. Note that >> + # 'token' still refers to the previous token. >> + >> + name = match.group(1) >> + keyword = _get_keyword(name) >> + if keyword: >> + # It's a keyword >> + token = keyword >> + # Jump past it >> + i = match.end() >> + >> + elif token not in _STRING_LEX: >> + # It's a non-const symbol, except we translate n, m, and y >> + # into the corresponding constant symbols, like the C >> + # implementation >> + >> + if "$" in name: >> + # Macro expansion within symbol name >> + name, s, i = self._expand_name(s, i) >> + else: >> + i = match.end() >> + >> + token = self.const_syms[name] if name in STR_TO_TRI else \ >> + self._lookup_sym(name) >> + >> + else: >> + # It's a case of missing quotes. For example, the >> + # following is accepted: >> + # >> + # menu unquoted_title >> + # >> + # config A >> + # tristate unquoted_prompt >> + # >> + # endmenu >> + # >> + # Named choices ('choice FOO') also end up here. >> + >> + if token is not _T_CHOICE: >> + self._warn("style: quotes recommended around '{}' in '{}'" >> + .format(name, self._line.strip()), >> + self.filename, self.linenr) >> + >> + token = name >> + i = match.end() >> + >> + else: >> + # Neither a keyword nor a non-const symbol >> + >> + # We always strip whitespace after tokens, so it is safe to >> + # assume that s[i] is the start of a token here. >> + c = s[i] >> + >> + if c in "\"'": >> + if "$" not in s and "\\" not in s: >> + # Fast path for lines without $ and \. Find the >> + # matching quote. >> + end_i = s.find(c, i + 1) + 1 >> + if not end_i: >> + self._parse_error("unterminated string") >> + val = s[i + 1:end_i - 1] >> + i = end_i >> + else: >> + # Slow path >> + s, end_i = self._expand_str(s, i) >> + >> + # os.path.expandvars() and the $UNAME_RELEASE replace() >> + # is a backwards compatibility hack, which should be >> + # reasonably safe as expandvars() leaves references to >> + # undefined env. vars. as is. >> + # >> + # The preprocessor functionality changed how >> + # environment variables are referenced, to $(FOO). >> + val = expandvars(s[i + 1:end_i - 1] >> + .replace("$UNAME_RELEASE", >> + _UNAME_RELEASE)) >> + >> + i = end_i >> + >> + # This is the only place where we don't survive with a >> + # single token of lookback: 'option env="FOO"' does not >> + # refer to a constant symbol named "FOO". >> + token = \ >> + val if token in _STRING_LEX or tokens[0] is _T_OPTION \ >> + else self._lookup_const_sym(val) >> + >> + elif s.startswith("&&", i): >> + token = _T_AND >> + i += 2 >> + >> + elif s.startswith("||", i): >> + token = _T_OR >> + i += 2 >> + >> + elif c == "=": >> + token = _T_EQUAL >> + i += 1 >> + >> + elif s.startswith("!=", i): >> + token = _T_UNEQUAL >> + i += 2 >> + >> + elif c == "!": >> + token = _T_NOT >> + i += 1 >> + >> + elif c == "(": >> + token = _T_OPEN_PAREN >> + i += 1 >> + >> + elif c == ")": >> + token = _T_CLOSE_PAREN >> + i += 1 >> + >> + elif c == "#": >> + break >> + >> + >> + # Very rare >> + >> + elif s.startswith("<=", i): >> + token = _T_LESS_EQUAL >> + i += 2 >> + >> + elif c == "<": >> + token = _T_LESS >> + i += 1 >> + >> + elif s.startswith(">=", i): >> + token = _T_GREATER_EQUAL >> + i += 2 >> + >> + elif c == ">": >> + token = _T_GREATER >> + i += 1 >> + >> + >> + else: >> + self._parse_error("unknown tokens in line") >> + >> + >> + # Skip trailing whitespace >> + while i < len(s) and s[i].isspace(): >> + i += 1 >> + >> + >> + # Add the token >> + tokens.append(token) >> + >> + # None-terminating the token list makes token fetching simpler/faster >> + tokens.append(None) >> + >> + return tokens >> + >> + # Helpers for syntax checking and token fetching. See the >> + # 'Intro to expressions' section for what a constant symbol is. >> + # >> + # More of these could be added, but the single-use cases are inlined as an >> + # optimization. >> + >> + def _expect_sym(self): >> + token = self._tokens[self._tokens_i] >> + self._tokens_i += 1 >> + >> + if token.__class__ is not Symbol: >> + self._parse_error("expected symbol") >> + >> + return token >> + >> + def _expect_nonconst_sym(self): >> + # Used for 'select' and 'imply' only. We know the token indices. >> + >> + token = self._tokens[1] >> + self._tokens_i = 2 >> + >> + if token.__class__ is not Symbol or token.is_constant: >> + self._parse_error("expected nonconstant symbol") >> + >> + return token >> + >> + def _expect_str_and_eol(self): >> + token = self._tokens[self._tokens_i] >> + self._tokens_i += 1 >> + >> + if token.__class__ is not str: >> + self._parse_error("expected string") >> + >> + if self._tokens[self._tokens_i] is not None: >> + self._trailing_tokens_error() >> + >> + return token >> + >> + def _expect_expr_and_eol(self): >> + expr = self._parse_expr(True) >> + >> + if self._tokens[self._tokens_i] is not None: >> + self._trailing_tokens_error() >> + >> + return expr >> + >> + def _check_token(self, token): >> + # If the next token is 'token', removes it and returns True >> + >> + if self._tokens[self._tokens_i] is token: >> + self._tokens_i += 1 >> + return True >> + return False >> + >> + # >> + # Preprocessor logic >> + # >> + >> + def _parse_assignment(self, s): >> + # Parses a preprocessor variable assignment, registering the variable >> + # if it doesn't already exist. Also takes care of bare macros on lines >> + # (which are allowed, and can be useful for their side effects). >> + >> + # Expand any macros in the left-hand side of the assignment (the >> + # variable name) >> + s = s.lstrip() >> + i = 0 >> + while 1: >> + i = _assignment_lhs_fragment_match(s, i).end() >> + if s.startswith("$(", i): >> + s, i = self._expand_macro(s, i, ()) >> + else: >> + break >> + >> + if s.isspace(): >> + # We also accept a bare macro on a line (e.g. >> + # $(warning-if,$(foo),ops)), provided it expands to a blank string >> + return >> + >> + # Assigned variable >> + name = s[:i] >> + >> + >> + # Extract assignment operator (=, :=, or +=) and value >> + rhs_match = _assignment_rhs_match(s, i) >> + if not rhs_match: >> + self._parse_error("syntax error") >> + >> + op, val = rhs_match.groups() >> + >> + >> + if name in self.variables: >> + # Already seen variable >> + var = self.variables[name] >> + else: >> + # New variable >> + var = Variable() >> + var.kconfig = self >> + var.name = name >> + var._n_expansions = 0 >> + self.variables[name] = var >> + >> + # += acts like = on undefined variables (defines a recursive >> + # variable) >> + if op == "+=": >> + op = "=" >> + >> + if op == "=": >> + var.is_recursive = True >> + var.value = val >> + elif op == ":=": >> + var.is_recursive = False >> + var.value = self._expand_whole(val, ()) >> + else: # op == "+=" >> + # += does immediate expansion if the variable was last set >> + # with := >> + var.value += " " + (val if var.is_recursive else >> + self._expand_whole(val, ())) >> + >> + def _expand_whole(self, s, args): >> + # Expands preprocessor macros in all of 's'. Used whenever we don't >> + # have to worry about delimiters. See _expand_macro() re. the 'args' >> + # parameter. >> + # >> + # Returns the expanded string. >> + >> + i = 0 >> + while 1: >> + i = s.find("$(", i) >> + if i == -1: >> + break >> + s, i = self._expand_macro(s, i, args) >> + return s >> + >> + def _expand_name(self, s, i): >> + # Expands a symbol name starting at index 'i' in 's'. >> + # >> + # Returns the expanded name, the expanded 's' (including the part >> + # before the name), and the index of the first character in the next >> + # token after the name. >> + >> + s, end_i = self._expand_name_iter(s, i) >> + name = s[i:end_i] >> + # isspace() is False for empty strings >> + if not name.strip(): >> + # Avoid creating a Kconfig symbol with a blank name. It's almost >> + # guaranteed to be an error. >> + self._parse_error("macro expanded to blank string") >> + >> + # Skip trailing whitespace >> + while end_i < len(s) and s[end_i].isspace(): >> + end_i += 1 >> + >> + return name, s, end_i >> + >> + def _expand_name_iter(self, s, i): >> + # Expands a symbol name starting at index 'i' in 's'. >> + # >> + # Returns the expanded 's' (including the part before the name) and the >> + # index of the first character after the expanded name in 's'. >> + >> + while 1: >> + match = _name_special_search(s, i) >> + >> + if match.group() != "$(": >> + return (s, match.start()) >> + s, i = self._expand_macro(s, match.start(), ()) >> + >> + def _expand_str(self, s, i): >> + # Expands a quoted string starting at index 'i' in 's'. Handles both >> + # backslash escapes and macro expansion. >> + # >> + # Returns the expanded 's' (including the part before the string) and >> + # the index of the first character after the expanded string in 's'. >> + >> + quote = s[i] >> + i += 1 # Skip over initial "/' >> + while 1: >> + match = _string_special_search(s, i) >> + if not match: >> + self._parse_error("unterminated string") >> + >> + >> + if match.group() == quote: >> + # Found the end of the string >> + return (s, match.end()) >> + >> + elif match.group() == "\\": >> + # Replace '\x' with 'x'. 'i' ends up pointing to the character >> + # after 'x', which allows macros to be canceled with '\$(foo)'. >> + i = match.end() >> + s = s[:match.start()] + s[i:] >> + >> + elif match.group() == "$(": >> + # A macro call within the string >> + s, i = self._expand_macro(s, match.start(), ()) >> + >> + else: >> + # A ' quote within " quotes or vice versa >> + i += 1 >> + >> + def _expand_macro(self, s, i, args): >> + # Expands a macro starting at index 'i' in 's'. If this macro resulted >> + # from the expansion of another macro, 'args' holds the arguments >> + # passed to that macro. >> + # >> + # Returns the expanded 's' (including the part before the macro) and >> + # the index of the first character after the expanded macro in 's'. >> + >> + res = s[:i] >> + i += 2 # Skip over "$(" >> + >> + arg_start = i # Start of current macro argument >> + new_args = [] # Arguments of this macro call >> + nesting = 0 # Current parentheses nesting level >> + >> + while 1: >> + match = _macro_special_search(s, i) >> + if not match: >> + self._parse_error("missing end parenthesis in macro expansion") >> + >> + >> + if match.group() == "(": >> + nesting += 1 >> + i = match.end() >> + >> + elif match.group() == ")": >> + if nesting: >> + nesting -= 1 >> + i = match.end() >> + continue >> + >> + # Found the end of the macro >> + >> + new_args.append(s[arg_start:match.start()]) >> + >> + # $(1) is replaced by the first argument to the function, etc., >> + # provided at least that many arguments were passed >> + >> + try: >> + # Does the macro look like an integer, with a corresponding >> + # argument? If so, expand it to the value of the argument. >> + res += args[int(new_args[0])] >> + except (ValueError, IndexError): >> + # Regular variables are just functions without arguments, >> + # and also go through the function value path >> + res += self._fn_val(new_args) >> + >> + return (res + s[match.end():], len(res)) >> + >> + elif match.group() == ",": >> + i = match.end() >> + if nesting: >> + continue >> + >> + # Found the end of a macro argument >> + new_args.append(s[arg_start:match.start()]) >> + arg_start = i >> + >> + else: # match.group() == "$(" >> + # A nested macro call within the macro >> + s, i = self._expand_macro(s, match.start(), args) >> + >> + def _fn_val(self, args): >> + # Returns the result of calling the function args[0] with the arguments >> + # args[1..len(args)-1]. Plain variables are treated as functions >> + # without arguments. >> + >> + fn = args[0] >> + >> + if fn in self.variables: >> + var = self.variables[fn] >> + >> + if len(args) == 1: >> + # Plain variable >> + if var._n_expansions: >> + self._parse_error("Preprocessor variable {} recursively " >> + "references itself".format(var.name)) >> + elif var._n_expansions > 100: >> + # Allow functions to call themselves, but guess that functions >> + # that are overly recursive are stuck >> + self._parse_error("Preprocessor function {} seems stuck " >> + "in infinite recursion".format(var.name)) >> + >> + var._n_expansions += 1 >> + res = self._expand_whole(self.variables[fn].value, args) >> + var._n_expansions -= 1 >> + return res >> + >> + if fn in self._functions: >> + # Built-in or user-defined function >> + >> + py_fn, min_arg, max_arg = self._functions[fn] >> + >> + if len(args) - 1 < min_arg or \ >> + (max_arg is not None and len(args) - 1 > max_arg): >> + >> + if min_arg == max_arg: >> + expected_args = min_arg >> + elif max_arg is None: >> + expected_args = "{} or more".format(min_arg) >> + else: >> + expected_args = "{}-{}".format(min_arg, max_arg) >> + >> + raise KconfigError("{}:{}: bad number of arguments in call " >> + "to {}, expected {}, got {}" >> + .format(self.filename, self.linenr, fn, >> + expected_args, len(args) - 1)) >> + >> + return py_fn(self, *args) >> + >> + # Environment variables are tried last >> + if fn in os.environ: >> + self.env_vars.add(fn) >> + return os.environ[fn] >> + >> + return "" >> + >> + # >> + # Parsing >> + # >> + >> + def _make_and(self, e1, e2): >> + # Constructs an AND (&&) expression. Performs trivial simplification. >> + >> + if e1 is self.y: >> + return e2 >> + >> + if e2 is self.y: >> + return e1 >> + >> + if e1 is self.n or e2 is self.n: >> + return self.n >> + >> + return (AND, e1, e2) >> + >> + def _make_or(self, e1, e2): >> + # Constructs an OR (||) expression. Performs trivial simplification. >> + >> + if e1 is self.n: >> + return e2 >> + >> + if e2 is self.n: >> + return e1 >> + >> + if e1 is self.y or e2 is self.y: >> + return self.y >> + >> + return (OR, e1, e2) >> + >> + def _parse_block(self, end_token, parent, prev): >> + # Parses a block, which is the contents of either a file or an if, >> + # menu, or choice statement. >> + # >> + # end_token: >> + # The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. >> + # None for files. >> + # >> + # parent: >> + # The parent menu node, corresponding to a menu, Choice, or 'if'. >> + # 'if's are flattened after parsing. >> + # >> + # prev: >> + # The previous menu node. New nodes will be added after this one (by >> + # modifying 'next' pointers). >> + # >> + # 'prev' is reused to parse a list of child menu nodes (for a menu or >> + # Choice): After parsing the children, the 'next' pointer is assigned >> + # to the 'list' pointer to "tilt up" the children above the node. >> + # >> + # Returns the final menu node in the block (or 'prev' if the block is >> + # empty). This allows chaining. >> + >> + while self._next_line(): >> + t0 = self._tokens[0] >> + >> + if t0 is _T_CONFIG or t0 is _T_MENUCONFIG: >> + # The tokenizer allocates Symbol objects for us >> + sym = self._tokens[1] >> + >> + if sym.__class__ is not Symbol or sym.is_constant: >> + self._parse_error("missing or bad symbol name") >> + >> + if self._tokens[2] is not None: >> + self._trailing_tokens_error() >> + >> + self.defined_syms.append(sym) >> + >> + node = MenuNode() >> + node.kconfig = self >> + node.item = sym >> + node.is_menuconfig = (t0 is _T_MENUCONFIG) >> + node.prompt = node.help = node.list = None >> + node.parent = parent >> + node.filename = self.filename >> + node.linenr = self.linenr >> + node.include_path = self._include_path >> + >> + sym.nodes.append(node) >> + >> + self._parse_props(node) >> + >> + if node.is_menuconfig and not node.prompt: >> + self._warn("the menuconfig symbol {} has no prompt" >> + .format(sym.name_and_loc)) >> + >> + # Equivalent to >> + # >> + # prev.next = node >> + # prev = node >> + # >> + # due to tricky Python semantics. The order matters. >> + prev.next = prev = node >> + >> + elif t0 is None: >> + # Blank line >> + continue >> + >> + elif t0 in _SOURCE_TOKENS: >> + pattern = self._expect_str_and_eol() >> + >> + if t0 in _REL_SOURCE_TOKENS: >> + # Relative source >> + pattern = join(dirname(self.filename), pattern) >> + >> + # - glob() doesn't support globbing relative to a directory, so >> + # we need to prepend $srctree to 'pattern'. Use join() >> + # instead of '+' so that an absolute path in 'pattern' is >> + # preserved. >> + # >> + # - Sort the glob results to ensure a consistent ordering of >> + # Kconfig symbols, which indirectly ensures a consistent >> + # ordering in e.g. .config files >> + filenames = sorted(iglob(join(self._srctree_prefix, pattern))) >> + >> + if not filenames and t0 in _OBL_SOURCE_TOKENS: >> + raise KconfigError( >> + "{}:{}: '{}' not found (in '{}'). Check that " >> + "environment variables are set correctly (e.g. " >> + "$srctree, which is {}). Also note that unset " >> + "environment variables expand to the empty string." >> + .format(self.filename, self.linenr, pattern, >> + self._line.strip(), >> + "set to '{}'".format(self.srctree) >> + if self.srctree else "unset or blank")) >> + >> + for filename in filenames: >> + self._enter_file(filename) >> + prev = self._parse_block(None, parent, prev) >> + self._leave_file() >> + >> + elif t0 is end_token: >> + # Reached the end of the block. Terminate the final node and >> + # return it. >> + >> + if self._tokens[1] is not None: >> + self._trailing_tokens_error() >> + >> + prev.next = None >> + return prev >> + >> + elif t0 is _T_IF: >> + node = MenuNode() >> + node.item = node.prompt = None >> + node.parent = parent >> + node.dep = self._expect_expr_and_eol() >> + >> + self._parse_block(_T_ENDIF, node, node) >> + node.list = node.next >> + >> + prev.next = prev = node >> + >> + elif t0 is _T_MENU: >> + node = MenuNode() >> + node.kconfig = self >> + node.item = t0 # _T_MENU == MENU >> + node.is_menuconfig = True >> + node.prompt = (self._expect_str_and_eol(), self.y) >> + node.visibility = self.y >> + node.parent = parent >> + node.filename = self.filename >> + node.linenr = self.linenr >> + node.include_path = self._include_path >> + >> + self.menus.append(node) >> + >> + self._parse_props(node) >> + self._parse_block(_T_ENDMENU, node, node) >> + node.list = node.next >> + >> + prev.next = prev = node >> + >> + elif t0 is _T_COMMENT: >> + node = MenuNode() >> + node.kconfig = self >> + node.item = t0 # _T_COMMENT == COMMENT >> + node.is_menuconfig = False >> + node.prompt = (self._expect_str_and_eol(), self.y) >> + node.list = None >> + node.parent = parent >> + node.filename = self.filename >> + node.linenr = self.linenr >> + node.include_path = self._include_path >> + >> + self.comments.append(node) >> + >> + self._parse_props(node) >> + >> + prev.next = prev = node >> + >> + elif t0 is _T_CHOICE: >> + if self._tokens[1] is None: >> + choice = Choice() >> + choice.direct_dep = self.n >> + else: >> + # Named choice >> + name = self._expect_str_and_eol() >> + choice = self.named_choices.get(name) >> + if not choice: >> + choice = Choice() >> + choice.name = name >> + choice.direct_dep = self.n >> + self.named_choices[name] = choice >> + >> + self.choices.append(choice) >> + >> + node = MenuNode() >> + node.kconfig = choice.kconfig = self >> + node.item = choice >> + node.is_menuconfig = True >> + node.prompt = node.help = None >> + node.parent = parent >> + node.filename = self.filename >> + node.linenr = self.linenr >> + node.include_path = self._include_path >> + >> + choice.nodes.append(node) >> + >> + self._parse_props(node) >> + self._parse_block(_T_ENDCHOICE, node, node) >> + node.list = node.next >> + >> + prev.next = prev = node >> + >> + elif t0 is _T_MAINMENU: >> + self.top_node.prompt = (self._expect_str_and_eol(), self.y) >> + >> + else: >> + # A valid endchoice/endif/endmenu is caught by the 'end_token' >> + # check above >> + self._parse_error( >> + "no corresponding 'choice'" if t0 is _T_ENDCHOICE else >> + "no corresponding 'if'" if t0 is _T_ENDIF else >> + "no corresponding 'menu'" if t0 is _T_ENDMENU else >> + "unrecognized construct") >> + >> + # End of file reached. Return the last node. >> + >> + if end_token: >> + raise KconfigError( >> + "error: expected '{}' at end of '{}'" >> + .format("endchoice" if end_token is _T_ENDCHOICE else >> + "endif" if end_token is _T_ENDIF else >> + "endmenu", >> + self.filename)) >> + >> + return prev >> + >> + def _parse_cond(self): >> + # Parses an optional 'if <expr>' construct and returns the parsed >> + # <expr>, or self.y if the next token is not _T_IF >> + >> + expr = self._parse_expr(True) if self._check_token(_T_IF) else self.y >> + >> + if self._tokens[self._tokens_i] is not None: >> + self._trailing_tokens_error() >> + >> + return expr >> + >> + def _parse_props(self, node): >> + # Parses and adds properties to the MenuNode 'node' (type, 'prompt', >> + # 'default's, etc.) Properties are later copied up to symbols and >> + # choices in a separate pass after parsing, in e.g. >> + # _add_props_to_sym(). >> + # >> + # An older version of this code added properties directly to symbols >> + # and choices instead of to their menu nodes (and handled dependency >> + # propagation simultaneously), but that loses information on where a >> + # property is added when a symbol or choice is defined in multiple >> + # locations. Some Kconfig configuration systems rely heavily on such >> + # symbols, and better docs can be generated by keeping track of where >> + # properties are added. >> + # >> + # node: >> + # The menu node we're parsing properties on >> + >> + # Dependencies from 'depends on'. Will get propagated to the properties >> + # below. >> + node.dep = self.y >> + >> + while self._next_line(): >> + t0 = self._tokens[0] >> + >> + if t0 in _TYPE_TOKENS: >> + # Relies on '_T_BOOL is BOOL', etc., to save a conversion >> + self._set_type(node.item, t0) >> + if self._tokens[1] is not None: >> + self._parse_prompt(node) >> + >> + elif t0 is _T_DEPENDS: >> + if not self._check_token(_T_ON): >> + self._parse_error("expected 'on' after 'depends'") >> + >> + node.dep = self._make_and(node.dep, >> + self._expect_expr_and_eol()) >> + >> + elif t0 is _T_HELP: >> + self._parse_help(node) >> + >> + elif t0 is _T_SELECT: >> + if node.item.__class__ is not Symbol: >> + self._parse_error("only symbols can select") >> + >> + node.selects.append((self._expect_nonconst_sym(), >> + self._parse_cond())) >> + >> + elif t0 is None: >> + # Blank line >> + continue >> + >> + elif t0 is _T_DEFAULT: >> + node.defaults.append((self._parse_expr(False), >> + self._parse_cond())) >> + >> + elif t0 in _DEF_TOKEN_TO_TYPE: >> + self._set_type(node.item, _DEF_TOKEN_TO_TYPE[t0]) >> + node.defaults.append((self._parse_expr(False), >> + self._parse_cond())) >> + >> + elif t0 is _T_PROMPT: >> + self._parse_prompt(node) >> + >> + elif t0 is _T_RANGE: >> + node.ranges.append((self._expect_sym(), self._expect_sym(), >> + self._parse_cond())) >> + >> + elif t0 is _T_IMPLY: >> + if node.item.__class__ is not Symbol: >> + self._parse_error("only symbols can imply") >> + >> + node.implies.append((self._expect_nonconst_sym(), >> + self._parse_cond())) >> + >> + elif t0 is _T_VISIBLE: >> + if not self._check_token(_T_IF): >> + self._parse_error("expected 'if' after 'visible'") >> + >> + node.visibility = self._make_and(node.visibility, >> + self._expect_expr_and_eol()) >> + >> + elif t0 is _T_OPTION: >> + if self._check_token(_T_ENV): >> + if not self._check_token(_T_EQUAL): >> + self._parse_error("expected '=' after 'env'") >> + >> + env_var = self._expect_str_and_eol() >> + node.item.env_var = env_var >> + >> + if env_var in os.environ: >> + node.defaults.append( >> + (self._lookup_const_sym(os.environ[env_var]), >> + self.y)) >> + else: >> + self._warn("{1} has 'option env=\"{0}\"', " >> + "but the environment variable {0} is not " >> + "set".format(node.item.name, env_var), >> + self.filename, self.linenr) >> + >> + if env_var != node.item.name: >> + self._warn("Kconfiglib expands environment variables " >> + "in strings directly, meaning you do not " >> + "need 'option env=...' \"bounce\" symbols. " >> + "For compatibility with the C tools, " >> + "rename {} to {} (so that the symbol name " >> + "matches the environment variable name)." >> + .format(node.item.name, env_var), >> + self.filename, self.linenr) >> + >> + elif self._check_token(_T_DEFCONFIG_LIST): >> + if not self.defconfig_list: >> + self.defconfig_list = node.item >> + else: >> + self._warn("'option defconfig_list' set on multiple " >> + "symbols ({0} and {1}). Only {0} will be " >> + "used.".format(self.defconfig_list.name, >> + node.item.name), >> + self.filename, self.linenr) >> + >> + elif self._check_token(_T_MODULES): >> + # To reduce warning spam, only warn if 'option modules' is >> + # set on some symbol that isn't MODULES, which should be >> + # safe. I haven't run into any projects that make use >> + # modules besides the kernel yet, and there it's likely to >> + # keep being called "MODULES". >> + if node.item is not self.modules: >> + self._warn("the 'modules' option is not supported. " >> + "Let me know if this is a problem for you, " >> + "as it wouldn't be that hard to implement. " >> + "Note that modules are supported -- " >> + "Kconfiglib just assumes the symbol name " >> + "MODULES, like older versions of the C " >> + "implementation did when 'option modules' " >> + "wasn't used.", >> + self.filename, self.linenr) >> + >> + elif self._check_token(_T_ALLNOCONFIG_Y): >> + if node.item.__class__ is not Symbol: >> + self._parse_error("the 'allnoconfig_y' option is only " >> + "valid for symbols") >> + >> + node.item.is_allnoconfig_y = True >> + >> + else: >> + self._parse_error("unrecognized option") >> + >> + elif t0 is _T_OPTIONAL: >> + if node.item.__class__ is not Choice: >> + self._parse_error('"optional" is only valid for choices') >> + >> + node.item.is_optional = True >> + >> + else: >> + # Reuse the tokens for the non-property line later >> + self._reuse_tokens = True >> + return >> + >> + def _set_type(self, sc, new_type): >> + # Sets the type of 'sc' (symbol or choice) to 'new_type' >> + >> + # UNKNOWN is falsy >> + if sc.orig_type and sc.orig_type is not new_type: >> + self._warn("{} defined with multiple types, {} will be used" >> + .format(sc.name_and_loc, TYPE_TO_STR[new_type])) >> + >> + sc.orig_type = new_type >> + >> + def _parse_prompt(self, node): >> + # 'prompt' properties override each other within a single definition of >> + # a symbol, but additional prompts can be added by defining the symbol >> + # multiple times >> + >> + if node.prompt: >> + self._warn(node.item.name_and_loc + >> + " defined with multiple prompts in single location") >> + >> + prompt = self._tokens[1] >> + self._tokens_i = 2 >> + >> + if prompt.__class__ is not str: >> + self._parse_error("expected prompt string") >> + >> + if prompt != prompt.strip(): >> + self._warn(node.item.name_and_loc + >> + " has leading or trailing whitespace in its prompt") >> + >> + # This avoid issues for e.g. reStructuredText documentation, where >> + # '*prompt *' is invalid >> + prompt = prompt.strip() >> + >> + node.prompt = (prompt, self._parse_cond()) >> + >> + def _parse_help(self, node): >> + if node.help is not None: >> + self._warn(node.item.name_and_loc + " defined with more than " >> + "one help text -- only the last one will be used") >> + >> + # Micro-optimization. This code is pretty hot. >> + readline = self._readline >> + >> + # Find first non-blank (not all-space) line and get its >> + # indentation >> + >> + while 1: >> + line = readline() >> + self.linenr += 1 >> + if not line: >> + self._empty_help(node, line) >> + return >> + if not line.isspace(): >> + break >> + >> + len_ = len # Micro-optimization >> + >> + # Use a separate 'expline' variable here and below to avoid stomping on >> + # any tabs people might've put deliberately into the first line after >> + # the help text >> + expline = line.expandtabs() >> + indent = len_(expline) - len_(expline.lstrip()) >> + if not indent: >> + self._empty_help(node, line) >> + return >> + >> + # The help text goes on till the first non-blank line with less indent >> + # than the first line >> + >> + # Add the first line >> + lines = [expline[indent:]] >> + add_line = lines.append # Micro-optimization >> + >> + while 1: >> + line = readline() >> + if line.isspace(): >> + # No need to preserve the exact whitespace in these >> + add_line("\n") >> + elif not line: >> + # End of file >> + break >> + else: >> + expline = line.expandtabs() >> + if len_(expline) - len_(expline.lstrip()) < indent: >> + break >> + add_line(expline[indent:]) >> + >> + self.linenr += len_(lines) >> + node.help = "".join(lines).rstrip() >> + if line: >> + self._line_after_help(line) >> + >> + def _empty_help(self, node, line): >> + self._warn(node.item.name_and_loc + >> + " has 'help' but empty help text") >> + node.help = "" >> + if line: >> + self._line_after_help(line) >> + >> + def _parse_expr(self, transform_m): >> + # Parses an expression from the tokens in Kconfig._tokens using a >> + # simple top-down approach. See the module docstring for the expression >> + # format. >> + # >> + # transform_m: >> + # True if m should be rewritten to m && MODULES. See the >> + # Kconfig.eval_string() documentation. >> + >> + # Grammar: >> + # >> + # expr: and_expr ['||' expr] >> + # and_expr: factor ['&&' and_expr] >> + # factor: <symbol> ['='/'!='/'<'/... <symbol>] >> + # '!' factor >> + # '(' expr ')' >> + # >> + # It helps to think of the 'expr: and_expr' case as a single-operand OR >> + # (no ||), and of the 'and_expr: factor' case as a single-operand AND >> + # (no &&). Parsing code is always a bit tricky. >> + >> + # Mind dump: parse_factor() and two nested loops for OR and AND would >> + # work as well. The straightforward implementation there gives a >> + # (op, (op, (op, A, B), C), D) parse for A op B op C op D. Representing >> + # expressions as (op, [list of operands]) instead goes nicely with that >> + # version, but is wasteful for short expressions and complicates >> + # expression evaluation and other code that works on expressions (more >> + # complicated code likely offsets any performance gain from less >> + # recursion too). If we also try to optimize the list representation by >> + # merging lists when possible (e.g. when ANDing two AND expressions), >> + # we end up allocating a ton of lists instead of reusing expressions, >> + # which is bad. >> + >> + and_expr = self._parse_and_expr(transform_m) >> + >> + # Return 'and_expr' directly if we have a "single-operand" OR. >> + # Otherwise, parse the expression on the right and make an OR node. >> + # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). >> + return and_expr if not self._check_token(_T_OR) else \ >> + (OR, and_expr, self._parse_expr(transform_m)) >> + >> + def _parse_and_expr(self, transform_m): >> + factor = self._parse_factor(transform_m) >> + >> + # Return 'factor' directly if we have a "single-operand" AND. >> + # Otherwise, parse the right operand and make an AND node. This turns >> + # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). >> + return factor if not self._check_token(_T_AND) else \ >> + (AND, factor, self._parse_and_expr(transform_m)) >> + >> + def _parse_factor(self, transform_m): >> + token = self._tokens[self._tokens_i] >> + self._tokens_i += 1 >> + >> + if token.__class__ is Symbol: >> + # Plain symbol or relation >> + >> + if self._tokens[self._tokens_i] not in _RELATIONS: >> + # Plain symbol >> + >> + # For conditional expressions ('depends on <expr>', >> + # '... if <expr>', etc.), m is rewritten to m && MODULES. >> + if transform_m and token is self.m: >> + return (AND, self.m, self.modules) >> + >> + return token >> + >> + # Relation >> + # >> + # _T_EQUAL, _T_UNEQUAL, etc., deliberately have the same values as >> + # EQUAL, UNEQUAL, etc., so we can just use the token directly >> + self._tokens_i += 1 >> + return (self._tokens[self._tokens_i - 1], token, >> + self._expect_sym()) >> + >> + if token is _T_NOT: >> + # token == _T_NOT == NOT >> + return (token, self._parse_factor(transform_m)) >> + >> + if token is _T_OPEN_PAREN: >> + expr_parse = self._parse_expr(transform_m) >> + if self._check_token(_T_CLOSE_PAREN): >> + return expr_parse >> + >> + self._parse_error("malformed expression") >> + >> + # >> + # Caching and invalidation >> + # >> + >> + def _build_dep(self): >> + # Populates the Symbol/Choice._dependents sets, which contain all other >> + # items (symbols and choices) that immediately depend on the item in >> + # the sense that changing the value of the item might affect the value >> + # of the dependent items. This is used for caching/invalidation. >> + # >> + # The calculated sets might be larger than necessary as we don't do any >> + # complex analysis of the expressions. >> + >> + depend_on = _depend_on # Micro-optimization >> + >> + # Only calculate _dependents for defined symbols. Constant and >> + # undefined symbols could theoretically be selected/implied, but it >> + # wouldn't change their value, so it's not a true dependency. >> + for sym in self.unique_defined_syms: >> + # Symbols depend on the following: >> + >> + # The prompt conditions >> + for node in sym.nodes: >> + if node.prompt: >> + depend_on(sym, node.prompt[1]) >> + >> + # The default values and their conditions >> + for value, cond in sym.defaults: >> + depend_on(sym, value) >> + depend_on(sym, cond) >> + >> + # The reverse and weak reverse dependencies >> + depend_on(sym, sym.rev_dep) >> + depend_on(sym, sym.weak_rev_dep) >> + >> + # The ranges along with their conditions >> + for low, high, cond in sym.ranges: >> + depend_on(sym, low) >> + depend_on(sym, high) >> + depend_on(sym, cond) >> + >> + # The direct dependencies. This is usually redundant, as the direct >> + # dependencies get propagated to properties, but it's needed to get >> + # invalidation solid for 'imply', which only checks the direct >> + # dependencies (even if there are no properties to propagate it >> + # to). >> + depend_on(sym, sym.direct_dep) >> + >> + # In addition to the above, choice symbols depend on the choice >> + # they're in, but that's handled automatically since the Choice is >> + # propagated to the conditions of the properties before >> + # _build_dep() runs. >> + >> + for choice in self.unique_choices: >> + # Choices depend on the following: >> + >> + # The prompt conditions >> + for node in choice.nodes: >> + if node.prompt: >> + depend_on(choice, node.prompt[1]) >> + >> + # The default symbol conditions >> + for _, cond in choice.defaults: >> + depend_on(choice, cond) >> + >> + def _add_choice_deps(self): >> + # Choices also depend on the choice symbols themselves, because the >> + # y-mode selection of the choice might change if a choice symbol's >> + # visibility changes. >> + # >> + # We add these dependencies separately after dependency loop detection. >> + # The invalidation algorithm can handle the resulting >> + # <choice symbol> <-> <choice> dependency loops, but they make loop >> + # detection awkward. >> + >> + for choice in self.unique_choices: >> + for sym in choice.syms: >> + sym._dependents.add(choice) >> + >> + def _invalidate_all(self): >> + # Undefined symbols never change value and don't need to be >> + # invalidated, so we can just iterate over defined symbols. >> + # Invalidating constant symbols would break things horribly. >> + for sym in self.unique_defined_syms: >> + sym._invalidate() >> + >> + for choice in self.unique_choices: >> + choice._invalidate() >> + >> + # >> + # Post-parsing menu tree processing, including dependency propagation and >> + # implicit submenu creation >> + # >> + >> + def _finalize_node(self, node, visible_if): >> + # Finalizes a menu node and its children: >> + # >> + # - Copies properties from menu nodes up to their contained >> + # symbols/choices >> + # >> + # - Propagates dependencies from parent to child nodes >> + # >> + # - Creates implicit menus (see kconfig-language.txt) >> + # >> + # - Removes 'if' nodes >> + # >> + # - Sets 'choice' types and registers choice symbols >> + # >> + # menu_finalize() in the C implementation is similar. >> + # >> + # node: >> + # The menu node to finalize. This node and its children will have >> + # been finalized when the function returns, and any implicit menus >> + # will have been created. >> + # >> + # visible_if: >> + # Dependencies from 'visible if' on parent menus. These are added to >> + # the prompts of symbols and choices. >> + >> + if node.item.__class__ is Symbol: >> + # Copy defaults, ranges, selects, and implies to the Symbol >> + self._add_props_to_sym(node) >> + >> + # Find any items that should go in an implicit menu rooted at the >> + # symbol >> + cur = node >> + while cur.next and _auto_menu_dep(node, cur.next): >> + # This makes implicit submenu creation work recursively, with >> + # implicit menus inside implicit menus >> + self._finalize_node(cur.next, visible_if) >> + cur = cur.next >> + cur.parent = node >> + >> + if cur is not node: >> + # Found symbols that should go in an implicit submenu. Tilt >> + # them up above us. >> + node.list = node.next >> + node.next = cur.next >> + cur.next = None >> + >> + elif node.list: >> + # The menu node is a choice, menu, or if. Finalize each child node. >> + >> + if node.item is MENU: >> + visible_if = self._make_and(visible_if, node.visibility) >> + >> + # Propagate the menu node's dependencies to each child menu node. >> + # >> + # This needs to go before the recursive _finalize_node() call so >> + # that implicit submenu creation can look ahead at dependencies. >> + self._propagate_deps(node, visible_if) >> + >> + # Finalize the children >> + cur = node.list >> + while cur: >> + self._finalize_node(cur, visible_if) >> + cur = cur.next >> + >> + if node.list: >> + # node's children have been individually finalized. Do final steps >> + # to finalize this "level" in the menu tree. >> + _flatten(node.list) >> + _remove_ifs(node) >> + >> + # Empty choices (node.list None) are possible, so this needs to go >> + # outside >> + if node.item.__class__ is Choice: >> + # Add the node's non-node-specific properties to the choice, like >> + # _add_props_to_sym() does >> + choice = node.item >> + choice.direct_dep = self._make_or(choice.direct_dep, node.dep) >> + choice.defaults += node.defaults >> + >> + _finalize_choice(node) >> + >> + def _propagate_deps(self, node, visible_if): >> + # Propagates 'node's dependencies to its child menu nodes >> + >> + # If the parent node holds a Choice, we use the Choice itself as the >> + # parent dependency. This makes sense as the value (mode) of the choice >> + # limits the visibility of the contained choice symbols. The C >> + # implementation works the same way. >> + # >> + # Due to the similar interface, Choice works as a drop-in replacement >> + # for Symbol here. >> + basedep = node.item if node.item.__class__ is Choice else node.dep >> + >> + cur = node.list >> + while cur: >> + dep = cur.dep = self._make_and(cur.dep, basedep) >> + >> + if cur.item.__class__ in _SYMBOL_CHOICE: >> + # Propagate 'visible if' and dependencies to the prompt >> + if cur.prompt: >> + cur.prompt = (cur.prompt[0], >> + self._make_and( >> + cur.prompt[1], >> + self._make_and(visible_if, dep))) >> + >> + # Propagate dependencies to defaults >> + if cur.defaults: >> + cur.defaults = [(default, self._make_and(cond, dep)) >> + for default, cond in cur.defaults] >> + >> + # Propagate dependencies to ranges >> + if cur.ranges: >> + cur.ranges = [(low, high, self._make_and(cond, dep)) >> + for low, high, cond in cur.ranges] >> + >> + # Propagate dependencies to selects >> + if cur.selects: >> + cur.selects = [(target, self._make_and(cond, dep)) >> + for target, cond in cur.selects] >> + >> + # Propagate dependencies to implies >> + if cur.implies: >> + cur.implies = [(target, self._make_and(cond, dep)) >> + for target, cond in cur.implies] >> + >> + elif cur.prompt: # Not a symbol/choice >> + # Propagate dependencies to the prompt. 'visible if' is only >> + # propagated to symbols/choices. >> + cur.prompt = (cur.prompt[0], >> + self._make_and(cur.prompt[1], dep)) >> + >> + cur = cur.next >> + >> + def _add_props_to_sym(self, node): >> + # Copies properties from the menu node 'node' up to its contained >> + # symbol, and adds (weak) reverse dependencies to selected/implied >> + # symbols. >> + # >> + # This can't be rolled into _propagate_deps(), because that function >> + # traverses the menu tree roughly breadth-first, meaning properties on >> + # symbols defined in multiple locations could end up in the wrong >> + # order. >> + >> + sym = node.item >> + >> + # See the Symbol class docstring >> + sym.direct_dep = self._make_or(sym.direct_dep, node.dep) >> + >> + sym.defaults += node.defaults >> + sym.ranges += node.ranges >> + sym.selects += node.selects >> + sym.implies += node.implies >> + >> + # Modify the reverse dependencies of the selected symbol >> + for target, cond in node.selects: >> + target.rev_dep = self._make_or( >> + target.rev_dep, >> + self._make_and(sym, cond)) >> + >> + # Modify the weak reverse dependencies of the implied >> + # symbol >> + for target, cond in node.implies: >> + target.weak_rev_dep = self._make_or( >> + target.weak_rev_dep, >> + self._make_and(sym, cond)) >> + >> + # >> + # Misc. >> + # >> + >> + def _check_sym_sanity(self): >> + # Checks various symbol properties that are handiest to check after >> + # parsing. Only generates errors and warnings. >> + >> + def num_ok(sym, type_): >> + # Returns True if the (possibly constant) symbol 'sym' is valid as a value >> + # for a symbol of type type_ (INT or HEX) >> + >> + # 'not sym.nodes' implies a constant or undefined symbol, e.g. a plain >> + # "123" >> + if not sym.nodes: >> + return _is_base_n(sym.name, _TYPE_TO_BASE[type_]) >> + >> + return sym.orig_type is type_ >> + >> + for sym in self.unique_defined_syms: >> + if sym.orig_type in _BOOL_TRISTATE: >> + # A helper function could be factored out here, but keep it >> + # speedy/straightforward >> + >> + for target_sym, _ in sym.selects: >> + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: >> + self._warn("{} selects the {} symbol {}, which is not " >> + "bool or tristate" >> + .format(sym.name_and_loc, >> + TYPE_TO_STR[target_sym.orig_type], >> + target_sym.name_and_loc)) >> + >> + for target_sym, _ in sym.implies: >> + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: >> + self._warn("{} implies the {} symbol {}, which is not " >> + "bool or tristate" >> + .format(sym.name_and_loc, >> + TYPE_TO_STR[target_sym.orig_type], >> + target_sym.name_and_loc)) >> + >> + elif sym.orig_type: # STRING/INT/HEX >> + for default, _ in sym.defaults: >> + if default.__class__ is not Symbol: >> + raise KconfigError( >> + "the {} symbol {} has a malformed default {} -- " >> + "expected a single symbol" >> + .format(TYPE_TO_STR[sym.orig_type], >> + sym.name_and_loc, expr_str(default))) >> + >> + if sym.orig_type is STRING: >> + if not default.is_constant and not default.nodes and \ >> + not default.name.isupper(): >> + # 'default foo' on a string symbol could be either a symbol >> + # reference or someone leaving out the quotes. Guess that >> + # the quotes were left out if 'foo' isn't all-uppercase >> + # (and no symbol named 'foo' exists). >> + self._warn("style: quotes recommended around " >> + "default value for string symbol " >> + + sym.name_and_loc) >> + >> + elif not num_ok(default, sym.orig_type): # INT/HEX >> + self._warn("the {0} symbol {1} has a non-{0} default {2}" >> + .format(TYPE_TO_STR[sym.orig_type], >> + sym.name_and_loc, >> + default.name_and_loc)) >> + >> + if sym.selects or sym.implies: >> + self._warn("the {} symbol {} has selects or implies" >> + .format(TYPE_TO_STR[sym.orig_type], >> + sym.name_and_loc)) >> + >> + else: # UNKNOWN >> + self._warn("{} defined without a type" >> + .format(sym.name_and_loc)) >> + >> + >> + if sym.ranges: >> + if sym.orig_type not in _INT_HEX: >> + self._warn( >> + "the {} symbol {} has ranges, but is not int or hex" >> + .format(TYPE_TO_STR[sym.orig_type], >> + sym.name_and_loc)) >> + else: >> + for low, high, _ in sym.ranges: >> + if not num_ok(low, sym.orig_type) or \ >> + not num_ok(high, sym.orig_type): >> + >> + self._warn("the {0} symbol {1} has a non-{0} " >> + "range [{2}, {3}]" >> + .format(TYPE_TO_STR[sym.orig_type], >> + sym.name_and_loc, >> + low.name_and_loc, >> + high.name_and_loc)) >> + >> + def _check_choice_sanity(self): >> + # Checks various choice properties that are handiest to check after >> + # parsing. Only generates errors and warnings. >> + >> + def warn_select_imply(sym, expr, expr_type): >> + msg = "the choice symbol {} is {} by the following symbols, but " \ >> + "select/imply has no effect on choice symbols" \ >> + .format(sym.name_and_loc, expr_type) >> + >> + # si = select/imply >> + for si in split_expr(expr, OR): >> + msg += "\n - " + split_expr(si, AND)[0].name_and_loc >> + >> + self._warn(msg) >> + >> + for choice in self.unique_choices: >> + if choice.orig_type not in _BOOL_TRISTATE: >> + self._warn("{} defined with type {}" >> + .format(choice.name_and_loc, >> + TYPE_TO_STR[choice.orig_type])) >> + >> + for node in choice.nodes: >> + if node.prompt: >> + break >> + else: >> + self._warn(choice.name_and_loc + " defined without a prompt") >> + >> + for default, _ in choice.defaults: >> + if default.__class__ is not Symbol: >> + raise KconfigError( >> + "{} has a malformed default {}" >> + .format(choice.name_and_loc, expr_str(default))) >> + >> + if default.choice is not choice: >> + self._warn("the default selection {} of {} is not " >> + "contained in the choice" >> + .format(default.name_and_loc, >> + choice.name_and_loc)) >> + >> + for sym in choice.syms: >> + if sym.defaults: >> + self._warn("default on the choice symbol {} will have " >> + "no effect, as defaults do not affect choice " >> + "symbols".format(sym.name_and_loc)) >> + >> + if sym.rev_dep is not sym.kconfig.n: >> + warn_select_imply(sym, sym.rev_dep, "selected") >> + >> + if sym.weak_rev_dep is not sym.kconfig.n: >> + warn_select_imply(sym, sym.weak_rev_dep, "implied") >> + >> + for node in sym.nodes: >> + if node.parent.item is choice: >> + if not node.prompt: >> + self._warn("the choice symbol {} has no prompt" >> + .format(sym.name_and_loc)) >> + >> + elif node.prompt: >> + self._warn("the choice symbol {} is defined with a " >> + "prompt outside the choice" >> + .format(sym.name_and_loc)) >> + >> + def _parse_error(self, msg): >> + raise KconfigError("{}error: couldn't parse '{}': {}".format( >> + "" if self.filename is None else >> + "{}:{}: ".format(self.filename, self.linenr), >> + self._line.strip(), msg)) >> + >> + def _trailing_tokens_error(self): >> + self._parse_error("extra tokens at end of line") >> + >> + def _open(self, filename, mode): >> + # open() wrapper: >> + # >> + # - Enable universal newlines mode on Python 2 to ease >> + # interoperability between Linux and Windows. It's already the >> + # default on Python 3. >> + # >> + # The "U" flag would currently work for both Python 2 and 3, but it's >> + # deprecated on Python 3, so play it future-safe. >> + # >> + # io.open() defaults to universal newlines on Python 2 (and is an >> + # alias for open() on Python 3), but it returns 'unicode' strings and >> + # slows things down: >> + # >> + # Parsing x86 Kconfigs on Python 2 >> + # >> + # with open(..., "rU"): >> + # >> + # real 0m0.930s >> + # user 0m0.905s >> + # sys 0m0.025s >> + # >> + # with io.open(): >> + # >> + # real 0m1.069s >> + # user 0m1.040s >> + # sys 0m0.029s >> + # >> + # There's no appreciable performance difference between "r" and >> + # "rU" for parsing performance on Python 2. >> + # >> + # - For Python 3, force the encoding. Forcing the encoding on Python 2 >> + # turns strings into Unicode strings, which gets messy. Python 2 >> + # doesn't decode regular strings anyway. >> + return open(filename, "rU" if mode == "r" else mode) if _IS_PY2 else \ >> + open(filename, mode, encoding=self._encoding) >> + >> + def _check_undef_syms(self): >> + # Prints warnings for all references to undefined symbols within the >> + # Kconfig files >> + >> + def is_num(s): >> + # Returns True if the string 's' looks like a number. >> + # >> + # Internally, all operands in Kconfig are symbols, only undefined symbols >> + # (which numbers usually are) get their name as their value. >> + # >> + # Only hex numbers that start with 0x/0X are classified as numbers. >> + # Otherwise, symbols whose names happen to contain only the letters A-F >> + # would trigger false positives. >> + >> + try: >> + int(s) >> + except ValueError: >> + if not s.startswith(("0x", "0X")): >> + return False >> + >> + try: >> + int(s, 16) >> + except ValueError: >> + return False >> + >> + return True >> + >> + for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)(): >> + # - sym.nodes empty means the symbol is undefined (has no >> + # definition locations) >> + # >> + # - Due to Kconfig internals, numbers show up as undefined Kconfig >> + # symbols, but shouldn't be flagged >> + # >> + # - The MODULES symbol always exists >> + if not sym.nodes and not is_num(sym.name) and \ >> + sym.name != "MODULES": >> + >> + msg = "undefined symbol {}:".format(sym.name) >> + for node in self.node_iter(): >> + if sym in node.referenced: >> + msg += "\n\n- Referenced at {}:{}:\n\n{}" \ >> + .format(node.filename, node.linenr, node) >> + self._warn(msg) >> + >> + def _warn(self, msg, filename=None, linenr=None): >> + # For printing general warnings >> + >> + if not self.warn: >> + return >> + >> + msg = "warning: " + msg >> + if filename is not None: >> + msg = "{}:{}: {}".format(filename, linenr, msg) >> + >> + self.warnings.append(msg) >> + if self.warn_to_stderr: >> + sys.stderr.write(msg + "\n") >> + >> + >> +class Symbol(object): >> + """ >> + Represents a configuration symbol: >> + >> + (menu)config FOO >> + ... >> + >> + The following attributes are available. They should be viewed as read-only, >> + and some are implemented through @property magic (but are still efficient >> + to access due to internal caching). >> + >> + Note: Prompts, help texts, and locations are stored in the Symbol's >> + MenuNode(s) rather than in the Symbol itself. Check the MenuNode class and >> + the Symbol.nodes attribute. This organization matches the C tools. >> + >> + name: >> + The name of the symbol, e.g. "FOO" for 'config FOO'. >> + >> + type: >> + The type of the symbol. One of BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN. >> + UNKNOWN is for undefined symbols, (non-special) constant symbols, and >> + symbols defined without a type. >> + >> + When running without modules (MODULES having the value n), TRISTATE >> + symbols magically change type to BOOL. This also happens for symbols >> + within choices in "y" mode. This matches the C tools, and makes sense for >> + menuconfig-like functionality. >> + >> + orig_type: >> + The type as given in the Kconfig file, without any magic applied. Used >> + when printing the symbol. >> + >> + tri_value: >> + The tristate value of the symbol as an integer. One of 0, 1, 2, >> + representing n, m, y. Always 0 (n) for non-bool/tristate symbols. >> + >> + This is the symbol value that's used outside of relation expressions >> + (A, !A, A && B, A || B). >> + >> + str_value: >> + The value of the symbol as a string. Gives the value for string/int/hex >> + symbols. For bool/tristate symbols, gives "n", "m", or "y". >> + >> + This is the symbol value that's used in relational expressions >> + (A = B, A != B, etc.) >> + >> + Gotcha: For int/hex symbols, the exact format of the value is often >> + preserved (e.g. when writing a .config file), hence why you can't get it >> + directly as an int. Do int(int_sym.str_value) or >> + int(hex_sym.str_value, 16) to get the integer value. >> + >> + user_value: >> + The user value of the symbol. None if no user value has been assigned >> + (via Kconfig.load_config() or Symbol.set_value()). >> + >> + Holds 0, 1, or 2 for bool/tristate symbols, and a string for the other >> + symbol types. >> + >> + WARNING: Do not assign directly to this. It will break things. Use >> + Symbol.set_value(). >> + >> + assignable: >> + A tuple containing the tristate user values that can currently be >> + assigned to the symbol (that would be respected), ordered from lowest (0, >> + representing n) to highest (2, representing y). This corresponds to the >> + selections available in the menuconfig interface. The set of assignable >> + values is calculated from the symbol's visibility and selects/implies. >> + >> + Returns the empty set for non-bool/tristate symbols and for symbols with >> + visibility n. The other possible values are (0, 2), (0, 1, 2), (1, 2), >> + (1,), and (2,). A (1,) or (2,) result means the symbol is visible but >> + "locked" to m or y through a select, perhaps in combination with the >> + visibility. menuconfig represents this as -M- and -*-, respectively. >> + >> + For string/hex/int symbols, check if Symbol.visibility is non-0 (non-n) >> + instead to determine if the value can be changed. >> + >> + Some handy 'assignable' idioms: >> + >> + # Is 'sym' an assignable (visible) bool/tristate symbol? >> + if sym.assignable: >> + # What's the highest value it can be assigned? [-1] in Python >> + # gives the last element. >> + sym_high = sym.assignable[-1] >> + >> + # The lowest? >> + sym_low = sym.assignable[0] >> + >> + # Can the symbol be set to at least m? >> + if sym.assignable[-1] >= 1: >> + ... >> + >> + # Can the symbol be set to m? >> + if 1 in sym.assignable: >> + ... >> + >> + visibility: >> + The visibility of the symbol. One of 0, 1, 2, representing n, m, y. See >> + the module documentation for an overview of symbol values and visibility. >> + >> + config_string: >> + The .config assignment string that would get written out for the symbol >> + by Kconfig.write_config(). Returns the empty string if no .config >> + assignment would get written out. >> + >> + In general, visible symbols, symbols with (active) defaults, and selected >> + symbols get written out. This includes all non-n-valued bool/tristate >> + symbols, and all visible string/int/hex symbols. >> + >> + Symbols with the (no longer needed) 'option env=...' option generate no >> + configuration output, and neither does the special >> + 'option defconfig_list' symbol. >> + >> + Tip: This field is useful when generating custom configuration output, >> + even for non-.config-like formats. To write just the symbols that would >> + get written out to .config files, do this: >> + >> + if sym.config_string: >> + *Write symbol, e.g. by looking sym.str_value* >> + >> + This is a superset of the symbols written out by write_autoconf(). >> + That function skips all n-valued symbols. >> + >> + There usually won't be any great harm in just writing all symbols either, >> + though you might get some special symbols and possibly some "redundant" >> + n-valued symbol entries in there. >> + >> + name_and_loc: >> + Holds a string like >> + >> + "MY_SYMBOL (defined at foo/Kconfig:12, bar/Kconfig:14)" >> + >> + , giving the name of the symbol and its definition location(s). >> + >> + If the symbol is undefined, the location is given as "(undefined)". >> + >> + nodes: >> + A list of MenuNodes for this symbol. Will contain a single MenuNode for >> + most symbols. Undefined and constant symbols have an empty nodes list. >> + Symbols defined in multiple locations get one node for each location. >> + >> + choice: >> + Holds the parent Choice for choice symbols, and None for non-choice >> + symbols. Doubles as a flag for whether a symbol is a choice symbol. >> + >> + defaults: >> + List of (default, cond) tuples for the symbol's 'default' properties. For >> + example, 'default A && B if C || D' is represented as >> + ((AND, A, B), (OR, C, D)). If no condition was given, 'cond' is >> + self.kconfig.y. >> + >> + Note that 'depends on' and parent dependencies are propagated to >> + 'default' conditions. >> + >> + selects: >> + List of (symbol, cond) tuples for the symbol's 'select' properties. For >> + example, 'select A if B && C' is represented as (A, (AND, B, C)). If no >> + condition was given, 'cond' is self.kconfig.y. >> + >> + Note that 'depends on' and parent dependencies are propagated to 'select' >> + conditions. >> + >> + implies: >> + Like 'selects', for imply. >> + >> + ranges: >> + List of (low, high, cond) tuples for the symbol's 'range' properties. For >> + example, 'range 1 2 if A' is represented as (1, 2, A). If there is no >> + condition, 'cond' is self.kconfig.y. >> + >> + Note that 'depends on' and parent dependencies are propagated to 'range' >> + conditions. >> + >> + Gotcha: 1 and 2 above will be represented as (undefined) Symbols rather >> + than plain integers. Undefined symbols get their name as their string >> + value, so this works out. The C tools work the same way. >> + >> + orig_defaults: >> + orig_selects: >> + orig_implies: >> + orig_ranges: >> + See the corresponding attributes on the MenuNode class. >> + >> + rev_dep: >> + Reverse dependency expression from other symbols selecting this symbol. >> + Multiple selections get ORed together. A condition on a select is ANDed >> + with the selecting symbol. >> + >> + For example, if A has 'select FOO' and B has 'select FOO if C', then >> + FOO's rev_dep will be (OR, A, (AND, B, C)). >> + >> + weak_rev_dep: >> + Like rev_dep, for imply. >> + >> + direct_dep: >> + The direct ('depends on') dependencies for the symbol, or self.kconfig.y >> + if there are no direct dependencies. >> + >> + This attribute includes any dependencies from surrounding menus and ifs. >> + Those get propagated to the direct dependencies, and the resulting direct >> + dependencies in turn get propagated to the conditions of all properties. >> + >> + If the symbol is defined in multiple locations, the dependencies from the >> + different locations get ORed together. >> + >> + referenced: >> + A set() with all symbols and choices referenced in the properties and >> + property conditions of the symbol. >> + >> + Also includes dependencies from surrounding menus and ifs, because those >> + get propagated to the symbol (see the 'Intro to symbol values' section in >> + the module docstring). >> + >> + Choices appear in the dependencies of choice symbols. >> + >> + For the following definitions, only B and not C appears in A's >> + 'referenced'. To get transitive references, you'll have to recursively >> + expand 'references' until no new items appear. >> + >> + config A >> + bool >> + depends on B >> + >> + config B >> + bool >> + depends on C >> + >> + config C >> + bool >> + >> + See the Symbol.direct_dep attribute if you're only interested in the >> + direct dependencies of the symbol (its 'depends on'). You can extract the >> + symbols in it with the global expr_items() function. >> + >> + env_var: >> + If the Symbol has an 'option env="FOO"' option, this contains the name >> + ("FOO") of the environment variable. None for symbols without no >> + 'option env'. >> + >> + 'option env="FOO"' acts like a 'default' property whose value is the >> + value of $FOO. >> + >> + Symbols with 'option env' are never written out to .config files, even if >> + they are visible. env_var corresponds to a flag called SYMBOL_AUTO in the >> + C implementation. >> + >> + is_allnoconfig_y: >> + True if the symbol has 'option allnoconfig_y' set on it. This has no >> + effect internally (except when printing symbols), but can be checked by >> + scripts. >> + >> + is_constant: >> + True if the symbol is a constant (quoted) symbol. >> + >> + kconfig: >> + The Kconfig instance this symbol is from. >> + """ >> + __slots__ = ( >> + "_cached_assignable", >> + "_cached_str_val", >> + "_cached_tri_val", >> + "_cached_vis", >> + "_dependents", >> + "_old_val", >> + "_visited", >> + "_was_set", >> + "_write_to_conf", >> + "choice", >> + "defaults", >> + "direct_dep", >> + "env_var", >> + "implies", >> + "is_allnoconfig_y", >> + "is_constant", >> + "kconfig", >> + "name", >> + "nodes", >> + "orig_type", >> + "ranges", >> + "rev_dep", >> + "selects", >> + "user_value", >> + "weak_rev_dep", >> + ) >> + >> + # >> + # Public interface >> + # >> + >> + @property >> + def type(self): >> + """ >> + See the class documentation. >> + """ >> + if self.orig_type is TRISTATE and \ >> + (self.choice and self.choice.tri_value == 2 or >> + not self.kconfig.modules.tri_value): >> + >> + return BOOL >> + >> + return self.orig_type >> + >> + @property >> + def str_value(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_str_val is not None: >> + return self._cached_str_val >> + >> + if self.orig_type in _BOOL_TRISTATE: >> + # Also calculates the visibility, so invalidation safe >> + self._cached_str_val = TRI_TO_STR[self.tri_value] >> + return self._cached_str_val >> + >> + # As a quirk of Kconfig, undefined symbols get their name as their >> + # string value. This is why things like "FOO = bar" work for seeing if >> + # FOO has the value "bar". >> + if not self.orig_type: # UNKNOWN >> + self._cached_str_val = self.name >> + return self.name >> + >> + val = "" >> + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden >> + # function call (property magic) >> + vis = self.visibility >> + >> + self._write_to_conf = (vis != 0) >> + >> + if self.orig_type in _INT_HEX: >> + # The C implementation checks the user value against the range in a >> + # separate code path (post-processing after loading a .config). >> + # Checking all values here instead makes more sense for us. It >> + # requires that we check for a range first. >> + >> + base = _TYPE_TO_BASE[self.orig_type] >> + >> + # Check if a range is in effect >> + for low_expr, high_expr, cond in self.ranges: >> + if expr_value(cond): >> + has_active_range = True >> + >> + # The zeros are from the C implementation running strtoll() >> + # on empty strings >> + low = int(low_expr.str_value, base) if \ >> + _is_base_n(low_expr.str_value, base) else 0 >> + high = int(high_expr.str_value, base) if \ >> + _is_base_n(high_expr.str_value, base) else 0 >> + >> + break >> + else: >> + has_active_range = False >> + >> + # Defaults are used if the symbol is invisible, lacks a user value, >> + # or has an out-of-range user value >> + use_defaults = True >> + >> + if vis and self.user_value: >> + user_val = int(self.user_value, base) >> + if has_active_range and not low <= user_val <= high: >> + num2str = str if base == 10 else hex >> + self.kconfig._warn( >> + "user value {} on the {} symbol {} ignored due to " >> + "being outside the active range ([{}, {}]) -- falling " >> + "back on defaults" >> + .format(num2str(user_val), TYPE_TO_STR[self.orig_type], >> + self.name_and_loc, >> + num2str(low), num2str(high))) >> + else: >> + # If the user value is well-formed and satisfies range >> + # contraints, it is stored in exactly the same form as >> + # specified in the assignment (with or without "0x", etc.) >> + val = self.user_value >> + use_defaults = False >> + >> + if use_defaults: >> + # No user value or invalid user value. Look at defaults. >> + >> + # Used to implement the warning below >> + has_default = False >> + >> + for sym, cond in self.defaults: >> + if expr_value(cond): >> + has_default = self._write_to_conf = True >> + >> + val = sym.str_value >> + >> + if _is_base_n(val, base): >> + val_num = int(val, base) >> + else: >> + val_num = 0 # strtoll() on empty string >> + >> + break >> + else: >> + val_num = 0 # strtoll() on empty string >> + >> + # This clamping procedure runs even if there's no default >> + if has_active_range: >> + clamp = None >> + if val_num < low: >> + clamp = low >> + elif val_num > high: >> + clamp = high >> + >> + if clamp is not None: >> + # The value is rewritten to a standard form if it is >> + # clamped >> + val = str(clamp) \ >> + if self.orig_type is INT else \ >> + hex(clamp) >> + >> + if has_default: >> + num2str = str if base == 10 else hex >> + self.kconfig._warn( >> + "default value {} on {} clamped to {} due to " >> + "being outside the active range ([{}, {}])" >> + .format(val_num, self.name_and_loc, >> + num2str(clamp), num2str(low), >> + num2str(high))) >> + >> + elif self.orig_type is STRING: >> + if vis and self.user_value is not None: >> + # If the symbol is visible and has a user value, use that >> + val = self.user_value >> + else: >> + # Otherwise, look at defaults >> + for sym, cond in self.defaults: >> + if expr_value(cond): >> + val = sym.str_value >> + self._write_to_conf = True >> + break >> + >> + # env_var corresponds to SYMBOL_AUTO in the C implementation, and is >> + # also set on the defconfig_list symbol there. Test for the >> + # defconfig_list symbol explicitly instead here, to avoid a nonsensical >> + # env_var setting and the defconfig_list symbol being printed >> + # incorrectly. This code is pretty cold anyway. >> + if self.env_var is not None or self is self.kconfig.defconfig_list: >> + self._write_to_conf = False >> + >> + self._cached_str_val = val >> + return val >> + >> + @property >> + def tri_value(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_tri_val is not None: >> + return self._cached_tri_val >> + >> + if self.orig_type not in _BOOL_TRISTATE: >> + if self.orig_type: # != UNKNOWN >> + # Would take some work to give the location here >> + self.kconfig._warn( >> + "The {} symbol {} is being evaluated in a logical context " >> + "somewhere. It will always evaluate to n." >> + .format(TYPE_TO_STR[self.orig_type], self.name_and_loc)) >> + >> + self._cached_tri_val = 0 >> + return 0 >> + >> + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden >> + # function call (property magic) >> + vis = self.visibility >> + self._write_to_conf = (vis != 0) >> + >> + val = 0 >> + >> + if not self.choice: >> + # Non-choice symbol >> + >> + if vis and self.user_value is not None: >> + # If the symbol is visible and has a user value, use that >> + val = min(self.user_value, vis) >> + >> + else: >> + # Otherwise, look at defaults and weak reverse dependencies >> + # (implies) >> + >> + for default, cond in self.defaults: >> + dep_val = expr_value(cond) >> + if dep_val: >> + val = min(expr_value(default), dep_val) >> + if val: >> + self._write_to_conf = True >> + break >> + >> + # Weak reverse dependencies are only considered if our >> + # direct dependencies are met >> + dep_val = expr_value(self.weak_rev_dep) >> + if dep_val and expr_value(self.direct_dep): >> + val = max(dep_val, val) >> + self._write_to_conf = True >> + >> + # Reverse (select-related) dependencies take precedence >> + dep_val = expr_value(self.rev_dep) >> + if dep_val: >> + if expr_value(self.direct_dep) < dep_val: >> + self._warn_select_unsatisfied_deps() >> + >> + val = max(dep_val, val) >> + self._write_to_conf = True >> + >> + # m is promoted to y for (1) bool symbols and (2) symbols with a >> + # weak_rev_dep (from imply) of y >> + if val == 1 and \ >> + (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): >> + val = 2 >> + >> + elif vis == 2: >> + # Visible choice symbol in y-mode choice. The choice mode limits >> + # the visibility of choice symbols, so it's sufficient to just >> + # check the visibility of the choice symbols themselves. >> + val = 2 if self.choice.selection is self else 0 >> + >> + elif vis and self.user_value: >> + # Visible choice symbol in m-mode choice, with set non-0 user value >> + val = 1 >> + >> + self._cached_tri_val = val >> + return val >> + >> + @property >> + def assignable(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_assignable is None: >> + self._cached_assignable = self._assignable() >> + return self._cached_assignable >> + >> + @property >> + def visibility(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_vis is None: >> + self._cached_vis = _visibility(self) >> + return self._cached_vis >> + >> + @property >> + def config_string(self): >> + """ >> + See the class documentation. >> + """ >> + # _write_to_conf is determined when the value is calculated. This is a >> + # hidden function call due to property magic. >> + val = self.str_value >> + if not self._write_to_conf: >> + return "" >> + >> + if self.orig_type in _BOOL_TRISTATE: >> + return "{}{}={}\n" \ >> + .format(self.kconfig.config_prefix, self.name, val) \ >> + if val != "n" else \ >> + "# {}{} is not set\n" \ >> + .format(self.kconfig.config_prefix, self.name) >> + >> + if self.orig_type in _INT_HEX: >> + return "{}{}={}\n" \ >> + .format(self.kconfig.config_prefix, self.name, val) >> + >> + # sym.orig_type is STRING >> + return '{}{}="{}"\n' \ >> + .format(self.kconfig.config_prefix, self.name, escape(val)) >> + >> + @property >> + def name_and_loc(self): >> + """ >> + See the class documentation. >> + """ >> + return self.name + " " + _locs(self) >> + >> + def set_value(self, value): >> + """ >> + Sets the user value of the symbol. >> + >> + Equal in effect to assigning the value to the symbol within a .config >> + file. For bool and tristate symbols, use the 'assignable' attribute to >> + check which values can currently be assigned. Setting values outside >> + 'assignable' will cause Symbol.user_value to differ from >> + Symbol.str/tri_value (be truncated down or up). >> + >> + Setting a choice symbol to 2 (y) sets Choice.user_selection to the >> + choice symbol in addition to setting Symbol.user_value. >> + Choice.user_selection is considered when the choice is in y mode (the >> + "normal" mode). >> + >> + Other symbols that depend (possibly indirectly) on this symbol are >> + automatically recalculated to reflect the assigned value. >> + >> + value: >> + The user value to give to the symbol. For bool and tristate symbols, >> + n/m/y can be specified either as 0/1/2 (the usual format for tristate >> + values in Kconfiglib) or as one of the strings "n", "m", or "y". For >> + other symbol types, pass a string. >> + >> + Note that the value for an int/hex symbol is passed as a string, e.g. >> + "123" or "0x0123". The format of this string is preserved in the >> + output. >> + >> + Values that are invalid for the type (such as "foo" or 1 (m) for a >> + BOOL or "0x123" for an INT) are ignored and won't be stored in >> + Symbol.user_value. Kconfiglib will print a warning by default for >> + invalid assignments, and set_value() will return False. >> + >> + Returns True if the value is valid for the type of the symbol, and >> + False otherwise. This only looks at the form of the value. For BOOL and >> + TRISTATE symbols, check the Symbol.assignable attribute to see what >> + values are currently in range and would actually be reflected in the >> + value of the symbol. For other symbol types, check whether the >> + visibility is non-n. >> + """ >> + if self.orig_type in _BOOL_TRISTATE and value in STR_TO_TRI: >> + value = STR_TO_TRI[value] >> + >> + # If the new user value matches the old, nothing changes, and we can >> + # avoid invalidating cached values. >> + # >> + # This optimization is skipped for choice symbols: Setting a choice >> + # symbol's user value to y might change the state of the choice, so it >> + # wouldn't be safe (symbol user values always match the values set in a >> + # .config file or via set_value(), and are never implicitly updated). >> + if value == self.user_value and not self.choice: >> + self._was_set = True >> + return True >> + >> + # Check if the value is valid for our type >> + if not (self.orig_type is BOOL and value in (2, 0) or >> + self.orig_type is TRISTATE and value in TRI_TO_STR or >> + value.__class__ is str and >> + (self.orig_type is STRING or >> + self.orig_type is INT and _is_base_n(value, 10) or >> + self.orig_type is HEX and _is_base_n(value, 16) >> + and int(value, 16) >= 0)): >> + >> + # Display tristate values as n, m, y in the warning >> + self.kconfig._warn( >> + "the value {} is invalid for {}, which has type {} -- " >> + "assignment ignored" >> + .format(TRI_TO_STR[value] if value in TRI_TO_STR else >> + "'{}'".format(value), >> + self.name_and_loc, TYPE_TO_STR[self.orig_type])) >> + >> + return False >> + >> + self.user_value = value >> + self._was_set = True >> + >> + if self.choice and value == 2: >> + # Setting a choice symbol to y makes it the user selection of the >> + # choice. Like for symbol user values, the user selection is not >> + # guaranteed to match the actual selection of the choice, as >> + # dependencies come into play. >> + self.choice.user_selection = self >> + self.choice._was_set = True >> + self.choice._rec_invalidate() >> + else: >> + self._rec_invalidate_if_has_prompt() >> + >> + return True >> + >> + def unset_value(self): >> + """ >> + Removes any user value from the symbol, as if the symbol had never >> + gotten a user value via Kconfig.load_config() or Symbol.set_value(). >> + """ >> + if self.user_value is not None: >> + self.user_value = None >> + self._rec_invalidate_if_has_prompt() >> + >> + @property >> + def referenced(self): >> + """ >> + See the class documentation. >> + """ >> + return {item for node in self.nodes for item in node.referenced} >> + >> + @property >> + def orig_defaults(self): >> + """ >> + See the class documentation. >> + """ >> + return [d for node in self.nodes for d in node.orig_defaults] >> + >> + @property >> + def orig_selects(self): >> + """ >> + See the class documentation. >> + """ >> + return [s for node in self.nodes for s in node.orig_selects] >> + >> + @property >> + def orig_implies(self): >> + """ >> + See the class documentation. >> + """ >> + return [i for node in self.nodes for i in node.orig_implies] >> + >> + @property >> + def orig_ranges(self): >> + """ >> + See the class documentation. >> + """ >> + return [r for node in self.nodes for r in node.orig_ranges] >> + >> + def __repr__(self): >> + """ >> + Returns a string with information about the symbol (including its name, >> + value, visibility, and location(s)) when it is evaluated on e.g. the >> + interactive Python prompt. >> + """ >> + fields = ["symbol " + self.name, TYPE_TO_STR[self.type]] >> + add = fields.append >> + >> + for node in self.nodes: >> + if node.prompt: >> + add('"{}"'.format(node.prompt[0])) >> + >> + # Only add quotes for non-bool/tristate symbols >> + add("value " + (self.str_value if self.orig_type in _BOOL_TRISTATE >> + else '"{}"'.format(self.str_value))) >> + >> + if not self.is_constant: >> + # These aren't helpful to show for constant symbols >> + >> + if self.user_value is not None: >> + # Only add quotes for non-bool/tristate symbols >> + add("user value " + (TRI_TO_STR[self.user_value] >> + if self.orig_type in _BOOL_TRISTATE >> + else '"{}"'.format(self.user_value))) >> + >> + add("visibility " + TRI_TO_STR[self.visibility]) >> + >> + if self.choice: >> + add("choice symbol") >> + >> + if self.is_allnoconfig_y: >> + add("allnoconfig_y") >> + >> + if self is self.kconfig.defconfig_list: >> + add("is the defconfig_list symbol") >> + >> + if self.env_var is not None: >> + add("from environment variable " + self.env_var) >> + >> + if self is self.kconfig.modules: >> + add("is the modules symbol") >> + >> + add("direct deps " + TRI_TO_STR[expr_value(self.direct_dep)]) >> + >> + if self.nodes: >> + for node in self.nodes: >> + add("{}:{}".format(node.filename, node.linenr)) >> + else: >> + add("constant" if self.is_constant else "undefined") >> + >> + return "<{}>".format(", ".join(fields)) >> + >> + def __str__(self): >> + """ >> + Returns a string representation of the symbol when it is printed. >> + Matches the Kconfig format, with any parent dependencies propagated to >> + the 'depends on' condition. >> + >> + The string is constructed by joining the strings returned by >> + MenuNode.__str__() for each of the symbol's menu nodes, so symbols >> + defined in multiple locations will return a string with all >> + definitions. >> + >> + The returned string does not end in a newline. An empty string is >> + returned for undefined and constant symbols. >> + """ >> + return self.custom_str(standard_sc_expr_str) >> + >> + def custom_str(self, sc_expr_str_fn): >> + """ >> + Works like Symbol.__str__(), but allows a custom format to be used for >> + all symbol/choice references. See expr_str(). >> + """ >> + return "\n\n".join(node.custom_str(sc_expr_str_fn) >> + for node in self.nodes) >> + >> + # >> + # Private methods >> + # >> + >> + def __init__(self): >> + """ >> + Symbol constructor -- not intended to be called directly by Kconfiglib >> + clients. >> + """ >> + # These attributes are always set on the instance from outside and >> + # don't need defaults: >> + # kconfig >> + # direct_dep >> + # is_constant >> + # name >> + # rev_dep >> + # weak_rev_dep >> + >> + # - UNKNOWN == 0 >> + # - _visited is used during tree iteration and dep. loop detection >> + self.orig_type = self._visited = 0 >> + >> + self.nodes = [] >> + >> + self.defaults = [] >> + self.selects = [] >> + self.implies = [] >> + self.ranges = [] >> + >> + self.user_value = \ >> + self.choice = \ >> + self.env_var = \ >> + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ >> + self._cached_assignable = None >> + >> + # _write_to_conf is calculated along with the value. If True, the >> + # Symbol gets a .config entry. >> + >> + self.is_allnoconfig_y = \ >> + self._was_set = \ >> + self._write_to_conf = False >> + >> + # See Kconfig._build_dep() >> + self._dependents = set() >> + >> + def _assignable(self): >> + # Worker function for the 'assignable' attribute >> + >> + if self.orig_type not in _BOOL_TRISTATE: >> + return () >> + >> + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden >> + # function call (property magic) >> + vis = self.visibility >> + if not vis: >> + return () >> + >> + rev_dep_val = expr_value(self.rev_dep) >> + >> + if vis == 2: >> + if self.choice: >> + return (2,) >> + >> + if not rev_dep_val: >> + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: >> + return (0, 2) >> + return (0, 1, 2) >> + >> + if rev_dep_val == 2: >> + return (2,) >> + >> + # rev_dep_val == 1 >> + >> + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: >> + return (2,) >> + return (1, 2) >> + >> + # vis == 1 >> + >> + # Must be a tristate here, because bool m visibility gets promoted to y >> + >> + if not rev_dep_val: >> + return (0, 1) if expr_value(self.weak_rev_dep) != 2 else (0, 2) >> + >> + if rev_dep_val == 2: >> + return (2,) >> + >> + # vis == rev_dep_val == 1 >> + >> + return (1,) >> + >> + def _invalidate(self): >> + # Marks the symbol as needing to be recalculated >> + >> + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ >> + self._cached_assignable = None >> + >> + def _rec_invalidate(self): >> + # Invalidates the symbol and all items that (possibly) depend on it >> + >> + if self is self.kconfig.modules: >> + # Invalidating MODULES has wide-ranging effects >> + self.kconfig._invalidate_all() >> + else: >> + self._invalidate() >> + >> + for item in self._dependents: >> + # _cached_vis doubles as a flag that tells us whether 'item' >> + # has cached values, because it's calculated as a side effect >> + # of calculating all other (non-constant) cached values. >> + # >> + # If item._cached_vis is None, it means there can't be cached >> + # values on other items that depend on 'item', because if there >> + # were, some value on 'item' would have been calculated and >> + # item._cached_vis set as a side effect. It's therefore safe to >> + # stop the invalidation at symbols with _cached_vis None. >> + # >> + # This approach massively speeds up scripts that set a lot of >> + # values, vs simply invalidating all possibly dependent symbols >> + # (even when you already have a list of all the dependent >> + # symbols, because some symbols get huge dependency trees). >> + # >> + # This gracefully handles dependency loops too, which is nice >> + # for choices, where the choice depends on the choice symbols >> + # and vice versa. >> + if item._cached_vis is not None: >> + item._rec_invalidate() >> + >> + def _rec_invalidate_if_has_prompt(self): >> + # Invalidates the symbol and its dependent symbols, but only if the >> + # symbol has a prompt. User values never have an effect on promptless >> + # symbols, so we skip invalidation for them as an optimization. >> + # >> + # This also prevents constant (quoted) symbols from being invalidated >> + # if set_value() is called on them, which would make them lose their >> + # value and break things. >> + # >> + # Prints a warning if the symbol has no prompt. In some contexts (e.g. >> + # when loading a .config files) assignments to promptless symbols are >> + # normal and expected, so the warning can be disabled. >> + >> + for node in self.nodes: >> + if node.prompt: >> + self._rec_invalidate() >> + return >> + >> + if self.kconfig._warn_assign_no_prompt: >> + self.kconfig._warn(self.name_and_loc + " has no prompt, meaning " >> + "user values have no effect on it") >> + >> + def _str_default(self): >> + # write_min_config() helper function. Returns the value the symbol >> + # would get from defaults if it didn't have a user value. Uses exactly >> + # the same algorithm as the C implementation (though a bit cleaned up), >> + # for compatibility. >> + >> + if self.orig_type in _BOOL_TRISTATE: >> + val = 0 >> + >> + # Defaults, selects, and implies do not affect choice symbols >> + if not self.choice: >> + for default, cond in self.defaults: >> + cond_val = expr_value(cond) >> + if cond_val: >> + val = min(expr_value(default), cond_val) >> + break >> + >> + val = max(expr_value(self.rev_dep), >> + expr_value(self.weak_rev_dep), >> + val) >> + >> + # Transpose mod to yes if type is bool (possibly due to modules >> + # being disabled) >> + if val == 1 and self.type is BOOL: >> + val = 2 >> + >> + return TRI_TO_STR[val] >> + >> + if self.orig_type: # STRING/INT/HEX >> + for default, cond in self.defaults: >> + if expr_value(cond): >> + return default.str_value >> + >> + return "" >> + >> + def _warn_select_unsatisfied_deps(self): >> + # Helper for printing an informative warning when a symbol with >> + # unsatisfied direct dependencies (dependencies from 'depends on', ifs, >> + # and menus) is selected by some other symbol. Also warn if a symbol >> + # whose direct dependencies evaluate to m is selected to y. >> + >> + msg = "{} has direct dependencies {} with value {}, but is " \ >> + "currently being {}-selected by the following symbols:" \ >> + .format(self.name_and_loc, expr_str(self.direct_dep), >> + TRI_TO_STR[expr_value(self.direct_dep)], >> + TRI_TO_STR[expr_value(self.rev_dep)]) >> + >> + # The reverse dependencies from each select are ORed together >> + for select in split_expr(self.rev_dep, OR): >> + if expr_value(select) <= expr_value(self.direct_dep): >> + # Only include selects that exceed the direct dependencies >> + continue >> + >> + # - 'select A if B' turns into A && B >> + # - 'select A' just turns into A >> + # >> + # In both cases, we can split on AND and pick the first operand >> + selecting_sym = split_expr(select, AND)[0] >> + >> + msg += "\n - {}, with value {}, direct dependencies {} " \ >> + "(value: {})" \ >> + .format(selecting_sym.name_and_loc, >> + selecting_sym.str_value, >> + expr_str(selecting_sym.direct_dep), >> + TRI_TO_STR[expr_value(selecting_sym.direct_dep)]) >> + >> + if select.__class__ is tuple: >> + msg += ", and select condition {} (value: {})" \ >> + .format(expr_str(select[2]), >> + TRI_TO_STR[expr_value(select[2])]) >> + >> + self.kconfig._warn(msg) >> + >> + >> +class Choice(object): >> + """ >> + Represents a choice statement: >> + >> + choice >> + ... >> + endchoice >> + >> + The following attributes are available on Choice instances. They should be >> + treated as read-only, and some are implemented through @property magic (but >> + are still efficient to access due to internal caching). >> + >> + Note: Prompts, help texts, and locations are stored in the Choice's >> + MenuNode(s) rather than in the Choice itself. Check the MenuNode class and >> + the Choice.nodes attribute. This organization matches the C tools. >> + >> + name: >> + The name of the choice, e.g. "FOO" for 'choice FOO', or None if the >> + Choice has no name. >> + >> + type: >> + The type of the choice. One of BOOL, TRISTATE, UNKNOWN. UNKNOWN is for >> + choices defined without a type where none of the contained symbols have a >> + type either (otherwise the choice inherits the type of the first symbol >> + defined with a type). >> + >> + When running without modules (CONFIG_MODULES=n), TRISTATE choices >> + magically change type to BOOL. This matches the C tools, and makes sense >> + for menuconfig-like functionality. >> + >> + orig_type: >> + The type as given in the Kconfig file, without any magic applied. Used >> + when printing the choice. >> + >> + tri_value: >> + The tristate value (mode) of the choice. A choice can be in one of three >> + modes: >> + >> + 0 (n) - The choice is disabled and no symbols can be selected. For >> + visible choices, this mode is only possible for choices with >> + the 'optional' flag set (see kconfig-language.txt). >> + >> + 1 (m) - Any number of choice symbols can be set to m, the rest will >> + be n. >> + >> + 2 (y) - One symbol will be y, the rest n. >> + >> + Only tristate choices can be in m mode. The visibility of the choice is >> + an upper bound on the mode, and the mode in turn is an upper bound on the >> + visibility of the choice symbols. >> + >> + To change the mode, use Choice.set_value(). >> + >> + Implementation note: >> + The C tools internally represent choices as a type of symbol, with >> + special-casing in many code paths. This is why there is a lot of >> + similarity to Symbol. The value (mode) of a choice is really just a >> + normal symbol value, and an implicit reverse dependency forces its >> + lower bound to m for visible non-optional choices (the reverse >> + dependency is 'm && <visibility>'). >> + >> + Symbols within choices get the choice propagated as a dependency to >> + their properties. This turns the mode of the choice into an upper bound >> + on e.g. the visibility of choice symbols, and explains the gotcha >> + related to printing choice symbols mentioned in the module docstring. >> + >> + Kconfiglib uses a separate Choice class only because it makes the code >> + and interface less confusing (especially in a user-facing interface). >> + Corresponding attributes have the same name in the Symbol and Choice >> + classes, for consistency and compatibility. >> + >> + str_value: >> + Like choice.tri_value, but gives the value as one of the strings >> + "n", "m", or "y" >> + >> + user_value: >> + The value (mode) selected by the user through Choice.set_value(). Either >> + 0, 1, or 2, or None if the user hasn't selected a mode. See >> + Symbol.user_value. >> + >> + WARNING: Do not assign directly to this. It will break things. Use >> + Choice.set_value() instead. >> + >> + assignable: >> + See the symbol class documentation. Gives the assignable values (modes). >> + >> + selection: >> + The Symbol instance of the currently selected symbol. None if the Choice >> + is not in y mode or has no selected symbol (due to unsatisfied >> + dependencies on choice symbols). >> + >> + WARNING: Do not assign directly to this. It will break things. Call >> + sym.set_value(2) on the choice symbol you want to select instead. >> + >> + user_selection: >> + The symbol selected by the user (by setting it to y). Ignored if the >> + choice is not in y mode, but still remembered so that the choice "snaps >> + back" to the user selection if the mode is changed back to y. This might >> + differ from 'selection' due to unsatisfied dependencies. >> + >> + WARNING: Do not assign directly to this. It will break things. Call >> + sym.set_value(2) on the choice symbol to be selected instead. >> + >> + visibility: >> + See the Symbol class documentation. Acts on the value (mode). >> + >> + name_and_loc: >> + Holds a string like >> + >> + "<choice MY_CHOICE> (defined at foo/Kconfig:12)" >> + >> + , giving the name of the choice and its definition location(s). If the >> + choice has no name (isn't defined with 'choice MY_CHOICE'), then it will >> + be shown as "<choice>" before the list of locations (always a single one >> + in that case). >> + >> + syms: >> + List of symbols contained in the choice. >> + >> + Obscure gotcha: If a symbol depends on the previous symbol within a >> + choice so that an implicit menu is created, it won't be a choice symbol, >> + and won't be included in 'syms'. >> + >> + nodes: >> + A list of MenuNodes for this choice. In practice, the list will probably >> + always contain a single MenuNode, but it is possible to give a choice a >> + name and define it in multiple locations. >> + >> + defaults: >> + List of (symbol, cond) tuples for the choice's 'defaults' properties. For >> + example, 'default A if B && C' is represented as (A, (AND, B, C)). If >> + there is no condition, 'cond' is self.kconfig.y. >> + >> + Note that 'depends on' and parent dependencies are propagated to >> + 'default' conditions. >> + >> + orig_defaults: >> + See the corresponding attribute on the MenuNode class. >> + >> + direct_dep: >> + See Symbol.direct_dep. >> + >> + referenced: >> + A set() with all symbols referenced in the properties and property >> + conditions of the choice. >> + >> + Also includes dependencies from surrounding menus and ifs, because those >> + get propagated to the choice (see the 'Intro to symbol values' section in >> + the module docstring). >> + >> + is_optional: >> + True if the choice has the 'optional' flag set on it and can be in >> + n mode. >> + >> + kconfig: >> + The Kconfig instance this choice is from. >> + """ >> + __slots__ = ( >> + "_cached_assignable", >> + "_cached_selection", >> + "_cached_vis", >> + "_dependents", >> + "_visited", >> + "_was_set", >> + "defaults", >> + "direct_dep", >> + "is_constant", >> + "is_optional", >> + "kconfig", >> + "name", >> + "nodes", >> + "orig_type", >> + "syms", >> + "user_selection", >> + "user_value", >> + ) >> + >> + # >> + # Public interface >> + # >> + >> + @property >> + def type(self): >> + """ >> + Returns the type of the choice. See Symbol.type. >> + """ >> + if self.orig_type is TRISTATE and not self.kconfig.modules.tri_value: >> + return BOOL >> + return self.orig_type >> + >> + @property >> + def str_value(self): >> + """ >> + See the class documentation. >> + """ >> + return TRI_TO_STR[self.tri_value] >> + >> + @property >> + def tri_value(self): >> + """ >> + See the class documentation. >> + """ >> + # This emulates a reverse dependency of 'm && visibility' for >> + # non-optional choices, which is how the C implementation does it >> + >> + val = 0 if self.is_optional else 1 >> + >> + if self.user_value is not None: >> + val = max(val, self.user_value) >> + >> + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden >> + # function call (property magic) >> + val = min(val, self.visibility) >> + >> + # Promote m to y for boolean choices >> + return 2 if val == 1 and self.type is BOOL else val >> + >> + @property >> + def assignable(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_assignable is None: >> + self._cached_assignable = self._assignable() >> + return self._cached_assignable >> + >> + @property >> + def visibility(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_vis is None: >> + self._cached_vis = _visibility(self) >> + return self._cached_vis >> + >> + @property >> + def name_and_loc(self): >> + """ >> + See the class documentation. >> + """ >> + # Reuse the expression format, which is '<choice (name, if any)>'. >> + return standard_sc_expr_str(self) + " " + _locs(self) >> + >> + @property >> + def selection(self): >> + """ >> + See the class documentation. >> + """ >> + if self._cached_selection is _NO_CACHED_SELECTION: >> + self._cached_selection = self._selection() >> + return self._cached_selection >> + >> + def set_value(self, value): >> + """ >> + Sets the user value (mode) of the choice. Like for Symbol.set_value(), >> + the visibility might truncate the value. Choices without the 'optional' >> + attribute (is_optional) can never be in n mode, but 0/"n" is still >> + accepted since it's not a malformed value (though it will have no >> + effect). >> + >> + Returns True if the value is valid for the type of the choice, and >> + False otherwise. This only looks at the form of the value. Check the >> + Choice.assignable attribute to see what values are currently in range >> + and would actually be reflected in the mode of the choice. >> + """ >> + if value in STR_TO_TRI: >> + value = STR_TO_TRI[value] >> + >> + if value == self.user_value: >> + # We know the value must be valid if it was successfully set >> + # previously >> + self._was_set = True >> + return True >> + >> + if not (self.orig_type is BOOL and value in (2, 0) or >> + self.orig_type is TRISTATE and value in TRI_TO_STR): >> + >> + # Display tristate values as n, m, y in the warning >> + self.kconfig._warn( >> + "the value {} is invalid for {}, which has type {} -- " >> + "assignment ignored" >> + .format(TRI_TO_STR[value] if value in TRI_TO_STR else >> + "'{}'".format(value), >> + self.name_and_loc, TYPE_TO_STR[self.orig_type])) >> + >> + return False >> + >> + self.user_value = value >> + self._was_set = True >> + self._rec_invalidate() >> + >> + return True >> + >> + def unset_value(self): >> + """ >> + Resets the user value (mode) and user selection of the Choice, as if >> + the user had never touched the mode or any of the choice symbols. >> + """ >> + if self.user_value is not None or self.user_selection: >> + self.user_value = self.user_selection = None >> + self._rec_invalidate() >> + >> + @property >> + def referenced(self): >> + """ >> + See the class documentation. >> + """ >> + return {item for node in self.nodes for item in node.referenced} >> + >> + @property >> + def orig_defaults(self): >> + """ >> + See the class documentation. >> + """ >> + return [d for node in self.nodes for d in node.orig_defaults] >> + >> + def __repr__(self): >> + """ >> + Returns a string with information about the choice when it is evaluated >> + on e.g. the interactive Python prompt. >> + """ >> + fields = ["choice " + self.name if self.name else "choice", >> + TYPE_TO_STR[self.type]] >> + add = fields.append >> + >> + for node in self.nodes: >> + if node.prompt: >> + add('"{}"'.format(node.prompt[0])) >> + >> + add("mode " + self.str_value) >> + >> + if self.user_value is not None: >> + add('user mode {}'.format(TRI_TO_STR[self.user_value])) >> + >> + if self.selection: >> + add("{} selected".format(self.selection.name)) >> + >> + if self.user_selection: >> + user_sel_str = "{} selected by user" \ >> + .format(self.user_selection.name) >> + >> + if self.selection is not self.user_selection: >> + user_sel_str += " (overridden)" >> + >> + add(user_sel_str) >> + >> + add("visibility " + TRI_TO_STR[self.visibility]) >> + >> + if self.is_optional: >> + add("optional") >> + >> + for node in self.nodes: >> + add("{}:{}".format(node.filename, node.linenr)) >> + >> + return "<{}>".format(", ".join(fields)) >> + >> + def __str__(self): >> + """ >> + Returns a string representation of the choice when it is printed. >> + Matches the Kconfig format (though without the contained choice >> + symbols), with any parent dependencies propagated to the 'depends on' >> + condition. >> + >> + The returned string does not end in a newline. >> + >> + See Symbol.__str__() as well. >> + """ >> + return self.custom_str(standard_sc_expr_str) >> + >> + def custom_str(self, sc_expr_str_fn): >> + """ >> + Works like Choice.__str__(), but allows a custom format to be used for >> + all symbol/choice references. See expr_str(). >> + """ >> + return "\n\n".join(node.custom_str(sc_expr_str_fn) >> + for node in self.nodes) >> + >> + # >> + # Private methods >> + # >> + >> + def __init__(self): >> + """ >> + Choice constructor -- not intended to be called directly by Kconfiglib >> + clients. >> + """ >> + # These attributes are always set on the instance from outside and >> + # don't need defaults: >> + # direct_dep >> + # kconfig >> + >> + # - UNKNOWN == 0 >> + # - _visited is used during dep. loop detection >> + self.orig_type = self._visited = 0 >> + >> + self.nodes = [] >> + >> + self.syms = [] >> + self.defaults = [] >> + >> + self.name = \ >> + self.user_value = self.user_selection = \ >> + self._cached_vis = self._cached_assignable = None >> + >> + self._cached_selection = _NO_CACHED_SELECTION >> + >> + # is_constant is checked by _depend_on(). Just set it to avoid having >> + # to special-case choices. >> + self.is_constant = self.is_optional = False >> + >> + # See Kconfig._build_dep() >> + self._dependents = set() >> + >> + def _assignable(self): >> + # Worker function for the 'assignable' attribute >> + >> + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden >> + # function call (property magic) >> + vis = self.visibility >> + >> + if not vis: >> + return () >> + >> + if vis == 2: >> + if not self.is_optional: >> + return (2,) if self.type is BOOL else (1, 2) >> + return (0, 2) if self.type is BOOL else (0, 1, 2) >> + >> + # vis == 1 >> + >> + return (0, 1) if self.is_optional else (1,) >> + >> + def _selection(self): >> + # Worker function for the 'selection' attribute >> + >> + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden >> + # function call (property magic) >> + if self.tri_value != 2: >> + # Not in y mode, so no selection >> + return None >> + >> + # Use the user selection if it's visible >> + if self.user_selection and self.user_selection.visibility: >> + return self.user_selection >> + >> + # Otherwise, check if we have a default >> + return self._selection_from_defaults() >> + >> + def _selection_from_defaults(self): >> + # Check if we have a default >> + for sym, cond in self.defaults: >> + # The default symbol must be visible too >> + if expr_value(cond) and sym.visibility: >> + return sym >> + >> + # Otherwise, pick the first visible symbol, if any >> + for sym in self.syms: >> + if sym.visibility: >> + return sym >> + >> + # Couldn't find a selection >> + return None >> + >> + def _invalidate(self): >> + self._cached_vis = self._cached_assignable = None >> + self._cached_selection = _NO_CACHED_SELECTION >> + >> + def _rec_invalidate(self): >> + # See Symbol._rec_invalidate() >> + >> + self._invalidate() >> + >> + for item in self._dependents: >> + if item._cached_vis is not None: >> + item._rec_invalidate() >> + >> + >> +class MenuNode(object): >> + """ >> + Represents a menu node in the configuration. This corresponds to an entry >> + in e.g. the 'make menuconfig' interface, though non-visible choices, menus, >> + and comments also get menu nodes. If a symbol or choice is defined in >> + multiple locations, it gets one menu node for each location. >> + >> + The top-level menu node, corresponding to the implicit top-level menu, is >> + available in Kconfig.top_node. >> + >> + The menu nodes for a Symbol or Choice can be found in the >> + Symbol/Choice.nodes attribute. Menus and comments are represented as plain >> + menu nodes, with their text stored in the prompt attribute (prompt[0]). >> + This mirrors the C implementation. >> + >> + The following attributes are available on MenuNode instances. They should >> + be viewed as read-only. >> + >> + item: >> + Either a Symbol, a Choice, or one of the constants MENU and COMMENT. >> + Menus and comments are represented as plain menu nodes. Ifs are collapsed >> + (matching the C implementation) and do not appear in the final menu tree. >> + >> + next: >> + The following menu node. None if there is no following node. >> + >> + list: >> + The first child menu node. None if there are no children. >> + >> + Choices and menus naturally have children, but Symbols can also have >> + children because of menus created automatically from dependencies (see >> + kconfig-language.txt). >> + >> + parent: >> + The parent menu node. None if there is no parent. >> + >> + prompt: >> + A (string, cond) tuple with the prompt for the menu node and its >> + conditional expression (which is self.kconfig.y if there is no >> + condition). None if there is no prompt. >> + >> + For symbols and choices, the prompt is stored in the MenuNode rather than >> + the Symbol or Choice instance. For menus and comments, the prompt holds >> + the text. >> + >> + defaults: >> + The 'default' properties for this particular menu node. See >> + symbol.defaults. >> + >> + When evaluating defaults, you should use Symbol/Choice.defaults instead, >> + as it include properties from all menu nodes (a symbol/choice can have >> + multiple definition locations/menu nodes). MenuNode.defaults is meant for >> + documentation generation. >> + >> + selects: >> + Like MenuNode.defaults, for selects. >> + >> + implies: >> + Like MenuNode.defaults, for implies. >> + >> + ranges: >> + Like MenuNode.defaults, for ranges. >> + >> + orig_prompt: >> + orig_defaults: >> + orig_selects: >> + orig_implies: >> + orig_ranges: >> + These work the like the corresponding attributes without orig_*, but omit >> + any dependencies propagated from 'depends on' and surrounding 'if's (the >> + direct dependencies, stored in MenuNode.dep). >> + >> + One use for this is generating less cluttered documentation, by only >> + showing the direct dependencies in one place. >> + >> + help: >> + The help text for the menu node for Symbols and Choices. None if there is >> + no help text. Always stored in the node rather than the Symbol or Choice. >> + It is possible to have a separate help text at each location if a symbol >> + is defined in multiple locations. >> + >> + Trailing whitespace (including a final newline) is stripped from the help >> + text. This was not the case before Kconfiglib 10.21.0, where the format >> + was undocumented. >> + >> + dep: >> + The direct ('depends on') dependencies for the menu node, or >> + self.kconfig.y if there are no direct dependencies. >> + >> + This attribute includes any dependencies from surrounding menus and ifs. >> + Those get propagated to the direct dependencies, and the resulting direct >> + dependencies in turn get propagated to the conditions of all properties. >> + >> + If a symbol or choice is defined in multiple locations, only the >> + properties defined at a particular location get the corresponding >> + MenuNode.dep dependencies propagated to them. >> + >> + visibility: >> + The 'visible if' dependencies for the menu node (which must represent a >> + menu), or self.kconfig.y if there are no 'visible if' dependencies. >> + 'visible if' dependencies are recursively propagated to the prompts of >> + symbols and choices within the menu. >> + >> + referenced: >> + A set() with all symbols and choices referenced in the properties and >> + property conditions of the menu node. >> + >> + Also includes dependencies inherited from surrounding menus and ifs. >> + Choices appear in the dependencies of choice symbols. >> + >> + is_menuconfig: >> + Set to True if the children of the menu node should be displayed in a >> + separate menu. This is the case for the following items: >> + >> + - Menus (node.item == MENU) >> + >> + - Choices >> + >> + - Symbols defined with the 'menuconfig' keyword. The children come from >> + implicitly created submenus, and should be displayed in a separate >> + menu rather than being indented. >> + >> + 'is_menuconfig' is just a hint on how to display the menu node. It's >> + ignored internally by Kconfiglib, except when printing symbols. >> + >> + filename/linenr: >> + The location where the menu node appears. The filename is relative to >> + $srctree (or to the current directory if $srctree isn't set), except >> + absolute paths are used for paths outside $srctree. >> + >> + include_path: >> + A tuple of (filename, linenr) tuples, giving the locations of the >> + 'source' statements via which the Kconfig file containing this menu node >> + was included. The first element is the location of the 'source' statement >> + in the top-level Kconfig file passed to Kconfig.__init__(), etc. >> + >> + Note that the Kconfig file of the menu node itself isn't included. Check >> + 'filename' and 'linenr' for that. >> + >> + kconfig: >> + The Kconfig instance the menu node is from. >> + """ >> + __slots__ = ( >> + "dep", >> + "filename", >> + "help", >> + "include_path", >> + "is_menuconfig", >> + "item", >> + "kconfig", >> + "linenr", >> + "list", >> + "next", >> + "parent", >> + "prompt", >> + "visibility", >> + >> + # Properties >> + "defaults", >> + "selects", >> + "implies", >> + "ranges", >> + ) >> + >> + def __init__(self): >> + # Properties defined on this particular menu node. A local 'depends on' >> + # only applies to these, in case a symbol is defined in multiple >> + # locations. >> + self.defaults = [] >> + self.selects = [] >> + self.implies = [] >> + self.ranges = [] >> + >> + @property >> + def orig_prompt(self): >> + """ >> + See the class documentation. >> + """ >> + if not self.prompt: >> + return None >> + return (self.prompt[0], self._strip_dep(self.prompt[1])) >> + >> + @property >> + def orig_defaults(self): >> + """ >> + See the class documentation. >> + """ >> + return [(default, self._strip_dep(cond)) >> + for default, cond in self.defaults] >> + >> + @property >> + def orig_selects(self): >> + """ >> + See the class documentation. >> + """ >> + return [(select, self._strip_dep(cond)) >> + for select, cond in self.selects] >> + >> + @property >> + def orig_implies(self): >> + """ >> + See the class documentation. >> + """ >> + return [(imply, self._strip_dep(cond)) >> + for imply, cond in self.implies] >> + >> + @property >> + def orig_ranges(self): >> + """ >> + See the class documentation. >> + """ >> + return [(low, high, self._strip_dep(cond)) >> + for low, high, cond in self.ranges] >> + >> + @property >> + def referenced(self): >> + """ >> + See the class documentation. >> + """ >> + # self.dep is included to catch dependencies from a lone 'depends on' >> + # when there are no properties to propagate it to >> + res = expr_items(self.dep) >> + >> + if self.prompt: >> + res |= expr_items(self.prompt[1]) >> + >> + if self.item is MENU: >> + res |= expr_items(self.visibility) >> + >> + for value, cond in self.defaults: >> + res |= expr_items(value) >> + res |= expr_items(cond) >> + >> + for value, cond in self.selects: >> + res.add(value) >> + res |= expr_items(cond) >> + >> + for value, cond in self.implies: >> + res.add(value) >> + res |= expr_items(cond) >> + >> + for low, high, cond in self.ranges: >> + res.add(low) >> + res.add(high) >> + res |= expr_items(cond) >> + >> + return res >> + >> + def __repr__(self): >> + """ >> + Returns a string with information about the menu node when it is >> + evaluated on e.g. the interactive Python prompt. >> + """ >> + fields = [] >> + add = fields.append >> + >> + if self.item.__class__ is Symbol: >> + add("menu node for symbol " + self.item.name) >> + >> + elif self.item.__class__ is Choice: >> + s = "menu node for choice" >> + if self.item.name is not None: >> + s += " " + self.item.name >> + add(s) >> + >> + elif self.item is MENU: >> + add("menu node for menu") >> + >> + else: # self.item is COMMENT >> + add("menu node for comment") >> + >> + if self.prompt: >> + add('prompt "{}" (visibility {})'.format( >> + self.prompt[0], TRI_TO_STR[expr_value(self.prompt[1])])) >> + >> + if self.item.__class__ is Symbol and self.is_menuconfig: >> + add("is menuconfig") >> + >> + add("deps " + TRI_TO_STR[expr_value(self.dep)]) >> + >> + if self.item is MENU: >> + add("'visible if' deps " + TRI_TO_STR[expr_value(self.visibility)]) >> + >> + if self.item.__class__ in _SYMBOL_CHOICE and self.help is not None: >> + add("has help") >> + >> + if self.list: >> + add("has child") >> + >> + if self.next: >> + add("has next") >> + >> + add("{}:{}".format(self.filename, self.linenr)) >> + >> + return "<{}>".format(", ".join(fields)) >> + >> + def __str__(self): >> + """ >> + Returns a string representation of the menu node. Matches the Kconfig >> + format, with any parent dependencies propagated to the 'depends on' >> + condition. >> + >> + The output could (almost) be fed back into a Kconfig parser to redefine >> + the object associated with the menu node. See the module documentation >> + for a gotcha related to choice symbols. >> + >> + For symbols and choices with multiple menu nodes (multiple definition >> + locations), properties that aren't associated with a particular menu >> + node are shown on all menu nodes ('option env=...', 'optional' for >> + choices, etc.). >> + >> + The returned string does not end in a newline. >> + """ >> + return self.custom_str(standard_sc_expr_str) >> + >> + def custom_str(self, sc_expr_str_fn): >> + """ >> + Works like MenuNode.__str__(), but allows a custom format to be used >> + for all symbol/choice references. See expr_str(). >> + """ >> + return self._menu_comment_node_str(sc_expr_str_fn) \ >> + if self.item in _MENU_COMMENT else \ >> + self._sym_choice_node_str(sc_expr_str_fn) >> + >> + def _menu_comment_node_str(self, sc_expr_str_fn): >> + s = '{} "{}"'.format("menu" if self.item is MENU else "comment", >> + self.prompt[0]) >> + >> + if self.dep is not self.kconfig.y: >> + s += "\n\tdepends on {}".format(expr_str(self.dep, sc_expr_str_fn)) >> + >> + if self.item is MENU and self.visibility is not self.kconfig.y: >> + s += "\n\tvisible if {}".format(expr_str(self.visibility, >> + sc_expr_str_fn)) >> + >> + return s >> + >> + def _sym_choice_node_str(self, sc_expr_str_fn): >> + def indent_add(s): >> + lines.append("\t" + s) >> + >> + def indent_add_cond(s, cond): >> + if cond is not self.kconfig.y: >> + s += " if " + expr_str(cond, sc_expr_str_fn) >> + indent_add(s) >> + >> + sc = self.item >> + >> + if sc.__class__ is Symbol: >> + lines = [("menuconfig " if self.is_menuconfig else "config ") >> + + sc.name] >> + else: >> + lines = ["choice " + sc.name if sc.name else "choice"] >> + >> + if sc.orig_type and not self.prompt: # sc.orig_type != UNKNOWN >> + # If there's a prompt, we'll use the '<type> "prompt"' shorthand >> + # instead >> + indent_add(TYPE_TO_STR[sc.orig_type]) >> + >> + if self.prompt: >> + if sc.orig_type: >> + prefix = TYPE_TO_STR[sc.orig_type] >> + else: >> + # Symbol defined without a type (which generates a warning) >> + prefix = "prompt" >> + >> + indent_add_cond(prefix + ' "{}"'.format(escape(self.prompt[0])), >> + self.orig_prompt[1]) >> + >> + if sc.__class__ is Symbol: >> + if sc.is_allnoconfig_y: >> + indent_add("option allnoconfig_y") >> + >> + if sc is sc.kconfig.defconfig_list: >> + indent_add("option defconfig_list") >> + >> + if sc.env_var is not None: >> + indent_add('option env="{}"'.format(sc.env_var)) >> + >> + if sc is sc.kconfig.modules: >> + indent_add("option modules") >> + >> + for low, high, cond in self.orig_ranges: >> + indent_add_cond( >> + "range {} {}".format(sc_expr_str_fn(low), >> + sc_expr_str_fn(high)), >> + cond) >> + >> + for default, cond in self.orig_defaults: >> + indent_add_cond("default " + expr_str(default, sc_expr_str_fn), >> + cond) >> + >> + if sc.__class__ is Choice and sc.is_optional: >> + indent_add("optional") >> + >> + if sc.__class__ is Symbol: >> + for select, cond in self.orig_selects: >> + indent_add_cond("select " + sc_expr_str_fn(select), cond) >> + >> + for imply, cond in self.orig_implies: >> + indent_add_cond("imply " + sc_expr_str_fn(imply), cond) >> + >> + if self.dep is not sc.kconfig.y: >> + indent_add("depends on " + expr_str(self.dep, sc_expr_str_fn)) >> + >> + if self.help is not None: >> + indent_add("help") >> + for line in self.help.splitlines(): >> + indent_add(" " + line) >> + >> + return "\n".join(lines) >> + >> + def _strip_dep(self, expr): >> + # Helper function for removing MenuNode.dep from 'expr'. Uses two >> + # pieces of internal knowledge: (1) Expressions are reused rather than >> + # copied, and (2) the direct dependencies always appear at the end. >> + >> + # ... if dep -> ... if y >> + if self.dep is expr: >> + return self.kconfig.y >> + >> + # (AND, X, dep) -> X >> + if expr.__class__ is tuple and expr[0] is AND and expr[2] is self.dep: >> + return expr[1] >> + >> + return expr >> + >> + >> +class Variable(object): >> + """ >> + Represents a preprocessor variable/function. >> + >> + The following attributes are available: >> + >> + name: >> + The name of the variable. >> + >> + value: >> + The unexpanded value of the variable. >> + >> + expanded_value: >> + The expanded value of the variable. For simple variables (those defined >> + with :=), this will equal 'value'. Accessing this property will raise a >> + KconfigError if the expansion seems to be stuck in a loop. >> + >> + Accessing this field is the same as calling expanded_value_w_args() with >> + no arguments. I hadn't considered function arguments when adding it. It >> + is retained for backwards compatibility though. >> + >> + is_recursive: >> + True if the variable is recursive (defined with =). >> + """ >> + __slots__ = ( >> + "_n_expansions", >> + "is_recursive", >> + "kconfig", >> + "name", >> + "value", >> + ) >> + >> + @property >> + def expanded_value(self): >> + """ >> + See the class documentation. >> + """ >> + return self.expanded_value_w_args() >> + >> + def expanded_value_w_args(self, *args): >> + """ >> + Returns the expanded value of the variable/function. Any arguments >> + passed will be substituted for $(1), $(2), etc. >> + >> + Raises a KconfigError if the expansion seems to be stuck in a loop. >> + """ >> + return self.kconfig._fn_val((self.name,) + args) >> + >> + def __repr__(self): >> + return "<variable {}, {}, value '{}'>" \ >> + .format(self.name, >> + "recursive" if self.is_recursive else "immediate", >> + self.value) >> + >> + >> +class KconfigError(Exception): >> + """ >> + Exception raised for Kconfig-related errors. >> + >> + KconfigError and KconfigSyntaxError are the same class. The >> + KconfigSyntaxError alias is only maintained for backwards compatibility. >> + """ >> + >> +KconfigSyntaxError = KconfigError # Backwards compatibility >> + >> + >> +class InternalError(Exception): >> + "Never raised. Kept around for backwards compatibility." >> + >> + >> +# Workaround: >> +# >> +# If 'errno' and 'strerror' are set on IOError, then __str__() always returns >> +# "[Errno <errno>] <strerror>", ignoring any custom message passed to the >> +# constructor. By defining our own subclass, we can use a custom message while >> +# also providing 'errno', 'strerror', and 'filename' to scripts. >> +class _KconfigIOError(IOError): >> + def __init__(self, ioerror, msg): >> + self.msg = msg >> + super(_KconfigIOError, self).__init__( >> + ioerror.errno, ioerror.strerror, ioerror.filename) >> + >> + def __str__(self): >> + return self.msg >> + >> + >> +# >> +# Public functions >> +# >> + >> + >> +def expr_value(expr): >> + """ >> + Evaluates the expression 'expr' to a tristate value. Returns 0 (n), 1 (m), >> + or 2 (y). >> + >> + 'expr' must be an already-parsed expression from a Symbol, Choice, or >> + MenuNode property. To evaluate an expression represented as a string, use >> + Kconfig.eval_string(). >> + >> + Passing subexpressions of expressions to this function works as expected. >> + """ >> + if expr.__class__ is not tuple: >> + return expr.tri_value >> + >> + if expr[0] is AND: >> + v1 = expr_value(expr[1]) >> + # Short-circuit the n case as an optimization (~5% faster >> + # allnoconfig.py and allyesconfig.py, as of writing) >> + return 0 if not v1 else min(v1, expr_value(expr[2])) >> + >> + if expr[0] is OR: >> + v1 = expr_value(expr[1]) >> + # Short-circuit the y case as an optimization >> + return 2 if v1 == 2 else max(v1, expr_value(expr[2])) >> + >> + if expr[0] is NOT: >> + return 2 - expr_value(expr[1]) >> + >> + # Relation >> + # >> + # Implements <, <=, >, >= comparisons as well. These were added to >> + # kconfig in 31847b67 (kconfig: allow use of relations other than >> + # (in)equality). >> + >> + rel, v1, v2 = expr >> + >> + # If both operands are strings... >> + if v1.orig_type is STRING and v2.orig_type is STRING: >> + # ...then compare them lexicographically >> + comp = _strcmp(v1.str_value, v2.str_value) >> + else: >> + # Otherwise, try to compare them as numbers >> + try: >> + comp = _sym_to_num(v1) - _sym_to_num(v2) >> + except ValueError: >> + # Fall back on a lexicographic comparison if the operands don't >> + # parse as numbers >> + comp = _strcmp(v1.str_value, v2.str_value) >> + >> + return 2*(comp == 0 if rel is EQUAL else >> + comp != 0 if rel is UNEQUAL else >> + comp < 0 if rel is LESS else >> + comp <= 0 if rel is LESS_EQUAL else >> + comp > 0 if rel is GREATER else >> + comp >= 0) >> + >> + >> +def standard_sc_expr_str(sc): >> + """ >> + Standard symbol/choice printing function. Uses plain Kconfig syntax, and >> + displays choices as <choice> (or <choice NAME>, for named choices). >> + >> + See expr_str(). >> + """ >> + if sc.__class__ is Symbol: >> + if sc.is_constant and sc.name not in STR_TO_TRI: >> + return '"{}"'.format(escape(sc.name)) >> + return sc.name >> + >> + return "<choice {}>".format(sc.name) if sc.name else "<choice>" >> + >> + >> +def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str): >> + """ >> + Returns the string representation of the expression 'expr', as in a Kconfig >> + file. >> + >> + Passing subexpressions of expressions to this function works as expected. >> + >> + sc_expr_str_fn (default: standard_sc_expr_str): >> + This function is called for every symbol/choice (hence "sc") appearing in >> + the expression, with the symbol/choice as the argument. It is expected to >> + return a string to be used for the symbol/choice. >> + >> + This can be used e.g. to turn symbols/choices into links when generating >> + documentation, or for printing the value of each symbol/choice after it. >> + >> + Note that quoted values are represented as constants symbols >> + (Symbol.is_constant == True). >> + """ >> + if expr.__class__ is not tuple: >> + return sc_expr_str_fn(expr) >> + >> + if expr[0] is AND: >> + return "{} && {}".format(_parenthesize(expr[1], OR, sc_expr_str_fn), >> + _parenthesize(expr[2], OR, sc_expr_str_fn)) >> + >> + if expr[0] is OR: >> + # This turns A && B || C && D into "(A && B) || (C && D)", which is >> + # redundant, but more readable >> + return "{} || {}".format(_parenthesize(expr[1], AND, sc_expr_str_fn), >> + _parenthesize(expr[2], AND, sc_expr_str_fn)) >> + >> + if expr[0] is NOT: >> + if expr[1].__class__ is tuple: >> + return "!({})".format(expr_str(expr[1], sc_expr_str_fn)) >> + return "!" + sc_expr_str_fn(expr[1]) # Symbol >> + >> + # Relation >> + # >> + # Relation operands are always symbols (quoted strings are constant >> + # symbols) >> + return "{} {} {}".format(sc_expr_str_fn(expr[1]), REL_TO_STR[expr[0]], >> + sc_expr_str_fn(expr[2])) >> + >> + >> +def expr_items(expr): >> + """ >> + Returns a set() of all items (symbols and choices) that appear in the >> + expression 'expr'. >> + >> + Passing subexpressions of expressions to this function works as expected. >> + """ >> + res = set() >> + >> + def rec(subexpr): >> + if subexpr.__class__ is tuple: >> + # AND, OR, NOT, or relation >> + >> + rec(subexpr[1]) >> + >> + # NOTs only have a single operand >> + if subexpr[0] is not NOT: >> + rec(subexpr[2]) >> + >> + else: >> + # Symbol or choice >> + res.add(subexpr) >> + >> + rec(expr) >> + return res >> + >> + >> +def split_expr(expr, op): >> + """ >> + Returns a list containing the top-level AND or OR operands in the >> + expression 'expr', in the same (left-to-right) order as they appear in >> + the expression. >> + >> + This can be handy e.g. for splitting (weak) reverse dependencies >> + from 'select' and 'imply' into individual selects/implies. >> + >> + op: >> + Either AND to get AND operands, or OR to get OR operands. >> + >> + (Having this as an operand might be more future-safe than having two >> + hardcoded functions.) >> + >> + >> + Pseudo-code examples: >> + >> + split_expr( A , OR ) -> [A] >> + split_expr( A && B , OR ) -> [A && B] >> + split_expr( A || B , OR ) -> [A, B] >> + split_expr( A || B , AND ) -> [A || B] >> + split_expr( A || B || (C && D) , OR ) -> [A, B, C && D] >> + >> + # Second || is not at the top level >> + split_expr( A || (B && (C || D)) , OR ) -> [A, B && (C || D)] >> + >> + # Parentheses don't matter as long as we stay at the top level (don't >> + # encounter any non-'op' nodes) >> + split_expr( (A || B) || C , OR ) -> [A, B, C] >> + split_expr( A || (B || C) , OR ) -> [A, B, C] >> + """ >> + res = [] >> + >> + def rec(subexpr): >> + if subexpr.__class__ is tuple and subexpr[0] is op: >> + rec(subexpr[1]) >> + rec(subexpr[2]) >> + else: >> + res.append(subexpr) >> + >> + rec(expr) >> + return res >> + >> + >> +def escape(s): >> + r""" >> + Escapes the string 's' in the same fashion as is done for display in >> + Kconfig format and when writing strings to a .config file. " and \ are >> + replaced by \" and \\, respectively. >> + """ >> + # \ must be escaped before " to avoid double escaping >> + return s.replace("\\", r"\\").replace('"', r'\"') >> + >> + >> +def unescape(s): >> + r""" >> + Unescapes the string 's'. \ followed by any character is replaced with just >> + that character. Used internally when reading .config files. >> + """ >> + return _unescape_sub(r"\1", s) >> + >> +# unescape() helper >> +_unescape_sub = re.compile(r"\\(.)").sub >> + >> + >> +def standard_kconfig(description=None): >> + """ >> + Argument parsing helper for tools that take a single optional Kconfig file >> + argument (default: Kconfig). Returns the Kconfig instance for the parsed >> + configuration. Uses argparse internally. >> + >> + Exits with sys.exit() (which raises SystemExit) on errors. >> + >> + description (default: None): >> + The 'description' passed to argparse.ArgumentParser(). >> + argparse.RawDescriptionHelpFormatter is used, so formatting is preserved. >> + """ >> + import argparse >> + >> + parser = argparse.ArgumentParser( >> + formatter_class=argparse.RawDescriptionHelpFormatter, >> + description=description) >> + >> + parser.add_argument( >> + "kconfig", >> + metavar="KCONFIG", >> + default="Kconfig", >> + nargs="?", >> + help="Top-level Kconfig file (default: Kconfig)") >> + >> + return Kconfig(parser.parse_args().kconfig, suppress_traceback=True) >> + >> + >> +def standard_config_filename(): >> + """ >> + Helper for tools. Returns the value of KCONFIG_CONFIG (which specifies the >> + .config file to load/save) if it is set, and ".config" otherwise. >> + >> + Calling load_config() with filename=None might give the behavior you want, >> + without having to use this function. >> + """ >> + return os.getenv("KCONFIG_CONFIG", ".config") >> + >> + >> +def load_allconfig(kconf, filename): >> + """ >> + Use Kconfig.load_allconfig() instead, which was added in Kconfiglib 13.4.0. >> + Supported for backwards compatibility. Might be removed at some point after >> + a long period of deprecation warnings. >> + """ >> + allconfig = os.getenv("KCONFIG_ALLCONFIG") >> + if allconfig is None: >> + return >> + >> + def std_msg(e): >> + # "Upcasts" a _KconfigIOError to an IOError, removing the custom >> + # __str__() message. The standard message is better here. >> + # >> + # This might also convert an OSError to an IOError in obscure cases, >> + # but it's probably not a big deal. The distinction is shaky (see >> + # PEP-3151). >> + return IOError(e.errno, e.strerror, e.filename) >> + >> + old_warn_assign_override = kconf.warn_assign_override >> + old_warn_assign_redun = kconf.warn_assign_redun >> + kconf.warn_assign_override = kconf.warn_assign_redun = False >> + >> + if allconfig in ("", "1"): >> + try: >> + print(kconf.load_config(filename, False)) >> + except EnvironmentError as e1: >> + try: >> + print(kconf.load_config("all.config", False)) >> + except EnvironmentError as e2: >> + sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} " >> + "nor all.config could be opened: {}, {}" >> + .format(filename, std_msg(e1), std_msg(e2))) >> + else: >> + try: >> + print(kconf.load_config(allconfig, False)) >> + except EnvironmentError as e: >> + sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which " >> + "could not be opened: {}" >> + .format(allconfig, std_msg(e))) >> + >> + kconf.warn_assign_override = old_warn_assign_override >> + kconf.warn_assign_redun = old_warn_assign_redun >> + >> + >> +# >> +# Internal functions >> +# >> + >> + >> +def _visibility(sc): >> + # Symbols and Choices have a "visibility" that acts as an upper bound on >> + # the values a user can set for them, corresponding to the visibility in >> + # e.g. 'make menuconfig'. This function calculates the visibility for the >> + # Symbol or Choice 'sc' -- the logic is nearly identical. >> + >> + vis = 0 >> + >> + for node in sc.nodes: >> + if node.prompt: >> + vis = max(vis, expr_value(node.prompt[1])) >> + >> + if sc.__class__ is Symbol and sc.choice: >> + if sc.choice.orig_type is TRISTATE and \ >> + sc.orig_type is not TRISTATE and sc.choice.tri_value != 2: >> + # Non-tristate choice symbols are only visible in y mode >> + return 0 >> + >> + if sc.orig_type is TRISTATE and vis == 1 and sc.choice.tri_value == 2: >> + # Choice symbols with m visibility are not visible in y mode >> + return 0 >> + >> + # Promote m to y if we're dealing with a non-tristate (possibly due to >> + # modules being disabled) >> + if vis == 1 and sc.type is not TRISTATE: >> + return 2 >> + >> + return vis >> + >> + >> +def _depend_on(sc, expr): >> + # Adds 'sc' (symbol or choice) as a "dependee" to all symbols in 'expr'. >> + # Constant symbols in 'expr' are skipped as they can never change value >> + # anyway. >> + >> + if expr.__class__ is tuple: >> + # AND, OR, NOT, or relation >> + >> + _depend_on(sc, expr[1]) >> + >> + # NOTs only have a single operand >> + if expr[0] is not NOT: >> + _depend_on(sc, expr[2]) >> + >> + elif not expr.is_constant: >> + # Non-constant symbol, or choice >> + expr._dependents.add(sc) >> + >> + >> +def _parenthesize(expr, type_, sc_expr_str_fn): >> + # expr_str() helper. Adds parentheses around expressions of type 'type_'. >> + >> + if expr.__class__ is tuple and expr[0] is type_: >> + return "({})".format(expr_str(expr, sc_expr_str_fn)) >> + return expr_str(expr, sc_expr_str_fn) >> + >> + >> +def _ordered_unique(lst): >> + # Returns 'lst' with any duplicates removed, preserving order. This hacky >> + # version seems to be a common idiom. It relies on short-circuit evaluation >> + # and set.add() returning None, which is falsy. >> + >> + seen = set() >> + seen_add = seen.add >> + return [x for x in lst if x not in seen and not seen_add(x)] >> + >> + >> +def _is_base_n(s, n): >> + try: >> + int(s, n) >> + return True >> + except ValueError: >> + return False >> + >> + >> +def _strcmp(s1, s2): >> + # strcmp()-alike that returns -1, 0, or 1 >> + >> + return (s1 > s2) - (s1 < s2) >> + >> + >> +def _sym_to_num(sym): >> + # expr_value() helper for converting a symbol to a number. Raises >> + # ValueError for symbols that can't be converted. >> + >> + # For BOOL and TRISTATE, n/m/y count as 0/1/2. This mirrors 9059a3493ef >> + # ("kconfig: fix relational operators for bool and tristate symbols") in >> + # the C implementation. >> + return sym.tri_value if sym.orig_type in _BOOL_TRISTATE else \ >> + int(sym.str_value, _TYPE_TO_BASE[sym.orig_type]) >> + >> + >> +def _touch_dep_file(path, sym_name): >> + # If sym_name is MY_SYM_NAME, touches my/sym/name.h. See the sync_deps() >> + # docstring. >> + >> + sym_path = path + os.sep + sym_name.lower().replace("_", os.sep) + ".h" >> + sym_path_dir = dirname(sym_path) >> + if not exists(sym_path_dir): >> + os.makedirs(sym_path_dir, 0o755) >> + >> + # A kind of truncating touch, mirroring the C tools >> + os.close(os.open( >> + sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)) >> + >> + >> +def _save_old(path): >> + # See write_config() >> + >> + def copy(src, dst): >> + # Import as needed, to save some startup time >> + import shutil >> + shutil.copyfile(src, dst) >> + >> + if islink(path): >> + # Preserve symlinks >> + copy_fn = copy >> + elif hasattr(os, "replace"): >> + # Python 3 (3.3+) only. Best choice when available, because it >> + # removes <filename>.old on both *nix and Windows. >> + copy_fn = os.replace >> + elif os.name == "posix": >> + # Removes <filename>.old on POSIX systems >> + copy_fn = os.rename >> + else: >> + # Fall back on copying >> + copy_fn = copy >> + >> + try: >> + copy_fn(path, path + ".old") >> + except Exception: >> + # Ignore errors from 'path' missing as well as other errors. >> + # <filename>.old file is usually more of a nice-to-have, and not worth >> + # erroring out over e.g. if <filename>.old happens to be a directory or >> + # <filename> is something like /dev/null. >> + pass >> + >> + >> +def _locs(sc): >> + # Symbol/Choice.name_and_loc helper. Returns the "(defined at ...)" part of >> + # the string. 'sc' is a Symbol or Choice. >> + >> + if sc.nodes: >> + return "(defined at {})".format( >> + ", ".join("{0.filename}:{0.linenr}".format(node) >> + for node in sc.nodes)) >> + >> + return "(undefined)" >> + >> + >> +# Menu manipulation >> + >> + >> +def _expr_depends_on(expr, sym): >> + # Reimplementation of expr_depends_symbol() from mconf.c. Used to determine >> + # if a submenu should be implicitly created. This also influences which >> + # items inside choice statements are considered choice items. >> + >> + if expr.__class__ is not tuple: >> + return expr is sym >> + >> + if expr[0] in _EQUAL_UNEQUAL: >> + # Check for one of the following: >> + # sym = m/y, m/y = sym, sym != n, n != sym >> + >> + left, right = expr[1:] >> + >> + if right is sym: >> + left, right = right, left >> + elif left is not sym: >> + return False >> + >> + return (expr[0] is EQUAL and right is sym.kconfig.m or >> + right is sym.kconfig.y) or \ >> + (expr[0] is UNEQUAL and right is sym.kconfig.n) >> + >> + return expr[0] is AND and \ >> + (_expr_depends_on(expr[1], sym) or >> + _expr_depends_on(expr[2], sym)) >> + >> + >> +def _auto_menu_dep(node1, node2): >> + # Returns True if node2 has an "automatic menu dependency" on node1. If >> + # node2 has a prompt, we check its condition. Otherwise, we look directly >> + # at node2.dep. >> + >> + return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep, >> + node1.item) >> + >> + >> +def _flatten(node): >> + # "Flattens" menu nodes without prompts (e.g. 'if' nodes and non-visible >> + # symbols with children from automatic menu creation) so that their >> + # children appear after them instead. This gives a clean menu structure >> + # with no unexpected "jumps" in the indentation. >> + # >> + # Do not flatten promptless choices (which can appear "legitimately" if a >> + # named choice is defined in multiple locations to add on symbols). It >> + # looks confusing, and the menuconfig already shows all choice symbols if >> + # you enter the choice at some location with a prompt. >> + >> + while node: >> + if node.list and not node.prompt and \ >> + node.item.__class__ is not Choice: >> + >> + last_node = node.list >> + while 1: >> + last_node.parent = node.parent >> + if not last_node.next: >> + break >> + last_node = last_node.next >> + >> + last_node.next = node.next >> + node.next = node.list >> + node.list = None >> + >> + node = node.next >> + >> + >> +def _remove_ifs(node): >> + # Removes 'if' nodes (which can be recognized by MenuNode.item being None), >> + # which are assumed to already have been flattened. The C implementation >> + # doesn't bother to do this, but we expose the menu tree directly, and it >> + # makes it nicer to work with. >> + >> + cur = node.list >> + while cur and not cur.item: >> + cur = cur.next >> + >> + node.list = cur >> + >> + while cur: >> + next = cur.next >> + while next and not next.item: >> + next = next.next >> + >> + # Equivalent to >> + # >> + # cur.next = next >> + # cur = next >> + # >> + # due to tricky Python semantics. The order matters. >> + cur.next = cur = next >> + >> + >> +def _finalize_choice(node): >> + # Finalizes a choice, marking each symbol whose menu node has the choice as >> + # the parent as a choice symbol, and automatically determining types if not >> + # specified. >> + >> + choice = node.item >> + >> + cur = node.list >> + while cur: >> + if cur.item.__class__ is Symbol: >> + cur.item.choice = choice >> + choice.syms.append(cur.item) >> + cur = cur.next >> + >> + # If no type is specified for the choice, its type is that of >> + # the first choice item with a specified type >> + if not choice.orig_type: >> + for item in choice.syms: >> + if item.orig_type: >> + choice.orig_type = item.orig_type >> + break >> + >> + # Each choice item of UNKNOWN type gets the type of the choice >> + for sym in choice.syms: >> + if not sym.orig_type: >> + sym.orig_type = choice.orig_type >> + >> + >> +def _check_dep_loop_sym(sym, ignore_choice): >> + # Detects dependency loops using depth-first search on the dependency graph >> + # (which is calculated earlier in Kconfig._build_dep()). >> + # >> + # Algorithm: >> + # >> + # 1. Symbols/choices start out with _visited = 0, meaning unvisited. >> + # >> + # 2. When a symbol/choice is first visited, _visited is set to 1, meaning >> + # "visited, potentially part of a dependency loop". The recursive >> + # search then continues from the symbol/choice. >> + # >> + # 3. If we run into a symbol/choice X with _visited already set to 1, >> + # there's a dependency loop. The loop is found on the call stack by >> + # recording symbols while returning ("on the way back") until X is seen >> + # again. >> + # >> + # 4. Once a symbol/choice and all its dependencies (or dependents in this >> + # case) have been checked recursively without detecting any loops, its >> + # _visited is set to 2, meaning "visited, not part of a dependency >> + # loop". >> + # >> + # This saves work if we run into the symbol/choice again in later calls >> + # to _check_dep_loop_sym(). We just return immediately. >> + # >> + # Choices complicate things, as every choice symbol depends on every other >> + # choice symbol in a sense. When a choice is "entered" via a choice symbol >> + # X, we visit all choice symbols from the choice except X, and prevent >> + # immediately revisiting the choice with a flag (ignore_choice). >> + # >> + # Maybe there's a better way to handle this (different flags or the >> + # like...) >> + >> + if not sym._visited: >> + # sym._visited == 0, unvisited >> + >> + sym._visited = 1 >> + >> + for dep in sym._dependents: >> + # Choices show up in Symbol._dependents when the choice has the >> + # symbol in a 'prompt' or 'default' condition (e.g. >> + # 'default ... if SYM'). >> + # >> + # Since we aren't entering the choice via a choice symbol, all >> + # choice symbols need to be checked, hence the None. >> + loop = _check_dep_loop_choice(dep, None) \ >> + if dep.__class__ is Choice \ >> + else _check_dep_loop_sym(dep, False) >> + >> + if loop: >> + # Dependency loop found >> + return _found_dep_loop(loop, sym) >> + >> + if sym.choice and not ignore_choice: >> + loop = _check_dep_loop_choice(sym.choice, sym) >> + if loop: >> + # Dependency loop found >> + return _found_dep_loop(loop, sym) >> + >> + # The symbol is not part of a dependency loop >> + sym._visited = 2 >> + >> + # No dependency loop found >> + return None >> + >> + if sym._visited == 2: >> + # The symbol was checked earlier and is already known to not be part of >> + # a dependency loop >> + return None >> + >> + # sym._visited == 1, found a dependency loop. Return the symbol as the >> + # first element in it. >> + return (sym,) >> + >> + >> +def _check_dep_loop_choice(choice, skip): >> + if not choice._visited: >> + # choice._visited == 0, unvisited >> + >> + choice._visited = 1 >> + >> + # Check for loops involving choice symbols. If we came here via a >> + # choice symbol, skip that one, as we'd get a false positive >> + # '<sym FOO> -> <choice> -> <sym FOO>' loop otherwise. >> + for sym in choice.syms: >> + if sym is not skip: >> + # Prevent the choice from being immediately re-entered via the >> + # "is a choice symbol" path by passing True >> + loop = _check_dep_loop_sym(sym, True) >> + if loop: >> + # Dependency loop found >> + return _found_dep_loop(loop, choice) >> + >> + # The choice is not part of a dependency loop >> + choice._visited = 2 >> + >> + # No dependency loop found >> + return None >> + >> + if choice._visited == 2: >> + # The choice was checked earlier and is already known to not be part of >> + # a dependency loop >> + return None >> + >> + # choice._visited == 1, found a dependency loop. Return the choice as the >> + # first element in it. >> + return (choice,) >> + >> + >> +def _found_dep_loop(loop, cur): >> + # Called "on the way back" when we know we have a loop >> + >> + # Is the symbol/choice 'cur' where the loop started? >> + if cur is not loop[0]: >> + # Nope, it's just a part of the loop >> + return loop + (cur,) >> + >> + # Yep, we have the entire loop. Throw an exception that shows it. >> + >> + msg = "\nDependency loop\n" \ >> + "===============\n\n" >> + >> + for item in loop: >> + if item is not loop[0]: >> + msg += "...depends on " >> + if item.__class__ is Symbol and item.choice: >> + msg += "the choice symbol " >> + >> + msg += "{}, with definition...\n\n{}\n\n" \ >> + .format(item.name_and_loc, item) >> + >> + # Small wart: Since we reuse the already calculated >> + # Symbol/Choice._dependents sets for recursive dependency detection, we >> + # lose information on whether a dependency came from a 'select'/'imply' >> + # condition or e.g. a 'depends on'. >> + # >> + # This might cause selecting symbols to "disappear". For example, >> + # a symbol B having 'select A if C' gives a direct dependency from A to >> + # C, since it corresponds to a reverse dependency of B && C. >> + # >> + # Always print reverse dependencies for symbols that have them to make >> + # sure information isn't lost. I wonder if there's some neat way to >> + # improve this. >> + >> + if item.__class__ is Symbol: >> + if item.rev_dep is not item.kconfig.n: >> + msg += "(select-related dependencies: {})\n\n" \ >> + .format(expr_str(item.rev_dep)) >> + >> + if item.weak_rev_dep is not item.kconfig.n: >> + msg += "(imply-related dependencies: {})\n\n" \ >> + .format(expr_str(item.rev_dep)) >> + >> + msg += "...depends again on " + loop[0].name_and_loc >> + >> + raise KconfigError(msg) >> + >> + >> +def _decoding_error(e, filename, macro_linenr=None): >> + # Gives the filename and context for UnicodeDecodeError's, which are a pain >> + # to debug otherwise. 'e' is the UnicodeDecodeError object. >> + # >> + # If the decoding error is for the output of a $(shell,...) command, >> + # macro_linenr holds the line number where it was run (the exact line >> + # number isn't available for decoding errors in files). >> + >> + raise KconfigError( >> + "\n" >> + "Malformed {} in {}\n" >> + "Context: {}\n" >> + "Problematic data: {}\n" >> + "Reason: {}".format( >> + e.encoding, >> + "'{}'".format(filename) if macro_linenr is None else >> + "output from macro at {}:{}".format(filename, macro_linenr), >> + e.object[max(e.start - 40, 0):e.end + 40], >> + e.object[e.start:e.end], >> + e.reason)) >> + >> + >> +def _warn_verbose_deprecated(fn_name): >> + sys.stderr.write( >> + "Deprecation warning: {0}()'s 'verbose' argument has no effect. Since " >> + "Kconfiglib 12.0.0, the message is returned from {0}() instead, " >> + "and is always generated. Do e.g. print(kconf.{0}()) if you want to " >> + "want to show a message like \"Loaded configuration '.config'\" on " >> + "stdout. The old API required ugly hacks to reuse messages in " >> + "configuration interfaces.\n".format(fn_name)) >> + >> + >> +# Predefined preprocessor functions >> + >> + >> +def _filename_fn(kconf, _): >> + return kconf.filename >> + >> + >> +def _lineno_fn(kconf, _): >> + return str(kconf.linenr) >> + >> + >> +def _info_fn(kconf, _, msg): >> + print("{}:{}: {}".format(kconf.filename, kconf.linenr, msg)) >> + >> + return "" >> + >> + >> +def _warning_if_fn(kconf, _, cond, msg): >> + if cond == "y": >> + kconf._warn(msg, kconf.filename, kconf.linenr) >> + >> + return "" >> + >> + >> +def _error_if_fn(kconf, _, cond, msg): >> + if cond == "y": >> + raise KconfigError("{}:{}: {}".format( >> + kconf.filename, kconf.linenr, msg)) >> + >> + return "" >> + >> + >> +def _shell_fn(kconf, _, command): >> + import subprocess # Only import as needed, to save some startup time >> + >> + stdout, stderr = subprocess.Popen( >> + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE >> + ).communicate() >> + >> + if not _IS_PY2: >> + try: >> + stdout = stdout.decode(kconf._encoding) >> + stderr = stderr.decode(kconf._encoding) >> + except UnicodeDecodeError as e: >> + _decoding_error(e, kconf.filename, kconf.linenr) >> + >> + if stderr: >> + kconf._warn("'{}' wrote to stderr: {}".format( >> + command, "\n".join(stderr.splitlines())), >> + kconf.filename, kconf.linenr) >> + >> + # Universal newlines with splitlines() (to prevent e.g. stray \r's in >> + # command output on Windows), trailing newline removal, and >> + # newline-to-space conversion. >> + # >> + # On Python 3 versions before 3.6, it's not possible to specify the >> + # encoding when passing universal_newlines=True to Popen() (the 'encoding' >> + # parameter was added in 3.6), so we do this manual version instead. >> + return "\n".join(stdout.splitlines()).rstrip("\n").replace("\n", " ") >> + >> +# >> +# Global constants >> +# >> + >> +TRI_TO_STR = { >> + 0: "n", >> + 1: "m", >> + 2: "y", >> +} >> + >> +STR_TO_TRI = { >> + "n": 0, >> + "m": 1, >> + "y": 2, >> +} >> + >> +# Constant representing that there's no cached choice selection. This is >> +# distinct from a cached None (no selection). Any object that's not None or a >> +# Symbol will do. We test this with 'is'. >> +_NO_CACHED_SELECTION = 0 >> + >> +# Are we running on Python 2? >> +_IS_PY2 = sys.version_info[0] < 3 >> + >> +try: >> + _UNAME_RELEASE = os.uname()[2] >> +except AttributeError: >> + # Only import as needed, to save some startup time >> + import platform >> + _UNAME_RELEASE = platform.uname()[2] >> + >> +# The token and type constants below are safe to test with 'is', which is a bit >> +# faster (~30% faster on my machine, and a few % faster for total parsing >> +# time), even without assuming Python's small integer optimization (which >> +# caches small integer objects). The constants end up pointing to unique >> +# integer objects, and since we consistently refer to them via the names below, >> +# we always get the same object. >> +# >> +# Client code should use == though. >> + >> +# Tokens, with values 1, 2, ... . Avoiding 0 simplifies some checks by making >> +# all tokens except empty strings truthy. >> +( >> + _T_ALLNOCONFIG_Y, >> + _T_AND, >> + _T_BOOL, >> + _T_CHOICE, >> + _T_CLOSE_PAREN, >> + _T_COMMENT, >> + _T_CONFIG, >> + _T_DEFAULT, >> + _T_DEFCONFIG_LIST, >> + _T_DEF_BOOL, >> + _T_DEF_HEX, >> + _T_DEF_INT, >> + _T_DEF_STRING, >> + _T_DEF_TRISTATE, >> + _T_DEPENDS, >> + _T_ENDCHOICE, >> + _T_ENDIF, >> + _T_ENDMENU, >> + _T_ENV, >> + _T_EQUAL, >> + _T_GREATER, >> + _T_GREATER_EQUAL, >> + _T_HELP, >> + _T_HEX, >> + _T_IF, >> + _T_IMPLY, >> + _T_INT, >> + _T_LESS, >> + _T_LESS_EQUAL, >> + _T_MAINMENU, >> + _T_MENU, >> + _T_MENUCONFIG, >> + _T_MODULES, >> + _T_NOT, >> + _T_ON, >> + _T_OPEN_PAREN, >> + _T_OPTION, >> + _T_OPTIONAL, >> + _T_OR, >> + _T_ORSOURCE, >> + _T_OSOURCE, >> + _T_PROMPT, >> + _T_RANGE, >> + _T_RSOURCE, >> + _T_SELECT, >> + _T_SOURCE, >> + _T_STRING, >> + _T_TRISTATE, >> + _T_UNEQUAL, >> + _T_VISIBLE, >> +) = range(1, 51) >> + >> +# Keyword to token map, with the get() method assigned directly as a small >> +# optimization >> +_get_keyword = { >> + "---help---": _T_HELP, >> + "allnoconfig_y": _T_ALLNOCONFIG_Y, >> + "bool": _T_BOOL, >> + "boolean": _T_BOOL, >> + "choice": _T_CHOICE, >> + "comment": _T_COMMENT, >> + "config": _T_CONFIG, >> + "def_bool": _T_DEF_BOOL, >> + "def_hex": _T_DEF_HEX, >> + "def_int": _T_DEF_INT, >> + "def_string": _T_DEF_STRING, >> + "def_tristate": _T_DEF_TRISTATE, >> + "default": _T_DEFAULT, >> + "defconfig_list": _T_DEFCONFIG_LIST, >> + "depends": _T_DEPENDS, >> + "endchoice": _T_ENDCHOICE, >> + "endif": _T_ENDIF, >> + "endmenu": _T_ENDMENU, >> + "env": _T_ENV, >> + "grsource": _T_ORSOURCE, # Backwards compatibility >> + "gsource": _T_OSOURCE, # Backwards compatibility >> + "help": _T_HELP, >> + "hex": _T_HEX, >> + "if": _T_IF, >> + "imply": _T_IMPLY, >> + "int": _T_INT, >> + "mainmenu": _T_MAINMENU, >> + "menu": _T_MENU, >> + "menuconfig": _T_MENUCONFIG, >> + "modules": _T_MODULES, >> + "on": _T_ON, >> + "option": _T_OPTION, >> + "optional": _T_OPTIONAL, >> + "orsource": _T_ORSOURCE, >> + "osource": _T_OSOURCE, >> + "prompt": _T_PROMPT, >> + "range": _T_RANGE, >> + "rsource": _T_RSOURCE, >> + "select": _T_SELECT, >> + "source": _T_SOURCE, >> + "string": _T_STRING, >> + "tristate": _T_TRISTATE, >> + "visible": _T_VISIBLE, >> +}.get >> + >> +# The constants below match the value of the corresponding tokens to remove the >> +# need for conversion >> + >> +# Node types >> +MENU = _T_MENU >> +COMMENT = _T_COMMENT >> + >> +# Expression types >> +AND = _T_AND >> +OR = _T_OR >> +NOT = _T_NOT >> +EQUAL = _T_EQUAL >> +UNEQUAL = _T_UNEQUAL >> +LESS = _T_LESS >> +LESS_EQUAL = _T_LESS_EQUAL >> +GREATER = _T_GREATER >> +GREATER_EQUAL = _T_GREATER_EQUAL >> + >> +REL_TO_STR = { >> + EQUAL: "=", >> + UNEQUAL: "!=", >> + LESS: "<", >> + LESS_EQUAL: "<=", >> + GREATER: ">", >> + GREATER_EQUAL: ">=", >> +} >> + >> +# Symbol/choice types. UNKNOWN is 0 (falsy) to simplify some checks. >> +# Client code shouldn't rely on it though, as it was non-zero in >> +# older versions. >> +UNKNOWN = 0 >> +BOOL = _T_BOOL >> +TRISTATE = _T_TRISTATE >> +STRING = _T_STRING >> +INT = _T_INT >> +HEX = _T_HEX >> + >> +TYPE_TO_STR = { >> + UNKNOWN: "unknown", >> + BOOL: "bool", >> + TRISTATE: "tristate", >> + STRING: "string", >> + INT: "int", >> + HEX: "hex", >> +} >> + >> +# Used in comparisons. 0 means the base is inferred from the format of the >> +# string. >> +_TYPE_TO_BASE = { >> + HEX: 16, >> + INT: 10, >> + STRING: 0, >> + UNKNOWN: 0, >> +} >> + >> +# def_bool -> BOOL, etc. >> +_DEF_TOKEN_TO_TYPE = { >> + _T_DEF_BOOL: BOOL, >> + _T_DEF_HEX: HEX, >> + _T_DEF_INT: INT, >> + _T_DEF_STRING: STRING, >> + _T_DEF_TRISTATE: TRISTATE, >> +} >> + >> +# Tokens after which strings are expected. This is used to tell strings from >> +# constant symbol references during tokenization, both of which are enclosed in >> +# quotes. >> +# >> +# Identifier-like lexemes ("missing quotes") are also treated as strings after >> +# these tokens. _T_CHOICE is included to avoid symbols being registered for >> +# named choices. >> +_STRING_LEX = frozenset({ >> + _T_BOOL, >> + _T_CHOICE, >> + _T_COMMENT, >> + _T_HEX, >> + _T_INT, >> + _T_MAINMENU, >> + _T_MENU, >> + _T_ORSOURCE, >> + _T_OSOURCE, >> + _T_PROMPT, >> + _T_RSOURCE, >> + _T_SOURCE, >> + _T_STRING, >> + _T_TRISTATE, >> +}) >> + >> +# Various sets for quick membership tests. Gives a single global lookup and >> +# avoids creating temporary dicts/tuples. >> + >> +_TYPE_TOKENS = frozenset({ >> + _T_BOOL, >> + _T_TRISTATE, >> + _T_INT, >> + _T_HEX, >> + _T_STRING, >> +}) >> + >> +_SOURCE_TOKENS = frozenset({ >> + _T_SOURCE, >> + _T_RSOURCE, >> + _T_OSOURCE, >> + _T_ORSOURCE, >> +}) >> + >> +_REL_SOURCE_TOKENS = frozenset({ >> + _T_RSOURCE, >> + _T_ORSOURCE, >> +}) >> + >> +# Obligatory (non-optional) sources >> +_OBL_SOURCE_TOKENS = frozenset({ >> + _T_SOURCE, >> + _T_RSOURCE, >> +}) >> + >> +_BOOL_TRISTATE = frozenset({ >> + BOOL, >> + TRISTATE, >> +}) >> + >> +_BOOL_TRISTATE_UNKNOWN = frozenset({ >> + BOOL, >> + TRISTATE, >> + UNKNOWN, >> +}) >> + >> +_INT_HEX = frozenset({ >> + INT, >> + HEX, >> +}) >> + >> +_SYMBOL_CHOICE = frozenset({ >> + Symbol, >> + Choice, >> +}) >> + >> +_MENU_COMMENT = frozenset({ >> + MENU, >> + COMMENT, >> +}) >> + >> +_EQUAL_UNEQUAL = frozenset({ >> + EQUAL, >> + UNEQUAL, >> +}) >> + >> +_RELATIONS = frozenset({ >> + EQUAL, >> + UNEQUAL, >> + LESS, >> + LESS_EQUAL, >> + GREATER, >> + GREATER_EQUAL, >> +}) >> + >> +# Helper functions for getting compiled regular expressions, with the needed >> +# matching function returned directly as a small optimization. >> +# >> +# Use ASCII regex matching on Python 3. It's already the default on Python 2. >> + >> + >> +def _re_match(regex): >> + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).match >> + >> + >> +def _re_search(regex): >> + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).search >> + >> + >> +# Various regular expressions used during parsing >> + >> +# The initial token on a line. Also eats leading and trailing whitespace, so >> +# that we can jump straight to the next token (or to the end of the line if >> +# there is only one token). >> +# >> +# This regex will also fail to match for empty lines and comment lines. >> +# >> +# '$' is included to detect preprocessor variable assignments with macro >> +# expansions in the left-hand side. >> +_command_match = _re_match(r"\s*([A-Za-z0-9_$-]+)\s*") >> + >> +# An identifier/keyword after the first token. Also eats trailing whitespace. >> +# '$' is included to detect identifiers containing macro expansions. >> +_id_keyword_match = _re_match(r"([A-Za-z0-9_$/.-]+)\s*") >> + >> +# A fragment in the left-hand side of a preprocessor variable assignment. These >> +# are the portions between macro expansions ($(foo)). Macros are supported in >> +# the LHS (variable name). >> +_assignment_lhs_fragment_match = _re_match("[A-Za-z0-9_-]*") >> + >> +# The assignment operator and value (right-hand side) in a preprocessor >> +# variable assignment >> +_assignment_rhs_match = _re_match(r"\s*(=|:=|\+=)\s*(.*)") >> + >> +# Special characters/strings while expanding a macro ('(', ')', ',', and '$(') >> +_macro_special_search = _re_search(r"\(|\)|,|\$\(") >> + >> +# Special characters/strings while expanding a string (quotes, '\', and '$(') >> +_string_special_search = _re_search(r'"|\'|\\|\$\(') >> + >> +# Special characters/strings while expanding a symbol name. Also includes >> +# end-of-line, in case the macro is the last thing on the line. >> +_name_special_search = _re_search(r'[^A-Za-z0-9_$/.-]|\$\(|$') >> + >> +# A valid right-hand side for an assignment to a string symbol in a .config >> +# file, including escaped characters. Extracts the contents. >> +_conf_string_match = _re_match(r'"((?:[^\\"]|\\.)*)"') >> diff --git a/scripts/Kconfiglib/listnewconfig.py b/scripts/Kconfiglib/listnewconfig.py >> new file mode 100755 >> index 0000000..8276de1 >> --- /dev/null >> +++ b/scripts/Kconfiglib/listnewconfig.py >> @@ -0,0 +1,76 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Lists all user-modifiable symbols that are not given a value in the >> +configuration file. Usually, these are new symbols that have been added to the >> +Kconfig files. >> + >> +The default configuration filename is '.config'. A different filename can be >> +passed in the KCONFIG_CONFIG environment variable. >> +""" >> +from __future__ import print_function >> + >> +import argparse >> +import sys >> + >> +from kconfiglib import Kconfig, BOOL, TRISTATE, INT, HEX, STRING, TRI_TO_STR >> + >> + >> +def main(): >> + parser = argparse.ArgumentParser( >> + formatter_class=argparse.RawDescriptionHelpFormatter, >> + description=__doc__) >> + >> + parser.add_argument( >> + "--show-help", "-l", >> + action="store_true", >> + help="Show any help texts as well") >> + >> + parser.add_argument( >> + "kconfig", >> + metavar="KCONFIG", >> + nargs="?", >> + default="Kconfig", >> + help="Top-level Kconfig file (default: Kconfig)") >> + >> + args = parser.parse_args() >> + >> + kconf = Kconfig(args.kconfig, suppress_traceback=True) >> + # Make it possible to filter this message out >> + print(kconf.load_config(), file=sys.stderr) >> + >> + for sym in kconf.unique_defined_syms: >> + # Only show symbols that can be toggled. Choice symbols are a special >> + # case in that sym.assignable will be (2,) (length 1) for visible >> + # symbols in choices in y mode, but they can still be toggled by >> + # selecting some other symbol. >> + if sym.user_value is None and \ >> + (len(sym.assignable) > 1 or >> + (sym.visibility and (sym.orig_type in (INT, HEX, STRING) or >> + sym.choice))): >> + >> + # Don't reuse the 'config_string' format for bool/tristate symbols, >> + # to show n-valued symbols as 'CONFIG_FOO=n' instead of >> + # '# CONFIG_FOO is not set'. This matches the C tools. >> + if sym.orig_type in (BOOL, TRISTATE): >> + s = "{}{}={}\n".format(kconf.config_prefix, sym.name, >> + TRI_TO_STR[sym.tri_value]) >> + else: >> + s = sym.config_string >> + >> + print(s, end="") >> + if args.show_help: >> + for node in sym.nodes: >> + if node.help is not None: >> + # Indent by two spaces. textwrap.indent() is not >> + # available in Python 2 (it's 3.3+). >> + print("\n".join(" " + line >> + for line in node.help.split("\n"))) >> + break >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/menuconfig.py b/scripts/Kconfiglib/menuconfig.py >> new file mode 100755 >> index 0000000..b595a69 >> --- /dev/null >> +++ b/scripts/Kconfiglib/menuconfig.py >> @@ -0,0 +1,3282 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Overview >> +======== >> + >> +A curses-based Python 2/3 menuconfig implementation. The interface should feel >> +familiar to people used to mconf ('make menuconfig'). >> + >> +Supports the same keys as mconf, and also supports a set of keybindings >> +inspired by Vi: >> + >> + J/K : Down/Up >> + L : Enter menu/Toggle item >> + H : Leave menu >> + Ctrl-D/U: Page Down/Page Up >> + G/End : Jump to end of list >> + g/Home : Jump to beginning of list >> + >> +[Space] toggles values if possible, and enters menus otherwise. [Enter] works >> +the other way around. >> + >> +The mconf feature where pressing a key jumps to a menu entry with that >> +character in it in the current menu isn't supported. A jump-to feature for >> +jumping directly to any symbol (including invisible symbols), choice, menu or >> +comment (as in a Kconfig 'comment "Foo"') is available instead. >> + >> +A few different modes are available: >> + >> + F: Toggle show-help mode, which shows the help text of the currently selected >> + item in the window at the bottom of the menu display. This is handy when >> + browsing through options. >> + >> + C: Toggle show-name mode, which shows the symbol name before each symbol menu >> + entry >> + >> + A: Toggle show-all mode, which shows all items, including currently invisible >> + items and items that lack a prompt. Invisible items are drawn in a different >> + style to make them stand out. >> + >> + >> +Running >> +======= >> + >> +menuconfig.py can be run either as a standalone executable or by calling the >> +menuconfig() function with an existing Kconfig instance. The second option is a >> +bit inflexible in that it will still load and save .config, etc. >> + >> +When run in standalone mode, the top-level Kconfig file to load can be passed >> +as a command-line argument. With no argument, it defaults to "Kconfig". >> + >> +The KCONFIG_CONFIG environment variable specifies the .config file to load (if >> +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. >> + >> +When overwriting a configuration file, the old version is saved to >> +<filename>.old (e.g. .config.old). >> + >> +$srctree is supported through Kconfiglib. >> + >> + >> +Color schemes >> +============= >> + >> +It is possible to customize the color scheme by setting the MENUCONFIG_STYLE >> +environment variable. For example, setting it to 'aquatic' will enable an >> +alternative, less yellow, more 'make menuconfig'-like color scheme, contributed >> +by Mitja Horvat (pinkfluid). >> + >> +This is the current list of built-in styles: >> + - default classic Kconfiglib theme with a yellow accent >> + - monochrome colorless theme (uses only bold and standout) attributes, >> + this style is used if the terminal doesn't support colors >> + - aquatic blue-tinted style loosely resembling the lxdialog theme >> + >> +It is possible to customize the current style by changing colors of UI >> +elements on the screen. This is the list of elements that can be stylized: >> + >> + - path Top row in the main display, with the menu path >> + - separator Separator lines between windows. Also used for the top line >> + in the symbol information display. >> + - list List of items, e.g. the main display >> + - selection Style for the selected item >> + - inv-list Like list, but for invisible items. Used in show-all mode. >> + - inv-selection Like selection, but for invisible items. Used in show-all >> + mode. >> + - help Help text windows at the bottom of various fullscreen >> + dialogs >> + - show-help Window showing the help text in show-help mode >> + - frame Frame around dialog boxes >> + - body Body of dialog boxes >> + - edit Edit box in pop-up dialogs >> + - jump-edit Edit box in jump-to dialog >> + - text Symbol information text >> + >> +The color definition is a comma separated list of attributes: >> + >> + - fg:COLOR Set the foreground/background colors. COLOR can be one of >> + * or * the basic 16 colors (black, red, green, yellow, blue, >> + - bg:COLOR magenta, cyan, white and brighter versions, for example, >> + brightred). On terminals that support more than 8 colors, >> + you can also directly put in a color number, e.g. fg:123 >> + (hexadecimal and octal constants are accepted as well). >> + Colors outside the range -1..curses.COLORS-1 (which is >> + terminal-dependent) are ignored (with a warning). The COLOR >> + can be also specified using a RGB value in the HTML >> + notation, for example #RRGGBB. If the terminal supports >> + color changing, the color is rendered accurately. >> + Otherwise, the visually nearest color is used. >> + >> + If the background or foreground color of an element is not >> + specified, it defaults to -1, representing the default >> + terminal foreground or background color. >> + >> + Note: On some terminals a bright version of the color >> + implies bold. >> + - bold Use bold text >> + - underline Use underline text >> + - standout Standout text attribute (reverse color) >> + >> +More often than not, some UI elements share the same color definition. In such >> +cases the right value may specify an UI element from which the color definition >> +will be copied. For example, "separator=help" will apply the current color >> +definition for "help" to "separator". >> + >> +A keyword without the '=' is assumed to be a style template. The template name >> +is looked up in the built-in styles list and the style definition is expanded >> +in-place. With this, built-in styles can be used as basis for new styles. >> + >> +For example, take the aquatic theme and give it a red selection bar: >> + >> +MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red" >> + >> +If there's an error in the style definition or if a missing style is assigned >> +to, the assignment will be ignored, along with a warning being printed on >> +stderr. >> + >> +The 'default' theme is always implicitly parsed first, so the following two >> +settings have the same effect: >> + >> + MENUCONFIG_STYLE="selection=fg:white,bg:red" >> + MENUCONFIG_STYLE="default selection=fg:white,bg:red" >> + >> +If the terminal doesn't support colors, the 'monochrome' theme is used, and >> +MENUCONFIG_STYLE is ignored. The assumption is that the environment is broken >> +somehow, and that the important thing is to get something usable. >> + >> + >> +Other features >> +============== >> + >> + - Seamless terminal resizing >> + >> + - No dependencies on *nix, as the 'curses' module is in the Python standard >> + library >> + >> + - Unicode text entry >> + >> + - Improved information screen compared to mconf: >> + >> + * Expressions are split up by their top-level &&/|| operands to improve >> + readability >> + >> + * Undefined symbols in expressions are pointed out >> + >> + * Menus and comments have information displays >> + >> + * Kconfig definitions are printed >> + >> + * The include path is shown, listing the locations of the 'source' >> + statements that included the Kconfig file of the symbol (or other >> + item) >> + >> + >> +Limitations >> +=========== >> + >> +Doesn't work out of the box on Windows, but can be made to work with >> + >> + pip install windows-curses >> + >> +See the https://github.com/zephyrproject-rtos/windows-curses repository. >> +""" >> +from __future__ import print_function >> + >> +import os >> +import sys >> + >> +_IS_WINDOWS = os.name == "nt" # Are we running on Windows? >> + >> +try: >> + import curses >> +except ImportError as e: >> + if not _IS_WINDOWS: >> + raise >> + sys.exit("""\ >> +menuconfig failed to import the standard Python 'curses' library. Try >> +installing a package like windows-curses >> +(https://github.com/zephyrproject-rtos/windows-curses) by running this command >> +in cmd.exe: >> + >> + pip install windows-curses >> + >> +Starting with Kconfiglib 13.0.0, windows-curses is no longer automatically >> +installed when installing Kconfiglib via pip on Windows (because it breaks >> +installation on MSYS2). >> + >> +Exception: >> +{}: {}""".format(type(e).__name__, e)) >> + >> +import errno >> +import locale >> +import re >> +import textwrap >> + >> +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ >> + BOOL, TRISTATE, STRING, INT, HEX, \ >> + AND, OR, \ >> + expr_str, expr_value, split_expr, \ >> + standard_sc_expr_str, \ >> + TRI_TO_STR, TYPE_TO_STR, \ >> + standard_kconfig, standard_config_filename >> + >> + >> +# >> +# Configuration variables >> +# >> + >> +# If True, try to change LC_CTYPE to a UTF-8 locale if it is set to the C >> +# locale (which implies ASCII). This fixes curses Unicode I/O issues on systems >> +# with bad defaults. ncurses configures itself from the locale settings. >> +# >> +# Related PEP: https://www.python.org/dev/peps/pep-0538/ >> +_CHANGE_C_LC_CTYPE_TO_UTF8 = True >> + >> +# How many steps an implicit submenu will be indented. Implicit submenus are >> +# created when an item depends on the symbol before it. Note that symbols >> +# defined with 'menuconfig' create a separate menu instead of indenting. >> +_SUBMENU_INDENT = 4 >> + >> +# Number of steps for Page Up/Down to jump >> +_PG_JUMP = 6 >> + >> +# Height of the help window in show-help mode >> +_SHOW_HELP_HEIGHT = 8 >> + >> +# How far the cursor needs to be from the edge of the window before it starts >> +# to scroll. Used for the main menu display, the information display, the >> +# search display, and for text boxes. >> +_SCROLL_OFFSET = 5 >> + >> +# Minimum width of dialogs that ask for text input >> +_INPUT_DIALOG_MIN_WIDTH = 30 >> + >> +# Number of arrows pointing up/down to draw when a window is scrolled >> +_N_SCROLL_ARROWS = 14 >> + >> +# Lines of help text shown at the bottom of the "main" display >> +_MAIN_HELP_LINES = """ >> +[Space/Enter] Toggle/enter [ESC] Leave menu [S] Save >> +[O] Load [?] Symbol info [/] Jump to symbol >> +[F] Toggle show-help mode [C] Toggle show-name mode [A] Toggle show-all mode >> +[Q] Quit (prompts for save) [D] Save minimal config (advanced) >> +"""[1:-1].split("\n") >> + >> +# Lines of help text shown at the bottom of the information dialog >> +_INFO_HELP_LINES = """ >> +[ESC/q] Return to menu [/] Jump to symbol >> +"""[1:-1].split("\n") >> + >> +# Lines of help text shown at the bottom of the search dialog >> +_JUMP_TO_HELP_LINES = """ >> +Type text to narrow the search. Regexes are supported (via Python's 're' >> +module). The up/down cursor keys step in the list. [Enter] jumps to the >> +selected symbol. [ESC] aborts the search. Type multiple space-separated >> +strings/regexes to find entries that match all of them. Type Ctrl-F to >> +view the help of the selected item without leaving the dialog. >> +"""[1:-1].split("\n") >> + >> +# >> +# Styling >> +# >> + >> +_STYLES = { >> + "default": """ >> + path=fg:black,bg:white,bold >> + separator=fg:black,bg:yellow,bold >> + list=fg:black,bg:white >> + selection=fg:white,bg:blue,bold >> + inv-list=fg:red,bg:white >> + inv-selection=fg:red,bg:blue >> + help=path >> + show-help=list >> + frame=fg:black,bg:yellow,bold >> + body=fg:white,bg:black >> + edit=fg:white,bg:blue >> + jump-edit=edit >> + text=list >> + """, >> + >> + # This style is forced on terminals that do no support colors >> + "monochrome": """ >> + path=bold >> + separator=bold,standout >> + list= >> + selection=bold,standout >> + inv-list=bold >> + inv-selection=bold,standout >> + help=bold >> + show-help= >> + frame=bold,standout >> + body= >> + edit=standout >> + jump-edit= >> + text= >> + """, >> + >> + # Blue-tinted style loosely resembling lxdialog >> + "aquatic": """ >> + path=fg:white,bg:blue >> + separator=fg:white,bg:cyan >> + help=path >> + frame=fg:white,bg:cyan >> + body=fg:white,bg:blue >> + edit=fg:black,bg:white >> + """ >> +} >> + >> +_NAMED_COLORS = { >> + # Basic colors >> + "black": curses.COLOR_BLACK, >> + "red": curses.COLOR_RED, >> + "green": curses.COLOR_GREEN, >> + "yellow": curses.COLOR_YELLOW, >> + "blue": curses.COLOR_BLUE, >> + "magenta": curses.COLOR_MAGENTA, >> + "cyan": curses.COLOR_CYAN, >> + "white": curses.COLOR_WHITE, >> + >> + # Bright versions >> + "brightblack": curses.COLOR_BLACK + 8, >> + "brightred": curses.COLOR_RED + 8, >> + "brightgreen": curses.COLOR_GREEN + 8, >> + "brightyellow": curses.COLOR_YELLOW + 8, >> + "brightblue": curses.COLOR_BLUE + 8, >> + "brightmagenta": curses.COLOR_MAGENTA + 8, >> + "brightcyan": curses.COLOR_CYAN + 8, >> + "brightwhite": curses.COLOR_WHITE + 8, >> + >> + # Aliases >> + "purple": curses.COLOR_MAGENTA, >> + "brightpurple": curses.COLOR_MAGENTA + 8, >> +} >> + >> + >> +def _rgb_to_6cube(rgb): >> + # Converts an 888 RGB color to a 3-tuple (nice in that it's hashable) >> + # representing the closest xterm 256-color 6x6x6 color cube color. >> + # >> + # The xterm 256-color extension uses a RGB color palette with components in >> + # the range 0-5 (a 6x6x6 cube). The catch is that the mapping is nonlinear. >> + # Index 0 in the 6x6x6 cube is mapped to 0, index 1 to 95, then 135, 175, >> + # etc., in increments of 40. See the links below: >> + # >> + # https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg >> + # https://github.com/tmux/tmux/blob/master/colour.c >> + >> + # 48 is the middle ground between 0 and 95. >> + return tuple(0 if x < 48 else int(round(max(1, (x - 55)/40))) for x in rgb) >> + >> + >> +def _6cube_to_rgb(r6g6b6): >> + # Returns the 888 RGB color for a 666 xterm color cube index >> + >> + return tuple(0 if x == 0 else 40*x + 55 for x in r6g6b6) >> + >> + >> +def _rgb_to_gray(rgb): >> + # Converts an 888 RGB color to the index of an xterm 256-color grayscale >> + # color with approx. the same perceived brightness >> + >> + # Calculate the luminance (gray intensity) of the color. See >> + # https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color >> + # and >> + # https://www.w3.org/TR/AERT/#color-contrast >> + luma = 0.299*rgb[0] + 0.587*rgb[1] + 0.114*rgb[2] >> + >> + # Closest index in the grayscale palette, which starts at RGB 0x080808, >> + # with stepping 0x0A0A0A >> + index = int(round((luma - 8)/10)) >> + >> + # Clamp the index to 0-23, corresponding to 232-255 >> + return max(0, min(index, 23)) >> + >> + >> +def _gray_to_rgb(index): >> + # Convert a grayscale index to its closet single RGB component >> + >> + return 3*(10*index + 8,) # Returns a 3-tuple >> + >> + >> +# Obscure Python: We never pass a value for rgb2index, and it keeps pointing to >> +# the same dict. This avoids a global. >> +def _alloc_rgb(rgb, rgb2index={}): >> + # Initialize a new entry in the xterm palette to the given RGB color, >> + # returning its index. If the color has already been initialized, the index >> + # of the existing entry is returned. >> + # >> + # ncurses is palette-based, so we need to overwrite palette entries to make >> + # new colors. >> + # >> + # The colors from 0 to 15 are user-defined, and there's no way to query >> + # their RGB values, so we better leave them untouched. Also leave any >> + # hypothetical colors above 255 untouched (though we're unlikely to >> + # allocate that many colors anyway). >> + >> + if rgb in rgb2index: >> + return rgb2index[rgb] >> + >> + # Many terminals allow the user to customize the first 16 colors. Avoid >> + # changing their values. >> + color_index = 16 + len(rgb2index) >> + if color_index >= 256: >> + _warn("Unable to allocate new RGB color ", rgb, ". Too many colors " >> + "allocated.") >> + return 0 >> + >> + # Map each RGB component from the range 0-255 to the range 0-1000, which is >> + # what curses uses >> + curses.init_color(color_index, *(int(round(1000*x/255)) for x in rgb)) >> + rgb2index[rgb] = color_index >> + >> + return color_index >> + >> + >> +def _color_from_num(num): >> + # Returns the index of a color that looks like color 'num' in the xterm >> + # 256-color palette (but that might not be 'num', if we're redefining >> + # colors) >> + >> + # - _alloc_rgb() won't touch the first 16 colors or any (hypothetical) >> + # colors above 255, so we can always return them as-is >> + # >> + # - If the terminal doesn't support changing color definitions, or if >> + # curses.COLORS < 256, _alloc_rgb() won't touch any color, and all colors >> + # can be returned as-is >> + if num < 16 or num > 255 or not curses.can_change_color() or \ >> + curses.COLORS < 256: >> + return num >> + >> + # _alloc_rgb() might redefine colors, so emulate the xterm 256-color >> + # palette by allocating new colors instead of returning color numbers >> + # directly >> + >> + if num < 232: >> + num -= 16 >> + return _alloc_rgb(_6cube_to_rgb(((num//36)%6, (num//6)%6, num%6))) >> + >> + return _alloc_rgb(_gray_to_rgb(num - 232)) >> + >> + >> +def _color_from_rgb(rgb): >> + # Returns the index of a color matching the 888 RGB color 'rgb'. The >> + # returned color might be an ~exact match or an approximation, depending on >> + # terminal capabilities. >> + >> + # Calculates the Euclidean distance between two RGB colors >> + def dist(r1, r2): return sum((x - y)**2 for x, y in zip(r1, r2)) >> + >> + if curses.COLORS >= 256: >> + # Assume we're dealing with xterm's 256-color extension >> + >> + if curses.can_change_color(): >> + # Best case -- the terminal supports changing palette entries via >> + # curses.init_color(). Initialize an unused palette entry and >> + # return it. >> + return _alloc_rgb(rgb) >> + >> + # Second best case -- pick between the xterm 256-color extension colors >> + >> + # Closest 6-cube "color" color >> + c6 = _rgb_to_6cube(rgb) >> + # Closest gray color >> + gray = _rgb_to_gray(rgb) >> + >> + if dist(rgb, _6cube_to_rgb(c6)) < dist(rgb, _gray_to_rgb(gray)): >> + # Use the "color" color from the 6x6x6 color palette. Calculate the >> + # color number from the 6-cube index triplet. >> + return 16 + 36*c6[0] + 6*c6[1] + c6[2] >> + >> + # Use the color from the gray palette >> + return 232 + gray >> + >> + # Terminal not in xterm 256-color mode. This is probably the best we can >> + # do, or is it? Submit patches. :) >> + min_dist = float('inf') >> + best = -1 >> + for color in range(curses.COLORS): >> + # ncurses uses the range 0..1000. Scale that down to 0..255. >> + d = dist(rgb, tuple(int(round(255*c/1000)) >> + for c in curses.color_content(color))) >> + if d < min_dist: >> + min_dist = d >> + best = color >> + >> + return best >> + >> + >> +def _parse_style(style_str, parsing_default): >> + # Parses a string with '<element>=<style>' assignments. Anything not >> + # containing '=' is assumed to be a reference to a built-in style, which is >> + # treated as if all the assignments from the style were inserted at that >> + # point in the string. >> + # >> + # The parsing_default flag is set to True when we're implicitly parsing the >> + # 'default'/'monochrome' style, to prevent warnings. >> + >> + for sline in style_str.split(): >> + # Words without a "=" character represents a style template >> + if "=" in sline: >> + key, data = sline.split("=", 1) >> + >> + # The 'default' style template is assumed to define all keys. We >> + # run _style_to_curses() for non-existing keys as well, so that we >> + # print warnings for errors to the right of '=' for those too. >> + if key not in _style and not parsing_default: >> + _warn("Ignoring non-existent style", key) >> + >> + # If data is a reference to another key, copy its style >> + if data in _style: >> + _style[key] = _style[data] >> + else: >> + _style[key] = _style_to_curses(data) >> + >> + elif sline in _STYLES: >> + # Recursively parse style template. Ignore styles that don't exist, >> + # for backwards/forwards compatibility. >> + _parse_style(_STYLES[sline], parsing_default) >> + >> + else: >> + _warn("Ignoring non-existent style template", sline) >> + >> +# Dictionary mapping element types to the curses attributes used to display >> +# them >> +_style = {} >> + >> + >> +def _style_to_curses(style_def): >> + # Parses a style definition string (<element>=<style>), returning >> + # a (fg_color, bg_color, attributes) tuple. >> + >> + def parse_color(color_def): >> + color_def = color_def.split(":", 1)[1] >> + >> + # HTML format, #RRGGBB >> + if re.match("#[A-Fa-f0-9]{6}", color_def): >> + return _color_from_rgb(( >> + int(color_def[1:3], 16), >> + int(color_def[3:5], 16), >> + int(color_def[5:7], 16))) >> + >> + if color_def in _NAMED_COLORS: >> + color_num = _color_from_num(_NAMED_COLORS[color_def]) >> + else: >> + try: >> + color_num = _color_from_num(int(color_def, 0)) >> + except ValueError: >> + _warn("Ignoring color", color_def, "that's neither " >> + "predefined nor a number") >> + return -1 >> + >> + if not -1 <= color_num < curses.COLORS: >> + _warn("Ignoring color {}, which is outside the range " >> + "-1..curses.COLORS-1 (-1..{})" >> + .format(color_def, curses.COLORS - 1)) >> + return -1 >> + >> + return color_num >> + >> + fg_color = -1 >> + bg_color = -1 >> + attrs = 0 >> + >> + if style_def: >> + for field in style_def.split(","): >> + if field.startswith("fg:"): >> + fg_color = parse_color(field) >> + elif field.startswith("bg:"): >> + bg_color = parse_color(field) >> + elif field == "bold": >> + # A_BOLD tends to produce faint and hard-to-read text on the >> + # Windows console, especially with the old color scheme, before >> + # the introduction of >> + # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/ >> + attrs |= curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD >> + elif field == "standout": >> + attrs |= curses.A_STANDOUT >> + elif field == "underline": >> + attrs |= curses.A_UNDERLINE >> + else: >> + _warn("Ignoring unknown style attribute", field) >> + >> + return _style_attr(fg_color, bg_color, attrs) >> + >> + >> +def _init_styles(): >> + if curses.has_colors(): >> + try: >> + curses.use_default_colors() >> + except curses.error: >> + # Ignore errors on funky terminals that support colors but not >> + # using default colors. Worst it can do is break transparency and >> + # the like. Ran across this with the MSYS2/winpty setup in >> + # https://github.com/msys2/MINGW-packages/issues/5823, though there >> + # seems to be a lot of general brokenness there. >> + pass >> + >> + # Use the 'default' theme as the base, and add any user-defined style >> + # settings from the environment >> + _parse_style("default", True) >> + if "MENUCONFIG_STYLE" in os.environ: >> + _parse_style(os.environ["MENUCONFIG_STYLE"], False) >> + else: >> + # Force the 'monochrome' theme if the terminal doesn't support colors. >> + # MENUCONFIG_STYLE is likely to mess things up here (though any colors >> + # would be ignored), so ignore it. >> + _parse_style("monochrome", True) >> + >> + >> +# color_attribs holds the color pairs we've already created, indexed by a >> +# (<foreground color>, <background color>) tuple. >> +# >> +# Obscure Python: We never pass a value for color_attribs, and it keeps >> +# pointing to the same dict. This avoids a global. >> +def _style_attr(fg_color, bg_color, attribs, color_attribs={}): >> + # Returns an attribute with the specified foreground and background color >> + # and the attributes in 'attribs'. Reuses color pairs already created if >> + # possible, and creates a new color pair otherwise. >> + # >> + # Returns 'attribs' if colors aren't supported. >> + >> + if not curses.has_colors(): >> + return attribs >> + >> + if (fg_color, bg_color) not in color_attribs: >> + # Create new color pair. Color pair number 0 is hardcoded and cannot be >> + # changed, hence the +1s. >> + curses.init_pair(len(color_attribs) + 1, fg_color, bg_color) >> + color_attribs[(fg_color, bg_color)] = \ >> + curses.color_pair(len(color_attribs) + 1) >> + >> + return color_attribs[(fg_color, bg_color)] | attribs >> + >> + >> +# >> +# Main application >> +# >> + >> + >> +def _main(): >> + menuconfig(standard_kconfig(__doc__)) >> + >> + >> +def menuconfig(kconf): >> + """ >> + Launches the configuration interface, returning after the user exits. >> + >> + kconf: >> + Kconfig instance to be configured >> + """ >> + global _kconf >> + global _conf_filename >> + global _conf_changed >> + global _minconf_filename >> + global _show_all >> + >> + _kconf = kconf >> + >> + # Filename to save configuration to >> + _conf_filename = standard_config_filename() >> + >> + # Load existing configuration and set _conf_changed True if it is outdated >> + _conf_changed = _load_config() >> + >> + # Filename to save minimal configuration to >> + _minconf_filename = "defconfig" >> + >> + # Any visible items in the top menu? >> + _show_all = False >> + if not _shown_nodes(kconf.top_node): >> + # Nothing visible. Start in show-all mode and try again. >> + _show_all = True >> + if not _shown_nodes(kconf.top_node): >> + # Give up. The implementation relies on always having a selected >> + # node. >> + print("Empty configuration -- nothing to configure.\n" >> + "Check that environment variables are set properly.") >> + return >> + >> + # Disable warnings. They get mangled in curses mode, and we deal with >> + # errors ourselves. >> + kconf.warn = False >> + >> + try: >> + # Make curses use the locale settings specified in the environment >> + locale.setlocale(locale.LC_ALL, "") >> + except locale.Error: >> + # fall back to the default locale >> + locale.setlocale(locale.LC_ALL, "C") >> + >> + # Try to fix Unicode issues on systems with bad defaults >> + if _CHANGE_C_LC_CTYPE_TO_UTF8: >> + _change_c_lc_ctype_to_utf8() >> + >> + # Get rid of the delay between pressing ESC and jumping to the parent menu, >> + # unless the user has set ESCDELAY (see ncurses(3)). This makes the UI much >> + # smoother to work with. >> + # >> + # Note: This is strictly pretty iffy, since escape codes for e.g. cursor >> + # keys start with ESC, but I've never seen it cause problems in practice >> + # (probably because it's unlikely that the escape code for a key would get >> + # split up across read()s, at least with a terminal emulator). Please >> + # report if you run into issues. Some suitable small default value could be >> + # used here instead in that case. Maybe it's silly to not put in the >> + # smallest imperceptible delay here already, though I don't like guessing. >> + # >> + # (From a quick glance at the ncurses source code, ESCDELAY might only be >> + # relevant for mouse events there, so maybe escapes are assumed to arrive >> + # in one piece already...) >> + os.environ.setdefault("ESCDELAY", "0") >> + >> + # Enter curses mode. _menuconfig() returns a string to print on exit, after >> + # curses has been de-initialized. >> + print(curses.wrapper(_menuconfig)) >> + >> + >> +def _load_config(): >> + # Loads any existing .config file. See the Kconfig.load_config() docstring. >> + # >> + # Returns True if .config is missing or outdated. We always prompt for >> + # saving the configuration in that case. >> + >> + print(_kconf.load_config()) >> + if not os.path.exists(_conf_filename): >> + # No .config >> + return True >> + >> + return _needs_save() >> + >> + >> +def _needs_save(): >> + # Returns True if a just-loaded .config file is outdated (would get >> + # modified when saving) >> + >> + if _kconf.missing_syms: >> + # Assignments to undefined symbols in the .config >> + return True >> + >> + for sym in _kconf.unique_defined_syms: >> + if sym.user_value is None: >> + if sym.config_string: >> + # Unwritten symbol >> + return True >> + elif sym.orig_type in (BOOL, TRISTATE): >> + if sym.tri_value != sym.user_value: >> + # Written bool/tristate symbol, new value >> + return True >> + elif sym.str_value != sym.user_value: >> + # Written string/int/hex symbol, new value >> + return True >> + >> + # No need to prompt for save >> + return False >> + >> + >> +# Global variables used below: >> +# >> +# _stdscr: >> +# stdscr from curses >> +# >> +# _cur_menu: >> +# Menu node of the menu (or menuconfig symbol, or choice) currently being >> +# shown >> +# >> +# _shown: >> +# List of items in _cur_menu that are shown (ignoring scrolling). In >> +# show-all mode, this list contains all items in _cur_menu. Otherwise, it >> +# contains just the visible items. >> +# >> +# _sel_node_i: >> +# Index in _shown of the currently selected node >> +# >> +# _menu_scroll: >> +# Index in _shown of the top row of the main display >> +# >> +# _parent_screen_rows: >> +# List/stack of the row numbers that the selections in the parent menus >> +# appeared on. This is used to prevent the scrolling from jumping around >> +# when going in and out of menus. >> +# >> +# _show_help/_show_name/_show_all: >> +# If True, the corresponding mode is on. See the module docstring. >> +# >> +# _conf_filename: >> +# File to save the configuration to >> +# >> +# _minconf_filename: >> +# File to save minimal configurations to >> +# >> +# _conf_changed: >> +# True if the configuration has been changed. If False, we don't bother >> +# showing the save-and-quit dialog. >> +# >> +# We reset this to False whenever the configuration is saved explicitly >> +# from the save dialog. >> + >> + >> +def _menuconfig(stdscr): >> + # Logic for the main display, with the list of symbols, etc. >> + >> + global _stdscr >> + global _conf_filename >> + global _conf_changed >> + global _minconf_filename >> + global _show_help >> + global _show_name >> + >> + _stdscr = stdscr >> + >> + _init() >> + >> + while True: >> + _draw_main() >> + curses.doupdate() >> + >> + >> + c = _getch_compat(_menu_win) >> + >> + if c == curses.KEY_RESIZE: >> + _resize_main() >> + >> + elif c in (curses.KEY_DOWN, "j", "J"): >> + _select_next_menu_entry() >> + >> + elif c in (curses.KEY_UP, "k", "K"): >> + _select_prev_menu_entry() >> + >> + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D >> + # Keep it simple. This way we get sane behavior for small windows, >> + # etc., for free. >> + for _ in range(_PG_JUMP): >> + _select_next_menu_entry() >> + >> + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U >> + for _ in range(_PG_JUMP): >> + _select_prev_menu_entry() >> + >> + elif c in (curses.KEY_END, "G"): >> + _select_last_menu_entry() >> + >> + elif c in (curses.KEY_HOME, "g"): >> + _select_first_menu_entry() >> + >> + elif c == " ": >> + # Toggle the node if possible >> + sel_node = _shown[_sel_node_i] >> + if not _change_node(sel_node): >> + _enter_menu(sel_node) >> + >> + elif c in (curses.KEY_RIGHT, "\n", "l", "L"): >> + # Enter the node if possible >> + sel_node = _shown[_sel_node_i] >> + if not _enter_menu(sel_node): >> + _change_node(sel_node) >> + >> + elif c in ("n", "N"): >> + _set_sel_node_tri_val(0) >> + >> + elif c in ("m", "M"): >> + _set_sel_node_tri_val(1) >> + >> + elif c in ("y", "Y"): >> + _set_sel_node_tri_val(2) >> + >> + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, >> + "\x1B", "h", "H"): # \x1B = ESC >> + >> + if c == "\x1B" and _cur_menu is _kconf.top_node: >> + res = _quit_dialog() >> + if res: >> + return res >> + else: >> + _leave_menu() >> + >> + elif c in ("o", "O"): >> + _load_dialog() >> + >> + elif c in ("s", "S"): >> + filename = _save_dialog(_kconf.write_config, _conf_filename, >> + "configuration") >> + if filename: >> + _conf_filename = filename >> + _conf_changed = False >> + >> + elif c in ("d", "D"): >> + filename = _save_dialog(_kconf.write_min_config, _minconf_filename, >> + "minimal configuration") >> + if filename: >> + _minconf_filename = filename >> + >> + elif c == "/": >> + _jump_to_dialog() >> + # The terminal might have been resized while the fullscreen jump-to >> + # dialog was open >> + _resize_main() >> + >> + elif c == "?": >> + _info_dialog(_shown[_sel_node_i], False) >> + # The terminal might have been resized while the fullscreen info >> + # dialog was open >> + _resize_main() >> + >> + elif c in ("f", "F"): >> + _show_help = not _show_help >> + _set_style(_help_win, "show-help" if _show_help else "help") >> + _resize_main() >> + >> + elif c in ("c", "C"): >> + _show_name = not _show_name >> + >> + elif c in ("a", "A"): >> + _toggle_show_all() >> + >> + elif c in ("q", "Q"): >> + res = _quit_dialog() >> + if res: >> + return res >> + >> + >> +def _quit_dialog(): >> + if not _conf_changed: >> + return "No changes to save (for '{}')".format(_conf_filename) >> + >> + while True: >> + c = _key_dialog( >> + "Quit", >> + " Save configuration?\n" >> + "\n" >> + "(Y)es (N)o (C)ancel", >> + "ync") >> + >> + if c is None or c == "c": >> + return None >> + >> + if c == "y": >> + # Returns a message to print >> + msg = _try_save(_kconf.write_config, _conf_filename, "configuration") >> + if msg: >> + return msg >> + >> + elif c == "n": >> + return "Configuration ({}) was not saved".format(_conf_filename) >> + >> + >> +def _init(): >> + # Initializes the main display with the list of symbols, etc. Also does >> + # misc. global initialization that needs to happen after initializing >> + # curses. >> + >> + global _ERASE_CHAR >> + >> + global _path_win >> + global _top_sep_win >> + global _menu_win >> + global _bot_sep_win >> + global _help_win >> + >> + global _parent_screen_rows >> + global _cur_menu >> + global _shown >> + global _sel_node_i >> + global _menu_scroll >> + >> + global _show_help >> + global _show_name >> + >> + # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes >> + # backspace work with TERM=vt100. That makes it likely to work in sane >> + # environments. >> + _ERASE_CHAR = curses.erasechar() >> + if sys.version_info[0] >= 3: >> + # erasechar() returns a one-byte bytes object on Python 3. This sets >> + # _ERASE_CHAR to a blank string if it can't be decoded, which should be >> + # harmless. >> + _ERASE_CHAR = _ERASE_CHAR.decode("utf-8", "ignore") >> + >> + _init_styles() >> + >> + # Hide the cursor >> + _safe_curs_set(0) >> + >> + # Initialize windows >> + >> + # Top row, with menu path >> + _path_win = _styled_win("path") >> + >> + # Separator below menu path, with title and arrows pointing up >> + _top_sep_win = _styled_win("separator") >> + >> + # List of menu entries with symbols, etc. >> + _menu_win = _styled_win("list") >> + _menu_win.keypad(True) >> + >> + # Row below menu list, with arrows pointing down >> + _bot_sep_win = _styled_win("separator") >> + >> + # Help window with keys at the bottom. Shows help texts in show-help mode. >> + _help_win = _styled_win("help") >> + >> + # The rows we'd like the nodes in the parent menus to appear on. This >> + # prevents the scroll from jumping around when going in and out of menus. >> + _parent_screen_rows = [] >> + >> + # Initial state >> + >> + _cur_menu = _kconf.top_node >> + _shown = _shown_nodes(_cur_menu) >> + _sel_node_i = _menu_scroll = 0 >> + >> + _show_help = _show_name = False >> + >> + # Give windows their initial size >> + _resize_main() >> + >> + >> +def _resize_main(): >> + # Resizes the main display, with the list of symbols, etc., to fill the >> + # terminal >> + >> + global _menu_scroll >> + >> + screen_height, screen_width = _stdscr.getmaxyx() >> + >> + _path_win.resize(1, screen_width) >> + _top_sep_win.resize(1, screen_width) >> + _bot_sep_win.resize(1, screen_width) >> + >> + help_win_height = _SHOW_HELP_HEIGHT if _show_help else \ >> + len(_MAIN_HELP_LINES) >> + >> + menu_win_height = screen_height - help_win_height - 3 >> + >> + if menu_win_height >= 1: >> + _menu_win.resize(menu_win_height, screen_width) >> + _help_win.resize(help_win_height, screen_width) >> + >> + _top_sep_win.mvwin(1, 0) >> + _menu_win.mvwin(2, 0) >> + _bot_sep_win.mvwin(2 + menu_win_height, 0) >> + _help_win.mvwin(2 + menu_win_height + 1, 0) >> + else: >> + # Degenerate case. Give up on nice rendering and just prevent errors. >> + >> + menu_win_height = 1 >> + >> + _menu_win.resize(1, screen_width) >> + _help_win.resize(1, screen_width) >> + >> + for win in _top_sep_win, _menu_win, _bot_sep_win, _help_win: >> + win.mvwin(0, 0) >> + >> + # Adjust the scroll so that the selected node is still within the window, >> + # if needed >> + if _sel_node_i - _menu_scroll >= menu_win_height: >> + _menu_scroll = _sel_node_i - menu_win_height + 1 >> + >> + >> +def _height(win): >> + # Returns the height of 'win' >> + >> + return win.getmaxyx()[0] >> + >> + >> +def _width(win): >> + # Returns the width of 'win' >> + >> + return win.getmaxyx()[1] >> + >> + >> +def _enter_menu(menu): >> + # Makes 'menu' the currently displayed menu. In addition to actual 'menu's, >> + # "menu" here includes choices and symbols defined with the 'menuconfig' >> + # keyword. >> + # >> + # Returns False if 'menu' can't be entered. >> + >> + global _cur_menu >> + global _shown >> + global _sel_node_i >> + global _menu_scroll >> + >> + if not menu.is_menuconfig: >> + return False # Not a menu >> + >> + shown_sub = _shown_nodes(menu) >> + # Never enter empty menus. We depend on having a current node. >> + if not shown_sub: >> + return False >> + >> + # Remember where the current node appears on the screen, so we can try >> + # to get it to appear in the same place when we leave the menu >> + _parent_screen_rows.append(_sel_node_i - _menu_scroll) >> + >> + # Jump into menu >> + _cur_menu = menu >> + _shown = shown_sub >> + _sel_node_i = _menu_scroll = 0 >> + >> + if isinstance(menu.item, Choice): >> + _select_selected_choice_sym() >> + >> + return True >> + >> + >> +def _select_selected_choice_sym(): >> + # Puts the cursor on the currently selected (y-valued) choice symbol, if >> + # any. Does nothing if if the choice has no selection (is not visible/in y >> + # mode). >> + >> + global _sel_node_i >> + >> + choice = _cur_menu.item >> + if choice.selection: >> + # Search through all menu nodes to handle choice symbols being defined >> + # in multiple locations >> + for node in choice.selection.nodes: >> + if node in _shown: >> + _sel_node_i = _shown.index(node) >> + _center_vertically() >> + return >> + >> + >> +def _jump_to(node): >> + # Jumps directly to the menu node 'node' >> + >> + global _cur_menu >> + global _shown >> + global _sel_node_i >> + global _menu_scroll >> + global _show_all >> + global _parent_screen_rows >> + >> + # Clear remembered menu locations. We might not even have been in the >> + # parent menus before. >> + _parent_screen_rows = [] >> + >> + old_show_all = _show_all >> + jump_into = (isinstance(node.item, Choice) or node.item == MENU) and \ >> + node.list >> + >> + # If we're jumping to a non-empty choice or menu, jump to the first entry >> + # in it instead of jumping to its menu node >> + if jump_into: >> + _cur_menu = node >> + node = node.list >> + else: >> + _cur_menu = _parent_menu(node) >> + >> + _shown = _shown_nodes(_cur_menu) >> + if node not in _shown: >> + # The node wouldn't be shown. Turn on show-all to show it. >> + _show_all = True >> + _shown = _shown_nodes(_cur_menu) >> + >> + _sel_node_i = _shown.index(node) >> + >> + if jump_into and not old_show_all and _show_all: >> + # If we're jumping into a choice or menu and were forced to turn on >> + # show-all because the first entry wasn't visible, try turning it off. >> + # That will land us at the first visible node if there are visible >> + # nodes, and is a no-op otherwise. >> + _toggle_show_all() >> + >> + _center_vertically() >> + >> + # If we're jumping to a non-empty choice, jump to the selected symbol, if >> + # any >> + if jump_into and isinstance(_cur_menu.item, Choice): >> + _select_selected_choice_sym() >> + >> + >> +def _leave_menu(): >> + # Jumps to the parent menu of the current menu. Does nothing if we're in >> + # the top menu. >> + >> + global _cur_menu >> + global _shown >> + global _sel_node_i >> + global _menu_scroll >> + >> + if _cur_menu is _kconf.top_node: >> + return >> + >> + # Jump to parent menu >> + parent = _parent_menu(_cur_menu) >> + _shown = _shown_nodes(parent) >> + _sel_node_i = _shown.index(_cur_menu) >> + _cur_menu = parent >> + >> + # Try to make the menu entry appear on the same row on the screen as it did >> + # before we entered the menu. >> + >> + if _parent_screen_rows: >> + # The terminal might have shrunk since we were last in the parent menu >> + screen_row = min(_parent_screen_rows.pop(), _height(_menu_win) - 1) >> + _menu_scroll = max(_sel_node_i - screen_row, 0) >> + else: >> + # No saved parent menu locations, meaning we jumped directly to some >> + # node earlier >> + _center_vertically() >> + >> + >> +def _select_next_menu_entry(): >> + # Selects the menu entry after the current one, adjusting the scroll if >> + # necessary. Does nothing if we're already at the last menu entry. >> + >> + global _sel_node_i >> + global _menu_scroll >> + >> + if _sel_node_i < len(_shown) - 1: >> + # Jump to the next node >> + _sel_node_i += 1 >> + >> + # If the new node is sufficiently close to the edge of the menu window >> + # (as determined by _SCROLL_OFFSET), increase the scroll by one. This >> + # gives nice and non-jumpy behavior even when >> + # _SCROLL_OFFSET >= _height(_menu_win). >> + if _sel_node_i >= _menu_scroll + _height(_menu_win) - _SCROLL_OFFSET \ >> + and _menu_scroll < _max_scroll(_shown, _menu_win): >> + >> + _menu_scroll += 1 >> + >> + >> +def _select_prev_menu_entry(): >> + # Selects the menu entry before the current one, adjusting the scroll if >> + # necessary. Does nothing if we're already at the first menu entry. >> + >> + global _sel_node_i >> + global _menu_scroll >> + >> + if _sel_node_i > 0: >> + # Jump to the previous node >> + _sel_node_i -= 1 >> + >> + # See _select_next_menu_entry() >> + if _sel_node_i < _menu_scroll + _SCROLL_OFFSET: >> + _menu_scroll = max(_menu_scroll - 1, 0) >> + >> + >> +def _select_last_menu_entry(): >> + # Selects the last menu entry in the current menu >> + >> + global _sel_node_i >> + global _menu_scroll >> + >> + _sel_node_i = len(_shown) - 1 >> + _menu_scroll = _max_scroll(_shown, _menu_win) >> + >> + >> +def _select_first_menu_entry(): >> + # Selects the first menu entry in the current menu >> + >> + global _sel_node_i >> + global _menu_scroll >> + >> + _sel_node_i = _menu_scroll = 0 >> + >> + >> +def _toggle_show_all(): >> + # Toggles show-all mode on/off. If turning it off would give no visible >> + # items in the current menu, it is left on. >> + >> + global _show_all >> + global _shown >> + global _sel_node_i >> + global _menu_scroll >> + >> + # Row on the screen the cursor is on. Preferably we want the same row to >> + # stay highlighted. >> + old_row = _sel_node_i - _menu_scroll >> + >> + _show_all = not _show_all >> + # List of new nodes to be shown after toggling _show_all >> + new_shown = _shown_nodes(_cur_menu) >> + >> + # Find a good node to select. The selected node might disappear if show-all >> + # mode is turned off. >> + >> + # Select the previously selected node itself if it is still visible. If >> + # there are visible nodes before it, select the closest one. >> + for node in _shown[_sel_node_i::-1]: >> + if node in new_shown: >> + _sel_node_i = new_shown.index(node) >> + break >> + else: >> + # No visible nodes before the previously selected node. Select the >> + # closest visible node after it instead. >> + for node in _shown[_sel_node_i + 1:]: >> + if node in new_shown: >> + _sel_node_i = new_shown.index(node) >> + break >> + else: >> + # No visible nodes at all, meaning show-all was turned off inside >> + # an invisible menu. Don't allow that, as the implementation relies >> + # on always having a selected node. >> + _show_all = True >> + return >> + >> + _shown = new_shown >> + >> + # Try to make the cursor stay on the same row in the menu window. This >> + # might be impossible if too many nodes have disappeared above the node. >> + _menu_scroll = max(_sel_node_i - old_row, 0) >> + >> + >> +def _center_vertically(): >> + # Centers the selected node vertically, if possible >> + >> + global _menu_scroll >> + >> + _menu_scroll = min(max(_sel_node_i - _height(_menu_win)//2, 0), >> + _max_scroll(_shown, _menu_win)) >> + >> + >> +def _draw_main(): >> + # Draws the "main" display, with the list of symbols, the header, and the >> + # footer. >> + # >> + # This could be optimized to only update the windows that have actually >> + # changed, but keep it simple for now and let curses sort it out. >> + >> + term_width = _width(_stdscr) >> + >> + # >> + # Update the separator row below the menu path >> + # >> + >> + _top_sep_win.erase() >> + >> + # Draw arrows pointing up if the symbol window is scrolled down. Draw them >> + # before drawing the title, so the title ends up on top for small windows. >> + if _menu_scroll > 0: >> + _safe_hline(_top_sep_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) >> + >> + # Add the 'mainmenu' text as the title, centered at the top >> + _safe_addstr(_top_sep_win, >> + 0, max((term_width - len(_kconf.mainmenu_text))//2, 0), >> + _kconf.mainmenu_text) >> + >> + _top_sep_win.noutrefresh() >> + >> + # Note: The menu path at the top is deliberately updated last. See below. >> + >> + # >> + # Update the symbol window >> + # >> + >> + _menu_win.erase() >> + >> + # Draw the _shown nodes starting from index _menu_scroll up to either as >> + # many as fit in the window, or to the end of _shown >> + for i in range(_menu_scroll, >> + min(_menu_scroll + _height(_menu_win), len(_shown))): >> + >> + node = _shown[i] >> + >> + # The 'not _show_all' test avoids showing invisible items in red >> + # outside show-all mode, which could look confusing/broken. Invisible >> + # symbols show up outside show-all mode if an invisible symbol has >> + # visible children in an implicit (indented) menu. >> + if _visible(node) or not _show_all: >> + style = _style["selection" if i == _sel_node_i else "list"] >> + else: >> + style = _style["inv-selection" if i == _sel_node_i else "inv-list"] >> + >> + _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style) >> + >> + _menu_win.noutrefresh() >> + >> + # >> + # Update the bottom separator window >> + # >> + >> + _bot_sep_win.erase() >> + >> + # Draw arrows pointing down if the symbol window is scrolled up >> + if _menu_scroll < _max_scroll(_shown, _menu_win): >> + _safe_hline(_bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) >> + >> + # Indicate when show-name/show-help/show-all mode is enabled >> + enabled_modes = [] >> + if _show_help: >> + enabled_modes.append("show-help (toggle with [F])") >> + if _show_name: >> + enabled_modes.append("show-name") >> + if _show_all: >> + enabled_modes.append("show-all") >> + if enabled_modes: >> + s = " and ".join(enabled_modes) + " mode enabled" >> + _safe_addstr(_bot_sep_win, 0, max(term_width - len(s) - 2, 0), s) >> + >> + _bot_sep_win.noutrefresh() >> + >> + # >> + # Update the help window, which shows either key bindings or help texts >> + # >> + >> + _help_win.erase() >> + >> + if _show_help: >> + node = _shown[_sel_node_i] >> + if isinstance(node.item, (Symbol, Choice)) and node.help: >> + help_lines = textwrap.wrap(node.help, _width(_help_win)) >> + for i in range(min(_height(_help_win), len(help_lines))): >> + _safe_addstr(_help_win, i, 0, help_lines[i]) >> + else: >> + _safe_addstr(_help_win, 0, 0, "(no help)") >> + else: >> + for i, line in enumerate(_MAIN_HELP_LINES): >> + _safe_addstr(_help_win, i, 0, line) >> + >> + _help_win.noutrefresh() >> + >> + # >> + # Update the top row with the menu path. >> + # >> + # Doing this last leaves the cursor on the top row, which avoids some minor >> + # annoying jumpiness in gnome-terminal when reducing the height of the >> + # terminal. It seems to happen whenever the row with the cursor on it >> + # disappears. >> + # >> + >> + _path_win.erase() >> + >> + # Draw the menu path ("(Top) -> Menu -> Submenu -> ...") >> + >> + menu_prompts = [] >> + >> + menu = _cur_menu >> + while menu is not _kconf.top_node: >> + # Promptless choices can be entered in show-all mode. Use >> + # standard_sc_expr_str() for them, so they show up as >> + # '<choice (name if any)>'. >> + menu_prompts.append(menu.prompt[0] if menu.prompt else >> + standard_sc_expr_str(menu.item)) >> + menu = menu.parent >> + menu_prompts.append("(Top)") >> + menu_prompts.reverse() >> + >> + # Hack: We can't put ACS_RARROW directly in the string. Temporarily >> + # represent it with NULL. >> + menu_path_str = " \0 ".join(menu_prompts) >> + >> + # Scroll the menu path to the right if needed to make the current menu's >> + # title visible >> + if len(menu_path_str) > term_width: >> + menu_path_str = menu_path_str[len(menu_path_str) - term_width:] >> + >> + # Print the path with the arrows reinserted >> + split_path = menu_path_str.split("\0") >> + _safe_addstr(_path_win, split_path[0]) >> + for s in split_path[1:]: >> + _safe_addch(_path_win, curses.ACS_RARROW) >> + _safe_addstr(_path_win, s) >> + >> + _path_win.noutrefresh() >> + >> + >> +def _parent_menu(node): >> + # Returns the menu node of the menu that contains 'node'. In addition to >> + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. >> + # "Menu" here means a menu in the interface. >> + >> + menu = node.parent >> + while not menu.is_menuconfig: >> + menu = menu.parent >> + return menu >> + >> + >> +def _shown_nodes(menu): >> + # Returns the list of menu nodes from 'menu' (see _parent_menu()) that >> + # would be shown when entering it >> + >> + def rec(node): >> + res = [] >> + >> + while node: >> + if _visible(node) or _show_all: >> + res.append(node) >> + if node.list and not node.is_menuconfig: >> + # Nodes from implicit menu created from dependencies. Will >> + # be shown indented. Note that is_menuconfig is True for >> + # menus and choices as well as 'menuconfig' symbols. >> + res += rec(node.list) >> + >> + elif node.list and isinstance(node.item, Symbol): >> + # Show invisible symbols if they have visible children. This >> + # can happen for an m/y-valued symbol with an optional prompt >> + # ('prompt "foo" is COND') that is currently disabled. Note >> + # that it applies to both 'config' and 'menuconfig' symbols. >> + shown_children = rec(node.list) >> + if shown_children: >> + res.append(node) >> + if not node.is_menuconfig: >> + res += shown_children >> + >> + node = node.next >> + >> + return res >> + >> + if isinstance(menu.item, Choice): >> + # For named choices defined in multiple locations, entering the choice >> + # at a particular menu node would normally only show the choice symbols >> + # defined there (because that's what the MenuNode tree looks like). >> + # >> + # That might look confusing, and makes extending choices by defining >> + # them in multiple locations less useful. Instead, gather all the child >> + # menu nodes for all the choices whenever a choice is entered. That >> + # makes all choice symbols visible at all locations. >> + # >> + # Choices can contain non-symbol items (people do all sorts of weird >> + # stuff with them), hence the generality here. We really need to >> + # preserve the menu tree at each choice location. >> + # >> + # Note: Named choices are pretty broken in the C tools, and this is >> + # super obscure, so you probably won't find much that relies on this. >> + # This whole 'if' could be deleted if you don't care about defining >> + # choices in multiple locations to add symbols (which will still work, >> + # just with things being displayed in a way that might be unexpected). >> + >> + # Do some additional work to avoid listing choice symbols twice if all >> + # or part of the choice is copied in multiple locations (e.g. by >> + # including some Kconfig file multiple times). We give the prompts at >> + # the current location precedence. >> + seen_syms = {node.item for node in rec(menu.list) >> + if isinstance(node.item, Symbol)} >> + res = [] >> + for choice_node in menu.item.nodes: >> + for node in rec(choice_node.list): >> + # 'choice_node is menu' checks if we're dealing with the >> + # current location >> + if node.item not in seen_syms or choice_node is menu: >> + res.append(node) >> + if isinstance(node.item, Symbol): >> + seen_syms.add(node.item) >> + return res >> + >> + return rec(menu.list) >> + >> + >> +def _visible(node): >> + # Returns True if the node should appear in the menu (outside show-all >> + # mode) >> + >> + return node.prompt and expr_value(node.prompt[1]) and not \ >> + (node.item == MENU and not expr_value(node.visibility)) >> + >> + >> +def _change_node(node): >> + # Changes the value of the menu node 'node' if it is a symbol. Bools and >> + # tristates are toggled, while other symbol types pop up a text entry >> + # dialog. >> + # >> + # Returns False if the value of 'node' can't be changed. >> + >> + if not _changeable(node): >> + return False >> + >> + # sc = symbol/choice >> + sc = node.item >> + >> + if sc.orig_type in (INT, HEX, STRING): >> + s = sc.str_value >> + >> + while True: >> + s = _input_dialog( >> + "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.orig_type]), >> + s, _range_info(sc)) >> + >> + if s is None: >> + break >> + >> + if sc.orig_type in (INT, HEX): >> + s = s.strip() >> + >> + # 'make menuconfig' does this too. Hex values not starting with >> + # '0x' are accepted when loading .config files though. >> + if sc.orig_type == HEX and not s.startswith(("0x", "0X")): >> + s = "0x" + s >> + >> + if _check_valid(sc, s): >> + _set_val(sc, s) >> + break >> + >> + elif len(sc.assignable) == 1: >> + # Handles choice symbols for choices in y mode, which are a special >> + # case: .assignable can be (2,) while .tri_value is 0. >> + _set_val(sc, sc.assignable[0]) >> + >> + else: >> + # Set the symbol to the value after the current value in >> + # sc.assignable, with wrapping >> + val_index = sc.assignable.index(sc.tri_value) >> + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) >> + >> + >> + if _is_y_mode_choice_sym(sc) and not node.list: >> + # Immediately jump to the parent menu after making a choice selection, >> + # like 'make menuconfig' does, except if the menu node has children >> + # (which can happen if a symbol 'depends on' a choice symbol that >> + # immediately precedes it). >> + _leave_menu() >> + >> + >> + return True >> + >> + >> +def _changeable(node): >> + # Returns True if the value if 'node' can be changed >> + >> + sc = node.item >> + >> + if not isinstance(sc, (Symbol, Choice)): >> + return False >> + >> + # This will hit for invisible symbols, which appear in show-all mode and >> + # when an invisible symbol has visible children (which can happen e.g. for >> + # symbols with optional prompts) >> + if not (node.prompt and expr_value(node.prompt[1])): >> + return False >> + >> + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ >> + or _is_y_mode_choice_sym(sc) >> + >> + >> +def _set_sel_node_tri_val(tri_val): >> + # Sets the value of the currently selected menu entry to 'tri_val', if that >> + # value can be assigned >> + >> + sc = _shown[_sel_node_i].item >> + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: >> + _set_val(sc, tri_val) >> + >> + >> +def _set_val(sc, val): >> + # Wrapper around Symbol/Choice.set_value() for updating the menu state and >> + # _conf_changed >> + >> + global _conf_changed >> + >> + # Use the string representation of tristate values. This makes the format >> + # consistent for all symbol types. >> + if val in TRI_TO_STR: >> + val = TRI_TO_STR[val] >> + >> + if val != sc.str_value: >> + sc.set_value(val) >> + _conf_changed = True >> + >> + # Changing the value of the symbol might have changed what items in the >> + # current menu are visible. Recalculate the state. >> + _update_menu() >> + >> + >> +def _update_menu(): >> + # Updates the current menu after the value of a symbol or choice has been >> + # changed. Changing a value might change which items in the menu are >> + # visible. >> + # >> + # If possible, preserves the location of the cursor on the screen when >> + # items are added/removed above the selected item. >> + >> + global _shown >> + global _sel_node_i >> + global _menu_scroll >> + >> + # Row on the screen the cursor was on >> + old_row = _sel_node_i - _menu_scroll >> + >> + sel_node = _shown[_sel_node_i] >> + >> + # New visible nodes >> + _shown = _shown_nodes(_cur_menu) >> + >> + # New index of selected node >> + _sel_node_i = _shown.index(sel_node) >> + >> + # Try to make the cursor stay on the same row in the menu window. This >> + # might be impossible if too many nodes have disappeared above the node. >> + _menu_scroll = max(_sel_node_i - old_row, 0) >> + >> + >> +def _input_dialog(title, initial_text, info_text=None): >> + # Pops up a dialog that prompts the user for a string >> + # >> + # title: >> + # Title to display at the top of the dialog window's border >> + # >> + # initial_text: >> + # Initial text to prefill the input field with >> + # >> + # info_text: >> + # String to show next to the input field. If None, just the input field >> + # is shown. >> + >> + win = _styled_win("body") >> + win.keypad(True) >> + >> + info_lines = info_text.split("\n") if info_text else [] >> + >> + # Give the input dialog its initial size >> + _resize_input_dialog(win, title, info_lines) >> + >> + _safe_curs_set(2) >> + >> + # Input field text >> + s = initial_text >> + >> + # Cursor position >> + i = len(initial_text) >> + >> + def edit_width(): >> + return _width(win) - 4 >> + >> + # Horizontal scroll offset >> + hscroll = max(i - edit_width() + 1, 0) >> + >> + while True: >> + # Draw the "main" display with the menu, etc., so that resizing still >> + # works properly. This is like a stack of windows, only hardcoded for >> + # now. >> + _draw_main() >> + _draw_input_dialog(win, title, info_lines, s, i, hscroll) >> + curses.doupdate() >> + >> + >> + c = _getch_compat(win) >> + >> + if c == curses.KEY_RESIZE: >> + # Resize the main display too. The dialog floats above it. >> + _resize_main() >> + _resize_input_dialog(win, title, info_lines) >> + >> + elif c == "\n": >> + _safe_curs_set(0) >> + return s >> + >> + elif c == "\x1B": # \x1B = ESC >> + _safe_curs_set(0) >> + return None >> + >> + else: >> + s, i, hscroll = _edit_text(c, s, i, hscroll, edit_width()) >> + >> + >> +def _resize_input_dialog(win, title, info_lines): >> + # Resizes the input dialog to a size appropriate for the terminal size >> + >> + screen_height, screen_width = _stdscr.getmaxyx() >> + >> + win_height = 5 >> + if info_lines: >> + win_height += len(info_lines) + 1 >> + win_height = min(win_height, screen_height) >> + >> + win_width = max(_INPUT_DIALOG_MIN_WIDTH, >> + len(title) + 4, >> + *(len(line) + 4 for line in info_lines)) >> + win_width = min(win_width, screen_width) >> + >> + win.resize(win_height, win_width) >> + win.mvwin((screen_height - win_height)//2, >> + (screen_width - win_width)//2) >> + >> + >> +def _draw_input_dialog(win, title, info_lines, s, i, hscroll): >> + edit_width = _width(win) - 4 >> + >> + win.erase() >> + >> + # Note: Perhaps having a separate window for the input field would be nicer >> + visible_s = s[hscroll:hscroll + edit_width] >> + _safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)), >> + _style["edit"]) >> + >> + for linenr, line in enumerate(info_lines): >> + _safe_addstr(win, 4 + linenr, 2, line) >> + >> + # Draw the frame last so that it overwrites the body text for small windows >> + _draw_frame(win, title) >> + >> + _safe_move(win, 2, 2 + i - hscroll) >> + >> + win.noutrefresh() >> + >> + >> +def _load_dialog(): >> + # Dialog for loading a new configuration >> + >> + global _conf_changed >> + global _conf_filename >> + global _show_all >> + >> + if _conf_changed: >> + c = _key_dialog( >> + "Load", >> + "You have unsaved changes. Load new\n" >> + "configuration anyway?\n" >> + "\n" >> + " (O)K (C)ancel", >> + "oc") >> + >> + if c is None or c == "c": >> + return >> + >> + filename = _conf_filename >> + while True: >> + filename = _input_dialog("File to load", filename, _load_save_info()) >> + if filename is None: >> + return >> + >> + filename = os.path.expanduser(filename) >> + >> + if _try_load(filename): >> + _conf_filename = filename >> + _conf_changed = _needs_save() >> + >> + # Turn on show-all mode if the selected node is not visible after >> + # loading the new configuration. _shown still holds the old state. >> + if _shown[_sel_node_i] not in _shown_nodes(_cur_menu): >> + _show_all = True >> + >> + _update_menu() >> + >> + # The message dialog indirectly updates the menu display, so _msg() >> + # must be called after the new state has been initialized >> + _msg("Success", "Loaded " + filename) >> + return >> + >> + >> +def _try_load(filename): >> + # Tries to load a configuration file. Pops up an error and returns False on >> + # failure. >> + # >> + # filename: >> + # Configuration file to load >> + >> + try: >> + _kconf.load_config(filename) >> + return True >> + except EnvironmentError as e: >> + _error("Error loading '{}'\n\n{} (errno: {})" >> + .format(filename, e.strerror, errno.errorcode[e.errno])) >> + return False >> + >> + >> +def _save_dialog(save_fn, default_filename, description): >> + # Dialog for saving the current configuration >> + # >> + # save_fn: >> + # Function to call with 'filename' to save the file >> + # >> + # default_filename: >> + # Prefilled filename in the input field >> + # >> + # description: >> + # String describing the thing being saved >> + # >> + # Return value: >> + # The path to the saved file, or None if no file was saved >> + >> + filename = default_filename >> + while True: >> + filename = _input_dialog("Filename to save {} to".format(description), >> + filename, _load_save_info()) >> + if filename is None: >> + return None >> + >> + filename = os.path.expanduser(filename) >> + >> + msg = _try_save(save_fn, filename, description) >> + if msg: >> + _msg("Success", msg) >> + return filename >> + >> + >> +def _try_save(save_fn, filename, description): >> + # Tries to save a configuration file. Returns a message to print on >> + # success. >> + # >> + # save_fn: >> + # Function to call with 'filename' to save the file >> + # >> + # description: >> + # String describing the thing being saved >> + # >> + # Return value: >> + # A message to print on success, and None on failure >> + >> + try: >> + # save_fn() returns a message to print >> + return save_fn(filename) >> + except EnvironmentError as e: >> + _error("Error saving {} to '{}'\n\n{} (errno: {})" >> + .format(description, e.filename, e.strerror, >> + errno.errorcode[e.errno])) >> + return None >> + >> + >> +def _key_dialog(title, text, keys): >> + # Pops up a dialog that can be closed by pressing a key >> + # >> + # title: >> + # Title to display at the top of the dialog window's border >> + # >> + # text: >> + # Text to show in the dialog >> + # >> + # keys: >> + # List of keys that will close the dialog. Other keys (besides ESC) are >> + # ignored. The caller is responsible for providing a hint about which >> + # keys can be pressed in 'text'. >> + # >> + # Return value: >> + # The key that was pressed to close the dialog. Uppercase characters are >> + # converted to lowercase. ESC will always close the dialog, and returns >> + # None. >> + >> + win = _styled_win("body") >> + win.keypad(True) >> + >> + _resize_key_dialog(win, text) >> + >> + while True: >> + # See _input_dialog() >> + _draw_main() >> + _draw_key_dialog(win, title, text) >> + curses.doupdate() >> + >> + >> + c = _getch_compat(win) >> + >> + if c == curses.KEY_RESIZE: >> + # Resize the main display too. The dialog floats above it. >> + _resize_main() >> + _resize_key_dialog(win, text) >> + >> + elif c == "\x1B": # \x1B = ESC >> + return None >> + >> + elif isinstance(c, str): >> + c = c.lower() >> + if c in keys: >> + return c >> + >> + >> +def _resize_key_dialog(win, text): >> + # Resizes the key dialog to a size appropriate for the terminal size >> + >> + screen_height, screen_width = _stdscr.getmaxyx() >> + >> + lines = text.split("\n") >> + >> + win_height = min(len(lines) + 4, screen_height) >> + win_width = min(max(len(line) for line in lines) + 4, screen_width) >> + >> + win.resize(win_height, win_width) >> + win.mvwin((screen_height - win_height)//2, >> + (screen_width - win_width)//2) >> + >> + >> +def _draw_key_dialog(win, title, text): >> + win.erase() >> + >> + for i, line in enumerate(text.split("\n")): >> + _safe_addstr(win, 2 + i, 2, line) >> + >> + # Draw the frame last so that it overwrites the body text for small windows >> + _draw_frame(win, title) >> + >> + win.noutrefresh() >> + >> + >> +def _draw_frame(win, title): >> + # Draw a frame around the inner edges of 'win', with 'title' at the top >> + >> + win_height, win_width = win.getmaxyx() >> + >> + win.attron(_style["frame"]) >> + >> + # Draw top/bottom edge >> + _safe_hline(win, 0, 0, " ", win_width) >> + _safe_hline(win, win_height - 1, 0, " ", win_width) >> + >> + # Draw left/right edge >> + _safe_vline(win, 0, 0, " ", win_height) >> + _safe_vline(win, 0, win_width - 1, " ", win_height) >> + >> + # Draw title >> + _safe_addstr(win, 0, max((win_width - len(title))//2, 0), title) >> + >> + win.attroff(_style["frame"]) >> + >> + >> +def _jump_to_dialog(): >> + # Implements the jump-to dialog, where symbols can be looked up via >> + # incremental search and jumped to. >> + # >> + # Returns True if the user jumped to a symbol, and False if the dialog was >> + # canceled. >> + >> + s = "" # Search text >> + prev_s = None # Previous search text >> + s_i = 0 # Search text cursor position >> + hscroll = 0 # Horizontal scroll offset >> + >> + sel_node_i = 0 # Index of selected row >> + scroll = 0 # Index in 'matches' of the top row of the list >> + >> + # Edit box at the top >> + edit_box = _styled_win("jump-edit") >> + edit_box.keypad(True) >> + >> + # List of matches >> + matches_win = _styled_win("list") >> + >> + # Bottom separator, with arrows pointing down >> + bot_sep_win = _styled_win("separator") >> + >> + # Help window with instructions at the bottom >> + help_win = _styled_win("help") >> + >> + # Give windows their initial size >> + _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, >> + sel_node_i, scroll) >> + >> + _safe_curs_set(2) >> + >> + # Logic duplication with _select_{next,prev}_menu_entry(), except we do a >> + # functional variant that returns the new (sel_node_i, scroll) values to >> + # avoid 'nonlocal'. TODO: Can this be factored out in some nice way? >> + >> + def select_next_match(): >> + if sel_node_i == len(matches) - 1: >> + return sel_node_i, scroll >> + >> + if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \ >> + and scroll < _max_scroll(matches, matches_win): >> + >> + return sel_node_i + 1, scroll + 1 >> + >> + return sel_node_i + 1, scroll >> + >> + def select_prev_match(): >> + if sel_node_i == 0: >> + return sel_node_i, scroll >> + >> + if sel_node_i - 1 < scroll + _SCROLL_OFFSET: >> + return sel_node_i - 1, max(scroll - 1, 0) >> + >> + return sel_node_i - 1, scroll >> + >> + while True: >> + if s != prev_s: >> + # The search text changed. Find new matching nodes. >> + >> + prev_s = s >> + >> + try: >> + # We could use re.IGNORECASE here instead of lower(), but this >> + # is noticeably less jerky while inputting regexes like >> + # '.*debug$' (though the '.*' is redundant there). Those >> + # probably have bad interactions with re.search(), which >> + # matches anywhere in the string. >> + # >> + # It's not horrible either way. Just a bit smoother. >> + regex_searches = [re.compile(regex).search >> + for regex in s.lower().split()] >> + >> + # No exception thrown, so the regexes are okay >> + bad_re = None >> + >> + # List of matching nodes >> + matches = [] >> + add_match = matches.append >> + >> + # Search symbols and choices >> + >> + for node in _sorted_sc_nodes(): >> + # Symbol/choice >> + sc = node.item >> + >> + for search in regex_searches: >> + # Both the name and the prompt might be missing, since >> + # we're searching both symbols and choices >> + >> + # Does the regex match either the symbol name or the >> + # prompt (if any)? >> + if not (sc.name and search(sc.name.lower()) or >> + node.prompt and search(node.prompt[0].lower())): >> + >> + # Give up on the first regex that doesn't match, to >> + # speed things up a bit when multiple regexes are >> + # entered >> + break >> + >> + else: >> + add_match(node) >> + >> + # Search menus and comments >> + >> + for node in _sorted_menu_comment_nodes(): >> + for search in regex_searches: >> + if not search(node.prompt[0].lower()): >> + break >> + else: >> + add_match(node) >> + >> + except re.error as e: >> + # Bad regex. Remember the error message so we can show it. >> + bad_re = "Bad regular expression" >> + # re.error.msg was added in Python 3.5 >> + if hasattr(e, "msg"): >> + bad_re += ": " + e.msg >> + >> + matches = [] >> + >> + # Reset scroll and jump to the top of the list of matches >> + sel_node_i = scroll = 0 >> + >> + _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, >> + s, s_i, hscroll, >> + bad_re, matches, sel_node_i, scroll) >> + curses.doupdate() >> + >> + >> + c = _getch_compat(edit_box) >> + >> + if c == "\n": >> + if matches: >> + _jump_to(matches[sel_node_i]) >> + _safe_curs_set(0) >> + return True >> + >> + elif c == "\x1B": # \x1B = ESC >> + _safe_curs_set(0) >> + return False >> + >> + elif c == curses.KEY_RESIZE: >> + # We adjust the scroll so that the selected node stays visible in >> + # the list when the terminal is resized, hence the 'scroll' >> + # assignment >> + scroll = _resize_jump_to_dialog( >> + edit_box, matches_win, bot_sep_win, help_win, >> + sel_node_i, scroll) >> + >> + elif c == "\x06": # \x06 = Ctrl-F >> + if matches: >> + _safe_curs_set(0) >> + _info_dialog(matches[sel_node_i], True) >> + _safe_curs_set(2) >> + >> + scroll = _resize_jump_to_dialog( >> + edit_box, matches_win, bot_sep_win, help_win, >> + sel_node_i, scroll) >> + >> + elif c == curses.KEY_DOWN: >> + sel_node_i, scroll = select_next_match() >> + >> + elif c == curses.KEY_UP: >> + sel_node_i, scroll = select_prev_match() >> + >> + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D >> + # Keep it simple. This way we get sane behavior for small windows, >> + # etc., for free. >> + for _ in range(_PG_JUMP): >> + sel_node_i, scroll = select_next_match() >> + >> + # Page Up (no Ctrl-U, as it's already used by the edit box) >> + elif c == curses.KEY_PPAGE: >> + for _ in range(_PG_JUMP): >> + sel_node_i, scroll = select_prev_match() >> + >> + elif c == curses.KEY_END: >> + sel_node_i = len(matches) - 1 >> + scroll = _max_scroll(matches, matches_win) >> + >> + elif c == curses.KEY_HOME: >> + sel_node_i = scroll = 0 >> + >> + else: >> + s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, >> + _width(edit_box) - 2) >> + >> + >> +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing >> +# to the same list. This avoids a global. >> +def _sorted_sc_nodes(cached_nodes=[]): >> + # Returns a sorted list of symbol and choice nodes to search. The symbol >> + # nodes appear first, sorted by name, and then the choice nodes, sorted by >> + # prompt and (secondarily) name. >> + >> + if not cached_nodes: >> + # Add symbol nodes >> + for sym in sorted(_kconf.unique_defined_syms, >> + key=lambda sym: sym.name): >> + # += is in-place for lists >> + cached_nodes += sym.nodes >> + >> + # Add choice nodes >> + >> + choices = sorted(_kconf.unique_choices, >> + key=lambda choice: choice.name or "") >> + >> + cached_nodes += sorted( >> + [node for choice in choices for node in choice.nodes], >> + key=lambda node: node.prompt[0] if node.prompt else "") >> + >> + return cached_nodes >> + >> + >> +def _sorted_menu_comment_nodes(cached_nodes=[]): >> + # Returns a list of menu and comment nodes to search, sorted by prompt, >> + # with the menus first >> + >> + if not cached_nodes: >> + def prompt_text(mc): >> + return mc.prompt[0] >> + >> + cached_nodes += sorted(_kconf.menus, key=prompt_text) >> + cached_nodes += sorted(_kconf.comments, key=prompt_text) >> + >> + return cached_nodes >> + >> + >> +def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, >> + sel_node_i, scroll): >> + # Resizes the jump-to dialog to fill the terminal. >> + # >> + # Returns the new scroll index. We adjust the scroll if needed so that the >> + # selected node stays visible. >> + >> + screen_height, screen_width = _stdscr.getmaxyx() >> + >> + bot_sep_win.resize(1, screen_width) >> + >> + help_win_height = len(_JUMP_TO_HELP_LINES) >> + matches_win_height = screen_height - help_win_height - 4 >> + >> + if matches_win_height >= 1: >> + edit_box.resize(3, screen_width) >> + matches_win.resize(matches_win_height, screen_width) >> + help_win.resize(help_win_height, screen_width) >> + >> + matches_win.mvwin(3, 0) >> + bot_sep_win.mvwin(3 + matches_win_height, 0) >> + help_win.mvwin(3 + matches_win_height + 1, 0) >> + else: >> + # Degenerate case. Give up on nice rendering and just prevent errors. >> + >> + matches_win_height = 1 >> + >> + edit_box.resize(screen_height, screen_width) >> + matches_win.resize(1, screen_width) >> + help_win.resize(1, screen_width) >> + >> + for win in matches_win, bot_sep_win, help_win: >> + win.mvwin(0, 0) >> + >> + # Adjust the scroll so that the selected row is still within the window, if >> + # needed >> + if sel_node_i - scroll >= matches_win_height: >> + return sel_node_i - matches_win_height + 1 >> + return scroll >> + >> + >> +def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, >> + s, s_i, hscroll, >> + bad_re, matches, sel_node_i, scroll): >> + >> + edit_width = _width(edit_box) - 2 >> + >> + # >> + # Update list of matches >> + # >> + >> + matches_win.erase() >> + >> + if matches: >> + for i in range(scroll, >> + min(scroll + _height(matches_win), len(matches))): >> + >> + node = matches[i] >> + >> + if isinstance(node.item, (Symbol, Choice)): >> + node_str = _name_and_val_str(node.item) >> + if node.prompt: >> + node_str += ' "{}"'.format(node.prompt[0]) >> + elif node.item == MENU: >> + node_str = 'menu "{}"'.format(node.prompt[0]) >> + else: # node.item == COMMENT >> + node_str = 'comment "{}"'.format(node.prompt[0]) >> + >> + _safe_addstr(matches_win, i - scroll, 0, node_str, >> + _style["selection" if i == sel_node_i else "list"]) >> + >> + else: >> + # bad_re holds the error message from the re.error exception on errors >> + _safe_addstr(matches_win, 0, 0, bad_re or "No matches") >> + >> + matches_win.noutrefresh() >> + >> + # >> + # Update bottom separator line >> + # >> + >> + bot_sep_win.erase() >> + >> + # Draw arrows pointing down if the symbol list is scrolled up >> + if scroll < _max_scroll(matches, matches_win): >> + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) >> + >> + bot_sep_win.noutrefresh() >> + >> + # >> + # Update help window at bottom >> + # >> + >> + help_win.erase() >> + >> + for i, line in enumerate(_JUMP_TO_HELP_LINES): >> + _safe_addstr(help_win, i, 0, line) >> + >> + help_win.noutrefresh() >> + >> + # >> + # Update edit box. We do this last since it makes it handy to position the >> + # cursor. >> + # >> + >> + edit_box.erase() >> + >> + _draw_frame(edit_box, "Jump to symbol/choice/menu/comment") >> + >> + # Draw arrows pointing up if the symbol list is scrolled down >> + if scroll > 0: >> + # TODO: Bit ugly that _style["frame"] is repeated here >> + _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS, >> + _style["frame"]) >> + >> + visible_s = s[hscroll:hscroll + edit_width] >> + _safe_addstr(edit_box, 1, 1, visible_s) >> + >> + _safe_move(edit_box, 1, 1 + s_i - hscroll) >> + >> + edit_box.noutrefresh() >> + >> + >> +def _info_dialog(node, from_jump_to_dialog): >> + # Shows a fullscreen window with information about 'node'. >> + # >> + # If 'from_jump_to_dialog' is True, the information dialog was opened from >> + # within the jump-to-dialog. In this case, we make '/' from within the >> + # information dialog just return, to avoid a confusing recursive invocation >> + # of the jump-to-dialog. >> + >> + # Top row, with title and arrows point up >> + top_line_win = _styled_win("separator") >> + >> + # Text display >> + text_win = _styled_win("text") >> + text_win.keypad(True) >> + >> + # Bottom separator, with arrows pointing down >> + bot_sep_win = _styled_win("separator") >> + >> + # Help window with keys at the bottom >> + help_win = _styled_win("help") >> + >> + # Give windows their initial size >> + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) >> + >> + >> + # Get lines of help text >> + lines = _info_str(node).split("\n") >> + >> + # Index of first row in 'lines' to show >> + scroll = 0 >> + >> + while True: >> + _draw_info_dialog(node, lines, scroll, top_line_win, text_win, >> + bot_sep_win, help_win) >> + curses.doupdate() >> + >> + >> + c = _getch_compat(text_win) >> + >> + if c == curses.KEY_RESIZE: >> + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) >> + >> + elif c in (curses.KEY_DOWN, "j", "J"): >> + if scroll < _max_scroll(lines, text_win): >> + scroll += 1 >> + >> + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D >> + scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win)) >> + >> + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U >> + scroll = max(scroll - _PG_JUMP, 0) >> + >> + elif c in (curses.KEY_END, "G"): >> + scroll = _max_scroll(lines, text_win) >> + >> + elif c in (curses.KEY_HOME, "g"): >> + scroll = 0 >> + >> + elif c in (curses.KEY_UP, "k", "K"): >> + if scroll > 0: >> + scroll -= 1 >> + >> + elif c == "/": >> + # Support starting a search from within the information dialog >> + >> + if from_jump_to_dialog: >> + return # Avoid recursion >> + >> + if _jump_to_dialog(): >> + return # Jumped to a symbol. Cancel the information dialog. >> + >> + # Stay in the information dialog if the jump-to dialog was >> + # canceled. Resize it in case the terminal was resized while the >> + # fullscreen jump-to dialog was open. >> + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) >> + >> + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, >> + "\x1B", # \x1B = ESC >> + "q", "Q", "h", "H"): >> + >> + return >> + >> + >> +def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): >> + # Resizes the info dialog to fill the terminal >> + >> + screen_height, screen_width = _stdscr.getmaxyx() >> + >> + top_line_win.resize(1, screen_width) >> + bot_sep_win.resize(1, screen_width) >> + >> + help_win_height = len(_INFO_HELP_LINES) >> + text_win_height = screen_height - help_win_height - 2 >> + >> + if text_win_height >= 1: >> + text_win.resize(text_win_height, screen_width) >> + help_win.resize(help_win_height, screen_width) >> + >> + text_win.mvwin(1, 0) >> + bot_sep_win.mvwin(1 + text_win_height, 0) >> + help_win.mvwin(1 + text_win_height + 1, 0) >> + else: >> + # Degenerate case. Give up on nice rendering and just prevent errors. >> + >> + text_win.resize(1, screen_width) >> + help_win.resize(1, screen_width) >> + >> + for win in text_win, bot_sep_win, help_win: >> + win.mvwin(0, 0) >> + >> + >> +def _draw_info_dialog(node, lines, scroll, top_line_win, text_win, >> + bot_sep_win, help_win): >> + >> + text_win_height, text_win_width = text_win.getmaxyx() >> + >> + # Note: The top row is deliberately updated last. See _draw_main(). >> + >> + # >> + # Update text display >> + # >> + >> + text_win.erase() >> + >> + for i, line in enumerate(lines[scroll:scroll + text_win_height]): >> + _safe_addstr(text_win, i, 0, line) >> + >> + text_win.noutrefresh() >> + >> + # >> + # Update bottom separator line >> + # >> + >> + bot_sep_win.erase() >> + >> + # Draw arrows pointing down if the symbol window is scrolled up >> + if scroll < _max_scroll(lines, text_win): >> + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) >> + >> + bot_sep_win.noutrefresh() >> + >> + # >> + # Update help window at bottom >> + # >> + >> + help_win.erase() >> + >> + for i, line in enumerate(_INFO_HELP_LINES): >> + _safe_addstr(help_win, i, 0, line) >> + >> + help_win.noutrefresh() >> + >> + # >> + # Update top row >> + # >> + >> + top_line_win.erase() >> + >> + # Draw arrows pointing up if the information window is scrolled down. Draw >> + # them before drawing the title, so the title ends up on top for small >> + # windows. >> + if scroll > 0: >> + _safe_hline(top_line_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) >> + >> + title = ("Symbol" if isinstance(node.item, Symbol) else >> + "Choice" if isinstance(node.item, Choice) else >> + "Menu" if node.item == MENU else >> + "Comment") + " information" >> + _safe_addstr(top_line_win, 0, max((text_win_width - len(title))//2, 0), >> + title) >> + >> + top_line_win.noutrefresh() >> + >> + >> +def _info_str(node): >> + # Returns information about the menu node 'node' as a string. >> + # >> + # The helper functions are responsible for adding newlines. This allows >> + # them to return "" if they don't want to add any output. >> + >> + if isinstance(node.item, Symbol): >> + sym = node.item >> + >> + return ( >> + _name_info(sym) + >> + _prompt_info(sym) + >> + "Type: {}\n".format(TYPE_TO_STR[sym.type]) + >> + _value_info(sym) + >> + _help_info(sym) + >> + _direct_dep_info(sym) + >> + _defaults_info(sym) + >> + _select_imply_info(sym) + >> + _kconfig_def_info(sym) >> + ) >> + >> + if isinstance(node.item, Choice): >> + choice = node.item >> + >> + return ( >> + _name_info(choice) + >> + _prompt_info(choice) + >> + "Type: {}\n".format(TYPE_TO_STR[choice.type]) + >> + 'Mode: {}\n'.format(choice.str_value) + >> + _help_info(choice) + >> + _choice_syms_info(choice) + >> + _direct_dep_info(choice) + >> + _defaults_info(choice) + >> + _kconfig_def_info(choice) >> + ) >> + >> + return _kconfig_def_info(node) # node.item in (MENU, COMMENT) >> + >> + >> +def _name_info(sc): >> + # Returns a string with the name of the symbol/choice. Names are optional >> + # for choices. >> + >> + return "Name: {}\n".format(sc.name) if sc.name else "" >> + >> + >> +def _prompt_info(sc): >> + # Returns a string listing the prompts of 'sc' (Symbol or Choice) >> + >> + s = "" >> + >> + for node in sc.nodes: >> + if node.prompt: >> + s += "Prompt: {}\n".format(node.prompt[0]) >> + >> + return s >> + >> + >> +def _value_info(sym): >> + # Returns a string showing 'sym's value >> + >> + # Only put quotes around the value for string symbols >> + return "Value: {}\n".format( >> + '"{}"'.format(sym.str_value) >> + if sym.orig_type == STRING >> + else sym.str_value) >> + >> + >> +def _choice_syms_info(choice): >> + # Returns a string listing the choice symbols in 'choice'. Adds >> + # "(selected)" next to the selected one. >> + >> + s = "Choice symbols:\n" >> + >> + for sym in choice.syms: >> + s += " - " + sym.name >> + if sym is choice.selection: >> + s += " (selected)" >> + s += "\n" >> + >> + return s + "\n" >> + >> + >> +def _help_info(sc): >> + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). >> + # Symbols and choices defined in multiple locations can have multiple help >> + # texts. >> + >> + s = "\n" >> + >> + for node in sc.nodes: >> + if node.help is not None: >> + s += "Help:\n\n{}\n\n".format(_indent(node.help, 2)) >> + >> + return s >> + >> + >> +def _direct_dep_info(sc): >> + # Returns a string describing the direct dependencies of 'sc' (Symbol or >> + # Choice). The direct dependencies are the OR of the dependencies from each >> + # definition location. The dependencies at each definition location come >> + # from 'depends on' and dependencies inherited from parent items. >> + >> + return "" if sc.direct_dep is _kconf.y else \ >> + 'Direct dependencies (={}):\n{}\n' \ >> + .format(TRI_TO_STR[expr_value(sc.direct_dep)], >> + _split_expr_info(sc.direct_dep, 2)) >> + >> + >> +def _defaults_info(sc): >> + # Returns a string describing the defaults of 'sc' (Symbol or Choice) >> + >> + if not sc.defaults: >> + return "" >> + >> + s = "Default" >> + if len(sc.defaults) > 1: >> + s += "s" >> + s += ":\n" >> + >> + for val, cond in sc.orig_defaults: >> + s += " - " >> + if isinstance(sc, Symbol): >> + s += _expr_str(val) >> + >> + # Skip the tristate value hint if the expression is just a single >> + # symbol. _expr_str() already shows its value as a string. >> + # >> + # This also avoids showing the tristate value for string/int/hex >> + # defaults, which wouldn't make any sense. >> + if isinstance(val, tuple): >> + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) >> + else: >> + # Don't print the value next to the symbol name for choice >> + # defaults, as it looks a bit confusing >> + s += val.name >> + s += "\n" >> + >> + if cond is not _kconf.y: >> + s += " Condition (={}):\n{}" \ >> + .format(TRI_TO_STR[expr_value(cond)], >> + _split_expr_info(cond, 4)) >> + >> + return s + "\n" >> + >> + >> +def _split_expr_info(expr, indent): >> + # Returns a string with 'expr' split into its top-level && or || operands, >> + # with one operand per line, together with the operand's value. This is >> + # usually enough to get something readable for long expressions. A fancier >> + # recursive thingy would be possible too. >> + # >> + # indent: >> + # Number of leading spaces to add before the split expression. >> + >> + if len(split_expr(expr, AND)) > 1: >> + split_op = AND >> + op_str = "&&" >> + else: >> + split_op = OR >> + op_str = "||" >> + >> + s = "" >> + for i, term in enumerate(split_expr(expr, split_op)): >> + s += "{}{} {}".format(indent*" ", >> + " " if i == 0 else op_str, >> + _expr_str(term)) >> + >> + # Don't bother showing the value hint if the expression is just a >> + # single symbol. _expr_str() already shows its value. >> + if isinstance(term, tuple): >> + s += " (={})".format(TRI_TO_STR[expr_value(term)]) >> + >> + s += "\n" >> + >> + return s >> + >> + >> +def _select_imply_info(sym): >> + # Returns a string with information about which symbols 'select' or 'imply' >> + # 'sym'. The selecting/implying symbols are grouped according to which >> + # value they select/imply 'sym' to (n/m/y). >> + >> + def sis(expr, val, title): >> + # sis = selects/implies >> + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] >> + if not sis: >> + return "" >> + >> + res = title >> + for si in sis: >> + res += " - {}\n".format(split_expr(si, AND)[0].name) >> + return res + "\n" >> + >> + s = "" >> + >> + if sym.rev_dep is not _kconf.n: >> + s += sis(sym.rev_dep, 2, >> + "Symbols currently y-selecting this symbol:\n") >> + s += sis(sym.rev_dep, 1, >> + "Symbols currently m-selecting this symbol:\n") >> + s += sis(sym.rev_dep, 0, >> + "Symbols currently n-selecting this symbol (no effect):\n") >> + >> + if sym.weak_rev_dep is not _kconf.n: >> + s += sis(sym.weak_rev_dep, 2, >> + "Symbols currently y-implying this symbol:\n") >> + s += sis(sym.weak_rev_dep, 1, >> + "Symbols currently m-implying this symbol:\n") >> + s += sis(sym.weak_rev_dep, 0, >> + "Symbols currently n-implying this symbol (no effect):\n") >> + >> + return s >> + >> + >> +def _kconfig_def_info(item): >> + # Returns a string with the definition of 'item' in Kconfig syntax, >> + # together with the definition location(s) and their include and menu paths >> + >> + nodes = [item] if isinstance(item, MenuNode) else item.nodes >> + >> + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ >> + .format("s" if len(nodes) > 1 else "") >> + s += (len(s) - 1)*"=" >> + >> + for node in nodes: >> + s += "\n\n" \ >> + "At {}:{}\n" \ >> + "{}" \ >> + "Menu path: {}\n\n" \ >> + "{}" \ >> + .format(node.filename, node.linenr, >> + _include_path_info(node), >> + _menu_path_info(node), >> + _indent(node.custom_str(_name_and_val_str), 2)) >> + >> + return s >> + >> + >> +def _include_path_info(node): >> + if not node.include_path: >> + # In the top-level Kconfig file >> + return "" >> + >> + return "Included via {}\n".format( >> + " -> ".join("{}:{}".format(filename, linenr) >> + for filename, linenr in node.include_path)) >> + >> + >> +def _menu_path_info(node): >> + # Returns a string describing the menu path leading up to 'node' >> + >> + path = "" >> + >> + while node.parent is not _kconf.top_node: >> + node = node.parent >> + >> + # Promptless choices might appear among the parents. Use >> + # standard_sc_expr_str() for them, so that they show up as >> + # '<choice (name if any)>'. >> + path = " -> " + (node.prompt[0] if node.prompt else >> + standard_sc_expr_str(node.item)) + path >> + >> + return "(Top)" + path >> + >> + >> +def _indent(s, n): >> + # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not >> + # available in Python 2 (it's 3.3+). >> + >> + return "\n".join(n*" " + line for line in s.split("\n")) >> + >> + >> +def _name_and_val_str(sc): >> + # Custom symbol/choice printer that shows symbol values after symbols >> + >> + # Show the values of non-constant (non-quoted) symbols that don't look like >> + # numbers. Things like 123 are actually symbol references, and only work as >> + # expected due to undefined symbols getting their name as their value. >> + # Showing the symbol value for those isn't helpful though. >> + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): >> + if not sc.nodes: >> + # Undefined symbol reference >> + return "{}(undefined/n)".format(sc.name) >> + >> + return '{}(={})'.format(sc.name, sc.str_value) >> + >> + # For other items, use the standard format >> + return standard_sc_expr_str(sc) >> + >> + >> +def _expr_str(expr): >> + # Custom expression printer that shows symbol values >> + return expr_str(expr, _name_and_val_str) >> + >> + >> +def _styled_win(style): >> + # Returns a new curses window with style 'style' and space as the fill >> + # character. The initial dimensions are (1, 1), so the window needs to be >> + # sized and positioned separately. >> + >> + win = curses.newwin(1, 1) >> + _set_style(win, style) >> + return win >> + >> + >> +def _set_style(win, style): >> + # Changes the style of an existing window >> + >> + win.bkgdset(" ", _style[style]) >> + >> + >> +def _max_scroll(lst, win): >> + # Assuming 'lst' is a list of items to be displayed in 'win', >> + # returns the maximum number of steps 'win' can be scrolled down. >> + # We stop scrolling when the bottom item is visible. >> + >> + return max(0, len(lst) - _height(win)) >> + >> + >> +def _edit_text(c, s, i, hscroll, width): >> + # Implements text editing commands for edit boxes. Takes a character (which >> + # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns >> + # the new state after the character has been processed. >> + # >> + # c: >> + # Character from user >> + # >> + # s: >> + # Current contents of string >> + # >> + # i: >> + # Current cursor index in string >> + # >> + # hscroll: >> + # Index in s of the leftmost character in the edit box, for horizontal >> + # scrolling >> + # >> + # width: >> + # Width in characters of the edit box >> + # >> + # Return value: >> + # An (s, i, hscroll) tuple for the new state >> + >> + if c == curses.KEY_LEFT: >> + if i > 0: >> + i -= 1 >> + >> + elif c == curses.KEY_RIGHT: >> + if i < len(s): >> + i += 1 >> + >> + elif c in (curses.KEY_HOME, "\x01"): # \x01 = CTRL-A >> + i = 0 >> + >> + elif c in (curses.KEY_END, "\x05"): # \x05 = CTRL-E >> + i = len(s) >> + >> + elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR): >> + if i > 0: >> + s = s[:i-1] + s[i:] >> + i -= 1 >> + >> + elif c == curses.KEY_DC: >> + s = s[:i] + s[i+1:] >> + >> + elif c == "\x17": # \x17 = CTRL-W >> + # The \W removes characters like ',' one at a time >> + new_i = re.search(r"(?:\w*|\W)\s*$", s[:i]).start() >> + s = s[:new_i] + s[i:] >> + i = new_i >> + >> + elif c == "\x0B": # \x0B = CTRL-K >> + s = s[:i] >> + >> + elif c == "\x15": # \x15 = CTRL-U >> + s = s[i:] >> + i = 0 >> + >> + elif isinstance(c, str): >> + # Insert character >> + s = s[:i] + c + s[i:] >> + i += 1 >> + >> + # Adjust the horizontal scroll so that the cursor never touches the left or >> + # right edges of the edit box, except when it's at the beginning or the end >> + # of the string >> + if i < hscroll + _SCROLL_OFFSET: >> + hscroll = max(i - _SCROLL_OFFSET, 0) >> + elif i >= hscroll + width - _SCROLL_OFFSET: >> + max_scroll = max(len(s) - width + 1, 0) >> + hscroll = min(i - width + _SCROLL_OFFSET + 1, max_scroll) >> + >> + return s, i, hscroll >> + >> + >> +def _load_save_info(): >> + # Returns an information string for load/save dialog boxes >> + >> + return "(Relative to {})\n\nRefer to your home directory with ~" \ >> + .format(os.path.join(os.getcwd(), "")) >> + >> + >> +def _msg(title, text): >> + # Pops up a message dialog that can be dismissed with Space/Enter/ESC >> + >> + _key_dialog(title, text, " \n") >> + >> + >> +def _error(text): >> + # Pops up an error dialog that can be dismissed with Space/Enter/ESC >> + >> + _msg("Error", text) >> + >> + >> +def _node_str(node): >> + # Returns the complete menu entry text for a menu node. >> + # >> + # Example return value: "[*] Support for X" >> + >> + # Calculate the indent to print the item with by checking how many levels >> + # above it the closest 'menuconfig' item is (this includes menus and >> + # choices as well as menuconfig symbols) >> + indent = 0 >> + parent = node.parent >> + while not parent.is_menuconfig: >> + indent += _SUBMENU_INDENT >> + parent = parent.parent >> + >> + # This approach gives nice alignment for empty string symbols ("() Foo") >> + s = "{:{}}".format(_value_str(node), 3 + indent) >> + >> + if _should_show_name(node): >> + if isinstance(node.item, Symbol): >> + s += " <{}>".format(node.item.name) >> + else: >> + # For choices, use standard_sc_expr_str(). That way they show up as >> + # '<choice (name if any)>'. >> + s += " " + standard_sc_expr_str(node.item) >> + >> + if node.prompt: >> + if node.item == COMMENT: >> + s += " *** {} ***".format(node.prompt[0]) >> + else: >> + s += " " + node.prompt[0] >> + >> + if isinstance(node.item, Symbol): >> + sym = node.item >> + >> + # Print "(NEW)" next to symbols without a user value (from e.g. a >> + # .config), but skip it for choice symbols in choices in y mode, >> + # and for symbols of UNKNOWN type (which generate a warning though) >> + if sym.user_value is None and sym.orig_type and \ >> + not (sym.choice and sym.choice.tri_value == 2): >> + >> + s += " (NEW)" >> + >> + if isinstance(node.item, Choice) and node.item.tri_value == 2: >> + # Print the prompt of the selected symbol after the choice for >> + # choices in y mode >> + sym = node.item.selection >> + if sym: >> + for sym_node in sym.nodes: >> + # Use the prompt used at this choice location, in case the >> + # choice symbol is defined in multiple locations >> + if sym_node.parent is node and sym_node.prompt: >> + s += " ({})".format(sym_node.prompt[0]) >> + break >> + else: >> + # If the symbol isn't defined at this choice location, then >> + # just use whatever prompt we can find for it >> + for sym_node in sym.nodes: >> + if sym_node.prompt: >> + s += " ({})".format(sym_node.prompt[0]) >> + break >> + >> + # Print "--->" next to nodes that have menus that can potentially be >> + # entered. Print "----" if the menu is empty. We don't allow those to be >> + # entered. >> + if node.is_menuconfig: >> + s += " --->" if _shown_nodes(node) else " ----" >> + >> + return s >> + >> + >> +def _should_show_name(node): >> + # Returns True if 'node' is a symbol or choice whose name should shown (if >> + # any, as names are optional for choices) >> + >> + # The 'not node.prompt' case only hits in show-all mode, for promptless >> + # symbols and choices >> + return not node.prompt or \ >> + (_show_name and isinstance(node.item, (Symbol, Choice))) >> + >> + >> +def _value_str(node): >> + # Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu node >> + >> + item = node.item >> + >> + if item in (MENU, COMMENT): >> + return "" >> + >> + # Wouldn't normally happen, and generates a warning >> + if not item.orig_type: >> + return "" >> + >> + if item.orig_type in (STRING, INT, HEX): >> + return "({})".format(item.str_value) >> + >> + # BOOL or TRISTATE >> + >> + if _is_y_mode_choice_sym(item): >> + return "(X)" if item.choice.selection is item else "( )" >> + >> + tri_val_str = (" ", "M", "*")[item.tri_value] >> + >> + if len(item.assignable) <= 1: >> + # Pinned to a single value >> + return "" if isinstance(item, Choice) else "-{}-".format(tri_val_str) >> + >> + if item.type == BOOL: >> + return "[{}]".format(tri_val_str) >> + >> + # item.type == TRISTATE >> + if item.assignable == (1, 2): >> + return "{{{}}}".format(tri_val_str) # {M}/{*} >> + return "<{}>".format(tri_val_str) >> + >> + >> +def _is_y_mode_choice_sym(item): >> + # 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 >> + return isinstance(item, Symbol) and item.choice and item.visibility == 2 >> + >> + >> +def _check_valid(sym, s): >> + # Returns True if the string 's' is a well-formed value for 'sym'. >> + # Otherwise, displays an error and returns False. >> + >> + if sym.orig_type not in (INT, HEX): >> + return True # Anything goes for non-int/hex symbols >> + >> + base = 10 if sym.orig_type == INT else 16 >> + try: >> + int(s, base) >> + except ValueError: >> + _error("'{}' is a malformed {} value" >> + .format(s, TYPE_TO_STR[sym.orig_type])) >> + return False >> + >> + for low_sym, high_sym, cond in sym.ranges: >> + if expr_value(cond): >> + low_s = low_sym.str_value >> + high_s = high_sym.str_value >> + >> + if not int(low_s, base) <= int(s, base) <= int(high_s, base): >> + _error("{} is outside the range {}-{}" >> + .format(s, low_s, high_s)) >> + return False >> + >> + break >> + >> + return True >> + >> + >> +def _range_info(sym): >> + # Returns a string with information about the valid range for the symbol >> + # 'sym', or None if 'sym' doesn't have a range >> + >> + if sym.orig_type in (INT, HEX): >> + for low, high, cond in sym.ranges: >> + if expr_value(cond): >> + return "Range: {}-{}".format(low.str_value, high.str_value) >> + >> + return None >> + >> + >> +def _is_num(name): >> + # Heuristic to see if a symbol name looks like a number, for nicer output >> + # when printing expressions. Things like 16 are actually symbol names, only >> + # they get their name as their value when the symbol is undefined. >> + >> + try: >> + int(name) >> + except ValueError: >> + if not name.startswith(("0x", "0X")): >> + return False >> + >> + try: >> + int(name, 16) >> + except ValueError: >> + return False >> + >> + return True >> + >> + >> +def _getch_compat(win): >> + # Uses get_wch() if available (Python 3.3+) and getch() otherwise. >> + # >> + # Also falls back on getch() if get_wch() raises curses.error, to work >> + # around an issue when resizing the terminal on at least macOS Catalina. >> + # See https://github.com/ulfalizer/Kconfiglib/issues/84. >> + # >> + # Also handles a PDCurses resizing quirk. >> + >> + try: >> + c = win.get_wch() >> + except (AttributeError, curses.error): >> + c = win.getch() >> + if 0 <= c <= 255: >> + c = chr(c) >> + >> + # Decent resizing behavior on PDCurses requires calling resize_term(0, 0) >> + # after receiving KEY_RESIZE, while ncurses (usually) handles terminal >> + # resizing automatically in get(_w)ch() (see the end of the >> + # resizeterm(3NCURSES) man page). >> + # >> + # resize_term(0, 0) reliably fails and does nothing on ncurses, so this >> + # hack gives ncurses/PDCurses compatibility for resizing. I don't know >> + # whether it would cause trouble for other implementations. >> + if c == curses.KEY_RESIZE: >> + try: >> + curses.resize_term(0, 0) >> + except curses.error: >> + pass >> + >> + return c >> + >> + >> +def _warn(*args): >> + # Temporarily returns from curses to shell mode and prints a warning to >> + # stderr. The warning would get lost in curses mode. >> + curses.endwin() >> + print("menuconfig warning: ", end="", file=sys.stderr) >> + print(*args, file=sys.stderr) >> + curses.doupdate() >> + >> + >> +# Ignore exceptions from some functions that might fail, e.g. for small >> +# windows. They usually do reasonable things anyway. >> + >> + >> +def _safe_curs_set(visibility): >> + try: >> + curses.curs_set(visibility) >> + except curses.error: >> + pass >> + >> + >> +def _safe_addstr(win, *args): >> + # Clip the line to avoid wrapping to the next line, which looks glitchy. >> + # addchstr() would do it for us, but it's not available in the 'curses' >> + # module. >> + >> + attr = None >> + if isinstance(args[0], str): >> + y, x = win.getyx() >> + s = args[0] >> + if len(args) == 2: >> + attr = args[1] >> + else: >> + y, x, s = args[:3] >> + if len(args) == 4: >> + attr = args[3] >> + >> + maxlen = _width(win) - x >> + s = s.expandtabs() >> + >> + try: >> + # The 'curses' module uses wattr_set() internally if you pass 'attr', >> + # overwriting the background style, so setting 'attr' to 0 in the first >> + # case won't do the right thing >> + if attr is None: >> + win.addnstr(y, x, s, maxlen) >> + else: >> + win.addnstr(y, x, s, maxlen, attr) >> + except curses.error: >> + pass >> + >> + >> +def _safe_addch(win, *args): >> + try: >> + win.addch(*args) >> + except curses.error: >> + pass >> + >> + >> +def _safe_hline(win, *args): >> + try: >> + win.hline(*args) >> + except curses.error: >> + pass >> + >> + >> +def _safe_vline(win, *args): >> + try: >> + win.vline(*args) >> + except curses.error: >> + pass >> + >> + >> +def _safe_move(win, *args): >> + try: >> + win.move(*args) >> + except curses.error: >> + pass >> + >> + >> +def _change_c_lc_ctype_to_utf8(): >> + # See _CHANGE_C_LC_CTYPE_TO_UTF8 >> + >> + if _IS_WINDOWS: >> + # Windows rarely has issues here, and the PEP 538 implementation avoids >> + # changing the locale on it. None of the UTF-8 locales below were >> + # supported from some quick testing either. Play it safe. >> + return >> + >> + def try_set_locale(loc): >> + try: >> + locale.setlocale(locale.LC_CTYPE, loc) >> + return True >> + except locale.Error: >> + return False >> + >> + # Is LC_CTYPE set to the C locale? >> + if locale.setlocale(locale.LC_CTYPE) == "C": >> + # This list was taken from the PEP 538 implementation in the CPython >> + # code, in Python/pylifecycle.c >> + for loc in "C.UTF-8", "C.utf8", "UTF-8": >> + if try_set_locale(loc): >> + # LC_CTYPE successfully changed >> + return >> + >> + >> +if __name__ == "__main__": >> + _main() >> diff --git a/scripts/Kconfiglib/oldconfig.py b/scripts/Kconfiglib/oldconfig.py >> new file mode 100755 >> index 0000000..53434b2 >> --- /dev/null >> +++ b/scripts/Kconfiglib/oldconfig.py >> @@ -0,0 +1,246 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Implements oldconfig functionality. >> + >> + 1. Loads existing .config >> + 2. Prompts for the value of all modifiable symbols/choices that >> + aren't already set in the .config >> + 3. Writes an updated .config >> + >> +The default input/output filename is '.config'. A different filename can be >> +passed in the KCONFIG_CONFIG environment variable. >> + >> +When overwriting a configuration file, the old version is saved to >> +<filename>.old (e.g. .config.old). >> + >> +Entering '?' displays the help text of the symbol/choice, if any. >> + >> +Unlike 'make oldconfig', this script doesn't print menu titles and comments, >> +but gives Kconfig definition locations. Printing menus and comments would be >> +pretty easy to add: Look at the parents of each item, and print all menu >> +prompts and comments unless they have already been printed (assuming you want >> +to skip "irrelevant" menus). >> +""" >> +from __future__ import print_function >> + >> +import sys >> + >> +from kconfiglib import Symbol, Choice, BOOL, TRISTATE, HEX, standard_kconfig >> + >> + >> +# Python 2/3 compatibility hack >> +if sys.version_info[0] < 3: >> + input = raw_input >> + >> + >> +def _main(): >> + # Earlier symbols in Kconfig files might depend on later symbols and become >> + # visible if their values change. This flag is set to True if the value of >> + # any symbol changes, in which case we rerun the oldconfig to check for new >> + # visible symbols. >> + global conf_changed >> + >> + kconf = standard_kconfig(__doc__) >> + print(kconf.load_config()) >> + >> + while True: >> + conf_changed = False >> + >> + for node in kconf.node_iter(): >> + oldconfig(node) >> + >> + if not conf_changed: >> + break >> + >> + print(kconf.write_config()) >> + >> + >> +def oldconfig(node): >> + """ >> + Prompts the user for a value if node.item is a visible symbol/choice with >> + no user value. >> + """ >> + # See main() >> + global conf_changed >> + >> + # Only symbols and choices can be configured >> + if not isinstance(node.item, (Symbol, Choice)): >> + return >> + >> + # Skip symbols and choices that aren't visible >> + if not node.item.visibility: >> + return >> + >> + # Skip symbols and choices that don't have a prompt (at this location) >> + if not node.prompt: >> + return >> + >> + if isinstance(node.item, Symbol): >> + sym = node.item >> + >> + # Skip symbols that already have a user value >> + if sym.user_value is not None: >> + return >> + >> + # Skip symbols that can only have a single value, due to selects >> + if len(sym.assignable) == 1: >> + return >> + >> + # Skip symbols in choices in y mode. We ask once for the entire choice >> + # instead. >> + if sym.choice and sym.choice.tri_value == 2: >> + return >> + >> + # Loop until the user enters a valid value or enters a blank string >> + # (for the default value) >> + while True: >> + val = input("{} ({}) [{}] ".format( >> + node.prompt[0], _name_and_loc_str(sym), >> + _default_value_str(sym))) >> + >> + if val == "?": >> + _print_help(node) >> + continue >> + >> + # Substitute a blank string with the default value the symbol >> + # would get >> + if not val: >> + val = sym.str_value >> + >> + # Automatically add a "0x" prefix for hex symbols, like the >> + # menuconfig interface does. This isn't done when loading .config >> + # files, hence why set_value() doesn't do it automatically. >> + if sym.type == HEX and not val.startswith(("0x", "0X")): >> + val = "0x" + val >> + >> + old_str_val = sym.str_value >> + >> + # Kconfiglib itself will print a warning here if the value >> + # is invalid, so we don't need to bother >> + if sym.set_value(val): >> + # Valid value input. We're done with this node. >> + >> + if sym.str_value != old_str_val: >> + conf_changed = True >> + >> + return >> + >> + else: >> + choice = node.item >> + >> + # Skip choices that already have a visible user selection... >> + if choice.user_selection and choice.user_selection.visibility == 2: >> + # ...unless there are new visible symbols in the choice. (We know >> + # they have y (2) visibility in that case, because m-visible >> + # symbols get demoted to n-visibility in y-mode choices, and the >> + # user-selected symbol had visibility y.) >> + for sym in choice.syms: >> + if sym is not choice.user_selection and sym.visibility and \ >> + sym.user_value is None: >> + # New visible symbols in the choice >> + break >> + else: >> + # No new visible symbols in the choice >> + return >> + >> + # Get a list of available selections. The mode of the choice limits >> + # the visibility of the choice value symbols, so this will indirectly >> + # skip choices in n and m mode. >> + options = [sym for sym in choice.syms if sym.visibility == 2] >> + >> + if not options: >> + # No y-visible choice value symbols >> + return >> + >> + # Loop until the user enters a valid selection or a blank string (for >> + # the default selection) >> + while True: >> + print("{} ({})".format(node.prompt[0], _name_and_loc_str(choice))) >> + >> + for i, sym in enumerate(options, 1): >> + print("{} {}. {} ({})".format( >> + ">" if sym is choice.selection else " ", >> + i, >> + # Assume people don't define choice symbols with multiple >> + # prompts. That generates a warning anyway. >> + sym.nodes[0].prompt[0], >> + sym.name)) >> + >> + sel_index = input("choice[1-{}]: ".format(len(options))) >> + >> + if sel_index == "?": >> + _print_help(node) >> + continue >> + >> + # Pick the default selection if the string is blank >> + if not sel_index: >> + choice.selection.set_value(2) >> + break >> + >> + try: >> + sel_index = int(sel_index) >> + except ValueError: >> + print("Bad index", file=sys.stderr) >> + continue >> + >> + if not 1 <= sel_index <= len(options): >> + print("Bad index", file=sys.stderr) >> + continue >> + >> + # Valid selection >> + >> + if options[sel_index - 1].tri_value != 2: >> + conf_changed = True >> + >> + options[sel_index - 1].set_value(2) >> + break >> + >> + # Give all of the non-selected visible choice symbols the user value n. >> + # This makes it so that the choice is no longer considered new once we >> + # do additional passes, if the reason that it was considered new was >> + # that it had new visible choice symbols. >> + # >> + # Only giving visible choice symbols the user value n means we will >> + # prompt for the choice again if later user selections make more new >> + # choice symbols visible, which is correct. >> + for sym in choice.syms: >> + if sym is not choice.user_selection and sym.visibility: >> + sym.set_value(0) >> + >> + >> +def _name_and_loc_str(sc): >> + # Helper for printing the name of the symbol/choice 'sc' along with the >> + # location(s) in the Kconfig files where it is defined. Unnamed choices >> + # return "choice" instead of the name. >> + >> + return "{}, defined at {}".format( >> + sc.name or "choice", >> + ", ".join("{}:{}".format(node.filename, node.linenr) >> + for node in sc.nodes)) >> + >> + >> +def _print_help(node): >> + print("\n" + (node.help or "No help text\n")) >> + >> + >> +def _default_value_str(sym): >> + # Returns the "m/M/y" string in e.g. >> + # >> + # TRISTATE_SYM prompt (TRISTATE_SYM, defined at Kconfig:9) [n/M/y]: >> + # >> + # For string/int/hex, returns the default value as-is. >> + >> + if sym.type in (BOOL, TRISTATE): >> + return "/".join(("NMY" if sym.tri_value == tri else "nmy")[tri] >> + for tri in sym.assignable) >> + >> + # string/int/hex >> + return sym.str_value >> + >> + >> +if __name__ == "__main__": >> + _main() >> diff --git a/scripts/Kconfiglib/olddefconfig.py b/scripts/Kconfiglib/olddefconfig.py >> new file mode 100755 >> index 0000000..2dadfb4 >> --- /dev/null >> +++ b/scripts/Kconfiglib/olddefconfig.py >> @@ -0,0 +1,28 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2018-2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Updates an old .config file or creates a new one, by filling in default values >> +for all new symbols. This is the same as picking the default selection for all >> +symbols in oldconfig, or entering the menuconfig interface and immediately >> +saving. >> + >> +The default input/output filename is '.config'. A different filename can be >> +passed in the KCONFIG_CONFIG environment variable. >> + >> +When overwriting a configuration file, the old version is saved to >> +<filename>.old (e.g. .config.old). >> +""" >> +import kconfiglib >> + >> + >> +def main(): >> + kconf = kconfiglib.standard_kconfig(__doc__) >> + print(kconf.load_config()) >> + print(kconf.write_config()) >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/savedefconfig.py b/scripts/Kconfiglib/savedefconfig.py >> new file mode 100755 >> index 0000000..0f36bde >> --- /dev/null >> +++ b/scripts/Kconfiglib/savedefconfig.py >> @@ -0,0 +1,49 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Saves a minimal configuration file that only lists symbols that differ in value >> +from their defaults. Loading such a configuration file is equivalent to loading >> +the "full" configuration file. >> + >> +Minimal configuration files are handy to start from when editing configuration >> +files by hand. >> + >> +The default input configuration file is '.config'. A different input filename >> +can be passed in the KCONFIG_CONFIG environment variable. >> + >> +Note: Minimal configurations can also be generated from within the menuconfig >> +interface. >> +""" >> +import argparse >> + >> +import kconfiglib >> + >> + >> +def main(): >> + parser = argparse.ArgumentParser( >> + formatter_class=argparse.RawDescriptionHelpFormatter, >> + description=__doc__) >> + >> + parser.add_argument( >> + "--kconfig", >> + default="Kconfig", >> + help="Top-level Kconfig file (default: Kconfig)") >> + >> + parser.add_argument( >> + "--out", >> + metavar="MINIMAL_CONFIGURATION", >> + default="defconfig", >> + help="Output filename for minimal configuration (default: defconfig)") >> + >> + args = parser.parse_args() >> + >> + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) >> + print(kconf.load_config()) >> + print(kconf.write_min_config(args.out)) >> + >> + >> +if __name__ == "__main__": >> + main() >> diff --git a/scripts/Kconfiglib/setconfig.py b/scripts/Kconfiglib/setconfig.py >> new file mode 100755 >> index 0000000..f9cf5cd >> --- /dev/null >> +++ b/scripts/Kconfiglib/setconfig.py >> @@ -0,0 +1,92 @@ >> +#!/usr/bin/env python3 >> + >> +# Copyright (c) 2019, Ulf Magnusson >> +# SPDX-License-Identifier: ISC >> + >> +""" >> +Simple utility for setting configuration values from the command line. >> + >> +Sample usage: >> + >> + $ setconfig FOO_SUPPORT=y BAR_BITS=8 >> + >> +Note: Symbol names should not be prefixed with 'CONFIG_'. >> + >> +The exit status on errors is 1. >> + >> +The default input/output configuration file is '.config'. A different filename >> +can be passed in the KCONFIG_CONFIG environment variable. >> + >> +When overwriting a configuration file, the old version is saved to >> +<filename>.old (e.g. .config.old). >> +""" >> +import argparse >> +import sys >> + >> +import kconfiglib >> + >> + >> +def main(): >> + parser = argparse.ArgumentParser( >> + formatter_class=argparse.RawDescriptionHelpFormatter, >> + description=__doc__) >> + >> + parser.add_argument( >> + "--kconfig", >> + default="Kconfig", >> + help="Top-level Kconfig file (default: Kconfig)") >> + >> + parser.add_argument( >> + "--no-check-exists", >> + dest="check_exists", >> + action="store_false", >> + help="Ignore assignments to non-existent symbols instead of erroring " >> + "out") >> + >> + parser.add_argument( >> + "--no-check-value", >> + dest="check_value", >> + action="store_false", >> + help="Ignore assignments that didn't \"take\" (where the symbol got a " >> + "different value, e.g. due to unsatisfied dependencies) instead " >> + "of erroring out") >> + >> + parser.add_argument( >> + "assignments", >> + metavar="ASSIGNMENT", >> + nargs="*", >> + help="A 'NAME=value' assignment") >> + >> + args = parser.parse_args() >> + >> + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) >> + print(kconf.load_config()) >> + >> + for arg in args.assignments: >> + if "=" not in arg: >> + sys.exit("error: no '=' in assignment: '{}'".format(arg)) >> + name, value = arg.split("=", 1) >> + >> + if name not in kconf.syms: >> + if not args.check_exists: >> + continue >> + sys.exit("error: no symbol '{}' in configuration".format(name)) >> + >> + sym = kconf.syms[name] >> + >> + if not sym.set_value(value): >> + sys.exit("error: '{}' is an invalid value for the {} symbol {}" >> + .format(value, kconfiglib.TYPE_TO_STR[sym.orig_type], >> + name)) >> + >> + if args.check_value and sym.str_value != value: >> + sys.exit("error: {} was assigned the value '{}', but got the " >> + "value '{}'. Check the symbol's dependencies, and make " >> + "sure that it has a prompt." >> + .format(name, value, sym.str_value)) >> + >> + print(kconf.write_config()) >> + >> + >> +if __name__ == "__main__": >> + main() >> -- >> 2.45.2 >> >> -- >> You received this message because you are subscribed to the Google Groups "swupdate" group. >> To unsubscribe from this group and stop receiving emails from it, send an email to swupdate+unsubscribe@googlegroups.com. >> To view this discussion visit https://groups.google.com/d/msgid/swupdate/20250118205226.349745-4-oss%40braunwarth.dev. >
Hi Mark On 19.01.25 17:09, Mark Jonas wrote: > Hi Daniel, > > Instead of copying / forking Kconfiglib, what would speak against a > git submodule? I don't have good experience with submodules. My experience is: - They are not checked-out by default and makes things complicated for first-time users. - One cannot maintain local changes without forking the original repository. Syncing this bunch of files manually is much less pain I guess. Regards Daniel
Hi Daniel, On 19.01.25 17:33, Daniel Braunwarth wrote: > Hi Mark > > On 19.01.25 17:09, Mark Jonas wrote: >> Hi Daniel, >> >> Instead of copying / forking Kconfiglib, what would speak against a >> git submodule? > > I don't have good experience with submodules. > > My experience is: > > - They are not checked-out by default and makes things complicated for > first-time users. +1 Even for not first users. > - One cannot maintain local changes without forking the original > repository. +1 > > Syncing this bunch of files manually is much less pain I guess. > > Fully agree. Stefano > Regards > Daniel >
diff --git a/scripts/Kconfiglib/alldefconfig.py b/scripts/Kconfiglib/alldefconfig.py new file mode 100755 index 0000000..56c4caa --- /dev/null +++ b/scripts/Kconfiglib/alldefconfig.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where all symbols are set to their their default +values. + +The default output filename is '.config'. A different filename can be passed in +the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/alldefconfig.py +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig(__doc__) + kconf.load_allconfig("alldef.config") + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/allnoconfig.py b/scripts/Kconfiglib/allnoconfig.py new file mode 100755 index 0000000..de90d8b --- /dev/null +++ b/scripts/Kconfiglib/allnoconfig.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where as many symbols as possible are set to 'n'. + +The default output filename is '.config'. A different filename can be passed +in the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/allnoconfig.py +""" + +# See examples/allnoconfig_walk.py for another way to implement this script + +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig(__doc__) + + # Avoid warnings that would otherwise get printed by Kconfiglib for the + # following: + # + # 1. Assigning a value to a symbol without a prompt, which never has any + # effect + # + # 2. Assigning values invalid for the type (only bool/tristate symbols + # accept 0/1/2, for n/m/y). The assignments will be ignored for other + # symbol types, which is what we want. + kconf.warn = False + for sym in kconf.unique_defined_syms: + sym.set_value(2 if sym.is_allnoconfig_y else 0) + kconf.warn = True + + kconf.load_allconfig("allno.config") + + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/allyesconfig.py b/scripts/Kconfiglib/allyesconfig.py new file mode 100755 index 0000000..90eb9b8 --- /dev/null +++ b/scripts/Kconfiglib/allyesconfig.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where as many symbols as possible are set to 'y'. + +The default output filename is '.config'. A different filename can be passed +in the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/allyesconfig.py +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig(__doc__) + + # See allnoconfig.py + kconf.warn = False + + # Try to set all symbols to 'y'. Dependencies might truncate the value down + # later, but this will at least give the highest possible value. + # + # Assigning 0/1/2 to non-bool/tristate symbols has no effect (int/hex + # symbols still take a string, because they preserve formatting). + for sym in kconf.unique_defined_syms: + # Set choice symbols to 'm'. This value will be ignored for choices in + # 'y' mode (the "normal" mode), which will instead just get their + # default selection, but will set all symbols in m-mode choices to 'm', + # which is as high as they can go. + # + # Here's a convoluted example of how you might get an m-mode choice + # even during allyesconfig: + # + # choice + # tristate "weird choice" + # depends on m + sym.set_value(1 if sym.choice else 2) + + # Set all choices to the highest possible mode + for choice in kconf.unique_choices: + choice.set_value(2) + + kconf.warn = True + + kconf.load_allconfig("allyes.config") + + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/defconfig.py b/scripts/Kconfiglib/defconfig.py new file mode 100755 index 0000000..b179273 --- /dev/null +++ b/scripts/Kconfiglib/defconfig.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Reads a specified configuration file, then writes a new configuration file. +This can be used to initialize the configuration from e.g. an arch-specific +configuration file. This input configuration file would usually be a minimal +configuration file, as generated by e.g. savedefconfig. + +The default output filename is '.config'. A different filename can be passed in +the KCONFIG_CONFIG environment variable. +""" +import argparse + +import kconfiglib + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--kconfig", + default="Kconfig", + help="Top-level Kconfig file (default: Kconfig)") + + parser.add_argument( + "config", + metavar="CONFIGURATION", + help="Input configuration file") + + args = parser.parse_args() + + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) + print(kconf.load_config(args.config)) + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/genconfig.py b/scripts/Kconfiglib/genconfig.py new file mode 100755 index 0000000..62f065b --- /dev/null +++ b/scripts/Kconfiglib/genconfig.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Generates a header file with #defines from the configuration, matching the +format of include/generated/autoconf.h in the Linux kernel. + +Optionally, also writes the configuration output as a .config file. See +--config-out. + +The --sync-deps, --file-list, and --env-list options generate information that +can be used to avoid needless rebuilds/reconfigurations. + +Before writing a header or configuration file, Kconfiglib compares the old +contents of the file against the new contents. If there's no change, the write +is skipped. This avoids updating file metadata like the modification time, and +might save work depending on your build setup. + +By default, the configuration is generated from '.config'. A different +configuration file can be passed in the KCONFIG_CONFIG environment variable. + +A custom header string can be inserted at the beginning of generated +configuration and header files by setting the KCONFIG_CONFIG_HEADER and +KCONFIG_AUTOHEADER_HEADER environment variables, respectively (this also works +for other scripts). The string is not automatically made a comment (this is by +design, to allow anything to be added), and no trailing newline is added, so +add '/* */', '#', and newlines as appropriate. + +See https://www.gnu.org/software/make/manual/make.html#Multi_002dLine for a +handy way to define multi-line variables in makefiles, for use with custom +headers. Remember to export the variable to the environment. +""" +import argparse +import os +import sys + +import kconfiglib + + +DEFAULT_SYNC_DEPS_PATH = "deps/" + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--header-path", + metavar="HEADER_FILE", + help=""" +Path to write the generated header file to. If not specified, the path in the +environment variable KCONFIG_AUTOHEADER is used if it is set, and 'config.h' +otherwise. +""") + + parser.add_argument( + "--config-out", + metavar="CONFIG_FILE", + help=""" +Write the configuration to CONFIG_FILE. This is useful if you include .config +files in Makefiles, as the generated configuration file will be a full .config +file even if .config is outdated. The generated configuration matches what +olddefconfig would produce. If you use sync-deps, you can include +deps/auto.conf instead. --config-out is meant for cases where incremental build +information isn't needed. +""") + + parser.add_argument( + "--sync-deps", + metavar="OUTPUT_DIR", + nargs="?", + const=DEFAULT_SYNC_DEPS_PATH, + help=""" +Enable generation of symbol dependency information for incremental builds, +optionally specifying the output directory (default: {}). See the docstring of +Kconfig.sync_deps() in Kconfiglib for more information. +""".format(DEFAULT_SYNC_DEPS_PATH)) + + parser.add_argument( + "--file-list", + metavar="OUTPUT_FILE", + help=""" +Write a list of all Kconfig files to OUTPUT_FILE, with one file per line. The +paths are relative to $srctree (or to the current directory if $srctree is +unset). Files appear in the order they're 'source'd. +""") + + parser.add_argument( + "--env-list", + metavar="OUTPUT_FILE", + help=""" +Write a list of all environment variables referenced in Kconfig files to +OUTPUT_FILE, with one variable per line. Each line has the format NAME=VALUE. +Only environment variables referenced with the preprocessor $(VAR) syntax are +included, and not variables referenced with the older $VAR syntax (which is +only supported for backwards compatibility). +""") + + parser.add_argument( + "kconfig", + metavar="KCONFIG", + nargs="?", + default="Kconfig", + help="Top-level Kconfig file (default: Kconfig)") + + args = parser.parse_args() + + + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) + kconf.load_config() + + if args.header_path is None: + if "KCONFIG_AUTOHEADER" in os.environ: + kconf.write_autoconf() + else: + # Kconfiglib defaults to include/generated/autoconf.h to be + # compatible with the C tools. 'config.h' is used here instead for + # backwards compatibility. It's probably a saner default for tools + # as well. + kconf.write_autoconf("config.h") + else: + kconf.write_autoconf(args.header_path) + + if args.config_out is not None: + kconf.write_config(args.config_out, save_old=False) + + if args.sync_deps is not None: + kconf.sync_deps(args.sync_deps) + + if args.file_list is not None: + with _open_write(args.file_list) as f: + for path in kconf.kconfig_filenames: + f.write(path + "\n") + + if args.env_list is not None: + with _open_write(args.env_list) as f: + for env_var in kconf.env_vars: + f.write("{}={}\n".format(env_var, os.environ[env_var])) + + +def _open_write(path): + # Python 2/3 compatibility. io.open() is available on both, but makes + # write() expect 'unicode' strings on Python 2. + + if sys.version_info[0] < 3: + return open(path, "w") + return open(path, "w", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/guiconfig.py b/scripts/Kconfiglib/guiconfig.py new file mode 100755 index 0000000..7804fdc --- /dev/null +++ b/scripts/Kconfiglib/guiconfig.py @@ -0,0 +1,2324 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Overview +======== + +A Tkinter-based menuconfig implementation, based around a treeview control and +a help display. The interface should feel familiar to people used to qconf +('make xconfig'). Compatible with both Python 2 and Python 3. + +The display can be toggled between showing the full tree and showing just a +single menu (like menuconfig.py). Only single-menu mode distinguishes between +symbols defined with 'config' and symbols defined with 'menuconfig'. + +A show-all mode is available that shows invisible items in red. + +Supports both mouse and keyboard controls. The following keyboard shortcuts are +available: + + Ctrl-S : Save configuration + Ctrl-O : Open configuration + Ctrl-A : Toggle show-all mode + Ctrl-N : Toggle show-name mode + Ctrl-M : Toggle single-menu mode + Ctrl-F, /: Open jump-to dialog + ESC : Close + +Running +======= + +guiconfig.py can be run either as a standalone executable or by calling the +menuconfig() function with an existing Kconfig instance. The second option is a +bit inflexible in that it will still load and save .config, etc. + +When run in standalone mode, the top-level Kconfig file to load can be passed +as a command-line argument. With no argument, it defaults to "Kconfig". + +The KCONFIG_CONFIG environment variable specifies the .config file to load (if +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). + +$srctree is supported through Kconfiglib. +""" + +# Note: There's some code duplication with menuconfig.py below, especially for +# the help text. Maybe some of it could be moved into kconfiglib.py or a shared +# helper script, but OTOH it's pretty nice to have things standalone and +# customizable. + +import errno +import os +import sys + +_PY2 = sys.version_info[0] < 3 + +if _PY2: + # Python 2 + from Tkinter import * + import ttk + import tkFont as font + import tkFileDialog as filedialog + import tkMessageBox as messagebox +else: + # Python 3 + from tkinter import * + import tkinter.ttk as ttk + import tkinter.font as font + from tkinter import filedialog, messagebox + +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ + BOOL, TRISTATE, STRING, INT, HEX, \ + AND, OR, \ + expr_str, expr_value, split_expr, \ + standard_sc_expr_str, \ + TRI_TO_STR, TYPE_TO_STR, \ + standard_kconfig, standard_config_filename + + +# If True, use GIF image data embedded in this file instead of separate GIF +# files. See _load_images(). +_USE_EMBEDDED_IMAGES = True + + +# Help text for the jump-to dialog +_JUMP_TO_HELP = """\ +Type one or more strings/regexes and press Enter to list items that match all +of them. Python's regex flavor is used (see the 're' module). Double-clicking +an item will jump to it. Item values can be toggled directly within the dialog.\ +""" + + +def _main(): + menuconfig(standard_kconfig(__doc__)) + + +# Global variables used below: +# +# _root: +# The Toplevel instance for the main window +# +# _tree: +# The Treeview in the main window +# +# _jump_to_tree: +# The Treeview in the jump-to dialog. None if the jump-to dialog isn't +# open. Doubles as a flag. +# +# _jump_to_matches: +# List of Nodes shown in the jump-to dialog +# +# _menupath: +# The Label that shows the menu path of the selected item +# +# _backbutton: +# The button shown in single-menu mode for jumping to the parent menu +# +# _status_label: +# Label with status text shown at the bottom of the main window +# ("Modified", "Saved to ...", etc.) +# +# _id_to_node: +# We can't use Node objects directly as Treeview item IDs, so we use their +# id()s instead. This dictionary maps Node id()s back to Nodes. (The keys +# are actually str(id(node)), just to simplify lookups.) +# +# _cur_menu: +# The current menu. Ignored outside single-menu mode. +# +# _show_all_var/_show_name_var/_single_menu_var: +# Tkinter Variable instances bound to the corresponding checkboxes +# +# _show_all/_single_menu: +# Plain Python bools that track _show_all_var and _single_menu_var, to +# speed up and simplify things a bit +# +# _conf_filename: +# File to save the configuration to +# +# _minconf_filename: +# File to save minimal configurations to +# +# _conf_changed: +# True if the configuration has been changed. If False, we don't bother +# showing the save-and-quit dialog. +# +# We reset this to False whenever the configuration is saved. +# +# _*_img: +# PhotoImage instances for images + + +def menuconfig(kconf): + """ + Launches the configuration interface, returning after the user exits. + + kconf: + Kconfig instance to be configured + """ + global _kconf + global _conf_filename + global _minconf_filename + global _jump_to_tree + global _cur_menu + + _kconf = kconf + + _jump_to_tree = None + + _create_id_to_node() + + _create_ui() + + # Filename to save configuration to + _conf_filename = standard_config_filename() + + # Load existing configuration and check if it's outdated + _set_conf_changed(_load_config()) + + # Filename to save minimal configuration to + _minconf_filename = "defconfig" + + # Current menu in single-menu mode + _cur_menu = _kconf.top_node + + # Any visible items in the top menu? + if not _shown_menu_nodes(kconf.top_node): + # Nothing visible. Start in show-all mode and try again. + _show_all_var.set(True) + if not _shown_menu_nodes(kconf.top_node): + # Give up and show an error. It's nice to be able to assume that + # the tree is non-empty in the rest of the code. + _root.wait_visibility() + messagebox.showerror( + "Error", + "Empty configuration -- nothing to configure.\n\n" + "Check that environment variables are set properly.") + _root.destroy() + return + + # Build the initial tree + _update_tree() + + # Select the first item and focus the Treeview, so that keyboard controls + # work immediately + _select(_tree, _tree.get_children()[0]) + _tree.focus_set() + + # Make geometry information available for centering the window. This + # indirectly creates the window, so hide it so that it's never shown at the + # old location. + _root.withdraw() + _root.update_idletasks() + + # Center the window + _root.geometry("+{}+{}".format( + (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2, + (_root.winfo_screenheight() - _root.winfo_reqheight())//2)) + + # Show it + _root.deiconify() + + # Prevent the window from being automatically resized. Otherwise, it + # changes size when scrollbars appear/disappear before the user has + # manually resized it. + _root.geometry(_root.geometry()) + + _root.mainloop() + + +def _load_config(): + # Loads any existing .config file. See the Kconfig.load_config() docstring. + # + # Returns True if .config is missing or outdated. We always prompt for + # saving the configuration in that case. + + print(_kconf.load_config()) + if not os.path.exists(_conf_filename): + # No .config + return True + + return _needs_save() + + +def _needs_save(): + # Returns True if a just-loaded .config file is outdated (would get + # modified when saving) + + if _kconf.missing_syms: + # Assignments to undefined symbols in the .config + return True + + for sym in _kconf.unique_defined_syms: + if sym.user_value is None: + if sym.config_string: + # Unwritten symbol + return True + elif sym.orig_type in (BOOL, TRISTATE): + if sym.tri_value != sym.user_value: + # Written bool/tristate symbol, new value + return True + elif sym.str_value != sym.user_value: + # Written string/int/hex symbol, new value + return True + + # No need to prompt for save + return False + + +def _create_id_to_node(): + global _id_to_node + + _id_to_node = {str(id(node)): node for node in _kconf.node_iter()} + + +def _create_ui(): + # Creates the main window UI + + global _root + global _tree + + # Create the root window. This initializes Tkinter and makes e.g. + # PhotoImage available, so do it early. + _root = Tk() + + _load_images() + _init_misc_ui() + _fix_treeview_issues() + + _create_top_widgets() + # Create the pane with the Kconfig tree and description text + panedwindow, _tree = _create_kconfig_tree_and_desc(_root) + panedwindow.grid(column=0, row=1, sticky="nsew") + _create_status_bar() + + _root.columnconfigure(0, weight=1) + # Only the pane with the Kconfig tree and description grows vertically + _root.rowconfigure(1, weight=1) + + # Start with show-name disabled + _do_showname() + + _tree.bind("<Left>", _tree_left_key) + _tree.bind("<Right>", _tree_right_key) + # Note: Binding this for the jump-to tree as well would cause issues due to + # the Tk bug mentioned in _tree_open() + _tree.bind("<<TreeviewOpen>>", _tree_open) + # add=True to avoid overriding the description text update + _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True) + + _root.bind("<Control-s>", _save) + _root.bind("<Control-o>", _open) + _root.bind("<Control-a>", _toggle_showall) + _root.bind("<Control-n>", _toggle_showname) + _root.bind("<Control-m>", _toggle_tree_mode) + _root.bind("<Control-f>", _jump_to_dialog) + _root.bind("/", _jump_to_dialog) + _root.bind("<Escape>", _on_quit) + + +def _load_images(): + # Loads GIF images, creating the global _*_img PhotoImage variables. + # Base64-encoded images embedded in this script are used if + # _USE_EMBEDDED_IMAGES is True, and separate image files in the same + # directory as the script otherwise. + # + # Using a global variable indirectly prevents the image from being + # garbage-collected. Passing an image to a Tkinter function isn't enough to + # keep it alive. + + def load_image(name, data): + var_name = "_{}_img".format(name) + + if _USE_EMBEDDED_IMAGES: + globals()[var_name] = PhotoImage(data=data, format="gif") + else: + globals()[var_name] = PhotoImage( + file=os.path.join(os.path.dirname(__file__), name + ".gif"), + format="gif") + + # Note: Base64 data can be put on the clipboard with + # $ base64 -w0 foo.gif | xclip + + load_image("icon", "R0lGODlhMAAwAPEDAAAAAADQAO7u7v///yH5BAUKAAMALAAAAAAwADAAAAL/nI+gy+2Pokyv2jazuZxryQjiSJZmyXxHeLbumH6sEATvW8OLNtf5bfLZRLFITzgEipDJ4mYxYv6A0ubuqYhWk66tVTE4enHer7jcKvt0LLUw6P45lvEprT6c0+v7OBuqhYdHohcoqIbSAHc4ljhDwrh1UlgSydRCWWlp5wiYZvmSuSh4IzrqV6p4cwhkCsmY+nhK6uJ6t1mrOhuJqfu6+WYiCiwl7HtLjNSZZZis/MeM7NY3TaRKS40ooDeoiVqIultsrav92bi9c3a5KkkOsOJZpSS99m4k/0zPng4Gks9JSbB+8DIcoQfnjwpZCHv5W+ip4aQrKrB0uOikYhiMCBw1/uPoQUMBADs=") + load_image("n_bool", "R0lGODdhEAAQAPAAAAgICP///ywAAAAAEAAQAAACIISPacHtvp5kcb5qG85hZ2+BkyiRF8BBaEqtrKkqslEAADs=") + load_image("y_bool", "R0lGODdhEAAQAPEAAAgICADQAP///wAAACwAAAAAEAAQAAACMoSPacLtvlh4YrIYsst2cV19AvaVF9CUXBNJJoum7ymrsKuCnhiupIWjSSjAFuWhSCIKADs=") + load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=") + load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=") + load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7") + load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=") + load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==") + load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==") + load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==") + load_image("y_locked", "R0lGODlhEAAQAPD/AAAAAADQACH5BAUKAAIALAAAAAAQABAAAAIylC8AyKzNgnlCtoDTwvZwrHydIYpQmR3KWq4uK74IOnp0HQPmnD3cOVlUIAgKsShkFAAAOw==") + load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7") + load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=") + load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=") + + +def _fix_treeview_issues(): + # Fixes some Treeview issues + + global _treeview_rowheight + + style = ttk.Style() + + # The treeview rowheight isn't adjusted automatically on high-DPI displays, + # so do it ourselves. The font will probably always be TkDefaultFont, but + # play it safe and look it up. + + _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \ + .metrics("linespace") + 2 + + style.configure("Treeview", rowheight=_treeview_rowheight) + + # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae, + # which breaks tag background colors + + for option in "foreground", "background": + # Filter out any styles starting with ("!disabled", "!selected", ...). + # style.map() returns an empty list for missing options, so this should + # be future-safe. + style.map( + "Treeview", + **{option: [elm for elm in style.map("Treeview", query_opt=option) + if elm[:2] != ("!disabled", "!selected")]}) + + +def _init_misc_ui(): + # Does misc. UI initialization, like setting the title, icon, and theme + + _root.title(_kconf.mainmenu_text) + # iconphoto() isn't available in Python 2's Tkinter + _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img) + # Reducing the width of the window to 1 pixel makes it move around, at + # least on GNOME. Prevent weird stuff like that. + _root.minsize(128, 128) + _root.protocol("WM_DELETE_WINDOW", _on_quit) + + # Use the 'clam' theme on *nix if it's available. It looks nicer than the + # 'default' theme. + if _root.tk.call("tk", "windowingsystem") == "x11": + style = ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + + +def _create_top_widgets(): + # Creates the controls above the Kconfig tree in the main window + + global _show_all_var + global _show_name_var + global _single_menu_var + global _menupath + global _backbutton + + topframe = ttk.Frame(_root) + topframe.grid(column=0, row=0, sticky="ew") + + ttk.Button(topframe, text="Save", command=_save) \ + .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c") + + ttk.Button(topframe, text="Save as...", command=_save_as) \ + .grid(column=1, row=0, sticky="ew") + + ttk.Button(topframe, text="Save minimal (advanced)...", + command=_save_minimal) \ + .grid(column=2, row=0, sticky="ew", padx=".05c") + + ttk.Button(topframe, text="Open...", command=_open) \ + .grid(column=3, row=0) + + ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \ + .grid(column=4, row=0, padx=".05c") + + _show_name_var = BooleanVar() + ttk.Checkbutton(topframe, text="Show name", command=_do_showname, + variable=_show_name_var) \ + .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c", + ipady=".2c") + + _show_all_var = BooleanVar() + ttk.Checkbutton(topframe, text="Show all", command=_do_showall, + variable=_show_all_var) \ + .grid(column=1, row=1, sticky="nsew", pady="0 .05c") + + # Allow the show-all and single-menu status to be queried via plain global + # Python variables, which is faster and simpler + + def show_all_updated(*_): + global _show_all + _show_all = _show_all_var.get() + + _trace_write(_show_all_var, show_all_updated) + _show_all_var.set(False) + + _single_menu_var = BooleanVar() + ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode, + variable=_single_menu_var) \ + .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c") + + _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu, + state="disabled") + _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c") + + def tree_mode_updated(*_): + global _single_menu + _single_menu = _single_menu_var.get() + + if _single_menu: + _backbutton.grid() + else: + _backbutton.grid_remove() + + _trace_write(_single_menu_var, tree_mode_updated) + _single_menu_var.set(False) + + # Column to the right of the buttons that the menu path extends into, so + # that it can grow wider than the buttons + topframe.columnconfigure(5, weight=1) + + _menupath = ttk.Label(topframe) + _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c", + pady="0 .05c") + + +def _create_kconfig_tree_and_desc(parent): + # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text + # that shows a description of the selected node. Returns a tuple with the + # Panedwindow and the Treeview. This code is shared between the main window + # and the jump-to dialog. + + panedwindow = ttk.Panedwindow(parent, orient=VERTICAL) + + tree_frame, tree = _create_kconfig_tree(panedwindow) + desc_frame, desc = _create_kconfig_desc(panedwindow) + + panedwindow.add(tree_frame, weight=1) + panedwindow.add(desc_frame) + + def tree_select(_): + # The Text widget does not allow editing the text in its disabled + # state. We need to temporarily enable it. + desc["state"] = "normal" + + sel = tree.selection() + if not sel: + desc.delete("1.0", "end") + desc["state"] = "disabled" + return + + # Text.replace() is not available in Python 2's Tkinter + desc.delete("1.0", "end") + desc.insert("end", _info_str(_id_to_node[sel[0]])) + + desc["state"] = "disabled" + + tree.bind("<<TreeviewSelect>>", tree_select) + tree.bind("<1>", _tree_click) + tree.bind("<Double-1>", _tree_double_click) + tree.bind("<Return>", _tree_enter) + tree.bind("<KP_Enter>", _tree_enter) + tree.bind("<space>", _tree_toggle) + tree.bind("n", _tree_set_val(0)) + tree.bind("m", _tree_set_val(1)) + tree.bind("y", _tree_set_val(2)) + + return panedwindow, tree + + +def _create_kconfig_tree(parent): + # Creates a Treeview for showing Kconfig nodes + + frame = ttk.Frame(parent) + + tree = ttk.Treeview(frame, selectmode="browse", height=20, + columns=("name",)) + tree.heading("#0", text="Option", anchor="w") + tree.heading("name", text="Name", anchor="w") + + tree.tag_configure("n-bool", image=_n_bool_img) + tree.tag_configure("y-bool", image=_y_bool_img) + tree.tag_configure("m-tri", image=_m_tri_img) + tree.tag_configure("n-tri", image=_n_tri_img) + tree.tag_configure("m-tri", image=_m_tri_img) + tree.tag_configure("y-tri", image=_y_tri_img) + tree.tag_configure("m-my", image=_m_my_img) + tree.tag_configure("y-my", image=_y_my_img) + tree.tag_configure("n-locked", image=_n_locked_img) + tree.tag_configure("m-locked", image=_m_locked_img) + tree.tag_configure("y-locked", image=_y_locked_img) + tree.tag_configure("not-selected", image=_not_selected_img) + tree.tag_configure("selected", image=_selected_img) + tree.tag_configure("edit", image=_edit_img) + tree.tag_configure("invisible", foreground="red") + + tree.grid(column=0, row=0, sticky="nsew") + + _add_vscrollbar(frame, tree) + + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + # Create items for all menu nodes. These can be detached/moved later. + # Micro-optimize this a bit. + insert = tree.insert + id_ = id + Symbol_ = Symbol + for node in _kconf.node_iter(): + item = node.item + insert("", "end", iid=id_(node), + values=item.name if item.__class__ is Symbol_ else "") + + return frame, tree + + +def _create_kconfig_desc(parent): + # Creates a Text for showing the description of the selected Kconfig node + + frame = ttk.Frame(parent) + + desc = Text(frame, height=12, wrap="none", borderwidth=0, + state="disabled") + desc.grid(column=0, row=0, sticky="nsew") + + # Work around not being to Ctrl-C/V text from a disabled Text widget, with a + # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only + desc.bind("<1>", lambda _: desc.focus_set()) + + _add_vscrollbar(frame, desc) + + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + return frame, desc + + +def _add_vscrollbar(parent, widget): + # Adds a vertical scrollbar to 'widget' that's only shown as needed + + vscrollbar = ttk.Scrollbar(parent, orient="vertical", + command=widget.yview) + vscrollbar.grid(column=1, row=0, sticky="ns") + + def yscrollcommand(first, last): + # Only show the scrollbar when needed. 'first' and 'last' are + # strings. + if float(first) <= 0.0 and float(last) >= 1.0: + vscrollbar.grid_remove() + else: + vscrollbar.grid() + + vscrollbar.set(first, last) + + widget["yscrollcommand"] = yscrollcommand + + +def _create_status_bar(): + # Creates the status bar at the bottom of the main window + + global _status_label + + _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0") + _status_label.grid(column=0, row=3, sticky="ew") + + +def _set_status(s): + # Sets the text in the status bar to 's' + + _status_label["text"] = s + + +def _set_conf_changed(changed): + # Updates the status re. whether there are unsaved changes + + global _conf_changed + + _conf_changed = changed + if changed: + _set_status("Modified") + + +def _update_tree(): + # Updates the Kconfig tree in the main window by first detaching all nodes + # and then updating and reattaching them. The tree structure might have + # changed. + + # If a selected/focused item is detached and later reattached, it stays + # selected/focused. That can give multiple selections even though + # selectmode=browse. Save and later restore the selection and focus as a + # workaround. + old_selection = _tree.selection() + old_focus = _tree.focus() + + # Detach all tree items before re-stringing them. This is relatively fast, + # luckily. + _tree.detach(*_id_to_node.keys()) + + if _single_menu: + _build_menu_tree() + else: + _build_full_tree(_kconf.top_node) + + _tree.selection_set(old_selection) + _tree.focus(old_focus) + + +def _build_full_tree(menu): + # Updates the tree starting from menu.list, in full-tree mode. To speed + # things up, only open menus are updated. The menu-at-a-time logic here is + # to deal with invisible items that can show up outside show-all mode (see + # _shown_full_nodes()). + + for node in _shown_full_nodes(menu): + _add_to_tree(node, _kconf.top_node) + + # _shown_full_nodes() includes nodes from menus rooted at symbols, so + # we only need to check "real" menus/choices here + if node.list and not isinstance(node.item, Symbol): + if _tree.item(id(node), "open"): + _build_full_tree(node) + else: + # We're just probing here, so _shown_menu_nodes() will work + # fine, and might be a bit faster + shown = _shown_menu_nodes(node) + if shown: + # Dummy element to make the open/closed toggle appear + _tree.move(id(shown[0]), id(shown[0].parent), "end") + + +def _shown_full_nodes(menu): + # Returns the list of menu nodes shown in 'menu' (a menu node for a menu) + # for full-tree mode. A tricky detail is that invisible items need to be + # shown if they have visible children. + + def rec(node): + res = [] + + while node: + if _visible(node) or _show_all: + res.append(node) + if node.list and isinstance(node.item, Symbol): + # Nodes from menu created from dependencies + res += rec(node.list) + + elif node.list and isinstance(node.item, Symbol): + # Show invisible symbols (defined with either 'config' and + # 'menuconfig') if they have visible children. This can happen + # for an m/y-valued symbol with an optional prompt + # ('prompt "foo" is COND') that is currently disabled. + shown_children = rec(node.list) + if shown_children: + res.append(node) + res += shown_children + + node = node.next + + return res + + return rec(menu.list) + + +def _build_menu_tree(): + # Updates the tree in single-menu mode. See _build_full_tree() as well. + + for node in _shown_menu_nodes(_cur_menu): + _add_to_tree(node, _cur_menu) + + +def _shown_menu_nodes(menu): + # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't + # include children of symbols defined with 'menuconfig'. + + def rec(node): + res = [] + + while node: + if _visible(node) or _show_all: + res.append(node) + if node.list and not node.is_menuconfig: + res += rec(node.list) + + elif node.list and isinstance(node.item, Symbol): + shown_children = rec(node.list) + if shown_children: + # Invisible item with visible children + res.append(node) + if not node.is_menuconfig: + res += shown_children + + node = node.next + + return res + + return rec(menu.list) + + +def _visible(node): + # Returns True if the node should appear in the menu (outside show-all + # mode) + + return node.prompt and expr_value(node.prompt[1]) and not \ + (node.item == MENU and not expr_value(node.visibility)) + + +def _add_to_tree(node, top): + # Adds 'node' to the tree, at the end of its menu. We rely on going through + # the nodes linearly to get the correct order. 'top' holds the menu that + # corresponds to the top-level menu, and can vary in single-menu mode. + + parent = node.parent + _tree.move(id(node), "" if parent is top else id(parent), "end") + _tree.item( + id(node), + text=_node_str(node), + # The _show_all test avoids showing invisible items in red outside + # show-all mode, which could look confusing/broken. Invisible symbols + # are shown outside show-all mode if an invisible symbol has visible + # children in an implicit menu. + tags=_img_tag(node) if _visible(node) or not _show_all else + _img_tag(node) + " invisible") + + +def _node_str(node): + # Returns the string shown to the right of the image (if any) for the node + + if node.prompt: + if node.item == COMMENT: + s = "*** {} ***".format(node.prompt[0]) + else: + s = node.prompt[0] + + if isinstance(node.item, Symbol): + sym = node.item + + # Print "(NEW)" next to symbols without a user value (from e.g. a + # .config), but skip it for choice symbols in choices in y mode, + # and for symbols of UNKNOWN type (which generate a warning though) + if sym.user_value is None and sym.type and not \ + (sym.choice and sym.choice.tri_value == 2): + + s += " (NEW)" + + elif isinstance(node.item, Symbol): + # Symbol without prompt (can show up in show-all) + s = "<{}>".format(node.item.name) + + else: + # Choice without prompt. Use standard_sc_expr_str() so that it shows up + # as '<choice (name if any)>'. + s = standard_sc_expr_str(node.item) + + + if isinstance(node.item, Symbol): + sym = node.item + if sym.orig_type == STRING: + s += ": " + sym.str_value + elif sym.orig_type in (INT, HEX): + s = "({}) {}".format(sym.str_value, s) + + elif isinstance(node.item, Choice) and node.item.tri_value == 2: + # Print the prompt of the selected symbol after the choice for + # choices in y mode + sym = node.item.selection + if sym: + for sym_node in sym.nodes: + # Use the prompt used at this choice location, in case the + # choice symbol is defined in multiple locations + if sym_node.parent is node and sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + else: + # If the symbol isn't defined at this choice location, then + # just use whatever prompt we can find for it + for sym_node in sym.nodes: + if sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + + # In single-menu mode, print "--->" next to nodes that have menus that can + # potentially be entered. Print "----" if the menu is empty. We don't allow + # those to be entered. + if _single_menu and node.is_menuconfig: + s += " --->" if _shown_menu_nodes(node) else " ----" + + return s + + +def _img_tag(node): + # Returns the tag for the image that should be shown next to 'node', or the + # empty string if it shouldn't have an image + + item = node.item + + if item in (MENU, COMMENT) or not item.orig_type: + return "" + + if item.orig_type in (STRING, INT, HEX): + return "edit" + + # BOOL or TRISTATE + + if _is_y_mode_choice_sym(item): + # Choice symbol in y-mode choice + return "selected" if item.choice.selection is item else "not-selected" + + if len(item.assignable) <= 1: + # Pinned to a single value + return "" if isinstance(item, Choice) else item.str_value + "-locked" + + if item.type == BOOL: + return item.str_value + "-bool" + + # item.type == TRISTATE + if item.assignable == (1, 2): + return item.str_value + "-my" + return item.str_value + "-tri" + + +def _is_y_mode_choice_sym(item): + # 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 + return isinstance(item, Symbol) and item.choice and item.visibility == 2 + + +def _tree_click(event): + # Click on the Kconfig Treeview + + tree = event.widget + if tree.identify_element(event.x, event.y) == "image": + item = tree.identify_row(event.y) + # Select the item before possibly popping up a dialog for + # string/int/hex items, so that its help is visible + _select(tree, item) + _change_node(_id_to_node[item], tree.winfo_toplevel()) + return "break" + + +def _tree_double_click(event): + # Double-click on the Kconfig treeview + + # Do an extra check to avoid weirdness when double-clicking in the tree + # heading area + if not _in_heading(event): + return _tree_enter(event) + + +def _in_heading(event): + # Returns True if 'event' took place in the tree heading + + tree = event.widget + return hasattr(tree, "identify_region") and \ + tree.identify_region(event.x, event.y) in ("heading", "separator") + + +def _tree_enter(event): + # Enter press or double-click within the Kconfig treeview. Prefer to + # open/close/enter menus, but toggle the value if that's not possible. + + tree = event.widget + sel = tree.focus() + if sel: + node = _id_to_node[sel] + + if tree.get_children(sel): + _tree_toggle_open(sel) + elif _single_menu_mode_menu(node, tree): + _enter_menu_and_select_first(node) + else: + _change_node(node, tree.winfo_toplevel()) + + return "break" + + +def _tree_toggle(event): + # Space press within the Kconfig treeview. Prefer to toggle the value, but + # open/close/enter the menu if that's not possible. + + tree = event.widget + sel = tree.focus() + if sel: + node = _id_to_node[sel] + + if _changeable(node): + _change_node(node, tree.winfo_toplevel()) + elif _single_menu_mode_menu(node, tree): + _enter_menu_and_select_first(node) + elif tree.get_children(sel): + _tree_toggle_open(sel) + + return "break" + + +def _tree_left_key(_): + # Left arrow key press within the Kconfig treeview + + if _single_menu: + # Leave the current menu in single-menu mode + _leave_menu() + return "break" + + # Otherwise, default action + + +def _tree_right_key(_): + # Right arrow key press within the Kconfig treeview + + sel = _tree.focus() + if sel: + node = _id_to_node[sel] + # If the node can be entered in single-menu mode, do it + if _single_menu_mode_menu(node, _tree): + _enter_menu_and_select_first(node) + return "break" + + # Otherwise, default action + + +def _single_menu_mode_menu(node, tree): + # Returns True if single-menu mode is on and 'node' is an (interface) + # menu that can be entered + + return _single_menu and tree is _tree and node.is_menuconfig and \ + _shown_menu_nodes(node) + + +def _changeable(node): + # Returns True if 'node' is a Symbol/Choice whose value can be changed + + sc = node.item + + if not isinstance(sc, (Symbol, Choice)): + return False + + # This will hit for invisible symbols, which appear in show-all mode and + # when an invisible symbol has visible children (which can happen e.g. for + # symbols with optional prompts) + if not (node.prompt and expr_value(node.prompt[1])): + return False + + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ + or _is_y_mode_choice_sym(sc) + + +def _tree_toggle_open(item): + # Opens/closes the Treeview item 'item' + + if _tree.item(item, "open"): + _tree.item(item, open=False) + else: + node = _id_to_node[item] + if not isinstance(node.item, Symbol): + # Can only get here in full-tree mode + _build_full_tree(node) + _tree.item(item, open=True) + + +def _tree_set_val(tri_val): + def tree_set_val(event): + # n/m/y press within the Kconfig treeview + + # Sets the value of the currently selected item to 'tri_val', if that + # value can be assigned + + sel = event.widget.focus() + if sel: + sc = _id_to_node[sel].item + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: + _set_val(sc, tri_val) + + return tree_set_val + + +def _tree_open(_): + # Lazily populates the Kconfig tree when menus are opened in full-tree mode + + if _single_menu: + # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e + # ("ttk::treeview open/closed indicators can be toggled while hidden"). + # Clicking on the hidden indicator will call _build_full_tree() in + # single-menu mode otherwise. + return + + node = _id_to_node[_tree.focus()] + # _shown_full_nodes() includes nodes from menus rooted at symbols, so we + # only need to check "real" menus and choices here + if not isinstance(node.item, Symbol): + _build_full_tree(node) + + +def _update_menu_path(_): + # Updates the displayed menu path when nodes are selected in the Kconfig + # treeview + + sel = _tree.selection() + _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else "" + + +def _item_row(item): + # Returns the row number 'item' appears on within the Kconfig treeview, + # starting from the top of the tree. Used to preserve scrolling. + # + # ttkTreeview.c in the Tk sources defines a RowNumber() function that does + # the same thing, but it's not exposed. + + row = 0 + + while True: + prev = _tree.prev(item) + if prev: + item = prev + row += _n_rows(item) + else: + item = _tree.parent(item) + if not item: + return row + row += 1 + + +def _n_rows(item): + # _item_row() helper. Returns the number of rows occupied by 'item' and # + # its children. + + rows = 1 + + if _tree.item(item, "open"): + for child in _tree.get_children(item): + rows += _n_rows(child) + + return rows + + +def _attached(item): + # Heuristic for checking if a Treeview item is attached. Doesn't seem to be + # good APIs for this. Might fail for super-obscure cases with tiny trees, + # but you'd just get a small scroll mess-up. + + return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item)) + + +def _change_node(node, parent): + # Toggles/changes the value of 'node'. 'parent' is the parent window + # (either the main window or the jump-to dialog), in case we need to pop up + # a dialog. + + if not _changeable(node): + return + + # sc = symbol/choice + sc = node.item + + if sc.type in (INT, HEX, STRING): + s = _set_val_dialog(node, parent) + + # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib + # can't deal with. UTF-8-encode the string to work around it. + if _PY2 and isinstance(s, unicode): + s = s.encode("utf-8", "ignore") + + if s is not None: + _set_val(sc, s) + + elif len(sc.assignable) == 1: + # Handles choice symbols for choices in y mode, which are a special + # case: .assignable can be (2,) while .tri_value is 0. + _set_val(sc, sc.assignable[0]) + + else: + # Set the symbol to the value after the current value in + # sc.assignable, with wrapping + val_index = sc.assignable.index(sc.tri_value) + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) + + +def _set_val(sc, val): + # Wrapper around Symbol/Choice.set_value() for updating the menu state and + # _conf_changed + + # Use the string representation of tristate values. This makes the format + # consistent for all symbol types. + if val in TRI_TO_STR: + val = TRI_TO_STR[val] + + if val != sc.str_value: + sc.set_value(val) + _set_conf_changed(True) + + # Update the tree and try to preserve the scroll. Do a cheaper variant + # than in the show-all case, that might mess up the scroll slightly in + # rare cases, but is fast and flicker-free. + + stayput = _loc_ref_item() # Item to preserve scroll for + old_row = _item_row(stayput) + + _update_tree() + + # If the reference item disappeared (can happen if the change was done + # from the jump-to dialog), then avoid messing with the scroll and hope + # for the best + if _attached(stayput): + _tree.yview_scroll(_item_row(stayput) - old_row, "units") + + if _jump_to_tree: + _update_jump_to_display() + + +def _set_val_dialog(node, parent): + # Pops up a dialog for setting the value of the string/int/hex + # symbol at node 'node'. 'parent' is the parent window. + + def ok(_=None): + # No 'nonlocal' in Python 2 + global _entry_res + + s = entry.get() + if sym.type == HEX and not s.startswith(("0x", "0X")): + s = "0x" + s + + if _check_valid(dialog, entry, sym, s): + _entry_res = s + dialog.destroy() + + def cancel(_=None): + global _entry_res + _entry_res = None + dialog.destroy() + + sym = node.item + + dialog = Toplevel(parent) + dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type])) + dialog.resizable(False, False) + dialog.transient(parent) + dialog.protocol("WM_DELETE_WINDOW", cancel) + + ttk.Label(dialog, text=node.prompt[0] + ":") \ + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c", + pady=".2c .05c") + + entry = ttk.Entry(dialog, width=30) + # Start with the previous value in the editbox, selected + entry.insert(0, sym.str_value) + entry.selection_range(0, "end") + entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c") + entry.focus_set() + + range_info = _range_info(sym) + if range_info: + ttk.Label(dialog, text=range_info) \ + .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c", + pady=".2c 0") + + ttk.Button(dialog, text="OK", command=ok) \ + .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c", + pady=".4c") + + ttk.Button(dialog, text="Cancel", command=cancel) \ + .grid(column=1, row=4 if range_info else 3, padx="0 .3c") + + # Give all horizontal space to the grid cell with the OK button, so that + # Cancel moves to the right + dialog.columnconfigure(0, weight=1) + + _center_on_root(dialog) + + # Hack to scroll the entry so that the end of the text is shown, from + # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail. + # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff + def scroll_entry(_): + _root.update_idletasks() + entry.unbind("<Expose>") + entry.xview_moveto(1) + entry.bind("<Expose>", scroll_entry) + + # The dialog must be visible before we can grab the input + dialog.wait_visibility() + dialog.grab_set() + + dialog.bind("<Return>", ok) + dialog.bind("<KP_Enter>", ok) + dialog.bind("<Escape>", cancel) + + # Wait for the user to be done with the dialog + parent.wait_window(dialog) + + # Regrab the input in the parent + parent.grab_set() + + return _entry_res + + +def _center_on_root(dialog): + # Centers 'dialog' on the root window. It often ends up at some bad place + # like the top-left corner of the screen otherwise. See the menuconfig() + # function, which has similar logic. + + dialog.withdraw() + _root.update_idletasks() + + dialog_width = dialog.winfo_reqwidth() + dialog_height = dialog.winfo_reqheight() + + screen_width = _root.winfo_screenwidth() + screen_height = _root.winfo_screenheight() + + x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2 + y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2 + + # Clamp so that no part of the dialog is outside the screen + if x + dialog_width > screen_width: + x = screen_width - dialog_width + elif x < 0: + x = 0 + if y + dialog_height > screen_height: + y = screen_height - dialog_height + elif y < 0: + y = 0 + + dialog.geometry("+{}+{}".format(x, y)) + + dialog.deiconify() + + +def _check_valid(dialog, entry, sym, s): + # Returns True if the string 's' is a well-formed value for 'sym'. + # Otherwise, pops up an error and returns False. + + if sym.type not in (INT, HEX): + # Anything goes for non-int/hex symbols + return True + + base = 10 if sym.type == INT else 16 + try: + int(s, base) + except ValueError: + messagebox.showerror( + "Bad value", + "'{}' is a malformed {} value".format( + s, TYPE_TO_STR[sym.type]), + parent=dialog) + entry.focus_set() + return False + + for low_sym, high_sym, cond in sym.ranges: + if expr_value(cond): + low_s = low_sym.str_value + high_s = high_sym.str_value + + if not int(low_s, base) <= int(s, base) <= int(high_s, base): + messagebox.showerror( + "Value out of range", + "{} is outside the range {}-{}".format(s, low_s, high_s), + parent=dialog) + entry.focus_set() + return False + + break + + return True + + +def _range_info(sym): + # Returns a string with information about the valid range for the symbol + # 'sym', or None if 'sym' doesn't have a range + + if sym.type in (INT, HEX): + for low, high, cond in sym.ranges: + if expr_value(cond): + return "Range: {}-{}".format(low.str_value, high.str_value) + + return None + + +def _save(_=None): + # Tries to save the configuration + + if _try_save(_kconf.write_config, _conf_filename, "configuration"): + _set_conf_changed(False) + + _tree.focus_set() + + +def _save_as(): + # Pops up a dialog for saving the configuration to a specific location + + global _conf_filename + + filename = _conf_filename + while True: + filename = filedialog.asksaveasfilename( + title="Save configuration as", + initialdir=os.path.dirname(filename), + initialfile=os.path.basename(filename), + parent=_root) + + if not filename: + break + + if _try_save(_kconf.write_config, filename, "configuration"): + _conf_filename = filename + break + + _tree.focus_set() + + +def _save_minimal(): + # Pops up a dialog for saving a minimal configuration (defconfig) to a + # specific location + + global _minconf_filename + + filename = _minconf_filename + while True: + filename = filedialog.asksaveasfilename( + title="Save minimal configuration as", + initialdir=os.path.dirname(filename), + initialfile=os.path.basename(filename), + parent=_root) + + if not filename: + break + + if _try_save(_kconf.write_min_config, filename, + "minimal configuration"): + + _minconf_filename = filename + break + + _tree.focus_set() + + +def _open(_=None): + # Pops up a dialog for loading a configuration + + global _conf_filename + + if _conf_changed and \ + not messagebox.askokcancel( + "Unsaved changes", + "You have unsaved changes. Load new configuration anyway?"): + + return + + filename = _conf_filename + while True: + filename = filedialog.askopenfilename( + title="Open configuration", + initialdir=os.path.dirname(filename), + initialfile=os.path.basename(filename), + parent=_root) + + if not filename: + break + + if _try_load(filename): + # Maybe something fancier could be done here later to try to + # preserve the scroll + + _conf_filename = filename + _set_conf_changed(_needs_save()) + + if _single_menu and not _shown_menu_nodes(_cur_menu): + # Turn on show-all if we're in single-menu mode and would end + # up with an empty menu + _show_all_var.set(True) + + _update_tree() + + break + + _tree.focus_set() + + +def _toggle_showname(_): + # Toggles show-name mode on/off + + _show_name_var.set(not _show_name_var.get()) + _do_showname() + + +def _do_showname(): + # Updates the UI for the current show-name setting + + # Columns do not automatically shrink/expand, so we have to update + # column widths ourselves + + tree_width = _tree.winfo_width() + + if _show_name_var.get(): + _tree["displaycolumns"] = ("name",) + _tree["show"] = "tree headings" + name_width = tree_width//3 + _tree.column("#0", width=max(tree_width - name_width, 1)) + _tree.column("name", width=name_width) + else: + _tree["displaycolumns"] = () + _tree["show"] = "tree" + _tree.column("#0", width=tree_width) + + _tree.focus_set() + + +def _toggle_showall(_): + # Toggles show-all mode on/off + + _show_all_var.set(not _show_all) + _do_showall() + + +def _do_showall(): + # Updates the UI for the current show-all setting + + # Don't allow turning off show-all if we'd end up with no visible nodes + if _nothing_shown(): + _show_all_var.set(True) + return + + # Save scroll information. old_scroll can end up negative here, if the + # reference item isn't shown (only invisible items on the screen, and + # show-all being turned off). + + stayput = _vis_loc_ref_item() + # Probe the middle of the first row, to play it safe. identify_row(0) seems + # to return the row before the top row. + old_scroll = _item_row(stayput) - \ + _item_row(_tree.identify_row(_treeview_rowheight//2)) + + _update_tree() + + if _show_all: + # Deep magic: Unless we call update_idletasks(), the scroll adjustment + # below is restricted to the height of the old tree, instead of the + # height of the new tree. Since the tree with show-all on is guaranteed + # to be taller, and we want the maximum range, we only call it when + # turning show-all on. + # + # Strictly speaking, something similar ought to be done when changing + # symbol values, but it causes annoying flicker, and in 99% of cases + # things work anyway there (with usually minor scroll mess-ups in the + # 1% case). + _root.update_idletasks() + + # Restore scroll + _tree.yview(_item_row(stayput) - old_scroll) + + _tree.focus_set() + + +def _nothing_shown(): + # _do_showall() helper. Returns True if no nodes would get + # shown with the current show-all setting. Also handles the + # (obscure) case when there are no visible nodes in the entire + # tree, meaning guiconfig was automatically started in + # show-all mode, which mustn't be turned off. + + return not _shown_menu_nodes( + _cur_menu if _single_menu else _kconf.top_node) + + +def _toggle_tree_mode(_): + # Toggles single-menu mode on/off + + _single_menu_var.set(not _single_menu) + _do_tree_mode() + + +def _do_tree_mode(): + # Updates the UI for the current tree mode (full-tree or single-menu) + + loc_ref_node = _id_to_node[_loc_ref_item()] + + if not _single_menu: + # _jump_to() -> _enter_menu() already updates the tree, but + # _jump_to() -> load_parents() doesn't, because it isn't always needed. + # We always need to update the tree here, e.g. to add/remove "--->". + _update_tree() + + _jump_to(loc_ref_node) + _tree.focus_set() + + +def _enter_menu_and_select_first(menu): + # Enters the menu 'menu' and selects the first item. Used in single-menu + # mode. + + _enter_menu(menu) + _select(_tree, _tree.get_children()[0]) + + +def _enter_menu(menu): + # Enters the menu 'menu'. Used in single-menu mode. + + global _cur_menu + + _cur_menu = menu + _update_tree() + + _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal" + + +def _leave_menu(): + # Leaves the current menu. Used in single-menu mode. + + global _cur_menu + + if _cur_menu is not _kconf.top_node: + old_menu = _cur_menu + + _cur_menu = _parent_menu(_cur_menu) + _update_tree() + + _select(_tree, id(old_menu)) + + if _cur_menu is _kconf.top_node: + _backbutton["state"] = "disabled" + + _tree.focus_set() + + +def _select(tree, item): + # Selects, focuses, and see()s 'item' in 'tree' + + tree.selection_set(item) + tree.focus(item) + tree.see(item) + + +def _loc_ref_item(): + # Returns a Treeview item that can serve as a reference for the current + # scroll location. We try to make this item stay on the same row on the + # screen when updating the tree. + + # If the selected item is visible, use that + sel = _tree.selection() + if sel and _tree.bbox(sel[0]): + return sel[0] + + # Otherwise, use the middle item on the screen. If it doesn't exist, the + # tree is probably really small, so use the first item in the entire tree. + return _tree.identify_row(_tree.winfo_height()//2) or \ + _tree.get_children()[0] + + +def _vis_loc_ref_item(): + # Like _loc_ref_item(), but finds a visible item around the reference item. + # Used when changing show-all mode, where non-visible (red) items will + # disappear. + + item = _loc_ref_item() + + vis_before = _vis_before(item) + if vis_before and _tree.bbox(vis_before): + return vis_before + + vis_after = _vis_after(item) + if vis_after and _tree.bbox(vis_after): + return vis_after + + return vis_before or vis_after + + +def _vis_before(item): + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, + # searching backwards from 'item'. + + while item: + if not _tree.tag_has("invisible", item): + return item + + prev = _tree.prev(item) + item = prev if prev else _tree.parent(item) + + return None + + +def _vis_after(item): + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, + # searching forwards from 'item'. + + while item: + if not _tree.tag_has("invisible", item): + return item + + next = _tree.next(item) + if next: + item = next + else: + item = _tree.parent(item) + if not item: + break + item = _tree.next(item) + + return None + + +def _on_quit(_=None): + # Called when the user wants to exit + + if not _conf_changed: + _quit("No changes to save (for '{}')".format(_conf_filename)) + return + + while True: + ync = messagebox.askyesnocancel("Quit", "Save changes?") + if ync is None: + return + + if not ync: + _quit("Configuration ({}) was not saved".format(_conf_filename)) + return + + if _try_save(_kconf.write_config, _conf_filename, "configuration"): + # _try_save() already prints the "Configuration saved to ..." + # message + _quit() + return + + +def _quit(msg=None): + # Quits the application + + # Do not call sys.exit() here, in case we're being run from a script + _root.destroy() + if msg: + print(msg) + + +def _try_save(save_fn, filename, description): + # Tries to save a configuration file. Pops up an error and returns False on + # failure. + # + # save_fn: + # Function to call with 'filename' to save the file + # + # description: + # String describing the thing being saved + + try: + # save_fn() returns a message to print + msg = save_fn(filename) + _set_status(msg) + print(msg) + return True + except EnvironmentError as e: + messagebox.showerror( + "Error saving " + description, + "Error saving {} to '{}': {} (errno: {})" + .format(description, e.filename, e.strerror, + errno.errorcode[e.errno])) + return False + + +def _try_load(filename): + # Tries to load a configuration file. Pops up an error and returns False on + # failure. + # + # filename: + # Configuration file to load + + try: + msg = _kconf.load_config(filename) + _set_status(msg) + print(msg) + return True + except EnvironmentError as e: + messagebox.showerror( + "Error loading configuration", + "Error loading '{}': {} (errno: {})" + .format(filename, e.strerror, errno.errorcode[e.errno])) + return False + + +def _jump_to_dialog(_=None): + # Pops up a dialog for jumping directly to a particular node. Symbol values + # can also be changed within the dialog. + # + # Note: There's nothing preventing this from doing an incremental search + # like menuconfig.py does, but currently it's a bit jerky for large Kconfig + # trees, at least when inputting the beginning of the search string. We'd + # need to somehow only update the tree items that are shown in the Treeview + # to fix it. + + global _jump_to_tree + + def search(_=None): + _update_jump_to_matches(msglabel, entry.get()) + + def jump_to_selected(event=None): + # Jumps to the selected node and closes the dialog + + # Ignore double clicks on the image and in the heading area + if event and (tree.identify_element(event.x, event.y) == "image" or + _in_heading(event)): + return + + sel = tree.selection() + if not sel: + return + + node = _id_to_node[sel[0]] + + if node not in _shown_menu_nodes(_parent_menu(node)): + _show_all_var.set(True) + if not _single_menu: + # See comment in _do_tree_mode() + _update_tree() + + _jump_to(node) + + dialog.destroy() + + def tree_select(_): + jumpto_button["state"] = "normal" if tree.selection() else "disabled" + + + dialog = Toplevel(_root) + dialog.geometry("+{}+{}".format( + _root.winfo_rootx() + 50, _root.winfo_rooty() + 50)) + dialog.title("Jump to symbol/choice/menu/comment") + dialog.minsize(128, 128) # See _create_ui() + dialog.transient(_root) + + ttk.Label(dialog, text=_JUMP_TO_HELP) \ + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c", + pady=".1c") + + entry = ttk.Entry(dialog) + entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c") + entry.focus_set() + + entry.bind("<Return>", search) + entry.bind("<KP_Enter>", search) + + ttk.Button(dialog, text="Search", command=search) \ + .grid(column=1, row=1, padx="0 .1c", pady="0 .1c") + + msglabel = ttk.Label(dialog) + msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c") + + panedwindow, tree = _create_kconfig_tree_and_desc(dialog) + panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew") + + # Clear tree + tree.set_children("") + + _jump_to_tree = tree + + jumpto_button = ttk.Button(dialog, text="Jump to selected item", + state="disabled", command=jump_to_selected) + jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c") + + dialog.columnconfigure(0, weight=1) + # Only the pane with the Kconfig tree and description grows vertically + dialog.rowconfigure(3, weight=1) + + # See the menuconfig() function + _root.update_idletasks() + dialog.geometry(dialog.geometry()) + + # The dialog must be visible before we can grab the input + dialog.wait_visibility() + dialog.grab_set() + + tree.bind("<Double-1>", jump_to_selected) + tree.bind("<Return>", jump_to_selected) + tree.bind("<KP_Enter>", jump_to_selected) + # add=True to avoid overriding the description text update + tree.bind("<<TreeviewSelect>>", tree_select, add=True) + + dialog.bind("<Escape>", lambda _: dialog.destroy()) + + # Wait for the user to be done with the dialog + _root.wait_window(dialog) + + _jump_to_tree = None + + _tree.focus_set() + + +def _update_jump_to_matches(msglabel, search_string): + # Searches for nodes matching the search string and updates + # _jump_to_matches. Puts a message in 'msglabel' if there are no matches, + # or regex errors. + + global _jump_to_matches + + _jump_to_tree.selection_set(()) + + try: + # We could use re.IGNORECASE here instead of lower(), but this is + # faster for regexes like '.*debug$' (though the '.*' is redundant + # there). Those probably have bad interactions with re.search(), which + # matches anywhere in the string. + regex_searches = [re.compile(regex).search + for regex in search_string.lower().split()] + except re.error as e: + msg = "Bad regular expression" + # re.error.msg was added in Python 3.5 + if hasattr(e, "msg"): + msg += ": " + e.msg + msglabel["text"] = msg + # Clear tree + _jump_to_tree.set_children("") + return + + _jump_to_matches = [] + add_match = _jump_to_matches.append + + for node in _sorted_sc_nodes(): + # Symbol/choice + sc = node.item + + for search in regex_searches: + # Both the name and the prompt might be missing, since + # we're searching both symbols and choices + + # Does the regex match either the symbol name or the + # prompt (if any)? + if not (sc.name and search(sc.name.lower()) or + node.prompt and search(node.prompt[0].lower())): + + # Give up on the first regex that doesn't match, to + # speed things up a bit when multiple regexes are + # entered + break + + else: + add_match(node) + + # Search menus and comments + + for node in _sorted_menu_comment_nodes(): + for search in regex_searches: + if not search(node.prompt[0].lower()): + break + else: + add_match(node) + + msglabel["text"] = "" if _jump_to_matches else "No matches" + + _update_jump_to_display() + + if _jump_to_matches: + item = id(_jump_to_matches[0]) + _jump_to_tree.selection_set(item) + _jump_to_tree.focus(item) + + +def _update_jump_to_display(): + # Updates the images and text for the items in _jump_to_matches, and sets + # them as the items of _jump_to_tree + + # Micro-optimize a bit + item = _jump_to_tree.item + id_ = id + node_str = _node_str + img_tag = _img_tag + visible = _visible + for node in _jump_to_matches: + item(id_(node), + text=node_str(node), + tags=img_tag(node) if visible(node) else + img_tag(node) + " invisible") + + _jump_to_tree.set_children("", *map(id, _jump_to_matches)) + + +def _jump_to(node): + # Jumps directly to 'node' and selects it + + if _single_menu: + _enter_menu(_parent_menu(node)) + else: + _load_parents(node) + + _select(_tree, id(node)) + + +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing +# to the same list. This avoids a global. +def _sorted_sc_nodes(cached_nodes=[]): + # Returns a sorted list of symbol and choice nodes to search. The symbol + # nodes appear first, sorted by name, and then the choice nodes, sorted by + # prompt and (secondarily) name. + + if not cached_nodes: + # Add symbol nodes + for sym in sorted(_kconf.unique_defined_syms, + key=lambda sym: sym.name): + # += is in-place for lists + cached_nodes += sym.nodes + + # Add choice nodes + + choices = sorted(_kconf.unique_choices, + key=lambda choice: choice.name or "") + + cached_nodes += sorted( + [node for choice in choices for node in choice.nodes], + key=lambda node: node.prompt[0] if node.prompt else "") + + return cached_nodes + + +def _sorted_menu_comment_nodes(cached_nodes=[]): + # Returns a list of menu and comment nodes to search, sorted by prompt, + # with the menus first + + if not cached_nodes: + def prompt_text(mc): + return mc.prompt[0] + + cached_nodes += sorted(_kconf.menus, key=prompt_text) + cached_nodes += sorted(_kconf.comments, key=prompt_text) + + return cached_nodes + + +def _load_parents(node): + # Menus are lazily populated as they're opened in full-tree mode, but + # jumping to an item needs its parent menus to be populated. This function + # populates 'node's parents. + + # Get all parents leading up to 'node', sorted with the root first + parents = [] + cur = node.parent + while cur is not _kconf.top_node: + parents.append(cur) + cur = cur.parent + parents.reverse() + + for i, parent in enumerate(parents): + if not _tree.item(id(parent), "open"): + # Found a closed menu. Populate it and all the remaining menus + # leading up to 'node'. + for parent in parents[i:]: + # We only need to populate "real" menus/choices. Implicit menus + # are populated when their parents menus are entered. + if not isinstance(parent.item, Symbol): + _build_full_tree(parent) + return + + +def _parent_menu(node): + # Returns the menu node of the menu that contains 'node'. In addition to + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. + # "Menu" here means a menu in the interface. + + menu = node.parent + while not menu.is_menuconfig: + menu = menu.parent + return menu + + +def _trace_write(var, fn): + # Makes fn() be called whenever the Tkinter Variable 'var' changes value + + # trace_variable() is deprecated according to the docstring, + # which recommends trace_add() + if hasattr(var, "trace_add"): + var.trace_add("write", fn) + else: + var.trace_variable("w", fn) + + +def _info_str(node): + # Returns information about the menu node 'node' as a string. + # + # The helper functions are responsible for adding newlines. This allows + # them to return "" if they don't want to add any output. + + if isinstance(node.item, Symbol): + sym = node.item + + return ( + _name_info(sym) + + _help_info(sym) + + _direct_dep_info(sym) + + _defaults_info(sym) + + _select_imply_info(sym) + + _kconfig_def_info(sym) + ) + + if isinstance(node.item, Choice): + choice = node.item + + return ( + _name_info(choice) + + _help_info(choice) + + 'Mode: {}\n\n'.format(choice.str_value) + + _choice_syms_info(choice) + + _direct_dep_info(choice) + + _defaults_info(choice) + + _kconfig_def_info(choice) + ) + + # node.item in (MENU, COMMENT) + return _kconfig_def_info(node) + + +def _name_info(sc): + # Returns a string with the name of the symbol/choice. Choices are shown as + # <choice (name if any)>. + + return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n" + + +def _value_info(sym): + # Returns a string showing 'sym's value + + # Only put quotes around the value for string symbols + return "Value: {}\n".format( + '"{}"'.format(sym.str_value) + if sym.orig_type == STRING + else sym.str_value) + + +def _choice_syms_info(choice): + # Returns a string listing the choice symbols in 'choice'. Adds + # "(selected)" next to the selected one. + + s = "Choice symbols:\n" + + for sym in choice.syms: + s += " - " + sym.name + if sym is choice.selection: + s += " (selected)" + s += "\n" + + return s + "\n" + + +def _help_info(sc): + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). + # Symbols and choices defined in multiple locations can have multiple help + # texts. + + s = "" + + for node in sc.nodes: + if node.help is not None: + s += node.help + "\n\n" + + return s + + +def _direct_dep_info(sc): + # Returns a string describing the direct dependencies of 'sc' (Symbol or + # Choice). The direct dependencies are the OR of the dependencies from each + # definition location. The dependencies at each definition location come + # from 'depends on' and dependencies inherited from parent items. + + return "" if sc.direct_dep is _kconf.y else \ + 'Direct dependencies (={}):\n{}\n' \ + .format(TRI_TO_STR[expr_value(sc.direct_dep)], + _split_expr_info(sc.direct_dep, 2)) + + +def _defaults_info(sc): + # Returns a string describing the defaults of 'sc' (Symbol or Choice) + + if not sc.defaults: + return "" + + s = "Default" + if len(sc.defaults) > 1: + s += "s" + s += ":\n" + + for val, cond in sc.orig_defaults: + s += " - " + if isinstance(sc, Symbol): + s += _expr_str(val) + + # Skip the tristate value hint if the expression is just a single + # symbol. _expr_str() already shows its value as a string. + # + # This also avoids showing the tristate value for string/int/hex + # defaults, which wouldn't make any sense. + if isinstance(val, tuple): + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) + else: + # Don't print the value next to the symbol name for choice + # defaults, as it looks a bit confusing + s += val.name + s += "\n" + + if cond is not _kconf.y: + s += " Condition (={}):\n{}" \ + .format(TRI_TO_STR[expr_value(cond)], + _split_expr_info(cond, 4)) + + return s + "\n" + + +def _split_expr_info(expr, indent): + # Returns a string with 'expr' split into its top-level && or || operands, + # with one operand per line, together with the operand's value. This is + # usually enough to get something readable for long expressions. A fancier + # recursive thingy would be possible too. + # + # indent: + # Number of leading spaces to add before the split expression. + + if len(split_expr(expr, AND)) > 1: + split_op = AND + op_str = "&&" + else: + split_op = OR + op_str = "||" + + s = "" + for i, term in enumerate(split_expr(expr, split_op)): + s += "{}{} {}".format(indent*" ", + " " if i == 0 else op_str, + _expr_str(term)) + + # Don't bother showing the value hint if the expression is just a + # single symbol. _expr_str() already shows its value. + if isinstance(term, tuple): + s += " (={})".format(TRI_TO_STR[expr_value(term)]) + + s += "\n" + + return s + + +def _select_imply_info(sym): + # Returns a string with information about which symbols 'select' or 'imply' + # 'sym'. The selecting/implying symbols are grouped according to which + # value they select/imply 'sym' to (n/m/y). + + def sis(expr, val, title): + # sis = selects/implies + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] + if not sis: + return "" + + res = title + for si in sis: + res += " - {}\n".format(split_expr(si, AND)[0].name) + return res + "\n" + + s = "" + + if sym.rev_dep is not _kconf.n: + s += sis(sym.rev_dep, 2, + "Symbols currently y-selecting this symbol:\n") + s += sis(sym.rev_dep, 1, + "Symbols currently m-selecting this symbol:\n") + s += sis(sym.rev_dep, 0, + "Symbols currently n-selecting this symbol (no effect):\n") + + if sym.weak_rev_dep is not _kconf.n: + s += sis(sym.weak_rev_dep, 2, + "Symbols currently y-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 1, + "Symbols currently m-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 0, + "Symbols currently n-implying this symbol (no effect):\n") + + return s + + +def _kconfig_def_info(item): + # Returns a string with the definition of 'item' in Kconfig syntax, + # together with the definition location(s) and their include and menu paths + + nodes = [item] if isinstance(item, MenuNode) else item.nodes + + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ + .format("s" if len(nodes) > 1 else "") + s += (len(s) - 1)*"=" + + for node in nodes: + s += "\n\n" \ + "At {}:{}\n" \ + "{}" \ + "Menu path: {}\n\n" \ + "{}" \ + .format(node.filename, node.linenr, + _include_path_info(node), + _menu_path_info(node), + node.custom_str(_name_and_val_str)) + + return s + + +def _include_path_info(node): + if not node.include_path: + # In the top-level Kconfig file + return "" + + return "Included via {}\n".format( + " -> ".join("{}:{}".format(filename, linenr) + for filename, linenr in node.include_path)) + + +def _menu_path_info(node): + # Returns a string describing the menu path leading up to 'node' + + path = "" + + while node.parent is not _kconf.top_node: + node = node.parent + + # Promptless choices might appear among the parents. Use + # standard_sc_expr_str() for them, so that they show up as + # '<choice (name if any)>'. + path = " -> " + (node.prompt[0] if node.prompt else + standard_sc_expr_str(node.item)) + path + + return "(Top)" + path + + +def _name_and_val_str(sc): + # Custom symbol/choice printer that shows symbol values after symbols + + # Show the values of non-constant (non-quoted) symbols that don't look like + # numbers. Things like 123 are actually symbol references, and only work as + # expected due to undefined symbols getting their name as their value. + # Showing the symbol value for those isn't helpful though. + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): + if not sc.nodes: + # Undefined symbol reference + return "{}(undefined/n)".format(sc.name) + + return '{}(={})'.format(sc.name, sc.str_value) + + # For other items, use the standard format + return standard_sc_expr_str(sc) + + +def _expr_str(expr): + # Custom expression printer that shows symbol values + return expr_str(expr, _name_and_val_str) + + +def _is_num(name): + # Heuristic to see if a symbol name looks like a number, for nicer output + # when printing expressions. Things like 16 are actually symbol names, only + # they get their name as their value when the symbol is undefined. + + try: + int(name) + except ValueError: + if not name.startswith(("0x", "0X")): + return False + + try: + int(name, 16) + except ValueError: + return False + + return True + + +if __name__ == "__main__": + _main() diff --git a/scripts/Kconfiglib/kconfiglib.py b/scripts/Kconfiglib/kconfiglib.py new file mode 100644 index 0000000..a50312e --- /dev/null +++ b/scripts/Kconfiglib/kconfiglib.py @@ -0,0 +1,7160 @@ +# Copyright (c) 2011-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Overview +======== + +Kconfiglib is a Python 2/3 library for scripting and extracting information +from Kconfig (https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt) +configuration systems. + +See the homepage at https://github.com/zephyrproject-rtos/Kconfiglib for a longer +overview. + +Since Kconfiglib 12.0.0, the library version is available in +kconfiglib.VERSION, which is a (<major>, <minor>, <patch>) tuple, e.g. +(12, 0, 0). + + +Using Kconfiglib on the Linux kernel with the Makefile targets +============================================================== + +For the Linux kernel, a handy interface is provided by the +scripts/kconfig/Makefile patch, which can be applied with either 'git am' or +the 'patch' utility: + + $ wget -qO- https://raw.githubusercontent.com/zephyrproject-rtos/Kconfiglib/master/makefile.patch | git am + $ wget -qO- https://raw.githubusercontent.com/zephyrproject-rtos/Kconfiglib/master/makefile.patch | patch -p1 + +Warning: Not passing -p1 to patch will cause the wrong file to be patched. + +Please tell me if the patch does not apply. It should be trivial to apply +manually, as it's just a block of text that needs to be inserted near the other +*conf: targets in scripts/kconfig/Makefile. + +Look further down for a motivation for the Makefile patch and for instructions +on how you can use Kconfiglib without it. + +If you do not wish to install Kconfiglib via pip, the Makefile patch is set up +so that you can also just clone Kconfiglib into the kernel root: + + $ git clone git://github.com/zephyrproject-rtos/Kconfiglib.git + $ git am Kconfiglib/makefile.patch (or 'patch -p1 < Kconfiglib/makefile.patch') + +Warning: The directory name Kconfiglib/ is significant in this case, because +it's added to PYTHONPATH by the new targets in makefile.patch. + +The targets added by the Makefile patch are described in the following +sections. + + +make kmenuconfig +---------------- + +This target runs the curses menuconfig interface with Python 3. As of +Kconfiglib 12.2.0, both Python 2 and Python 3 are supported (previously, only +Python 3 was supported, so this was a backport). + + +make guiconfig +-------------- + +This target runs the Tkinter menuconfig interface. Both Python 2 and Python 3 +are supported. To change the Python interpreter used, pass +PYTHONCMD=<executable> to 'make'. The default is 'python'. + + +make [ARCH=<arch>] iscriptconfig +-------------------------------- + +This target gives an interactive Python prompt where a Kconfig instance has +been preloaded and is available in 'kconf'. To change the Python interpreter +used, pass PYTHONCMD=<executable> to 'make'. The default is 'python'. + +To get a feel for the API, try evaluating and printing the symbols in +kconf.defined_syms, and explore the MenuNode menu tree starting at +kconf.top_node by following 'next' and 'list' pointers. + +The item contained in a menu node is found in MenuNode.item (note that this can +be one of the constants kconfiglib.MENU and kconfiglib.COMMENT), and all +symbols and choices have a 'nodes' attribute containing their menu nodes +(usually only one). Printing a menu node will print its item, in Kconfig +format. + +If you want to look up a symbol by name, use the kconf.syms dictionary. + + +make scriptconfig SCRIPT=<script> [SCRIPT_ARG=<arg>] +---------------------------------------------------- + +This target runs the Python script given by the SCRIPT parameter on the +configuration. sys.argv[1] holds the name of the top-level Kconfig file +(currently always "Kconfig" in practice), and sys.argv[2] holds the SCRIPT_ARG +argument, if given. + +See the examples/ subdirectory for example scripts. + + +make dumpvarsconfig +------------------- + +This target prints a list of all environment variables referenced from the +Kconfig files, together with their values. See the +Kconfiglib/examples/dumpvars.py script. + +Only environment variables that are referenced via the Kconfig preprocessor +$(FOO) syntax are included. The preprocessor was added in Linux 4.18. + + +Using Kconfiglib without the Makefile targets +============================================= + +The make targets are only needed to pick up environment variables exported from +the Kbuild makefiles and referenced inside Kconfig files, via e.g. +'source "arch/$(SRCARCH)/Kconfig" and commands run via '$(shell,...)'. + +These variables are referenced as of writing (Linux 4.18), together with sample +values: + + srctree (.) + ARCH (x86) + SRCARCH (x86) + KERNELVERSION (4.18.0) + CC (gcc) + HOSTCC (gcc) + HOSTCXX (g++) + CC_VERSION_TEXT (gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0) + +Older kernels only reference ARCH, SRCARCH, and KERNELVERSION. + +If your kernel is recent enough (4.18+), you can get a list of referenced +environment variables via 'make dumpvarsconfig' (see above). Note that this +command is added by the Makefile patch. + +To run Kconfiglib without the Makefile patch, set the environment variables +manually: + + $ srctree=. ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` ... python(3) + >>> import kconfiglib + >>> kconf = kconfiglib.Kconfig() # filename defaults to "Kconfig" + +Search the top-level Makefile for "Additional ARCH settings" to see other +possibilities for ARCH and SRCARCH. + + +Intro to symbol values +====================== + +Kconfiglib has the same assignment semantics as the C implementation. + +Any symbol can be assigned a value by the user (via Kconfig.load_config() or +Symbol.set_value()), but this user value is only respected if the symbol is +visible, which corresponds to it (currently) being visible in the menuconfig +interface. + +For symbols with prompts, the visibility of the symbol is determined by the +condition on the prompt. Symbols without prompts are never visible, so setting +a user value on them is pointless. A warning will be printed by default if +Symbol.set_value() is called on a promptless symbol. Assignments to promptless +symbols are normal within a .config file, so no similar warning will be printed +by load_config(). + +Dependencies from parents and 'if'/'depends on' are propagated to properties, +including prompts, so these two configurations are logically equivalent: + +(1) + + menu "menu" + depends on A + + if B + + config FOO + tristate "foo" if D + default y + depends on C + + endif + + endmenu + +(2) + + menu "menu" + depends on A + + config FOO + tristate "foo" if A && B && C && D + default y if A && B && C + + endmenu + +In this example, A && B && C && D (the prompt condition) needs to be non-n for +FOO to be visible (assignable). If its value is m, the symbol can only be +assigned the value m: The visibility sets an upper bound on the value that can +be assigned by the user, and any higher user value will be truncated down. + +'default' properties are independent of the visibility, though a 'default' will +often get the same condition as the prompt due to dependency propagation. +'default' properties are used if the symbol is not visible or has no user +value. + +Symbols with no user value (or that have a user value but are not visible) and +no (active) 'default' default to n for bool/tristate symbols, and to the empty +string for other symbol types. + +'select' works similarly to symbol visibility, but sets a lower bound on the +value of the symbol. The lower bound is determined by the value of the +select*ing* symbol. 'select' does not respect visibility, so non-visible +symbols can be forced to a particular (minimum) value by a select as well. + +For non-bool/tristate symbols, it only matters whether the visibility is n or +non-n: m visibility acts the same as y visibility. + +Conditions on 'default' and 'select' work in mostly intuitive ways. If the +condition is n, the 'default' or 'select' is disabled. If it is m, the +'default' or 'select' value (the value of the selecting symbol) is truncated +down to m. + +When writing a configuration with Kconfig.write_config(), only symbols that are +visible, have an (active) default, or are selected will get written out (note +that this includes all symbols that would accept user values). Kconfiglib +matches the .config format produced by the C implementations down to the +character. This eases testing. + +For a visible bool/tristate symbol FOO with value n, this line is written to +.config: + + # CONFIG_FOO is not set + +The point is to remember the user n selection (which might differ from the +default value the symbol would get), while at the same sticking to the rule +that undefined corresponds to n (.config uses Makefile format, making the line +above a comment). When the .config file is read back in, this line will be +treated the same as the following assignment: + + CONFIG_FOO=n + +In Kconfiglib, the set of (currently) assignable values for a bool/tristate +symbol appear in Symbol.assignable. For other symbol types, just check if +sym.visibility is non-0 (non-n) to see whether the user value will have an +effect. + + +Intro to the menu tree +====================== + +The menu structure, as seen in e.g. menuconfig, is represented by a tree of +MenuNode objects. The top node of the configuration corresponds to an implicit +top-level menu, the title of which is shown at the top in the standard +menuconfig interface. (The title is also available in Kconfig.mainmenu_text in +Kconfiglib.) + +The top node is found in Kconfig.top_node. From there, you can visit child menu +nodes by following the 'list' pointer, and any following menu nodes by +following the 'next' pointer. Usually, a non-None 'list' pointer indicates a +menu or Choice, but menu nodes for symbols can sometimes have a non-None 'list' +pointer too due to submenus created implicitly from dependencies. + +MenuNode.item is either a Symbol or a Choice object, or one of the constants +MENU and COMMENT. The prompt of the menu node can be found in MenuNode.prompt, +which also holds the title for menus and comments. For Symbol and Choice, +MenuNode.help holds the help text (if any, otherwise None). + +Most symbols will only have a single menu node. A symbol defined in multiple +locations will have one menu node for each location. The list of menu nodes for +a Symbol or Choice can be found in the Symbol/Choice.nodes attribute. + +Note that prompts and help texts for symbols and choices are stored in their +menu node(s) rather than in the Symbol or Choice objects themselves. This makes +it possible to define a symbol in multiple locations with a different prompt or +help text in each location. To get the help text or prompt for a symbol with a +single menu node, do sym.nodes[0].help and sym.nodes[0].prompt, respectively. +The prompt is a (text, condition) tuple, where condition determines the +visibility (see 'Intro to expressions' below). + +This organization mirrors the C implementation. MenuNode is called +'struct menu' there, but I thought "menu" was a confusing name. + +It is possible to give a Choice a name and define it in multiple locations, +hence why Choice.nodes is also a list. + +As a convenience, the properties added at a particular definition location are +available on the MenuNode itself, in e.g. MenuNode.defaults. This is helpful +when generating documentation, so that symbols/choices defined in multiple +locations can be shown with the correct properties at each location. + + +Intro to expressions +==================== + +Expressions can be evaluated with the expr_value() function and printed with +the expr_str() function (these are used internally as well). Evaluating an +expression always yields a tristate value, where n, m, and y are represented as +0, 1, and 2, respectively. + +The following table should help you figure out how expressions are represented. +A, B, C, ... are symbols (Symbol instances), NOT is the kconfiglib.NOT +constant, etc. + +Expression Representation +---------- -------------- +A A +"A" A (constant symbol) +!A (NOT, A) +A && B (AND, A, B) +A && B && C (AND, A, (AND, B, C)) +A || B (OR, A, B) +A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) +A = B (EQUAL, A, B) +A != "foo" (UNEQUAL, A, foo (constant symbol)) +A && B = C && D (AND, A, (AND, (EQUAL, B, C), D)) +n Kconfig.n (constant symbol) +m Kconfig.m (constant symbol) +y Kconfig.y (constant symbol) +"y" Kconfig.y (constant symbol) + +Strings like "foo" in 'default "foo"' or 'depends on SYM = "foo"' are +represented as constant symbols, so the only values that appear in expressions +are symbols***. This mirrors the C implementation. + +***For choice symbols, the parent Choice will appear in expressions as well, +but it's usually invisible as the value interfaces of Symbol and Choice are +identical. This mirrors the C implementation and makes different choice modes +"just work". + +Manual evaluation examples: + + - The value of A && B is min(A.tri_value, B.tri_value) + + - The value of A || B is max(A.tri_value, B.tri_value) + + - The value of !A is 2 - A.tri_value + + - The value of A = B is 2 (y) if A.str_value == B.str_value, and 0 (n) + otherwise. Note that str_value is used here instead of tri_value. + + For constant (as well as undefined) symbols, str_value matches the name of + the symbol. This mirrors the C implementation and explains why + 'depends on SYM = "foo"' above works as expected. + +n/m/y are automatically converted to the corresponding constant symbols +"n"/"m"/"y" (Kconfig.n/m/y) during parsing. + +Kconfig.const_syms is a dictionary like Kconfig.syms but for constant symbols. + +If a condition is missing (e.g., <cond> when the 'if <cond>' is removed from +'default A if <cond>'), it is actually Kconfig.y. The standard __str__() +functions just avoid printing 'if y' conditions to give cleaner output. + + +Kconfig extensions +================== + +Kconfiglib includes a couple of Kconfig extensions: + +'source' with relative path +--------------------------- + +The 'rsource' statement sources Kconfig files with a path relative to directory +of the Kconfig file containing the 'rsource' statement, instead of relative to +the project root. + +Consider following directory tree: + + Project + +--Kconfig + | + +--src + +--Kconfig + | + +--SubSystem1 + +--Kconfig + | + +--ModuleA + +--Kconfig + +In this example, assume that src/SubSystem1/Kconfig wants to source +src/SubSystem1/ModuleA/Kconfig. + +With 'source', this statement would be used: + + source "src/SubSystem1/ModuleA/Kconfig" + +With 'rsource', this turns into + + rsource "ModuleA/Kconfig" + +If an absolute path is given to 'rsource', it acts the same as 'source'. + +'rsource' can be used to create "position-independent" Kconfig trees that can +be moved around freely. + + +Globbing 'source' +----------------- + +'source' and 'rsource' accept glob patterns, sourcing all matching Kconfig +files. They require at least one matching file, raising a KconfigError +otherwise. + +For example, the following statement might source sub1/foofoofoo and +sub2/foobarfoo: + + source "sub[12]/foo*foo" + +The glob patterns accepted are the same as for the standard glob.glob() +function. + +Two additional statements are provided for cases where it's acceptable for a +pattern to match no files: 'osource' and 'orsource' (the o is for "optional"). + +For example, the following statements will be no-ops if neither "foo" nor any +files matching "bar*" exist: + + osource "foo" + osource "bar*" + +'orsource' does a relative optional source. + +'source' and 'osource' are analogous to 'include' and '-include' in Make. + + +Generalized def_* keywords +-------------------------- + +def_int, def_hex, and def_string are available in addition to def_bool and +def_tristate, allowing int, hex, and string symbols to be given a type and a +default at the same time. + + +Extra optional warnings +----------------------- + +Some optional warnings can be controlled via environment variables: + + - KCONFIG_WARN_UNDEF: If set to 'y', warnings will be generated for all + references to undefined symbols within Kconfig files. The only gotcha is + that all hex literals must be prefixed with "0x" or "0X", to make it + possible to distinguish them from symbol references. + + Some projects (e.g. the Linux kernel) use multiple Kconfig trees with many + shared Kconfig files, leading to some safe undefined symbol references. + KCONFIG_WARN_UNDEF is useful in projects that only have a single Kconfig + tree though. + + KCONFIG_STRICT is an older alias for this environment variable, supported + for backwards compatibility. + + - KCONFIG_WARN_UNDEF_ASSIGN: If set to 'y', warnings will be generated for + all assignments to undefined symbols within .config files. By default, no + such warnings are generated. + + This warning can also be enabled/disabled via the Kconfig.warn_assign_undef + variable. + + +Preprocessor user functions defined in Python +--------------------------------------------- + +Preprocessor functions can be defined in Python, which makes it simple to +integrate information from existing Python tools into Kconfig (e.g. to have +Kconfig symbols depend on hardware information stored in some other format). + +Putting a Python module named kconfigfunctions(.py) anywhere in sys.path will +cause it to be imported by Kconfiglib (in Kconfig.__init__()). Note that +sys.path can be customized via PYTHONPATH, and includes the directory of the +module being run by default, as well as installation directories. + +If the KCONFIG_FUNCTIONS environment variable is set, it gives a different +module name to use instead of 'kconfigfunctions'. + +The imported module is expected to define a global dictionary named 'functions' +that maps function names to Python functions, as follows: + + def my_fn(kconf, name, arg_1, arg_2, ...): + # kconf: + # Kconfig instance + # + # name: + # Name of the user-defined function ("my-fn"). Think argv[0]. + # + # arg_1, arg_2, ...: + # Arguments passed to the function from Kconfig (strings) + # + # Returns a string to be substituted as the result of calling the + # function + ... + + def my_other_fn(kconf, name, arg_1, arg_2, ...): + ... + + functions = { + "my-fn": (my_fn, <min.args>, <max.args>/None), + "my-other-fn": (my_other_fn, <min.args>, <max.args>/None), + ... + } + + ... + +<min.args> and <max.args> are the minimum and maximum number of arguments +expected by the function (excluding the implicit 'name' argument). If +<max.args> is None, there is no upper limit to the number of arguments. Passing +an invalid number of arguments will generate a KconfigError exception. + +Functions can access the current parsing location as kconf.filename/linenr. +Accessing other fields of the Kconfig object is not safe. See the warning +below. + +Keep in mind that for a variable defined like 'foo = $(fn)', 'fn' will be +called only when 'foo' is expanded. If 'fn' uses the parsing location and the +intent is to use the location of the assignment, you want 'foo := $(fn)' +instead, which calls the function immediately. + +Once defined, user functions can be called from Kconfig in the same way as +other preprocessor functions: + + config FOO + ... + depends on $(my-fn,arg1,arg2) + +If my_fn() returns "n", this will result in + + config FOO + ... + depends on n + +Warning +******* + +User-defined preprocessor functions are called as they're encountered at parse +time, before all Kconfig files have been processed, and before the menu tree +has been finalized. There are no guarantees that accessing Kconfig symbols or +the menu tree via the 'kconf' parameter will work, and it could potentially +lead to a crash. + +Preferably, user-defined functions should be stateless. + + +Feedback +======== + +For bug reports, suggestions, and questions, please open a ticket on the GitHub +page. +""" +import errno +import importlib +import os +import re +import sys + +# Get rid of some attribute lookups. These are obvious in context. +from glob import iglob +from os.path import dirname, exists, expandvars, islink, join, realpath + + +VERSION = (14, 1, 0) + + +# File layout: +# +# Public classes +# Public functions +# Internal functions +# Global constants + +# Line length: 79 columns + + +# +# Public classes +# + + +class Kconfig(object): + """ + Represents a Kconfig configuration, e.g. for x86 or ARM. This is the set of + symbols, choices, and menu nodes appearing in the configuration. Creating + any number of Kconfig objects (including for different architectures) is + safe. Kconfiglib doesn't keep any global state. + + The following attributes are available. They should be treated as + read-only, and some are implemented through @property magic. + + syms: + A dictionary with all symbols in the configuration, indexed by name. Also + includes all symbols that are referenced in expressions but never + defined, except for constant (quoted) symbols. + + Undefined symbols can be recognized by Symbol.nodes being empty -- see + the 'Intro to the menu tree' section in the module docstring. + + const_syms: + A dictionary like 'syms' for constant (quoted) symbols + + named_choices: + A dictionary like 'syms' for named choices (choice FOO) + + defined_syms: + A list with all defined symbols, in the same order as they appear in the + Kconfig files. Symbols defined in multiple locations appear multiple + times. + + Note: You probably want to use 'unique_defined_syms' instead. This + attribute is mostly maintained for backwards compatibility. + + unique_defined_syms: + A list like 'defined_syms', but with duplicates removed. Just the first + instance is kept for symbols defined in multiple locations. Kconfig order + is preserved otherwise. + + Using this attribute instead of 'defined_syms' can save work, and + automatically gives reasonable behavior when writing configuration output + (symbols defined in multiple locations only generate output once, while + still preserving Kconfig order for readability). + + choices: + A list with all choices, in the same order as they appear in the Kconfig + files. + + Note: You probably want to use 'unique_choices' instead. This attribute + is mostly maintained for backwards compatibility. + + unique_choices: + Analogous to 'unique_defined_syms', for choices. Named choices can have + multiple definition locations. + + menus: + A list with all menus, in the same order as they appear in the Kconfig + files + + comments: + A list with all comments, in the same order as they appear in the Kconfig + files + + kconfig_filenames: + A list with the filenames of all Kconfig files included in the + configuration, relative to $srctree (or relative to the current directory + if $srctree isn't set), except absolute paths (e.g. + 'source "/foo/Kconfig"') are kept as-is. + + The files are listed in the order they are source'd, starting with the + top-level Kconfig file. If a file is source'd multiple times, it will + appear multiple times. Use set() to get unique filenames. + + Note that Kconfig.sync_deps() already indirectly catches any file + modifications that change configuration output. + + env_vars: + A set() with the names of all environment variables referenced in the + Kconfig files. + + Only environment variables referenced with the preprocessor $(FOO) syntax + will be registered. The older $FOO syntax is only supported for backwards + compatibility. + + Also note that $(FOO) won't be registered unless the environment variable + $FOO is actually set. If it isn't, $(FOO) is an expansion of an unset + preprocessor variable (which gives the empty string). + + Another gotcha is that environment variables referenced in the values of + recursively expanded preprocessor variables (those defined with =) will + only be registered if the variable is actually used (expanded) somewhere. + + The note from the 'kconfig_filenames' documentation applies here too. + + n/m/y: + The predefined constant symbols n/m/y. Also available in const_syms. + + modules: + The Symbol instance for the modules symbol. Currently hardcoded to + MODULES, which is backwards compatible. Kconfiglib will warn if + 'option modules' is set on some other symbol. Tell me if you need proper + 'option modules' support. + + 'modules' is never None. If the MODULES symbol is not explicitly defined, + its tri_value will be 0 (n), as expected. + + A simple way to enable modules is to do 'kconf.modules.set_value(2)' + (provided the MODULES symbol is defined and visible). Modules are + disabled by default in the kernel Kconfig files as of writing, though + nearly all defconfig files enable them (with 'CONFIG_MODULES=y'). + + defconfig_list: + The Symbol instance for the 'option defconfig_list' symbol, or None if no + defconfig_list symbol exists. The defconfig filename derived from this + symbol can be found in Kconfig.defconfig_filename. + + defconfig_filename: + The filename given by the defconfig_list symbol. This is taken from the + first 'default' with a satisfied condition where the specified file + exists (can be opened for reading). If a defconfig file foo/defconfig is + not found and $srctree was set when the Kconfig was created, + $srctree/foo/defconfig is looked up as well. + + 'defconfig_filename' is None if either no defconfig_list symbol exists, + or if the defconfig_list symbol has no 'default' with a satisfied + condition that specifies a file that exists. + + Gotcha: scripts/kconfig/Makefile might pass --defconfig=<defconfig> to + scripts/kconfig/conf when running e.g. 'make defconfig'. This option + overrides the defconfig_list symbol, meaning defconfig_filename might not + always match what 'make defconfig' would use. + + top_node: + The menu node (see the MenuNode class) of the implicit top-level menu. + Acts as the root of the menu tree. + + mainmenu_text: + The prompt (title) of the top menu (top_node). Defaults to "Main menu". + Can be changed with the 'mainmenu' statement (see kconfig-language.txt). + + variables: + A dictionary with all preprocessor variables, indexed by name. See the + Variable class. + + warn: + Set this variable to True/False to enable/disable warnings. See + Kconfig.__init__(). + + When 'warn' is False, the values of the other warning-related variables + are ignored. + + This variable as well as the other warn* variables can be read to check + the current warning settings. + + warn_to_stderr: + Set this variable to True/False to enable/disable warnings on stderr. See + Kconfig.__init__(). + + warn_assign_undef: + Set this variable to True to generate warnings for assignments to + undefined symbols in configuration files. + + This variable is False by default unless the KCONFIG_WARN_UNDEF_ASSIGN + environment variable was set to 'y' when the Kconfig instance was + created. + + warn_assign_override: + Set this variable to True to generate warnings for multiple assignments + to the same symbol in configuration files, where the assignments set + different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where the + last value would get used). + + This variable is True by default. Disabling it might be useful when + merging configurations. + + warn_assign_redun: + Like warn_assign_override, but for multiple assignments setting a symbol + to the same value. + + This variable is True by default. Disabling it might be useful when + merging configurations. + + warnings: + A list of strings containing all warnings that have been generated, for + cases where more flexibility is needed. + + See the 'warn_to_stderr' parameter to Kconfig.__init__() and the + Kconfig.warn_to_stderr variable as well. Note that warnings still get + added to Kconfig.warnings when 'warn_to_stderr' is True. + + Just as for warnings printed to stderr, only warnings that are enabled + will get added to Kconfig.warnings. See the various Kconfig.warn* + variables. + + missing_syms: + A list with (name, value) tuples for all assignments to undefined symbols + within the most recently loaded .config file(s). 'name' is the symbol + name without the 'CONFIG_' prefix. 'value' is a string that gives the + right-hand side of the assignment verbatim. + + See Kconfig.load_config() as well. + + srctree: + The value the $srctree environment variable had when the Kconfig instance + was created, or the empty string if $srctree wasn't set. This gives nice + behavior with os.path.join(), which treats "" as the current directory, + without adding "./". + + Kconfig files are looked up relative to $srctree (unless absolute paths + are used), and .config files are looked up relative to $srctree if they + are not found in the current directory. This is used to support + out-of-tree builds. The C tools use this environment variable in the same + way. + + Changing $srctree after creating the Kconfig instance has no effect. Only + the value when the configuration is loaded matters. This avoids surprises + if multiple configurations are loaded with different values for $srctree. + + config_prefix: + The value the CONFIG_ environment variable had when the Kconfig instance + was created, or "CONFIG_" if CONFIG_ wasn't set. This is the prefix used + (and expected) on symbol names in .config files and C headers. Used in + the same way in the C tools. + + config_header: + The value the KCONFIG_CONFIG_HEADER environment variable had when the + Kconfig instance was created, or the empty string if + KCONFIG_CONFIG_HEADER wasn't set. This string is inserted verbatim at the + beginning of configuration files. See write_config(). + + header_header: + The value the KCONFIG_AUTOHEADER_HEADER environment variable had when the + Kconfig instance was created, or the empty string if + KCONFIG_AUTOHEADER_HEADER wasn't set. This string is inserted verbatim at + the beginning of header files. See write_autoconf(). + + filename/linenr: + The current parsing location, for use in Python preprocessor functions. + See the module docstring. + """ + __slots__ = ( + "_encoding", + "_functions", + "_set_match", + "_srctree_prefix", + "_unset_match", + "_warn_assign_no_prompt", + "choices", + "comments", + "config_header", + "config_prefix", + "const_syms", + "defconfig_list", + "defined_syms", + "env_vars", + "header_header", + "kconfig_filenames", + "m", + "menus", + "missing_syms", + "modules", + "n", + "named_choices", + "srctree", + "syms", + "top_node", + "unique_choices", + "unique_defined_syms", + "variables", + "warn", + "warn_assign_override", + "warn_assign_redun", + "warn_assign_undef", + "warn_to_stderr", + "warnings", + "y", + + # Parsing-related + "_parsing_kconfigs", + "_readline", + "filename", + "linenr", + "_include_path", + "_filestack", + "_line", + "_tokens", + "_tokens_i", + "_reuse_tokens", + ) + + # + # Public interface + # + + def __init__(self, filename="Kconfig", warn=True, warn_to_stderr=True, + encoding="utf-8", suppress_traceback=False): + """ + Creates a new Kconfig object by parsing Kconfig files. + Note that Kconfig files are not the same as .config files (which store + configuration symbol values). + + See the module docstring for some environment variables that influence + default warning settings (KCONFIG_WARN_UNDEF and + KCONFIG_WARN_UNDEF_ASSIGN). + + Raises KconfigError on syntax/semantic errors, and OSError or (possibly + a subclass of) IOError on IO errors ('errno', 'strerror', and + 'filename' are available). Note that IOError is an alias for OSError on + Python 3, so it's enough to catch OSError there. If you need Python 2/3 + compatibility, it's easiest to catch EnvironmentError, which is a + common base class of OSError/IOError on Python 2 and an alias for + OSError on Python 3. + + filename (default: "Kconfig"): + The Kconfig file to load. For the Linux kernel, you'll want "Kconfig" + from the top-level directory, as environment variables will make sure + the right Kconfig is included from there (arch/$SRCARCH/Kconfig as of + writing). + + If $srctree is set, 'filename' will be looked up relative to it. + $srctree is also used to look up source'd files within Kconfig files. + See the class documentation. + + If you are using Kconfiglib via 'make scriptconfig', the filename of + the base base Kconfig file will be in sys.argv[1]. It's currently + always "Kconfig" in practice. + + warn (default: True): + True if warnings related to this configuration should be generated. + This can be changed later by setting Kconfig.warn to True/False. It + is provided as a constructor argument since warnings might be + generated during parsing. + + See the other Kconfig.warn_* variables as well, which enable or + suppress certain warnings when warnings are enabled. + + All generated warnings are added to the Kconfig.warnings list. See + the class documentation. + + warn_to_stderr (default: True): + True if warnings should be printed to stderr in addition to being + added to Kconfig.warnings. + + This can be changed later by setting Kconfig.warn_to_stderr to + True/False. + + encoding (default: "utf-8"): + The encoding to use when reading and writing files, and when decoding + output from commands run via $(shell). If None, the encoding + specified in the current locale will be used. + + The "utf-8" default avoids exceptions on systems that are configured + to use the C locale, which implies an ASCII encoding. + + This parameter has no effect on Python 2, due to implementation + issues (regular strings turning into Unicode strings, which are + distinct in Python 2). Python 2 doesn't decode regular strings + anyway. + + Related PEP: https://www.python.org/dev/peps/pep-0538/ + + suppress_traceback (default: False): + Helper for tools. When True, any EnvironmentError or KconfigError + generated during parsing is caught, the exception message is printed + to stderr together with the command name, and sys.exit(1) is called + (which generates SystemExit). + + This hides the Python traceback for "expected" errors like syntax + errors in Kconfig files. + + Other exceptions besides EnvironmentError and KconfigError are still + propagated when suppress_traceback is True. + """ + try: + self._init(filename, warn, warn_to_stderr, encoding) + except (EnvironmentError, KconfigError) as e: + if suppress_traceback: + cmd = sys.argv[0] # Empty string if missing + if cmd: + cmd += ": " + # Some long exception messages have extra newlines for better + # formatting when reported as an unhandled exception. Strip + # them here. + sys.exit(cmd + str(e).strip()) + raise + + def _init(self, filename, warn, warn_to_stderr, encoding): + # See __init__() + + self._encoding = encoding + + self.srctree = os.getenv("srctree", "") + # A prefix we can reliably strip from glob() results to get a filename + # relative to $srctree. relpath() can cause issues for symlinks, + # because it assumes symlink/../foo is the same as foo/. + self._srctree_prefix = realpath(self.srctree) + os.sep + + self.warn = warn + self.warn_to_stderr = warn_to_stderr + self.warn_assign_undef = os.getenv("KCONFIG_WARN_UNDEF_ASSIGN") == "y" + self.warn_assign_override = True + self.warn_assign_redun = True + self._warn_assign_no_prompt = True + + self.warnings = [] + + self.config_prefix = os.getenv("CONFIG_", "CONFIG_") + # Regular expressions for parsing .config files + self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)") + self._unset_match = _re_match(r"# {}([^ ]+) is not set".format( + self.config_prefix)) + + self.config_header = os.getenv("KCONFIG_CONFIG_HEADER", "") + self.header_header = os.getenv("KCONFIG_AUTOHEADER_HEADER", "") + + self.syms = {} + self.const_syms = {} + self.defined_syms = [] + self.missing_syms = [] + self.named_choices = {} + self.choices = [] + self.menus = [] + self.comments = [] + + for nmy in "n", "m", "y": + sym = Symbol() + sym.kconfig = self + sym.name = nmy + sym.is_constant = True + sym.orig_type = TRISTATE + sym._cached_tri_val = STR_TO_TRI[nmy] + + self.const_syms[nmy] = sym + + self.n = self.const_syms["n"] + self.m = self.const_syms["m"] + self.y = self.const_syms["y"] + + # Make n/m/y well-formed symbols + for nmy in "n", "m", "y": + sym = self.const_syms[nmy] + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + # Maps preprocessor variables names to Variable instances + self.variables = {} + + # Predefined preprocessor functions, with min/max number of arguments + self._functions = { + "info": (_info_fn, 1, 1), + "error-if": (_error_if_fn, 2, 2), + "filename": (_filename_fn, 0, 0), + "lineno": (_lineno_fn, 0, 0), + "shell": (_shell_fn, 1, 1), + "warning-if": (_warning_if_fn, 2, 2), + } + + # Add any user-defined preprocessor functions + try: + self._functions.update( + importlib.import_module( + os.getenv("KCONFIG_FUNCTIONS", "kconfigfunctions") + ).functions) + except ImportError: + pass + + # This determines whether previously unseen symbols are registered. + # They shouldn't be if we parse expressions after parsing, as part of + # Kconfig.eval_string(). + self._parsing_kconfigs = True + + self.modules = self._lookup_sym("MODULES") + self.defconfig_list = None + + self.top_node = MenuNode() + self.top_node.kconfig = self + self.top_node.item = MENU + self.top_node.is_menuconfig = True + self.top_node.visibility = self.y + self.top_node.prompt = ("Main menu", self.y) + self.top_node.parent = None + self.top_node.dep = self.y + self.top_node.filename = filename + self.top_node.linenr = 1 + self.top_node.include_path = () + + # Parse the Kconfig files + + # Not used internally. Provided as a convenience. + self.kconfig_filenames = [filename] + self.env_vars = set() + + # Keeps track of the location in the parent Kconfig files. Kconfig + # files usually source other Kconfig files. See _enter_file(). + self._filestack = [] + self._include_path = () + + # The current parsing location + self.filename = filename + self.linenr = 0 + + # Used to avoid retokenizing lines when we discover that they're not + # part of the construct currently being parsed. This is kinda like an + # unget operation. + self._reuse_tokens = False + + # Open the top-level Kconfig file. Store the readline() method directly + # as a small optimization. + self._readline = self._open(join(self.srctree, filename), "r").readline + + try: + # Parse the Kconfig files. Returns the last node, which we + # terminate with '.next = None'. + self._parse_block(None, self.top_node, self.top_node).next = None + self.top_node.list = self.top_node.next + self.top_node.next = None + except UnicodeDecodeError as e: + _decoding_error(e, self.filename) + + # Close the top-level Kconfig file. __self__ fetches the 'file' object + # for the method. + self._readline.__self__.close() + + self._parsing_kconfigs = False + + # Do various menu tree post-processing + self._finalize_node(self.top_node, self.y) + + self.unique_defined_syms = _ordered_unique(self.defined_syms) + self.unique_choices = _ordered_unique(self.choices) + + # Do sanity checks. Some of these depend on everything being finalized. + self._check_sym_sanity() + self._check_choice_sanity() + + # KCONFIG_STRICT is an older alias for KCONFIG_WARN_UNDEF, supported + # for backwards compatibility + if os.getenv("KCONFIG_WARN_UNDEF") == "y" or \ + os.getenv("KCONFIG_STRICT") == "y": + + self._check_undef_syms() + + # Build Symbol._dependents for all symbols and choices + self._build_dep() + + # Check for dependency loops + check_dep_loop_sym = _check_dep_loop_sym # Micro-optimization + for sym in self.unique_defined_syms: + check_dep_loop_sym(sym, False) + + # Add extra dependencies from choices to choice symbols that get + # awkward during dependency loop detection + self._add_choice_deps() + + @property + def mainmenu_text(self): + """ + See the class documentation. + """ + return self.top_node.prompt[0] + + @property + def defconfig_filename(self): + """ + See the class documentation. + """ + if self.defconfig_list: + for filename, cond in self.defconfig_list.defaults: + if expr_value(cond): + try: + with self._open_config(filename.str_value) as f: + return f.name + except EnvironmentError: + continue + + return None + + def load_config(self, filename=None, replace=True, verbose=None): + """ + Loads symbol values from a file in the .config format. Equivalent to + calling Symbol.set_value() to set each of the values. + + "# CONFIG_FOO is not set" within a .config file sets the user value of + FOO to n. The C tools work the same way. + + For each symbol, the Symbol.user_value attribute holds the value the + symbol was assigned in the .config file (if any). The user value might + differ from Symbol.str/tri_value if there are unsatisfied dependencies. + + Calling this function also updates the Kconfig.missing_syms attribute + with a list of all assignments to undefined symbols within the + configuration file. Kconfig.missing_syms is cleared if 'replace' is + True, and appended to otherwise. See the documentation for + Kconfig.missing_syms as well. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + filename (default: None): + Path to load configuration from (a string). Respects $srctree if set + (see the class documentation). + + If 'filename' is None (the default), the configuration file to load + (if any) is calculated automatically, giving the behavior you'd + usually want: + + 1. If the KCONFIG_CONFIG environment variable is set, it gives the + path to the configuration file to load. Otherwise, ".config" is + used. See standard_config_filename(). + + 2. If the path from (1.) doesn't exist, the configuration file + given by kconf.defconfig_filename is loaded instead, which is + derived from the 'option defconfig_list' symbol. + + 3. If (1.) and (2.) fail to find a configuration file to load, no + configuration file is loaded, and symbols retain their current + values (e.g., their default values). This is not an error. + + See the return value as well. + + replace (default: True): + If True, all existing user values will be cleared before loading the + .config. Pass False to merge configurations. + + verbose (default: None): + Limited backwards compatibility to prevent crashes. A warning is + printed if anything but None is passed. + + Prior to Kconfiglib 12.0.0, this option enabled printing of messages + to stdout when 'filename' was None. A message is (always) returned + now instead, which is more flexible. + + Will probably be removed in some future version. + + Returns a string with a message saying which file got loaded (or + possibly that no file got loaded, when 'filename' is None). This is + meant to reduce boilerplate in tools, which can do e.g. + print(kconf.load_config()). The returned message distinguishes between + loading (replace == True) and merging (replace == False). + """ + if verbose is not None: + _warn_verbose_deprecated("load_config") + + msg = None + if filename is None: + filename = standard_config_filename() + if not exists(filename) and \ + not exists(join(self.srctree, filename)): + defconfig = self.defconfig_filename + if defconfig is None: + return "Using default symbol values (no '{}')" \ + .format(filename) + + msg = " default configuration '{}' (no '{}')" \ + .format(defconfig, filename) + filename = defconfig + + if not msg: + msg = " configuration '{}'".format(filename) + + # Disable the warning about assigning to symbols without prompts. This + # is normal and expected within a .config file. + self._warn_assign_no_prompt = False + + # This stub only exists to make sure _warn_assign_no_prompt gets + # reenabled + try: + self._load_config(filename, replace) + except UnicodeDecodeError as e: + _decoding_error(e, filename) + finally: + self._warn_assign_no_prompt = True + + return ("Loaded" if replace else "Merged") + msg + + def _load_config(self, filename, replace): + with self._open_config(filename) as f: + if replace: + self.missing_syms = [] + + # If we're replacing the configuration, keep track of which + # symbols and choices got set so that we can unset the rest + # later. This avoids invalidating everything and is faster. + # Another benefit is that invalidation must be rock solid for + # it to work, making it a good test. + + for sym in self.unique_defined_syms: + sym._was_set = False + + for choice in self.unique_choices: + choice._was_set = False + + # Small optimizations + set_match = self._set_match + unset_match = self._unset_match + get_sym = self.syms.get + + for linenr, line in enumerate(f, 1): + # The C tools ignore trailing whitespace + line = line.rstrip() + + match = set_match(line) + if match: + name, val = match.groups() + sym = get_sym(name) + if not sym or not sym.nodes: + self._undef_assign(name, val, filename, linenr) + continue + + if sym.orig_type in _BOOL_TRISTATE: + # The C implementation only checks the first character + # to the right of '=', for whatever reason + if not (sym.orig_type is BOOL + and val.startswith(("y", "n")) or + sym.orig_type is TRISTATE + and val.startswith(("y", "m", "n"))): + self._warn("'{}' is not a valid value for the {} " + "symbol {}. Assignment ignored." + .format(val, TYPE_TO_STR[sym.orig_type], + sym.name_and_loc), + filename, linenr) + continue + + val = val[0] + + if sym.choice and val != "n": + # During .config loading, we infer the mode of the + # choice from the kind of values that are assigned + # to the choice symbols + + prev_mode = sym.choice.user_value + if prev_mode is not None and \ + TRI_TO_STR[prev_mode] != val: + + self._warn("both m and y assigned to symbols " + "within the same choice", + filename, linenr) + + # Set the choice's mode + sym.choice.set_value(val) + + elif sym.orig_type is STRING: + match = _conf_string_match(val) + if not match: + self._warn("malformed string literal in " + "assignment to {}. Assignment ignored." + .format(sym.name_and_loc), + filename, linenr) + continue + + val = unescape(match.group(1)) + + else: + match = unset_match(line) + if not match: + # Print a warning for lines that match neither + # set_match() nor unset_match() and that are not blank + # lines or comments. 'line' has already been + # rstrip()'d, so blank lines show up as "" here. + if line and not line.lstrip().startswith("#"): + self._warn("ignoring malformed line '{}'" + .format(line), + filename, linenr) + + continue + + name = match.group(1) + sym = get_sym(name) + if not sym or not sym.nodes: + self._undef_assign(name, "n", filename, linenr) + continue + + if sym.orig_type not in _BOOL_TRISTATE: + continue + + val = "n" + + # Done parsing the assignment. Set the value. + + if sym._was_set: + self._assigned_twice(sym, val, filename, linenr) + + sym.set_value(val) + + if replace: + # If we're replacing the configuration, unset the symbols that + # didn't get set + + for sym in self.unique_defined_syms: + if not sym._was_set: + sym.unset_value() + + for choice in self.unique_choices: + if not choice._was_set: + choice.unset_value() + + def _undef_assign(self, name, val, filename, linenr): + # Called for assignments to undefined symbols during .config loading + + self.missing_syms.append((name, val)) + if self.warn_assign_undef: + self._warn( + "attempt to assign the value '{}' to the undefined symbol {}" + .format(val, name), filename, linenr) + + def _assigned_twice(self, sym, new_val, filename, linenr): + # Called when a symbol is assigned more than once in a .config file + + # Use strings for bool/tristate user values in the warning + if sym.orig_type in _BOOL_TRISTATE: + user_val = TRI_TO_STR[sym.user_value] + else: + user_val = sym.user_value + + msg = '{} set more than once. Old value "{}", new value "{}".'.format( + sym.name_and_loc, user_val, new_val) + + if user_val == new_val: + if self.warn_assign_redun: + self._warn(msg, filename, linenr) + elif self.warn_assign_override: + self._warn(msg, filename, linenr) + + def load_allconfig(self, filename): + """ + Helper for all*config. Loads (merges) the configuration file specified + by KCONFIG_ALLCONFIG, if any. See Documentation/kbuild/kconfig.txt in + the Linux kernel. + + Disables warnings for duplicated assignments within configuration files + for the duration of the call + (kconf.warn_assign_override/warn_assign_redun = False), and restores + the previous warning settings at the end. The KCONFIG_ALLCONFIG + configuration file is expected to override symbols. + + Exits with sys.exit() (which raises a SystemExit exception) and prints + an error to stderr if KCONFIG_ALLCONFIG is set but the configuration + file can't be opened. + + filename: + Command-specific configuration filename - "allyes.config", + "allno.config", etc. + """ + load_allconfig(self, filename) + + def write_autoconf(self, filename=None, header=None): + r""" + Writes out symbol values as a C header file, matching the format used + by include/generated/autoconf.h in the kernel. + + The ordering of the #defines matches the one generated by + write_config(). The order in the C implementation depends on the hash + table implementation as of writing, and so won't match. + + If 'filename' exists and its contents is identical to what would get + written out, it is left untouched. This avoids updating file metadata + like the modification time and possibly triggering redundant work in + build tools. + + filename (default: None): + Path to write header to. + + If None (the default), the path in the environment variable + KCONFIG_AUTOHEADER is used if set, and "include/generated/autoconf.h" + otherwise. This is compatible with the C tools. + + header (default: None): + Text inserted verbatim at the beginning of the file. You would + usually want it enclosed in '/* */' to make it a C comment, and + include a trailing newline. + + If None (the default), the value of the environment variable + KCONFIG_AUTOHEADER_HEADER had when the Kconfig instance was created + will be used if it was set, and no header otherwise. See the + Kconfig.header_header attribute. + + Returns a string with a message saying that the header got saved, or + that there were no changes to it. This is meant to reduce boilerplate + in tools, which can do e.g. print(kconf.write_autoconf()). + """ + if filename is None: + filename = os.getenv("KCONFIG_AUTOHEADER", + "include/generated/autoconf.h") + + if self._write_if_changed(filename, self._autoconf_contents(header)): + return "Kconfig header saved to '{}'".format(filename) + return "No change to Kconfig header in '{}'".format(filename) + + def _autoconf_contents(self, header): + # write_autoconf() helper. Returns the contents to write as a string, + # with 'header' or KCONFIG_AUTOHEADER_HEADER at the beginning. + + if header is None: + header = self.header_header + + chunks = [header] # "".join()ed later + add = chunks.append + + for sym in self.unique_defined_syms: + # _write_to_conf is determined when the value is calculated. This + # is a hidden function call due to property magic. + # + # Note: In client code, you can check if sym.config_string is empty + # instead, to avoid accessing the internal _write_to_conf variable + # (though it's likely to keep working). + val = sym.str_value + if not sym._write_to_conf: + continue + + if sym.orig_type in _BOOL_TRISTATE: + if val == "y": + add("#define {}{} 1\n" + .format(self.config_prefix, sym.name)) + elif val == "m": + add("#define {}{}_MODULE 1\n" + .format(self.config_prefix, sym.name)) + + elif sym.orig_type is STRING: + add('#define {}{} "{}"\n' + .format(self.config_prefix, sym.name, escape(val))) + + else: # sym.orig_type in _INT_HEX: + if sym.orig_type is HEX and \ + not val.startswith(("0x", "0X")): + val = "0x" + val + + add("#define {}{} {}\n" + .format(self.config_prefix, sym.name, val)) + + return "".join(chunks) + + def write_config(self, filename=None, header=None, save_old=True, + verbose=None): + r""" + Writes out symbol values in the .config format. The format matches the + C implementation, including ordering. + + Symbols appear in the same order in generated .config files as they do + in the Kconfig files. For symbols defined in multiple locations, a + single assignment is written out corresponding to the first location + where the symbol is defined. + + See the 'Intro to symbol values' section in the module docstring to + understand which symbols get written out. + + If 'filename' exists and its contents is identical to what would get + written out, it is left untouched. This avoids updating file metadata + like the modification time and possibly triggering redundant work in + build tools. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + filename (default: None): + Path to write configuration to (a string). + + If None (the default), the path in the environment variable + KCONFIG_CONFIG is used if set, and ".config" otherwise. See + standard_config_filename(). + + header (default: None): + Text inserted verbatim at the beginning of the file. You would + usually want each line to start with '#' to make it a comment, and + include a trailing newline. + + if None (the default), the value of the environment variable + KCONFIG_CONFIG_HEADER had when the Kconfig instance was created will + be used if it was set, and no header otherwise. See the + Kconfig.config_header attribute. + + save_old (default: True): + If True and <filename> already exists, a copy of it will be saved to + <filename>.old in the same directory before the new configuration is + written. + + Errors are silently ignored if <filename>.old cannot be written (e.g. + due to being a directory, or <filename> being something like + /dev/null). + + verbose (default: None): + Limited backwards compatibility to prevent crashes. A warning is + printed if anything but None is passed. + + Prior to Kconfiglib 12.0.0, this option enabled printing of messages + to stdout when 'filename' was None. A message is (always) returned + now instead, which is more flexible. + + Will probably be removed in some future version. + + Returns a string with a message saying which file got saved. This is + meant to reduce boilerplate in tools, which can do e.g. + print(kconf.write_config()). + """ + if verbose is not None: + _warn_verbose_deprecated("write_config") + + if filename is None: + filename = standard_config_filename() + + contents = self._config_contents(header) + if self._contents_eq(filename, contents): + return "No change to configuration in '{}'".format(filename) + + if save_old: + _save_old(filename) + + with self._open(filename, "w") as f: + f.write(contents) + + return "Configuration saved to '{}'".format(filename) + + def _config_contents(self, header): + # write_config() helper. Returns the contents to write as a string, + # with 'header' or KCONFIG_CONFIG_HEADER at the beginning. + # + # More memory friendly would be to 'yield' the strings and + # "".join(_config_contents()), but it was a bit slower on my system. + + # node_iter() was used here before commit 3aea9f7 ("Add '# end of + # <menu>' after menus in .config"). Those comments get tricky to + # implement with it. + + for sym in self.unique_defined_syms: + sym._visited = False + + if header is None: + header = self.config_header + + chunks = [header] # "".join()ed later + add = chunks.append + + # Did we just print an '# end of ...' comment? + after_end_comment = False + + node = self.top_node + while 1: + # Jump to the next node with an iterative tree walk + if node.list: + node = node.list + elif node.next: + node = node.next + else: + while node.parent: + node = node.parent + + # Add a comment when leaving visible menus + if node.item is MENU and expr_value(node.dep) and \ + expr_value(node.visibility) and \ + node is not self.top_node: + add("# end of {}\n".format(node.prompt[0])) + after_end_comment = True + + if node.next: + node = node.next + break + else: + # No more nodes + return "".join(chunks) + + # Generate configuration output for the node + + item = node.item + + if item.__class__ is Symbol: + if item._visited: + continue + item._visited = True + + conf_string = item.config_string + if not conf_string: + continue + + if after_end_comment: + # Add a blank line before the first symbol printed after an + # '# end of ...' comment + after_end_comment = False + add("\n") + add(conf_string) + + elif expr_value(node.dep) and \ + ((item is MENU and expr_value(node.visibility)) or + item is COMMENT): + + add("\n#\n# {}\n#\n".format(node.prompt[0])) + after_end_comment = False + + def write_min_config(self, filename, header=None): + """ + Writes out a "minimal" configuration file, omitting symbols whose value + matches their default value. The format matches the one produced by + 'make savedefconfig'. + + The resulting configuration file is incomplete, but a complete + configuration can be derived from it by loading it. Minimal + configuration files can serve as a more manageable configuration format + compared to a "full" .config file, especially when configurations files + are merged or edited by hand. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + filename: + Path to write minimal configuration to. + + header (default: None): + Text inserted verbatim at the beginning of the file. You would + usually want each line to start with '#' to make it a comment, and + include a final terminating newline. + + if None (the default), the value of the environment variable + KCONFIG_CONFIG_HEADER had when the Kconfig instance was created will + be used if it was set, and no header otherwise. See the + Kconfig.config_header attribute. + + Returns a string with a message saying the minimal configuration got + saved, or that there were no changes to it. This is meant to reduce + boilerplate in tools, which can do e.g. + print(kconf.write_min_config()). + """ + if self._write_if_changed(filename, self._min_config_contents(header)): + return "Minimal configuration saved to '{}'".format(filename) + return "No change to minimal configuration in '{}'".format(filename) + + def _min_config_contents(self, header): + # write_min_config() helper. Returns the contents to write as a string, + # with 'header' or KCONFIG_CONFIG_HEADER at the beginning. + + if header is None: + header = self.config_header + + chunks = [header] # "".join()ed later + add = chunks.append + + for sym in self.unique_defined_syms: + # Skip symbols that cannot be changed. Only check + # non-choice symbols, as selects don't affect choice + # symbols. + if not sym.choice and \ + sym.visibility <= expr_value(sym.rev_dep): + continue + + # Skip symbols whose value matches their default + if sym.str_value == sym._str_default(): + continue + + # Skip symbols that would be selected by default in a + # choice, unless the choice is optional or the symbol type + # isn't bool (it might be possible to set the choice mode + # to n or the symbol to m in those cases). + if sym.choice and \ + not sym.choice.is_optional and \ + sym.choice._selection_from_defaults() is sym and \ + sym.orig_type is BOOL and \ + sym.tri_value == 2: + continue + + add(sym.config_string) + + return "".join(chunks) + + def sync_deps(self, path): + """ + Creates or updates a directory structure that can be used to avoid + doing a full rebuild whenever the configuration is changed, mirroring + include/config/ in the kernel. + + This function is intended to be called during each build, before + compiling source files that depend on configuration symbols. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + path: + Path to directory + + sync_deps(path) does the following: + + 1. If the directory <path> does not exist, it is created. + + 2. If <path>/auto.conf exists, old symbol values are loaded from it, + which are then compared against the current symbol values. If a + symbol has changed value (would generate different output in + autoconf.h compared to before), the change is signaled by + touch'ing a file corresponding to the symbol. + + The first time sync_deps() is run on a directory, <path>/auto.conf + won't exist, and no old symbol values will be available. This + logically has the same effect as updating the entire + configuration. + + The path to a symbol's file is calculated from the symbol's name + by replacing all '_' with '/' and appending '.h'. For example, the + symbol FOO_BAR_BAZ gets the file <path>/foo/bar/baz.h, and FOO + gets the file <path>/foo.h. + + This scheme matches the C tools. The point is to avoid having a + single directory with a huge number of files, which the underlying + filesystem might not handle well. + + 3. A new auto.conf with the current symbol values is written, to keep + track of them for the next build. + + If auto.conf exists and its contents is identical to what would + get written out, it is left untouched. This avoids updating file + metadata like the modification time and possibly triggering + redundant work in build tools. + + + The last piece of the puzzle is knowing what symbols each source file + depends on. Knowing that, dependencies can be added from source files + to the files corresponding to the symbols they depends on. The source + file will then get recompiled (only) when the symbol value changes + (provided sync_deps() is run first during each build). + + The tool in the kernel that extracts symbol dependencies from source + files is scripts/basic/fixdep.c. Missing symbol files also correspond + to "not changed", which fixdep deals with by using the $(wildcard) Make + function when adding symbol prerequisites to source files. + + In case you need a different scheme for your project, the sync_deps() + implementation can be used as a template. + """ + if not exists(path): + os.mkdir(path, 0o755) + + # Load old values from auto.conf, if any + self._load_old_vals(path) + + for sym in self.unique_defined_syms: + # _write_to_conf is determined when the value is calculated. This + # is a hidden function call due to property magic. + # + # Note: In client code, you can check if sym.config_string is empty + # instead, to avoid accessing the internal _write_to_conf variable + # (though it's likely to keep working). + val = sym.str_value + + # n tristate values do not get written to auto.conf and autoconf.h, + # making a missing symbol logically equivalent to n + + if sym._write_to_conf: + if sym._old_val is None and \ + sym.orig_type in _BOOL_TRISTATE and \ + val == "n": + # No old value (the symbol was missing or n), new value n. + # No change. + continue + + if val == sym._old_val: + # New value matches old. No change. + continue + + elif sym._old_val is None: + # The symbol wouldn't appear in autoconf.h (because + # _write_to_conf is false), and it wouldn't have appeared in + # autoconf.h previously either (because it didn't appear in + # auto.conf). No change. + continue + + # 'sym' has a new value. Flag it. + _touch_dep_file(path, sym.name) + + # Remember the current values as the "new old" values. + # + # This call could go anywhere after the call to _load_old_vals(), but + # putting it last means _sync_deps() can be safely rerun if it fails + # before this point. + self._write_old_vals(path) + + def _load_old_vals(self, path): + # Loads old symbol values from auto.conf into a dedicated + # Symbol._old_val field. Mirrors load_config(). + # + # The extra field could be avoided with some trickery involving dumping + # symbol values and restoring them later, but this is simpler and + # faster. The C tools also use a dedicated field for this purpose. + + for sym in self.unique_defined_syms: + sym._old_val = None + + try: + auto_conf = self._open(join(path, "auto.conf"), "r") + except EnvironmentError as e: + if e.errno == errno.ENOENT: + # No old values + return + raise + + with auto_conf as f: + for line in f: + match = self._set_match(line) + if not match: + # We only expect CONFIG_FOO=... (and possibly a header + # comment) in auto.conf + continue + + name, val = match.groups() + if name in self.syms: + sym = self.syms[name] + + if sym.orig_type is STRING: + match = _conf_string_match(val) + if not match: + continue + val = unescape(match.group(1)) + + self.syms[name]._old_val = val + else: + # Flag that the symbol no longer exists, in + # case something still depends on it + _touch_dep_file(path, name) + + def _write_old_vals(self, path): + # Helper for writing auto.conf. Basically just a simplified + # write_config() that doesn't write any comments (including + # '# CONFIG_FOO is not set' comments). The format matches the C + # implementation, though the ordering is arbitrary there (depends on + # the hash table implementation). + # + # A separate helper function is neater than complicating write_config() + # by passing a flag to it, plus we only need to look at symbols here. + + self._write_if_changed( + os.path.join(path, "auto.conf"), + self._old_vals_contents()) + + def _old_vals_contents(self): + # _write_old_vals() helper. Returns the contents to write as a string. + + # Temporary list instead of generator makes this a bit faster + return "".join([ + sym.config_string for sym in self.unique_defined_syms + if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value) + ]) + + def node_iter(self, unique_syms=False): + """ + Returns a generator for iterating through all MenuNode's in the Kconfig + tree. The iteration is done in Kconfig definition order (each node is + visited before its children, and the children of a node are visited + before the next node). + + The Kconfig.top_node menu node is skipped. It contains an implicit menu + that holds the top-level items. + + As an example, the following code will produce a list equal to + Kconfig.defined_syms: + + defined_syms = [node.item for node in kconf.node_iter() + if isinstance(node.item, Symbol)] + + unique_syms (default: False): + If True, only the first MenuNode will be included for symbols defined + in multiple locations. + + Using kconf.node_iter(True) in the example above would give a list + equal to unique_defined_syms. + """ + if unique_syms: + for sym in self.unique_defined_syms: + sym._visited = False + + node = self.top_node + while 1: + # Jump to the next node with an iterative tree walk + if node.list: + node = node.list + elif node.next: + node = node.next + else: + while node.parent: + node = node.parent + if node.next: + node = node.next + break + else: + # No more nodes + return + + if unique_syms and node.item.__class__ is Symbol: + if node.item._visited: + continue + node.item._visited = True + + yield node + + def eval_string(self, s): + """ + Returns the tristate value of the expression 's', represented as 0, 1, + and 2 for n, m, and y, respectively. Raises KconfigError on syntax + errors. Warns if undefined symbols are referenced. + + As an example, if FOO and BAR are tristate symbols at least one of + which has the value y, then eval_string("y && (FOO || BAR)") returns + 2 (y). + + To get the string value of non-bool/tristate symbols, use + Symbol.str_value. eval_string() always returns a tristate value, and + all non-bool/tristate symbols have the tristate value 0 (n). + + The expression parsing is consistent with how parsing works for + conditional ('if ...') expressions in the configuration, and matches + the C implementation. m is rewritten to 'm && MODULES', so + eval_string("m") will return 0 (n) unless modules are enabled. + """ + # The parser is optimized to be fast when parsing Kconfig files (where + # an expression can never appear at the beginning of a line). We have + # to monkey-patch things a bit here to reuse it. + + self.filename = None + + self._tokens = self._tokenize("if " + s) + # Strip "if " to avoid giving confusing error messages + self._line = s + self._tokens_i = 1 # Skip the 'if' token + + return expr_value(self._expect_expr_and_eol()) + + def unset_values(self): + """ + Removes any user values from all symbols, as if Kconfig.load_config() + or Symbol.set_value() had never been called. + """ + self._warn_assign_no_prompt = False + try: + # set_value() already rejects undefined symbols, and they don't + # need to be invalidated (because their value never changes), so we + # can just iterate over defined symbols + for sym in self.unique_defined_syms: + sym.unset_value() + + for choice in self.unique_choices: + choice.unset_value() + finally: + self._warn_assign_no_prompt = True + + def enable_warnings(self): + """ + Do 'Kconfig.warn = True' instead. Maintained for backwards + compatibility. + """ + self.warn = True + + def disable_warnings(self): + """ + Do 'Kconfig.warn = False' instead. Maintained for backwards + compatibility. + """ + self.warn = False + + def enable_stderr_warnings(self): + """ + Do 'Kconfig.warn_to_stderr = True' instead. Maintained for backwards + compatibility. + """ + self.warn_to_stderr = True + + def disable_stderr_warnings(self): + """ + Do 'Kconfig.warn_to_stderr = False' instead. Maintained for backwards + compatibility. + """ + self.warn_to_stderr = False + + def enable_undef_warnings(self): + """ + Do 'Kconfig.warn_assign_undef = True' instead. Maintained for backwards + compatibility. + """ + self.warn_assign_undef = True + + def disable_undef_warnings(self): + """ + Do 'Kconfig.warn_assign_undef = False' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_undef = False + + def enable_override_warnings(self): + """ + Do 'Kconfig.warn_assign_override = True' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_override = True + + def disable_override_warnings(self): + """ + Do 'Kconfig.warn_assign_override = False' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_override = False + + def enable_redun_warnings(self): + """ + Do 'Kconfig.warn_assign_redun = True' instead. Maintained for backwards + compatibility. + """ + self.warn_assign_redun = True + + def disable_redun_warnings(self): + """ + Do 'Kconfig.warn_assign_redun = False' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_redun = False + + def __repr__(self): + """ + Returns a string with information about the Kconfig object when it is + evaluated on e.g. the interactive Python prompt. + """ + def status(flag): + return "enabled" if flag else "disabled" + + return "<{}>".format(", ".join(( + "configuration with {} symbols".format(len(self.syms)), + 'main menu prompt "{}"'.format(self.mainmenu_text), + "srctree is current directory" if not self.srctree else + 'srctree "{}"'.format(self.srctree), + 'config symbol prefix "{}"'.format(self.config_prefix), + "warnings " + status(self.warn), + "printing of warnings to stderr " + status(self.warn_to_stderr), + "undef. symbol assignment warnings " + + status(self.warn_assign_undef), + "overriding symbol assignment warnings " + + status(self.warn_assign_override), + "redundant symbol assignment warnings " + + status(self.warn_assign_redun) + ))) + + # + # Private methods + # + + + # + # File reading + # + + def _open_config(self, filename): + # Opens a .config file. First tries to open 'filename', then + # '$srctree/filename' if $srctree was set when the configuration was + # loaded. + + try: + return self._open(filename, "r") + except EnvironmentError as e: + # This will try opening the same file twice if $srctree is unset, + # but it's not a big deal + try: + return self._open(join(self.srctree, filename), "r") + except EnvironmentError as e2: + # This is needed for Python 3, because e2 is deleted after + # the try block: + # + # https://docs.python.org/3/reference/compound_stmts.html#the-try-statement + e = e2 + + raise _KconfigIOError( + e, "Could not open '{}' ({}: {}). Check that the $srctree " + "environment variable ({}) is set correctly." + .format(filename, errno.errorcode[e.errno], e.strerror, + "set to '{}'".format(self.srctree) if self.srctree + else "unset or blank")) + + def _enter_file(self, filename): + # Jumps to the beginning of a sourced Kconfig file, saving the previous + # position and file object. + # + # filename: + # Absolute path to file + + # Path relative to $srctree, stored in e.g. self.filename (which makes + # it indirectly show up in MenuNode.filename). Equals 'filename' for + # absolute paths passed to 'source'. + if filename.startswith(self._srctree_prefix): + # Relative path (or a redundant absolute path to within $srctree, + # but it's probably fine to reduce those too) + rel_filename = filename[len(self._srctree_prefix):] + else: + # Absolute path + rel_filename = filename + + self.kconfig_filenames.append(rel_filename) + + # The parent Kconfig files are represented as a list of + # (<include path>, <Python 'file' object for Kconfig file>) tuples. + # + # <include path> is immutable and holds a *tuple* of + # (<filename>, <linenr>) tuples, giving the locations of the 'source' + # statements in the parent Kconfig files. The current include path is + # also available in Kconfig._include_path. + # + # The point of this redundant setup is to allow Kconfig._include_path + # to be assigned directly to MenuNode.include_path without having to + # copy it, sharing it wherever possible. + + # Save include path and 'file' object (via its 'readline' function) + # before entering the file + self._filestack.append((self._include_path, self._readline)) + + # _include_path is a tuple, so this rebinds the variable instead of + # doing in-place modification + self._include_path += ((self.filename, self.linenr),) + + # Check for recursive 'source' + for name, _ in self._include_path: + if name == rel_filename: + raise KconfigError( + "\n{}:{}: recursive 'source' of '{}' detected. Check that " + "environment variables are set correctly.\n" + "Include path:\n{}" + .format(self.filename, self.linenr, rel_filename, + "\n".join("{}:{}".format(name, linenr) + for name, linenr in self._include_path))) + + try: + self._readline = self._open(filename, "r").readline + except EnvironmentError as e: + # We already know that the file exists + raise _KconfigIOError( + e, "{}:{}: Could not open '{}' (in '{}') ({}: {})" + .format(self.filename, self.linenr, filename, + self._line.strip(), + errno.errorcode[e.errno], e.strerror)) + + self.filename = rel_filename + self.linenr = 0 + + def _leave_file(self): + # Returns from a Kconfig file to the file that sourced it. See + # _enter_file(). + + # Restore location from parent Kconfig file + self.filename, self.linenr = self._include_path[-1] + # Restore include path and 'file' object + self._readline.__self__.close() # __self__ fetches the 'file' object + self._include_path, self._readline = self._filestack.pop() + + def _next_line(self): + # Fetches and tokenizes the next line from the current Kconfig file. + # Returns False at EOF and True otherwise. + + # We might already have tokens from parsing a line and discovering that + # it's part of a different construct + if self._reuse_tokens: + self._reuse_tokens = False + # self._tokens_i is known to be 1 here, because _parse_props() + # leaves it like that when it can't recognize a line (or parses a + # help text) + return True + + # readline() returns '' over and over at EOF, which we rely on for help + # texts at the end of files (see _line_after_help()) + line = self._readline() + if not line: + return False + self.linenr += 1 + + # Handle line joining + while line.endswith("\\\n"): + line = line[:-2] + self._readline() + self.linenr += 1 + + self._tokens = self._tokenize(line) + # Initialize to 1 instead of 0 to factor out code from _parse_block() + # and _parse_props(). They immediately fetch self._tokens[0]. + self._tokens_i = 1 + + return True + + def _line_after_help(self, line): + # Tokenizes a line after a help text. This case is special in that the + # line has already been fetched (to discover that it isn't part of the + # help text). + # + # An earlier version used a _saved_line variable instead that was + # checked in _next_line(). This special-casing gets rid of it and makes + # _reuse_tokens alone sufficient to handle unget. + + # Handle line joining + while line.endswith("\\\n"): + line = line[:-2] + self._readline() + self.linenr += 1 + + self._tokens = self._tokenize(line) + self._reuse_tokens = True + + def _write_if_changed(self, filename, contents): + # Writes 'contents' into 'filename', but only if it differs from the + # current contents of the file. + # + # Another variant would be write a temporary file on the same + # filesystem, compare the files, and rename() the temporary file if it + # differs, but it breaks stuff like write_config("/dev/null"), which is + # used out there to force evaluation-related warnings to be generated. + # This simple version is pretty failsafe and portable. + # + # Returns True if the file has changed and is updated, and False + # otherwise. + + if self._contents_eq(filename, contents): + return False + with self._open(filename, "w") as f: + f.write(contents) + return True + + def _contents_eq(self, filename, contents): + # Returns True if the contents of 'filename' is 'contents' (a string), + # and False otherwise (including if 'filename' can't be opened/read) + + try: + with self._open(filename, "r") as f: + # Robust re. things like encoding and line endings (mmap() + # trickery isn't) + return f.read(len(contents) + 1) == contents + except EnvironmentError: + # If the error here would prevent writing the file as well, we'll + # notice it later + return False + + # + # Tokenization + # + + def _lookup_sym(self, name): + # Fetches the symbol 'name' from the symbol table, creating and + # registering it if it does not exist. If '_parsing_kconfigs' is False, + # it means we're in eval_string(), and new symbols won't be registered. + + if name in self.syms: + return self.syms[name] + + sym = Symbol() + sym.kconfig = self + sym.name = name + sym.is_constant = False + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + if self._parsing_kconfigs: + self.syms[name] = sym + else: + self._warn("no symbol {} in configuration".format(name)) + + return sym + + def _lookup_const_sym(self, name): + # Like _lookup_sym(), for constant (quoted) symbols + + if name in self.const_syms: + return self.const_syms[name] + + sym = Symbol() + sym.kconfig = self + sym.name = name + sym.is_constant = True + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + if self._parsing_kconfigs: + self.const_syms[name] = sym + + return sym + + def _tokenize(self, s): + # Parses 's', returning a None-terminated list of tokens. Registers any + # new symbols encountered with _lookup(_const)_sym(). + # + # Tries to be reasonably speedy by processing chunks of text via + # regexes and string operations where possible. This is the biggest + # hotspot during parsing. + # + # It might be possible to rewrite this to 'yield' tokens instead, + # working across multiple lines. Lookback and compatibility with old + # janky versions of the C tools complicate things though. + + self._line = s # Used for error reporting + + # Initial token on the line + match = _command_match(s) + if not match: + if s.isspace() or s.lstrip().startswith("#"): + return (None,) + self._parse_error("unknown token at start of line") + + # Tricky implementation detail: While parsing a token, 'token' refers + # to the previous token. See _STRING_LEX for why this is needed. + token = _get_keyword(match.group(1)) + if not token: + # Backwards compatibility with old versions of the C tools, which + # (accidentally) accepted stuff like "--help--" and "-help---". + # This was fixed in the C tools by commit c2264564 ("kconfig: warn + # of unhandled characters in Kconfig commands"), committed in July + # 2015, but it seems people still run Kconfiglib on older kernels. + if s.strip(" \t\n-") == "help": + return (_T_HELP, None) + + # If the first token is not a keyword (and not a weird help token), + # we have a preprocessor variable assignment (or a bare macro on a + # line) + self._parse_assignment(s) + return (None,) + + tokens = [token] + # The current index in the string being tokenized + i = match.end() + + # Main tokenization loop (for tokens past the first one) + while i < len(s): + # Test for an identifier/keyword first. This is the most common + # case. + match = _id_keyword_match(s, i) + if match: + # We have an identifier or keyword + + # Check what it is. lookup_sym() will take care of allocating + # new symbols for us the first time we see them. Note that + # 'token' still refers to the previous token. + + name = match.group(1) + keyword = _get_keyword(name) + if keyword: + # It's a keyword + token = keyword + # Jump past it + i = match.end() + + elif token not in _STRING_LEX: + # It's a non-const symbol, except we translate n, m, and y + # into the corresponding constant symbols, like the C + # implementation + + if "$" in name: + # Macro expansion within symbol name + name, s, i = self._expand_name(s, i) + else: + i = match.end() + + token = self.const_syms[name] if name in STR_TO_TRI else \ + self._lookup_sym(name) + + else: + # It's a case of missing quotes. For example, the + # following is accepted: + # + # menu unquoted_title + # + # config A + # tristate unquoted_prompt + # + # endmenu + # + # Named choices ('choice FOO') also end up here. + + if token is not _T_CHOICE: + self._warn("style: quotes recommended around '{}' in '{}'" + .format(name, self._line.strip()), + self.filename, self.linenr) + + token = name + i = match.end() + + else: + # Neither a keyword nor a non-const symbol + + # We always strip whitespace after tokens, so it is safe to + # assume that s[i] is the start of a token here. + c = s[i] + + if c in "\"'": + if "$" not in s and "\\" not in s: + # Fast path for lines without $ and \. Find the + # matching quote. + end_i = s.find(c, i + 1) + 1 + if not end_i: + self._parse_error("unterminated string") + val = s[i + 1:end_i - 1] + i = end_i + else: + # Slow path + s, end_i = self._expand_str(s, i) + + # os.path.expandvars() and the $UNAME_RELEASE replace() + # is a backwards compatibility hack, which should be + # reasonably safe as expandvars() leaves references to + # undefined env. vars. as is. + # + # The preprocessor functionality changed how + # environment variables are referenced, to $(FOO). + val = expandvars(s[i + 1:end_i - 1] + .replace("$UNAME_RELEASE", + _UNAME_RELEASE)) + + i = end_i + + # This is the only place where we don't survive with a + # single token of lookback: 'option env="FOO"' does not + # refer to a constant symbol named "FOO". + token = \ + val if token in _STRING_LEX or tokens[0] is _T_OPTION \ + else self._lookup_const_sym(val) + + elif s.startswith("&&", i): + token = _T_AND + i += 2 + + elif s.startswith("||", i): + token = _T_OR + i += 2 + + elif c == "=": + token = _T_EQUAL + i += 1 + + elif s.startswith("!=", i): + token = _T_UNEQUAL + i += 2 + + elif c == "!": + token = _T_NOT + i += 1 + + elif c == "(": + token = _T_OPEN_PAREN + i += 1 + + elif c == ")": + token = _T_CLOSE_PAREN + i += 1 + + elif c == "#": + break + + + # Very rare + + elif s.startswith("<=", i): + token = _T_LESS_EQUAL + i += 2 + + elif c == "<": + token = _T_LESS + i += 1 + + elif s.startswith(">=", i): + token = _T_GREATER_EQUAL + i += 2 + + elif c == ">": + token = _T_GREATER + i += 1 + + + else: + self._parse_error("unknown tokens in line") + + + # Skip trailing whitespace + while i < len(s) and s[i].isspace(): + i += 1 + + + # Add the token + tokens.append(token) + + # None-terminating the token list makes token fetching simpler/faster + tokens.append(None) + + return tokens + + # Helpers for syntax checking and token fetching. See the + # 'Intro to expressions' section for what a constant symbol is. + # + # More of these could be added, but the single-use cases are inlined as an + # optimization. + + def _expect_sym(self): + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + + if token.__class__ is not Symbol: + self._parse_error("expected symbol") + + return token + + def _expect_nonconst_sym(self): + # Used for 'select' and 'imply' only. We know the token indices. + + token = self._tokens[1] + self._tokens_i = 2 + + if token.__class__ is not Symbol or token.is_constant: + self._parse_error("expected nonconstant symbol") + + return token + + def _expect_str_and_eol(self): + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + + if token.__class__ is not str: + self._parse_error("expected string") + + if self._tokens[self._tokens_i] is not None: + self._trailing_tokens_error() + + return token + + def _expect_expr_and_eol(self): + expr = self._parse_expr(True) + + if self._tokens[self._tokens_i] is not None: + self._trailing_tokens_error() + + return expr + + def _check_token(self, token): + # If the next token is 'token', removes it and returns True + + if self._tokens[self._tokens_i] is token: + self._tokens_i += 1 + return True + return False + + # + # Preprocessor logic + # + + def _parse_assignment(self, s): + # Parses a preprocessor variable assignment, registering the variable + # if it doesn't already exist. Also takes care of bare macros on lines + # (which are allowed, and can be useful for their side effects). + + # Expand any macros in the left-hand side of the assignment (the + # variable name) + s = s.lstrip() + i = 0 + while 1: + i = _assignment_lhs_fragment_match(s, i).end() + if s.startswith("$(", i): + s, i = self._expand_macro(s, i, ()) + else: + break + + if s.isspace(): + # We also accept a bare macro on a line (e.g. + # $(warning-if,$(foo),ops)), provided it expands to a blank string + return + + # Assigned variable + name = s[:i] + + + # Extract assignment operator (=, :=, or +=) and value + rhs_match = _assignment_rhs_match(s, i) + if not rhs_match: + self._parse_error("syntax error") + + op, val = rhs_match.groups() + + + if name in self.variables: + # Already seen variable + var = self.variables[name] + else: + # New variable + var = Variable() + var.kconfig = self + var.name = name + var._n_expansions = 0 + self.variables[name] = var + + # += acts like = on undefined variables (defines a recursive + # variable) + if op == "+=": + op = "=" + + if op == "=": + var.is_recursive = True + var.value = val + elif op == ":=": + var.is_recursive = False + var.value = self._expand_whole(val, ()) + else: # op == "+=" + # += does immediate expansion if the variable was last set + # with := + var.value += " " + (val if var.is_recursive else + self._expand_whole(val, ())) + + def _expand_whole(self, s, args): + # Expands preprocessor macros in all of 's'. Used whenever we don't + # have to worry about delimiters. See _expand_macro() re. the 'args' + # parameter. + # + # Returns the expanded string. + + i = 0 + while 1: + i = s.find("$(", i) + if i == -1: + break + s, i = self._expand_macro(s, i, args) + return s + + def _expand_name(self, s, i): + # Expands a symbol name starting at index 'i' in 's'. + # + # Returns the expanded name, the expanded 's' (including the part + # before the name), and the index of the first character in the next + # token after the name. + + s, end_i = self._expand_name_iter(s, i) + name = s[i:end_i] + # isspace() is False for empty strings + if not name.strip(): + # Avoid creating a Kconfig symbol with a blank name. It's almost + # guaranteed to be an error. + self._parse_error("macro expanded to blank string") + + # Skip trailing whitespace + while end_i < len(s) and s[end_i].isspace(): + end_i += 1 + + return name, s, end_i + + def _expand_name_iter(self, s, i): + # Expands a symbol name starting at index 'i' in 's'. + # + # Returns the expanded 's' (including the part before the name) and the + # index of the first character after the expanded name in 's'. + + while 1: + match = _name_special_search(s, i) + + if match.group() != "$(": + return (s, match.start()) + s, i = self._expand_macro(s, match.start(), ()) + + def _expand_str(self, s, i): + # Expands a quoted string starting at index 'i' in 's'. Handles both + # backslash escapes and macro expansion. + # + # Returns the expanded 's' (including the part before the string) and + # the index of the first character after the expanded string in 's'. + + quote = s[i] + i += 1 # Skip over initial "/' + while 1: + match = _string_special_search(s, i) + if not match: + self._parse_error("unterminated string") + + + if match.group() == quote: + # Found the end of the string + return (s, match.end()) + + elif match.group() == "\\": + # Replace '\x' with 'x'. 'i' ends up pointing to the character + # after 'x', which allows macros to be canceled with '\$(foo)'. + i = match.end() + s = s[:match.start()] + s[i:] + + elif match.group() == "$(": + # A macro call within the string + s, i = self._expand_macro(s, match.start(), ()) + + else: + # A ' quote within " quotes or vice versa + i += 1 + + def _expand_macro(self, s, i, args): + # Expands a macro starting at index 'i' in 's'. If this macro resulted + # from the expansion of another macro, 'args' holds the arguments + # passed to that macro. + # + # Returns the expanded 's' (including the part before the macro) and + # the index of the first character after the expanded macro in 's'. + + res = s[:i] + i += 2 # Skip over "$(" + + arg_start = i # Start of current macro argument + new_args = [] # Arguments of this macro call + nesting = 0 # Current parentheses nesting level + + while 1: + match = _macro_special_search(s, i) + if not match: + self._parse_error("missing end parenthesis in macro expansion") + + + if match.group() == "(": + nesting += 1 + i = match.end() + + elif match.group() == ")": + if nesting: + nesting -= 1 + i = match.end() + continue + + # Found the end of the macro + + new_args.append(s[arg_start:match.start()]) + + # $(1) is replaced by the first argument to the function, etc., + # provided at least that many arguments were passed + + try: + # Does the macro look like an integer, with a corresponding + # argument? If so, expand it to the value of the argument. + res += args[int(new_args[0])] + except (ValueError, IndexError): + # Regular variables are just functions without arguments, + # and also go through the function value path + res += self._fn_val(new_args) + + return (res + s[match.end():], len(res)) + + elif match.group() == ",": + i = match.end() + if nesting: + continue + + # Found the end of a macro argument + new_args.append(s[arg_start:match.start()]) + arg_start = i + + else: # match.group() == "$(" + # A nested macro call within the macro + s, i = self._expand_macro(s, match.start(), args) + + def _fn_val(self, args): + # Returns the result of calling the function args[0] with the arguments + # args[1..len(args)-1]. Plain variables are treated as functions + # without arguments. + + fn = args[0] + + if fn in self.variables: + var = self.variables[fn] + + if len(args) == 1: + # Plain variable + if var._n_expansions: + self._parse_error("Preprocessor variable {} recursively " + "references itself".format(var.name)) + elif var._n_expansions > 100: + # Allow functions to call themselves, but guess that functions + # that are overly recursive are stuck + self._parse_error("Preprocessor function {} seems stuck " + "in infinite recursion".format(var.name)) + + var._n_expansions += 1 + res = self._expand_whole(self.variables[fn].value, args) + var._n_expansions -= 1 + return res + + if fn in self._functions: + # Built-in or user-defined function + + py_fn, min_arg, max_arg = self._functions[fn] + + if len(args) - 1 < min_arg or \ + (max_arg is not None and len(args) - 1 > max_arg): + + if min_arg == max_arg: + expected_args = min_arg + elif max_arg is None: + expected_args = "{} or more".format(min_arg) + else: + expected_args = "{}-{}".format(min_arg, max_arg) + + raise KconfigError("{}:{}: bad number of arguments in call " + "to {}, expected {}, got {}" + .format(self.filename, self.linenr, fn, + expected_args, len(args) - 1)) + + return py_fn(self, *args) + + # Environment variables are tried last + if fn in os.environ: + self.env_vars.add(fn) + return os.environ[fn] + + return "" + + # + # Parsing + # + + def _make_and(self, e1, e2): + # Constructs an AND (&&) expression. Performs trivial simplification. + + if e1 is self.y: + return e2 + + if e2 is self.y: + return e1 + + if e1 is self.n or e2 is self.n: + return self.n + + return (AND, e1, e2) + + def _make_or(self, e1, e2): + # Constructs an OR (||) expression. Performs trivial simplification. + + if e1 is self.n: + return e2 + + if e2 is self.n: + return e1 + + if e1 is self.y or e2 is self.y: + return self.y + + return (OR, e1, e2) + + def _parse_block(self, end_token, parent, prev): + # Parses a block, which is the contents of either a file or an if, + # menu, or choice statement. + # + # end_token: + # The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. + # None for files. + # + # parent: + # The parent menu node, corresponding to a menu, Choice, or 'if'. + # 'if's are flattened after parsing. + # + # prev: + # The previous menu node. New nodes will be added after this one (by + # modifying 'next' pointers). + # + # 'prev' is reused to parse a list of child menu nodes (for a menu or + # Choice): After parsing the children, the 'next' pointer is assigned + # to the 'list' pointer to "tilt up" the children above the node. + # + # Returns the final menu node in the block (or 'prev' if the block is + # empty). This allows chaining. + + while self._next_line(): + t0 = self._tokens[0] + + if t0 is _T_CONFIG or t0 is _T_MENUCONFIG: + # The tokenizer allocates Symbol objects for us + sym = self._tokens[1] + + if sym.__class__ is not Symbol or sym.is_constant: + self._parse_error("missing or bad symbol name") + + if self._tokens[2] is not None: + self._trailing_tokens_error() + + self.defined_syms.append(sym) + + node = MenuNode() + node.kconfig = self + node.item = sym + node.is_menuconfig = (t0 is _T_MENUCONFIG) + node.prompt = node.help = node.list = None + node.parent = parent + node.filename = self.filename + node.linenr = self.linenr + node.include_path = self._include_path + + sym.nodes.append(node) + + self._parse_props(node) + + if node.is_menuconfig and not node.prompt: + self._warn("the menuconfig symbol {} has no prompt" + .format(sym.name_and_loc)) + + # Equivalent to + # + # prev.next = node + # prev = node + # + # due to tricky Python semantics. The order matters. + prev.next = prev = node + + elif t0 is None: + # Blank line + continue + + elif t0 in _SOURCE_TOKENS: + pattern = self._expect_str_and_eol() + + if t0 in _REL_SOURCE_TOKENS: + # Relative source + pattern = join(dirname(self.filename), pattern) + + # - glob() doesn't support globbing relative to a directory, so + # we need to prepend $srctree to 'pattern'. Use join() + # instead of '+' so that an absolute path in 'pattern' is + # preserved. + # + # - Sort the glob results to ensure a consistent ordering of + # Kconfig symbols, which indirectly ensures a consistent + # ordering in e.g. .config files + filenames = sorted(iglob(join(self._srctree_prefix, pattern))) + + if not filenames and t0 in _OBL_SOURCE_TOKENS: + raise KconfigError( + "{}:{}: '{}' not found (in '{}'). Check that " + "environment variables are set correctly (e.g. " + "$srctree, which is {}). Also note that unset " + "environment variables expand to the empty string." + .format(self.filename, self.linenr, pattern, + self._line.strip(), + "set to '{}'".format(self.srctree) + if self.srctree else "unset or blank")) + + for filename in filenames: + self._enter_file(filename) + prev = self._parse_block(None, parent, prev) + self._leave_file() + + elif t0 is end_token: + # Reached the end of the block. Terminate the final node and + # return it. + + if self._tokens[1] is not None: + self._trailing_tokens_error() + + prev.next = None + return prev + + elif t0 is _T_IF: + node = MenuNode() + node.item = node.prompt = None + node.parent = parent + node.dep = self._expect_expr_and_eol() + + self._parse_block(_T_ENDIF, node, node) + node.list = node.next + + prev.next = prev = node + + elif t0 is _T_MENU: + node = MenuNode() + node.kconfig = self + node.item = t0 # _T_MENU == MENU + node.is_menuconfig = True + node.prompt = (self._expect_str_and_eol(), self.y) + node.visibility = self.y + node.parent = parent + node.filename = self.filename + node.linenr = self.linenr + node.include_path = self._include_path + + self.menus.append(node) + + self._parse_props(node) + self._parse_block(_T_ENDMENU, node, node) + node.list = node.next + + prev.next = prev = node + + elif t0 is _T_COMMENT: + node = MenuNode() + node.kconfig = self + node.item = t0 # _T_COMMENT == COMMENT + node.is_menuconfig = False + node.prompt = (self._expect_str_and_eol(), self.y) + node.list = None + node.parent = parent + node.filename = self.filename + node.linenr = self.linenr + node.include_path = self._include_path + + self.comments.append(node) + + self._parse_props(node) + + prev.next = prev = node + + elif t0 is _T_CHOICE: + if self._tokens[1] is None: + choice = Choice() + choice.direct_dep = self.n + else: + # Named choice + name = self._expect_str_and_eol() + choice = self.named_choices.get(name) + if not choice: + choice = Choice() + choice.name = name + choice.direct_dep = self.n + self.named_choices[name] = choice + + self.choices.append(choice) + + node = MenuNode() + node.kconfig = choice.kconfig = self + node.item = choice + node.is_menuconfig = True + node.prompt = node.help = None + node.parent = parent + node.filename = self.filename + node.linenr = self.linenr + node.include_path = self._include_path + + choice.nodes.append(node) + + self._parse_props(node) + self._parse_block(_T_ENDCHOICE, node, node) + node.list = node.next + + prev.next = prev = node + + elif t0 is _T_MAINMENU: + self.top_node.prompt = (self._expect_str_and_eol(), self.y) + + else: + # A valid endchoice/endif/endmenu is caught by the 'end_token' + # check above + self._parse_error( + "no corresponding 'choice'" if t0 is _T_ENDCHOICE else + "no corresponding 'if'" if t0 is _T_ENDIF else + "no corresponding 'menu'" if t0 is _T_ENDMENU else + "unrecognized construct") + + # End of file reached. Return the last node. + + if end_token: + raise KconfigError( + "error: expected '{}' at end of '{}'" + .format("endchoice" if end_token is _T_ENDCHOICE else + "endif" if end_token is _T_ENDIF else + "endmenu", + self.filename)) + + return prev + + def _parse_cond(self): + # Parses an optional 'if <expr>' construct and returns the parsed + # <expr>, or self.y if the next token is not _T_IF + + expr = self._parse_expr(True) if self._check_token(_T_IF) else self.y + + if self._tokens[self._tokens_i] is not None: + self._trailing_tokens_error() + + return expr + + def _parse_props(self, node): + # Parses and adds properties to the MenuNode 'node' (type, 'prompt', + # 'default's, etc.) Properties are later copied up to symbols and + # choices in a separate pass after parsing, in e.g. + # _add_props_to_sym(). + # + # An older version of this code added properties directly to symbols + # and choices instead of to their menu nodes (and handled dependency + # propagation simultaneously), but that loses information on where a + # property is added when a symbol or choice is defined in multiple + # locations. Some Kconfig configuration systems rely heavily on such + # symbols, and better docs can be generated by keeping track of where + # properties are added. + # + # node: + # The menu node we're parsing properties on + + # Dependencies from 'depends on'. Will get propagated to the properties + # below. + node.dep = self.y + + while self._next_line(): + t0 = self._tokens[0] + + if t0 in _TYPE_TOKENS: + # Relies on '_T_BOOL is BOOL', etc., to save a conversion + self._set_type(node.item, t0) + if self._tokens[1] is not None: + self._parse_prompt(node) + + elif t0 is _T_DEPENDS: + if not self._check_token(_T_ON): + self._parse_error("expected 'on' after 'depends'") + + node.dep = self._make_and(node.dep, + self._expect_expr_and_eol()) + + elif t0 is _T_HELP: + self._parse_help(node) + + elif t0 is _T_SELECT: + if node.item.__class__ is not Symbol: + self._parse_error("only symbols can select") + + node.selects.append((self._expect_nonconst_sym(), + self._parse_cond())) + + elif t0 is None: + # Blank line + continue + + elif t0 is _T_DEFAULT: + node.defaults.append((self._parse_expr(False), + self._parse_cond())) + + elif t0 in _DEF_TOKEN_TO_TYPE: + self._set_type(node.item, _DEF_TOKEN_TO_TYPE[t0]) + node.defaults.append((self._parse_expr(False), + self._parse_cond())) + + elif t0 is _T_PROMPT: + self._parse_prompt(node) + + elif t0 is _T_RANGE: + node.ranges.append((self._expect_sym(), self._expect_sym(), + self._parse_cond())) + + elif t0 is _T_IMPLY: + if node.item.__class__ is not Symbol: + self._parse_error("only symbols can imply") + + node.implies.append((self._expect_nonconst_sym(), + self._parse_cond())) + + elif t0 is _T_VISIBLE: + if not self._check_token(_T_IF): + self._parse_error("expected 'if' after 'visible'") + + node.visibility = self._make_and(node.visibility, + self._expect_expr_and_eol()) + + elif t0 is _T_OPTION: + if self._check_token(_T_ENV): + if not self._check_token(_T_EQUAL): + self._parse_error("expected '=' after 'env'") + + env_var = self._expect_str_and_eol() + node.item.env_var = env_var + + if env_var in os.environ: + node.defaults.append( + (self._lookup_const_sym(os.environ[env_var]), + self.y)) + else: + self._warn("{1} has 'option env=\"{0}\"', " + "but the environment variable {0} is not " + "set".format(node.item.name, env_var), + self.filename, self.linenr) + + if env_var != node.item.name: + self._warn("Kconfiglib expands environment variables " + "in strings directly, meaning you do not " + "need 'option env=...' \"bounce\" symbols. " + "For compatibility with the C tools, " + "rename {} to {} (so that the symbol name " + "matches the environment variable name)." + .format(node.item.name, env_var), + self.filename, self.linenr) + + elif self._check_token(_T_DEFCONFIG_LIST): + if not self.defconfig_list: + self.defconfig_list = node.item + else: + self._warn("'option defconfig_list' set on multiple " + "symbols ({0} and {1}). Only {0} will be " + "used.".format(self.defconfig_list.name, + node.item.name), + self.filename, self.linenr) + + elif self._check_token(_T_MODULES): + # To reduce warning spam, only warn if 'option modules' is + # set on some symbol that isn't MODULES, which should be + # safe. I haven't run into any projects that make use + # modules besides the kernel yet, and there it's likely to + # keep being called "MODULES". + if node.item is not self.modules: + self._warn("the 'modules' option is not supported. " + "Let me know if this is a problem for you, " + "as it wouldn't be that hard to implement. " + "Note that modules are supported -- " + "Kconfiglib just assumes the symbol name " + "MODULES, like older versions of the C " + "implementation did when 'option modules' " + "wasn't used.", + self.filename, self.linenr) + + elif self._check_token(_T_ALLNOCONFIG_Y): + if node.item.__class__ is not Symbol: + self._parse_error("the 'allnoconfig_y' option is only " + "valid for symbols") + + node.item.is_allnoconfig_y = True + + else: + self._parse_error("unrecognized option") + + elif t0 is _T_OPTIONAL: + if node.item.__class__ is not Choice: + self._parse_error('"optional" is only valid for choices') + + node.item.is_optional = True + + else: + # Reuse the tokens for the non-property line later + self._reuse_tokens = True + return + + def _set_type(self, sc, new_type): + # Sets the type of 'sc' (symbol or choice) to 'new_type' + + # UNKNOWN is falsy + if sc.orig_type and sc.orig_type is not new_type: + self._warn("{} defined with multiple types, {} will be used" + .format(sc.name_and_loc, TYPE_TO_STR[new_type])) + + sc.orig_type = new_type + + def _parse_prompt(self, node): + # 'prompt' properties override each other within a single definition of + # a symbol, but additional prompts can be added by defining the symbol + # multiple times + + if node.prompt: + self._warn(node.item.name_and_loc + + " defined with multiple prompts in single location") + + prompt = self._tokens[1] + self._tokens_i = 2 + + if prompt.__class__ is not str: + self._parse_error("expected prompt string") + + if prompt != prompt.strip(): + self._warn(node.item.name_and_loc + + " has leading or trailing whitespace in its prompt") + + # This avoid issues for e.g. reStructuredText documentation, where + # '*prompt *' is invalid + prompt = prompt.strip() + + node.prompt = (prompt, self._parse_cond()) + + def _parse_help(self, node): + if node.help is not None: + self._warn(node.item.name_and_loc + " defined with more than " + "one help text -- only the last one will be used") + + # Micro-optimization. This code is pretty hot. + readline = self._readline + + # Find first non-blank (not all-space) line and get its + # indentation + + while 1: + line = readline() + self.linenr += 1 + if not line: + self._empty_help(node, line) + return + if not line.isspace(): + break + + len_ = len # Micro-optimization + + # Use a separate 'expline' variable here and below to avoid stomping on + # any tabs people might've put deliberately into the first line after + # the help text + expline = line.expandtabs() + indent = len_(expline) - len_(expline.lstrip()) + if not indent: + self._empty_help(node, line) + return + + # The help text goes on till the first non-blank line with less indent + # than the first line + + # Add the first line + lines = [expline[indent:]] + add_line = lines.append # Micro-optimization + + while 1: + line = readline() + if line.isspace(): + # No need to preserve the exact whitespace in these + add_line("\n") + elif not line: + # End of file + break + else: + expline = line.expandtabs() + if len_(expline) - len_(expline.lstrip()) < indent: + break + add_line(expline[indent:]) + + self.linenr += len_(lines) + node.help = "".join(lines).rstrip() + if line: + self._line_after_help(line) + + def _empty_help(self, node, line): + self._warn(node.item.name_and_loc + + " has 'help' but empty help text") + node.help = "" + if line: + self._line_after_help(line) + + def _parse_expr(self, transform_m): + # Parses an expression from the tokens in Kconfig._tokens using a + # simple top-down approach. See the module docstring for the expression + # format. + # + # transform_m: + # True if m should be rewritten to m && MODULES. See the + # Kconfig.eval_string() documentation. + + # Grammar: + # + # expr: and_expr ['||' expr] + # and_expr: factor ['&&' and_expr] + # factor: <symbol> ['='/'!='/'<'/... <symbol>] + # '!' factor + # '(' expr ')' + # + # It helps to think of the 'expr: and_expr' case as a single-operand OR + # (no ||), and of the 'and_expr: factor' case as a single-operand AND + # (no &&). Parsing code is always a bit tricky. + + # Mind dump: parse_factor() and two nested loops for OR and AND would + # work as well. The straightforward implementation there gives a + # (op, (op, (op, A, B), C), D) parse for A op B op C op D. Representing + # expressions as (op, [list of operands]) instead goes nicely with that + # version, but is wasteful for short expressions and complicates + # expression evaluation and other code that works on expressions (more + # complicated code likely offsets any performance gain from less + # recursion too). If we also try to optimize the list representation by + # merging lists when possible (e.g. when ANDing two AND expressions), + # we end up allocating a ton of lists instead of reusing expressions, + # which is bad. + + and_expr = self._parse_and_expr(transform_m) + + # Return 'and_expr' directly if we have a "single-operand" OR. + # Otherwise, parse the expression on the right and make an OR node. + # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). + return and_expr if not self._check_token(_T_OR) else \ + (OR, and_expr, self._parse_expr(transform_m)) + + def _parse_and_expr(self, transform_m): + factor = self._parse_factor(transform_m) + + # Return 'factor' directly if we have a "single-operand" AND. + # Otherwise, parse the right operand and make an AND node. This turns + # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). + return factor if not self._check_token(_T_AND) else \ + (AND, factor, self._parse_and_expr(transform_m)) + + def _parse_factor(self, transform_m): + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + + if token.__class__ is Symbol: + # Plain symbol or relation + + if self._tokens[self._tokens_i] not in _RELATIONS: + # Plain symbol + + # For conditional expressions ('depends on <expr>', + # '... if <expr>', etc.), m is rewritten to m && MODULES. + if transform_m and token is self.m: + return (AND, self.m, self.modules) + + return token + + # Relation + # + # _T_EQUAL, _T_UNEQUAL, etc., deliberately have the same values as + # EQUAL, UNEQUAL, etc., so we can just use the token directly + self._tokens_i += 1 + return (self._tokens[self._tokens_i - 1], token, + self._expect_sym()) + + if token is _T_NOT: + # token == _T_NOT == NOT + return (token, self._parse_factor(transform_m)) + + if token is _T_OPEN_PAREN: + expr_parse = self._parse_expr(transform_m) + if self._check_token(_T_CLOSE_PAREN): + return expr_parse + + self._parse_error("malformed expression") + + # + # Caching and invalidation + # + + def _build_dep(self): + # Populates the Symbol/Choice._dependents sets, which contain all other + # items (symbols and choices) that immediately depend on the item in + # the sense that changing the value of the item might affect the value + # of the dependent items. This is used for caching/invalidation. + # + # The calculated sets might be larger than necessary as we don't do any + # complex analysis of the expressions. + + depend_on = _depend_on # Micro-optimization + + # Only calculate _dependents for defined symbols. Constant and + # undefined symbols could theoretically be selected/implied, but it + # wouldn't change their value, so it's not a true dependency. + for sym in self.unique_defined_syms: + # Symbols depend on the following: + + # The prompt conditions + for node in sym.nodes: + if node.prompt: + depend_on(sym, node.prompt[1]) + + # The default values and their conditions + for value, cond in sym.defaults: + depend_on(sym, value) + depend_on(sym, cond) + + # The reverse and weak reverse dependencies + depend_on(sym, sym.rev_dep) + depend_on(sym, sym.weak_rev_dep) + + # The ranges along with their conditions + for low, high, cond in sym.ranges: + depend_on(sym, low) + depend_on(sym, high) + depend_on(sym, cond) + + # The direct dependencies. This is usually redundant, as the direct + # dependencies get propagated to properties, but it's needed to get + # invalidation solid for 'imply', which only checks the direct + # dependencies (even if there are no properties to propagate it + # to). + depend_on(sym, sym.direct_dep) + + # In addition to the above, choice symbols depend on the choice + # they're in, but that's handled automatically since the Choice is + # propagated to the conditions of the properties before + # _build_dep() runs. + + for choice in self.unique_choices: + # Choices depend on the following: + + # The prompt conditions + for node in choice.nodes: + if node.prompt: + depend_on(choice, node.prompt[1]) + + # The default symbol conditions + for _, cond in choice.defaults: + depend_on(choice, cond) + + def _add_choice_deps(self): + # Choices also depend on the choice symbols themselves, because the + # y-mode selection of the choice might change if a choice symbol's + # visibility changes. + # + # We add these dependencies separately after dependency loop detection. + # The invalidation algorithm can handle the resulting + # <choice symbol> <-> <choice> dependency loops, but they make loop + # detection awkward. + + for choice in self.unique_choices: + for sym in choice.syms: + sym._dependents.add(choice) + + def _invalidate_all(self): + # Undefined symbols never change value and don't need to be + # invalidated, so we can just iterate over defined symbols. + # Invalidating constant symbols would break things horribly. + for sym in self.unique_defined_syms: + sym._invalidate() + + for choice in self.unique_choices: + choice._invalidate() + + # + # Post-parsing menu tree processing, including dependency propagation and + # implicit submenu creation + # + + def _finalize_node(self, node, visible_if): + # Finalizes a menu node and its children: + # + # - Copies properties from menu nodes up to their contained + # symbols/choices + # + # - Propagates dependencies from parent to child nodes + # + # - Creates implicit menus (see kconfig-language.txt) + # + # - Removes 'if' nodes + # + # - Sets 'choice' types and registers choice symbols + # + # menu_finalize() in the C implementation is similar. + # + # node: + # The menu node to finalize. This node and its children will have + # been finalized when the function returns, and any implicit menus + # will have been created. + # + # visible_if: + # Dependencies from 'visible if' on parent menus. These are added to + # the prompts of symbols and choices. + + if node.item.__class__ is Symbol: + # Copy defaults, ranges, selects, and implies to the Symbol + self._add_props_to_sym(node) + + # Find any items that should go in an implicit menu rooted at the + # symbol + cur = node + while cur.next and _auto_menu_dep(node, cur.next): + # This makes implicit submenu creation work recursively, with + # implicit menus inside implicit menus + self._finalize_node(cur.next, visible_if) + cur = cur.next + cur.parent = node + + if cur is not node: + # Found symbols that should go in an implicit submenu. Tilt + # them up above us. + node.list = node.next + node.next = cur.next + cur.next = None + + elif node.list: + # The menu node is a choice, menu, or if. Finalize each child node. + + if node.item is MENU: + visible_if = self._make_and(visible_if, node.visibility) + + # Propagate the menu node's dependencies to each child menu node. + # + # This needs to go before the recursive _finalize_node() call so + # that implicit submenu creation can look ahead at dependencies. + self._propagate_deps(node, visible_if) + + # Finalize the children + cur = node.list + while cur: + self._finalize_node(cur, visible_if) + cur = cur.next + + if node.list: + # node's children have been individually finalized. Do final steps + # to finalize this "level" in the menu tree. + _flatten(node.list) + _remove_ifs(node) + + # Empty choices (node.list None) are possible, so this needs to go + # outside + if node.item.__class__ is Choice: + # Add the node's non-node-specific properties to the choice, like + # _add_props_to_sym() does + choice = node.item + choice.direct_dep = self._make_or(choice.direct_dep, node.dep) + choice.defaults += node.defaults + + _finalize_choice(node) + + def _propagate_deps(self, node, visible_if): + # Propagates 'node's dependencies to its child menu nodes + + # If the parent node holds a Choice, we use the Choice itself as the + # parent dependency. This makes sense as the value (mode) of the choice + # limits the visibility of the contained choice symbols. The C + # implementation works the same way. + # + # Due to the similar interface, Choice works as a drop-in replacement + # for Symbol here. + basedep = node.item if node.item.__class__ is Choice else node.dep + + cur = node.list + while cur: + dep = cur.dep = self._make_and(cur.dep, basedep) + + if cur.item.__class__ in _SYMBOL_CHOICE: + # Propagate 'visible if' and dependencies to the prompt + if cur.prompt: + cur.prompt = (cur.prompt[0], + self._make_and( + cur.prompt[1], + self._make_and(visible_if, dep))) + + # Propagate dependencies to defaults + if cur.defaults: + cur.defaults = [(default, self._make_and(cond, dep)) + for default, cond in cur.defaults] + + # Propagate dependencies to ranges + if cur.ranges: + cur.ranges = [(low, high, self._make_and(cond, dep)) + for low, high, cond in cur.ranges] + + # Propagate dependencies to selects + if cur.selects: + cur.selects = [(target, self._make_and(cond, dep)) + for target, cond in cur.selects] + + # Propagate dependencies to implies + if cur.implies: + cur.implies = [(target, self._make_and(cond, dep)) + for target, cond in cur.implies] + + elif cur.prompt: # Not a symbol/choice + # Propagate dependencies to the prompt. 'visible if' is only + # propagated to symbols/choices. + cur.prompt = (cur.prompt[0], + self._make_and(cur.prompt[1], dep)) + + cur = cur.next + + def _add_props_to_sym(self, node): + # Copies properties from the menu node 'node' up to its contained + # symbol, and adds (weak) reverse dependencies to selected/implied + # symbols. + # + # This can't be rolled into _propagate_deps(), because that function + # traverses the menu tree roughly breadth-first, meaning properties on + # symbols defined in multiple locations could end up in the wrong + # order. + + sym = node.item + + # See the Symbol class docstring + sym.direct_dep = self._make_or(sym.direct_dep, node.dep) + + sym.defaults += node.defaults + sym.ranges += node.ranges + sym.selects += node.selects + sym.implies += node.implies + + # Modify the reverse dependencies of the selected symbol + for target, cond in node.selects: + target.rev_dep = self._make_or( + target.rev_dep, + self._make_and(sym, cond)) + + # Modify the weak reverse dependencies of the implied + # symbol + for target, cond in node.implies: + target.weak_rev_dep = self._make_or( + target.weak_rev_dep, + self._make_and(sym, cond)) + + # + # Misc. + # + + def _check_sym_sanity(self): + # Checks various symbol properties that are handiest to check after + # parsing. Only generates errors and warnings. + + def num_ok(sym, type_): + # Returns True if the (possibly constant) symbol 'sym' is valid as a value + # for a symbol of type type_ (INT or HEX) + + # 'not sym.nodes' implies a constant or undefined symbol, e.g. a plain + # "123" + if not sym.nodes: + return _is_base_n(sym.name, _TYPE_TO_BASE[type_]) + + return sym.orig_type is type_ + + for sym in self.unique_defined_syms: + if sym.orig_type in _BOOL_TRISTATE: + # A helper function could be factored out here, but keep it + # speedy/straightforward + + for target_sym, _ in sym.selects: + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: + self._warn("{} selects the {} symbol {}, which is not " + "bool or tristate" + .format(sym.name_and_loc, + TYPE_TO_STR[target_sym.orig_type], + target_sym.name_and_loc)) + + for target_sym, _ in sym.implies: + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: + self._warn("{} implies the {} symbol {}, which is not " + "bool or tristate" + .format(sym.name_and_loc, + TYPE_TO_STR[target_sym.orig_type], + target_sym.name_and_loc)) + + elif sym.orig_type: # STRING/INT/HEX + for default, _ in sym.defaults: + if default.__class__ is not Symbol: + raise KconfigError( + "the {} symbol {} has a malformed default {} -- " + "expected a single symbol" + .format(TYPE_TO_STR[sym.orig_type], + sym.name_and_loc, expr_str(default))) + + if sym.orig_type is STRING: + if not default.is_constant and not default.nodes and \ + not default.name.isupper(): + # 'default foo' on a string symbol could be either a symbol + # reference or someone leaving out the quotes. Guess that + # the quotes were left out if 'foo' isn't all-uppercase + # (and no symbol named 'foo' exists). + self._warn("style: quotes recommended around " + "default value for string symbol " + + sym.name_and_loc) + + elif not num_ok(default, sym.orig_type): # INT/HEX + self._warn("the {0} symbol {1} has a non-{0} default {2}" + .format(TYPE_TO_STR[sym.orig_type], + sym.name_and_loc, + default.name_and_loc)) + + if sym.selects or sym.implies: + self._warn("the {} symbol {} has selects or implies" + .format(TYPE_TO_STR[sym.orig_type], + sym.name_and_loc)) + + else: # UNKNOWN + self._warn("{} defined without a type" + .format(sym.name_and_loc)) + + + if sym.ranges: + if sym.orig_type not in _INT_HEX: + self._warn( + "the {} symbol {} has ranges, but is not int or hex" + .format(TYPE_TO_STR[sym.orig_type], + sym.name_and_loc)) + else: + for low, high, _ in sym.ranges: + if not num_ok(low, sym.orig_type) or \ + not num_ok(high, sym.orig_type): + + self._warn("the {0} symbol {1} has a non-{0} " + "range [{2}, {3}]" + .format(TYPE_TO_STR[sym.orig_type], + sym.name_and_loc, + low.name_and_loc, + high.name_and_loc)) + + def _check_choice_sanity(self): + # Checks various choice properties that are handiest to check after + # parsing. Only generates errors and warnings. + + def warn_select_imply(sym, expr, expr_type): + msg = "the choice symbol {} is {} by the following symbols, but " \ + "select/imply has no effect on choice symbols" \ + .format(sym.name_and_loc, expr_type) + + # si = select/imply + for si in split_expr(expr, OR): + msg += "\n - " + split_expr(si, AND)[0].name_and_loc + + self._warn(msg) + + for choice in self.unique_choices: + if choice.orig_type not in _BOOL_TRISTATE: + self._warn("{} defined with type {}" + .format(choice.name_and_loc, + TYPE_TO_STR[choice.orig_type])) + + for node in choice.nodes: + if node.prompt: + break + else: + self._warn(choice.name_and_loc + " defined without a prompt") + + for default, _ in choice.defaults: + if default.__class__ is not Symbol: + raise KconfigError( + "{} has a malformed default {}" + .format(choice.name_and_loc, expr_str(default))) + + if default.choice is not choice: + self._warn("the default selection {} of {} is not " + "contained in the choice" + .format(default.name_and_loc, + choice.name_and_loc)) + + for sym in choice.syms: + if sym.defaults: + self._warn("default on the choice symbol {} will have " + "no effect, as defaults do not affect choice " + "symbols".format(sym.name_and_loc)) + + if sym.rev_dep is not sym.kconfig.n: + warn_select_imply(sym, sym.rev_dep, "selected") + + if sym.weak_rev_dep is not sym.kconfig.n: + warn_select_imply(sym, sym.weak_rev_dep, "implied") + + for node in sym.nodes: + if node.parent.item is choice: + if not node.prompt: + self._warn("the choice symbol {} has no prompt" + .format(sym.name_and_loc)) + + elif node.prompt: + self._warn("the choice symbol {} is defined with a " + "prompt outside the choice" + .format(sym.name_and_loc)) + + def _parse_error(self, msg): + raise KconfigError("{}error: couldn't parse '{}': {}".format( + "" if self.filename is None else + "{}:{}: ".format(self.filename, self.linenr), + self._line.strip(), msg)) + + def _trailing_tokens_error(self): + self._parse_error("extra tokens at end of line") + + def _open(self, filename, mode): + # open() wrapper: + # + # - Enable universal newlines mode on Python 2 to ease + # interoperability between Linux and Windows. It's already the + # default on Python 3. + # + # The "U" flag would currently work for both Python 2 and 3, but it's + # deprecated on Python 3, so play it future-safe. + # + # io.open() defaults to universal newlines on Python 2 (and is an + # alias for open() on Python 3), but it returns 'unicode' strings and + # slows things down: + # + # Parsing x86 Kconfigs on Python 2 + # + # with open(..., "rU"): + # + # real 0m0.930s + # user 0m0.905s + # sys 0m0.025s + # + # with io.open(): + # + # real 0m1.069s + # user 0m1.040s + # sys 0m0.029s + # + # There's no appreciable performance difference between "r" and + # "rU" for parsing performance on Python 2. + # + # - For Python 3, force the encoding. Forcing the encoding on Python 2 + # turns strings into Unicode strings, which gets messy. Python 2 + # doesn't decode regular strings anyway. + return open(filename, "rU" if mode == "r" else mode) if _IS_PY2 else \ + open(filename, mode, encoding=self._encoding) + + def _check_undef_syms(self): + # Prints warnings for all references to undefined symbols within the + # Kconfig files + + def is_num(s): + # Returns True if the string 's' looks like a number. + # + # Internally, all operands in Kconfig are symbols, only undefined symbols + # (which numbers usually are) get their name as their value. + # + # Only hex numbers that start with 0x/0X are classified as numbers. + # Otherwise, symbols whose names happen to contain only the letters A-F + # would trigger false positives. + + try: + int(s) + except ValueError: + if not s.startswith(("0x", "0X")): + return False + + try: + int(s, 16) + except ValueError: + return False + + return True + + for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)(): + # - sym.nodes empty means the symbol is undefined (has no + # definition locations) + # + # - Due to Kconfig internals, numbers show up as undefined Kconfig + # symbols, but shouldn't be flagged + # + # - The MODULES symbol always exists + if not sym.nodes and not is_num(sym.name) and \ + sym.name != "MODULES": + + msg = "undefined symbol {}:".format(sym.name) + for node in self.node_iter(): + if sym in node.referenced: + msg += "\n\n- Referenced at {}:{}:\n\n{}" \ + .format(node.filename, node.linenr, node) + self._warn(msg) + + def _warn(self, msg, filename=None, linenr=None): + # For printing general warnings + + if not self.warn: + return + + msg = "warning: " + msg + if filename is not None: + msg = "{}:{}: {}".format(filename, linenr, msg) + + self.warnings.append(msg) + if self.warn_to_stderr: + sys.stderr.write(msg + "\n") + + +class Symbol(object): + """ + Represents a configuration symbol: + + (menu)config FOO + ... + + The following attributes are available. They should be viewed as read-only, + and some are implemented through @property magic (but are still efficient + to access due to internal caching). + + Note: Prompts, help texts, and locations are stored in the Symbol's + MenuNode(s) rather than in the Symbol itself. Check the MenuNode class and + the Symbol.nodes attribute. This organization matches the C tools. + + name: + The name of the symbol, e.g. "FOO" for 'config FOO'. + + type: + The type of the symbol. One of BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN. + UNKNOWN is for undefined symbols, (non-special) constant symbols, and + symbols defined without a type. + + When running without modules (MODULES having the value n), TRISTATE + symbols magically change type to BOOL. This also happens for symbols + within choices in "y" mode. This matches the C tools, and makes sense for + menuconfig-like functionality. + + orig_type: + The type as given in the Kconfig file, without any magic applied. Used + when printing the symbol. + + tri_value: + The tristate value of the symbol as an integer. One of 0, 1, 2, + representing n, m, y. Always 0 (n) for non-bool/tristate symbols. + + This is the symbol value that's used outside of relation expressions + (A, !A, A && B, A || B). + + str_value: + The value of the symbol as a string. Gives the value for string/int/hex + symbols. For bool/tristate symbols, gives "n", "m", or "y". + + This is the symbol value that's used in relational expressions + (A = B, A != B, etc.) + + Gotcha: For int/hex symbols, the exact format of the value is often + preserved (e.g. when writing a .config file), hence why you can't get it + directly as an int. Do int(int_sym.str_value) or + int(hex_sym.str_value, 16) to get the integer value. + + user_value: + The user value of the symbol. None if no user value has been assigned + (via Kconfig.load_config() or Symbol.set_value()). + + Holds 0, 1, or 2 for bool/tristate symbols, and a string for the other + symbol types. + + WARNING: Do not assign directly to this. It will break things. Use + Symbol.set_value(). + + assignable: + A tuple containing the tristate user values that can currently be + assigned to the symbol (that would be respected), ordered from lowest (0, + representing n) to highest (2, representing y). This corresponds to the + selections available in the menuconfig interface. The set of assignable + values is calculated from the symbol's visibility and selects/implies. + + Returns the empty set for non-bool/tristate symbols and for symbols with + visibility n. The other possible values are (0, 2), (0, 1, 2), (1, 2), + (1,), and (2,). A (1,) or (2,) result means the symbol is visible but + "locked" to m or y through a select, perhaps in combination with the + visibility. menuconfig represents this as -M- and -*-, respectively. + + For string/hex/int symbols, check if Symbol.visibility is non-0 (non-n) + instead to determine if the value can be changed. + + Some handy 'assignable' idioms: + + # Is 'sym' an assignable (visible) bool/tristate symbol? + if sym.assignable: + # What's the highest value it can be assigned? [-1] in Python + # gives the last element. + sym_high = sym.assignable[-1] + + # The lowest? + sym_low = sym.assignable[0] + + # Can the symbol be set to at least m? + if sym.assignable[-1] >= 1: + ... + + # Can the symbol be set to m? + if 1 in sym.assignable: + ... + + visibility: + The visibility of the symbol. One of 0, 1, 2, representing n, m, y. See + the module documentation for an overview of symbol values and visibility. + + config_string: + The .config assignment string that would get written out for the symbol + by Kconfig.write_config(). Returns the empty string if no .config + assignment would get written out. + + In general, visible symbols, symbols with (active) defaults, and selected + symbols get written out. This includes all non-n-valued bool/tristate + symbols, and all visible string/int/hex symbols. + + Symbols with the (no longer needed) 'option env=...' option generate no + configuration output, and neither does the special + 'option defconfig_list' symbol. + + Tip: This field is useful when generating custom configuration output, + even for non-.config-like formats. To write just the symbols that would + get written out to .config files, do this: + + if sym.config_string: + *Write symbol, e.g. by looking sym.str_value* + + This is a superset of the symbols written out by write_autoconf(). + That function skips all n-valued symbols. + + There usually won't be any great harm in just writing all symbols either, + though you might get some special symbols and possibly some "redundant" + n-valued symbol entries in there. + + name_and_loc: + Holds a string like + + "MY_SYMBOL (defined at foo/Kconfig:12, bar/Kconfig:14)" + + , giving the name of the symbol and its definition location(s). + + If the symbol is undefined, the location is given as "(undefined)". + + nodes: + A list of MenuNodes for this symbol. Will contain a single MenuNode for + most symbols. Undefined and constant symbols have an empty nodes list. + Symbols defined in multiple locations get one node for each location. + + choice: + Holds the parent Choice for choice symbols, and None for non-choice + symbols. Doubles as a flag for whether a symbol is a choice symbol. + + defaults: + List of (default, cond) tuples for the symbol's 'default' properties. For + example, 'default A && B if C || D' is represented as + ((AND, A, B), (OR, C, D)). If no condition was given, 'cond' is + self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to + 'default' conditions. + + selects: + List of (symbol, cond) tuples for the symbol's 'select' properties. For + example, 'select A if B && C' is represented as (A, (AND, B, C)). If no + condition was given, 'cond' is self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to 'select' + conditions. + + implies: + Like 'selects', for imply. + + ranges: + List of (low, high, cond) tuples for the symbol's 'range' properties. For + example, 'range 1 2 if A' is represented as (1, 2, A). If there is no + condition, 'cond' is self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to 'range' + conditions. + + Gotcha: 1 and 2 above will be represented as (undefined) Symbols rather + than plain integers. Undefined symbols get their name as their string + value, so this works out. The C tools work the same way. + + orig_defaults: + orig_selects: + orig_implies: + orig_ranges: + See the corresponding attributes on the MenuNode class. + + rev_dep: + Reverse dependency expression from other symbols selecting this symbol. + Multiple selections get ORed together. A condition on a select is ANDed + with the selecting symbol. + + For example, if A has 'select FOO' and B has 'select FOO if C', then + FOO's rev_dep will be (OR, A, (AND, B, C)). + + weak_rev_dep: + Like rev_dep, for imply. + + direct_dep: + The direct ('depends on') dependencies for the symbol, or self.kconfig.y + if there are no direct dependencies. + + This attribute includes any dependencies from surrounding menus and ifs. + Those get propagated to the direct dependencies, and the resulting direct + dependencies in turn get propagated to the conditions of all properties. + + If the symbol is defined in multiple locations, the dependencies from the + different locations get ORed together. + + referenced: + A set() with all symbols and choices referenced in the properties and + property conditions of the symbol. + + Also includes dependencies from surrounding menus and ifs, because those + get propagated to the symbol (see the 'Intro to symbol values' section in + the module docstring). + + Choices appear in the dependencies of choice symbols. + + For the following definitions, only B and not C appears in A's + 'referenced'. To get transitive references, you'll have to recursively + expand 'references' until no new items appear. + + config A + bool + depends on B + + config B + bool + depends on C + + config C + bool + + See the Symbol.direct_dep attribute if you're only interested in the + direct dependencies of the symbol (its 'depends on'). You can extract the + symbols in it with the global expr_items() function. + + env_var: + If the Symbol has an 'option env="FOO"' option, this contains the name + ("FOO") of the environment variable. None for symbols without no + 'option env'. + + 'option env="FOO"' acts like a 'default' property whose value is the + value of $FOO. + + Symbols with 'option env' are never written out to .config files, even if + they are visible. env_var corresponds to a flag called SYMBOL_AUTO in the + C implementation. + + is_allnoconfig_y: + True if the symbol has 'option allnoconfig_y' set on it. This has no + effect internally (except when printing symbols), but can be checked by + scripts. + + is_constant: + True if the symbol is a constant (quoted) symbol. + + kconfig: + The Kconfig instance this symbol is from. + """ + __slots__ = ( + "_cached_assignable", + "_cached_str_val", + "_cached_tri_val", + "_cached_vis", + "_dependents", + "_old_val", + "_visited", + "_was_set", + "_write_to_conf", + "choice", + "defaults", + "direct_dep", + "env_var", + "implies", + "is_allnoconfig_y", + "is_constant", + "kconfig", + "name", + "nodes", + "orig_type", + "ranges", + "rev_dep", + "selects", + "user_value", + "weak_rev_dep", + ) + + # + # Public interface + # + + @property + def type(self): + """ + See the class documentation. + """ + if self.orig_type is TRISTATE and \ + (self.choice and self.choice.tri_value == 2 or + not self.kconfig.modules.tri_value): + + return BOOL + + return self.orig_type + + @property + def str_value(self): + """ + See the class documentation. + """ + if self._cached_str_val is not None: + return self._cached_str_val + + if self.orig_type in _BOOL_TRISTATE: + # Also calculates the visibility, so invalidation safe + self._cached_str_val = TRI_TO_STR[self.tri_value] + return self._cached_str_val + + # As a quirk of Kconfig, undefined symbols get their name as their + # string value. This is why things like "FOO = bar" work for seeing if + # FOO has the value "bar". + if not self.orig_type: # UNKNOWN + self._cached_str_val = self.name + return self.name + + val = "" + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + + self._write_to_conf = (vis != 0) + + if self.orig_type in _INT_HEX: + # The C implementation checks the user value against the range in a + # separate code path (post-processing after loading a .config). + # Checking all values here instead makes more sense for us. It + # requires that we check for a range first. + + base = _TYPE_TO_BASE[self.orig_type] + + # Check if a range is in effect + for low_expr, high_expr, cond in self.ranges: + if expr_value(cond): + has_active_range = True + + # The zeros are from the C implementation running strtoll() + # on empty strings + low = int(low_expr.str_value, base) if \ + _is_base_n(low_expr.str_value, base) else 0 + high = int(high_expr.str_value, base) if \ + _is_base_n(high_expr.str_value, base) else 0 + + break + else: + has_active_range = False + + # Defaults are used if the symbol is invisible, lacks a user value, + # or has an out-of-range user value + use_defaults = True + + if vis and self.user_value: + user_val = int(self.user_value, base) + if has_active_range and not low <= user_val <= high: + num2str = str if base == 10 else hex + self.kconfig._warn( + "user value {} on the {} symbol {} ignored due to " + "being outside the active range ([{}, {}]) -- falling " + "back on defaults" + .format(num2str(user_val), TYPE_TO_STR[self.orig_type], + self.name_and_loc, + num2str(low), num2str(high))) + else: + # If the user value is well-formed and satisfies range + # contraints, it is stored in exactly the same form as + # specified in the assignment (with or without "0x", etc.) + val = self.user_value + use_defaults = False + + if use_defaults: + # No user value or invalid user value. Look at defaults. + + # Used to implement the warning below + has_default = False + + for sym, cond in self.defaults: + if expr_value(cond): + has_default = self._write_to_conf = True + + val = sym.str_value + + if _is_base_n(val, base): + val_num = int(val, base) + else: + val_num = 0 # strtoll() on empty string + + break + else: + val_num = 0 # strtoll() on empty string + + # This clamping procedure runs even if there's no default + if has_active_range: + clamp = None + if val_num < low: + clamp = low + elif val_num > high: + clamp = high + + if clamp is not None: + # The value is rewritten to a standard form if it is + # clamped + val = str(clamp) \ + if self.orig_type is INT else \ + hex(clamp) + + if has_default: + num2str = str if base == 10 else hex + self.kconfig._warn( + "default value {} on {} clamped to {} due to " + "being outside the active range ([{}, {}])" + .format(val_num, self.name_and_loc, + num2str(clamp), num2str(low), + num2str(high))) + + elif self.orig_type is STRING: + if vis and self.user_value is not None: + # If the symbol is visible and has a user value, use that + val = self.user_value + else: + # Otherwise, look at defaults + for sym, cond in self.defaults: + if expr_value(cond): + val = sym.str_value + self._write_to_conf = True + break + + # env_var corresponds to SYMBOL_AUTO in the C implementation, and is + # also set on the defconfig_list symbol there. Test for the + # defconfig_list symbol explicitly instead here, to avoid a nonsensical + # env_var setting and the defconfig_list symbol being printed + # incorrectly. This code is pretty cold anyway. + if self.env_var is not None or self is self.kconfig.defconfig_list: + self._write_to_conf = False + + self._cached_str_val = val + return val + + @property + def tri_value(self): + """ + See the class documentation. + """ + if self._cached_tri_val is not None: + return self._cached_tri_val + + if self.orig_type not in _BOOL_TRISTATE: + if self.orig_type: # != UNKNOWN + # Would take some work to give the location here + self.kconfig._warn( + "The {} symbol {} is being evaluated in a logical context " + "somewhere. It will always evaluate to n." + .format(TYPE_TO_STR[self.orig_type], self.name_and_loc)) + + self._cached_tri_val = 0 + return 0 + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + self._write_to_conf = (vis != 0) + + val = 0 + + if not self.choice: + # Non-choice symbol + + if vis and self.user_value is not None: + # If the symbol is visible and has a user value, use that + val = min(self.user_value, vis) + + else: + # Otherwise, look at defaults and weak reverse dependencies + # (implies) + + for default, cond in self.defaults: + dep_val = expr_value(cond) + if dep_val: + val = min(expr_value(default), dep_val) + if val: + self._write_to_conf = True + break + + # Weak reverse dependencies are only considered if our + # direct dependencies are met + dep_val = expr_value(self.weak_rev_dep) + if dep_val and expr_value(self.direct_dep): + val = max(dep_val, val) + self._write_to_conf = True + + # Reverse (select-related) dependencies take precedence + dep_val = expr_value(self.rev_dep) + if dep_val: + if expr_value(self.direct_dep) < dep_val: + self._warn_select_unsatisfied_deps() + + val = max(dep_val, val) + self._write_to_conf = True + + # m is promoted to y for (1) bool symbols and (2) symbols with a + # weak_rev_dep (from imply) of y + if val == 1 and \ + (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): + val = 2 + + elif vis == 2: + # Visible choice symbol in y-mode choice. The choice mode limits + # the visibility of choice symbols, so it's sufficient to just + # check the visibility of the choice symbols themselves. + val = 2 if self.choice.selection is self else 0 + + elif vis and self.user_value: + # Visible choice symbol in m-mode choice, with set non-0 user value + val = 1 + + self._cached_tri_val = val + return val + + @property + def assignable(self): + """ + See the class documentation. + """ + if self._cached_assignable is None: + self._cached_assignable = self._assignable() + return self._cached_assignable + + @property + def visibility(self): + """ + See the class documentation. + """ + if self._cached_vis is None: + self._cached_vis = _visibility(self) + return self._cached_vis + + @property + def config_string(self): + """ + See the class documentation. + """ + # _write_to_conf is determined when the value is calculated. This is a + # hidden function call due to property magic. + val = self.str_value + if not self._write_to_conf: + return "" + + if self.orig_type in _BOOL_TRISTATE: + return "{}{}={}\n" \ + .format(self.kconfig.config_prefix, self.name, val) \ + if val != "n" else \ + "# {}{} is not set\n" \ + .format(self.kconfig.config_prefix, self.name) + + if self.orig_type in _INT_HEX: + return "{}{}={}\n" \ + .format(self.kconfig.config_prefix, self.name, val) + + # sym.orig_type is STRING + return '{}{}="{}"\n' \ + .format(self.kconfig.config_prefix, self.name, escape(val)) + + @property + def name_and_loc(self): + """ + See the class documentation. + """ + return self.name + " " + _locs(self) + + def set_value(self, value): + """ + Sets the user value of the symbol. + + Equal in effect to assigning the value to the symbol within a .config + file. For bool and tristate symbols, use the 'assignable' attribute to + check which values can currently be assigned. Setting values outside + 'assignable' will cause Symbol.user_value to differ from + Symbol.str/tri_value (be truncated down or up). + + Setting a choice symbol to 2 (y) sets Choice.user_selection to the + choice symbol in addition to setting Symbol.user_value. + Choice.user_selection is considered when the choice is in y mode (the + "normal" mode). + + Other symbols that depend (possibly indirectly) on this symbol are + automatically recalculated to reflect the assigned value. + + value: + The user value to give to the symbol. For bool and tristate symbols, + n/m/y can be specified either as 0/1/2 (the usual format for tristate + values in Kconfiglib) or as one of the strings "n", "m", or "y". For + other symbol types, pass a string. + + Note that the value for an int/hex symbol is passed as a string, e.g. + "123" or "0x0123". The format of this string is preserved in the + output. + + Values that are invalid for the type (such as "foo" or 1 (m) for a + BOOL or "0x123" for an INT) are ignored and won't be stored in + Symbol.user_value. Kconfiglib will print a warning by default for + invalid assignments, and set_value() will return False. + + Returns True if the value is valid for the type of the symbol, and + False otherwise. This only looks at the form of the value. For BOOL and + TRISTATE symbols, check the Symbol.assignable attribute to see what + values are currently in range and would actually be reflected in the + value of the symbol. For other symbol types, check whether the + visibility is non-n. + """ + if self.orig_type in _BOOL_TRISTATE and value in STR_TO_TRI: + value = STR_TO_TRI[value] + + # If the new user value matches the old, nothing changes, and we can + # avoid invalidating cached values. + # + # This optimization is skipped for choice symbols: Setting a choice + # symbol's user value to y might change the state of the choice, so it + # wouldn't be safe (symbol user values always match the values set in a + # .config file or via set_value(), and are never implicitly updated). + if value == self.user_value and not self.choice: + self._was_set = True + return True + + # Check if the value is valid for our type + if not (self.orig_type is BOOL and value in (2, 0) or + self.orig_type is TRISTATE and value in TRI_TO_STR or + value.__class__ is str and + (self.orig_type is STRING or + self.orig_type is INT and _is_base_n(value, 10) or + self.orig_type is HEX and _is_base_n(value, 16) + and int(value, 16) >= 0)): + + # Display tristate values as n, m, y in the warning + self.kconfig._warn( + "the value {} is invalid for {}, which has type {} -- " + "assignment ignored" + .format(TRI_TO_STR[value] if value in TRI_TO_STR else + "'{}'".format(value), + self.name_and_loc, TYPE_TO_STR[self.orig_type])) + + return False + + self.user_value = value + self._was_set = True + + if self.choice and value == 2: + # Setting a choice symbol to y makes it the user selection of the + # choice. Like for symbol user values, the user selection is not + # guaranteed to match the actual selection of the choice, as + # dependencies come into play. + self.choice.user_selection = self + self.choice._was_set = True + self.choice._rec_invalidate() + else: + self._rec_invalidate_if_has_prompt() + + return True + + def unset_value(self): + """ + Removes any user value from the symbol, as if the symbol had never + gotten a user value via Kconfig.load_config() or Symbol.set_value(). + """ + if self.user_value is not None: + self.user_value = None + self._rec_invalidate_if_has_prompt() + + @property + def referenced(self): + """ + See the class documentation. + """ + return {item for node in self.nodes for item in node.referenced} + + @property + def orig_defaults(self): + """ + See the class documentation. + """ + return [d for node in self.nodes for d in node.orig_defaults] + + @property + def orig_selects(self): + """ + See the class documentation. + """ + return [s for node in self.nodes for s in node.orig_selects] + + @property + def orig_implies(self): + """ + See the class documentation. + """ + return [i for node in self.nodes for i in node.orig_implies] + + @property + def orig_ranges(self): + """ + See the class documentation. + """ + return [r for node in self.nodes for r in node.orig_ranges] + + def __repr__(self): + """ + Returns a string with information about the symbol (including its name, + value, visibility, and location(s)) when it is evaluated on e.g. the + interactive Python prompt. + """ + fields = ["symbol " + self.name, TYPE_TO_STR[self.type]] + add = fields.append + + for node in self.nodes: + if node.prompt: + add('"{}"'.format(node.prompt[0])) + + # Only add quotes for non-bool/tristate symbols + add("value " + (self.str_value if self.orig_type in _BOOL_TRISTATE + else '"{}"'.format(self.str_value))) + + if not self.is_constant: + # These aren't helpful to show for constant symbols + + if self.user_value is not None: + # Only add quotes for non-bool/tristate symbols + add("user value " + (TRI_TO_STR[self.user_value] + if self.orig_type in _BOOL_TRISTATE + else '"{}"'.format(self.user_value))) + + add("visibility " + TRI_TO_STR[self.visibility]) + + if self.choice: + add("choice symbol") + + if self.is_allnoconfig_y: + add("allnoconfig_y") + + if self is self.kconfig.defconfig_list: + add("is the defconfig_list symbol") + + if self.env_var is not None: + add("from environment variable " + self.env_var) + + if self is self.kconfig.modules: + add("is the modules symbol") + + add("direct deps " + TRI_TO_STR[expr_value(self.direct_dep)]) + + if self.nodes: + for node in self.nodes: + add("{}:{}".format(node.filename, node.linenr)) + else: + add("constant" if self.is_constant else "undefined") + + return "<{}>".format(", ".join(fields)) + + def __str__(self): + """ + Returns a string representation of the symbol when it is printed. + Matches the Kconfig format, with any parent dependencies propagated to + the 'depends on' condition. + + The string is constructed by joining the strings returned by + MenuNode.__str__() for each of the symbol's menu nodes, so symbols + defined in multiple locations will return a string with all + definitions. + + The returned string does not end in a newline. An empty string is + returned for undefined and constant symbols. + """ + return self.custom_str(standard_sc_expr_str) + + def custom_str(self, sc_expr_str_fn): + """ + Works like Symbol.__str__(), but allows a custom format to be used for + all symbol/choice references. See expr_str(). + """ + return "\n\n".join(node.custom_str(sc_expr_str_fn) + for node in self.nodes) + + # + # Private methods + # + + def __init__(self): + """ + Symbol constructor -- not intended to be called directly by Kconfiglib + clients. + """ + # These attributes are always set on the instance from outside and + # don't need defaults: + # kconfig + # direct_dep + # is_constant + # name + # rev_dep + # weak_rev_dep + + # - UNKNOWN == 0 + # - _visited is used during tree iteration and dep. loop detection + self.orig_type = self._visited = 0 + + self.nodes = [] + + self.defaults = [] + self.selects = [] + self.implies = [] + self.ranges = [] + + self.user_value = \ + self.choice = \ + self.env_var = \ + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ + self._cached_assignable = None + + # _write_to_conf is calculated along with the value. If True, the + # Symbol gets a .config entry. + + self.is_allnoconfig_y = \ + self._was_set = \ + self._write_to_conf = False + + # See Kconfig._build_dep() + self._dependents = set() + + def _assignable(self): + # Worker function for the 'assignable' attribute + + if self.orig_type not in _BOOL_TRISTATE: + return () + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + if not vis: + return () + + rev_dep_val = expr_value(self.rev_dep) + + if vis == 2: + if self.choice: + return (2,) + + if not rev_dep_val: + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: + return (0, 2) + return (0, 1, 2) + + if rev_dep_val == 2: + return (2,) + + # rev_dep_val == 1 + + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: + return (2,) + return (1, 2) + + # vis == 1 + + # Must be a tristate here, because bool m visibility gets promoted to y + + if not rev_dep_val: + return (0, 1) if expr_value(self.weak_rev_dep) != 2 else (0, 2) + + if rev_dep_val == 2: + return (2,) + + # vis == rev_dep_val == 1 + + return (1,) + + def _invalidate(self): + # Marks the symbol as needing to be recalculated + + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ + self._cached_assignable = None + + def _rec_invalidate(self): + # Invalidates the symbol and all items that (possibly) depend on it + + if self is self.kconfig.modules: + # Invalidating MODULES has wide-ranging effects + self.kconfig._invalidate_all() + else: + self._invalidate() + + for item in self._dependents: + # _cached_vis doubles as a flag that tells us whether 'item' + # has cached values, because it's calculated as a side effect + # of calculating all other (non-constant) cached values. + # + # If item._cached_vis is None, it means there can't be cached + # values on other items that depend on 'item', because if there + # were, some value on 'item' would have been calculated and + # item._cached_vis set as a side effect. It's therefore safe to + # stop the invalidation at symbols with _cached_vis None. + # + # This approach massively speeds up scripts that set a lot of + # values, vs simply invalidating all possibly dependent symbols + # (even when you already have a list of all the dependent + # symbols, because some symbols get huge dependency trees). + # + # This gracefully handles dependency loops too, which is nice + # for choices, where the choice depends on the choice symbols + # and vice versa. + if item._cached_vis is not None: + item._rec_invalidate() + + def _rec_invalidate_if_has_prompt(self): + # Invalidates the symbol and its dependent symbols, but only if the + # symbol has a prompt. User values never have an effect on promptless + # symbols, so we skip invalidation for them as an optimization. + # + # This also prevents constant (quoted) symbols from being invalidated + # if set_value() is called on them, which would make them lose their + # value and break things. + # + # Prints a warning if the symbol has no prompt. In some contexts (e.g. + # when loading a .config files) assignments to promptless symbols are + # normal and expected, so the warning can be disabled. + + for node in self.nodes: + if node.prompt: + self._rec_invalidate() + return + + if self.kconfig._warn_assign_no_prompt: + self.kconfig._warn(self.name_and_loc + " has no prompt, meaning " + "user values have no effect on it") + + def _str_default(self): + # write_min_config() helper function. Returns the value the symbol + # would get from defaults if it didn't have a user value. Uses exactly + # the same algorithm as the C implementation (though a bit cleaned up), + # for compatibility. + + if self.orig_type in _BOOL_TRISTATE: + val = 0 + + # Defaults, selects, and implies do not affect choice symbols + if not self.choice: + for default, cond in self.defaults: + cond_val = expr_value(cond) + if cond_val: + val = min(expr_value(default), cond_val) + break + + val = max(expr_value(self.rev_dep), + expr_value(self.weak_rev_dep), + val) + + # Transpose mod to yes if type is bool (possibly due to modules + # being disabled) + if val == 1 and self.type is BOOL: + val = 2 + + return TRI_TO_STR[val] + + if self.orig_type: # STRING/INT/HEX + for default, cond in self.defaults: + if expr_value(cond): + return default.str_value + + return "" + + def _warn_select_unsatisfied_deps(self): + # Helper for printing an informative warning when a symbol with + # unsatisfied direct dependencies (dependencies from 'depends on', ifs, + # and menus) is selected by some other symbol. Also warn if a symbol + # whose direct dependencies evaluate to m is selected to y. + + msg = "{} has direct dependencies {} with value {}, but is " \ + "currently being {}-selected by the following symbols:" \ + .format(self.name_and_loc, expr_str(self.direct_dep), + TRI_TO_STR[expr_value(self.direct_dep)], + TRI_TO_STR[expr_value(self.rev_dep)]) + + # The reverse dependencies from each select are ORed together + for select in split_expr(self.rev_dep, OR): + if expr_value(select) <= expr_value(self.direct_dep): + # Only include selects that exceed the direct dependencies + continue + + # - 'select A if B' turns into A && B + # - 'select A' just turns into A + # + # In both cases, we can split on AND and pick the first operand + selecting_sym = split_expr(select, AND)[0] + + msg += "\n - {}, with value {}, direct dependencies {} " \ + "(value: {})" \ + .format(selecting_sym.name_and_loc, + selecting_sym.str_value, + expr_str(selecting_sym.direct_dep), + TRI_TO_STR[expr_value(selecting_sym.direct_dep)]) + + if select.__class__ is tuple: + msg += ", and select condition {} (value: {})" \ + .format(expr_str(select[2]), + TRI_TO_STR[expr_value(select[2])]) + + self.kconfig._warn(msg) + + +class Choice(object): + """ + Represents a choice statement: + + choice + ... + endchoice + + The following attributes are available on Choice instances. They should be + treated as read-only, and some are implemented through @property magic (but + are still efficient to access due to internal caching). + + Note: Prompts, help texts, and locations are stored in the Choice's + MenuNode(s) rather than in the Choice itself. Check the MenuNode class and + the Choice.nodes attribute. This organization matches the C tools. + + name: + The name of the choice, e.g. "FOO" for 'choice FOO', or None if the + Choice has no name. + + type: + The type of the choice. One of BOOL, TRISTATE, UNKNOWN. UNKNOWN is for + choices defined without a type where none of the contained symbols have a + type either (otherwise the choice inherits the type of the first symbol + defined with a type). + + When running without modules (CONFIG_MODULES=n), TRISTATE choices + magically change type to BOOL. This matches the C tools, and makes sense + for menuconfig-like functionality. + + orig_type: + The type as given in the Kconfig file, without any magic applied. Used + when printing the choice. + + tri_value: + The tristate value (mode) of the choice. A choice can be in one of three + modes: + + 0 (n) - The choice is disabled and no symbols can be selected. For + visible choices, this mode is only possible for choices with + the 'optional' flag set (see kconfig-language.txt). + + 1 (m) - Any number of choice symbols can be set to m, the rest will + be n. + + 2 (y) - One symbol will be y, the rest n. + + Only tristate choices can be in m mode. The visibility of the choice is + an upper bound on the mode, and the mode in turn is an upper bound on the + visibility of the choice symbols. + + To change the mode, use Choice.set_value(). + + Implementation note: + The C tools internally represent choices as a type of symbol, with + special-casing in many code paths. This is why there is a lot of + similarity to Symbol. The value (mode) of a choice is really just a + normal symbol value, and an implicit reverse dependency forces its + lower bound to m for visible non-optional choices (the reverse + dependency is 'm && <visibility>'). + + Symbols within choices get the choice propagated as a dependency to + their properties. This turns the mode of the choice into an upper bound + on e.g. the visibility of choice symbols, and explains the gotcha + related to printing choice symbols mentioned in the module docstring. + + Kconfiglib uses a separate Choice class only because it makes the code + and interface less confusing (especially in a user-facing interface). + Corresponding attributes have the same name in the Symbol and Choice + classes, for consistency and compatibility. + + str_value: + Like choice.tri_value, but gives the value as one of the strings + "n", "m", or "y" + + user_value: + The value (mode) selected by the user through Choice.set_value(). Either + 0, 1, or 2, or None if the user hasn't selected a mode. See + Symbol.user_value. + + WARNING: Do not assign directly to this. It will break things. Use + Choice.set_value() instead. + + assignable: + See the symbol class documentation. Gives the assignable values (modes). + + selection: + The Symbol instance of the currently selected symbol. None if the Choice + is not in y mode or has no selected symbol (due to unsatisfied + dependencies on choice symbols). + + WARNING: Do not assign directly to this. It will break things. Call + sym.set_value(2) on the choice symbol you want to select instead. + + user_selection: + The symbol selected by the user (by setting it to y). Ignored if the + choice is not in y mode, but still remembered so that the choice "snaps + back" to the user selection if the mode is changed back to y. This might + differ from 'selection' due to unsatisfied dependencies. + + WARNING: Do not assign directly to this. It will break things. Call + sym.set_value(2) on the choice symbol to be selected instead. + + visibility: + See the Symbol class documentation. Acts on the value (mode). + + name_and_loc: + Holds a string like + + "<choice MY_CHOICE> (defined at foo/Kconfig:12)" + + , giving the name of the choice and its definition location(s). If the + choice has no name (isn't defined with 'choice MY_CHOICE'), then it will + be shown as "<choice>" before the list of locations (always a single one + in that case). + + syms: + List of symbols contained in the choice. + + Obscure gotcha: If a symbol depends on the previous symbol within a + choice so that an implicit menu is created, it won't be a choice symbol, + and won't be included in 'syms'. + + nodes: + A list of MenuNodes for this choice. In practice, the list will probably + always contain a single MenuNode, but it is possible to give a choice a + name and define it in multiple locations. + + defaults: + List of (symbol, cond) tuples for the choice's 'defaults' properties. For + example, 'default A if B && C' is represented as (A, (AND, B, C)). If + there is no condition, 'cond' is self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to + 'default' conditions. + + orig_defaults: + See the corresponding attribute on the MenuNode class. + + direct_dep: + See Symbol.direct_dep. + + referenced: + A set() with all symbols referenced in the properties and property + conditions of the choice. + + Also includes dependencies from surrounding menus and ifs, because those + get propagated to the choice (see the 'Intro to symbol values' section in + the module docstring). + + is_optional: + True if the choice has the 'optional' flag set on it and can be in + n mode. + + kconfig: + The Kconfig instance this choice is from. + """ + __slots__ = ( + "_cached_assignable", + "_cached_selection", + "_cached_vis", + "_dependents", + "_visited", + "_was_set", + "defaults", + "direct_dep", + "is_constant", + "is_optional", + "kconfig", + "name", + "nodes", + "orig_type", + "syms", + "user_selection", + "user_value", + ) + + # + # Public interface + # + + @property + def type(self): + """ + Returns the type of the choice. See Symbol.type. + """ + if self.orig_type is TRISTATE and not self.kconfig.modules.tri_value: + return BOOL + return self.orig_type + + @property + def str_value(self): + """ + See the class documentation. + """ + return TRI_TO_STR[self.tri_value] + + @property + def tri_value(self): + """ + See the class documentation. + """ + # This emulates a reverse dependency of 'm && visibility' for + # non-optional choices, which is how the C implementation does it + + val = 0 if self.is_optional else 1 + + if self.user_value is not None: + val = max(val, self.user_value) + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + val = min(val, self.visibility) + + # Promote m to y for boolean choices + return 2 if val == 1 and self.type is BOOL else val + + @property + def assignable(self): + """ + See the class documentation. + """ + if self._cached_assignable is None: + self._cached_assignable = self._assignable() + return self._cached_assignable + + @property + def visibility(self): + """ + See the class documentation. + """ + if self._cached_vis is None: + self._cached_vis = _visibility(self) + return self._cached_vis + + @property + def name_and_loc(self): + """ + See the class documentation. + """ + # Reuse the expression format, which is '<choice (name, if any)>'. + return standard_sc_expr_str(self) + " " + _locs(self) + + @property + def selection(self): + """ + See the class documentation. + """ + if self._cached_selection is _NO_CACHED_SELECTION: + self._cached_selection = self._selection() + return self._cached_selection + + def set_value(self, value): + """ + Sets the user value (mode) of the choice. Like for Symbol.set_value(), + the visibility might truncate the value. Choices without the 'optional' + attribute (is_optional) can never be in n mode, but 0/"n" is still + accepted since it's not a malformed value (though it will have no + effect). + + Returns True if the value is valid for the type of the choice, and + False otherwise. This only looks at the form of the value. Check the + Choice.assignable attribute to see what values are currently in range + and would actually be reflected in the mode of the choice. + """ + if value in STR_TO_TRI: + value = STR_TO_TRI[value] + + if value == self.user_value: + # We know the value must be valid if it was successfully set + # previously + self._was_set = True + return True + + if not (self.orig_type is BOOL and value in (2, 0) or + self.orig_type is TRISTATE and value in TRI_TO_STR): + + # Display tristate values as n, m, y in the warning + self.kconfig._warn( + "the value {} is invalid for {}, which has type {} -- " + "assignment ignored" + .format(TRI_TO_STR[value] if value in TRI_TO_STR else + "'{}'".format(value), + self.name_and_loc, TYPE_TO_STR[self.orig_type])) + + return False + + self.user_value = value + self._was_set = True + self._rec_invalidate() + + return True + + def unset_value(self): + """ + Resets the user value (mode) and user selection of the Choice, as if + the user had never touched the mode or any of the choice symbols. + """ + if self.user_value is not None or self.user_selection: + self.user_value = self.user_selection = None + self._rec_invalidate() + + @property + def referenced(self): + """ + See the class documentation. + """ + return {item for node in self.nodes for item in node.referenced} + + @property + def orig_defaults(self): + """ + See the class documentation. + """ + return [d for node in self.nodes for d in node.orig_defaults] + + def __repr__(self): + """ + Returns a string with information about the choice when it is evaluated + on e.g. the interactive Python prompt. + """ + fields = ["choice " + self.name if self.name else "choice", + TYPE_TO_STR[self.type]] + add = fields.append + + for node in self.nodes: + if node.prompt: + add('"{}"'.format(node.prompt[0])) + + add("mode " + self.str_value) + + if self.user_value is not None: + add('user mode {}'.format(TRI_TO_STR[self.user_value])) + + if self.selection: + add("{} selected".format(self.selection.name)) + + if self.user_selection: + user_sel_str = "{} selected by user" \ + .format(self.user_selection.name) + + if self.selection is not self.user_selection: + user_sel_str += " (overridden)" + + add(user_sel_str) + + add("visibility " + TRI_TO_STR[self.visibility]) + + if self.is_optional: + add("optional") + + for node in self.nodes: + add("{}:{}".format(node.filename, node.linenr)) + + return "<{}>".format(", ".join(fields)) + + def __str__(self): + """ + Returns a string representation of the choice when it is printed. + Matches the Kconfig format (though without the contained choice + symbols), with any parent dependencies propagated to the 'depends on' + condition. + + The returned string does not end in a newline. + + See Symbol.__str__() as well. + """ + return self.custom_str(standard_sc_expr_str) + + def custom_str(self, sc_expr_str_fn): + """ + Works like Choice.__str__(), but allows a custom format to be used for + all symbol/choice references. See expr_str(). + """ + return "\n\n".join(node.custom_str(sc_expr_str_fn) + for node in self.nodes) + + # + # Private methods + # + + def __init__(self): + """ + Choice constructor -- not intended to be called directly by Kconfiglib + clients. + """ + # These attributes are always set on the instance from outside and + # don't need defaults: + # direct_dep + # kconfig + + # - UNKNOWN == 0 + # - _visited is used during dep. loop detection + self.orig_type = self._visited = 0 + + self.nodes = [] + + self.syms = [] + self.defaults = [] + + self.name = \ + self.user_value = self.user_selection = \ + self._cached_vis = self._cached_assignable = None + + self._cached_selection = _NO_CACHED_SELECTION + + # is_constant is checked by _depend_on(). Just set it to avoid having + # to special-case choices. + self.is_constant = self.is_optional = False + + # See Kconfig._build_dep() + self._dependents = set() + + def _assignable(self): + # Worker function for the 'assignable' attribute + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + + if not vis: + return () + + if vis == 2: + if not self.is_optional: + return (2,) if self.type is BOOL else (1, 2) + return (0, 2) if self.type is BOOL else (0, 1, 2) + + # vis == 1 + + return (0, 1) if self.is_optional else (1,) + + def _selection(self): + # Worker function for the 'selection' attribute + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + if self.tri_value != 2: + # Not in y mode, so no selection + return None + + # Use the user selection if it's visible + if self.user_selection and self.user_selection.visibility: + return self.user_selection + + # Otherwise, check if we have a default + return self._selection_from_defaults() + + def _selection_from_defaults(self): + # Check if we have a default + for sym, cond in self.defaults: + # The default symbol must be visible too + if expr_value(cond) and sym.visibility: + return sym + + # Otherwise, pick the first visible symbol, if any + for sym in self.syms: + if sym.visibility: + return sym + + # Couldn't find a selection + return None + + def _invalidate(self): + self._cached_vis = self._cached_assignable = None + self._cached_selection = _NO_CACHED_SELECTION + + def _rec_invalidate(self): + # See Symbol._rec_invalidate() + + self._invalidate() + + for item in self._dependents: + if item._cached_vis is not None: + item._rec_invalidate() + + +class MenuNode(object): + """ + Represents a menu node in the configuration. This corresponds to an entry + in e.g. the 'make menuconfig' interface, though non-visible choices, menus, + and comments also get menu nodes. If a symbol or choice is defined in + multiple locations, it gets one menu node for each location. + + The top-level menu node, corresponding to the implicit top-level menu, is + available in Kconfig.top_node. + + The menu nodes for a Symbol or Choice can be found in the + Symbol/Choice.nodes attribute. Menus and comments are represented as plain + menu nodes, with their text stored in the prompt attribute (prompt[0]). + This mirrors the C implementation. + + The following attributes are available on MenuNode instances. They should + be viewed as read-only. + + item: + Either a Symbol, a Choice, or one of the constants MENU and COMMENT. + Menus and comments are represented as plain menu nodes. Ifs are collapsed + (matching the C implementation) and do not appear in the final menu tree. + + next: + The following menu node. None if there is no following node. + + list: + The first child menu node. None if there are no children. + + Choices and menus naturally have children, but Symbols can also have + children because of menus created automatically from dependencies (see + kconfig-language.txt). + + parent: + The parent menu node. None if there is no parent. + + prompt: + A (string, cond) tuple with the prompt for the menu node and its + conditional expression (which is self.kconfig.y if there is no + condition). None if there is no prompt. + + For symbols and choices, the prompt is stored in the MenuNode rather than + the Symbol or Choice instance. For menus and comments, the prompt holds + the text. + + defaults: + The 'default' properties for this particular menu node. See + symbol.defaults. + + When evaluating defaults, you should use Symbol/Choice.defaults instead, + as it include properties from all menu nodes (a symbol/choice can have + multiple definition locations/menu nodes). MenuNode.defaults is meant for + documentation generation. + + selects: + Like MenuNode.defaults, for selects. + + implies: + Like MenuNode.defaults, for implies. + + ranges: + Like MenuNode.defaults, for ranges. + + orig_prompt: + orig_defaults: + orig_selects: + orig_implies: + orig_ranges: + These work the like the corresponding attributes without orig_*, but omit + any dependencies propagated from 'depends on' and surrounding 'if's (the + direct dependencies, stored in MenuNode.dep). + + One use for this is generating less cluttered documentation, by only + showing the direct dependencies in one place. + + help: + The help text for the menu node for Symbols and Choices. None if there is + no help text. Always stored in the node rather than the Symbol or Choice. + It is possible to have a separate help text at each location if a symbol + is defined in multiple locations. + + Trailing whitespace (including a final newline) is stripped from the help + text. This was not the case before Kconfiglib 10.21.0, where the format + was undocumented. + + dep: + The direct ('depends on') dependencies for the menu node, or + self.kconfig.y if there are no direct dependencies. + + This attribute includes any dependencies from surrounding menus and ifs. + Those get propagated to the direct dependencies, and the resulting direct + dependencies in turn get propagated to the conditions of all properties. + + If a symbol or choice is defined in multiple locations, only the + properties defined at a particular location get the corresponding + MenuNode.dep dependencies propagated to them. + + visibility: + The 'visible if' dependencies for the menu node (which must represent a + menu), or self.kconfig.y if there are no 'visible if' dependencies. + 'visible if' dependencies are recursively propagated to the prompts of + symbols and choices within the menu. + + referenced: + A set() with all symbols and choices referenced in the properties and + property conditions of the menu node. + + Also includes dependencies inherited from surrounding menus and ifs. + Choices appear in the dependencies of choice symbols. + + is_menuconfig: + Set to True if the children of the menu node should be displayed in a + separate menu. This is the case for the following items: + + - Menus (node.item == MENU) + + - Choices + + - Symbols defined with the 'menuconfig' keyword. The children come from + implicitly created submenus, and should be displayed in a separate + menu rather than being indented. + + 'is_menuconfig' is just a hint on how to display the menu node. It's + ignored internally by Kconfiglib, except when printing symbols. + + filename/linenr: + The location where the menu node appears. The filename is relative to + $srctree (or to the current directory if $srctree isn't set), except + absolute paths are used for paths outside $srctree. + + include_path: + A tuple of (filename, linenr) tuples, giving the locations of the + 'source' statements via which the Kconfig file containing this menu node + was included. The first element is the location of the 'source' statement + in the top-level Kconfig file passed to Kconfig.__init__(), etc. + + Note that the Kconfig file of the menu node itself isn't included. Check + 'filename' and 'linenr' for that. + + kconfig: + The Kconfig instance the menu node is from. + """ + __slots__ = ( + "dep", + "filename", + "help", + "include_path", + "is_menuconfig", + "item", + "kconfig", + "linenr", + "list", + "next", + "parent", + "prompt", + "visibility", + + # Properties + "defaults", + "selects", + "implies", + "ranges", + ) + + def __init__(self): + # Properties defined on this particular menu node. A local 'depends on' + # only applies to these, in case a symbol is defined in multiple + # locations. + self.defaults = [] + self.selects = [] + self.implies = [] + self.ranges = [] + + @property + def orig_prompt(self): + """ + See the class documentation. + """ + if not self.prompt: + return None + return (self.prompt[0], self._strip_dep(self.prompt[1])) + + @property + def orig_defaults(self): + """ + See the class documentation. + """ + return [(default, self._strip_dep(cond)) + for default, cond in self.defaults] + + @property + def orig_selects(self): + """ + See the class documentation. + """ + return [(select, self._strip_dep(cond)) + for select, cond in self.selects] + + @property + def orig_implies(self): + """ + See the class documentation. + """ + return [(imply, self._strip_dep(cond)) + for imply, cond in self.implies] + + @property + def orig_ranges(self): + """ + See the class documentation. + """ + return [(low, high, self._strip_dep(cond)) + for low, high, cond in self.ranges] + + @property + def referenced(self): + """ + See the class documentation. + """ + # self.dep is included to catch dependencies from a lone 'depends on' + # when there are no properties to propagate it to + res = expr_items(self.dep) + + if self.prompt: + res |= expr_items(self.prompt[1]) + + if self.item is MENU: + res |= expr_items(self.visibility) + + for value, cond in self.defaults: + res |= expr_items(value) + res |= expr_items(cond) + + for value, cond in self.selects: + res.add(value) + res |= expr_items(cond) + + for value, cond in self.implies: + res.add(value) + res |= expr_items(cond) + + for low, high, cond in self.ranges: + res.add(low) + res.add(high) + res |= expr_items(cond) + + return res + + def __repr__(self): + """ + Returns a string with information about the menu node when it is + evaluated on e.g. the interactive Python prompt. + """ + fields = [] + add = fields.append + + if self.item.__class__ is Symbol: + add("menu node for symbol " + self.item.name) + + elif self.item.__class__ is Choice: + s = "menu node for choice" + if self.item.name is not None: + s += " " + self.item.name + add(s) + + elif self.item is MENU: + add("menu node for menu") + + else: # self.item is COMMENT + add("menu node for comment") + + if self.prompt: + add('prompt "{}" (visibility {})'.format( + self.prompt[0], TRI_TO_STR[expr_value(self.prompt[1])])) + + if self.item.__class__ is Symbol and self.is_menuconfig: + add("is menuconfig") + + add("deps " + TRI_TO_STR[expr_value(self.dep)]) + + if self.item is MENU: + add("'visible if' deps " + TRI_TO_STR[expr_value(self.visibility)]) + + if self.item.__class__ in _SYMBOL_CHOICE and self.help is not None: + add("has help") + + if self.list: + add("has child") + + if self.next: + add("has next") + + add("{}:{}".format(self.filename, self.linenr)) + + return "<{}>".format(", ".join(fields)) + + def __str__(self): + """ + Returns a string representation of the menu node. Matches the Kconfig + format, with any parent dependencies propagated to the 'depends on' + condition. + + The output could (almost) be fed back into a Kconfig parser to redefine + the object associated with the menu node. See the module documentation + for a gotcha related to choice symbols. + + For symbols and choices with multiple menu nodes (multiple definition + locations), properties that aren't associated with a particular menu + node are shown on all menu nodes ('option env=...', 'optional' for + choices, etc.). + + The returned string does not end in a newline. + """ + return self.custom_str(standard_sc_expr_str) + + def custom_str(self, sc_expr_str_fn): + """ + Works like MenuNode.__str__(), but allows a custom format to be used + for all symbol/choice references. See expr_str(). + """ + return self._menu_comment_node_str(sc_expr_str_fn) \ + if self.item in _MENU_COMMENT else \ + self._sym_choice_node_str(sc_expr_str_fn) + + def _menu_comment_node_str(self, sc_expr_str_fn): + s = '{} "{}"'.format("menu" if self.item is MENU else "comment", + self.prompt[0]) + + if self.dep is not self.kconfig.y: + s += "\n\tdepends on {}".format(expr_str(self.dep, sc_expr_str_fn)) + + if self.item is MENU and self.visibility is not self.kconfig.y: + s += "\n\tvisible if {}".format(expr_str(self.visibility, + sc_expr_str_fn)) + + return s + + def _sym_choice_node_str(self, sc_expr_str_fn): + def indent_add(s): + lines.append("\t" + s) + + def indent_add_cond(s, cond): + if cond is not self.kconfig.y: + s += " if " + expr_str(cond, sc_expr_str_fn) + indent_add(s) + + sc = self.item + + if sc.__class__ is Symbol: + lines = [("menuconfig " if self.is_menuconfig else "config ") + + sc.name] + else: + lines = ["choice " + sc.name if sc.name else "choice"] + + if sc.orig_type and not self.prompt: # sc.orig_type != UNKNOWN + # If there's a prompt, we'll use the '<type> "prompt"' shorthand + # instead + indent_add(TYPE_TO_STR[sc.orig_type]) + + if self.prompt: + if sc.orig_type: + prefix = TYPE_TO_STR[sc.orig_type] + else: + # Symbol defined without a type (which generates a warning) + prefix = "prompt" + + indent_add_cond(prefix + ' "{}"'.format(escape(self.prompt[0])), + self.orig_prompt[1]) + + if sc.__class__ is Symbol: + if sc.is_allnoconfig_y: + indent_add("option allnoconfig_y") + + if sc is sc.kconfig.defconfig_list: + indent_add("option defconfig_list") + + if sc.env_var is not None: + indent_add('option env="{}"'.format(sc.env_var)) + + if sc is sc.kconfig.modules: + indent_add("option modules") + + for low, high, cond in self.orig_ranges: + indent_add_cond( + "range {} {}".format(sc_expr_str_fn(low), + sc_expr_str_fn(high)), + cond) + + for default, cond in self.orig_defaults: + indent_add_cond("default " + expr_str(default, sc_expr_str_fn), + cond) + + if sc.__class__ is Choice and sc.is_optional: + indent_add("optional") + + if sc.__class__ is Symbol: + for select, cond in self.orig_selects: + indent_add_cond("select " + sc_expr_str_fn(select), cond) + + for imply, cond in self.orig_implies: + indent_add_cond("imply " + sc_expr_str_fn(imply), cond) + + if self.dep is not sc.kconfig.y: + indent_add("depends on " + expr_str(self.dep, sc_expr_str_fn)) + + if self.help is not None: + indent_add("help") + for line in self.help.splitlines(): + indent_add(" " + line) + + return "\n".join(lines) + + def _strip_dep(self, expr): + # Helper function for removing MenuNode.dep from 'expr'. Uses two + # pieces of internal knowledge: (1) Expressions are reused rather than + # copied, and (2) the direct dependencies always appear at the end. + + # ... if dep -> ... if y + if self.dep is expr: + return self.kconfig.y + + # (AND, X, dep) -> X + if expr.__class__ is tuple and expr[0] is AND and expr[2] is self.dep: + return expr[1] + + return expr + + +class Variable(object): + """ + Represents a preprocessor variable/function. + + The following attributes are available: + + name: + The name of the variable. + + value: + The unexpanded value of the variable. + + expanded_value: + The expanded value of the variable. For simple variables (those defined + with :=), this will equal 'value'. Accessing this property will raise a + KconfigError if the expansion seems to be stuck in a loop. + + Accessing this field is the same as calling expanded_value_w_args() with + no arguments. I hadn't considered function arguments when adding it. It + is retained for backwards compatibility though. + + is_recursive: + True if the variable is recursive (defined with =). + """ + __slots__ = ( + "_n_expansions", + "is_recursive", + "kconfig", + "name", + "value", + ) + + @property + def expanded_value(self): + """ + See the class documentation. + """ + return self.expanded_value_w_args() + + def expanded_value_w_args(self, *args): + """ + Returns the expanded value of the variable/function. Any arguments + passed will be substituted for $(1), $(2), etc. + + Raises a KconfigError if the expansion seems to be stuck in a loop. + """ + return self.kconfig._fn_val((self.name,) + args) + + def __repr__(self): + return "<variable {}, {}, value '{}'>" \ + .format(self.name, + "recursive" if self.is_recursive else "immediate", + self.value) + + +class KconfigError(Exception): + """ + Exception raised for Kconfig-related errors. + + KconfigError and KconfigSyntaxError are the same class. The + KconfigSyntaxError alias is only maintained for backwards compatibility. + """ + +KconfigSyntaxError = KconfigError # Backwards compatibility + + +class InternalError(Exception): + "Never raised. Kept around for backwards compatibility." + + +# Workaround: +# +# If 'errno' and 'strerror' are set on IOError, then __str__() always returns +# "[Errno <errno>] <strerror>", ignoring any custom message passed to the +# constructor. By defining our own subclass, we can use a custom message while +# also providing 'errno', 'strerror', and 'filename' to scripts. +class _KconfigIOError(IOError): + def __init__(self, ioerror, msg): + self.msg = msg + super(_KconfigIOError, self).__init__( + ioerror.errno, ioerror.strerror, ioerror.filename) + + def __str__(self): + return self.msg + + +# +# Public functions +# + + +def expr_value(expr): + """ + Evaluates the expression 'expr' to a tristate value. Returns 0 (n), 1 (m), + or 2 (y). + + 'expr' must be an already-parsed expression from a Symbol, Choice, or + MenuNode property. To evaluate an expression represented as a string, use + Kconfig.eval_string(). + + Passing subexpressions of expressions to this function works as expected. + """ + if expr.__class__ is not tuple: + return expr.tri_value + + if expr[0] is AND: + v1 = expr_value(expr[1]) + # Short-circuit the n case as an optimization (~5% faster + # allnoconfig.py and allyesconfig.py, as of writing) + return 0 if not v1 else min(v1, expr_value(expr[2])) + + if expr[0] is OR: + v1 = expr_value(expr[1]) + # Short-circuit the y case as an optimization + return 2 if v1 == 2 else max(v1, expr_value(expr[2])) + + if expr[0] is NOT: + return 2 - expr_value(expr[1]) + + # Relation + # + # Implements <, <=, >, >= comparisons as well. These were added to + # kconfig in 31847b67 (kconfig: allow use of relations other than + # (in)equality). + + rel, v1, v2 = expr + + # If both operands are strings... + if v1.orig_type is STRING and v2.orig_type is STRING: + # ...then compare them lexicographically + comp = _strcmp(v1.str_value, v2.str_value) + else: + # Otherwise, try to compare them as numbers + try: + comp = _sym_to_num(v1) - _sym_to_num(v2) + except ValueError: + # Fall back on a lexicographic comparison if the operands don't + # parse as numbers + comp = _strcmp(v1.str_value, v2.str_value) + + return 2*(comp == 0 if rel is EQUAL else + comp != 0 if rel is UNEQUAL else + comp < 0 if rel is LESS else + comp <= 0 if rel is LESS_EQUAL else + comp > 0 if rel is GREATER else + comp >= 0) + + +def standard_sc_expr_str(sc): + """ + Standard symbol/choice printing function. Uses plain Kconfig syntax, and + displays choices as <choice> (or <choice NAME>, for named choices). + + See expr_str(). + """ + if sc.__class__ is Symbol: + if sc.is_constant and sc.name not in STR_TO_TRI: + return '"{}"'.format(escape(sc.name)) + return sc.name + + return "<choice {}>".format(sc.name) if sc.name else "<choice>" + + +def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str): + """ + Returns the string representation of the expression 'expr', as in a Kconfig + file. + + Passing subexpressions of expressions to this function works as expected. + + sc_expr_str_fn (default: standard_sc_expr_str): + This function is called for every symbol/choice (hence "sc") appearing in + the expression, with the symbol/choice as the argument. It is expected to + return a string to be used for the symbol/choice. + + This can be used e.g. to turn symbols/choices into links when generating + documentation, or for printing the value of each symbol/choice after it. + + Note that quoted values are represented as constants symbols + (Symbol.is_constant == True). + """ + if expr.__class__ is not tuple: + return sc_expr_str_fn(expr) + + if expr[0] is AND: + return "{} && {}".format(_parenthesize(expr[1], OR, sc_expr_str_fn), + _parenthesize(expr[2], OR, sc_expr_str_fn)) + + if expr[0] is OR: + # This turns A && B || C && D into "(A && B) || (C && D)", which is + # redundant, but more readable + return "{} || {}".format(_parenthesize(expr[1], AND, sc_expr_str_fn), + _parenthesize(expr[2], AND, sc_expr_str_fn)) + + if expr[0] is NOT: + if expr[1].__class__ is tuple: + return "!({})".format(expr_str(expr[1], sc_expr_str_fn)) + return "!" + sc_expr_str_fn(expr[1]) # Symbol + + # Relation + # + # Relation operands are always symbols (quoted strings are constant + # symbols) + return "{} {} {}".format(sc_expr_str_fn(expr[1]), REL_TO_STR[expr[0]], + sc_expr_str_fn(expr[2])) + + +def expr_items(expr): + """ + Returns a set() of all items (symbols and choices) that appear in the + expression 'expr'. + + Passing subexpressions of expressions to this function works as expected. + """ + res = set() + + def rec(subexpr): + if subexpr.__class__ is tuple: + # AND, OR, NOT, or relation + + rec(subexpr[1]) + + # NOTs only have a single operand + if subexpr[0] is not NOT: + rec(subexpr[2]) + + else: + # Symbol or choice + res.add(subexpr) + + rec(expr) + return res + + +def split_expr(expr, op): + """ + Returns a list containing the top-level AND or OR operands in the + expression 'expr', in the same (left-to-right) order as they appear in + the expression. + + This can be handy e.g. for splitting (weak) reverse dependencies + from 'select' and 'imply' into individual selects/implies. + + op: + Either AND to get AND operands, or OR to get OR operands. + + (Having this as an operand might be more future-safe than having two + hardcoded functions.) + + + Pseudo-code examples: + + split_expr( A , OR ) -> [A] + split_expr( A && B , OR ) -> [A && B] + split_expr( A || B , OR ) -> [A, B] + split_expr( A || B , AND ) -> [A || B] + split_expr( A || B || (C && D) , OR ) -> [A, B, C && D] + + # Second || is not at the top level + split_expr( A || (B && (C || D)) , OR ) -> [A, B && (C || D)] + + # Parentheses don't matter as long as we stay at the top level (don't + # encounter any non-'op' nodes) + split_expr( (A || B) || C , OR ) -> [A, B, C] + split_expr( A || (B || C) , OR ) -> [A, B, C] + """ + res = [] + + def rec(subexpr): + if subexpr.__class__ is tuple and subexpr[0] is op: + rec(subexpr[1]) + rec(subexpr[2]) + else: + res.append(subexpr) + + rec(expr) + return res + + +def escape(s): + r""" + Escapes the string 's' in the same fashion as is done for display in + Kconfig format and when writing strings to a .config file. " and \ are + replaced by \" and \\, respectively. + """ + # \ must be escaped before " to avoid double escaping + return s.replace("\\", r"\\").replace('"', r'\"') + + +def unescape(s): + r""" + Unescapes the string 's'. \ followed by any character is replaced with just + that character. Used internally when reading .config files. + """ + return _unescape_sub(r"\1", s) + +# unescape() helper +_unescape_sub = re.compile(r"\\(.)").sub + + +def standard_kconfig(description=None): + """ + Argument parsing helper for tools that take a single optional Kconfig file + argument (default: Kconfig). Returns the Kconfig instance for the parsed + configuration. Uses argparse internally. + + Exits with sys.exit() (which raises SystemExit) on errors. + + description (default: None): + The 'description' passed to argparse.ArgumentParser(). + argparse.RawDescriptionHelpFormatter is used, so formatting is preserved. + """ + import argparse + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=description) + + parser.add_argument( + "kconfig", + metavar="KCONFIG", + default="Kconfig", + nargs="?", + help="Top-level Kconfig file (default: Kconfig)") + + return Kconfig(parser.parse_args().kconfig, suppress_traceback=True) + + +def standard_config_filename(): + """ + Helper for tools. Returns the value of KCONFIG_CONFIG (which specifies the + .config file to load/save) if it is set, and ".config" otherwise. + + Calling load_config() with filename=None might give the behavior you want, + without having to use this function. + """ + return os.getenv("KCONFIG_CONFIG", ".config") + + +def load_allconfig(kconf, filename): + """ + Use Kconfig.load_allconfig() instead, which was added in Kconfiglib 13.4.0. + Supported for backwards compatibility. Might be removed at some point after + a long period of deprecation warnings. + """ + allconfig = os.getenv("KCONFIG_ALLCONFIG") + if allconfig is None: + return + + def std_msg(e): + # "Upcasts" a _KconfigIOError to an IOError, removing the custom + # __str__() message. The standard message is better here. + # + # This might also convert an OSError to an IOError in obscure cases, + # but it's probably not a big deal. The distinction is shaky (see + # PEP-3151). + return IOError(e.errno, e.strerror, e.filename) + + old_warn_assign_override = kconf.warn_assign_override + old_warn_assign_redun = kconf.warn_assign_redun + kconf.warn_assign_override = kconf.warn_assign_redun = False + + if allconfig in ("", "1"): + try: + print(kconf.load_config(filename, False)) + except EnvironmentError as e1: + try: + print(kconf.load_config("all.config", False)) + except EnvironmentError as e2: + sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} " + "nor all.config could be opened: {}, {}" + .format(filename, std_msg(e1), std_msg(e2))) + else: + try: + print(kconf.load_config(allconfig, False)) + except EnvironmentError as e: + sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which " + "could not be opened: {}" + .format(allconfig, std_msg(e))) + + kconf.warn_assign_override = old_warn_assign_override + kconf.warn_assign_redun = old_warn_assign_redun + + +# +# Internal functions +# + + +def _visibility(sc): + # Symbols and Choices have a "visibility" that acts as an upper bound on + # the values a user can set for them, corresponding to the visibility in + # e.g. 'make menuconfig'. This function calculates the visibility for the + # Symbol or Choice 'sc' -- the logic is nearly identical. + + vis = 0 + + for node in sc.nodes: + if node.prompt: + vis = max(vis, expr_value(node.prompt[1])) + + if sc.__class__ is Symbol and sc.choice: + if sc.choice.orig_type is TRISTATE and \ + sc.orig_type is not TRISTATE and sc.choice.tri_value != 2: + # Non-tristate choice symbols are only visible in y mode + return 0 + + if sc.orig_type is TRISTATE and vis == 1 and sc.choice.tri_value == 2: + # Choice symbols with m visibility are not visible in y mode + return 0 + + # Promote m to y if we're dealing with a non-tristate (possibly due to + # modules being disabled) + if vis == 1 and sc.type is not TRISTATE: + return 2 + + return vis + + +def _depend_on(sc, expr): + # Adds 'sc' (symbol or choice) as a "dependee" to all symbols in 'expr'. + # Constant symbols in 'expr' are skipped as they can never change value + # anyway. + + if expr.__class__ is tuple: + # AND, OR, NOT, or relation + + _depend_on(sc, expr[1]) + + # NOTs only have a single operand + if expr[0] is not NOT: + _depend_on(sc, expr[2]) + + elif not expr.is_constant: + # Non-constant symbol, or choice + expr._dependents.add(sc) + + +def _parenthesize(expr, type_, sc_expr_str_fn): + # expr_str() helper. Adds parentheses around expressions of type 'type_'. + + if expr.__class__ is tuple and expr[0] is type_: + return "({})".format(expr_str(expr, sc_expr_str_fn)) + return expr_str(expr, sc_expr_str_fn) + + +def _ordered_unique(lst): + # Returns 'lst' with any duplicates removed, preserving order. This hacky + # version seems to be a common idiom. It relies on short-circuit evaluation + # and set.add() returning None, which is falsy. + + seen = set() + seen_add = seen.add + return [x for x in lst if x not in seen and not seen_add(x)] + + +def _is_base_n(s, n): + try: + int(s, n) + return True + except ValueError: + return False + + +def _strcmp(s1, s2): + # strcmp()-alike that returns -1, 0, or 1 + + return (s1 > s2) - (s1 < s2) + + +def _sym_to_num(sym): + # expr_value() helper for converting a symbol to a number. Raises + # ValueError for symbols that can't be converted. + + # For BOOL and TRISTATE, n/m/y count as 0/1/2. This mirrors 9059a3493ef + # ("kconfig: fix relational operators for bool and tristate symbols") in + # the C implementation. + return sym.tri_value if sym.orig_type in _BOOL_TRISTATE else \ + int(sym.str_value, _TYPE_TO_BASE[sym.orig_type]) + + +def _touch_dep_file(path, sym_name): + # If sym_name is MY_SYM_NAME, touches my/sym/name.h. See the sync_deps() + # docstring. + + sym_path = path + os.sep + sym_name.lower().replace("_", os.sep) + ".h" + sym_path_dir = dirname(sym_path) + if not exists(sym_path_dir): + os.makedirs(sym_path_dir, 0o755) + + # A kind of truncating touch, mirroring the C tools + os.close(os.open( + sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)) + + +def _save_old(path): + # See write_config() + + def copy(src, dst): + # Import as needed, to save some startup time + import shutil + shutil.copyfile(src, dst) + + if islink(path): + # Preserve symlinks + copy_fn = copy + elif hasattr(os, "replace"): + # Python 3 (3.3+) only. Best choice when available, because it + # removes <filename>.old on both *nix and Windows. + copy_fn = os.replace + elif os.name == "posix": + # Removes <filename>.old on POSIX systems + copy_fn = os.rename + else: + # Fall back on copying + copy_fn = copy + + try: + copy_fn(path, path + ".old") + except Exception: + # Ignore errors from 'path' missing as well as other errors. + # <filename>.old file is usually more of a nice-to-have, and not worth + # erroring out over e.g. if <filename>.old happens to be a directory or + # <filename> is something like /dev/null. + pass + + +def _locs(sc): + # Symbol/Choice.name_and_loc helper. Returns the "(defined at ...)" part of + # the string. 'sc' is a Symbol or Choice. + + if sc.nodes: + return "(defined at {})".format( + ", ".join("{0.filename}:{0.linenr}".format(node) + for node in sc.nodes)) + + return "(undefined)" + + +# Menu manipulation + + +def _expr_depends_on(expr, sym): + # Reimplementation of expr_depends_symbol() from mconf.c. Used to determine + # if a submenu should be implicitly created. This also influences which + # items inside choice statements are considered choice items. + + if expr.__class__ is not tuple: + return expr is sym + + if expr[0] in _EQUAL_UNEQUAL: + # Check for one of the following: + # sym = m/y, m/y = sym, sym != n, n != sym + + left, right = expr[1:] + + if right is sym: + left, right = right, left + elif left is not sym: + return False + + return (expr[0] is EQUAL and right is sym.kconfig.m or + right is sym.kconfig.y) or \ + (expr[0] is UNEQUAL and right is sym.kconfig.n) + + return expr[0] is AND and \ + (_expr_depends_on(expr[1], sym) or + _expr_depends_on(expr[2], sym)) + + +def _auto_menu_dep(node1, node2): + # Returns True if node2 has an "automatic menu dependency" on node1. If + # node2 has a prompt, we check its condition. Otherwise, we look directly + # at node2.dep. + + return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep, + node1.item) + + +def _flatten(node): + # "Flattens" menu nodes without prompts (e.g. 'if' nodes and non-visible + # symbols with children from automatic menu creation) so that their + # children appear after them instead. This gives a clean menu structure + # with no unexpected "jumps" in the indentation. + # + # Do not flatten promptless choices (which can appear "legitimately" if a + # named choice is defined in multiple locations to add on symbols). It + # looks confusing, and the menuconfig already shows all choice symbols if + # you enter the choice at some location with a prompt. + + while node: + if node.list and not node.prompt and \ + node.item.__class__ is not Choice: + + last_node = node.list + while 1: + last_node.parent = node.parent + if not last_node.next: + break + last_node = last_node.next + + last_node.next = node.next + node.next = node.list + node.list = None + + node = node.next + + +def _remove_ifs(node): + # Removes 'if' nodes (which can be recognized by MenuNode.item being None), + # which are assumed to already have been flattened. The C implementation + # doesn't bother to do this, but we expose the menu tree directly, and it + # makes it nicer to work with. + + cur = node.list + while cur and not cur.item: + cur = cur.next + + node.list = cur + + while cur: + next = cur.next + while next and not next.item: + next = next.next + + # Equivalent to + # + # cur.next = next + # cur = next + # + # due to tricky Python semantics. The order matters. + cur.next = cur = next + + +def _finalize_choice(node): + # Finalizes a choice, marking each symbol whose menu node has the choice as + # the parent as a choice symbol, and automatically determining types if not + # specified. + + choice = node.item + + cur = node.list + while cur: + if cur.item.__class__ is Symbol: + cur.item.choice = choice + choice.syms.append(cur.item) + cur = cur.next + + # If no type is specified for the choice, its type is that of + # the first choice item with a specified type + if not choice.orig_type: + for item in choice.syms: + if item.orig_type: + choice.orig_type = item.orig_type + break + + # Each choice item of UNKNOWN type gets the type of the choice + for sym in choice.syms: + if not sym.orig_type: + sym.orig_type = choice.orig_type + + +def _check_dep_loop_sym(sym, ignore_choice): + # Detects dependency loops using depth-first search on the dependency graph + # (which is calculated earlier in Kconfig._build_dep()). + # + # Algorithm: + # + # 1. Symbols/choices start out with _visited = 0, meaning unvisited. + # + # 2. When a symbol/choice is first visited, _visited is set to 1, meaning + # "visited, potentially part of a dependency loop". The recursive + # search then continues from the symbol/choice. + # + # 3. If we run into a symbol/choice X with _visited already set to 1, + # there's a dependency loop. The loop is found on the call stack by + # recording symbols while returning ("on the way back") until X is seen + # again. + # + # 4. Once a symbol/choice and all its dependencies (or dependents in this + # case) have been checked recursively without detecting any loops, its + # _visited is set to 2, meaning "visited, not part of a dependency + # loop". + # + # This saves work if we run into the symbol/choice again in later calls + # to _check_dep_loop_sym(). We just return immediately. + # + # Choices complicate things, as every choice symbol depends on every other + # choice symbol in a sense. When a choice is "entered" via a choice symbol + # X, we visit all choice symbols from the choice except X, and prevent + # immediately revisiting the choice with a flag (ignore_choice). + # + # Maybe there's a better way to handle this (different flags or the + # like...) + + if not sym._visited: + # sym._visited == 0, unvisited + + sym._visited = 1 + + for dep in sym._dependents: + # Choices show up in Symbol._dependents when the choice has the + # symbol in a 'prompt' or 'default' condition (e.g. + # 'default ... if SYM'). + # + # Since we aren't entering the choice via a choice symbol, all + # choice symbols need to be checked, hence the None. + loop = _check_dep_loop_choice(dep, None) \ + if dep.__class__ is Choice \ + else _check_dep_loop_sym(dep, False) + + if loop: + # Dependency loop found + return _found_dep_loop(loop, sym) + + if sym.choice and not ignore_choice: + loop = _check_dep_loop_choice(sym.choice, sym) + if loop: + # Dependency loop found + return _found_dep_loop(loop, sym) + + # The symbol is not part of a dependency loop + sym._visited = 2 + + # No dependency loop found + return None + + if sym._visited == 2: + # The symbol was checked earlier and is already known to not be part of + # a dependency loop + return None + + # sym._visited == 1, found a dependency loop. Return the symbol as the + # first element in it. + return (sym,) + + +def _check_dep_loop_choice(choice, skip): + if not choice._visited: + # choice._visited == 0, unvisited + + choice._visited = 1 + + # Check for loops involving choice symbols. If we came here via a + # choice symbol, skip that one, as we'd get a false positive + # '<sym FOO> -> <choice> -> <sym FOO>' loop otherwise. + for sym in choice.syms: + if sym is not skip: + # Prevent the choice from being immediately re-entered via the + # "is a choice symbol" path by passing True + loop = _check_dep_loop_sym(sym, True) + if loop: + # Dependency loop found + return _found_dep_loop(loop, choice) + + # The choice is not part of a dependency loop + choice._visited = 2 + + # No dependency loop found + return None + + if choice._visited == 2: + # The choice was checked earlier and is already known to not be part of + # a dependency loop + return None + + # choice._visited == 1, found a dependency loop. Return the choice as the + # first element in it. + return (choice,) + + +def _found_dep_loop(loop, cur): + # Called "on the way back" when we know we have a loop + + # Is the symbol/choice 'cur' where the loop started? + if cur is not loop[0]: + # Nope, it's just a part of the loop + return loop + (cur,) + + # Yep, we have the entire loop. Throw an exception that shows it. + + msg = "\nDependency loop\n" \ + "===============\n\n" + + for item in loop: + if item is not loop[0]: + msg += "...depends on " + if item.__class__ is Symbol and item.choice: + msg += "the choice symbol " + + msg += "{}, with definition...\n\n{}\n\n" \ + .format(item.name_and_loc, item) + + # Small wart: Since we reuse the already calculated + # Symbol/Choice._dependents sets for recursive dependency detection, we + # lose information on whether a dependency came from a 'select'/'imply' + # condition or e.g. a 'depends on'. + # + # This might cause selecting symbols to "disappear". For example, + # a symbol B having 'select A if C' gives a direct dependency from A to + # C, since it corresponds to a reverse dependency of B && C. + # + # Always print reverse dependencies for symbols that have them to make + # sure information isn't lost. I wonder if there's some neat way to + # improve this. + + if item.__class__ is Symbol: + if item.rev_dep is not item.kconfig.n: + msg += "(select-related dependencies: {})\n\n" \ + .format(expr_str(item.rev_dep)) + + if item.weak_rev_dep is not item.kconfig.n: + msg += "(imply-related dependencies: {})\n\n" \ + .format(expr_str(item.rev_dep)) + + msg += "...depends again on " + loop[0].name_and_loc + + raise KconfigError(msg) + + +def _decoding_error(e, filename, macro_linenr=None): + # Gives the filename and context for UnicodeDecodeError's, which are a pain + # to debug otherwise. 'e' is the UnicodeDecodeError object. + # + # If the decoding error is for the output of a $(shell,...) command, + # macro_linenr holds the line number where it was run (the exact line + # number isn't available for decoding errors in files). + + raise KconfigError( + "\n" + "Malformed {} in {}\n" + "Context: {}\n" + "Problematic data: {}\n" + "Reason: {}".format( + e.encoding, + "'{}'".format(filename) if macro_linenr is None else + "output from macro at {}:{}".format(filename, macro_linenr), + e.object[max(e.start - 40, 0):e.end + 40], + e.object[e.start:e.end], + e.reason)) + + +def _warn_verbose_deprecated(fn_name): + sys.stderr.write( + "Deprecation warning: {0}()'s 'verbose' argument has no effect. Since " + "Kconfiglib 12.0.0, the message is returned from {0}() instead, " + "and is always generated. Do e.g. print(kconf.{0}()) if you want to " + "want to show a message like \"Loaded configuration '.config'\" on " + "stdout. The old API required ugly hacks to reuse messages in " + "configuration interfaces.\n".format(fn_name)) + + +# Predefined preprocessor functions + + +def _filename_fn(kconf, _): + return kconf.filename + + +def _lineno_fn(kconf, _): + return str(kconf.linenr) + + +def _info_fn(kconf, _, msg): + print("{}:{}: {}".format(kconf.filename, kconf.linenr, msg)) + + return "" + + +def _warning_if_fn(kconf, _, cond, msg): + if cond == "y": + kconf._warn(msg, kconf.filename, kconf.linenr) + + return "" + + +def _error_if_fn(kconf, _, cond, msg): + if cond == "y": + raise KconfigError("{}:{}: {}".format( + kconf.filename, kconf.linenr, msg)) + + return "" + + +def _shell_fn(kconf, _, command): + import subprocess # Only import as needed, to save some startup time + + stdout, stderr = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ).communicate() + + if not _IS_PY2: + try: + stdout = stdout.decode(kconf._encoding) + stderr = stderr.decode(kconf._encoding) + except UnicodeDecodeError as e: + _decoding_error(e, kconf.filename, kconf.linenr) + + if stderr: + kconf._warn("'{}' wrote to stderr: {}".format( + command, "\n".join(stderr.splitlines())), + kconf.filename, kconf.linenr) + + # Universal newlines with splitlines() (to prevent e.g. stray \r's in + # command output on Windows), trailing newline removal, and + # newline-to-space conversion. + # + # On Python 3 versions before 3.6, it's not possible to specify the + # encoding when passing universal_newlines=True to Popen() (the 'encoding' + # parameter was added in 3.6), so we do this manual version instead. + return "\n".join(stdout.splitlines()).rstrip("\n").replace("\n", " ") + +# +# Global constants +# + +TRI_TO_STR = { + 0: "n", + 1: "m", + 2: "y", +} + +STR_TO_TRI = { + "n": 0, + "m": 1, + "y": 2, +} + +# Constant representing that there's no cached choice selection. This is +# distinct from a cached None (no selection). Any object that's not None or a +# Symbol will do. We test this with 'is'. +_NO_CACHED_SELECTION = 0 + +# Are we running on Python 2? +_IS_PY2 = sys.version_info[0] < 3 + +try: + _UNAME_RELEASE = os.uname()[2] +except AttributeError: + # Only import as needed, to save some startup time + import platform + _UNAME_RELEASE = platform.uname()[2] + +# The token and type constants below are safe to test with 'is', which is a bit +# faster (~30% faster on my machine, and a few % faster for total parsing +# time), even without assuming Python's small integer optimization (which +# caches small integer objects). The constants end up pointing to unique +# integer objects, and since we consistently refer to them via the names below, +# we always get the same object. +# +# Client code should use == though. + +# Tokens, with values 1, 2, ... . Avoiding 0 simplifies some checks by making +# all tokens except empty strings truthy. +( + _T_ALLNOCONFIG_Y, + _T_AND, + _T_BOOL, + _T_CHOICE, + _T_CLOSE_PAREN, + _T_COMMENT, + _T_CONFIG, + _T_DEFAULT, + _T_DEFCONFIG_LIST, + _T_DEF_BOOL, + _T_DEF_HEX, + _T_DEF_INT, + _T_DEF_STRING, + _T_DEF_TRISTATE, + _T_DEPENDS, + _T_ENDCHOICE, + _T_ENDIF, + _T_ENDMENU, + _T_ENV, + _T_EQUAL, + _T_GREATER, + _T_GREATER_EQUAL, + _T_HELP, + _T_HEX, + _T_IF, + _T_IMPLY, + _T_INT, + _T_LESS, + _T_LESS_EQUAL, + _T_MAINMENU, + _T_MENU, + _T_MENUCONFIG, + _T_MODULES, + _T_NOT, + _T_ON, + _T_OPEN_PAREN, + _T_OPTION, + _T_OPTIONAL, + _T_OR, + _T_ORSOURCE, + _T_OSOURCE, + _T_PROMPT, + _T_RANGE, + _T_RSOURCE, + _T_SELECT, + _T_SOURCE, + _T_STRING, + _T_TRISTATE, + _T_UNEQUAL, + _T_VISIBLE, +) = range(1, 51) + +# Keyword to token map, with the get() method assigned directly as a small +# optimization +_get_keyword = { + "---help---": _T_HELP, + "allnoconfig_y": _T_ALLNOCONFIG_Y, + "bool": _T_BOOL, + "boolean": _T_BOOL, + "choice": _T_CHOICE, + "comment": _T_COMMENT, + "config": _T_CONFIG, + "def_bool": _T_DEF_BOOL, + "def_hex": _T_DEF_HEX, + "def_int": _T_DEF_INT, + "def_string": _T_DEF_STRING, + "def_tristate": _T_DEF_TRISTATE, + "default": _T_DEFAULT, + "defconfig_list": _T_DEFCONFIG_LIST, + "depends": _T_DEPENDS, + "endchoice": _T_ENDCHOICE, + "endif": _T_ENDIF, + "endmenu": _T_ENDMENU, + "env": _T_ENV, + "grsource": _T_ORSOURCE, # Backwards compatibility + "gsource": _T_OSOURCE, # Backwards compatibility + "help": _T_HELP, + "hex": _T_HEX, + "if": _T_IF, + "imply": _T_IMPLY, + "int": _T_INT, + "mainmenu": _T_MAINMENU, + "menu": _T_MENU, + "menuconfig": _T_MENUCONFIG, + "modules": _T_MODULES, + "on": _T_ON, + "option": _T_OPTION, + "optional": _T_OPTIONAL, + "orsource": _T_ORSOURCE, + "osource": _T_OSOURCE, + "prompt": _T_PROMPT, + "range": _T_RANGE, + "rsource": _T_RSOURCE, + "select": _T_SELECT, + "source": _T_SOURCE, + "string": _T_STRING, + "tristate": _T_TRISTATE, + "visible": _T_VISIBLE, +}.get + +# The constants below match the value of the corresponding tokens to remove the +# need for conversion + +# Node types +MENU = _T_MENU +COMMENT = _T_COMMENT + +# Expression types +AND = _T_AND +OR = _T_OR +NOT = _T_NOT +EQUAL = _T_EQUAL +UNEQUAL = _T_UNEQUAL +LESS = _T_LESS +LESS_EQUAL = _T_LESS_EQUAL +GREATER = _T_GREATER +GREATER_EQUAL = _T_GREATER_EQUAL + +REL_TO_STR = { + EQUAL: "=", + UNEQUAL: "!=", + LESS: "<", + LESS_EQUAL: "<=", + GREATER: ">", + GREATER_EQUAL: ">=", +} + +# Symbol/choice types. UNKNOWN is 0 (falsy) to simplify some checks. +# Client code shouldn't rely on it though, as it was non-zero in +# older versions. +UNKNOWN = 0 +BOOL = _T_BOOL +TRISTATE = _T_TRISTATE +STRING = _T_STRING +INT = _T_INT +HEX = _T_HEX + +TYPE_TO_STR = { + UNKNOWN: "unknown", + BOOL: "bool", + TRISTATE: "tristate", + STRING: "string", + INT: "int", + HEX: "hex", +} + +# Used in comparisons. 0 means the base is inferred from the format of the +# string. +_TYPE_TO_BASE = { + HEX: 16, + INT: 10, + STRING: 0, + UNKNOWN: 0, +} + +# def_bool -> BOOL, etc. +_DEF_TOKEN_TO_TYPE = { + _T_DEF_BOOL: BOOL, + _T_DEF_HEX: HEX, + _T_DEF_INT: INT, + _T_DEF_STRING: STRING, + _T_DEF_TRISTATE: TRISTATE, +} + +# Tokens after which strings are expected. This is used to tell strings from +# constant symbol references during tokenization, both of which are enclosed in +# quotes. +# +# Identifier-like lexemes ("missing quotes") are also treated as strings after +# these tokens. _T_CHOICE is included to avoid symbols being registered for +# named choices. +_STRING_LEX = frozenset({ + _T_BOOL, + _T_CHOICE, + _T_COMMENT, + _T_HEX, + _T_INT, + _T_MAINMENU, + _T_MENU, + _T_ORSOURCE, + _T_OSOURCE, + _T_PROMPT, + _T_RSOURCE, + _T_SOURCE, + _T_STRING, + _T_TRISTATE, +}) + +# Various sets for quick membership tests. Gives a single global lookup and +# avoids creating temporary dicts/tuples. + +_TYPE_TOKENS = frozenset({ + _T_BOOL, + _T_TRISTATE, + _T_INT, + _T_HEX, + _T_STRING, +}) + +_SOURCE_TOKENS = frozenset({ + _T_SOURCE, + _T_RSOURCE, + _T_OSOURCE, + _T_ORSOURCE, +}) + +_REL_SOURCE_TOKENS = frozenset({ + _T_RSOURCE, + _T_ORSOURCE, +}) + +# Obligatory (non-optional) sources +_OBL_SOURCE_TOKENS = frozenset({ + _T_SOURCE, + _T_RSOURCE, +}) + +_BOOL_TRISTATE = frozenset({ + BOOL, + TRISTATE, +}) + +_BOOL_TRISTATE_UNKNOWN = frozenset({ + BOOL, + TRISTATE, + UNKNOWN, +}) + +_INT_HEX = frozenset({ + INT, + HEX, +}) + +_SYMBOL_CHOICE = frozenset({ + Symbol, + Choice, +}) + +_MENU_COMMENT = frozenset({ + MENU, + COMMENT, +}) + +_EQUAL_UNEQUAL = frozenset({ + EQUAL, + UNEQUAL, +}) + +_RELATIONS = frozenset({ + EQUAL, + UNEQUAL, + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, +}) + +# Helper functions for getting compiled regular expressions, with the needed +# matching function returned directly as a small optimization. +# +# Use ASCII regex matching on Python 3. It's already the default on Python 2. + + +def _re_match(regex): + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).match + + +def _re_search(regex): + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).search + + +# Various regular expressions used during parsing + +# The initial token on a line. Also eats leading and trailing whitespace, so +# that we can jump straight to the next token (or to the end of the line if +# there is only one token). +# +# This regex will also fail to match for empty lines and comment lines. +# +# '$' is included to detect preprocessor variable assignments with macro +# expansions in the left-hand side. +_command_match = _re_match(r"\s*([A-Za-z0-9_$-]+)\s*") + +# An identifier/keyword after the first token. Also eats trailing whitespace. +# '$' is included to detect identifiers containing macro expansions. +_id_keyword_match = _re_match(r"([A-Za-z0-9_$/.-]+)\s*") + +# A fragment in the left-hand side of a preprocessor variable assignment. These +# are the portions between macro expansions ($(foo)). Macros are supported in +# the LHS (variable name). +_assignment_lhs_fragment_match = _re_match("[A-Za-z0-9_-]*") + +# The assignment operator and value (right-hand side) in a preprocessor +# variable assignment +_assignment_rhs_match = _re_match(r"\s*(=|:=|\+=)\s*(.*)") + +# Special characters/strings while expanding a macro ('(', ')', ',', and '$(') +_macro_special_search = _re_search(r"\(|\)|,|\$\(") + +# Special characters/strings while expanding a string (quotes, '\', and '$(') +_string_special_search = _re_search(r'"|\'|\\|\$\(') + +# Special characters/strings while expanding a symbol name. Also includes +# end-of-line, in case the macro is the last thing on the line. +_name_special_search = _re_search(r'[^A-Za-z0-9_$/.-]|\$\(|$') + +# A valid right-hand side for an assignment to a string symbol in a .config +# file, including escaped characters. Extracts the contents. +_conf_string_match = _re_match(r'"((?:[^\\"]|\\.)*)"') diff --git a/scripts/Kconfiglib/listnewconfig.py b/scripts/Kconfiglib/listnewconfig.py new file mode 100755 index 0000000..8276de1 --- /dev/null +++ b/scripts/Kconfiglib/listnewconfig.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Lists all user-modifiable symbols that are not given a value in the +configuration file. Usually, these are new symbols that have been added to the +Kconfig files. + +The default configuration filename is '.config'. A different filename can be +passed in the KCONFIG_CONFIG environment variable. +""" +from __future__ import print_function + +import argparse +import sys + +from kconfiglib import Kconfig, BOOL, TRISTATE, INT, HEX, STRING, TRI_TO_STR + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--show-help", "-l", + action="store_true", + help="Show any help texts as well") + + parser.add_argument( + "kconfig", + metavar="KCONFIG", + nargs="?", + default="Kconfig", + help="Top-level Kconfig file (default: Kconfig)") + + args = parser.parse_args() + + kconf = Kconfig(args.kconfig, suppress_traceback=True) + # Make it possible to filter this message out + print(kconf.load_config(), file=sys.stderr) + + for sym in kconf.unique_defined_syms: + # Only show symbols that can be toggled. Choice symbols are a special + # case in that sym.assignable will be (2,) (length 1) for visible + # symbols in choices in y mode, but they can still be toggled by + # selecting some other symbol. + if sym.user_value is None and \ + (len(sym.assignable) > 1 or + (sym.visibility and (sym.orig_type in (INT, HEX, STRING) or + sym.choice))): + + # Don't reuse the 'config_string' format for bool/tristate symbols, + # to show n-valued symbols as 'CONFIG_FOO=n' instead of + # '# CONFIG_FOO is not set'. This matches the C tools. + if sym.orig_type in (BOOL, TRISTATE): + s = "{}{}={}\n".format(kconf.config_prefix, sym.name, + TRI_TO_STR[sym.tri_value]) + else: + s = sym.config_string + + print(s, end="") + if args.show_help: + for node in sym.nodes: + if node.help is not None: + # Indent by two spaces. textwrap.indent() is not + # available in Python 2 (it's 3.3+). + print("\n".join(" " + line + for line in node.help.split("\n"))) + break + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/menuconfig.py b/scripts/Kconfiglib/menuconfig.py new file mode 100755 index 0000000..b595a69 --- /dev/null +++ b/scripts/Kconfiglib/menuconfig.py @@ -0,0 +1,3282 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Overview +======== + +A curses-based Python 2/3 menuconfig implementation. The interface should feel +familiar to people used to mconf ('make menuconfig'). + +Supports the same keys as mconf, and also supports a set of keybindings +inspired by Vi: + + J/K : Down/Up + L : Enter menu/Toggle item + H : Leave menu + Ctrl-D/U: Page Down/Page Up + G/End : Jump to end of list + g/Home : Jump to beginning of list + +[Space] toggles values if possible, and enters menus otherwise. [Enter] works +the other way around. + +The mconf feature where pressing a key jumps to a menu entry with that +character in it in the current menu isn't supported. A jump-to feature for +jumping directly to any symbol (including invisible symbols), choice, menu or +comment (as in a Kconfig 'comment "Foo"') is available instead. + +A few different modes are available: + + F: Toggle show-help mode, which shows the help text of the currently selected + item in the window at the bottom of the menu display. This is handy when + browsing through options. + + C: Toggle show-name mode, which shows the symbol name before each symbol menu + entry + + A: Toggle show-all mode, which shows all items, including currently invisible + items and items that lack a prompt. Invisible items are drawn in a different + style to make them stand out. + + +Running +======= + +menuconfig.py can be run either as a standalone executable or by calling the +menuconfig() function with an existing Kconfig instance. The second option is a +bit inflexible in that it will still load and save .config, etc. + +When run in standalone mode, the top-level Kconfig file to load can be passed +as a command-line argument. With no argument, it defaults to "Kconfig". + +The KCONFIG_CONFIG environment variable specifies the .config file to load (if +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). + +$srctree is supported through Kconfiglib. + + +Color schemes +============= + +It is possible to customize the color scheme by setting the MENUCONFIG_STYLE +environment variable. For example, setting it to 'aquatic' will enable an +alternative, less yellow, more 'make menuconfig'-like color scheme, contributed +by Mitja Horvat (pinkfluid). + +This is the current list of built-in styles: + - default classic Kconfiglib theme with a yellow accent + - monochrome colorless theme (uses only bold and standout) attributes, + this style is used if the terminal doesn't support colors + - aquatic blue-tinted style loosely resembling the lxdialog theme + +It is possible to customize the current style by changing colors of UI +elements on the screen. This is the list of elements that can be stylized: + + - path Top row in the main display, with the menu path + - separator Separator lines between windows. Also used for the top line + in the symbol information display. + - list List of items, e.g. the main display + - selection Style for the selected item + - inv-list Like list, but for invisible items. Used in show-all mode. + - inv-selection Like selection, but for invisible items. Used in show-all + mode. + - help Help text windows at the bottom of various fullscreen + dialogs + - show-help Window showing the help text in show-help mode + - frame Frame around dialog boxes + - body Body of dialog boxes + - edit Edit box in pop-up dialogs + - jump-edit Edit box in jump-to dialog + - text Symbol information text + +The color definition is a comma separated list of attributes: + + - fg:COLOR Set the foreground/background colors. COLOR can be one of + * or * the basic 16 colors (black, red, green, yellow, blue, + - bg:COLOR magenta, cyan, white and brighter versions, for example, + brightred). On terminals that support more than 8 colors, + you can also directly put in a color number, e.g. fg:123 + (hexadecimal and octal constants are accepted as well). + Colors outside the range -1..curses.COLORS-1 (which is + terminal-dependent) are ignored (with a warning). The COLOR + can be also specified using a RGB value in the HTML + notation, for example #RRGGBB. If the terminal supports + color changing, the color is rendered accurately. + Otherwise, the visually nearest color is used. + + If the background or foreground color of an element is not + specified, it defaults to -1, representing the default + terminal foreground or background color. + + Note: On some terminals a bright version of the color + implies bold. + - bold Use bold text + - underline Use underline text + - standout Standout text attribute (reverse color) + +More often than not, some UI elements share the same color definition. In such +cases the right value may specify an UI element from which the color definition +will be copied. For example, "separator=help" will apply the current color +definition for "help" to "separator". + +A keyword without the '=' is assumed to be a style template. The template name +is looked up in the built-in styles list and the style definition is expanded +in-place. With this, built-in styles can be used as basis for new styles. + +For example, take the aquatic theme and give it a red selection bar: + +MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red" + +If there's an error in the style definition or if a missing style is assigned +to, the assignment will be ignored, along with a warning being printed on +stderr. + +The 'default' theme is always implicitly parsed first, so the following two +settings have the same effect: + + MENUCONFIG_STYLE="selection=fg:white,bg:red" + MENUCONFIG_STYLE="default selection=fg:white,bg:red" + +If the terminal doesn't support colors, the 'monochrome' theme is used, and +MENUCONFIG_STYLE is ignored. The assumption is that the environment is broken +somehow, and that the important thing is to get something usable. + + +Other features +============== + + - Seamless terminal resizing + + - No dependencies on *nix, as the 'curses' module is in the Python standard + library + + - Unicode text entry + + - Improved information screen compared to mconf: + + * Expressions are split up by their top-level &&/|| operands to improve + readability + + * Undefined symbols in expressions are pointed out + + * Menus and comments have information displays + + * Kconfig definitions are printed + + * The include path is shown, listing the locations of the 'source' + statements that included the Kconfig file of the symbol (or other + item) + + +Limitations +=========== + +Doesn't work out of the box on Windows, but can be made to work with + + pip install windows-curses + +See the https://github.com/zephyrproject-rtos/windows-curses repository. +""" +from __future__ import print_function + +import os +import sys + +_IS_WINDOWS = os.name == "nt" # Are we running on Windows? + +try: + import curses +except ImportError as e: + if not _IS_WINDOWS: + raise + sys.exit("""\ +menuconfig failed to import the standard Python 'curses' library. Try +installing a package like windows-curses +(https://github.com/zephyrproject-rtos/windows-curses) by running this command +in cmd.exe: + + pip install windows-curses + +Starting with Kconfiglib 13.0.0, windows-curses is no longer automatically +installed when installing Kconfiglib via pip on Windows (because it breaks +installation on MSYS2). + +Exception: +{}: {}""".format(type(e).__name__, e)) + +import errno +import locale +import re +import textwrap + +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ + BOOL, TRISTATE, STRING, INT, HEX, \ + AND, OR, \ + expr_str, expr_value, split_expr, \ + standard_sc_expr_str, \ + TRI_TO_STR, TYPE_TO_STR, \ + standard_kconfig, standard_config_filename + + +# +# Configuration variables +# + +# If True, try to change LC_CTYPE to a UTF-8 locale if it is set to the C +# locale (which implies ASCII). This fixes curses Unicode I/O issues on systems +# with bad defaults. ncurses configures itself from the locale settings. +# +# Related PEP: https://www.python.org/dev/peps/pep-0538/ +_CHANGE_C_LC_CTYPE_TO_UTF8 = True + +# How many steps an implicit submenu will be indented. Implicit submenus are +# created when an item depends on the symbol before it. Note that symbols +# defined with 'menuconfig' create a separate menu instead of indenting. +_SUBMENU_INDENT = 4 + +# Number of steps for Page Up/Down to jump +_PG_JUMP = 6 + +# Height of the help window in show-help mode +_SHOW_HELP_HEIGHT = 8 + +# How far the cursor needs to be from the edge of the window before it starts +# to scroll. Used for the main menu display, the information display, the +# search display, and for text boxes. +_SCROLL_OFFSET = 5 + +# Minimum width of dialogs that ask for text input +_INPUT_DIALOG_MIN_WIDTH = 30 + +# Number of arrows pointing up/down to draw when a window is scrolled +_N_SCROLL_ARROWS = 14 + +# Lines of help text shown at the bottom of the "main" display +_MAIN_HELP_LINES = """ +[Space/Enter] Toggle/enter [ESC] Leave menu [S] Save +[O] Load [?] Symbol info [/] Jump to symbol +[F] Toggle show-help mode [C] Toggle show-name mode [A] Toggle show-all mode +[Q] Quit (prompts for save) [D] Save minimal config (advanced) +"""[1:-1].split("\n") + +# Lines of help text shown at the bottom of the information dialog +_INFO_HELP_LINES = """ +[ESC/q] Return to menu [/] Jump to symbol +"""[1:-1].split("\n") + +# Lines of help text shown at the bottom of the search dialog +_JUMP_TO_HELP_LINES = """ +Type text to narrow the search. Regexes are supported (via Python's 're' +module). The up/down cursor keys step in the list. [Enter] jumps to the +selected symbol. [ESC] aborts the search. Type multiple space-separated +strings/regexes to find entries that match all of them. Type Ctrl-F to +view the help of the selected item without leaving the dialog. +"""[1:-1].split("\n") + +# +# Styling +# + +_STYLES = { + "default": """ + path=fg:black,bg:white,bold + separator=fg:black,bg:yellow,bold + list=fg:black,bg:white + selection=fg:white,bg:blue,bold + inv-list=fg:red,bg:white + inv-selection=fg:red,bg:blue + help=path + show-help=list + frame=fg:black,bg:yellow,bold + body=fg:white,bg:black + edit=fg:white,bg:blue + jump-edit=edit + text=list + """, + + # This style is forced on terminals that do no support colors + "monochrome": """ + path=bold + separator=bold,standout + list= + selection=bold,standout + inv-list=bold + inv-selection=bold,standout + help=bold + show-help= + frame=bold,standout + body= + edit=standout + jump-edit= + text= + """, + + # Blue-tinted style loosely resembling lxdialog + "aquatic": """ + path=fg:white,bg:blue + separator=fg:white,bg:cyan + help=path + frame=fg:white,bg:cyan + body=fg:white,bg:blue + edit=fg:black,bg:white + """ +} + +_NAMED_COLORS = { + # Basic colors + "black": curses.COLOR_BLACK, + "red": curses.COLOR_RED, + "green": curses.COLOR_GREEN, + "yellow": curses.COLOR_YELLOW, + "blue": curses.COLOR_BLUE, + "magenta": curses.COLOR_MAGENTA, + "cyan": curses.COLOR_CYAN, + "white": curses.COLOR_WHITE, + + # Bright versions + "brightblack": curses.COLOR_BLACK + 8, + "brightred": curses.COLOR_RED + 8, + "brightgreen": curses.COLOR_GREEN + 8, + "brightyellow": curses.COLOR_YELLOW + 8, + "brightblue": curses.COLOR_BLUE + 8, + "brightmagenta": curses.COLOR_MAGENTA + 8, + "brightcyan": curses.COLOR_CYAN + 8, + "brightwhite": curses.COLOR_WHITE + 8, + + # Aliases + "purple": curses.COLOR_MAGENTA, + "brightpurple": curses.COLOR_MAGENTA + 8, +} + + +def _rgb_to_6cube(rgb): + # Converts an 888 RGB color to a 3-tuple (nice in that it's hashable) + # representing the closest xterm 256-color 6x6x6 color cube color. + # + # The xterm 256-color extension uses a RGB color palette with components in + # the range 0-5 (a 6x6x6 cube). The catch is that the mapping is nonlinear. + # Index 0 in the 6x6x6 cube is mapped to 0, index 1 to 95, then 135, 175, + # etc., in increments of 40. See the links below: + # + # https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg + # https://github.com/tmux/tmux/blob/master/colour.c + + # 48 is the middle ground between 0 and 95. + return tuple(0 if x < 48 else int(round(max(1, (x - 55)/40))) for x in rgb) + + +def _6cube_to_rgb(r6g6b6): + # Returns the 888 RGB color for a 666 xterm color cube index + + return tuple(0 if x == 0 else 40*x + 55 for x in r6g6b6) + + +def _rgb_to_gray(rgb): + # Converts an 888 RGB color to the index of an xterm 256-color grayscale + # color with approx. the same perceived brightness + + # Calculate the luminance (gray intensity) of the color. See + # https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color + # and + # https://www.w3.org/TR/AERT/#color-contrast + luma = 0.299*rgb[0] + 0.587*rgb[1] + 0.114*rgb[2] + + # Closest index in the grayscale palette, which starts at RGB 0x080808, + # with stepping 0x0A0A0A + index = int(round((luma - 8)/10)) + + # Clamp the index to 0-23, corresponding to 232-255 + return max(0, min(index, 23)) + + +def _gray_to_rgb(index): + # Convert a grayscale index to its closet single RGB component + + return 3*(10*index + 8,) # Returns a 3-tuple + + +# Obscure Python: We never pass a value for rgb2index, and it keeps pointing to +# the same dict. This avoids a global. +def _alloc_rgb(rgb, rgb2index={}): + # Initialize a new entry in the xterm palette to the given RGB color, + # returning its index. If the color has already been initialized, the index + # of the existing entry is returned. + # + # ncurses is palette-based, so we need to overwrite palette entries to make + # new colors. + # + # The colors from 0 to 15 are user-defined, and there's no way to query + # their RGB values, so we better leave them untouched. Also leave any + # hypothetical colors above 255 untouched (though we're unlikely to + # allocate that many colors anyway). + + if rgb in rgb2index: + return rgb2index[rgb] + + # Many terminals allow the user to customize the first 16 colors. Avoid + # changing their values. + color_index = 16 + len(rgb2index) + if color_index >= 256: + _warn("Unable to allocate new RGB color ", rgb, ". Too many colors " + "allocated.") + return 0 + + # Map each RGB component from the range 0-255 to the range 0-1000, which is + # what curses uses + curses.init_color(color_index, *(int(round(1000*x/255)) for x in rgb)) + rgb2index[rgb] = color_index + + return color_index + + +def _color_from_num(num): + # Returns the index of a color that looks like color 'num' in the xterm + # 256-color palette (but that might not be 'num', if we're redefining + # colors) + + # - _alloc_rgb() won't touch the first 16 colors or any (hypothetical) + # colors above 255, so we can always return them as-is + # + # - If the terminal doesn't support changing color definitions, or if + # curses.COLORS < 256, _alloc_rgb() won't touch any color, and all colors + # can be returned as-is + if num < 16 or num > 255 or not curses.can_change_color() or \ + curses.COLORS < 256: + return num + + # _alloc_rgb() might redefine colors, so emulate the xterm 256-color + # palette by allocating new colors instead of returning color numbers + # directly + + if num < 232: + num -= 16 + return _alloc_rgb(_6cube_to_rgb(((num//36)%6, (num//6)%6, num%6))) + + return _alloc_rgb(_gray_to_rgb(num - 232)) + + +def _color_from_rgb(rgb): + # Returns the index of a color matching the 888 RGB color 'rgb'. The + # returned color might be an ~exact match or an approximation, depending on + # terminal capabilities. + + # Calculates the Euclidean distance between two RGB colors + def dist(r1, r2): return sum((x - y)**2 for x, y in zip(r1, r2)) + + if curses.COLORS >= 256: + # Assume we're dealing with xterm's 256-color extension + + if curses.can_change_color(): + # Best case -- the terminal supports changing palette entries via + # curses.init_color(). Initialize an unused palette entry and + # return it. + return _alloc_rgb(rgb) + + # Second best case -- pick between the xterm 256-color extension colors + + # Closest 6-cube "color" color + c6 = _rgb_to_6cube(rgb) + # Closest gray color + gray = _rgb_to_gray(rgb) + + if dist(rgb, _6cube_to_rgb(c6)) < dist(rgb, _gray_to_rgb(gray)): + # Use the "color" color from the 6x6x6 color palette. Calculate the + # color number from the 6-cube index triplet. + return 16 + 36*c6[0] + 6*c6[1] + c6[2] + + # Use the color from the gray palette + return 232 + gray + + # Terminal not in xterm 256-color mode. This is probably the best we can + # do, or is it? Submit patches. :) + min_dist = float('inf') + best = -1 + for color in range(curses.COLORS): + # ncurses uses the range 0..1000. Scale that down to 0..255. + d = dist(rgb, tuple(int(round(255*c/1000)) + for c in curses.color_content(color))) + if d < min_dist: + min_dist = d + best = color + + return best + + +def _parse_style(style_str, parsing_default): + # Parses a string with '<element>=<style>' assignments. Anything not + # containing '=' is assumed to be a reference to a built-in style, which is + # treated as if all the assignments from the style were inserted at that + # point in the string. + # + # The parsing_default flag is set to True when we're implicitly parsing the + # 'default'/'monochrome' style, to prevent warnings. + + for sline in style_str.split(): + # Words without a "=" character represents a style template + if "=" in sline: + key, data = sline.split("=", 1) + + # The 'default' style template is assumed to define all keys. We + # run _style_to_curses() for non-existing keys as well, so that we + # print warnings for errors to the right of '=' for those too. + if key not in _style and not parsing_default: + _warn("Ignoring non-existent style", key) + + # If data is a reference to another key, copy its style + if data in _style: + _style[key] = _style[data] + else: + _style[key] = _style_to_curses(data) + + elif sline in _STYLES: + # Recursively parse style template. Ignore styles that don't exist, + # for backwards/forwards compatibility. + _parse_style(_STYLES[sline], parsing_default) + + else: + _warn("Ignoring non-existent style template", sline) + +# Dictionary mapping element types to the curses attributes used to display +# them +_style = {} + + +def _style_to_curses(style_def): + # Parses a style definition string (<element>=<style>), returning + # a (fg_color, bg_color, attributes) tuple. + + def parse_color(color_def): + color_def = color_def.split(":", 1)[1] + + # HTML format, #RRGGBB + if re.match("#[A-Fa-f0-9]{6}", color_def): + return _color_from_rgb(( + int(color_def[1:3], 16), + int(color_def[3:5], 16), + int(color_def[5:7], 16))) + + if color_def in _NAMED_COLORS: + color_num = _color_from_num(_NAMED_COLORS[color_def]) + else: + try: + color_num = _color_from_num(int(color_def, 0)) + except ValueError: + _warn("Ignoring color", color_def, "that's neither " + "predefined nor a number") + return -1 + + if not -1 <= color_num < curses.COLORS: + _warn("Ignoring color {}, which is outside the range " + "-1..curses.COLORS-1 (-1..{})" + .format(color_def, curses.COLORS - 1)) + return -1 + + return color_num + + fg_color = -1 + bg_color = -1 + attrs = 0 + + if style_def: + for field in style_def.split(","): + if field.startswith("fg:"): + fg_color = parse_color(field) + elif field.startswith("bg:"): + bg_color = parse_color(field) + elif field == "bold": + # A_BOLD tends to produce faint and hard-to-read text on the + # Windows console, especially with the old color scheme, before + # the introduction of + # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/ + attrs |= curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD + elif field == "standout": + attrs |= curses.A_STANDOUT + elif field == "underline": + attrs |= curses.A_UNDERLINE + else: + _warn("Ignoring unknown style attribute", field) + + return _style_attr(fg_color, bg_color, attrs) + + +def _init_styles(): + if curses.has_colors(): + try: + curses.use_default_colors() + except curses.error: + # Ignore errors on funky terminals that support colors but not + # using default colors. Worst it can do is break transparency and + # the like. Ran across this with the MSYS2/winpty setup in + # https://github.com/msys2/MINGW-packages/issues/5823, though there + # seems to be a lot of general brokenness there. + pass + + # Use the 'default' theme as the base, and add any user-defined style + # settings from the environment + _parse_style("default", True) + if "MENUCONFIG_STYLE" in os.environ: + _parse_style(os.environ["MENUCONFIG_STYLE"], False) + else: + # Force the 'monochrome' theme if the terminal doesn't support colors. + # MENUCONFIG_STYLE is likely to mess things up here (though any colors + # would be ignored), so ignore it. + _parse_style("monochrome", True) + + +# color_attribs holds the color pairs we've already created, indexed by a +# (<foreground color>, <background color>) tuple. +# +# Obscure Python: We never pass a value for color_attribs, and it keeps +# pointing to the same dict. This avoids a global. +def _style_attr(fg_color, bg_color, attribs, color_attribs={}): + # Returns an attribute with the specified foreground and background color + # and the attributes in 'attribs'. Reuses color pairs already created if + # possible, and creates a new color pair otherwise. + # + # Returns 'attribs' if colors aren't supported. + + if not curses.has_colors(): + return attribs + + if (fg_color, bg_color) not in color_attribs: + # Create new color pair. Color pair number 0 is hardcoded and cannot be + # changed, hence the +1s. + curses.init_pair(len(color_attribs) + 1, fg_color, bg_color) + color_attribs[(fg_color, bg_color)] = \ + curses.color_pair(len(color_attribs) + 1) + + return color_attribs[(fg_color, bg_color)] | attribs + + +# +# Main application +# + + +def _main(): + menuconfig(standard_kconfig(__doc__)) + + +def menuconfig(kconf): + """ + Launches the configuration interface, returning after the user exits. + + kconf: + Kconfig instance to be configured + """ + global _kconf + global _conf_filename + global _conf_changed + global _minconf_filename + global _show_all + + _kconf = kconf + + # Filename to save configuration to + _conf_filename = standard_config_filename() + + # Load existing configuration and set _conf_changed True if it is outdated + _conf_changed = _load_config() + + # Filename to save minimal configuration to + _minconf_filename = "defconfig" + + # Any visible items in the top menu? + _show_all = False + if not _shown_nodes(kconf.top_node): + # Nothing visible. Start in show-all mode and try again. + _show_all = True + if not _shown_nodes(kconf.top_node): + # Give up. The implementation relies on always having a selected + # node. + print("Empty configuration -- nothing to configure.\n" + "Check that environment variables are set properly.") + return + + # Disable warnings. They get mangled in curses mode, and we deal with + # errors ourselves. + kconf.warn = False + + try: + # Make curses use the locale settings specified in the environment + locale.setlocale(locale.LC_ALL, "") + except locale.Error: + # fall back to the default locale + locale.setlocale(locale.LC_ALL, "C") + + # Try to fix Unicode issues on systems with bad defaults + if _CHANGE_C_LC_CTYPE_TO_UTF8: + _change_c_lc_ctype_to_utf8() + + # Get rid of the delay between pressing ESC and jumping to the parent menu, + # unless the user has set ESCDELAY (see ncurses(3)). This makes the UI much + # smoother to work with. + # + # Note: This is strictly pretty iffy, since escape codes for e.g. cursor + # keys start with ESC, but I've never seen it cause problems in practice + # (probably because it's unlikely that the escape code for a key would get + # split up across read()s, at least with a terminal emulator). Please + # report if you run into issues. Some suitable small default value could be + # used here instead in that case. Maybe it's silly to not put in the + # smallest imperceptible delay here already, though I don't like guessing. + # + # (From a quick glance at the ncurses source code, ESCDELAY might only be + # relevant for mouse events there, so maybe escapes are assumed to arrive + # in one piece already...) + os.environ.setdefault("ESCDELAY", "0") + + # Enter curses mode. _menuconfig() returns a string to print on exit, after + # curses has been de-initialized. + print(curses.wrapper(_menuconfig)) + + +def _load_config(): + # Loads any existing .config file. See the Kconfig.load_config() docstring. + # + # Returns True if .config is missing or outdated. We always prompt for + # saving the configuration in that case. + + print(_kconf.load_config()) + if not os.path.exists(_conf_filename): + # No .config + return True + + return _needs_save() + + +def _needs_save(): + # Returns True if a just-loaded .config file is outdated (would get + # modified when saving) + + if _kconf.missing_syms: + # Assignments to undefined symbols in the .config + return True + + for sym in _kconf.unique_defined_syms: + if sym.user_value is None: + if sym.config_string: + # Unwritten symbol + return True + elif sym.orig_type in (BOOL, TRISTATE): + if sym.tri_value != sym.user_value: + # Written bool/tristate symbol, new value + return True + elif sym.str_value != sym.user_value: + # Written string/int/hex symbol, new value + return True + + # No need to prompt for save + return False + + +# Global variables used below: +# +# _stdscr: +# stdscr from curses +# +# _cur_menu: +# Menu node of the menu (or menuconfig symbol, or choice) currently being +# shown +# +# _shown: +# List of items in _cur_menu that are shown (ignoring scrolling). In +# show-all mode, this list contains all items in _cur_menu. Otherwise, it +# contains just the visible items. +# +# _sel_node_i: +# Index in _shown of the currently selected node +# +# _menu_scroll: +# Index in _shown of the top row of the main display +# +# _parent_screen_rows: +# List/stack of the row numbers that the selections in the parent menus +# appeared on. This is used to prevent the scrolling from jumping around +# when going in and out of menus. +# +# _show_help/_show_name/_show_all: +# If True, the corresponding mode is on. See the module docstring. +# +# _conf_filename: +# File to save the configuration to +# +# _minconf_filename: +# File to save minimal configurations to +# +# _conf_changed: +# True if the configuration has been changed. If False, we don't bother +# showing the save-and-quit dialog. +# +# We reset this to False whenever the configuration is saved explicitly +# from the save dialog. + + +def _menuconfig(stdscr): + # Logic for the main display, with the list of symbols, etc. + + global _stdscr + global _conf_filename + global _conf_changed + global _minconf_filename + global _show_help + global _show_name + + _stdscr = stdscr + + _init() + + while True: + _draw_main() + curses.doupdate() + + + c = _getch_compat(_menu_win) + + if c == curses.KEY_RESIZE: + _resize_main() + + elif c in (curses.KEY_DOWN, "j", "J"): + _select_next_menu_entry() + + elif c in (curses.KEY_UP, "k", "K"): + _select_prev_menu_entry() + + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D + # Keep it simple. This way we get sane behavior for small windows, + # etc., for free. + for _ in range(_PG_JUMP): + _select_next_menu_entry() + + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U + for _ in range(_PG_JUMP): + _select_prev_menu_entry() + + elif c in (curses.KEY_END, "G"): + _select_last_menu_entry() + + elif c in (curses.KEY_HOME, "g"): + _select_first_menu_entry() + + elif c == " ": + # Toggle the node if possible + sel_node = _shown[_sel_node_i] + if not _change_node(sel_node): + _enter_menu(sel_node) + + elif c in (curses.KEY_RIGHT, "\n", "l", "L"): + # Enter the node if possible + sel_node = _shown[_sel_node_i] + if not _enter_menu(sel_node): + _change_node(sel_node) + + elif c in ("n", "N"): + _set_sel_node_tri_val(0) + + elif c in ("m", "M"): + _set_sel_node_tri_val(1) + + elif c in ("y", "Y"): + _set_sel_node_tri_val(2) + + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, + "\x1B", "h", "H"): # \x1B = ESC + + if c == "\x1B" and _cur_menu is _kconf.top_node: + res = _quit_dialog() + if res: + return res + else: + _leave_menu() + + elif c in ("o", "O"): + _load_dialog() + + elif c in ("s", "S"): + filename = _save_dialog(_kconf.write_config, _conf_filename, + "configuration") + if filename: + _conf_filename = filename + _conf_changed = False + + elif c in ("d", "D"): + filename = _save_dialog(_kconf.write_min_config, _minconf_filename, + "minimal configuration") + if filename: + _minconf_filename = filename + + elif c == "/": + _jump_to_dialog() + # The terminal might have been resized while the fullscreen jump-to + # dialog was open + _resize_main() + + elif c == "?": + _info_dialog(_shown[_sel_node_i], False) + # The terminal might have been resized while the fullscreen info + # dialog was open + _resize_main() + + elif c in ("f", "F"): + _show_help = not _show_help + _set_style(_help_win, "show-help" if _show_help else "help") + _resize_main() + + elif c in ("c", "C"): + _show_name = not _show_name + + elif c in ("a", "A"): + _toggle_show_all() + + elif c in ("q", "Q"): + res = _quit_dialog() + if res: + return res + + +def _quit_dialog(): + if not _conf_changed: + return "No changes to save (for '{}')".format(_conf_filename) + + while True: + c = _key_dialog( + "Quit", + " Save configuration?\n" + "\n" + "(Y)es (N)o (C)ancel", + "ync") + + if c is None or c == "c": + return None + + if c == "y": + # Returns a message to print + msg = _try_save(_kconf.write_config, _conf_filename, "configuration") + if msg: + return msg + + elif c == "n": + return "Configuration ({}) was not saved".format(_conf_filename) + + +def _init(): + # Initializes the main display with the list of symbols, etc. Also does + # misc. global initialization that needs to happen after initializing + # curses. + + global _ERASE_CHAR + + global _path_win + global _top_sep_win + global _menu_win + global _bot_sep_win + global _help_win + + global _parent_screen_rows + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + + global _show_help + global _show_name + + # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes + # backspace work with TERM=vt100. That makes it likely to work in sane + # environments. + _ERASE_CHAR = curses.erasechar() + if sys.version_info[0] >= 3: + # erasechar() returns a one-byte bytes object on Python 3. This sets + # _ERASE_CHAR to a blank string if it can't be decoded, which should be + # harmless. + _ERASE_CHAR = _ERASE_CHAR.decode("utf-8", "ignore") + + _init_styles() + + # Hide the cursor + _safe_curs_set(0) + + # Initialize windows + + # Top row, with menu path + _path_win = _styled_win("path") + + # Separator below menu path, with title and arrows pointing up + _top_sep_win = _styled_win("separator") + + # List of menu entries with symbols, etc. + _menu_win = _styled_win("list") + _menu_win.keypad(True) + + # Row below menu list, with arrows pointing down + _bot_sep_win = _styled_win("separator") + + # Help window with keys at the bottom. Shows help texts in show-help mode. + _help_win = _styled_win("help") + + # The rows we'd like the nodes in the parent menus to appear on. This + # prevents the scroll from jumping around when going in and out of menus. + _parent_screen_rows = [] + + # Initial state + + _cur_menu = _kconf.top_node + _shown = _shown_nodes(_cur_menu) + _sel_node_i = _menu_scroll = 0 + + _show_help = _show_name = False + + # Give windows their initial size + _resize_main() + + +def _resize_main(): + # Resizes the main display, with the list of symbols, etc., to fill the + # terminal + + global _menu_scroll + + screen_height, screen_width = _stdscr.getmaxyx() + + _path_win.resize(1, screen_width) + _top_sep_win.resize(1, screen_width) + _bot_sep_win.resize(1, screen_width) + + help_win_height = _SHOW_HELP_HEIGHT if _show_help else \ + len(_MAIN_HELP_LINES) + + menu_win_height = screen_height - help_win_height - 3 + + if menu_win_height >= 1: + _menu_win.resize(menu_win_height, screen_width) + _help_win.resize(help_win_height, screen_width) + + _top_sep_win.mvwin(1, 0) + _menu_win.mvwin(2, 0) + _bot_sep_win.mvwin(2 + menu_win_height, 0) + _help_win.mvwin(2 + menu_win_height + 1, 0) + else: + # Degenerate case. Give up on nice rendering and just prevent errors. + + menu_win_height = 1 + + _menu_win.resize(1, screen_width) + _help_win.resize(1, screen_width) + + for win in _top_sep_win, _menu_win, _bot_sep_win, _help_win: + win.mvwin(0, 0) + + # Adjust the scroll so that the selected node is still within the window, + # if needed + if _sel_node_i - _menu_scroll >= menu_win_height: + _menu_scroll = _sel_node_i - menu_win_height + 1 + + +def _height(win): + # Returns the height of 'win' + + return win.getmaxyx()[0] + + +def _width(win): + # Returns the width of 'win' + + return win.getmaxyx()[1] + + +def _enter_menu(menu): + # Makes 'menu' the currently displayed menu. In addition to actual 'menu's, + # "menu" here includes choices and symbols defined with the 'menuconfig' + # keyword. + # + # Returns False if 'menu' can't be entered. + + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + + if not menu.is_menuconfig: + return False # Not a menu + + shown_sub = _shown_nodes(menu) + # Never enter empty menus. We depend on having a current node. + if not shown_sub: + return False + + # Remember where the current node appears on the screen, so we can try + # to get it to appear in the same place when we leave the menu + _parent_screen_rows.append(_sel_node_i - _menu_scroll) + + # Jump into menu + _cur_menu = menu + _shown = shown_sub + _sel_node_i = _menu_scroll = 0 + + if isinstance(menu.item, Choice): + _select_selected_choice_sym() + + return True + + +def _select_selected_choice_sym(): + # Puts the cursor on the currently selected (y-valued) choice symbol, if + # any. Does nothing if if the choice has no selection (is not visible/in y + # mode). + + global _sel_node_i + + choice = _cur_menu.item + if choice.selection: + # Search through all menu nodes to handle choice symbols being defined + # in multiple locations + for node in choice.selection.nodes: + if node in _shown: + _sel_node_i = _shown.index(node) + _center_vertically() + return + + +def _jump_to(node): + # Jumps directly to the menu node 'node' + + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + global _show_all + global _parent_screen_rows + + # Clear remembered menu locations. We might not even have been in the + # parent menus before. + _parent_screen_rows = [] + + old_show_all = _show_all + jump_into = (isinstance(node.item, Choice) or node.item == MENU) and \ + node.list + + # If we're jumping to a non-empty choice or menu, jump to the first entry + # in it instead of jumping to its menu node + if jump_into: + _cur_menu = node + node = node.list + else: + _cur_menu = _parent_menu(node) + + _shown = _shown_nodes(_cur_menu) + if node not in _shown: + # The node wouldn't be shown. Turn on show-all to show it. + _show_all = True + _shown = _shown_nodes(_cur_menu) + + _sel_node_i = _shown.index(node) + + if jump_into and not old_show_all and _show_all: + # If we're jumping into a choice or menu and were forced to turn on + # show-all because the first entry wasn't visible, try turning it off. + # That will land us at the first visible node if there are visible + # nodes, and is a no-op otherwise. + _toggle_show_all() + + _center_vertically() + + # If we're jumping to a non-empty choice, jump to the selected symbol, if + # any + if jump_into and isinstance(_cur_menu.item, Choice): + _select_selected_choice_sym() + + +def _leave_menu(): + # Jumps to the parent menu of the current menu. Does nothing if we're in + # the top menu. + + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + + if _cur_menu is _kconf.top_node: + return + + # Jump to parent menu + parent = _parent_menu(_cur_menu) + _shown = _shown_nodes(parent) + _sel_node_i = _shown.index(_cur_menu) + _cur_menu = parent + + # Try to make the menu entry appear on the same row on the screen as it did + # before we entered the menu. + + if _parent_screen_rows: + # The terminal might have shrunk since we were last in the parent menu + screen_row = min(_parent_screen_rows.pop(), _height(_menu_win) - 1) + _menu_scroll = max(_sel_node_i - screen_row, 0) + else: + # No saved parent menu locations, meaning we jumped directly to some + # node earlier + _center_vertically() + + +def _select_next_menu_entry(): + # Selects the menu entry after the current one, adjusting the scroll if + # necessary. Does nothing if we're already at the last menu entry. + + global _sel_node_i + global _menu_scroll + + if _sel_node_i < len(_shown) - 1: + # Jump to the next node + _sel_node_i += 1 + + # If the new node is sufficiently close to the edge of the menu window + # (as determined by _SCROLL_OFFSET), increase the scroll by one. This + # gives nice and non-jumpy behavior even when + # _SCROLL_OFFSET >= _height(_menu_win). + if _sel_node_i >= _menu_scroll + _height(_menu_win) - _SCROLL_OFFSET \ + and _menu_scroll < _max_scroll(_shown, _menu_win): + + _menu_scroll += 1 + + +def _select_prev_menu_entry(): + # Selects the menu entry before the current one, adjusting the scroll if + # necessary. Does nothing if we're already at the first menu entry. + + global _sel_node_i + global _menu_scroll + + if _sel_node_i > 0: + # Jump to the previous node + _sel_node_i -= 1 + + # See _select_next_menu_entry() + if _sel_node_i < _menu_scroll + _SCROLL_OFFSET: + _menu_scroll = max(_menu_scroll - 1, 0) + + +def _select_last_menu_entry(): + # Selects the last menu entry in the current menu + + global _sel_node_i + global _menu_scroll + + _sel_node_i = len(_shown) - 1 + _menu_scroll = _max_scroll(_shown, _menu_win) + + +def _select_first_menu_entry(): + # Selects the first menu entry in the current menu + + global _sel_node_i + global _menu_scroll + + _sel_node_i = _menu_scroll = 0 + + +def _toggle_show_all(): + # Toggles show-all mode on/off. If turning it off would give no visible + # items in the current menu, it is left on. + + global _show_all + global _shown + global _sel_node_i + global _menu_scroll + + # Row on the screen the cursor is on. Preferably we want the same row to + # stay highlighted. + old_row = _sel_node_i - _menu_scroll + + _show_all = not _show_all + # List of new nodes to be shown after toggling _show_all + new_shown = _shown_nodes(_cur_menu) + + # Find a good node to select. The selected node might disappear if show-all + # mode is turned off. + + # Select the previously selected node itself if it is still visible. If + # there are visible nodes before it, select the closest one. + for node in _shown[_sel_node_i::-1]: + if node in new_shown: + _sel_node_i = new_shown.index(node) + break + else: + # No visible nodes before the previously selected node. Select the + # closest visible node after it instead. + for node in _shown[_sel_node_i + 1:]: + if node in new_shown: + _sel_node_i = new_shown.index(node) + break + else: + # No visible nodes at all, meaning show-all was turned off inside + # an invisible menu. Don't allow that, as the implementation relies + # on always having a selected node. + _show_all = True + return + + _shown = new_shown + + # Try to make the cursor stay on the same row in the menu window. This + # might be impossible if too many nodes have disappeared above the node. + _menu_scroll = max(_sel_node_i - old_row, 0) + + +def _center_vertically(): + # Centers the selected node vertically, if possible + + global _menu_scroll + + _menu_scroll = min(max(_sel_node_i - _height(_menu_win)//2, 0), + _max_scroll(_shown, _menu_win)) + + +def _draw_main(): + # Draws the "main" display, with the list of symbols, the header, and the + # footer. + # + # This could be optimized to only update the windows that have actually + # changed, but keep it simple for now and let curses sort it out. + + term_width = _width(_stdscr) + + # + # Update the separator row below the menu path + # + + _top_sep_win.erase() + + # Draw arrows pointing up if the symbol window is scrolled down. Draw them + # before drawing the title, so the title ends up on top for small windows. + if _menu_scroll > 0: + _safe_hline(_top_sep_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) + + # Add the 'mainmenu' text as the title, centered at the top + _safe_addstr(_top_sep_win, + 0, max((term_width - len(_kconf.mainmenu_text))//2, 0), + _kconf.mainmenu_text) + + _top_sep_win.noutrefresh() + + # Note: The menu path at the top is deliberately updated last. See below. + + # + # Update the symbol window + # + + _menu_win.erase() + + # Draw the _shown nodes starting from index _menu_scroll up to either as + # many as fit in the window, or to the end of _shown + for i in range(_menu_scroll, + min(_menu_scroll + _height(_menu_win), len(_shown))): + + node = _shown[i] + + # The 'not _show_all' test avoids showing invisible items in red + # outside show-all mode, which could look confusing/broken. Invisible + # symbols show up outside show-all mode if an invisible symbol has + # visible children in an implicit (indented) menu. + if _visible(node) or not _show_all: + style = _style["selection" if i == _sel_node_i else "list"] + else: + style = _style["inv-selection" if i == _sel_node_i else "inv-list"] + + _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style) + + _menu_win.noutrefresh() + + # + # Update the bottom separator window + # + + _bot_sep_win.erase() + + # Draw arrows pointing down if the symbol window is scrolled up + if _menu_scroll < _max_scroll(_shown, _menu_win): + _safe_hline(_bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) + + # Indicate when show-name/show-help/show-all mode is enabled + enabled_modes = [] + if _show_help: + enabled_modes.append("show-help (toggle with [F])") + if _show_name: + enabled_modes.append("show-name") + if _show_all: + enabled_modes.append("show-all") + if enabled_modes: + s = " and ".join(enabled_modes) + " mode enabled" + _safe_addstr(_bot_sep_win, 0, max(term_width - len(s) - 2, 0), s) + + _bot_sep_win.noutrefresh() + + # + # Update the help window, which shows either key bindings or help texts + # + + _help_win.erase() + + if _show_help: + node = _shown[_sel_node_i] + if isinstance(node.item, (Symbol, Choice)) and node.help: + help_lines = textwrap.wrap(node.help, _width(_help_win)) + for i in range(min(_height(_help_win), len(help_lines))): + _safe_addstr(_help_win, i, 0, help_lines[i]) + else: + _safe_addstr(_help_win, 0, 0, "(no help)") + else: + for i, line in enumerate(_MAIN_HELP_LINES): + _safe_addstr(_help_win, i, 0, line) + + _help_win.noutrefresh() + + # + # Update the top row with the menu path. + # + # Doing this last leaves the cursor on the top row, which avoids some minor + # annoying jumpiness in gnome-terminal when reducing the height of the + # terminal. It seems to happen whenever the row with the cursor on it + # disappears. + # + + _path_win.erase() + + # Draw the menu path ("(Top) -> Menu -> Submenu -> ...") + + menu_prompts = [] + + menu = _cur_menu + while menu is not _kconf.top_node: + # Promptless choices can be entered in show-all mode. Use + # standard_sc_expr_str() for them, so they show up as + # '<choice (name if any)>'. + menu_prompts.append(menu.prompt[0] if menu.prompt else + standard_sc_expr_str(menu.item)) + menu = menu.parent + menu_prompts.append("(Top)") + menu_prompts.reverse() + + # Hack: We can't put ACS_RARROW directly in the string. Temporarily + # represent it with NULL. + menu_path_str = " \0 ".join(menu_prompts) + + # Scroll the menu path to the right if needed to make the current menu's + # title visible + if len(menu_path_str) > term_width: + menu_path_str = menu_path_str[len(menu_path_str) - term_width:] + + # Print the path with the arrows reinserted + split_path = menu_path_str.split("\0") + _safe_addstr(_path_win, split_path[0]) + for s in split_path[1:]: + _safe_addch(_path_win, curses.ACS_RARROW) + _safe_addstr(_path_win, s) + + _path_win.noutrefresh() + + +def _parent_menu(node): + # Returns the menu node of the menu that contains 'node'. In addition to + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. + # "Menu" here means a menu in the interface. + + menu = node.parent + while not menu.is_menuconfig: + menu = menu.parent + return menu + + +def _shown_nodes(menu): + # Returns the list of menu nodes from 'menu' (see _parent_menu()) that + # would be shown when entering it + + def rec(node): + res = [] + + while node: + if _visible(node) or _show_all: + res.append(node) + if node.list and not node.is_menuconfig: + # Nodes from implicit menu created from dependencies. Will + # be shown indented. Note that is_menuconfig is True for + # menus and choices as well as 'menuconfig' symbols. + res += rec(node.list) + + elif node.list and isinstance(node.item, Symbol): + # Show invisible symbols if they have visible children. This + # can happen for an m/y-valued symbol with an optional prompt + # ('prompt "foo" is COND') that is currently disabled. Note + # that it applies to both 'config' and 'menuconfig' symbols. + shown_children = rec(node.list) + if shown_children: + res.append(node) + if not node.is_menuconfig: + res += shown_children + + node = node.next + + return res + + if isinstance(menu.item, Choice): + # For named choices defined in multiple locations, entering the choice + # at a particular menu node would normally only show the choice symbols + # defined there (because that's what the MenuNode tree looks like). + # + # That might look confusing, and makes extending choices by defining + # them in multiple locations less useful. Instead, gather all the child + # menu nodes for all the choices whenever a choice is entered. That + # makes all choice symbols visible at all locations. + # + # Choices can contain non-symbol items (people do all sorts of weird + # stuff with them), hence the generality here. We really need to + # preserve the menu tree at each choice location. + # + # Note: Named choices are pretty broken in the C tools, and this is + # super obscure, so you probably won't find much that relies on this. + # This whole 'if' could be deleted if you don't care about defining + # choices in multiple locations to add symbols (which will still work, + # just with things being displayed in a way that might be unexpected). + + # Do some additional work to avoid listing choice symbols twice if all + # or part of the choice is copied in multiple locations (e.g. by + # including some Kconfig file multiple times). We give the prompts at + # the current location precedence. + seen_syms = {node.item for node in rec(menu.list) + if isinstance(node.item, Symbol)} + res = [] + for choice_node in menu.item.nodes: + for node in rec(choice_node.list): + # 'choice_node is menu' checks if we're dealing with the + # current location + if node.item not in seen_syms or choice_node is menu: + res.append(node) + if isinstance(node.item, Symbol): + seen_syms.add(node.item) + return res + + return rec(menu.list) + + +def _visible(node): + # Returns True if the node should appear in the menu (outside show-all + # mode) + + return node.prompt and expr_value(node.prompt[1]) and not \ + (node.item == MENU and not expr_value(node.visibility)) + + +def _change_node(node): + # Changes the value of the menu node 'node' if it is a symbol. Bools and + # tristates are toggled, while other symbol types pop up a text entry + # dialog. + # + # Returns False if the value of 'node' can't be changed. + + if not _changeable(node): + return False + + # sc = symbol/choice + sc = node.item + + if sc.orig_type in (INT, HEX, STRING): + s = sc.str_value + + while True: + s = _input_dialog( + "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.orig_type]), + s, _range_info(sc)) + + if s is None: + break + + if sc.orig_type in (INT, HEX): + s = s.strip() + + # 'make menuconfig' does this too. Hex values not starting with + # '0x' are accepted when loading .config files though. + if sc.orig_type == HEX and not s.startswith(("0x", "0X")): + s = "0x" + s + + if _check_valid(sc, s): + _set_val(sc, s) + break + + elif len(sc.assignable) == 1: + # Handles choice symbols for choices in y mode, which are a special + # case: .assignable can be (2,) while .tri_value is 0. + _set_val(sc, sc.assignable[0]) + + else: + # Set the symbol to the value after the current value in + # sc.assignable, with wrapping + val_index = sc.assignable.index(sc.tri_value) + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) + + + if _is_y_mode_choice_sym(sc) and not node.list: + # Immediately jump to the parent menu after making a choice selection, + # like 'make menuconfig' does, except if the menu node has children + # (which can happen if a symbol 'depends on' a choice symbol that + # immediately precedes it). + _leave_menu() + + + return True + + +def _changeable(node): + # Returns True if the value if 'node' can be changed + + sc = node.item + + if not isinstance(sc, (Symbol, Choice)): + return False + + # This will hit for invisible symbols, which appear in show-all mode and + # when an invisible symbol has visible children (which can happen e.g. for + # symbols with optional prompts) + if not (node.prompt and expr_value(node.prompt[1])): + return False + + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ + or _is_y_mode_choice_sym(sc) + + +def _set_sel_node_tri_val(tri_val): + # Sets the value of the currently selected menu entry to 'tri_val', if that + # value can be assigned + + sc = _shown[_sel_node_i].item + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: + _set_val(sc, tri_val) + + +def _set_val(sc, val): + # Wrapper around Symbol/Choice.set_value() for updating the menu state and + # _conf_changed + + global _conf_changed + + # Use the string representation of tristate values. This makes the format + # consistent for all symbol types. + if val in TRI_TO_STR: + val = TRI_TO_STR[val] + + if val != sc.str_value: + sc.set_value(val) + _conf_changed = True + + # Changing the value of the symbol might have changed what items in the + # current menu are visible. Recalculate the state. + _update_menu() + + +def _update_menu(): + # Updates the current menu after the value of a symbol or choice has been + # changed. Changing a value might change which items in the menu are + # visible. + # + # If possible, preserves the location of the cursor on the screen when + # items are added/removed above the selected item. + + global _shown + global _sel_node_i + global _menu_scroll + + # Row on the screen the cursor was on + old_row = _sel_node_i - _menu_scroll + + sel_node = _shown[_sel_node_i] + + # New visible nodes + _shown = _shown_nodes(_cur_menu) + + # New index of selected node + _sel_node_i = _shown.index(sel_node) + + # Try to make the cursor stay on the same row in the menu window. This + # might be impossible if too many nodes have disappeared above the node. + _menu_scroll = max(_sel_node_i - old_row, 0) + + +def _input_dialog(title, initial_text, info_text=None): + # Pops up a dialog that prompts the user for a string + # + # title: + # Title to display at the top of the dialog window's border + # + # initial_text: + # Initial text to prefill the input field with + # + # info_text: + # String to show next to the input field. If None, just the input field + # is shown. + + win = _styled_win("body") + win.keypad(True) + + info_lines = info_text.split("\n") if info_text else [] + + # Give the input dialog its initial size + _resize_input_dialog(win, title, info_lines) + + _safe_curs_set(2) + + # Input field text + s = initial_text + + # Cursor position + i = len(initial_text) + + def edit_width(): + return _width(win) - 4 + + # Horizontal scroll offset + hscroll = max(i - edit_width() + 1, 0) + + while True: + # Draw the "main" display with the menu, etc., so that resizing still + # works properly. This is like a stack of windows, only hardcoded for + # now. + _draw_main() + _draw_input_dialog(win, title, info_lines, s, i, hscroll) + curses.doupdate() + + + c = _getch_compat(win) + + if c == curses.KEY_RESIZE: + # Resize the main display too. The dialog floats above it. + _resize_main() + _resize_input_dialog(win, title, info_lines) + + elif c == "\n": + _safe_curs_set(0) + return s + + elif c == "\x1B": # \x1B = ESC + _safe_curs_set(0) + return None + + else: + s, i, hscroll = _edit_text(c, s, i, hscroll, edit_width()) + + +def _resize_input_dialog(win, title, info_lines): + # Resizes the input dialog to a size appropriate for the terminal size + + screen_height, screen_width = _stdscr.getmaxyx() + + win_height = 5 + if info_lines: + win_height += len(info_lines) + 1 + win_height = min(win_height, screen_height) + + win_width = max(_INPUT_DIALOG_MIN_WIDTH, + len(title) + 4, + *(len(line) + 4 for line in info_lines)) + win_width = min(win_width, screen_width) + + win.resize(win_height, win_width) + win.mvwin((screen_height - win_height)//2, + (screen_width - win_width)//2) + + +def _draw_input_dialog(win, title, info_lines, s, i, hscroll): + edit_width = _width(win) - 4 + + win.erase() + + # Note: Perhaps having a separate window for the input field would be nicer + visible_s = s[hscroll:hscroll + edit_width] + _safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)), + _style["edit"]) + + for linenr, line in enumerate(info_lines): + _safe_addstr(win, 4 + linenr, 2, line) + + # Draw the frame last so that it overwrites the body text for small windows + _draw_frame(win, title) + + _safe_move(win, 2, 2 + i - hscroll) + + win.noutrefresh() + + +def _load_dialog(): + # Dialog for loading a new configuration + + global _conf_changed + global _conf_filename + global _show_all + + if _conf_changed: + c = _key_dialog( + "Load", + "You have unsaved changes. Load new\n" + "configuration anyway?\n" + "\n" + " (O)K (C)ancel", + "oc") + + if c is None or c == "c": + return + + filename = _conf_filename + while True: + filename = _input_dialog("File to load", filename, _load_save_info()) + if filename is None: + return + + filename = os.path.expanduser(filename) + + if _try_load(filename): + _conf_filename = filename + _conf_changed = _needs_save() + + # Turn on show-all mode if the selected node is not visible after + # loading the new configuration. _shown still holds the old state. + if _shown[_sel_node_i] not in _shown_nodes(_cur_menu): + _show_all = True + + _update_menu() + + # The message dialog indirectly updates the menu display, so _msg() + # must be called after the new state has been initialized + _msg("Success", "Loaded " + filename) + return + + +def _try_load(filename): + # Tries to load a configuration file. Pops up an error and returns False on + # failure. + # + # filename: + # Configuration file to load + + try: + _kconf.load_config(filename) + return True + except EnvironmentError as e: + _error("Error loading '{}'\n\n{} (errno: {})" + .format(filename, e.strerror, errno.errorcode[e.errno])) + return False + + +def _save_dialog(save_fn, default_filename, description): + # Dialog for saving the current configuration + # + # save_fn: + # Function to call with 'filename' to save the file + # + # default_filename: + # Prefilled filename in the input field + # + # description: + # String describing the thing being saved + # + # Return value: + # The path to the saved file, or None if no file was saved + + filename = default_filename + while True: + filename = _input_dialog("Filename to save {} to".format(description), + filename, _load_save_info()) + if filename is None: + return None + + filename = os.path.expanduser(filename) + + msg = _try_save(save_fn, filename, description) + if msg: + _msg("Success", msg) + return filename + + +def _try_save(save_fn, filename, description): + # Tries to save a configuration file. Returns a message to print on + # success. + # + # save_fn: + # Function to call with 'filename' to save the file + # + # description: + # String describing the thing being saved + # + # Return value: + # A message to print on success, and None on failure + + try: + # save_fn() returns a message to print + return save_fn(filename) + except EnvironmentError as e: + _error("Error saving {} to '{}'\n\n{} (errno: {})" + .format(description, e.filename, e.strerror, + errno.errorcode[e.errno])) + return None + + +def _key_dialog(title, text, keys): + # Pops up a dialog that can be closed by pressing a key + # + # title: + # Title to display at the top of the dialog window's border + # + # text: + # Text to show in the dialog + # + # keys: + # List of keys that will close the dialog. Other keys (besides ESC) are + # ignored. The caller is responsible for providing a hint about which + # keys can be pressed in 'text'. + # + # Return value: + # The key that was pressed to close the dialog. Uppercase characters are + # converted to lowercase. ESC will always close the dialog, and returns + # None. + + win = _styled_win("body") + win.keypad(True) + + _resize_key_dialog(win, text) + + while True: + # See _input_dialog() + _draw_main() + _draw_key_dialog(win, title, text) + curses.doupdate() + + + c = _getch_compat(win) + + if c == curses.KEY_RESIZE: + # Resize the main display too. The dialog floats above it. + _resize_main() + _resize_key_dialog(win, text) + + elif c == "\x1B": # \x1B = ESC + return None + + elif isinstance(c, str): + c = c.lower() + if c in keys: + return c + + +def _resize_key_dialog(win, text): + # Resizes the key dialog to a size appropriate for the terminal size + + screen_height, screen_width = _stdscr.getmaxyx() + + lines = text.split("\n") + + win_height = min(len(lines) + 4, screen_height) + win_width = min(max(len(line) for line in lines) + 4, screen_width) + + win.resize(win_height, win_width) + win.mvwin((screen_height - win_height)//2, + (screen_width - win_width)//2) + + +def _draw_key_dialog(win, title, text): + win.erase() + + for i, line in enumerate(text.split("\n")): + _safe_addstr(win, 2 + i, 2, line) + + # Draw the frame last so that it overwrites the body text for small windows + _draw_frame(win, title) + + win.noutrefresh() + + +def _draw_frame(win, title): + # Draw a frame around the inner edges of 'win', with 'title' at the top + + win_height, win_width = win.getmaxyx() + + win.attron(_style["frame"]) + + # Draw top/bottom edge + _safe_hline(win, 0, 0, " ", win_width) + _safe_hline(win, win_height - 1, 0, " ", win_width) + + # Draw left/right edge + _safe_vline(win, 0, 0, " ", win_height) + _safe_vline(win, 0, win_width - 1, " ", win_height) + + # Draw title + _safe_addstr(win, 0, max((win_width - len(title))//2, 0), title) + + win.attroff(_style["frame"]) + + +def _jump_to_dialog(): + # Implements the jump-to dialog, where symbols can be looked up via + # incremental search and jumped to. + # + # Returns True if the user jumped to a symbol, and False if the dialog was + # canceled. + + s = "" # Search text + prev_s = None # Previous search text + s_i = 0 # Search text cursor position + hscroll = 0 # Horizontal scroll offset + + sel_node_i = 0 # Index of selected row + scroll = 0 # Index in 'matches' of the top row of the list + + # Edit box at the top + edit_box = _styled_win("jump-edit") + edit_box.keypad(True) + + # List of matches + matches_win = _styled_win("list") + + # Bottom separator, with arrows pointing down + bot_sep_win = _styled_win("separator") + + # Help window with instructions at the bottom + help_win = _styled_win("help") + + # Give windows their initial size + _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll) + + _safe_curs_set(2) + + # Logic duplication with _select_{next,prev}_menu_entry(), except we do a + # functional variant that returns the new (sel_node_i, scroll) values to + # avoid 'nonlocal'. TODO: Can this be factored out in some nice way? + + def select_next_match(): + if sel_node_i == len(matches) - 1: + return sel_node_i, scroll + + if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \ + and scroll < _max_scroll(matches, matches_win): + + return sel_node_i + 1, scroll + 1 + + return sel_node_i + 1, scroll + + def select_prev_match(): + if sel_node_i == 0: + return sel_node_i, scroll + + if sel_node_i - 1 < scroll + _SCROLL_OFFSET: + return sel_node_i - 1, max(scroll - 1, 0) + + return sel_node_i - 1, scroll + + while True: + if s != prev_s: + # The search text changed. Find new matching nodes. + + prev_s = s + + try: + # We could use re.IGNORECASE here instead of lower(), but this + # is noticeably less jerky while inputting regexes like + # '.*debug$' (though the '.*' is redundant there). Those + # probably have bad interactions with re.search(), which + # matches anywhere in the string. + # + # It's not horrible either way. Just a bit smoother. + regex_searches = [re.compile(regex).search + for regex in s.lower().split()] + + # No exception thrown, so the regexes are okay + bad_re = None + + # List of matching nodes + matches = [] + add_match = matches.append + + # Search symbols and choices + + for node in _sorted_sc_nodes(): + # Symbol/choice + sc = node.item + + for search in regex_searches: + # Both the name and the prompt might be missing, since + # we're searching both symbols and choices + + # Does the regex match either the symbol name or the + # prompt (if any)? + if not (sc.name and search(sc.name.lower()) or + node.prompt and search(node.prompt[0].lower())): + + # Give up on the first regex that doesn't match, to + # speed things up a bit when multiple regexes are + # entered + break + + else: + add_match(node) + + # Search menus and comments + + for node in _sorted_menu_comment_nodes(): + for search in regex_searches: + if not search(node.prompt[0].lower()): + break + else: + add_match(node) + + except re.error as e: + # Bad regex. Remember the error message so we can show it. + bad_re = "Bad regular expression" + # re.error.msg was added in Python 3.5 + if hasattr(e, "msg"): + bad_re += ": " + e.msg + + matches = [] + + # Reset scroll and jump to the top of the list of matches + sel_node_i = scroll = 0 + + _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + s, s_i, hscroll, + bad_re, matches, sel_node_i, scroll) + curses.doupdate() + + + c = _getch_compat(edit_box) + + if c == "\n": + if matches: + _jump_to(matches[sel_node_i]) + _safe_curs_set(0) + return True + + elif c == "\x1B": # \x1B = ESC + _safe_curs_set(0) + return False + + elif c == curses.KEY_RESIZE: + # We adjust the scroll so that the selected node stays visible in + # the list when the terminal is resized, hence the 'scroll' + # assignment + scroll = _resize_jump_to_dialog( + edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll) + + elif c == "\x06": # \x06 = Ctrl-F + if matches: + _safe_curs_set(0) + _info_dialog(matches[sel_node_i], True) + _safe_curs_set(2) + + scroll = _resize_jump_to_dialog( + edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll) + + elif c == curses.KEY_DOWN: + sel_node_i, scroll = select_next_match() + + elif c == curses.KEY_UP: + sel_node_i, scroll = select_prev_match() + + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D + # Keep it simple. This way we get sane behavior for small windows, + # etc., for free. + for _ in range(_PG_JUMP): + sel_node_i, scroll = select_next_match() + + # Page Up (no Ctrl-U, as it's already used by the edit box) + elif c == curses.KEY_PPAGE: + for _ in range(_PG_JUMP): + sel_node_i, scroll = select_prev_match() + + elif c == curses.KEY_END: + sel_node_i = len(matches) - 1 + scroll = _max_scroll(matches, matches_win) + + elif c == curses.KEY_HOME: + sel_node_i = scroll = 0 + + else: + s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, + _width(edit_box) - 2) + + +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing +# to the same list. This avoids a global. +def _sorted_sc_nodes(cached_nodes=[]): + # Returns a sorted list of symbol and choice nodes to search. The symbol + # nodes appear first, sorted by name, and then the choice nodes, sorted by + # prompt and (secondarily) name. + + if not cached_nodes: + # Add symbol nodes + for sym in sorted(_kconf.unique_defined_syms, + key=lambda sym: sym.name): + # += is in-place for lists + cached_nodes += sym.nodes + + # Add choice nodes + + choices = sorted(_kconf.unique_choices, + key=lambda choice: choice.name or "") + + cached_nodes += sorted( + [node for choice in choices for node in choice.nodes], + key=lambda node: node.prompt[0] if node.prompt else "") + + return cached_nodes + + +def _sorted_menu_comment_nodes(cached_nodes=[]): + # Returns a list of menu and comment nodes to search, sorted by prompt, + # with the menus first + + if not cached_nodes: + def prompt_text(mc): + return mc.prompt[0] + + cached_nodes += sorted(_kconf.menus, key=prompt_text) + cached_nodes += sorted(_kconf.comments, key=prompt_text) + + return cached_nodes + + +def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll): + # Resizes the jump-to dialog to fill the terminal. + # + # Returns the new scroll index. We adjust the scroll if needed so that the + # selected node stays visible. + + screen_height, screen_width = _stdscr.getmaxyx() + + bot_sep_win.resize(1, screen_width) + + help_win_height = len(_JUMP_TO_HELP_LINES) + matches_win_height = screen_height - help_win_height - 4 + + if matches_win_height >= 1: + edit_box.resize(3, screen_width) + matches_win.resize(matches_win_height, screen_width) + help_win.resize(help_win_height, screen_width) + + matches_win.mvwin(3, 0) + bot_sep_win.mvwin(3 + matches_win_height, 0) + help_win.mvwin(3 + matches_win_height + 1, 0) + else: + # Degenerate case. Give up on nice rendering and just prevent errors. + + matches_win_height = 1 + + edit_box.resize(screen_height, screen_width) + matches_win.resize(1, screen_width) + help_win.resize(1, screen_width) + + for win in matches_win, bot_sep_win, help_win: + win.mvwin(0, 0) + + # Adjust the scroll so that the selected row is still within the window, if + # needed + if sel_node_i - scroll >= matches_win_height: + return sel_node_i - matches_win_height + 1 + return scroll + + +def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + s, s_i, hscroll, + bad_re, matches, sel_node_i, scroll): + + edit_width = _width(edit_box) - 2 + + # + # Update list of matches + # + + matches_win.erase() + + if matches: + for i in range(scroll, + min(scroll + _height(matches_win), len(matches))): + + node = matches[i] + + if isinstance(node.item, (Symbol, Choice)): + node_str = _name_and_val_str(node.item) + if node.prompt: + node_str += ' "{}"'.format(node.prompt[0]) + elif node.item == MENU: + node_str = 'menu "{}"'.format(node.prompt[0]) + else: # node.item == COMMENT + node_str = 'comment "{}"'.format(node.prompt[0]) + + _safe_addstr(matches_win, i - scroll, 0, node_str, + _style["selection" if i == sel_node_i else "list"]) + + else: + # bad_re holds the error message from the re.error exception on errors + _safe_addstr(matches_win, 0, 0, bad_re or "No matches") + + matches_win.noutrefresh() + + # + # Update bottom separator line + # + + bot_sep_win.erase() + + # Draw arrows pointing down if the symbol list is scrolled up + if scroll < _max_scroll(matches, matches_win): + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) + + bot_sep_win.noutrefresh() + + # + # Update help window at bottom + # + + help_win.erase() + + for i, line in enumerate(_JUMP_TO_HELP_LINES): + _safe_addstr(help_win, i, 0, line) + + help_win.noutrefresh() + + # + # Update edit box. We do this last since it makes it handy to position the + # cursor. + # + + edit_box.erase() + + _draw_frame(edit_box, "Jump to symbol/choice/menu/comment") + + # Draw arrows pointing up if the symbol list is scrolled down + if scroll > 0: + # TODO: Bit ugly that _style["frame"] is repeated here + _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS, + _style["frame"]) + + visible_s = s[hscroll:hscroll + edit_width] + _safe_addstr(edit_box, 1, 1, visible_s) + + _safe_move(edit_box, 1, 1 + s_i - hscroll) + + edit_box.noutrefresh() + + +def _info_dialog(node, from_jump_to_dialog): + # Shows a fullscreen window with information about 'node'. + # + # If 'from_jump_to_dialog' is True, the information dialog was opened from + # within the jump-to-dialog. In this case, we make '/' from within the + # information dialog just return, to avoid a confusing recursive invocation + # of the jump-to-dialog. + + # Top row, with title and arrows point up + top_line_win = _styled_win("separator") + + # Text display + text_win = _styled_win("text") + text_win.keypad(True) + + # Bottom separator, with arrows pointing down + bot_sep_win = _styled_win("separator") + + # Help window with keys at the bottom + help_win = _styled_win("help") + + # Give windows their initial size + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) + + + # Get lines of help text + lines = _info_str(node).split("\n") + + # Index of first row in 'lines' to show + scroll = 0 + + while True: + _draw_info_dialog(node, lines, scroll, top_line_win, text_win, + bot_sep_win, help_win) + curses.doupdate() + + + c = _getch_compat(text_win) + + if c == curses.KEY_RESIZE: + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) + + elif c in (curses.KEY_DOWN, "j", "J"): + if scroll < _max_scroll(lines, text_win): + scroll += 1 + + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D + scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win)) + + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U + scroll = max(scroll - _PG_JUMP, 0) + + elif c in (curses.KEY_END, "G"): + scroll = _max_scroll(lines, text_win) + + elif c in (curses.KEY_HOME, "g"): + scroll = 0 + + elif c in (curses.KEY_UP, "k", "K"): + if scroll > 0: + scroll -= 1 + + elif c == "/": + # Support starting a search from within the information dialog + + if from_jump_to_dialog: + return # Avoid recursion + + if _jump_to_dialog(): + return # Jumped to a symbol. Cancel the information dialog. + + # Stay in the information dialog if the jump-to dialog was + # canceled. Resize it in case the terminal was resized while the + # fullscreen jump-to dialog was open. + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) + + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, + "\x1B", # \x1B = ESC + "q", "Q", "h", "H"): + + return + + +def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): + # Resizes the info dialog to fill the terminal + + screen_height, screen_width = _stdscr.getmaxyx() + + top_line_win.resize(1, screen_width) + bot_sep_win.resize(1, screen_width) + + help_win_height = len(_INFO_HELP_LINES) + text_win_height = screen_height - help_win_height - 2 + + if text_win_height >= 1: + text_win.resize(text_win_height, screen_width) + help_win.resize(help_win_height, screen_width) + + text_win.mvwin(1, 0) + bot_sep_win.mvwin(1 + text_win_height, 0) + help_win.mvwin(1 + text_win_height + 1, 0) + else: + # Degenerate case. Give up on nice rendering and just prevent errors. + + text_win.resize(1, screen_width) + help_win.resize(1, screen_width) + + for win in text_win, bot_sep_win, help_win: + win.mvwin(0, 0) + + +def _draw_info_dialog(node, lines, scroll, top_line_win, text_win, + bot_sep_win, help_win): + + text_win_height, text_win_width = text_win.getmaxyx() + + # Note: The top row is deliberately updated last. See _draw_main(). + + # + # Update text display + # + + text_win.erase() + + for i, line in enumerate(lines[scroll:scroll + text_win_height]): + _safe_addstr(text_win, i, 0, line) + + text_win.noutrefresh() + + # + # Update bottom separator line + # + + bot_sep_win.erase() + + # Draw arrows pointing down if the symbol window is scrolled up + if scroll < _max_scroll(lines, text_win): + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) + + bot_sep_win.noutrefresh() + + # + # Update help window at bottom + # + + help_win.erase() + + for i, line in enumerate(_INFO_HELP_LINES): + _safe_addstr(help_win, i, 0, line) + + help_win.noutrefresh() + + # + # Update top row + # + + top_line_win.erase() + + # Draw arrows pointing up if the information window is scrolled down. Draw + # them before drawing the title, so the title ends up on top for small + # windows. + if scroll > 0: + _safe_hline(top_line_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) + + title = ("Symbol" if isinstance(node.item, Symbol) else + "Choice" if isinstance(node.item, Choice) else + "Menu" if node.item == MENU else + "Comment") + " information" + _safe_addstr(top_line_win, 0, max((text_win_width - len(title))//2, 0), + title) + + top_line_win.noutrefresh() + + +def _info_str(node): + # Returns information about the menu node 'node' as a string. + # + # The helper functions are responsible for adding newlines. This allows + # them to return "" if they don't want to add any output. + + if isinstance(node.item, Symbol): + sym = node.item + + return ( + _name_info(sym) + + _prompt_info(sym) + + "Type: {}\n".format(TYPE_TO_STR[sym.type]) + + _value_info(sym) + + _help_info(sym) + + _direct_dep_info(sym) + + _defaults_info(sym) + + _select_imply_info(sym) + + _kconfig_def_info(sym) + ) + + if isinstance(node.item, Choice): + choice = node.item + + return ( + _name_info(choice) + + _prompt_info(choice) + + "Type: {}\n".format(TYPE_TO_STR[choice.type]) + + 'Mode: {}\n'.format(choice.str_value) + + _help_info(choice) + + _choice_syms_info(choice) + + _direct_dep_info(choice) + + _defaults_info(choice) + + _kconfig_def_info(choice) + ) + + return _kconfig_def_info(node) # node.item in (MENU, COMMENT) + + +def _name_info(sc): + # Returns a string with the name of the symbol/choice. Names are optional + # for choices. + + return "Name: {}\n".format(sc.name) if sc.name else "" + + +def _prompt_info(sc): + # Returns a string listing the prompts of 'sc' (Symbol or Choice) + + s = "" + + for node in sc.nodes: + if node.prompt: + s += "Prompt: {}\n".format(node.prompt[0]) + + return s + + +def _value_info(sym): + # Returns a string showing 'sym's value + + # Only put quotes around the value for string symbols + return "Value: {}\n".format( + '"{}"'.format(sym.str_value) + if sym.orig_type == STRING + else sym.str_value) + + +def _choice_syms_info(choice): + # Returns a string listing the choice symbols in 'choice'. Adds + # "(selected)" next to the selected one. + + s = "Choice symbols:\n" + + for sym in choice.syms: + s += " - " + sym.name + if sym is choice.selection: + s += " (selected)" + s += "\n" + + return s + "\n" + + +def _help_info(sc): + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). + # Symbols and choices defined in multiple locations can have multiple help + # texts. + + s = "\n" + + for node in sc.nodes: + if node.help is not None: + s += "Help:\n\n{}\n\n".format(_indent(node.help, 2)) + + return s + + +def _direct_dep_info(sc): + # Returns a string describing the direct dependencies of 'sc' (Symbol or + # Choice). The direct dependencies are the OR of the dependencies from each + # definition location. The dependencies at each definition location come + # from 'depends on' and dependencies inherited from parent items. + + return "" if sc.direct_dep is _kconf.y else \ + 'Direct dependencies (={}):\n{}\n' \ + .format(TRI_TO_STR[expr_value(sc.direct_dep)], + _split_expr_info(sc.direct_dep, 2)) + + +def _defaults_info(sc): + # Returns a string describing the defaults of 'sc' (Symbol or Choice) + + if not sc.defaults: + return "" + + s = "Default" + if len(sc.defaults) > 1: + s += "s" + s += ":\n" + + for val, cond in sc.orig_defaults: + s += " - " + if isinstance(sc, Symbol): + s += _expr_str(val) + + # Skip the tristate value hint if the expression is just a single + # symbol. _expr_str() already shows its value as a string. + # + # This also avoids showing the tristate value for string/int/hex + # defaults, which wouldn't make any sense. + if isinstance(val, tuple): + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) + else: + # Don't print the value next to the symbol name for choice + # defaults, as it looks a bit confusing + s += val.name + s += "\n" + + if cond is not _kconf.y: + s += " Condition (={}):\n{}" \ + .format(TRI_TO_STR[expr_value(cond)], + _split_expr_info(cond, 4)) + + return s + "\n" + + +def _split_expr_info(expr, indent): + # Returns a string with 'expr' split into its top-level && or || operands, + # with one operand per line, together with the operand's value. This is + # usually enough to get something readable for long expressions. A fancier + # recursive thingy would be possible too. + # + # indent: + # Number of leading spaces to add before the split expression. + + if len(split_expr(expr, AND)) > 1: + split_op = AND + op_str = "&&" + else: + split_op = OR + op_str = "||" + + s = "" + for i, term in enumerate(split_expr(expr, split_op)): + s += "{}{} {}".format(indent*" ", + " " if i == 0 else op_str, + _expr_str(term)) + + # Don't bother showing the value hint if the expression is just a + # single symbol. _expr_str() already shows its value. + if isinstance(term, tuple): + s += " (={})".format(TRI_TO_STR[expr_value(term)]) + + s += "\n" + + return s + + +def _select_imply_info(sym): + # Returns a string with information about which symbols 'select' or 'imply' + # 'sym'. The selecting/implying symbols are grouped according to which + # value they select/imply 'sym' to (n/m/y). + + def sis(expr, val, title): + # sis = selects/implies + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] + if not sis: + return "" + + res = title + for si in sis: + res += " - {}\n".format(split_expr(si, AND)[0].name) + return res + "\n" + + s = "" + + if sym.rev_dep is not _kconf.n: + s += sis(sym.rev_dep, 2, + "Symbols currently y-selecting this symbol:\n") + s += sis(sym.rev_dep, 1, + "Symbols currently m-selecting this symbol:\n") + s += sis(sym.rev_dep, 0, + "Symbols currently n-selecting this symbol (no effect):\n") + + if sym.weak_rev_dep is not _kconf.n: + s += sis(sym.weak_rev_dep, 2, + "Symbols currently y-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 1, + "Symbols currently m-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 0, + "Symbols currently n-implying this symbol (no effect):\n") + + return s + + +def _kconfig_def_info(item): + # Returns a string with the definition of 'item' in Kconfig syntax, + # together with the definition location(s) and their include and menu paths + + nodes = [item] if isinstance(item, MenuNode) else item.nodes + + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ + .format("s" if len(nodes) > 1 else "") + s += (len(s) - 1)*"=" + + for node in nodes: + s += "\n\n" \ + "At {}:{}\n" \ + "{}" \ + "Menu path: {}\n\n" \ + "{}" \ + .format(node.filename, node.linenr, + _include_path_info(node), + _menu_path_info(node), + _indent(node.custom_str(_name_and_val_str), 2)) + + return s + + +def _include_path_info(node): + if not node.include_path: + # In the top-level Kconfig file + return "" + + return "Included via {}\n".format( + " -> ".join("{}:{}".format(filename, linenr) + for filename, linenr in node.include_path)) + + +def _menu_path_info(node): + # Returns a string describing the menu path leading up to 'node' + + path = "" + + while node.parent is not _kconf.top_node: + node = node.parent + + # Promptless choices might appear among the parents. Use + # standard_sc_expr_str() for them, so that they show up as + # '<choice (name if any)>'. + path = " -> " + (node.prompt[0] if node.prompt else + standard_sc_expr_str(node.item)) + path + + return "(Top)" + path + + +def _indent(s, n): + # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not + # available in Python 2 (it's 3.3+). + + return "\n".join(n*" " + line for line in s.split("\n")) + + +def _name_and_val_str(sc): + # Custom symbol/choice printer that shows symbol values after symbols + + # Show the values of non-constant (non-quoted) symbols that don't look like + # numbers. Things like 123 are actually symbol references, and only work as + # expected due to undefined symbols getting their name as their value. + # Showing the symbol value for those isn't helpful though. + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): + if not sc.nodes: + # Undefined symbol reference + return "{}(undefined/n)".format(sc.name) + + return '{}(={})'.format(sc.name, sc.str_value) + + # For other items, use the standard format + return standard_sc_expr_str(sc) + + +def _expr_str(expr): + # Custom expression printer that shows symbol values + return expr_str(expr, _name_and_val_str) + + +def _styled_win(style): + # Returns a new curses window with style 'style' and space as the fill + # character. The initial dimensions are (1, 1), so the window needs to be + # sized and positioned separately. + + win = curses.newwin(1, 1) + _set_style(win, style) + return win + + +def _set_style(win, style): + # Changes the style of an existing window + + win.bkgdset(" ", _style[style]) + + +def _max_scroll(lst, win): + # Assuming 'lst' is a list of items to be displayed in 'win', + # returns the maximum number of steps 'win' can be scrolled down. + # We stop scrolling when the bottom item is visible. + + return max(0, len(lst) - _height(win)) + + +def _edit_text(c, s, i, hscroll, width): + # Implements text editing commands for edit boxes. Takes a character (which + # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns + # the new state after the character has been processed. + # + # c: + # Character from user + # + # s: + # Current contents of string + # + # i: + # Current cursor index in string + # + # hscroll: + # Index in s of the leftmost character in the edit box, for horizontal + # scrolling + # + # width: + # Width in characters of the edit box + # + # Return value: + # An (s, i, hscroll) tuple for the new state + + if c == curses.KEY_LEFT: + if i > 0: + i -= 1 + + elif c == curses.KEY_RIGHT: + if i < len(s): + i += 1 + + elif c in (curses.KEY_HOME, "\x01"): # \x01 = CTRL-A + i = 0 + + elif c in (curses.KEY_END, "\x05"): # \x05 = CTRL-E + i = len(s) + + elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR): + if i > 0: + s = s[:i-1] + s[i:] + i -= 1 + + elif c == curses.KEY_DC: + s = s[:i] + s[i+1:] + + elif c == "\x17": # \x17 = CTRL-W + # The \W removes characters like ',' one at a time + new_i = re.search(r"(?:\w*|\W)\s*$", s[:i]).start() + s = s[:new_i] + s[i:] + i = new_i + + elif c == "\x0B": # \x0B = CTRL-K + s = s[:i] + + elif c == "\x15": # \x15 = CTRL-U + s = s[i:] + i = 0 + + elif isinstance(c, str): + # Insert character + s = s[:i] + c + s[i:] + i += 1 + + # Adjust the horizontal scroll so that the cursor never touches the left or + # right edges of the edit box, except when it's at the beginning or the end + # of the string + if i < hscroll + _SCROLL_OFFSET: + hscroll = max(i - _SCROLL_OFFSET, 0) + elif i >= hscroll + width - _SCROLL_OFFSET: + max_scroll = max(len(s) - width + 1, 0) + hscroll = min(i - width + _SCROLL_OFFSET + 1, max_scroll) + + return s, i, hscroll + + +def _load_save_info(): + # Returns an information string for load/save dialog boxes + + return "(Relative to {})\n\nRefer to your home directory with ~" \ + .format(os.path.join(os.getcwd(), "")) + + +def _msg(title, text): + # Pops up a message dialog that can be dismissed with Space/Enter/ESC + + _key_dialog(title, text, " \n") + + +def _error(text): + # Pops up an error dialog that can be dismissed with Space/Enter/ESC + + _msg("Error", text) + + +def _node_str(node): + # Returns the complete menu entry text for a menu node. + # + # Example return value: "[*] Support for X" + + # Calculate the indent to print the item with by checking how many levels + # above it the closest 'menuconfig' item is (this includes menus and + # choices as well as menuconfig symbols) + indent = 0 + parent = node.parent + while not parent.is_menuconfig: + indent += _SUBMENU_INDENT + parent = parent.parent + + # This approach gives nice alignment for empty string symbols ("() Foo") + s = "{:{}}".format(_value_str(node), 3 + indent) + + if _should_show_name(node): + if isinstance(node.item, Symbol): + s += " <{}>".format(node.item.name) + else: + # For choices, use standard_sc_expr_str(). That way they show up as + # '<choice (name if any)>'. + s += " " + standard_sc_expr_str(node.item) + + if node.prompt: + if node.item == COMMENT: + s += " *** {} ***".format(node.prompt[0]) + else: + s += " " + node.prompt[0] + + if isinstance(node.item, Symbol): + sym = node.item + + # Print "(NEW)" next to symbols without a user value (from e.g. a + # .config), but skip it for choice symbols in choices in y mode, + # and for symbols of UNKNOWN type (which generate a warning though) + if sym.user_value is None and sym.orig_type and \ + not (sym.choice and sym.choice.tri_value == 2): + + s += " (NEW)" + + if isinstance(node.item, Choice) and node.item.tri_value == 2: + # Print the prompt of the selected symbol after the choice for + # choices in y mode + sym = node.item.selection + if sym: + for sym_node in sym.nodes: + # Use the prompt used at this choice location, in case the + # choice symbol is defined in multiple locations + if sym_node.parent is node and sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + else: + # If the symbol isn't defined at this choice location, then + # just use whatever prompt we can find for it + for sym_node in sym.nodes: + if sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + + # Print "--->" next to nodes that have menus that can potentially be + # entered. Print "----" if the menu is empty. We don't allow those to be + # entered. + if node.is_menuconfig: + s += " --->" if _shown_nodes(node) else " ----" + + return s + + +def _should_show_name(node): + # Returns True if 'node' is a symbol or choice whose name should shown (if + # any, as names are optional for choices) + + # The 'not node.prompt' case only hits in show-all mode, for promptless + # symbols and choices + return not node.prompt or \ + (_show_name and isinstance(node.item, (Symbol, Choice))) + + +def _value_str(node): + # Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu node + + item = node.item + + if item in (MENU, COMMENT): + return "" + + # Wouldn't normally happen, and generates a warning + if not item.orig_type: + return "" + + if item.orig_type in (STRING, INT, HEX): + return "({})".format(item.str_value) + + # BOOL or TRISTATE + + if _is_y_mode_choice_sym(item): + return "(X)" if item.choice.selection is item else "( )" + + tri_val_str = (" ", "M", "*")[item.tri_value] + + if len(item.assignable) <= 1: + # Pinned to a single value + return "" if isinstance(item, Choice) else "-{}-".format(tri_val_str) + + if item.type == BOOL: + return "[{}]".format(tri_val_str) + + # item.type == TRISTATE + if item.assignable == (1, 2): + return "{{{}}}".format(tri_val_str) # {M}/{*} + return "<{}>".format(tri_val_str) + + +def _is_y_mode_choice_sym(item): + # 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 + return isinstance(item, Symbol) and item.choice and item.visibility == 2 + + +def _check_valid(sym, s): + # Returns True if the string 's' is a well-formed value for 'sym'. + # Otherwise, displays an error and returns False. + + if sym.orig_type not in (INT, HEX): + return True # Anything goes for non-int/hex symbols + + base = 10 if sym.orig_type == INT else 16 + try: + int(s, base) + except ValueError: + _error("'{}' is a malformed {} value" + .format(s, TYPE_TO_STR[sym.orig_type])) + return False + + for low_sym, high_sym, cond in sym.ranges: + if expr_value(cond): + low_s = low_sym.str_value + high_s = high_sym.str_value + + if not int(low_s, base) <= int(s, base) <= int(high_s, base): + _error("{} is outside the range {}-{}" + .format(s, low_s, high_s)) + return False + + break + + return True + + +def _range_info(sym): + # Returns a string with information about the valid range for the symbol + # 'sym', or None if 'sym' doesn't have a range + + if sym.orig_type in (INT, HEX): + for low, high, cond in sym.ranges: + if expr_value(cond): + return "Range: {}-{}".format(low.str_value, high.str_value) + + return None + + +def _is_num(name): + # Heuristic to see if a symbol name looks like a number, for nicer output + # when printing expressions. Things like 16 are actually symbol names, only + # they get their name as their value when the symbol is undefined. + + try: + int(name) + except ValueError: + if not name.startswith(("0x", "0X")): + return False + + try: + int(name, 16) + except ValueError: + return False + + return True + + +def _getch_compat(win): + # Uses get_wch() if available (Python 3.3+) and getch() otherwise. + # + # Also falls back on getch() if get_wch() raises curses.error, to work + # around an issue when resizing the terminal on at least macOS Catalina. + # See https://github.com/ulfalizer/Kconfiglib/issues/84. + # + # Also handles a PDCurses resizing quirk. + + try: + c = win.get_wch() + except (AttributeError, curses.error): + c = win.getch() + if 0 <= c <= 255: + c = chr(c) + + # Decent resizing behavior on PDCurses requires calling resize_term(0, 0) + # after receiving KEY_RESIZE, while ncurses (usually) handles terminal + # resizing automatically in get(_w)ch() (see the end of the + # resizeterm(3NCURSES) man page). + # + # resize_term(0, 0) reliably fails and does nothing on ncurses, so this + # hack gives ncurses/PDCurses compatibility for resizing. I don't know + # whether it would cause trouble for other implementations. + if c == curses.KEY_RESIZE: + try: + curses.resize_term(0, 0) + except curses.error: + pass + + return c + + +def _warn(*args): + # Temporarily returns from curses to shell mode and prints a warning to + # stderr. The warning would get lost in curses mode. + curses.endwin() + print("menuconfig warning: ", end="", file=sys.stderr) + print(*args, file=sys.stderr) + curses.doupdate() + + +# Ignore exceptions from some functions that might fail, e.g. for small +# windows. They usually do reasonable things anyway. + + +def _safe_curs_set(visibility): + try: + curses.curs_set(visibility) + except curses.error: + pass + + +def _safe_addstr(win, *args): + # Clip the line to avoid wrapping to the next line, which looks glitchy. + # addchstr() would do it for us, but it's not available in the 'curses' + # module. + + attr = None + if isinstance(args[0], str): + y, x = win.getyx() + s = args[0] + if len(args) == 2: + attr = args[1] + else: + y, x, s = args[:3] + if len(args) == 4: + attr = args[3] + + maxlen = _width(win) - x + s = s.expandtabs() + + try: + # The 'curses' module uses wattr_set() internally if you pass 'attr', + # overwriting the background style, so setting 'attr' to 0 in the first + # case won't do the right thing + if attr is None: + win.addnstr(y, x, s, maxlen) + else: + win.addnstr(y, x, s, maxlen, attr) + except curses.error: + pass + + +def _safe_addch(win, *args): + try: + win.addch(*args) + except curses.error: + pass + + +def _safe_hline(win, *args): + try: + win.hline(*args) + except curses.error: + pass + + +def _safe_vline(win, *args): + try: + win.vline(*args) + except curses.error: + pass + + +def _safe_move(win, *args): + try: + win.move(*args) + except curses.error: + pass + + +def _change_c_lc_ctype_to_utf8(): + # See _CHANGE_C_LC_CTYPE_TO_UTF8 + + if _IS_WINDOWS: + # Windows rarely has issues here, and the PEP 538 implementation avoids + # changing the locale on it. None of the UTF-8 locales below were + # supported from some quick testing either. Play it safe. + return + + def try_set_locale(loc): + try: + locale.setlocale(locale.LC_CTYPE, loc) + return True + except locale.Error: + return False + + # Is LC_CTYPE set to the C locale? + if locale.setlocale(locale.LC_CTYPE) == "C": + # This list was taken from the PEP 538 implementation in the CPython + # code, in Python/pylifecycle.c + for loc in "C.UTF-8", "C.utf8", "UTF-8": + if try_set_locale(loc): + # LC_CTYPE successfully changed + return + + +if __name__ == "__main__": + _main() diff --git a/scripts/Kconfiglib/oldconfig.py b/scripts/Kconfiglib/oldconfig.py new file mode 100755 index 0000000..53434b2 --- /dev/null +++ b/scripts/Kconfiglib/oldconfig.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Implements oldconfig functionality. + + 1. Loads existing .config + 2. Prompts for the value of all modifiable symbols/choices that + aren't already set in the .config + 3. Writes an updated .config + +The default input/output filename is '.config'. A different filename can be +passed in the KCONFIG_CONFIG environment variable. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). + +Entering '?' displays the help text of the symbol/choice, if any. + +Unlike 'make oldconfig', this script doesn't print menu titles and comments, +but gives Kconfig definition locations. Printing menus and comments would be +pretty easy to add: Look at the parents of each item, and print all menu +prompts and comments unless they have already been printed (assuming you want +to skip "irrelevant" menus). +""" +from __future__ import print_function + +import sys + +from kconfiglib import Symbol, Choice, BOOL, TRISTATE, HEX, standard_kconfig + + +# Python 2/3 compatibility hack +if sys.version_info[0] < 3: + input = raw_input + + +def _main(): + # Earlier symbols in Kconfig files might depend on later symbols and become + # visible if their values change. This flag is set to True if the value of + # any symbol changes, in which case we rerun the oldconfig to check for new + # visible symbols. + global conf_changed + + kconf = standard_kconfig(__doc__) + print(kconf.load_config()) + + while True: + conf_changed = False + + for node in kconf.node_iter(): + oldconfig(node) + + if not conf_changed: + break + + print(kconf.write_config()) + + +def oldconfig(node): + """ + Prompts the user for a value if node.item is a visible symbol/choice with + no user value. + """ + # See main() + global conf_changed + + # Only symbols and choices can be configured + if not isinstance(node.item, (Symbol, Choice)): + return + + # Skip symbols and choices that aren't visible + if not node.item.visibility: + return + + # Skip symbols and choices that don't have a prompt (at this location) + if not node.prompt: + return + + if isinstance(node.item, Symbol): + sym = node.item + + # Skip symbols that already have a user value + if sym.user_value is not None: + return + + # Skip symbols that can only have a single value, due to selects + if len(sym.assignable) == 1: + return + + # Skip symbols in choices in y mode. We ask once for the entire choice + # instead. + if sym.choice and sym.choice.tri_value == 2: + return + + # Loop until the user enters a valid value or enters a blank string + # (for the default value) + while True: + val = input("{} ({}) [{}] ".format( + node.prompt[0], _name_and_loc_str(sym), + _default_value_str(sym))) + + if val == "?": + _print_help(node) + continue + + # Substitute a blank string with the default value the symbol + # would get + if not val: + val = sym.str_value + + # Automatically add a "0x" prefix for hex symbols, like the + # menuconfig interface does. This isn't done when loading .config + # files, hence why set_value() doesn't do it automatically. + if sym.type == HEX and not val.startswith(("0x", "0X")): + val = "0x" + val + + old_str_val = sym.str_value + + # Kconfiglib itself will print a warning here if the value + # is invalid, so we don't need to bother + if sym.set_value(val): + # Valid value input. We're done with this node. + + if sym.str_value != old_str_val: + conf_changed = True + + return + + else: + choice = node.item + + # Skip choices that already have a visible user selection... + if choice.user_selection and choice.user_selection.visibility == 2: + # ...unless there are new visible symbols in the choice. (We know + # they have y (2) visibility in that case, because m-visible + # symbols get demoted to n-visibility in y-mode choices, and the + # user-selected symbol had visibility y.) + for sym in choice.syms: + if sym is not choice.user_selection and sym.visibility and \ + sym.user_value is None: + # New visible symbols in the choice + break + else: + # No new visible symbols in the choice + return + + # Get a list of available selections. The mode of the choice limits + # the visibility of the choice value symbols, so this will indirectly + # skip choices in n and m mode. + options = [sym for sym in choice.syms if sym.visibility == 2] + + if not options: + # No y-visible choice value symbols + return + + # Loop until the user enters a valid selection or a blank string (for + # the default selection) + while True: + print("{} ({})".format(node.prompt[0], _name_and_loc_str(choice))) + + for i, sym in enumerate(options, 1): + print("{} {}. {} ({})".format( + ">" if sym is choice.selection else " ", + i, + # Assume people don't define choice symbols with multiple + # prompts. That generates a warning anyway. + sym.nodes[0].prompt[0], + sym.name)) + + sel_index = input("choice[1-{}]: ".format(len(options))) + + if sel_index == "?": + _print_help(node) + continue + + # Pick the default selection if the string is blank + if not sel_index: + choice.selection.set_value(2) + break + + try: + sel_index = int(sel_index) + except ValueError: + print("Bad index", file=sys.stderr) + continue + + if not 1 <= sel_index <= len(options): + print("Bad index", file=sys.stderr) + continue + + # Valid selection + + if options[sel_index - 1].tri_value != 2: + conf_changed = True + + options[sel_index - 1].set_value(2) + break + + # Give all of the non-selected visible choice symbols the user value n. + # This makes it so that the choice is no longer considered new once we + # do additional passes, if the reason that it was considered new was + # that it had new visible choice symbols. + # + # Only giving visible choice symbols the user value n means we will + # prompt for the choice again if later user selections make more new + # choice symbols visible, which is correct. + for sym in choice.syms: + if sym is not choice.user_selection and sym.visibility: + sym.set_value(0) + + +def _name_and_loc_str(sc): + # Helper for printing the name of the symbol/choice 'sc' along with the + # location(s) in the Kconfig files where it is defined. Unnamed choices + # return "choice" instead of the name. + + return "{}, defined at {}".format( + sc.name or "choice", + ", ".join("{}:{}".format(node.filename, node.linenr) + for node in sc.nodes)) + + +def _print_help(node): + print("\n" + (node.help or "No help text\n")) + + +def _default_value_str(sym): + # Returns the "m/M/y" string in e.g. + # + # TRISTATE_SYM prompt (TRISTATE_SYM, defined at Kconfig:9) [n/M/y]: + # + # For string/int/hex, returns the default value as-is. + + if sym.type in (BOOL, TRISTATE): + return "/".join(("NMY" if sym.tri_value == tri else "nmy")[tri] + for tri in sym.assignable) + + # string/int/hex + return sym.str_value + + +if __name__ == "__main__": + _main() diff --git a/scripts/Kconfiglib/olddefconfig.py b/scripts/Kconfiglib/olddefconfig.py new file mode 100755 index 0000000..2dadfb4 --- /dev/null +++ b/scripts/Kconfiglib/olddefconfig.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Updates an old .config file or creates a new one, by filling in default values +for all new symbols. This is the same as picking the default selection for all +symbols in oldconfig, or entering the menuconfig interface and immediately +saving. + +The default input/output filename is '.config'. A different filename can be +passed in the KCONFIG_CONFIG environment variable. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig(__doc__) + print(kconf.load_config()) + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/savedefconfig.py b/scripts/Kconfiglib/savedefconfig.py new file mode 100755 index 0000000..0f36bde --- /dev/null +++ b/scripts/Kconfiglib/savedefconfig.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Saves a minimal configuration file that only lists symbols that differ in value +from their defaults. Loading such a configuration file is equivalent to loading +the "full" configuration file. + +Minimal configuration files are handy to start from when editing configuration +files by hand. + +The default input configuration file is '.config'. A different input filename +can be passed in the KCONFIG_CONFIG environment variable. + +Note: Minimal configurations can also be generated from within the menuconfig +interface. +""" +import argparse + +import kconfiglib + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--kconfig", + default="Kconfig", + help="Top-level Kconfig file (default: Kconfig)") + + parser.add_argument( + "--out", + metavar="MINIMAL_CONFIGURATION", + default="defconfig", + help="Output filename for minimal configuration (default: defconfig)") + + args = parser.parse_args() + + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) + print(kconf.load_config()) + print(kconf.write_min_config(args.out)) + + +if __name__ == "__main__": + main() diff --git a/scripts/Kconfiglib/setconfig.py b/scripts/Kconfiglib/setconfig.py new file mode 100755 index 0000000..f9cf5cd --- /dev/null +++ b/scripts/Kconfiglib/setconfig.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Simple utility for setting configuration values from the command line. + +Sample usage: + + $ setconfig FOO_SUPPORT=y BAR_BITS=8 + +Note: Symbol names should not be prefixed with 'CONFIG_'. + +The exit status on errors is 1. + +The default input/output configuration file is '.config'. A different filename +can be passed in the KCONFIG_CONFIG environment variable. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). +""" +import argparse +import sys + +import kconfiglib + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--kconfig", + default="Kconfig", + help="Top-level Kconfig file (default: Kconfig)") + + parser.add_argument( + "--no-check-exists", + dest="check_exists", + action="store_false", + help="Ignore assignments to non-existent symbols instead of erroring " + "out") + + parser.add_argument( + "--no-check-value", + dest="check_value", + action="store_false", + help="Ignore assignments that didn't \"take\" (where the symbol got a " + "different value, e.g. due to unsatisfied dependencies) instead " + "of erroring out") + + parser.add_argument( + "assignments", + metavar="ASSIGNMENT", + nargs="*", + help="A 'NAME=value' assignment") + + args = parser.parse_args() + + kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True) + print(kconf.load_config()) + + for arg in args.assignments: + if "=" not in arg: + sys.exit("error: no '=' in assignment: '{}'".format(arg)) + name, value = arg.split("=", 1) + + if name not in kconf.syms: + if not args.check_exists: + continue + sys.exit("error: no symbol '{}' in configuration".format(name)) + + sym = kconf.syms[name] + + if not sym.set_value(value): + sys.exit("error: '{}' is an invalid value for the {} symbol {}" + .format(value, kconfiglib.TYPE_TO_STR[sym.orig_type], + name)) + + if args.check_value and sym.str_value != value: + sys.exit("error: {} was assigned the value '{}', but got the " + "value '{}'. Check the symbol's dependencies, and make " + "sure that it has a prompt." + .format(name, value, sym.str_value)) + + print(kconf.write_config()) + + +if __name__ == "__main__": + main()