kconfiglib/menuconfig/kconfig.py: Various improvements

Update Kconfiglib and menuconfig to upstream revision d3866958c7685, to
add various improvements:

 - Support HOME and END in the jump-to dialog in the menuconfig. END can
   be handy as choices, menus, and comments appear at the end.

 - Add more fine-grained warning controls for multiple assignments to a
   symbol in configuration files. Use it to simplify kconfig.py a bit.

   Clean up kconfig.py a bit in other ways too, e.g. by removing unused
   imports.

 - Improve Kconfig parsing performance slightly

Signed-off-by: Ulf Magnusson <Ulf.Magnusson@nordicsemi.no>
diff --git a/scripts/kconfig/kconfig.py b/scripts/kconfig/kconfig.py
index 9e9b6d7..0f8ca3d 100755
--- a/scripts/kconfig/kconfig.py
+++ b/scripts/kconfig/kconfig.py
@@ -5,7 +5,8 @@
 import sys
 import textwrap
 
-from kconfiglib import Kconfig, Symbol, BOOL, STRING, TRISTATE, TRI_TO_STR
+from kconfiglib import Kconfig, BOOL, TRISTATE, TRI_TO_STR
+
 
 # Warnings that won't be turned into errors (but that will still be printed),
 # identified by a substring of the warning. The warning texts from Kconfiglib
@@ -16,34 +17,25 @@
     "y-selected",
 )
 
+
 def fatal(warning):
     # Returns True if 'warning' is not whitelisted and should be turned into an
     # error
 
-    for wl_warning in WARNING_WHITELIST:
-        if wl_warning in warning:
-            return False
-
-    # Only allow enabled (printed) warnings to be fatal
-    return enabled(warning)
-
-
-def enabled(warning):
-    # Returns True if 'warning' should be printed
-
-    # Some prj.conf files seem to deliberately override settings from the board
-    # configuration (e.g. samples/bluetooth/hci_usb/prj.conf, with GPIO=y).
-    # Disable the warning about a symbol being assigned more than once.
-    return "set more than once" not in warning
+    return not any(wl_warning in warning for wl_warning in WARNING_WHITELIST)
 
 
 def main():
-    parse_args()
+    args = parse_args()
 
-    print("Parsing Kconfig tree in {}".format(args.kconfig_root))
+    print("Parsing Kconfig tree in " + args.kconfig_root)
     kconf = Kconfig(args.kconfig_root, warn_to_stderr=False)
 
-    # Enable warnings for assignments to undefined symbols
+    # prj.conf may override settings from the board configuration, so disable
+    # warnings about symbols being assigned more than once
+    kconf.disable_override_warnings()
+    kconf.disable_redun_warnings()
+    # Warn for assignments to undefined symbols
     kconf.enable_undef_warnings()
 
     for i, config in enumerate(args.conf_fragments):
@@ -73,11 +65,11 @@
     # fast.
     kconf.write_config(os.devnull)
 
-    # We could roll this into the loop below, but it's nice to always print all
-    # warnings, even if one of them turns out to be fatal
+    # Print warnings ourselves so that we can put a blank line between them for
+    # readability. We could roll this into the loop below, but it's nice to
+    # always print all warnings, even if one of them turns out to be fatal.
     for warning in kconf.warnings:
-        if enabled(warning):
-            print("\n" + warning, file=sys.stderr)
+        print("\n" + warning, file=sys.stderr)
 
     # Turn all warnings except for explicity whitelisted ones into errors. In
     # particular, this will turn assignments to undefined Kconfig variables
@@ -191,8 +183,6 @@
 
 
 def parse_args():
-    global args
-
     parser = argparse.ArgumentParser(
         description=__doc__,
         formatter_class=argparse.RawDescriptionHelpFormatter
@@ -203,7 +193,7 @@
     parser.add_argument("autoconf")
     parser.add_argument("conf_fragments", metavar='conf', type=str, nargs='+')
 
-    args = parser.parse_args()
+    return parser.parse_args()
 
 
 if __name__ == "__main__":
diff --git a/scripts/kconfig/kconfiglib.py b/scripts/kconfig/kconfiglib.py
index c7510b2..e8bf99e 100644
--- a/scripts/kconfig/kconfiglib.py
+++ b/scripts/kconfig/kconfiglib.py
@@ -152,7 +152,7 @@
 (1)
 
   menu "menu"
-  depends on A
+      depends on A
 
   if B
 
@@ -168,7 +168,7 @@
 (2)
 
   menu "menu"
-  depends on A
+      depends on A
 
   config FOO
       tristate "foo" if A && B && C && D
@@ -731,6 +731,7 @@
         "_set_match",
         "_unset_match",
         "_warn_for_no_prompt",
+        "_warn_for_override",
         "_warn_for_redun_assign",
         "_warn_for_undef_assign",
         "_warn_to_stderr",
@@ -852,7 +853,7 @@
         self._warn_to_stderr = warn_to_stderr
         self._warn_for_undef_assign = \
             os.environ.get("KCONFIG_WARN_UNDEF_ASSIGN") == "y"
-        self._warn_for_redun_assign = True
+        self._warn_for_redun_assign = self._warn_for_override = True
 
 
         self._encoding = encoding
@@ -1179,14 +1180,14 @@
                     else:
                         display_user_val = sym.user_value
 
-                    warn_msg = '{} set more than once. Old value: "{}", new value: "{}".'.format(
+                    msg = '{} set more than once. Old value: "{}", new value: "{}".'.format(
                         _name_and_loc(sym), display_user_val, val
                     )
 
                     if display_user_val == val:
-                        self._warn_redun_assign(warn_msg, filename, linenr)
+                        self._warn_redun_assign(msg, filename, linenr)
                     else:
-                        self._warn(             warn_msg, filename, linenr)
+                        self._warn_override(msg, filename, linenr)
 
                 sym.set_value(val)
 
@@ -1391,7 +1392,8 @@
         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."""
+        implementation can be used as a template.
+        """
         if not os.path.exists(path):
             os.mkdir(path, 0o755)
 
@@ -1590,7 +1592,7 @@
         self._tokens = self._tokenize("if " + s)[1:]
         self._tokens_i = -1
 
-        return expr_value(self._expect_expr_and_eol())  # transform_m
+        return expr_value(self._expect_expr_and_eol())
 
     def unset_values(self):
         """
@@ -1648,6 +1650,23 @@
         """
         self._warn_for_undef_assign = False
 
+    def enable_override_warnings(self):
+        """
+        Enables warnings for duplicated assignments in .config files that set
+        different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where
+        the last value set is used).
+
+        These warnings are enabled by default. Disabling them might be helpful
+        in certain cases when merging configurations.
+        """
+        self._warn_for_override = True
+
+    def disable_override_warnings(self):
+        """
+        See enable_override_warnings().
+        """
+        self._warn_for_override = False
+
     def enable_redun_warnings(self):
         """
         Enables warnings for duplicated assignments in .config files that all
@@ -1974,28 +1993,37 @@
                 c = s[i]
 
                 if c in "\"'":
-                    s, end_i = self._expand_str(s, i)
+                    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 = os.path.expandvars(
-                        s[i + 1:end_i - 1].replace("$UNAME_RELEASE",
-                                                   platform.uname()[2]))
+                        # 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 = os.path.expandvars(
+                            s[i + 1:end_i - 1].replace("$UNAME_RELEASE",
+                                                       platform.uname()[2]))
 
-                    i = end_i
+                        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)
+                    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
@@ -2830,6 +2858,7 @@
         # '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(_name_and_loc(node.item) +
                        " defined with multiple prompts in single location")
@@ -2903,7 +2932,7 @@
 
         self._linenr += len(help_lines)
 
-        node.help = "\n".join(help_lines).rstrip() + "\n"
+        node.help = "\n".join(help_lines).rstrip()
         self._line_after_help(line)
 
     def _parse_expr(self, transform_m):
@@ -3510,7 +3539,7 @@
             if self._warn_to_stderr:
                 sys.stderr.write(msg + "\n")
 
-    def _warn_undef_assign(self, msg, filename=None, linenr=None):
+    def _warn_undef_assign(self, msg, filename, linenr):
         # See the class documentation
 
         if self._warn_for_undef_assign:
@@ -3523,7 +3552,13 @@
             'attempt to assign the value "{}" to the undefined symbol {}'
             .format(val, name), filename, linenr)
 
-    def _warn_redun_assign(self, msg, filename=None, linenr=None):
+    def _warn_override(self, msg, filename, linenr):
+        # See the class documentation
+
+        if self._warn_for_override:
+            self._warn(msg, filename, linenr)
+
+    def _warn_redun_assign(self, msg, filename, linenr):
         # See the class documentation
 
         if self._warn_for_redun_assign:
@@ -3855,7 +3890,7 @@
                 has_active_range = False
 
             # Defaults are used if the symbol is invisible, lacks a user value,
-            # or has an out-of-range user value.
+            # or has an out-of-range user value
             use_defaults = True
 
             if vis and self.user_value:
@@ -4250,7 +4285,8 @@
         defined in multiple locations will return a string with all
         definitions.
 
-        An empty string is returned for undefined and constant symbols.
+        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)
 
@@ -4259,8 +4295,8 @@
         Works like Symbol.__str__(), but allows a custom format to be used for
         all symbol/choice references. See expr_str().
         """
-        return "\n".join(node.custom_str(sc_expr_str_fn)
-                         for node in self.nodes)
+        return "\n\n".join(node.custom_str(sc_expr_str_fn)
+                           for node in self.nodes)
 
     #
     # Private methods
@@ -4825,6 +4861,8 @@
         matching the Kconfig format (though without the contained choice
         symbols).
 
+        The returned string does not end in a newline.
+
         See Symbol.__str__() as well.
         """
         return self.custom_str(standard_sc_expr_str)
@@ -4834,8 +4872,8 @@
         Works like Choice.__str__(), but allows a custom format to be used for
         all symbol/choice references. See expr_str().
         """
-        return "\n".join(node.custom_str(sc_expr_str_fn)
-                         for node in self.nodes)
+        return "\n\n".join(node.custom_str(sc_expr_str_fn)
+                           for node in self.nodes)
 
     #
     # Private methods
@@ -5005,6 +5043,10 @@
       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 'depends on' dependencies for the menu node, or self.kconfig.y if
       there are no dependencies. Parent dependencies are propagated to this
@@ -5194,6 +5236,8 @@
         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)
 
@@ -5207,14 +5251,14 @@
                self._sym_choice_node_str(sc_expr_str_fn)
 
     def _menu_comment_node_str(self, sc_expr_str_fn):
-        s = '{} "{}"\n'.format("menu" if self.item is MENU else "comment",
-                               self.prompt[0])
+        s = '{} "{}"'.format("menu" if self.item is MENU else "comment",
+                             self.prompt[0])
 
         if self.dep is not self.kconfig.y:
-            s += "\tdepends on {}\n".format(expr_str(self.dep, sc_expr_str_fn))
+            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 += "\tvisible if {}\n".format(expr_str(self.visibility,
+            s += "\n\tvisible if {}".format(expr_str(self.visibility,
                                                      sc_expr_str_fn))
 
         return s
@@ -5288,7 +5332,7 @@
             for line in self.help.splitlines():
                 indent_add("  " + line)
 
-        return "\n".join(lines) + "\n"
+        return "\n".join(lines)
 
 class Variable(object):
     """
@@ -5600,6 +5644,61 @@
     """
     return os.environ.get("KCONFIG_CONFIG", ".config")
 
+def load_allconfig(kconf, 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 (disable_override_warnings() +
+    disable_redun_warnings()), and enables them 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.
+
+    kconf:
+      Kconfig instance to load the configuration in.
+
+    filename:
+      Command-specific configuration filename - "allyes.config",
+      "allno.config", etc.
+    """
+    def std_msg(e):
+        # "Upcasts" a _KconfigIOError to an IOError, removing the custom
+        # __str__() message. The standard message is better here.
+        return IOError(e.errno, e.strerror, e.filename)
+
+    kconf.disable_override_warnings()
+    kconf.disable_redun_warnings()
+
+    allconfig = os.environ.get("KCONFIG_ALLCONFIG")
+    if allconfig is not None:
+        if allconfig in ("", "1"):
+            try:
+                kconf.load_config(filename, False)
+            except IOError as e1:
+                try:
+                    kconf.load_config("all.config", False)
+                except IOError 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:
+                kconf.load_config(allconfig, False)
+            except IOError as e:
+                sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which "
+                         "could not be opened: {}"
+                         .format(allconfig, std_msg(e)))
+
+    # API wart: It would be nice if there was a way to query and/or push/pop
+    # warning settings
+    kconf.enable_override_warnings()
+    kconf.enable_redun_warnings()
+
 #
 # Internal functions
 #
@@ -5987,7 +6086,7 @@
             if isinstance(item, Symbol) and item.choice:
                 msg += "the choice symbol "
 
-        msg += "{}, with definition...\n\n{}\n" \
+        msg += "{}, with definition...\n\n{}\n\n" \
                .format(_name_and_loc(item), item)
 
         # Small wart: Since we reuse the already calculated
diff --git a/scripts/kconfig/menuconfig.py b/scripts/kconfig/menuconfig.py
index 271cc1e..d34f7d8 100755
--- a/scripts/kconfig/menuconfig.py
+++ b/scripts/kconfig/menuconfig.py
@@ -1131,9 +1131,10 @@
         # (as determined by _SCROLL_OFFSET), increase the scroll by one. This
         # gives nice and non-jumpy behavior even when
         # _SCROLL_OFFSET >= _menu_win_height().
-        if _sel_node_i >= _menu_scroll + _menu_win_height() - _SCROLL_OFFSET:
-            _menu_scroll = min(_menu_scroll + 1,
-                               _max_scroll(_shown, _menu_win))
+        if _sel_node_i >= _menu_scroll + _menu_win_height() - _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
@@ -1385,13 +1386,12 @@
                 rec(node.list) if node.list and not node.is_menuconfig else []
 
             # Always show the node if it is the root of an implicit submenu
-            # with visible items, even when the node itself is invisible. This
+            # with visible items, even if the node itself is invisible. This
             # can happen e.g. if the symbol has an optional prompt
             # ('prompt "foo" if COND') that is currently invisible.
             if shown(node) or shown_children:
                 res.append(node)
-
-            res.extend(shown_children)
+                res += shown_children
 
             node = node.next
 
@@ -1869,8 +1869,10 @@
         if sel_node_i < len(matches) - 1:
             sel_node_i += 1
 
-            if sel_node_i >= scroll + matches_win.getmaxyx()[0] - _SCROLL_OFFSET:
-                scroll = min(scroll + 1, _max_scroll(matches, matches_win))
+            if sel_node_i >= scroll + matches_win.getmaxyx()[0] - _SCROLL_OFFSET \
+               and scroll < _max_scroll(matches, matches_win):
+
+                scroll += 1
 
     def select_prev_match():
         nonlocal sel_node_i
@@ -1991,16 +1993,24 @@
         elif c == curses.KEY_UP:
             select_prev_match()
 
-        elif c == curses.KEY_NPAGE:  # Page Down
+        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_match()
 
-        elif c == curses.KEY_PPAGE:  # Page Up
+        # Page Up (no Ctrl-U, as it's already used by the edit box)
+        elif c == curses.KEY_PPAGE:
             for _ in range(_PG_JUMP):
                 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,
                                          edit_box.getmaxyx()[1] - 2)
@@ -2040,8 +2050,8 @@
         def prompt_text(mc):
             return mc.prompt[0]
 
-        cached_nodes += sorted(_kconf.menus, key=prompt_text) + \
-                        sorted(_kconf.comments, key=prompt_text)
+        cached_nodes += sorted(_kconf.menus, key=prompt_text)
+        cached_nodes += sorted(_kconf.comments, key=prompt_text)
 
     return cached_nodes
 
@@ -2155,7 +2165,7 @@
 
     edit_box.erase()
 
-    _draw_frame(edit_box, "Jump to symbol")
+    _draw_frame(edit_box, "Jump to symbol/choice/menu/comment")
 
     # Draw arrows pointing up if the symbol list is scrolled down
     if scroll > 0: