瀏覽代碼

将scons --menuconfig/--pyconfig/--pyconfig-silent统一调用kconfiglib

LaterComer 1 年之前
父節點
當前提交
6311bcaf24

+ 1 - 1
.github/workflows/bsp_buildings.yml

@@ -354,7 +354,7 @@ jobs:
           chmod 777 install_ubuntu.sh
           chmod 777 install_ubuntu.sh
           ./install_ubuntu.sh
           ./install_ubuntu.sh
           git config --global http.postBuffer 524288000
           git config --global http.postBuffer 524288000
-          python -c "import tools.menuconfig; tools.menuconfig.touch_env()"
+          python -c "import tools.menukconfig; tools.menukconfig.touch_env()"
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
 
 

+ 1 - 1
.github/workflows/compile_bsp_with_drivers.yml

@@ -52,7 +52,7 @@ jobs:
           git config --global http.postBuffer 524288000
           git config --global http.postBuffer 524288000
           git remote -v
           git remote -v
           git fetch origin
           git fetch origin
-          python -c "import tools.menuconfig; tools.menuconfig.touch_env()"
+          python -c "import tools.menukconfig; tools.menukconfig.touch_env()"
 
 
       - name: Install Arm ToolChains
       - name: Install Arm ToolChains
         if: ${{ success() }}
         if: ${{ success() }}

+ 1 - 1
.github/workflows/manual_dist.yml

@@ -66,7 +66,7 @@ jobs:
           chmod 777 install_ubuntu.sh
           chmod 777 install_ubuntu.sh
           ./install_ubuntu.sh
           ./install_ubuntu.sh
           git config --global http.postBuffer 524288000
           git config --global http.postBuffer 524288000
-          python -c "import tools.menuconfig; tools.menuconfig.touch_env()"
+          python -c "import tools.menukconfig; tools.menukconfig.touch_env()"
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
 
 

+ 1 - 1
.github/workflows/manual_trigger_scons_STM32_all.yml

@@ -141,7 +141,7 @@ jobs:
           chmod 777 install_ubuntu.sh
           chmod 777 install_ubuntu.sh
           ./install_ubuntu.sh
           ./install_ubuntu.sh
           git config --global http.postBuffer 524288000
           git config --global http.postBuffer 524288000
-          python -c "import tools.menuconfig; tools.menuconfig.touch_env()"
+          python -c "import tools.menukconfig; tools.menukconfig.touch_env()"
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
 
 

+ 1 - 1
.github/workflows/manual_trigger_scons_except_STM32_all.yml

@@ -286,7 +286,7 @@ jobs:
           chmod 777 install_ubuntu.sh
           chmod 777 install_ubuntu.sh
           ./install_ubuntu.sh
           ./install_ubuntu.sh
           git config --global http.postBuffer 524288000
           git config --global http.postBuffer 524288000
-          python -c "import tools.menuconfig; tools.menuconfig.touch_env()"
+          python -c "import tools.menukconfig; tools.menukconfig.touch_env()"
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
 
 

+ 1 - 1
.github/workflows/manual_trigger_scons_fail_bsp_all.yml

@@ -116,7 +116,7 @@ jobs:
           chmod 777 install_ubuntu.sh
           chmod 777 install_ubuntu.sh
           ./install_ubuntu.sh
           ./install_ubuntu.sh
           git config --global http.postBuffer 524288000
           git config --global http.postBuffer 524288000
-          python -c "import tools.menuconfig; tools.menuconfig.touch_env()"
+          python -c "import tools.menukconfig; tools.menukconfig.touch_env()"
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_ROOT=${{ github.workspace }}" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
           echo "RTT_CC=gcc" >> $GITHUB_ENV
 
 

+ 20 - 10
tools/building.py

@@ -139,10 +139,20 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
 
 
     # set RTT_ROOT in ENV
     # set RTT_ROOT in ENV
     Env['RTT_ROOT'] = Rtt_Root
     Env['RTT_ROOT'] = Rtt_Root
+    os.environ["RTT_DIR"] = Rtt_Root
     # set BSP_ROOT in ENV
     # set BSP_ROOT in ENV
     Env['BSP_ROOT'] = Dir('#').abspath
     Env['BSP_ROOT'] = Dir('#').abspath
+    os.environ["BSP_DIR"] = Dir('#').abspath
+    # set PKGS_ROOT in ENV
+    if not "PKGS_DIR" in os.environ:
+        if "ENV_ROOT" in os.environ:
+            os.environ["PKGS_DIR"] = os.path.join(os.environ["ENV_ROOT"], "packages")
+        elif sys.platform == "win32":
+            os.environ["PKGS_DIR"] = os.path.join(os.environ["USERPROFILE"], ".env/packages")
+        else:
+            os.environ["PKGS_DIR"] = os.path.join(os.environ["HOME"], ".env/packages")
 
 
-    sys.path = sys.path + [os.path.join(Rtt_Root, 'tools')]
+    sys.path = sys.path + [os.path.join(Rtt_Root, 'tools'), os.path.join(Rtt_Root, 'tools/kconfiglib')]
 
 
     # {target_name:(CROSS_TOOL, PLATFORM)}
     # {target_name:(CROSS_TOOL, PLATFORM)}
     tgt_dict = {'mdk':('keil', 'armcc'),
     tgt_dict = {'mdk':('keil', 'armcc'),
@@ -303,7 +313,7 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
             print('--global-macros arguments are illegal!')
             print('--global-macros arguments are illegal!')
 
 
     if GetOption('genconfig'):
     if GetOption('genconfig'):
-        from genconf import genconfig
+        from menukconfig import genconfig
         genconfig()
         genconfig()
         exit(0)
         exit(0)
 
 
@@ -311,25 +321,25 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
         from WCS import ThreadStackStaticAnalysis
         from WCS import ThreadStackStaticAnalysis
         ThreadStackStaticAnalysis(Env)
         ThreadStackStaticAnalysis(Env)
         exit(0)
         exit(0)
-    if platform.system() != 'Windows':
-        if GetOption('menuconfig'):
-            from menuconfig import menuconfig
-            menuconfig(Rtt_Root)
-            exit(0)
+
+    if GetOption('menuconfig'):
+        from menukconfig import menuconfig
+        menuconfig(Rtt_Root)
+        exit(0)
 
 
     if GetOption('pyconfig-silent'):
     if GetOption('pyconfig-silent'):
-        from menuconfig import guiconfig_silent
+        from menukconfig import guiconfig_silent
         guiconfig_silent(Rtt_Root)
         guiconfig_silent(Rtt_Root)
         exit(0)
         exit(0)
 
 
     elif GetOption('pyconfig'):
     elif GetOption('pyconfig'):
-        from menuconfig import guiconfig
+        from menukconfig import guiconfig
         guiconfig(Rtt_Root)
         guiconfig(Rtt_Root)
         exit(0)
         exit(0)
 
 
     configfn = GetOption('useconfig')
     configfn = GetOption('useconfig')
     if configfn:
     if configfn:
-        from menuconfig import mk_rtconfig
+        from menukconfig import mk_rtconfig
         mk_rtconfig(configfn)
         mk_rtconfig(configfn)
         exit(0)
         exit(0)
 
 

+ 0 - 32
tools/genconf.py

@@ -1,32 +0,0 @@
-import os
-
-def genconfig() :
-    from SCons.Script import SCons
-
-    PreProcessor = SCons.cpp.PreProcessor()
-
-    try:
-        f = open('rtconfig.h', 'r')
-        contents = f.read()
-        f.close()
-    except :
-        print("Open rtconfig.h file failed.")
-
-    PreProcessor.process_contents(contents)
-    options = PreProcessor.cpp_namespace
-
-    try:
-        f = open('.config', 'w')
-        for (opt, value) in options.items():
-            if type(value) == type(1):
-                f.write("CONFIG_%s=%d\n" % (opt, value))
-
-            if type(value) == type('') and value == '':
-                f.write("CONFIG_%s=y\n" % opt)
-            elif type(value) == type('str'):
-                f.write("CONFIG_%s=%s\n" % (opt, value))
-
-        print("Generate .config done!")
-        f.close()
-    except:
-        print("Generate .config file failed.")

+ 4 - 0
tools/kconfiglib/.gitignore

@@ -0,0 +1,4 @@
+*.py[co]
+build/
+*.egg-info/
+dist/

+ 5 - 0
tools/kconfiglib/LICENSE.txt

@@ -0,0 +1,5 @@
+Copyright (c) 2011-2019, Ulf Magnusson <ulfalizer@gmail.com>
+
+Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

+ 2 - 0
tools/kconfiglib/MANIFEST.in

@@ -0,0 +1,2 @@
+# Include the license file in source distributions
+include LICENSE.txt

+ 841 - 0
tools/kconfiglib/README.rst

@@ -0,0 +1,841 @@
+.. contents:: Table of contents
+   :backlinks: none
+
+News
+----
+
+Dependency loop with recent linux-next kernels
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+To fix issues with dependency loops on recent linux-next kernels, apply `this
+patch <https://www.spinics.net/lists/linux-kbuild/msg23455.html>`_. Hopefully,
+it will be in ``linux-next`` soon.
+
+``windows-curses`` is no longer automatically installed on Windows
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Starting with Kconfiglib 13.0.0, the `windows-curses
+<https://github.com/zephyrproject-rtos/windows-curses>`__ package is no longer
+automatically installed on Windows, and needs to be installed manually for the
+terminal ``menuconfig`` to work.
+
+This fixes installation of Kconfiglib on MSYS2, which is not compatible with
+``windows-curses``. See `this issue
+<https://github.com/ulfalizer/Kconfiglib/issues/77>`__.
+
+The ``menuconfig`` now shows a hint re. installing ``windows-curses`` when the
+``curses`` module can't be imported on Windows.
+
+Sorry if this change caused problems!
+
+Overview
+--------
+
+Kconfiglib is a `Kconfig
+<https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-language.rst>`__
+implementation in Python 2/3. It started out as a helper library, but now has a
+enough functionality to also work well as a standalone Kconfig implementation
+(including `terminal and GUI menuconfig interfaces <Menuconfig interfaces_>`_
+and `Kconfig extensions`_).
+
+The entire library is contained in `kconfiglib.py
+<https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_. The
+bundled scripts are implemented on top of it. Implementing your own scripts
+should be relatively easy, if needed.
+
+Kconfiglib is used exclusively by e.g. the `Zephyr
+<https://www.zephyrproject.org/>`__, `esp-idf
+<https://github.com/espressif/esp-idf>`__, and `ACRN
+<https://projectacrn.org/>`__ projects. It is also used for many small helper
+scripts in various projects.
+
+Since Kconfiglib is based around a library, it can be used e.g. to generate a
+`Kconfig cross-reference
+<https://docs.zephyrproject.org/latest/reference/kconfig/index.html>`_, using
+the same robust Kconfig parser used for other Kconfig tools, instead of brittle
+ad-hoc parsing. The documentation generation script can be found `here
+<https://github.com/zephyrproject-rtos/zephyr/blob/master/doc/scripts/genrest.py>`__.
+
+Kconfiglib implements the recently added `Kconfig preprocessor
+<https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-macro-language.rst>`__.
+For backwards compatibility, environment variables can be referenced both as
+``$(FOO)`` (the new syntax) and as ``$FOO`` (the old syntax). The old syntax is
+deprecated, but will probably be supported for a long time, as it's needed to
+stay compatible with older Linux kernels. The major version will be increased
+if support is ever dropped. Using the old syntax with an undefined environment
+variable keeps the string as is.
+
+Note: See `this issue <https://github.com/ulfalizer/Kconfiglib/issues/47>`__ if
+you run into a "macro expanded to blank string" error with kernel 4.18+.
+
+See `this page
+<https://docs.zephyrproject.org/latest/guides/kconfig/tips.html>`__ for some
+Kconfig tips and best practices.
+
+Installation
+------------
+
+Installation with pip
+~~~~~~~~~~~~~~~~~~~~~
+
+Kconfiglib is available on `PyPI <https://pypi.python.org/pypi/kconfiglib/>`_ and can be
+installed with e.g.
+
+.. code::
+
+    $ pip(3) install kconfiglib
+
+Microsoft Windows is supported.
+
+The ``pip`` installation will give you both the base library and the following
+executables. All but two (``genconfig`` and ``setconfig``) mirror functionality
+available in the C tools.
+
+- `menuconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_
+
+- `guiconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/guiconfig.py>`_
+
+- `oldconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/oldconfig.py>`_
+
+- `olddefconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/olddefconfig.py>`_
+
+- `savedefconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/savedefconfig.py>`_
+
+- `defconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/defconfig.py>`_
+
+- `alldefconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/alldefconfig.py>`_
+
+- `allnoconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/allnoconfig.py>`_
+
+- `allmodconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/allmodconfig.py>`_
+
+- `allyesconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/allyesconfig.py>`_
+
+- `listnewconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/listnewconfig.py>`_
+
+- `genconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/genconfig.py>`_
+
+- `setconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/setconfig.py>`_
+
+``genconfig`` is intended to be run at build time. It generates a C header from
+the configuration and (optionally) information that can be used to rebuild only
+files that reference Kconfig symbols that have changed value.
+
+Starting with Kconfiglib version 12.2.0, all utilities are compatible with both
+Python 2 and Python 3. Previously, ``menuconfig.py`` only ran under Python 3
+(i.e., it's now more backwards compatible than before).
+
+**Note:** If you install Kconfiglib with ``pip``'s ``--user`` flag, make sure
+that your ``PATH`` includes the directory where the executables end up. You can
+list the installed files with ``pip(3) show -f kconfiglib``.
+
+All releases have a corresponding tag in the git repository, e.g. ``v14.1.0``
+(the latest version).
+
+`Semantic versioning <http://semver.org/>`_ is used. There's been ten small
+changes to the behavior of the API, a Windows packaging change, and a hashbang
+change to use ``python3``
+(`1 <https://github.com/ulfalizer/Kconfiglib/commit/e8b4ecb6ff6ccc1c7be0818314fbccda2ef2b2ee>`_,
+`2 <https://github.com/ulfalizer/Kconfiglib/commit/db633015a4d7b0ba1e882f665e191f350932b2af>`_,
+`3 <https://github.com/ulfalizer/Kconfiglib/commit/8983f7eb297dd614faf0beee3129559bc8ba338e>`_,
+`4 <https://github.com/ulfalizer/Kconfiglib/commit/cbf32e29a130d22bc734b7778e6304ac9df2a3e8>`_,
+`5 <https://github.com/ulfalizer/Kconfiglib/commit/eb6c21a9b33a2d6e2bed9882d4f930d0cab2f03b>`_,
+`6 <https://github.com/ulfalizer/Kconfiglib/commit/c19fc11355b13d75d97286402c7a933fb23d3b70>`_,
+`7 <https://github.com/ulfalizer/Kconfiglib/commit/7a428aa415606820a44291f475248b08e3952c4b>`_,
+`8 <https://github.com/ulfalizer/Kconfiglib/commit/f247ddf618ad29718e5efd3e69f8baf75d4d347b>`_,
+`9 <https://github.com/ulfalizer/Kconfiglib/commit/4fed39d9271ceb68be4157ab3f96a45b94f77dc0>`_,
+`10 <https://github.com/ulfalizer/Kconfiglib/commit/55bc8c380869ea663092212e8fe388ad7abae596>`_,
+`Windows packaging change <https://github.com/ulfalizer/Kconfiglib/commit/21b4c1e3b6e2867b9a0788d21a358f6b1f581d86>`_,
+`Python 3 hashbang change <https://github.com/ulfalizer/Kconfiglib/commit/9e0a8d29fa76adcb3f27bb2e20f16fefc2a8591e>`_),
+which is why the major version is at 14 rather than 2. I do major version bumps
+for all behavior changes, even tiny ones, and most of these were fixes for baby
+issues in the early days of the Kconfiglib 2 API.
+
+Manual installation
+~~~~~~~~~~~~~~~~~~~
+
+Just drop ``kconfiglib.py`` and the scripts you want somewhere. There are no
+third-party dependencies, but the terminal ``menuconfig`` won't work on Windows
+unless a package like `windows-curses
+<https://github.com/zephyrproject-rtos/windows-curses>`__ is installed.
+
+Installation for the Linux kernel
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+See the module docstring at the top of `kconfiglib.py <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_.
+
+Python version compatibility (2.7/3.2+)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Kconfiglib and all utilities run under both Python 2.7 and Python 3.2 and
+later. The code mostly uses basic Python features and has no third-party
+dependencies, so keeping it backwards-compatible is pretty low effort.
+
+The 3.2 requirement comes from ``argparse``. ``format()`` with unnumbered
+``{}`` is used as well.
+
+A recent Python 3 version is recommended if you have a choice, as it'll give
+you better Unicode handling.
+
+Getting started
+---------------
+
+1. `Install <Installation_>`_ the library and the utilities.
+
+2. Write `Kconfig
+   <https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-language.rst>`__
+   files that describe the available configuration options. See `this page
+   <https://docs.zephyrproject.org/latest/guides/kconfig/tips.html>`__ for some
+   general Kconfig advice.
+
+3. Generate an initial configuration with e.g. ``menuconfig``/``guiconfig`` or
+   ``alldefconfig``. The configuration is saved as ``.config`` by default.
+
+   For more advanced projects, the ``defconfig`` utility can be used to
+   generate the initial configuration from an existing configuration file.
+   Usually, this existing configuration file would be a minimal configuration
+   file, as generated by e.g. ``savedefconfig``.
+
+4. Run ``genconfig`` to generate a header file. By default, it is saved as
+   ``config.h``.
+
+   Normally, ``genconfig`` would be run automatically as part of the build.
+
+   Before writing a header file or other configuration output, 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.
+   
+   Adding new configuration output formats should be relatively straightforward.
+   See the implementation of ``write_config()`` in `kconfiglib.py
+   <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_.
+   The documentation for the ``Symbol.config_string`` property has some tips as
+   well.
+   
+5. To update an old ``.config`` file after the Kconfig files have changed (e.g.
+   to add new options), run ``oldconfig`` (prompts for values for new options)
+   or ``olddefconfig`` (gives new options their default value). Entering the
+   ``menuconfig`` or ``guiconfig`` interface and saving the configuration will
+   also update it (the configuration interfaces always prompt for saving
+   on exit if it would modify the contents of the ``.config`` file).
+
+   Due to Kconfig semantics, simply loading an old ``.config`` file performs an
+   implicit ``olddefconfig``, so building will normally not be affected by
+   having an outdated configuration.
+
+Whenever ``.config`` is overwritten, the previous version of the file is saved
+to ``.config.old`` (or, more generally, to ``$KCONFIG_CONFIG.old``).
+
+Using ``.config`` files as Make input
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+``.config`` files use Make syntax and can be included directly in Makefiles to
+read configuration values from there. This is why ``n``-valued
+``bool``/``tristate`` values are written out as ``# CONFIG_FOO is not set`` (a
+Make comment) in ``.config``, allowing them to be tested with ``ifdef`` in
+Make.
+
+If you make use of this, you might want to pass ``--config-out <filename>`` to
+``genconfig`` and include the configuration file it generates instead of
+including ``.config`` directly. This has the advantage that the generated
+configuration file will always be a "full" configuration file, even if
+``.config`` is outdated. Otherwise, it might be necessary to run
+``old(def)config`` or ``menuconfig``/``guiconfig`` before rebuilding with an
+outdated ``.config``.
+
+If you use ``--sync-deps`` to generate incremental build information, you can
+include ``deps/auto.conf`` instead, which is also a full configuration file.
+
+Useful helper macros
+~~~~~~~~~~~~~~~~~~~~
+
+The `include/linux/kconfig.h
+<https://github.com/torvalds/linux/blob/master/include/linux/kconfig.h>`_
+header in the Linux kernel defines some useful helper macros for testing
+Kconfig configuration values.
+
+``IS_ENABLED()`` is generally useful, allowing configuration values to be
+tested in ``if`` statements with no runtime overhead.
+
+Incremental building
+~~~~~~~~~~~~~~~~~~~~
+
+See the docstring for ``Kconfig.sync_deps()`` in `kconfiglib.py
+<https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_ for hints
+on implementing incremental builds (rebuilding just source files that reference
+changed configuration values).
+
+Running the ``scripts/basic/fixdep.c`` tool from the kernel on the output of
+``gcc -MD <source file>`` might give you an idea of how it all fits together.
+
+Library documentation
+---------------------
+
+Kconfiglib comes with extensive documentation in the form of docstrings. To view it, run e.g.
+the following command:
+
+.. code:: sh
+
+    $ pydoc(3) kconfiglib
+
+For HTML output, add ``-w``:
+
+.. code:: sh
+
+    $ pydoc(3) -w kconfiglib
+
+This will also work after installing Kconfiglib with ``pip(3)``.
+
+Documentation for other modules can be viewed in the same way (though a plain
+``--help`` will work when they're run as executables):
+
+.. code:: sh
+
+    $ pydoc(3) menuconfig/guiconfig/...
+
+A good starting point for learning the library is to read the module docstring
+(which you could also just read directly at the beginning of `kconfiglib.py
+<https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_). It
+gives an introduction to symbol values, the menu tree, and expressions.
+
+After reading the module docstring, a good next step is to read the ``Kconfig``
+class documentation, and then the documentation for the ``Symbol``, ``Choice``,
+and ``MenuNode`` classes.
+
+Please tell me if something is unclear or can be explained better.
+
+Library features
+----------------
+
+Kconfiglib can do the following, among other things:
+
+- **Programmatically get and set symbol values**
+
+  See `allnoconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/allnoconfig.py>`_ and
+  `allyesconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/allyesconfig.py>`_,
+  which are automatically verified to produce identical output to the standard
+  ``make allnoconfig`` and ``make allyesconfig``.
+
+- **Read and write .config and defconfig files**
+
+  The generated ``.config`` and ``defconfig`` (minimal configuration) files are
+  character-for-character identical to what the C implementation would generate
+  (except for the header comment). The test suite relies on this, as it
+  compares the generated files.
+  
+- **Write C headers**
+
+  The generated headers use the same format as ``include/generated/autoconf.h``
+  from the Linux kernel. Output for symbols appears in the order that they're
+  defined, unlike in the C tools (where the order depends on the hash table
+  implementation).
+
+- **Implement incremental builds**
+
+  This uses the same scheme as the ``include/config`` directory in the kernel:
+  Symbols are translated into files that are touched when the symbol's value
+  changes between builds, which can be used to avoid having to do a full
+  rebuild whenever the configuration is changed.
+
+  See the ``sync_deps()`` function for more information.
+
+- **Inspect symbols**
+
+  Printing a symbol or other item (which calls ``__str__()``) returns its
+  definition in Kconfig format. This also works for symbols defined in multiple
+  locations.
+
+  A helpful ``__repr__()`` is  on all objects too.
+
+  All ``__str__()`` and ``__repr__()`` methods are deliberately implemented
+  with just public APIs, so all symbol information can be fetched separately as
+  well.
+
+- **Inspect expressions**
+
+  Expressions use a simple tuple-based format that can be processed manually
+  if needed. Expression printing and evaluation functions are provided,
+  implemented with public APIs.
+
+- **Inspect the menu tree**
+
+  The underlying menu tree is exposed, including submenus created implicitly
+  from symbols depending on preceding symbols. This can be used e.g. to
+  implement menuconfig-like functionality.
+  
+  See `menuconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_/`guiconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/guiconfig.py>`_ and the
+  minimalistic `menuconfig_example.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/menuconfig_example.py>`_
+  example.
+
+Kconfig extensions
+~~~~~~~~~~~~~~~~~~
+
+The following Kconfig extensions are available:
+
+- ``source`` supports glob patterns and includes each matching file. A pattern
+  is required to match at least one file.
+
+  A separate ``osource`` statement is available for cases where it's okay for
+  the pattern to match no files (in which case ``osource`` turns into a no-op).
+  
+- A relative ``source`` statement (``rsource``) is available, where file paths
+  are specified relative to the directory of the current Kconfig file. An
+  ``orsource`` statement is available as well, analogous to ``osource``.
+
+- Preprocessor user 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).
+
+  See the *Kconfig extensions* section in the
+  `kconfiglib.py <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_
+  module docstring for more information.
+
+- ``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.
+
+  These can be useful in projects that make use of symbols defined in multiple
+  locations, and remove some Kconfig inconsistency.
+  
+- Environment variables are expanded directly in e.g. ``source`` and
+  ``mainmenu`` statements, meaning ``option env`` symbols are redundant.
+
+  This is the standard behavior with the new `Kconfig preprocessor
+  <https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-macro-language.rst>`__,
+  which Kconfiglib implements.
+
+  ``option env`` symbols are accepted but ignored, which leads the caveat that
+  they must have the same name as the environment variables they reference
+  (Kconfiglib warns if the names differ). This keeps Kconfiglib compatible with
+  older Linux kernels, where the name of the ``option env`` symbol always
+  matched the environment variable. Compatibility with older Linux kernels is
+  the main reason ``option env`` is still supported.
+
+  The C tools have dropped support for ``option env``.
+
+- Two extra optional warnings can be enabled by setting environment variables,
+  covering cases that are easily missed when making changes to Kconfig files:
+
+  * ``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 by setting
+    ``Kconfig.warn_assign_undef`` to ``True``/``False``.
+
+Other features
+--------------
+
+- **Single-file implementation**
+  
+  The entire library is contained in `kconfiglib.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_.
+
+  The tools implemented on top of it are one file each.
+
+- **Robust and highly compatible with the C Kconfig tools**
+  
+  The `test suite <https://github.com/ulfalizer/Kconfiglib/blob/master/testsuite.py>`_
+  automatically compares output from Kconfiglib and the C tools
+  by diffing the generated ``.config`` files for the real kernel Kconfig and
+  defconfig files, for all ARCHes.
+  
+  This currently involves comparing the output for 36 ARCHes and 498 defconfig
+  files (or over 18000 ARCH/defconfig combinations in "obsessive" test suite
+  mode). All tests are expected to pass.
+
+  A comprehensive suite of selftests is included as well.
+
+- **Not horribly slow despite being a pure Python implementation**
+  
+  The `allyesconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/allyesconfig.py>`_
+  script currently runs in about 1.3 seconds on the Linux kernel on a Core i7
+  2600K (with a warm file cache), including the ``make`` overhead from ``make
+  scriptconfig``. Note that the Linux kernel Kconfigs are absolutely massive
+  (over 14k symbols for x86) compared to most projects, and also have overhead
+  from running shell commands via the Kconfig preprocessor.
+  
+  Kconfiglib is especially speedy in cases where multiple ``.config`` files
+  need to be processed, because the ``Kconfig`` files will only need to be parsed
+  once.
+
+  For long-running jobs, `PyPy <https://pypy.org/>`_ gives a big performance
+  boost. CPython is faster for short-running jobs as PyPy needs some time to
+  warm up.
+  
+  Kconfiglib also works well with the
+  `multiprocessing <https://docs.python.org/3/library/multiprocessing.html>`_
+  module. No global state is kept.
+
+- **Generates more warnings than the C implementation**
+
+  Generates the same warnings as the C implementation, plus additional ones.
+  Also detects dependency and ``source`` loops.
+
+  All warnings point out the location(s) in the ``Kconfig`` files where a
+  symbol is defined, where applicable.
+
+- **Unicode support**
+
+  Unicode characters in string literals in ``Kconfig`` and ``.config`` files are
+  correctly handled. This support mostly comes for free from Python.
+
+- **Windows support**
+
+  Nothing Linux-specific is used. Universal newlines mode is used for both
+  Python 2 and Python 3.
+  
+  The `Zephyr <https://www.zephyrproject.org/>`_ project uses Kconfiglib to
+  generate ``.config`` files and C headers on Linux as well as Windows.
+
+- **Internals that (mostly) mirror the C implementation**
+  
+  While being simpler to understand and tweak.
+
+Menuconfig interfaces
+---------------------
+
+Three configuration interfaces are currently available:
+
+- `menuconfig.py <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_
+  is a terminal-based configuration interface implemented using the standard
+  Python ``curses`` module. ``xconfig`` features like showing invisible symbols and
+  showing symbol names are included, and it's possible to jump directly to a symbol
+  in the menu tree (even if it's currently invisible).
+  
+  .. image:: https://raw.githubusercontent.com/ulfalizer/Kconfiglib/screenshots/screenshots/menuconfig.gif
+
+  *There is now also a show-help mode that shows the help text of the currently
+  selected symbol in the help window at the bottom.*
+
+  Starting with Kconfiglib 12.2.0, ``menuconfig.py`` runs under both Python 2
+  and Python 3 (previously, it only ran under Python 3, so this was a
+  backport). Running it under Python 3 provides better support for Unicode text
+  entry (``get_wch()`` is not available in the ``curses`` module on Python 2).
+
+  There are no third-party dependencies on \*nix. On Windows,
+  the ``curses`` modules is not available by default, but support
+  can be added by installing the ``windows-curses`` package:
+  
+  .. code-block:: shell
+
+      $ pip install windows-curses
+
+  This uses wheels built from `this repository
+  <https://github.com/zephyrproject-rtos/windows-curses>`_, which is in turn
+  based on Christoph Gohlke's `Python Extension Packages for Windows
+  <https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses>`_.
+
+  See the docstring at the top of `menuconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_ for
+  more information about the terminal menuconfig implementation.
+
+- `guiconfig.py
+  <https://github.com/ulfalizer/Kconfiglib/blob/master/guiconfig.py>`_ is a
+  graphical configuration interface written in `Tkinter
+  <https://docs.python.org/3/library/tkinter.html>`_. Like ``menuconfig.py``,
+  it supports showing all symbols (with invisible symbols in red) and jumping
+  directly to symbols. Symbol values can also be changed directly from the
+  jump-to dialog.
+
+  When single-menu mode is enabled, a single menu is shown at a time, like in
+  the terminal menuconfig. Only this mode distinguishes between symbols defined
+  with ``config`` and symbols defined with ``menuconfig``.
+
+  ``guiconfig.py`` has been tested on X11, Windows, and macOS, and is
+  compatible with both Python 2 and Python 3.
+
+  Despite being part of the Python standard library, ``tkinter`` often isn't
+  included by default in Python installations on Linux. These commands will
+  install it on a few different distributions:
+
+  - Ubuntu: ``sudo apt install python-tk``/``sudo apt install python3-tk``
+
+  - Fedora: ``dnf install python2-tkinter``/``dnf install python3-tkinter``
+
+  - Arch: ``sudo pacman -S tk``
+
+  - Clear Linux: ``sudo swupd bundle-add python3-tcl``
+
+  Screenshot below, with show-all mode enabled and the jump-to dialog open:
+
+  .. image:: https://raw.githubusercontent.com/ulfalizer/Kconfiglib/screenshots/screenshots/guiconfig.png
+
+  To avoid having to carry around a bunch of GIFs, the image data is embedded
+  in ``guiconfig.py``. To use separate GIF files instead, change
+  ``_USE_EMBEDDED_IMAGES`` to ``False`` in ``guiconfig.py``. The image files
+  can be found in the `screenshots
+  <https://github.com/ulfalizer/Kconfiglib/tree/screenshots/guiconfig>`_
+  branch.
+
+  I did my best with the images, but some are definitely only art adjacent.
+  Touch-ups are welcome. :)
+
+- `pymenuconfig <https://github.com/RomaVis/pymenuconfig>`_, built by `RomaVis
+  <https://github.com/RomaVis>`_, is an older portable Python 2/3 TkInter
+  menuconfig implementation.
+
+  Screenshot below:
+
+  .. image:: https://raw.githubusercontent.com/RomaVis/pymenuconfig/master/screenshot.PNG
+
+  While working on the terminal menuconfig implementation, I added a few APIs
+  to Kconfiglib that turned out to be handy. ``pymenuconfig`` predates
+  ``menuconfig.py`` and ``guiconfig.py``, and so didn't have them available.
+  Blame me for any workarounds.
+
+Examples
+--------
+
+Example scripts
+~~~~~~~~~~~~~~~
+
+The `examples/ <https://github.com/ulfalizer/Kconfiglib/blob/master/examples>`_ directory contains some simple example scripts. Among these are the following ones. Make sure you run them with the latest version of Kconfiglib, as they might make use of newly added features.
+
+- `eval_expr.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/eval_expr.py>`_ evaluates an expression in the context of a configuration.
+
+- `find_symbol.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/find_symbol.py>`_ searches through expressions to find references to a symbol, also printing a "backtrace" with parents for each reference found.
+
+- `help_grep.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/help_grep.py>`_ searches for a string in all help texts.
+
+- `print_tree.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/print_tree.py>`_ prints a tree of all configuration items.
+
+- `print_config_tree.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/print_config_tree.py>`_ is similar to ``print_tree.py``, but dumps the tree as it would appear in ``menuconfig``, including values. This can be handy for visually diffing between ``.config`` files and different versions of ``Kconfig`` files.
+
+- `list_undefined.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/list_undefined.py>`_ finds references to symbols that are not defined by any architecture in the Linux kernel.
+
+- `merge_config.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/merge_config.py>`_ merges configuration fragments to produce a complete .config, similarly to ``scripts/kconfig/merge_config.sh`` from the kernel.
+
+- `menuconfig_example.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/menuconfig_example.py>`_ implements a configuration interface that uses notation similar to ``make menuconfig``. It's deliberately kept as simple as possible to demonstrate just the core concepts.
+
+Real-world examples
+~~~~~~~~~~~~~~~~~~~
+
+- `kconfig.py
+  <https://github.com/zephyrproject-rtos/zephyr/blob/master/scripts/kconfig/kconfig.py>`_
+  from the `Zephyr <https://www.zephyrproject.org/>`_ project handles
+  ``.config`` and header file generation, also doing configuration fragment
+  merging
+
+- `genrest.py
+  <https://github.com/zephyrproject-rtos/zephyr/blob/master/doc/scripts/genrest.py>`_
+  generates a Kconfig symbol cross-reference, which can be viewed `here
+  <http://docs.zephyrproject.org/reference/kconfig/index.html>`__
+
+- `CMake and IDE integration
+  <https://github.com/espressif/esp-idf/tree/master/tools/kconfig_new>`_ from
+  the ESP-IDF project, via a configuration server program.
+
+- `A script for turning on USB-related options
+  <https://github.com/google/syzkaller/blob/master/dashboard/config/kconfiglib-merge-usb-configs.py>`_,
+  from the `syzkaller <https://github.com/google/syzkaller>`_ project.
+
+- `Various automated checks
+  <https://github.com/zephyrproject-rtos/ci-tools/blob/master/scripts/check_compliance.py>`_,
+  including a check for references to undefined Kconfig symbols in source code.
+  See the ``KconfigCheck`` class.
+
+- `Various utilities
+  <https://github.com/projectacrn/acrn-hypervisor/tree/master/scripts/kconfig>`_
+  from the `ACRN <https://projectacrn.org/>`_ project
+
+These use the older Kconfiglib 1 API, which was clunkier and not as general
+(functions instead of properties, no direct access to the menu structure or
+properties, uglier ``__str__()`` output):
+
+- `genboardscfg.py <http://git.denx.de/?p=u-boot.git;a=blob;f=tools/genboardscfg.py;hb=HEAD>`_ from `Das U-Boot <http://www.denx.de/wiki/U-Boot>`_ generates some sort of legacy board database by pulling information from a newly added Kconfig-based configuration system (as far as I understand it :).
+
+- `gen-manual-lists.py <https://git.busybox.net/buildroot/tree/support/scripts/gen-manual-lists.py?id=5676a2deea896f38123b99781da0a612865adeb0>`_ generated listings for an appendix in the `Buildroot <https://buildroot.org>`_ manual. (The listing has since been removed.)
+
+- `gen_kconfig_doc.py <https://github.com/espressif/esp-idf/blob/master/docs/gen-kconfig-doc.py>`_ from the `esp-idf <https://github.com/espressif/esp-idf>`_ project generates documentation from Kconfig files.
+
+- `SConf <https://github.com/CoryXie/SConf>`_ builds an interactive configuration interface (like ``menuconfig``) on top of Kconfiglib, for use e.g. with `SCons <scons.org>`_.
+
+- `kconfig-diff.py <https://gist.github.com/dubiousjim/5638961>`_ -- a script by `dubiousjim <https://github.com/dubiousjim>`_ that compares kernel configurations.
+
+- Originally, Kconfiglib was used in chapter 4 of my `master's thesis <http://liu.diva-portal.org/smash/get/diva2:473038/FULLTEXT01.pdf>`_ to automatically generate a "minimal" kernel for a given system. Parts of it bother me a bit now, but that's how it goes with old work.
+
+Sample ``make iscriptconfig`` session
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The following log should give some idea of the functionality available in the API:
+
+.. code-block::
+
+    $ make iscriptconfig
+    A Kconfig instance 'kconf' for the architecture x86 has been created.
+    >>> kconf  # Calls Kconfig.__repr__()
+    <configuration with 13711 symbols, main menu prompt "Linux/x86 4.14.0-rc7 Kernel Configuration", srctree ".", config symbol prefix "CONFIG_", warnings enabled, undef. symbol assignment warnings disabled>
+    >>> kconf.mainmenu_text  # Expanded main menu text
+    'Linux/x86 4.14.0-rc7 Kernel Configuration'
+    >>> kconf.top_node  # The implicit top-level menu
+    <menu node for menu, prompt "Linux/x86 4.14.0-rc7 Kernel Configuration" (visibility y), deps y, 'visible if' deps y, has child, Kconfig:5>
+    >>> kconf.top_node.list  # First child menu node
+    <menu node for symbol SRCARCH, deps y, has next, Kconfig:7>
+    >>> print(kconf.top_node.list)  # Calls MenuNode.__str__()
+    config SRCARCH
+    	string
+    	option env="SRCARCH"
+    	default "x86"
+    >>> sym = kconf.top_node.list.next.item  # Item contained in next menu node
+    >>> print(sym)  # Calls Symbol.__str__()
+    config 64BIT
+    	bool "64-bit kernel" if ARCH = "x86"
+    	default ARCH != "i386"
+    	help
+    	  Say yes to build a 64-bit kernel - formerly known as x86_64
+    	  Say no to build a 32-bit kernel - formerly known as i386
+    >>> sym  # Calls Symbol.__repr__()
+    <symbol 64BIT, bool, "64-bit kernel", value y, visibility y, direct deps y, arch/x86/Kconfig:2>
+    >>> sym.assignable  # Currently assignable values (0, 1, 2 = n, m, y)
+    (0, 2)
+    >>> sym.set_value(0)  # Set it to n
+    True
+    >>> sym.tri_value  # Check the new value
+    0
+    >>> sym = kconf.syms["X86_MPPARSE"]  # Look up symbol by name
+    >>> print(sym)
+    config X86_MPPARSE
+    	bool "Enable MPS table" if (ACPI || SFI) && X86_LOCAL_APIC
+    	default y if X86_LOCAL_APIC
+    	help
+    	  For old smp systems that do not have proper acpi support. Newer systems
+    	  (esp with 64bit cpus) with acpi support, MADT and DSDT will override it
+    >>> default = sym.defaults[0]  # Fetch its first default
+    >>> sym = default[1]  # Fetch the default's condition (just a Symbol here)
+    >>> print(sym)
+    config X86_LOCAL_APIC
+    	bool
+    	default y
+    	select IRQ_DOMAIN_HIERARCHY
+    	select PCI_MSI_IRQ_DOMAIN if PCI_MSI
+    	depends on X86_64 || SMP || X86_32_NON_STANDARD || X86_UP_APIC || PCI_MSI
+    >>> sym.nodes  # Show the MenuNode(s) associated with it
+    [<menu node for symbol X86_LOCAL_APIC, deps n, has next, arch/x86/Kconfig:1015>]
+    >>> kconfiglib.expr_str(sym.defaults[0][1])  # Print the default's condition
+    'X86_64 || SMP || X86_32_NON_STANDARD || X86_UP_APIC || PCI_MSI'
+    >>> kconfiglib.expr_value(sym.defaults[0][1])  # Evaluate it (0 = n)
+    0
+    >>> kconf.syms["64BIT"].set_value(2)
+    True
+    >>> kconfiglib.expr_value(sym.defaults[0][1])  # Evaluate it again (2 = y)
+    2
+    >>> kconf.write_config("myconfig")  # Save a .config
+    >>> ^D
+    $ cat myconfig
+    # Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)
+    CONFIG_64BIT=y
+    CONFIG_X86_64=y
+    CONFIG_X86=y
+    CONFIG_INSTRUCTION_DECODER=y
+    CONFIG_OUTPUT_FORMAT="elf64-x86-64"
+    CONFIG_ARCH_DEFCONFIG="arch/x86/configs/x86_64_defconfig"
+    CONFIG_LOCKDEP_SUPPORT=y
+    CONFIG_STACKTRACE_SUPPORT=y
+    CONFIG_MMU=y
+    ...
+ 
+Test suite
+----------
+
+The test suite is run with
+
+.. code::
+
+    $ python(3) Kconfiglib/testsuite.py
+    
+`pypy <https://pypy.org/>`_ works too, and is much speedier for everything except ``allnoconfig.py``/``allnoconfig_simpler.py``/``allyesconfig.py``, where it doesn't have time to warm up since
+the scripts are run via ``make scriptconfig``.
+
+The test suite must be run from the top-level kernel directory. It requires that the
+Kconfiglib git repository has been cloned into it and that the makefile patch has been applied.
+
+To get rid of warnings generated for the kernel ``Kconfig`` files, add ``2>/dev/null`` to the command to
+discard ``stderr``.
+
+**NOTE: Forgetting to apply the Makefile patch will cause some tests that compare generated configurations to fail**
+
+**NOTE: The test suite overwrites .config in the kernel root, so make sure to back it up.**
+
+The test suite consists of a set of selftests and a set of compatibility tests that
+compare configurations generated by Kconfiglib with
+configurations generated by the C tools, for a number of cases. See
+`testsuite.py <https://github.com/ulfalizer/Kconfiglib/blob/master/testsuite.py>`_
+for the available options.
+
+The `tests/reltest <https://github.com/ulfalizer/Kconfiglib/blob/master/tests/reltest>`_ script runs the test suite
+and all the example scripts for both Python 2 and Python 3, verifying that everything works.
+
+Rarely, the output from the C tools is changed slightly (most recently due to a
+`change <https://www.spinics.net/lists/linux-kbuild/msg17074.html>`_ I added).
+If you get test suite failures, try running the test suite again against the
+`linux-next tree <https://www.kernel.org/doc/man-pages/linux-next.html>`_,
+which has all the latest changes. I will make it clear if any
+non-backwards-compatible changes appear.
+
+A lot of time is spent waiting around for ``make`` and the C utilities (which need to reparse all the
+Kconfig files for each defconfig test). Adding some multiprocessing to the test suite would make sense
+too.
+
+Notes
+-----
+
+* This is version 2 of Kconfiglib, which is not backwards-compatible with
+  Kconfiglib 1. A summary of changes between Kconfiglib 1 and Kconfiglib
+  2 can be found `here
+  <https://github.com/ulfalizer/Kconfiglib/blob/screenshots/kconfiglib-2-changes.txt>`__.
+
+* I sometimes see people add custom output formats, which is pretty
+  straightforward to do (see the implementations of ``write_autoconf()`` and
+  ``write_config()`` for a template, and also the documentation of the
+  ``Symbol.config_string`` property). If you come up with something you think
+  might be useful to other people, I'm happy to take it in upstream. Batteries
+  included and all that.
+
+* Kconfiglib assumes the modules symbol is ``MODULES``, which is backwards-compatible.
+  A warning is printed by default if ``option modules`` is set on some other symbol.
+  
+  Let me know if you need proper ``option modules`` support. It wouldn't be that
+  hard to add.
+
+Thanks
+------
+
+- To `RomaVis <https://github.com/RomaVis>`_, for making
+  `pymenuconfig <https://github.com/RomaVis/pymenuconfig>`_ and suggesting
+  the ``rsource`` keyword.
+
+- To `Mitja Horvat <https://github.com/pinkfluid>`_, for adding support
+  for user-defined styles to the terminal menuconfig.
+
+- To `Philip Craig <https://github.com/philipc>`_ for adding
+  support for the ``allnoconfig_y`` option and fixing an obscure issue
+  with ``comment``\s inside ``choice``\s (that didn't affect correctness but
+  made outputs differ). ``allnoconfig_y`` is used to force certain symbols
+  to ``y`` during ``make allnoconfig`` to improve coverage.
+
+License
+-------
+
+See `LICENSE.txt <https://github.com/ulfalizer/Kconfiglib/blob/master/LICENSE.txt>`_. SPDX license identifiers are used in the
+source code.

+ 27 - 0
tools/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()

+ 46 - 0
tools/kconfiglib/allmodconfig.py

@@ -0,0 +1,46 @@
+#!/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 'm'.
+
+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/allmodconfig.py
+"""
+import kconfiglib
+
+
+def main():
+    kconf = kconfiglib.standard_kconfig(__doc__)
+
+    # See allnoconfig.py
+    kconf.warn = False
+
+    for sym in kconf.unique_defined_syms:
+        if sym.orig_type == kconfiglib.BOOL:
+            # 'bool' choice symbols get their default value, as determined by
+            # e.g. 'default's on the choice
+            if not sym.choice:
+                # All other bool symbols get set to 'y', like for allyesconfig
+                sym.set_value(2)
+        elif sym.orig_type == kconfiglib.TRISTATE:
+            sym.set_value(1)
+
+    for choice in kconf.unique_choices:
+        choice.set_value(2 if choice.orig_type == kconfiglib.BOOL else 1)
+
+    kconf.warn = True
+
+    kconf.load_allconfig("allmod.config")
+
+    print(kconf.write_config())
+
+
+if __name__ == "__main__":
+    main()

+ 45 - 0
tools/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()

+ 56 - 0
tools/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()

+ 3 - 3
tools/defconfig.py → tools/kconfiglib/defconfig.py

@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
 
 
 # Copyright (c) 2019, Ulf Magnusson
 # Copyright (c) 2019, Ulf Magnusson
 # SPDX-License-Identifier: ISC
 # SPDX-License-Identifier: ISC
@@ -25,7 +25,7 @@ def main():
     parser.add_argument(
     parser.add_argument(
         "--kconfig",
         "--kconfig",
         default="Kconfig",
         default="Kconfig",
-        help="Base Kconfig file (default: Kconfig)")
+        help="Top-level Kconfig file (default: Kconfig)")
 
 
     parser.add_argument(
     parser.add_argument(
         "config",
         "config",
@@ -34,7 +34,7 @@ def main():
 
 
     args = parser.parse_args()
     args = parser.parse_args()
 
 
-    kconf = kconfiglib.Kconfig(args.kconfig)
+    kconf = kconfiglib.Kconfig(args.kconfig, suppress_traceback=True)
     print(kconf.load_config(args.config))
     print(kconf.load_config(args.config))
     print(kconf.write_config())
     print(kconf.write_config())
 
 

+ 154 - 0
tools/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()

文件差異過大導致無法顯示
+ 2 - 2
tools/kconfiglib/guiconfig.py


文件差異過大導致無法顯示
+ 283 - 171
tools/kconfiglib/kconfiglib.py


+ 76 - 0
tools/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()

+ 48 - 0
tools/kconfiglib/makefile.patch

@@ -0,0 +1,48 @@
+From 93daf46f309b0c8f86149ef58c4906387d054c22 Mon Sep 17 00:00:00 2001
+From: Ulf Magnusson <ulfalizer@gmail.com>
+Date: Tue, 9 Jun 2015 13:01:34 +0200
+Subject: [PATCH] Kconfiglib scripts/kconfig/Makefile patch
+
+---
+ scripts/kconfig/Makefile | 29 +++++++++++++++++++++++++++++
+ 1 file changed, 29 insertions(+)
+
+diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile
+index 3f327e21f60e..8b7dd1292005 100644
+--- a/scripts/kconfig/Makefile
++++ b/scripts/kconfig/Makefile
+@@ -27,2 +27,31 @@ gconfig: $(obj)/gconf
+ 
++PHONY += scriptconfig iscriptconfig kmenuconfig guiconfig dumpvarsconfig
++
++PYTHONCMD ?= python
++kpython := PYTHONPATH=$(srctree)/Kconfiglib:$$PYTHONPATH $(PYTHONCMD)
++
++ifneq ($(filter scriptconfig,$(MAKECMDGOALS)),)
++ifndef SCRIPT
++$(error Use "make scriptconfig SCRIPT=<path to script> [SCRIPT_ARG=<argument>]")
++endif
++endif
++
++scriptconfig:
++	$(Q)$(kpython) $(SCRIPT) $(Kconfig) $(if $(SCRIPT_ARG),"$(SCRIPT_ARG)")
++
++iscriptconfig:
++	$(Q)$(kpython) -i -c \
++	  "import kconfiglib; \
++	   kconf = kconfiglib.Kconfig('$(Kconfig)'); \
++	   print('A Kconfig instance \'kconf\' for the architecture $(ARCH) has been created.')"
++
++kmenuconfig:
++	$(Q)$(kpython) $(srctree)/Kconfiglib/menuconfig.py $(Kconfig)
++
++guiconfig:
++	$(Q)$(kpython) $(srctree)/Kconfiglib/guiconfig.py $(Kconfig)
++
++dumpvarsconfig:
++	$(Q)$(kpython) $(srctree)/Kconfiglib/examples/dumpvars.py $(Kconfig)
++
+ menuconfig: $(obj)/mconf
+-- 
+2.20.1
+

+ 3278 - 0
tools/kconfiglib/menuconfig.py

@@ -0,0 +1,3278 @@
+#!/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
+
+    # Make curses use the locale settings specified in the environment
+    locale.setlocale(locale.LC_ALL, "")
+
+    # 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()

+ 246 - 0
tools/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()

+ 28 - 0
tools/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()

+ 49 - 0
tools/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()

+ 92 - 0
tools/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()

+ 7 - 0
tools/kconfiglib/setup.cfg

@@ -0,0 +1,7 @@
+[bdist_wheel]
+# We support both Python 2 and Python 3
+universal = 1
+
+[metadata]
+# Include the license file in wheels
+license_file = LICENSE.txt

+ 96 - 0
tools/kconfiglib/setup.py

@@ -0,0 +1,96 @@
+import io
+import os
+
+import setuptools
+
+
+setuptools.setup(
+    name="kconfiglib",
+    # MAJOR.MINOR.PATCH, per http://semver.org
+    version="14.1.0",
+    description="A flexible Python Kconfig implementation",
+
+    # Make sure that README.rst decodes on Python 3 in environments that use
+    # the C locale (which implies ASCII), by explicitly giving the encoding.
+    #
+    # io.open() has the 'encoding' parameter on both Python 2 and 3. open()
+    # doesn't have it on Python 2. This lets us use the same code for both.
+    long_description=io.open(
+        os.path.join(os.path.dirname(__file__), "README.rst"),
+        encoding="utf-8"
+    ).read(),
+
+    url="https://github.com/ulfalizer/Kconfiglib",
+    author='Ulf "Ulfalizer" Magnusson',
+    author_email="ulfalizer@gmail.com",
+    keywords="kconfig, kbuild, menuconfig, configuration-management",
+    license="ISC",
+
+    py_modules=(
+        "kconfiglib",
+        "menuconfig",
+        "guiconfig",
+        "genconfig",
+        "oldconfig",
+        "olddefconfig",
+        "savedefconfig",
+        "defconfig",
+        "alldefconfig",
+        "allnoconfig",
+        "allmodconfig",
+        "allyesconfig",
+        "listnewconfig",
+        "setconfig",
+    ),
+
+    entry_points={
+        "console_scripts": (
+            "menuconfig = menuconfig:_main",
+            "guiconfig = guiconfig:_main",
+            "genconfig = genconfig:main",
+            "oldconfig = oldconfig:_main",
+            "olddefconfig = olddefconfig:main",
+            "savedefconfig = savedefconfig:main",
+            "defconfig = defconfig:main",
+            "alldefconfig = alldefconfig:main",
+            "allnoconfig = allnoconfig:main",
+            "allmodconfig = allmodconfig:main",
+            "allyesconfig = allyesconfig:main",
+            "listnewconfig = listnewconfig:main",
+            "setconfig = setconfig:main",
+        )
+    },
+
+    # Note: windows-curses is not automatically installed on Windows anymore,
+    # because it made Kconfiglib impossible to install on MSYS2 with pip
+
+    # Needs support for unnumbered {} in format() and argparse
+    python_requires=">=2.7,!=3.0.*,!=3.1.*",
+
+    project_urls={
+        "GitHub repository": "https://github.com/ulfalizer/Kconfiglib",
+        "Examples": "https://github.com/ulfalizer/Kconfiglib/tree/master/examples",
+    },
+
+    classifiers=[
+        "Development Status :: 5 - Production/Stable",
+        "Intended Audience :: Developers",
+        "Topic :: Software Development :: Build Tools",
+        "Topic :: System :: Operating System Kernels :: Linux",
+        "License :: OSI Approved :: ISC License (ISCL)",
+        "Operating System :: POSIX",
+        "Operating System :: Microsoft :: Windows",
+        "Programming Language :: Python :: 2",
+        "Programming Language :: Python :: 2.7",
+        "Programming Language :: Python :: 3",
+        "Programming Language :: Python :: 3.2",
+        "Programming Language :: Python :: 3.3",
+        "Programming Language :: Python :: 3.4",
+        "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
+        "Programming Language :: Python :: 3.7",
+        "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: Implementation :: CPython",
+        "Programming Language :: Python :: Implementation :: PyPy",
+    ]
+)

+ 3203 - 0
tools/kconfiglib/testsuite.py

@@ -0,0 +1,3203 @@
+# Copyright (c) 2011-2019, Ulf Magnusson
+# SPDX-License-Identifier: ISC
+
+# This is the Kconfiglib test suite. It runs selftests on Kconfigs provided by
+# us and tests compatibility with the C Kconfig implementation by comparing the
+# output of Kconfiglib with the output of the scripts/kconfig/*conf utilities
+# for different targets and defconfigs. It should be run from the top-level
+# kernel directory with
+#
+#   $ python Kconfiglib/testsuite.py
+#
+# Some additional options can be turned on by passing them as arguments. They
+# default to off.
+#
+#  - obsessive:
+#    By default, only valid arch/defconfig pairs are tested. In obsessive mode,
+#    every arch will be tested with every defconfig. Increases the testing time
+#    by an order of magnitude. Occasionally finds (usually obscure) bugs, and I
+#    make sure everything passes with it.
+#
+#  - obsessive-min-config:
+#    Like obsessive, for the minimal configuation (defconfig) tests.
+#
+#  - log:
+#    Log timestamped defconfig test failures to the file test_defconfig_fails.
+#    Handy in obsessive mode.
+#
+# For example, this commands runs the test suite in obsessive mode with logging
+# enabled:
+#
+#   $ python(3) Kconfiglib/testsuite.py obsessive log
+#
+# pypy works too, and runs most tests much faster than CPython.
+#
+# All tests should pass. Report regressions to ulfalizer a.t Google's email
+# service.
+
+import difflib
+import errno
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import textwrap
+
+from kconfiglib import Kconfig, Symbol, Choice, COMMENT, MENU, MenuNode, \
+                       BOOL, TRISTATE, HEX, \
+                       TRI_TO_STR, \
+                       escape, unescape, \
+                       expr_str, expr_items, split_expr, \
+                       _ordered_unique, \
+                       OR, AND, \
+                       KconfigError
+
+
+def shell(cmd):
+    with open(os.devnull, "w") as devnull:
+        subprocess.call(cmd, shell=True, stdout=devnull, stderr=devnull)
+
+
+all_passed = True
+
+
+def fail(msg=None):
+    global all_passed
+    all_passed = False
+    if msg is not None:
+        print("fail: " + msg)
+
+
+def verify(cond, msg):
+    if not cond:
+        fail(msg)
+
+
+def verify_equal(x, y):
+    if x != y:
+        fail("'{}' does not equal '{}'".format(x, y))
+
+
+# Prevent accidental loading of configuration files by removing
+# KCONFIG_ALLCONFIG from the environment
+os.environ.pop("KCONFIG_ALLCONFIG", None)
+
+obsessive = False
+obsessive_min_config = False
+log = False
+
+
+def run_tests():
+    global obsessive, log
+    for s in sys.argv[1:]:
+        if s == "obsessive":
+            obsessive = True
+            print("Obsessive mode enabled")
+        elif s == "obsessive-min-config":
+            obsessive_min_config = True
+            print("Obsessive minimal config mode enabled")
+        elif s == "log":
+            log = True
+            print("Log mode enabled")
+        else:
+            print("Unrecognized option '{}'".format(s))
+            return
+
+    run_selftests()
+    run_compatibility_tests()
+
+
+def run_selftests():
+    #
+    # Common helper functions. These all expect 'c' to hold the current
+    # configuration.
+    #
+
+    def verify_value(sym_name, val):
+        # Verifies that a symbol has a particular value.
+
+        if isinstance(val, int):
+            val = TRI_TO_STR[val]
+
+        sym = c.syms[sym_name]
+        verify(sym.str_value == val,
+               'expected {} to have the value "{}", had the value "{}"'
+               .format(sym_name, val, sym.str_value))
+
+    def assign_and_verify_value(sym_name, val, new_val):
+        # Assigns 'val' to a symbol and verifies that its value becomes
+        # 'new_val'. Assumes (and tests) that 'val' is valid for the
+        # symbol type.
+
+        if isinstance(new_val, int):
+            new_val = TRI_TO_STR[new_val]
+
+        sym = c.syms[sym_name]
+        old_val = sym.str_value
+        verify(sym.set_value(val),
+               "assigning '{}' to {} unexpectedly failed"
+               .format(val, sym_name))
+        verify(sym.str_value == new_val,
+               "expected {} to have the value '{}' after being assigned the "
+               "value '{}'. Instead, the value is '{}'. The old value was "
+               "'{}'."
+               .format(sym_name, new_val, val, sym.str_value, old_val))
+
+    def assign_and_verify(sym_name, user_val):
+        # Like assign_and_verify_value(), with the expected value being the
+        # value just set.
+
+        assign_and_verify_value(sym_name, user_val, user_val)
+
+    def assign_and_verify_user_value(sym_name, val, user_val, valid):
+        # Assigns a user value to the symbol and verifies the new user value.
+        # If valid is True, the user value is valid for the type, otherwise
+        # not. This is used to test the set_value() return value.
+
+        sym = c.syms[sym_name]
+        sym_old_user_val = sym.user_value
+
+        verify(sym.set_value(val) == valid,
+               "expected the user value '{}' to be {} for {}, was not"
+               .format(val, "valid" if valid else "invalid", sym_name))
+        verify(sym.user_value == user_val,
+               "the assigned user value '{}' wasn't reflected in user_value "
+               "on the symbol {}. Instead, the new user_value was '{}'. The "
+               "old user value was '{}'."
+               .format(user_val, sym_name, sym.user_value, sym_old_user_val))
+
+    #
+    # Selftests
+    #
+
+    print("Testing string literal lexing")
+
+    # Dummy empty configuration just to get a Kconfig object
+    c = Kconfig("Kconfiglib/tests/empty")
+
+    def verify_string_lex(s, expected):
+        # Verifies that a constant symbol with the name 'res' is produced from
+        # lexing 's'
+
+        res = c._tokenize("if " + s)[1].name
+        verify(res == expected,
+               "expected <{}> to produced the constant symbol <{}>, "
+               'produced <{}>'.format(s[1:-1], expected, res))
+
+    verify_string_lex(r""" "" """, "")
+    verify_string_lex(r""" '' """, "")
+
+    verify_string_lex(r""" "a" """, "a")
+    verify_string_lex(r""" 'a' """, "a")
+    verify_string_lex(r""" "ab" """, "ab")
+    verify_string_lex(r""" 'ab' """, "ab")
+    verify_string_lex(r""" "abc" """, "abc")
+    verify_string_lex(r""" 'abc' """, "abc")
+
+    verify_string_lex(r""" "'" """, "'")
+    verify_string_lex(r""" '"' """, '"')
+
+    verify_string_lex(r""" "\"" """, '"')
+    verify_string_lex(r""" '\'' """, "'")
+
+    verify_string_lex(r""" "\"\"" """, '""')
+    verify_string_lex(r""" '\'\'' """, "''")
+
+    verify_string_lex(r""" "\'" """, "'")
+    verify_string_lex(r""" '\"' """, '"')
+
+    verify_string_lex(r""" "\\" """, "\\")
+    verify_string_lex(r""" '\\' """, "\\")
+
+    verify_string_lex(r""" "\a\\'\b\c\"'d" """, 'a\\\'bc"\'d')
+    verify_string_lex(r""" '\a\\"\b\c\'"d' """, "a\\\"bc'\"d")
+
+    def verify_string_bad(s):
+        # Verifies that tokenizing 's' throws a KconfigError. Strips the first
+        # and last characters from 's' so we can use readable raw strings as
+        # input.
+
+        try:
+            c.eval_string(s)
+        except KconfigError:
+            pass
+        else:
+            fail("expected tokenization of {} to fail, didn't".format(s[1:-1]))
+
+    verify_string_bad(r""" " """)
+    verify_string_bad(r""" ' """)
+    verify_string_bad(r""" "' """)
+    verify_string_bad(r""" '" """)
+    verify_string_bad(r""" "\" """)
+    verify_string_bad(r""" '\' """)
+    verify_string_bad(r""" "foo """)
+    verify_string_bad(r""" 'foo """)
+
+
+    print("Testing escape() and unescape()")
+
+    def verify_escape_unescape(s, sesc):
+        # Verify that 's' escapes to 'sesc' and that 'sesc' unescapes to 's'
+        verify_equal(escape(s), sesc)
+        verify_equal(unescape(sesc), s)
+
+    verify_escape_unescape(r''          , r''              )
+    verify_escape_unescape(r'foo'       , r'foo'           )
+    verify_escape_unescape(r'"'         , r'\"'            )
+    verify_escape_unescape(r'""'        , r'\"\"'          )
+    verify_escape_unescape('\\'         , r'\\'            )
+    verify_escape_unescape(r'\\'        , r'\\\\'          )
+    verify_escape_unescape(r'\"'        , r'\\\"'          )
+    verify_escape_unescape(r'"ab\cd"ef"', r'\"ab\\cd\"ef\"')
+
+    # Backslashes before any character should be unescaped, not just before "
+    # and \
+    verify_equal(unescape(r"\afoo\b\c\\d\\\e\\\\f"), r"afoobc\d\e\\f")
+
+
+    print("Testing _ordered_unique()")
+
+    verify_equal(_ordered_unique([]), [])
+    verify_equal(_ordered_unique([1]), [1])
+    verify_equal(_ordered_unique([1, 2]), [1, 2])
+    verify_equal(_ordered_unique([1, 1]), [1])
+    verify_equal(_ordered_unique([1, 1, 2]), [1, 2])
+    verify_equal(_ordered_unique([1, 2, 1]), [1, 2])
+    verify_equal(_ordered_unique([1, 2, 2]), [1, 2])
+    verify_equal(_ordered_unique([1, 2, 3, 2, 1, 2, 3, 4, 3, 2, 1, 0]),
+                                 [1, 2, 3, 4, 0])
+
+
+    print("Testing expression evaluation")
+
+    c = Kconfig("Kconfiglib/tests/Keval", warn=False)
+
+    def verify_eval(expr, val):
+        res = c.eval_string(expr)
+        verify(res == val,
+               "'{}' evaluated to {}, expected {}".format(expr, res, val))
+
+    # No modules
+    verify_eval("n", 0)
+    verify_eval("m", 0)
+    verify_eval("y", 2)
+    verify_eval("'n'", 0)
+    verify_eval("'m'", 0)
+    verify_eval("'y'", 2)
+    verify_eval("M", 2)
+
+    # Modules
+    c.modules.set_value(2)
+    verify_eval("n", 0)
+    verify_eval("m", 1)
+    verify_eval("y", 2)
+    verify_eval("'n'", 0)
+    verify_eval("'m'", 1)
+    verify_eval("'y'", 2)
+    verify_eval("M", 1)
+    verify_eval("(Y || N) && (m && y)", 1)
+
+    # Non-bool/non-tristate symbols are always n in a tristate sense
+    verify_eval("Y_STRING", 0)
+    verify_eval("Y_STRING || m", 1)
+
+    # As are all constants besides y and m
+    verify_eval('"foo"', 0)
+    verify_eval('"foo" || "bar"', 0)
+    verify_eval('"foo" || m', 1)
+
+    # Test equality for symbols
+
+    verify_eval("N = N", 2)
+    verify_eval("N = n", 2)
+    verify_eval("N = 'n'", 2)
+    verify_eval("N != N", 0)
+    verify_eval("N != n", 0)
+    verify_eval("N != 'n'", 0)
+
+    verify_eval("M = M", 2)
+    verify_eval("M = m", 2)
+    verify_eval("M = 'm'", 2)
+    verify_eval("M != M", 0)
+    verify_eval("M != m", 0)
+    verify_eval("M != 'm'", 0)
+
+    verify_eval("Y = Y", 2)
+    verify_eval("Y = y", 2)
+    verify_eval("Y = 'y'", 2)
+    verify_eval("Y != Y", 0)
+    verify_eval("Y != y", 0)
+    verify_eval("Y != 'y'", 0)
+
+    verify_eval("N != M", 2)
+    verify_eval("N != Y", 2)
+    verify_eval("M != Y", 2)
+
+    verify_eval("Y_STRING = y", 2)
+    verify_eval("Y_STRING = 'y'", 2)
+    verify_eval('FOO_BAR_STRING = "foo bar"', 2)
+    verify_eval('FOO_BAR_STRING != "foo bar baz"', 2)
+    verify_eval('INT_37 = 37', 2)
+    verify_eval("INT_37 = '37'", 2)
+    verify_eval('HEX_0X37 = 0x37', 2)
+    verify_eval("HEX_0X37 = '0x37'", 2)
+
+    # These should also hold after 31847b67 (kconfig: allow use of relations
+    # other than (in)equality)
+    verify_eval("HEX_0X37 = '0x037'", 2)
+    verify_eval("HEX_0X37 = '0x0037'", 2)
+
+    # Constant symbol comparisons
+    verify_eval('"foo" != "bar"', 2)
+    verify_eval('"foo" = "bar"', 0)
+    verify_eval('"foo" = "foo"', 2)
+
+    # Undefined symbols get their name as their value
+    c.warn = False
+    verify_eval("'not_defined' = not_defined", 2)
+    verify_eval("not_defined_2 = not_defined_2", 2)
+    verify_eval("not_defined_1 != not_defined_2", 2)
+
+    # Test less than/greater than
+
+    # Basic evaluation
+    verify_eval("INT_37 < 38", 2)
+    verify_eval("38 < INT_37", 0)
+    verify_eval("INT_37 < '38'", 2)
+    verify_eval("'38' < INT_37", 0)
+    verify_eval("INT_37 < 138", 2)
+    verify_eval("138 < INT_37", 0)
+    verify_eval("INT_37 < '138'", 2)
+    verify_eval("'138' < INT_37", 0)
+    verify_eval("INT_37 < -138", 0)
+    verify_eval("-138 < INT_37", 2)
+    verify_eval("INT_37 < '-138'", 0)
+    verify_eval("'-138' < INT_37", 2)
+    verify_eval("INT_37 < 37", 0)
+    verify_eval("37 < INT_37", 0)
+    verify_eval("INT_37 < 36", 0)
+    verify_eval("36 < INT_37", 2)
+
+    # Different formats in comparison
+    verify_eval("INT_37 < 0x26", 2) # 38
+    verify_eval("INT_37 < 0x25", 0) # 37
+    verify_eval("INT_37 < 0x24", 0) # 36
+    verify_eval("HEX_0X37 < 56", 2) # 0x38
+    verify_eval("HEX_0X37 < 55", 0) # 0x37
+    verify_eval("HEX_0X37 < 54", 0) # 0x36
+
+    # Other int comparisons
+    verify_eval("INT_37 <= 38", 2)
+    verify_eval("INT_37 <= 37", 2)
+    verify_eval("INT_37 <= 36", 0)
+    verify_eval("INT_37 >  38", 0)
+    verify_eval("INT_37 >  37", 0)
+    verify_eval("INT_37 >  36", 2)
+    verify_eval("INT_37 >= 38", 0)
+    verify_eval("INT_37 >= 37", 2)
+    verify_eval("INT_37 >= 36", 2)
+
+    # Other hex comparisons
+    verify_eval("HEX_0X37 <= 0x38", 2)
+    verify_eval("HEX_0X37 <= 0x37", 2)
+    verify_eval("HEX_0X37 <= 0x36", 0)
+    verify_eval("HEX_0X37 >  0x38", 0)
+    verify_eval("HEX_0X37 >  0x37", 0)
+    verify_eval("HEX_0X37 >  0x36", 2)
+    verify_eval("HEX_0X37 >= 0x38", 0)
+    verify_eval("HEX_0X37 >= 0x37", 2)
+    verify_eval("HEX_0X37 >= 0x36", 2)
+
+    # A hex holding a value without a "0x" prefix should still be treated as
+    # hexadecimal
+    verify_eval("HEX_37 < 0x38", 2)
+    verify_eval("HEX_37 < 0x37", 0)
+    verify_eval("HEX_37 < 0x36", 0)
+
+    # Symbol comparisons
+    verify_eval("INT_37   <  HEX_0X37", 2)
+    verify_eval("INT_37   >  HEX_0X37", 0)
+    verify_eval("HEX_0X37 <  INT_37  ", 0)
+    verify_eval("HEX_0X37 >  INT_37  ", 2)
+    verify_eval("INT_37   <  INT_37  ", 0)
+    verify_eval("INT_37   <= INT_37  ", 2)
+    verify_eval("INT_37   >  INT_37  ", 0)
+    verify_eval("INT_37   <= INT_37  ", 2)
+
+    # Tristate value comparisons
+    verify_eval("n < n", 0)
+    verify_eval("n < m", 2)
+    verify_eval("n < y", 2)
+    verify_eval("n < N", 0)
+    verify_eval("n < M", 2)
+    verify_eval("n < Y", 2)
+    verify_eval("0 > n", 0)
+    verify_eval("1 > n", 2)
+    verify_eval("2 > n", 2)
+    verify_eval("m < n", 0)
+    verify_eval("m < m", 0)
+    verify_eval("m < y", 2)
+
+    # Strings compare lexicographically
+    verify_eval("'aa' < 'ab'", 2)
+    verify_eval("'aa' > 'ab'", 0)
+    verify_eval("'ab' < 'aa'", 0)
+    verify_eval("'ab' > 'aa'", 2)
+
+    # Comparisons where one of the operands doesn't parse as a number also give
+    # a lexicographic comparison
+    verify_eval("INT_37 <  '37a' ", 2)
+    verify_eval("'37a'  >  INT_37", 2)
+    verify_eval("INT_37 <= '37a' ", 2)
+    verify_eval("'37a'  >= INT_37", 2)
+    verify_eval("INT_37 >= '37a' ", 0)
+    verify_eval("INT_37 >  '37a' ", 0)
+    verify_eval("'37a'  <  INT_37", 0)
+    verify_eval("'37a'  <= INT_37", 0)
+
+    def verify_eval_bad(expr):
+        try:
+            c.eval_string(expr)
+        except KconfigError:
+            pass
+        else:
+            fail('expected eval_string("{}") to throw KconfigError, '
+                 "didn't".format(expr))
+
+    # Verify that some bad stuff throws KconfigError's
+    verify_eval_bad("")
+    verify_eval_bad("&")
+    verify_eval_bad("|")
+    verify_eval_bad("!")
+    verify_eval_bad("(")
+    verify_eval_bad(")")
+    verify_eval_bad("=")
+    verify_eval_bad("(X")
+    verify_eval_bad("X)")
+    verify_eval_bad("X X")
+    verify_eval_bad("!X X")
+    verify_eval_bad("X !X")
+    verify_eval_bad("(X) X")
+    verify_eval_bad("X &&")
+    verify_eval_bad("&& X")
+    verify_eval_bad("X && && X")
+    verify_eval_bad("X && !&&")
+    verify_eval_bad("X ||")
+    verify_eval_bad("|| X")
+
+
+    print("Testing Symbol.__str__()/custom_str() and def_{int,hex,string}")
+
+    def verify_str(item, s):
+        verify_equal(str(item), s[1:-1])
+
+    def verify_custom_str(item, s):
+        verify_equal(item.custom_str(lambda sc: "[{}]".format(sc.name)),
+                     s[1:-1])
+
+    c = Kconfig("Kconfiglib/tests/Kstr", warn=False)
+
+    c.modules.set_value(2)
+
+    verify_str(c.syms["UNDEFINED"], """
+""")
+
+    verify_str(c.syms["BASIC_NO_PROMPT"], """
+config BASIC_NO_PROMPT
+	bool
+	help
+	  blah blah
+	  
+	    blah blah blah
+	  
+	   blah
+""")
+
+    verify_str(c.syms["BASIC_PROMPT"], """
+config BASIC_PROMPT
+	bool "basic"
+""")
+
+    verify_str(c.syms["ADVANCED"], """
+config ADVANCED
+	tristate "prompt" if DEP
+	default DEFAULT_1
+	default DEFAULT_2 if DEP
+	select SELECTED_1
+	select SELECTED_2 if DEP
+	imply IMPLIED_1
+	imply IMPLIED_2 if DEP
+	help
+	  first help text
+
+config ADVANCED
+	tristate "prompt 2"
+
+menuconfig ADVANCED
+	tristate "prompt 3"
+
+config ADVANCED
+	tristate
+	depends on (A || !B || (C && D) || !(E && F) || G = H || (I && !J && (K || L) && !(M || N) && O = P)) && DEP4 && DEP3
+	help
+	  second help text
+
+config ADVANCED
+	tristate "prompt 4" if VIS
+	depends on DEP4 && DEP3
+""")
+
+    verify_custom_str(c.syms["ADVANCED"], """
+config ADVANCED
+	tristate "prompt" if [DEP]
+	default [DEFAULT_1]
+	default [DEFAULT_2] if [DEP]
+	select [SELECTED_1]
+	select [SELECTED_2] if [DEP]
+	imply [IMPLIED_1]
+	imply [IMPLIED_2] if [DEP]
+	help
+	  first help text
+
+config ADVANCED
+	tristate "prompt 2"
+
+menuconfig ADVANCED
+	tristate "prompt 3"
+
+config ADVANCED
+	tristate
+	depends on ([A] || ![B] || ([C] && [D]) || !([E] && [F]) || [G] = [H] || ([I] && ![J] && ([K] || [L]) && !([M] || [N]) && [O] = [P])) && [DEP4] && [DEP3]
+	help
+	  second help text
+
+config ADVANCED
+	tristate "prompt 4" if [VIS]
+	depends on [DEP4] && [DEP3]
+""")
+
+
+    verify_str(c.syms["ONLY_DIRECT_DEPS"], """
+config ONLY_DIRECT_DEPS
+	int
+	depends on DEP1 && DEP2
+""")
+
+    verify_str(c.syms["STRING"], """
+config STRING
+	string
+	default "foo"
+	default "bar" if DEP
+	default STRING2
+	default STRING3 if DEP
+""")
+
+    verify_str(c.syms["INT"], """
+config INT
+	int
+	range 1 2
+	range FOO BAR
+	range BAZ QAZ if DEP
+	default 7 if DEP
+""")
+
+    verify_str(c.syms["HEX"], """
+config HEX
+	hex
+	range 0x100 0x200
+	range FOO BAR
+	range BAZ QAZ if DEP
+	default 0x123
+""")
+
+    verify_str(c.modules, """
+config MODULES
+	bool "MODULES"
+	option modules
+""")
+
+    verify_str(c.syms["OPTIONS"], """
+config OPTIONS
+	option allnoconfig_y
+	option defconfig_list
+	option env="ENV"
+""")
+
+    verify_str(c.syms["CORRECT_PROP_LOCS_BOOL"], """
+config CORRECT_PROP_LOCS_BOOL
+	bool "prompt 1"
+	default DEFAULT_1
+	default DEFAULT_2
+	select SELECT_1
+	select SELECT_2
+	imply IMPLY_1
+	imply IMPLY_2
+	depends on LOC_1
+	help
+	  help 1
+
+menuconfig CORRECT_PROP_LOCS_BOOL
+	bool "prompt 2"
+	default DEFAULT_3
+	default DEFAULT_4
+	select SELECT_3
+	select SELECT_4
+	imply IMPLY_3
+	imply IMPLY_4
+	depends on LOC_2
+	help
+	  help 2
+
+config CORRECT_PROP_LOCS_BOOL
+	bool "prompt 3"
+	default DEFAULT_5
+	default DEFAULT_6
+	select SELECT_5
+	select SELECT_6
+	imply IMPLY_5
+	imply IMPLY_6
+	depends on LOC_3
+	help
+	  help 2
+""")
+
+    verify_str(c.syms["CORRECT_PROP_LOCS_INT"], """
+config CORRECT_PROP_LOCS_INT
+	int
+	range 1 2
+	range 3 4
+	depends on LOC_1
+
+config CORRECT_PROP_LOCS_INT
+	int
+	range 5 6
+	range 7 8
+	depends on LOC_2
+""")
+
+    verify_str(c.syms["PROMPT_ONLY"], """
+config PROMPT_ONLY
+	prompt "prompt only"
+""")
+
+    verify_custom_str(c.syms["CORRECT_PROP_LOCS_INT"], """
+config CORRECT_PROP_LOCS_INT
+	int
+	range [1] [2]
+	range [3] [4]
+	depends on [LOC_1]
+
+config CORRECT_PROP_LOCS_INT
+	int
+	range [5] [6]
+	range [7] [8]
+	depends on [LOC_2]
+""")
+
+
+
+    print("Testing Choice.__str__()/custom_str()")
+
+    verify_str(c.named_choices["CHOICE"], """
+choice CHOICE
+	tristate "foo"
+	default CHOICE_1
+	default CHOICE_2 if dep
+""")
+
+    verify_str(c.named_choices["CHOICE"].nodes[0].next.item, """
+choice
+	tristate "no name"
+	optional
+""")
+
+    verify_str(c.named_choices["CORRECT_PROP_LOCS_CHOICE"], """
+choice CORRECT_PROP_LOCS_CHOICE
+	bool
+	default CHOICE_3
+	depends on LOC_1
+
+choice CORRECT_PROP_LOCS_CHOICE
+	bool
+	default CHOICE_4
+	depends on LOC_2
+
+choice CORRECT_PROP_LOCS_CHOICE
+	bool
+	default CHOICE_5
+	depends on LOC_3
+""")
+
+    verify_custom_str(c.named_choices["CORRECT_PROP_LOCS_CHOICE"], """
+choice CORRECT_PROP_LOCS_CHOICE
+	bool
+	default [CHOICE_3]
+	depends on [LOC_1]
+
+choice CORRECT_PROP_LOCS_CHOICE
+	bool
+	default [CHOICE_4]
+	depends on [LOC_2]
+
+choice CORRECT_PROP_LOCS_CHOICE
+	bool
+	default [CHOICE_5]
+	depends on [LOC_3]
+""")
+
+
+    print("Testing MenuNode.__str__()/custom_str() for menus and comments")
+
+    verify_str(c.syms["SIMPLE_MENU_HOOK"].nodes[0].next, """
+menu "simple menu"
+""")
+
+    verify_str(c.syms["ADVANCED_MENU_HOOK"].nodes[0].next, """
+menu "advanced menu"
+	depends on A
+	visible if B && (C || D)
+""")
+
+    verify_custom_str(c.syms["ADVANCED_MENU_HOOK"].nodes[0].next, """
+menu "advanced menu"
+	depends on [A]
+	visible if [B] && ([C] || [D])
+""")
+
+    verify_str(c.syms["SIMPLE_COMMENT_HOOK"].nodes[0].next, """
+comment "simple comment"
+""")
+
+    verify_str(c.syms["ADVANCED_COMMENT_HOOK"].nodes[0].next, """
+comment "advanced comment"
+	depends on A && B
+""")
+
+    verify_custom_str(c.syms["ADVANCED_COMMENT_HOOK"].nodes[0].next, """
+comment "advanced comment"
+	depends on [A] && [B]
+""")
+
+
+    print("Testing {MenuNode,Symbol,Choice}.orig_*")
+
+    # Just test some corner cases here re. MenuNode.orig_*. They are already
+    # indirectly tested above. Use MenuNode.__str__() as a proxy.
+
+    verify_str(c.syms["DEP_REM_CORNER_CASES"], """
+config DEP_REM_CORNER_CASES
+	bool
+	default A
+	depends on n
+
+config DEP_REM_CORNER_CASES
+	bool
+	default B if n
+
+config DEP_REM_CORNER_CASES
+	bool
+	default C
+	depends on m && MODULES
+
+config DEP_REM_CORNER_CASES
+	bool
+	default D if A
+
+config DEP_REM_CORNER_CASES
+	bool
+	default E if !E1
+	default F if F1 = F2
+	default G if G1 || H1
+	depends on !H
+
+config DEP_REM_CORNER_CASES
+	bool
+	default H
+	depends on "foo" = "bar"
+
+config DEP_REM_CORNER_CASES
+	bool "prompt" if FOO || BAR
+	depends on BAZ && QAZ
+""")
+
+    # Test {Symbol,Choice}.orig_*
+
+    def verify_deps(elms, dep_index, expected):
+        verify_equal(" ".join(expr_str(elm[dep_index]) for elm in elms),
+                     expected)
+
+    verify_deps(c.syms["BOOL_SYM_ORIG"].orig_defaults,        1, "DEP y y")
+    verify_deps(c.syms["BOOL_SYM_ORIG"].orig_selects,         1, "y DEP y")
+    verify_deps(c.syms["BOOL_SYM_ORIG"].orig_implies,         1, "y y DEP")
+    verify_deps(c.syms["INT_SYM_ORIG"].orig_ranges,           2, "DEP y DEP")
+    verify_deps(c.named_choices["CHOICE_ORIG"].orig_defaults, 1, "y DEP DEP")
+
+
+    print("Testing Symbol.__repr__()")
+
+    def verify_repr(item, s):
+        verify_equal(repr(item) + "\n", s[1:])
+
+    c = Kconfig("Kconfiglib/tests/Krepr", warn=False)
+
+    verify_repr(c.n, """
+<symbol n, tristate, value n, constant>
+""")
+
+    verify_repr(c.m, """
+<symbol m, tristate, value m, constant>
+""")
+
+    verify_repr(c.y, """
+<symbol y, tristate, value y, constant>
+""")
+
+    verify_repr(c.syms["UNDEFINED"], """
+<symbol UNDEFINED, unknown, value "UNDEFINED", visibility n, direct deps n, undefined>
+""")
+
+    verify_repr(c.syms["BASIC"], """
+<symbol BASIC, bool, value y, visibility n, direct deps y, Kconfiglib/tests/Krepr:9>
+""")
+
+    verify_repr(c.syms["VISIBLE"], """
+<symbol VISIBLE, bool, "visible", value n, visibility y, direct deps y, Kconfiglib/tests/Krepr:14>
+""")
+
+    c.syms["VISIBLE"].set_value(2)
+    c.syms["STRING"].set_value("foo")
+
+    verify_repr(c.syms["VISIBLE"], """
+<symbol VISIBLE, bool, "visible", value y, user value y, visibility y, direct deps y, Kconfiglib/tests/Krepr:14>
+""")
+
+    verify_repr(c.syms["STRING"], """
+<symbol STRING, string, "visible", value "foo", user value "foo", visibility y, direct deps y, Kconfiglib/tests/Krepr:17>
+""")
+
+    verify_repr(c.syms["DIR_DEP_N"], """
+<symbol DIR_DEP_N, unknown, value "DIR_DEP_N", visibility n, direct deps n, Kconfiglib/tests/Krepr:20>
+""")
+
+    verify_repr(c.syms["OPTIONS"], """
+<symbol OPTIONS, unknown, value "OPTIONS", visibility n, allnoconfig_y, is the defconfig_list symbol, from environment variable ENV, direct deps y, Kconfiglib/tests/Krepr:23>
+""")
+
+    verify_repr(c.syms["MULTI_DEF"], """
+<symbol MULTI_DEF, unknown, value "MULTI_DEF", visibility n, direct deps y, Kconfiglib/tests/Krepr:28, Kconfiglib/tests/Krepr:29>
+""")
+
+    verify_repr(c.syms["CHOICE_1"], """
+<symbol CHOICE_1, tristate, "choice sym", value n, visibility m, choice symbol, direct deps m, Kconfiglib/tests/Krepr:36>
+""")
+
+    verify_repr(c.modules, """
+<symbol MODULES, bool, value y, visibility n, is the modules symbol, direct deps y, Kconfiglib/tests/Krepr:1>
+""")
+
+
+    print("Testing Choice.__repr__()")
+
+    verify_repr(c.named_choices["CHOICE"], """
+<choice CHOICE, tristate, "choice", mode m, visibility y, Kconfiglib/tests/Krepr:33>
+""")
+
+    c.named_choices["CHOICE"].set_value(2)
+
+    verify_repr(c.named_choices["CHOICE"], """
+<choice CHOICE, tristate, "choice", mode y, user mode y, CHOICE_1 selected, visibility y, Kconfiglib/tests/Krepr:33>
+""")
+
+    c.syms["CHOICE_2"].set_value(2)
+
+    verify_repr(c.named_choices["CHOICE"], """
+<choice CHOICE, tristate, "choice", mode y, user mode y, CHOICE_2 selected, CHOICE_2 selected by user, visibility y, Kconfiglib/tests/Krepr:33>
+""")
+
+    c.named_choices["CHOICE"].set_value(1)
+
+    verify_repr(c.named_choices["CHOICE"], """
+<choice CHOICE, tristate, "choice", mode m, user mode m, CHOICE_2 selected by user (overridden), visibility y, Kconfiglib/tests/Krepr:33>
+""")
+
+    verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next.item, """
+<choice, tristate, "optional choice", mode n, visibility n, optional, Kconfiglib/tests/Krepr:46>
+""")
+
+
+    print("Testing MenuNode.__repr__()")
+
+    verify_repr(c.syms["BASIC"].nodes[0], """
+<menu node for symbol BASIC, deps y, has help, has next, Kconfiglib/tests/Krepr:9>
+""")
+
+    verify_repr(c.syms["DIR_DEP_N"].nodes[0], """
+<menu node for symbol DIR_DEP_N, deps n, has next, Kconfiglib/tests/Krepr:20>
+""")
+
+    verify_repr(c.syms["MULTI_DEF"].nodes[0], """
+<menu node for symbol MULTI_DEF, deps y, has next, Kconfiglib/tests/Krepr:28>
+""")
+
+    verify_repr(c.syms["MULTI_DEF"].nodes[1], """
+<menu node for symbol MULTI_DEF, deps y, has next, Kconfiglib/tests/Krepr:29>
+""")
+
+    verify_repr(c.syms["MENUCONFIG"].nodes[0], """
+<menu node for symbol MENUCONFIG, is menuconfig, deps y, has next, Kconfiglib/tests/Krepr:31>
+""")
+
+    verify_repr(c.named_choices["CHOICE"].nodes[0], """
+<menu node for choice CHOICE, prompt "choice" (visibility y), deps y, has child, has next, Kconfiglib/tests/Krepr:33>
+""")
+
+    verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next, """
+<menu node for choice, prompt "optional choice" (visibility n), deps y, has next, Kconfiglib/tests/Krepr:46>
+""")
+
+    verify_repr(c.syms["NO_VISIBLE_IF_HOOK"].nodes[0].next, """
+<menu node for menu, prompt "no visible if" (visibility y), deps y, 'visible if' deps y, has next, Kconfiglib/tests/Krepr:53>
+""")
+
+    verify_repr(c.syms["VISIBLE_IF_HOOK"].nodes[0].next, """
+<menu node for menu, prompt "visible if" (visibility y), deps y, 'visible if' deps m, has next, Kconfiglib/tests/Krepr:58>
+""")
+
+    verify_repr(c.syms["COMMENT_HOOK"].nodes[0].next, """
+<menu node for comment, prompt "comment" (visibility y), deps y, Kconfiglib/tests/Krepr:64>
+""")
+
+
+    print("Testing Kconfig.__repr__()")
+
+    verify_repr(c, """
+<configuration with 15 symbols, main menu prompt "Main menu", srctree is current directory, config symbol prefix "CONFIG_", warnings disabled, printing of warnings to stderr enabled, undef. symbol assignment warnings disabled, overriding symbol assignment warnings enabled, redundant symbol assignment warnings enabled>
+""")
+
+    os.environ["srctree"] = "Kconfiglib"
+    os.environ["CONFIG_"] = "CONFIG_ value"
+
+    c = Kconfig("tests/Krepr", warn=False)
+    c.warn = True
+    c.warn_to_stderr = False
+    c.warn_assign_override = False
+    c.warn_assign_redun = False
+    c.warn_assign_undef = True
+
+    verify_repr(c, """
+<configuration with 15 symbols, main menu prompt "Main menu", srctree "Kconfiglib", config symbol prefix "CONFIG_ value", warnings enabled, printing of warnings to stderr disabled, undef. symbol assignment warnings enabled, overriding symbol assignment warnings disabled, redundant symbol assignment warnings disabled>
+""")
+
+    os.environ.pop("srctree", None)
+    os.environ.pop("CONFIG_", None)
+
+
+    print("Testing tricky help strings")
+
+    c = Kconfig("Kconfiglib/tests/Khelp")
+
+    def verify_help(node, s):
+        verify_equal(node.help, s[1:-1])
+
+    verify_help(c.syms["TWO_HELP_STRINGS"].nodes[0], """
+first help string
+""")
+
+    verify_help(c.syms["TWO_HELP_STRINGS"].nodes[1], """
+second help string
+""")
+
+    verify_help(c.syms["NO_BLANK_AFTER_HELP"].nodes[0], """
+help for
+NO_BLANK_AFTER_HELP
+""")
+
+    verify_help(c.named_choices["CHOICE_HELP"].nodes[0], """
+help for
+CHOICE_HELP
+""")
+
+    verify_help(c.syms["HELP_TERMINATED_BY_COMMENT"].nodes[0], """
+a
+b
+c
+""")
+
+    verify_help(c.syms["TRICKY_HELP"].nodes[0], """
+a
+ b
+  c
+
+ d
+  e
+   f
+
+
+g
+ h
+  i
+""")
+
+
+    print("Testing locations, source/rsource/gsource/grsource, and "
+          "Kconfig.kconfig_filenames")
+
+    def verify_locations(nodes, *expected_locs):
+        verify(len(nodes) == len(expected_locs),
+               "Wrong number of locations for " + repr(nodes))
+
+        for node, expected_loc in zip(nodes, expected_locs):
+            node_loc = "{}:{}".format(node.filename, node.linenr)
+            verify(node_loc == expected_loc,
+                   "expected {} to have the location {}, had the location {}"
+                   .format(repr(node), expected_loc, node_loc))
+
+    # Expanded in the 'source' statement in Klocation
+
+    os.environ["TESTS_DIR_FROM_ENV"] = "tests"
+    os.environ["SUB_DIR_FROM_ENV"] = "sub"
+
+    os.environ["_SOURCED"] = "_sourced"
+    os.environ["_RSOURCED"] = "_rsourced"
+    os.environ["_GSOURCED"] = "_gsourced"
+    os.environ["_GRSOURCED"] = "_grsourced"
+
+    # Test twice, with $srctree as a relative and an absolute path,
+    # respectively
+    for srctree in "Kconfiglib", os.path.abspath("Kconfiglib"):
+        os.environ["srctree"] = srctree
+
+        # Has symbol with empty help text, so disable warnings
+        c = Kconfig("tests/Klocation", warn=False)
+
+        verify_locations(c.syms["UNDEFINED"].nodes)
+        verify_equal(c.syms["UNDEFINED"].name_and_loc, "UNDEFINED (undefined)")
+
+        verify_locations(c.syms["ONE_DEF"].nodes, "tests/Klocation:4")
+        verify_equal(c.syms["ONE_DEF"].name_and_loc,
+                     "ONE_DEF (defined at tests/Klocation:4)")
+
+        verify_locations(c.syms["TWO_DEF"].nodes,
+                         "tests/Klocation:7",
+                         "tests/Klocation:10")
+        verify_equal(c.syms["TWO_DEF"].name_and_loc,
+                     "TWO_DEF (defined at tests/Klocation:7, tests/Klocation:10)")
+
+        verify_locations(c.syms["MANY_DEF"].nodes,
+                         "tests/Klocation:13",
+                         "tests/Klocation:43",
+                         "tests/Klocation:45",
+                         "tests/Klocation_sourced:3",
+                         "tests/sub/Klocation_rsourced:2",
+                         "tests/sub/Klocation_gsourced1:1",
+                         "tests/sub/Klocation_gsourced2:1",
+                         "tests/sub/Klocation_gsourced1:1",
+                         "tests/sub/Klocation_gsourced2:1",
+                         "tests/sub/Klocation_grsourced1:1",
+                         "tests/sub/Klocation_grsourced2:1",
+                         "tests/sub/Klocation_grsourced1:1",
+                         "tests/sub/Klocation_grsourced2:1",
+                         "tests/Klocation:78")
+
+        verify_locations(c.named_choices["CHOICE_ONE_DEF"].nodes,
+                         "tests/Klocation_sourced:5")
+        verify_equal(c.named_choices["CHOICE_ONE_DEF"].name_and_loc,
+                     "<choice CHOICE_ONE_DEF> (defined at tests/Klocation_sourced:5)")
+
+        verify_locations(c.named_choices["CHOICE_TWO_DEF"].nodes,
+                         "tests/Klocation_sourced:9",
+                         "tests/Klocation_sourced:13")
+        verify_equal(c.named_choices["CHOICE_TWO_DEF"].name_and_loc,
+                     "<choice CHOICE_TWO_DEF> (defined at tests/Klocation_sourced:9, tests/Klocation_sourced:13)")
+
+        verify_locations([c.syms["MENU_HOOK"].nodes[0].next],
+                         "tests/Klocation_sourced:20")
+
+        verify_locations([c.syms["COMMENT_HOOK"].nodes[0].next],
+                         "tests/Klocation_sourced:26")
+
+        # Test Kconfig.kconfig_filenames
+
+        verify_equal(c.kconfig_filenames, [
+            "tests/Klocation",
+            "tests/Klocation_sourced",
+            "tests/sub/Klocation_rsourced",
+            "tests/sub/Klocation_gsourced1",
+            "tests/sub/Klocation_gsourced2",
+            "tests/sub/Klocation_gsourced1",
+            "tests/sub/Klocation_gsourced2",
+            "tests/sub/Klocation_grsourced1",
+            "tests/sub/Klocation_grsourced2",
+            "tests/sub/Klocation_grsourced1",
+            "tests/sub/Klocation_grsourced2"
+        ])
+
+        # Test recursive 'source' detection
+
+        try:
+            Kconfig("tests/Krecursive1")
+        except KconfigError as e:
+            verify_equal(str(e), """
+tests/Krecursive2:1: recursive 'source' of 'tests/Krecursive1' detected. Check that environment variables are set correctly.
+Include path:
+tests/Krecursive1:1
+tests/Krecursive2:1
+"""[:-1])
+        except:
+            fail("recursive 'source' raised wrong exception")
+        else:
+            fail("recursive 'source' did not raise exception")
+
+        # Verify that source and rsource throw exceptions for missing files
+
+        # TODO: Make an exception test helper
+
+        try:
+            Kconfig("tests/Kmissingsource")
+        except KconfigError as e:
+            if "not found" not in str(e):
+                fail("'source' with missing file raised wrong KconfigError")
+        except:
+            fail("'source' with missing file raised wrong exception")
+        else:
+            fail("'source' with missing file did not raise exception")
+
+        try:
+            Kconfig("tests/Kmissingrsource")
+        except KconfigError as e:
+            if "not found" not in str(e):
+                fail("'rsource' with missing file raised wrong KconfigError")
+        except:
+            fail("'rsource' with missing file raised wrong exception")
+        else:
+            fail("'rsource' with missing file did not raise exception")
+
+    # Test a tricky case involving symlinks. $srctree is tests/symlink, which
+    # points to tests/sub/sub, meaning tests/symlink/.. != tests/. Previously,
+    # using 'rsource' from a file sourced with an absolute path triggered an
+    # unsafe relpath() with tests/symlink/.. in it, crashing.
+
+    os.environ["srctree"] = "Kconfiglib/tests/symlink"
+    os.environ["KCONFIG_SYMLINK_2"] = os.path.abspath(
+        "Kconfiglib/tests/sub/Kconfig_symlink_2")
+    if not os.path.isabs(
+        Kconfig("Kconfig_symlink_1").syms["FOUNDME"].nodes[0].filename):
+
+        fail("Symlink + rsource issues")
+
+
+    print("Testing Kconfig.node_iter()")
+
+    # Reuse tests/Klocation. The node_iter(unique_syms=True) case already gets
+    # plenty of testing from write_config() as well.
+
+    os.environ["srctree"] = "Kconfiglib"
+    c = Kconfig("tests/Klocation", warn=False)
+
+    verify_equal(
+        [node.item.name for node in c.node_iter()
+         if isinstance(node.item, Symbol)],
+        ["ONE_DEF", "TWO_DEF", "TWO_DEF", "MANY_DEF", "HELP_1", "HELP_2",
+         "HELP_3", "MANY_DEF", "MANY_DEF", "MANY_DEF", "MENU_HOOK",
+         "COMMENT_HOOK"] + 10*["MANY_DEF"])
+
+    verify_equal(
+        [node.item.name for node in c.node_iter(True)
+         if isinstance(node.item, Symbol)],
+        ["ONE_DEF", "TWO_DEF", "MANY_DEF", "HELP_1", "HELP_2", "HELP_3",
+         "MENU_HOOK", "COMMENT_HOOK"])
+
+    verify_equal(
+        [node.prompt[0] for node in c.node_iter()
+         if not isinstance(node.item, Symbol)],
+        ["one-def choice", "two-def choice 1", "two-def choice 2",
+         "menu", "comment"])
+
+    verify_equal(
+        [node.prompt[0] for node in c.node_iter(True)
+         if not isinstance(node.item, Symbol)],
+        ["one-def choice", "two-def choice 1", "two-def choice 2",
+         "menu", "comment"])
+
+
+    print("Testing MenuNode.include_path")
+
+    os.environ["srctree"] = "Kconfiglib/tests"
+
+    c = Kconfig("Kinclude_path")
+
+    def verify_node_path(node, *expected):
+        if node.include_path != expected:
+            fail("Wrong include path for node {!r}. Got {}, expected {}."
+                 .format(node, node.include_path, expected))
+
+    def verify_sym_path(sym_name, node_i, *expected):
+        verify_node_path(c.syms[sym_name].nodes[node_i], *expected)
+
+    verify_sym_path("TOP", 0)
+    verify_sym_path("TOP", 1)
+    verify_sym_path("TOP", 2)
+
+    verify_sym_path("ONE_DOWN", 0, ("Kinclude_path", 4))
+    verify_sym_path("ONE_DOWN", 1, ("Kinclude_path", 4))
+    verify_sym_path("ONE_DOWN", 2, ("Kinclude_path", 4))
+    verify_sym_path("ONE_DOWN", 3, ("Kinclude_path", 9))
+    verify_sym_path("ONE_DOWN", 4, ("Kinclude_path", 9))
+    verify_sym_path("ONE_DOWN", 5, ("Kinclude_path", 9))
+
+    verify_sym_path("TWO_DOWN", 0,
+                    ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4))
+    verify_sym_path("TWO_DOWN", 1,
+                    ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 9))
+    verify_sym_path("TWO_DOWN", 2,
+                    ("Kinclude_path", 9), ("Kinclude_path_sourced_1", 4))
+    verify_sym_path("TWO_DOWN", 3,
+                    ("Kinclude_path", 9), ("Kinclude_path_sourced_1", 9))
+
+    verify_node_path(c.top_node)
+    verify_node_path(c.menus[0], ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4))
+    verify_node_path(c.comments[0], ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4))
+    verify_node_path(c.choices[0].nodes[0], ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4))
+
+    os.environ.pop("srctree", None)
+
+
+    print("Testing Kconfig.choices/menus/comments")
+
+    c = Kconfig("Kconfiglib/tests/Kitemlists")
+
+    def verify_prompts(items, *expected_prompts):
+        verify(len(items) == len(expected_prompts),
+               "Wrong number of prompts for {}".format(items))
+
+        for item, expected_prompt in zip(items, expected_prompts):
+            if not isinstance(item, MenuNode):
+                item = item.nodes[0]
+
+            verify(item.prompt[0] == expected_prompt,
+                   "Wrong prompt for {}, expected '{}'"
+                   .format(repr(item), expected_prompt))
+
+    verify_prompts(c.choices, "choice 1", "choice 2", "choice 3", "choice 2")
+    verify_prompts(c.menus, "menu 1", "menu 2", "menu 3", "menu 4", "menu 5")
+    verify_prompts(c.comments, "comment 1", "comment 2", "comment 3")
+
+
+    print("Testing Symbol/Choice.direct_dep")
+
+    c = Kconfig("Kconfiglib/tests/Kdirdep")
+
+    verify_equal(expr_str(c.syms["NO_DEP_SYM"].direct_dep), 'y')
+    verify_equal(expr_str(c.syms["DEP_SYM"].direct_dep), "A || (B && C) || !D")
+
+    verify_equal(expr_str(c.named_choices["NO_DEP_CHOICE"].direct_dep), 'y')
+    verify_equal(expr_str(c.named_choices["DEP_CHOICE"].direct_dep),
+                 "A || B || C")
+
+
+    print("Testing expr_items()")
+
+    c = Kconfig("Kconfiglib/tests/Kexpr_items")
+
+    def verify_expr_items(expr, *sym_names):
+        verify_equal(tuple(sorted(item.name for item in expr_items(expr))),
+                     sym_names)
+
+    verify_expr_items(
+        c.syms["TEST"].defaults[0][0],
+        "A", "B", "C", "D", "E", "F", "G", "H"
+    )
+
+    verify_expr_items(
+        c.syms["TEST_CHOICE"].nodes[0].prompt[1],
+        "A", "CHOICE"
+    )
+
+
+    print("Testing MenuNode/Symbol/Choice.referenced")
+
+    c = Kconfig("Kconfiglib/tests/Kreferenced", warn=False)
+
+    def verify_deps(item, *dep_names):
+        verify_equal(tuple(sorted(item.name for item in item.referenced)),
+                     dep_names)
+
+    verify_deps(c.top_node, "y")
+
+    verify_deps(c.syms["NO_REFS"].nodes[0], "y")
+
+    verify_deps(c.syms["JUST_DEPENDS_ON_REFS"].nodes[0], "A", "B")
+
+    verify_deps(c.syms["LOTS_OF_REFS"].nodes[0],
+                *(chr(n) for n in range(ord("A"), ord("Z") + 1)))
+
+    verify_deps(c.syms["INT_REFS"].nodes[0],
+                "A", "B", "C", "D", "E", "F", "G", "H", "y")
+
+    verify_deps(c.syms["CHOICE_REF"].nodes[0], "CHOICE")
+
+    verify_deps(c.menus[0], "A", "B", "C", "D")
+
+    verify_deps(c.comments[0], "A", "B")
+
+    verify_deps(c.syms["MULTI_DEF_SYM"], "A", "B", "C", "y")
+    verify_deps(c.named_choices["MULTI_DEF_CHOICE"], "A", "B", "C")
+
+
+    print("Testing split_expr()")
+
+    c = Kconfig("Kconfiglib/tests/empty")
+    c.warn = False
+
+    def verify_split(to_split, op, operand_strs):
+        # The same hackage as in Kconfig.eval_string()
+        c._tokens = c._tokenize("if " + to_split)[1:]
+        c._tokens_i = 0
+
+        operands = split_expr(c._parse_expr(False), op)
+
+        verify(len(operands) == len(operand_strs),
+               "Wrong number of operands when {} was split by {}"
+               .format(to_split, "OR" if op == OR else "AND"))
+
+        for operand, operand_str in zip(operands, operand_strs):
+            verify_equal(expr_str(operand), operand_str)
+
+    verify_split("A",                    OR, ("A",                ))
+    verify_split("!A",                   OR, ("!A",               ))
+    verify_split("A = B",                OR, ("A = B",            ))
+    verify_split("A && B",               OR, ("A && B",           ))
+    verify_split("A || B",               OR, ("A", "B"            ))
+    verify_split("(A || B) || C",        OR, ("A", "B", "C"       ))
+    verify_split("A || (B || C)",        OR, ("A", "B", "C"       ))
+    verify_split("A || !(B || C)",       OR, ("A", "!(B || C)"    ))
+    verify_split("A || (B && (C || D))", OR, ("A", "B && (C || D)"))
+    verify_split("(A && (B || C)) || D", OR, ("A && (B || C)", "D"))
+
+    verify_split("A",                    AND, ("A",                ))
+    verify_split("!A",                   AND, ("!A",               ))
+    verify_split("A = B",                AND, ("A = B",            ))
+    verify_split("A || B",               AND, ("A || B",           ))
+    verify_split("A && B",               AND, ("A", "B"            ))
+    verify_split("(A && B) && C",        AND, ("A", "B", "C"       ))
+    verify_split("A && (B && C)",        AND, ("A", "B", "C"       ))
+    verify_split("A && !(B && C)",       AND, ("A", "!(B && C)"    ))
+    verify_split("A && (B || (C && D))", AND, ("A", "B || (C && D)"))
+    verify_split("(A || (B && C)) && D", AND, ("A || (B && C)", "D"))
+
+
+    print("Testing visibility")
+
+    c = Kconfig("Kconfiglib/tests/Kvisibility")
+
+    def verify_visibility(item, no_module_vis, module_vis):
+        c.modules.set_value(0)
+        verify(item.visibility == no_module_vis,
+               "expected {} to have visibility {} without modules, had "
+               "visibility {}".
+               format(repr(item), no_module_vis, item.visibility))
+
+        c.modules.set_value(2)
+        verify(item.visibility == module_vis,
+               "expected {} to have visibility {} with modules, had "
+               "visibility {}".
+               format(repr(item), module_vis, item.visibility))
+
+    # Symbol visibility
+
+    verify_visibility(c.syms["NO_PROMPT"],     0, 0)
+    verify_visibility(c.syms["BOOL_N"],        0, 0)
+    verify_visibility(c.syms["BOOL_M"],        0, 2)
+    verify_visibility(c.syms["BOOL_MOD"],      2, 2)
+    verify_visibility(c.syms["BOOL_Y"],        2, 2)
+    verify_visibility(c.syms["TRISTATE_M"],    0, 1)
+    verify_visibility(c.syms["TRISTATE_MOD"],  2, 1)
+    verify_visibility(c.syms["TRISTATE_Y"],    2, 2)
+    verify_visibility(c.syms["BOOL_IF_N"],     0, 0)
+    verify_visibility(c.syms["BOOL_IF_M"],     0, 2)
+    verify_visibility(c.syms["BOOL_IF_Y"],     2, 2)
+    verify_visibility(c.syms["BOOL_MENU_N"],   0, 0)
+    verify_visibility(c.syms["BOOL_MENU_M"],   0, 2)
+    verify_visibility(c.syms["BOOL_MENU_Y"],   2, 2)
+    verify_visibility(c.syms["BOOL_CHOICE_N"], 0, 0)
+
+    # Non-tristate symbols in tristate choices are only visible if the choice
+    # is in y mode
+
+    # The choice can't be brought to y mode because of the 'if m'
+    verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0)
+    c.syms["BOOL_CHOICE_M"].choice.set_value(2)
+    verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0)
+
+    # The choice gets y mode only when running without modules, because it
+    # defaults to m mode
+    verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 0)
+    c.syms["BOOL_CHOICE_Y"].choice.set_value(2)
+    # When set to y mode, the choice symbol becomes visible both with and
+    # without modules
+    verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 2)
+
+    verify_visibility(c.syms["TRISTATE_IF_N"],     0, 0)
+    verify_visibility(c.syms["TRISTATE_IF_M"],     0, 1)
+    verify_visibility(c.syms["TRISTATE_IF_Y"],     2, 2)
+    verify_visibility(c.syms["TRISTATE_MENU_N"],   0, 0)
+    verify_visibility(c.syms["TRISTATE_MENU_M"],   0, 1)
+    verify_visibility(c.syms["TRISTATE_MENU_Y"],   2, 2)
+    verify_visibility(c.syms["TRISTATE_CHOICE_N"], 0, 0)
+    verify_visibility(c.syms["TRISTATE_CHOICE_M"], 0, 1)
+    verify_visibility(c.syms["TRISTATE_CHOICE_Y"], 2, 2)
+
+    verify_visibility(c.named_choices["BOOL_CHOICE_N"],     0, 0)
+    verify_visibility(c.named_choices["BOOL_CHOICE_M"],     0, 2)
+    verify_visibility(c.named_choices["BOOL_CHOICE_Y"],     2, 2)
+    verify_visibility(c.named_choices["TRISTATE_CHOICE_N"], 0, 0)
+    verify_visibility(c.named_choices["TRISTATE_CHOICE_M"], 0, 1)
+    verify_visibility(c.named_choices["TRISTATE_CHOICE_Y"], 2, 2)
+
+    verify_visibility(c.named_choices["TRISTATE_CHOICE_IF_M_AND_Y"],   0, 1)
+    verify_visibility(c.named_choices["TRISTATE_CHOICE_MENU_N_AND_Y"], 0, 0)
+
+    # Verify that 'visible if' visibility gets propagated to prompts
+
+    verify_visibility(c.syms["VISIBLE_IF_N"], 0, 0)
+    verify_visibility(c.syms["VISIBLE_IF_M"], 0, 1)
+    verify_visibility(c.syms["VISIBLE_IF_Y"], 2, 2)
+    verify_visibility(c.syms["VISIBLE_IF_M_2"], 0, 1)
+
+    # Verify that string/int/hex symbols with m visibility accept a user value
+
+    assign_and_verify("STRING_m", "foo bar")
+    assign_and_verify("INT_m", "123")
+    assign_and_verify("HEX_m", "0x123")
+
+
+    print("Testing .assignable")
+
+    c = Kconfig("Kconfiglib/tests/Kassignable")
+
+    def verify_assignable_imp(item, assignable_no_modules, assignable_modules):
+        # Verifies the assignable values for 'item', with and without modules.
+
+        for modules_val, assignable in (0, assignable_no_modules), \
+                                       (2, assignable_modules):
+
+            c.modules.set_value(modules_val)
+            module_msg = "without modules" if modules_val == 0 else \
+                         "with modules"
+
+            verify(item.assignable == assignable,
+                   "Incorrect assignable values for {} {}. Should be {}, "
+                   "was {}."
+                   .format(item.name, module_msg, assignable, item.assignable))
+
+            # Verify that the values can actually be assigned too
+
+            for val in item.assignable:
+                item.set_value(val)
+                verify(item.tri_value == val,
+                       "Unable to set {} to {} {}, even though it was in "
+                       ".assignable".format(item.name, val, module_msg))
+
+    def verify_assignable(sym_name, assignable_no_modules, assignable_modules):
+        verify_assignable_imp(c.syms[sym_name],
+                              assignable_no_modules,
+                              assignable_modules)
+
+    def verify_const_unassignable(sym_name):
+        verify_assignable_imp(c.const_syms[sym_name], (), ())
+
+    # Things that shouldn't be .assignable
+    verify_const_unassignable("n")
+    verify_const_unassignable("m")
+    verify_const_unassignable("y")
+    verify_const_unassignable("const")
+    verify_assignable("UNDEFINED", (), ())
+    verify_assignable("NO_PROMPT", (), ())
+    verify_assignable("STRING", (), ())
+    verify_assignable("INT", (), ())
+    verify_assignable("HEX", (), ())
+
+    # Non-selected symbols
+    verify_assignable("Y_VIS_BOOL", (0, 2), (0,    2))
+    verify_assignable("M_VIS_BOOL", (    ), (0,    2))  # Vis. promoted
+    verify_assignable("N_VIS_BOOL", (    ), (       ))
+    verify_assignable("Y_VIS_TRI",  (0, 2), (0, 1, 2))
+    verify_assignable("M_VIS_TRI",  (    ), (0, 1   ))
+    verify_assignable("N_VIS_TRI",  (    ), (       ))
+
+    # Symbols selected to y
+    verify_assignable("Y_SEL_Y_VIS_BOOL", (2,), (2,))
+    verify_assignable("Y_SEL_M_VIS_BOOL", (  ), (2,))  # Vis. promoted
+    verify_assignable("Y_SEL_N_VIS_BOOL", (  ), (  ))
+    verify_assignable("Y_SEL_Y_VIS_TRI",  (2,), (2,))
+    verify_assignable("Y_SEL_M_VIS_TRI",  (  ), (2,))
+    verify_assignable("Y_SEL_N_VIS_TRI",  (  ), (  ))
+
+    # Symbols selected to m
+    verify_assignable("M_SEL_Y_VIS_BOOL", (2,), (   2,))  # Value promoted
+    verify_assignable("M_SEL_M_VIS_BOOL", (  ), (   2,))  # Vis./value promoted
+    verify_assignable("M_SEL_N_VIS_BOOL", (  ), (     ))
+    verify_assignable("M_SEL_Y_VIS_TRI",  (2,), (1, 2 ))
+    verify_assignable("M_SEL_M_VIS_TRI",  (  ), (1,   ))
+    verify_assignable("M_SEL_N_VIS_TRI",  (  ), (     ))
+
+    # Symbols implied to y
+    verify_assignable("Y_IMP_Y_VIS_BOOL", (0, 2), (0, 2))
+    verify_assignable("Y_IMP_M_VIS_BOOL", (    ), (0, 2))  # Vis. promoted
+    verify_assignable("Y_IMP_N_VIS_BOOL", (    ), (    ))
+    verify_assignable("Y_IMP_Y_VIS_TRI",  (0, 2), (0, 2))  # m removed by imply
+    verify_assignable("Y_IMP_M_VIS_TRI",  (    ), (0, 2))  # m promoted to y by imply
+    verify_assignable("Y_IMP_N_VIS_TRI",  (    ), (    ))
+
+    # Symbols implied to m (never affects assignable values)
+    verify_assignable("M_IMP_Y_VIS_BOOL", (0, 2), (0,    2))
+    verify_assignable("M_IMP_M_VIS_BOOL", (    ), (0,    2))  # Vis. promoted
+    verify_assignable("M_IMP_N_VIS_BOOL", (    ), (       ))
+    verify_assignable("M_IMP_Y_VIS_TRI",  (0, 2), (0, 1, 2))
+    verify_assignable("M_IMP_M_VIS_TRI",  (    ), (0, 1   ))
+    verify_assignable("M_IMP_N_VIS_TRI",  (    ), (       ))
+
+    # Symbols in y-mode choice
+    verify_assignable("Y_CHOICE_BOOL",           (2,), (2,))
+    verify_assignable("Y_CHOICE_TRISTATE",       (2,), (2,))
+    verify_assignable("Y_CHOICE_N_VIS_TRISTATE", (  ), (  ))
+
+    # Symbols in m/y-mode choice, starting out in m mode, or y mode when
+    # running without modules
+    verify_assignable("MY_CHOICE_BOOL",           (2,), (    ))
+    verify_assignable("MY_CHOICE_TRISTATE",       (2,), (0, 1))
+    verify_assignable("MY_CHOICE_N_VIS_TRISTATE", (  ), (    ))
+
+    c.named_choices["MY_CHOICE"].set_value(2)
+
+    # Symbols in m/y-mode choice, now in y mode
+    verify_assignable("MY_CHOICE_BOOL",           (2,), (2,))
+    verify_assignable("MY_CHOICE_TRISTATE",       (2,), (2,))
+    verify_assignable("MY_CHOICE_N_VIS_TRISTATE", (  ), (  ))
+
+    def verify_choice_assignable(choice_name, assignable_no_modules,
+                                 assignable_modules):
+        verify_assignable_imp(c.named_choices[choice_name],
+                              assignable_no_modules,
+                              assignable_modules)
+
+    # Choices with various possible modes
+    verify_choice_assignable("Y_CHOICE",   (2,  ), (      2,))
+    verify_choice_assignable("MY_CHOICE",  (2,  ), (   1, 2 ))
+    verify_choice_assignable("NMY_CHOICE", (0, 2), (0, 1, 2 ))
+    verify_choice_assignable("NY_CHOICE",  (0, 2), (0,    2 ))
+    verify_choice_assignable("NM_CHOICE",  (    ), (0, 1    ))
+    verify_choice_assignable("M_CHOICE",   (    ), (   1,   ))
+    verify_choice_assignable("N_CHOICE",   (    ), (        ))
+
+
+    print("Testing object relations")
+
+    c = Kconfig("Kconfiglib/tests/Krelation")
+
+    verify(c.syms["A"].nodes[0].parent is c.top_node,
+           "A's parent should be the top node")
+
+    verify(c.syms["B"].nodes[0].parent.item is c.named_choices["CHOICE_1"],
+           "B's parent should be the first choice")
+
+    verify(c.syms["C"].nodes[0].parent.item is c.syms["B"],
+           "C's parent should be B (due to auto menus)")
+
+    verify(c.syms["E"].nodes[0].parent.item == MENU,
+           "E's parent should be a menu")
+
+    verify(c.syms["E"].nodes[0].parent.parent is c.top_node,
+           "E's grandparent should be the top node")
+
+    verify(c.syms["G"].nodes[0].parent.item is c.named_choices["CHOICE_2"],
+           "G's parent should be the second choice")
+
+    verify(c.syms["G"].nodes[0].parent.parent.item == MENU,
+           "G's grandparent should be a menu")
+
+
+    print("Testing hex/int ranges")
+
+    c = Kconfig("Kconfiglib/tests/Krange", warn=False)
+
+    for sym_name in "HEX_NO_RANGE", "INT_NO_RANGE", "HEX_40", "INT_40":
+        sym = c.syms[sym_name]
+        verify(not sym.ranges,
+               "{} should not have ranges".format(sym_name))
+
+    for sym_name in "HEX_ALL_RANGES_DISABLED", "INT_ALL_RANGES_DISABLED", \
+                    "HEX_RANGE_10_20_LOW_DEFAULT", \
+                    "INT_RANGE_10_20_LOW_DEFAULT":
+        sym = c.syms[sym_name]
+        verify(sym.ranges, "{} should have ranges".format(sym_name))
+
+    # hex/int symbols without defaults should get no default value
+    verify_value("HEX_NO_RANGE", "")
+    verify_value("INT_NO_RANGE", "")
+    # And neither if all ranges are disabled
+    verify_value("HEX_ALL_RANGES_DISABLED", "")
+    verify_value("INT_ALL_RANGES_DISABLED", "")
+    # Make sure they are assignable though, and test that the form of the user
+    # value is reflected in the value for hex symbols
+    assign_and_verify("HEX_NO_RANGE", "0x123")
+    assign_and_verify("HEX_NO_RANGE", "123")
+    assign_and_verify("INT_NO_RANGE", "123")
+
+    # Defaults outside of the valid range should be clamped
+    verify_value("HEX_RANGE_10_20_LOW_DEFAULT", "0x10")
+    verify_value("HEX_RANGE_10_20_HIGH_DEFAULT", "0x20")
+    verify_value("INT_RANGE_10_20_LOW_DEFAULT", "10")
+    verify_value("INT_RANGE_10_20_HIGH_DEFAULT", "20")
+    # Defaults inside the valid range should be preserved. For hex symbols,
+    # they should additionally use the same form as in the assignment.
+    verify_value("HEX_RANGE_10_20_OK_DEFAULT", "0x15")
+    verify_value("HEX_RANGE_10_20_OK_DEFAULT_ALTERNATE", "15")
+    verify_value("INT_RANGE_10_20_OK_DEFAULT", "15")
+
+    # hex/int symbols with no defaults but valid ranges should default to the
+    # lower end of the range if it's > 0
+    verify_value("HEX_RANGE_10_20", "0x10")
+    verify_value("HEX_RANGE_0_10", "")
+    verify_value("INT_RANGE_10_20", "10")
+    verify_value("INT_RANGE_0_10", "")
+    verify_value("INT_RANGE_NEG_10_10", "")
+
+    # User values and dependent ranges
+
+    # Avoid warnings for assigning values outside the active range
+    c.warn = False
+
+    def verify_range(sym_name, low, high, default):
+        # Verifies that all values in the range 'low'-'high' can be assigned,
+        # and that assigning values outside the range reverts the value back to
+        # 'default' (None if it should revert back to "").
+
+        is_hex = (c.syms[sym_name].type == HEX)
+
+        for i in range(low, high + 1):
+            assign_and_verify_user_value(sym_name, str(i), str(i), True)
+            if is_hex:
+                # The form of the user value should be preserved for hex
+                # symbols
+                assign_and_verify_user_value(sym_name, hex(i), hex(i), True)
+
+        # Verify that assigning a user value just outside the range causes
+        # defaults to be used
+
+        if default is None:
+            default_str = ""
+        else:
+            default_str = hex(default) if is_hex else str(default)
+
+        if is_hex:
+            too_low_str = hex(low - 1)
+            too_high_str = hex(high + 1)
+        else:
+            too_low_str = str(low - 1)
+            too_high_str = str(high + 1)
+
+        assign_and_verify_value(sym_name, too_low_str, default_str)
+        assign_and_verify_value(sym_name, too_high_str, default_str)
+
+    verify_range("HEX_RANGE_10_20_LOW_DEFAULT",  0x10, 0x20,  0x10)
+    verify_range("HEX_RANGE_10_20_HIGH_DEFAULT", 0x10, 0x20,  0x20)
+    verify_range("HEX_RANGE_10_20_OK_DEFAULT",   0x10, 0x20,  0x15)
+
+    verify_range("INT_RANGE_10_20_LOW_DEFAULT",  10,   20,    10)
+    verify_range("INT_RANGE_10_20_HIGH_DEFAULT", 10,   20,    20)
+    verify_range("INT_RANGE_10_20_OK_DEFAULT",   10,   20,    15)
+
+    verify_range("HEX_RANGE_10_20",              0x10, 0x20,  0x10)
+
+    verify_range("INT_RANGE_10_20",              10,  20,     10)
+    verify_range("INT_RANGE_0_10",               0,   10,     None)
+    verify_range("INT_RANGE_NEG_10_10",          -10, 10,     None)
+
+    # Dependent ranges
+
+    verify_value("HEX_40", "40")
+    verify_value("INT_40", "40")
+
+    c.syms["HEX_RANGE_10_20"].unset_value()
+    c.syms["INT_RANGE_10_20"].unset_value()
+    verify_value("HEX_RANGE_10_40_DEPENDENT", "0x10")
+    verify_value("INT_RANGE_10_40_DEPENDENT", "10")
+    c.syms["HEX_RANGE_10_20"].set_value("15")
+    c.syms["INT_RANGE_10_20"].set_value("15")
+    verify_value("HEX_RANGE_10_40_DEPENDENT", "0x15")
+    verify_value("INT_RANGE_10_40_DEPENDENT", "15")
+    c.unset_values()
+    verify_range("HEX_RANGE_10_40_DEPENDENT", 0x10, 0x40,  0x10)
+    verify_range("INT_RANGE_10_40_DEPENDENT", 10,   40,    10)
+
+    # Ranges and symbols defined in multiple locations
+
+    verify_value("INACTIVE_RANGE", "2")
+    verify_value("ACTIVE_RANGE", "1")
+
+
+    print("Testing defconfig_filename")
+
+    c = Kconfig("Kconfiglib/tests/empty")
+    verify(c.defconfig_filename is None,
+           "defconfig_filename should be None with no defconfig_list symbol")
+
+    c = Kconfig("Kconfiglib/tests/Kdefconfig_nonexistent")
+    verify(c.defconfig_filename is None,
+           "defconfig_filename should be None when none of the files in the "
+           "defconfig_list symbol exist")
+
+    # Referenced in Kdefconfig_existent(_but_n)
+    os.environ["FOO"] = "defconfig_2"
+
+    c = Kconfig("Kconfiglib/tests/Kdefconfig_existent_but_n")
+    verify(c.defconfig_filename is None,
+           "defconfig_filename should be None when the condition is n for all "
+           "the defaults")
+
+    c = Kconfig("Kconfiglib/tests/Kdefconfig_existent")
+    verify(c.defconfig_filename == "Kconfiglib/tests/defconfig_2",
+           "defconfig_filename should return the existing file "
+           "Kconfiglib/tests/defconfig_2")
+
+    # Should also look relative to $srctree if the specified defconfig is a
+    # relative path and can't be opened
+
+    c = Kconfig("Kconfiglib/tests/Kdefconfig_srctree")
+    verify(c.defconfig_filename == "Kconfiglib/tests/defconfig_2",
+           "defconfig_filename gave wrong file with $srctree unset")
+
+    os.environ["srctree"] = "Kconfiglib/tests"
+    c = Kconfig("Kdefconfig_srctree")
+    verify(c.defconfig_filename == "Kconfiglib/tests/sub/defconfig_in_sub",
+           "defconfig_filename gave wrong file with $srctree set")
+
+    os.environ.pop("srctree", None)
+
+
+    print("Testing mainmenu_text")
+
+    c = Kconfig("Kconfiglib/tests/empty")
+    verify(c.mainmenu_text == "Main menu",
+           "An empty Kconfig should get a default main menu prompt")
+
+    # Expanded in the mainmenu text
+    os.environ["FOO"] = "bar baz"
+    c = Kconfig("Kconfiglib/tests/Kmainmenu")
+    verify(c.mainmenu_text == "---bar baz---",
+           "Wrong mainmenu text")
+
+
+    print("Testing user_value")
+
+    # References undefined env. var. Disable warnings.
+    c = Kconfig("Kconfiglib/tests/Kmisc", warn=False)
+
+    # Avoid warnings from assigning invalid user values and assigning user
+    # values to symbols without prompts
+    c.warn = False
+
+    syms = [c.syms[name] for name in
+            ("BOOL", "TRISTATE", "STRING", "INT", "HEX")]
+
+    for sym in syms:
+        verify(sym.user_value is None,
+               "{} should not have a user value to begin with")
+
+    # Assign valid values for the types
+
+    assign_and_verify_user_value("BOOL", 0, 0, True)
+    assign_and_verify_user_value("BOOL", 2, 2, True)
+    assign_and_verify_user_value("TRISTATE", 0, 0, True)
+    assign_and_verify_user_value("TRISTATE", 1, 1, True)
+    assign_and_verify_user_value("TRISTATE", 2, 2, True)
+    assign_and_verify_user_value("STRING", "foo bar", "foo bar", True)
+    assign_and_verify_user_value("INT", "123", "123", True)
+    assign_and_verify_user_value("HEX", "0x123", "0x123", True)
+
+    # Assign invalid values for the types. They should retain their old user
+    # value.
+
+    assign_and_verify_user_value("BOOL", 1, 2, False)
+    assign_and_verify_user_value("BOOL", "foo", 2, False)
+    assign_and_verify_user_value("BOOL", "1", 2, False)
+    assign_and_verify_user_value("TRISTATE", "foo", 2, False)
+    assign_and_verify_user_value("TRISTATE", "1", 2, False)
+    assign_and_verify_user_value("STRING", 0, "foo bar", False)
+    assign_and_verify_user_value("INT", "foo", "123", False)
+    assign_and_verify_user_value("INT", 0, "123", False)
+    assign_and_verify_user_value("HEX", "foo", "0x123", False)
+    assign_and_verify_user_value("HEX", 0, "0x123", False)
+    assign_and_verify_user_value("HEX", "-0x1", "0x123", False)
+
+    for s in syms:
+        s.unset_value()
+        verify(s.user_value is None,
+               "{} should not have a user value after being reset".
+               format(s.name))
+
+
+    print("Testing is_menuconfig")
+
+    c = Kconfig("Kconfiglib/tests/Kmenuconfig")
+
+    for not_menuconfig in c.syms["NOT_MENUCONFIG_1"].nodes[0], \
+                          c.syms["NOT_MENUCONFIG_2"].nodes[0], \
+                          c.syms["MENUCONFIG_MULTI_DEF"].nodes[0], \
+                          c.syms["COMMENT_HOOK"].nodes[0].next:
+
+        verify(not not_menuconfig.is_menuconfig,
+               "'{}' should have is_menuconfig False".format(not_menuconfig))
+
+    for menuconfig in c.top_node, \
+                      c.syms["MENUCONFIG_1"].nodes[0], \
+                      c.syms["MENUCONFIG_MULTI_DEF"].nodes[1], \
+                      c.syms["MENU_HOOK"].nodes[0].next, \
+                      c.syms["CHOICE_HOOK"].nodes[0].next:
+
+        verify(menuconfig.is_menuconfig,
+               "'{}' should have is_menuconfig True".format(menuconfig))
+
+
+    print("Testing 'option env' semantics")
+
+    os.environ["ENV_VAR"] = "ENV_VAR value"
+
+    # References undefined env. var., so disable warnings
+    c = Kconfig("Kconfiglib/tests/Kmisc", warn=False)
+
+    # Verify that 'option env' is treated like a default
+    verify_value("FROM_ENV", "ENV_VAR value")
+    verify_value("FROM_ENV_MISSING", "missing")
+
+    verify_value("FROM_ENV_WEIRD", "weird")
+
+
+    print("Testing defined vs undefined symbols")
+
+    for name in "A", "B", "C", "D", "BOOL", "TRISTATE", "STRING", "INT", "HEX":
+        verify(c.syms[name].nodes,
+               "{} should be defined".format(name))
+
+    for name in "NOT_DEFINED_1", "NOT_DEFINED_2", "NOT_DEFINED_3", \
+                "NOT_DEFINED_4":
+        sym = c.syms[name]
+        verify(not c.syms[name].nodes,
+               "{} should not be defined".format(name))
+
+
+    print("Testing Symbol.choice")
+
+    for name in "A", "B", "C", "D":
+        verify(c.syms[name].choice is not None,
+               "{} should be a choice symbol".format(name))
+
+    for name in "Q1", "Q2", "Q3", "BOOL", "TRISTATE", "STRING", "INT", "HEX", \
+                "FROM_ENV", "FROM_ENV_MISSING", "NOT_DEFINED_1", \
+                "NOT_DEFINED_2", "NOT_DEFINED_3", "NOT_DEFINED_4":
+        verify(c.syms[name].choice is None,
+               "{} should not be a choice symbol".format(name))
+
+
+    print("Testing is_allnoconfig_y")
+
+    verify(not c.syms["NOT_ALLNOCONFIG_Y"].is_allnoconfig_y,
+           "NOT_ALLNOCONFIG_Y should not be allnoconfig_y")
+    verify(c.syms["ALLNOCONFIG_Y"].is_allnoconfig_y,
+           "ALLNOCONFIG_Y should be allnoconfig_y")
+
+
+    print("Testing .config reading and writing")
+
+    config_test_file = "Kconfiglib/tests/config_test"
+
+    def verify_file_contents(fname, contents):
+        with open(fname, "r") as f:
+            file_contents = f.read()
+            verify(file_contents == contents,
+                   "{} contains '{}'. Expected '{}'."
+                   .format(fname, file_contents, contents))
+
+    # Writing/reading strings with characters that need to be escaped
+
+    c = Kconfig("Kconfiglib/tests/Kescape")
+
+    # Test the default value
+    c.write_config(config_test_file + "_from_def")
+    verify_file_contents(config_test_file + "_from_def",
+                         r'''CONFIG_STRING="\"\\"''' "\n")
+    # Write our own value
+    c.syms["STRING"].set_value(r'''\"a'\\''')
+    c.write_config(config_test_file + "_from_user")
+    verify_file_contents(config_test_file + "_from_user",
+                         r'''CONFIG_STRING="\\\"a'\\\\"''' "\n")
+
+    # Read back the two configs and verify the respective values
+    c.load_config(config_test_file + "_from_def")
+    verify_value("STRING", '"\\')
+    c.load_config(config_test_file + "_from_user")
+    verify_value("STRING", r'''\"a'\\''')
+
+    # Appending values from a .config
+
+    c = Kconfig("Kconfiglib/tests/Kappend")
+
+    # Values before assigning
+    verify_value("BOOL", "n")
+    verify_value("STRING", "")
+
+    # Assign BOOL
+    c.load_config("Kconfiglib/tests/config_set_bool", replace=False)
+    verify_value("BOOL", "y")
+    verify_value("STRING", "")
+
+    # Assign STRING
+    c.load_config("Kconfiglib/tests/config_set_string", replace=False)
+    verify_value("BOOL", "y")
+    verify_value("STRING", "foo bar")
+
+    # Reset BOOL
+    c.load_config("Kconfiglib/tests/config_set_string")
+    verify_value("BOOL", "n")
+    verify_value("STRING", "foo bar")
+
+    # Loading a completely empty .config should reset values
+    c.load_config("Kconfiglib/tests/empty")
+    verify_value("STRING", "")
+
+    # An indented assignment in a .config should be ignored
+    c.load_config("Kconfiglib/tests/config_indented")
+    verify_value("IGNOREME", "y")
+
+    # Symbol order in headers and minimal configuration files should match
+    # definition order, like in .config files
+
+    c = Kconfig("Kconfiglib/tests/Korder")
+
+    c.write_autoconf(config_test_file)
+    verify_file_contents(config_test_file, """
+#define CONFIG_O 0
+#define CONFIG_R 1
+#define CONFIG_D 2
+#define CONFIG_E 3
+#define CONFIG_R2 4
+#define CONFIG_I 5
+#define CONFIG_N 6
+#define CONFIG_G 7
+"""[1:])
+
+    # Differs from defaults
+    c.syms["O"].set_value("-1")
+    c.syms["R"].set_value("-1")
+    c.syms["E"].set_value("-1")
+    c.syms["R2"].set_value("-1")
+    c.syms["N"].set_value("-1")
+    c.syms["G"].set_value("-1")
+    c.write_min_config(config_test_file)
+    verify_file_contents(config_test_file, """
+CONFIG_O=-1
+CONFIG_R=-1
+CONFIG_E=-1
+CONFIG_R2=-1
+CONFIG_N=-1
+CONFIG_G=-1
+"""[1:])
+
+    # Test header strings in configuration files and headers
+
+    os.environ["KCONFIG_CONFIG_HEADER"] = "config header from env.\n"
+    os.environ["KCONFIG_AUTOHEADER_HEADER"] = "header header from env.\n"
+
+    c = Kconfig("Kconfiglib/tests/Kheader")
+    c.write_config(config_test_file, header="config header from param\n")
+    verify_file_contents(config_test_file, """\
+config header from param
+CONFIG_FOO=y
+""")
+    c.write_min_config(config_test_file, header="min. config header from param\n")
+    verify_file_contents(config_test_file, """\
+min. config header from param
+""")
+    c.write_config(config_test_file)
+    verify_file_contents(config_test_file, """\
+config header from env.
+CONFIG_FOO=y
+""")
+    c.write_min_config(config_test_file)
+    verify_file_contents(config_test_file, """\
+config header from env.
+""")
+    c.write_autoconf(config_test_file, header="header header from param\n")
+    verify_file_contents(config_test_file, """\
+header header from param
+#define CONFIG_FOO 1
+""")
+    c.write_autoconf(config_test_file)
+    verify_file_contents(config_test_file, """\
+header header from env.
+#define CONFIG_FOO 1
+""")
+
+    del os.environ["KCONFIG_CONFIG_HEADER"]
+    del os.environ["KCONFIG_AUTOHEADER_HEADER"]
+
+
+    print("Testing Kconfig fetching and separation")
+
+    for c in Kconfig("Kconfiglib/tests/Kmisc", warn=False), \
+             Kconfig("Kconfiglib/tests/Kmisc", warn=False):
+        for item in c.syms["BOOL"], \
+                    c.syms["BOOL"].nodes[0], \
+                    c.named_choices["OPTIONAL"], \
+                    c.named_choices["OPTIONAL"].nodes[0], \
+                    c.syms["MENU_HOOK"].nodes[0].next, \
+                    c.syms["COMMENT_HOOK"].nodes[0].next:
+            verify(item.kconfig is c,
+                   ".kconfig not properly set for " + repr(item))
+
+
+    print("Testing imply semantics")
+
+    c = Kconfig("Kconfiglib/tests/Kimply")
+
+    verify_value("IMPLY_DIRECT_DEPS", "y")
+    verify_value("UNMET_DIRECT_1", "n")
+    verify_value("UNMET_DIRECT_2", "n")
+    verify_value("UNMET_DIRECT_3", "n")
+    verify_value("MET_DIRECT_1", "y")
+    verify_value("MET_DIRECT_2", "y")
+    verify_value("MET_DIRECT_3", "y")
+    verify_value("MET_DIRECT_4", "y")
+
+    verify_value("IMPLY_COND", "y")
+    verify_value("IMPLIED_N_COND", "n")
+    verify_value("IMPLIED_M_COND", "m")
+    verify_value("IMPLIED_Y_COND", "y")
+
+    verify_value("IMPLY_N_1", "n")
+    verify_value("IMPLY_N_2", "n")
+    verify_value("IMPLIED_FROM_N_1", "n")
+    verify_value("IMPLIED_FROM_N_2", "n")
+
+    verify_value("IMPLY_M", "m")
+    verify_value("IMPLIED_M", "m")
+    verify_value("IMPLIED_M_BOOL", "y")
+
+    verify_value("IMPLY_M_TO_Y", "y")
+    verify_value("IMPLIED_M_TO_Y", "y")
+
+    # Test user value semantics
+
+    # Verify that IMPLIED_TRISTATE is invalidated if the direct
+    # dependencies change
+
+    assign_and_verify("IMPLY", 2)
+    assign_and_verify("DIRECT_DEP", 2)
+    verify_value("IMPLIED_TRISTATE", 2)
+    assign_and_verify("DIRECT_DEP", 0)
+    verify_value("IMPLIED_TRISTATE", 0)
+    # Set back for later tests
+    assign_and_verify("DIRECT_DEP", 2)
+
+    # Verify that IMPLIED_TRISTATE can be set to anything when IMPLY has value
+    # n, and that it gets the value n by default (for non-imply-related
+    # reasons)
+
+    assign_and_verify("IMPLY", 0)
+    assign_and_verify("IMPLIED_TRISTATE", 0)
+    assign_and_verify("IMPLIED_TRISTATE", 1)
+    assign_and_verify("IMPLIED_TRISTATE", 2)
+    c.syms["IMPLIED_TRISTATE"].unset_value()
+    verify_value("IMPLIED_TRISTATE", "n")
+
+    # Same as above for m. Anything still goes, but m by default now.
+
+    assign_and_verify("IMPLY", 1)
+    assign_and_verify("IMPLIED_TRISTATE", 0)
+    assign_and_verify("IMPLIED_TRISTATE", 1)
+    assign_and_verify("IMPLIED_TRISTATE", 2)
+    c.syms["IMPLIED_TRISTATE"].unset_value()
+    verify_value("IMPLIED_TRISTATE", 1)
+
+    # Same as above for y. Only n and y should be accepted. m gets promoted to
+    # y. Default should be y.
+
+    assign_and_verify("IMPLY", 2)
+    assign_and_verify("IMPLIED_TRISTATE", 0)
+    assign_and_verify_value("IMPLIED_TRISTATE", 1, 2)
+    assign_and_verify("IMPLIED_TRISTATE", 2)
+    c.syms["IMPLIED_TRISTATE"].unset_value()
+    verify_value("IMPLIED_TRISTATE", 2)
+
+    # Being implied to either m or y should give a bool the value y
+
+    c.syms["IMPLY"].unset_value()
+    verify_value("IMPLIED_BOOL", 0)
+    assign_and_verify("IMPLY", 0)
+    verify_value("IMPLIED_BOOL", 0)
+    assign_and_verify("IMPLY", 1)
+    verify_value("IMPLIED_BOOL", 2)
+    assign_and_verify("IMPLY", 2)
+    verify_value("IMPLIED_BOOL", 2)
+
+    # A bool implied to m or y can take the values n and y
+
+    c.syms["IMPLY"].set_value(1)
+    assign_and_verify("IMPLIED_BOOL", 0)
+    assign_and_verify("IMPLIED_BOOL", 2)
+
+    c.syms["IMPLY"].set_value(2)
+    assign_and_verify("IMPLIED_BOOL", 0)
+    assign_and_verify("IMPLIED_BOOL", 2)
+
+
+    print("Testing choice semantics")
+
+    # Would warn for choice value symbols defined without a type, even
+    # though the type is automatically derived. This is probably more
+    # helpful than ignoring those cases, as this feature isn't used
+    # deliberately anywhere from what I've seen.
+    c = Kconfig("Kconfiglib/tests/Kchoice", warn=False)
+
+    for name in "BOOL", "BOOL_OPT", "BOOL_M", "DEFAULTS":
+        verify(c.named_choices[name].orig_type == BOOL,
+               "choice {} should have type bool".format(name))
+
+    for name in "TRISTATE", "TRISTATE_OPT", "TRISTATE_M":
+        verify(c.named_choices[name].orig_type == TRISTATE,
+               "choice {} should have type tristate".format(name))
+
+    def select_and_verify(sym):
+        choice = sym.nodes[0].parent.item
+        choice.set_value(2)
+
+        sym.set_value(2)
+
+        verify(sym.choice.selection is sym,
+               sym.name + " should be the selected symbol")
+
+        verify(choice.user_selection is sym,
+               sym.name + " should be the user selection of the choice")
+
+        verify(sym.tri_value == 2,
+               sym.name + " should have value y when selected")
+
+        verify(sym.user_value == 2,
+               sym.name + " should have user value y when selected")
+
+        for sibling in choice.syms:
+            if sibling is not sym:
+                verify(sibling.tri_value == 0,
+                       sibling.name + " should be n when not selected")
+
+    def select_and_verify_all(choice_name):
+        choice = c.named_choices[choice_name]
+
+        # Select in forward order
+        for sym in choice.syms:
+            select_and_verify(sym)
+
+        # Select in reverse order
+        for sym in reversed(choice.syms):
+            select_and_verify(sym)
+
+    def verify_mode(choice_name, no_modules_mode, modules_mode):
+        choice = c.named_choices[choice_name]
+
+        c.modules.set_value(0)
+        verify(choice.tri_value == no_modules_mode,
+               'Wrong mode for choice {} with no modules. Expected {}, got {}.'
+               .format(choice.name, no_modules_mode, choice.tri_value))
+
+        c.modules.set_value(2)
+        verify(choice.tri_value == modules_mode,
+               'Wrong mode for choice {} with modules. Expected {}, got {}.'
+               .format(choice.name, modules_mode, choice.tri_value))
+
+    verify_mode("BOOL",         2, 2)
+    verify_mode("BOOL_OPT",     0, 0)
+    verify_mode("TRISTATE",     2, 1)
+    verify_mode("TRISTATE_OPT", 0, 0)
+    verify_mode("BOOL_M",       0, 2)
+    verify_mode("TRISTATE_M",   0, 1)
+
+    # Test defaults
+
+    choice = c.named_choices["DEFAULTS"]
+
+    c.syms["TRISTATE_SYM"].set_value(0)
+    verify(choice.selection is c.syms["OPT_4"],
+           "Wrong choice default with TRISTATE_SYM = n")
+
+    c.syms["TRISTATE_SYM"].set_value(2)
+    verify(choice.selection is c.syms["OPT_2"],
+           "Wrong choice default with TRISTATE_SYM = y")
+
+    c.syms["OPT_1"].set_value(2)
+    verify(choice.selection is c.syms["OPT_1"],
+           "User selection should override defaults")
+
+    verify(c.named_choices["DEFAULTS_NOT_VISIBLE"].selection
+           is c.syms["OPT_8"],
+           "Non-visible choice symbols should cause the next default to be "
+           "considered")
+
+    # Test y mode selection
+
+    c.modules.set_value(2)
+
+    select_and_verify_all("BOOL")
+    select_and_verify_all("BOOL_OPT")
+    select_and_verify_all("TRISTATE")
+    select_and_verify_all("TRISTATE_OPT")
+    # For BOOL_M, the mode should have been promoted
+    select_and_verify_all("BOOL_M")
+
+    # Test m mode selection
+
+    c.named_choices["TRISTATE"].set_value(1)
+
+    verify(c.named_choices["TRISTATE"].tri_value == 1,
+           "TRISTATE choice should have mode m after explicit mode assignment")
+
+    assign_and_verify_value("T_1", 0, 0)
+    assign_and_verify_value("T_2", 0, 0)
+    assign_and_verify_value("T_1", 1, 1)
+    assign_and_verify_value("T_2", 1, 1)
+    assign_and_verify_value("T_1", 2, 1)
+    assign_and_verify_value("T_2", 2, 1)
+
+    # Switching to y mode should cause T_2 to become selected
+    c.named_choices["TRISTATE"].set_value(2)
+    verify_value("T_1", 0)
+    verify_value("T_2", 2)
+
+    # Verify that choices with no explicitly specified type get the type of the
+    # first contained symbol with a type
+
+    verify(c.named_choices["NO_TYPE_BOOL"].orig_type == BOOL,
+           "Expected first choice without explicit type to have type bool")
+
+    verify(c.named_choices["NO_TYPE_TRISTATE"].orig_type == TRISTATE,
+           "Expected second choice without explicit type to have type "
+           "tristate")
+
+    # Verify that symbols without a type in the choice get the type of the
+    # choice
+
+    for name in "MMT_1", "MMT_2", "MMT_4", "MMT_5":
+        verify(c.syms[name].orig_type == BOOL,
+               "Expected {} to get type bool".format(name))
+
+    verify(c.syms["MMT_3"].orig_type == TRISTATE,
+           "Expected MMT_3 to have type tristate")
+
+    # Verify that the default selection can change depending on the
+    # visibility of the choice symbols
+
+    default_with_dep_choice = c.named_choices["DEFAULT_WITH_DEP"]
+
+    verify(default_with_dep_choice.selection is c.syms["B"],
+           "Wrong choice default with unsatisfied deps on default")
+
+    c.syms["DEP"].set_value("y")
+
+    verify(default_with_dep_choice.selection is c.syms["A"],
+           "Wrong choice default with satisfied deps on default")
+
+    c.syms["DEP"].set_value("n")
+
+    verify(default_with_dep_choice.selection is c.syms["B"],
+           "Wrong choice default with unsatisfied deps on default (round two)")
+
+    # Verify that symbols in choices that depend on the preceding symbol aren't
+    # considered choice symbols
+
+    weird_choice = c.named_choices["WEIRD_SYMS"]
+
+    def verify_is_normal_choice_symbol(name):
+        sym = c.syms[name]
+        verify(sym.choice is not None and
+               sym in weird_choice.syms and
+               sym.nodes[0].parent.item is weird_choice,
+               "{} should be a normal choice symbol".format(sym.name))
+
+    def verify_is_weird_choice_symbol(name):
+        sym = c.syms[name]
+        verify(sym.choice is None and
+               sym not in weird_choice.syms,
+               "{} should be a weird (non-)choice symbol"
+               .format(sym.name))
+
+    verify_is_normal_choice_symbol("WS1")
+    verify_is_weird_choice_symbol("WS2")
+    verify_is_weird_choice_symbol("WS3")
+    verify_is_weird_choice_symbol("WS4")
+    verify_is_weird_choice_symbol("WS5")
+    verify_is_normal_choice_symbol("WS6")
+    verify_is_weird_choice_symbol("WS7")
+    verify_is_weird_choice_symbol("WS8")
+    verify_is_normal_choice_symbol("WS9")
+
+
+    print("Testing 'if' node removal")
+
+    c = Kconfig("Kconfiglib/tests/Kifremoval", warn=False)
+
+    nodes = tuple(c.node_iter())
+    verify_equal(nodes[0].item.name, "A")
+    verify_equal(nodes[1].item.name, "B")
+    verify_equal(nodes[2].item.name, "C")
+    verify_equal(nodes[3].item.name, "D")
+    verify_equal(nodes[4].prompt[0], "E")
+    verify_equal(nodes[5].prompt[0], "F")
+    verify_equal(nodes[6].prompt[0], "G")
+    verify_equal(nodes[7].item.name, "H")
+    verify_equal(nodes[8].item.name, "I")
+    verify_equal(nodes[9].item.name, "J")
+    verify(len(nodes) == 10,
+           "Wrong number of nodes after 'if' removal")
+
+
+    print("Testing multi.def. property copying")
+
+    c = Kconfig("Kconfiglib/tests/Kdepcopy", warn=False)
+
+    def verify_props(desc, props, prop_names):
+        actual = [prop[0].name for prop in props]
+        expected = prop_names.split()
+
+        verify(actual == expected,
+               "Wrong {} properties, expected '{}', got '{}'"
+               .format(desc, expected, actual))
+
+    verify_props("default", c.syms["MULTIDEF"].defaults,
+                 "A B C D E F G H I J K L M N O P Q R")
+
+    verify_props("select", c.syms["MULTIDEF"].selects,
+                 "AA BB CC DD EE FF GG HH II JJ")
+
+    verify_props("imply", c.syms["MULTIDEF"].selects,
+                 "AA BB CC DD EE FF GG HH II JJ")
+
+    verify_props("select", c.syms["MULTIDEF_CHOICE"].selects,
+                 "A B C")
+
+    verify_props("range", c.syms["MULTIDEF_RANGE"].ranges,
+                 "A B C D E F")
+
+    verify_props("default", c.choices[1].defaults,
+                 "A B C D E")
+
+
+    print("Testing dependency loop detection")
+
+    # These are all expected to raise dependency loop errors
+    for i in range(11):
+        filename = "Kconfiglib/tests/Kdeploop" + str(i)
+        try:
+            Kconfig(filename)
+        except KconfigError as e:
+            if "Dependency loop" not in str(e):
+                fail("dependency loop in {} raised wrong KconfigError"
+                     .format(filename))
+        except:
+            fail("dependency loop in {} raised wrong exception"
+                 .format(filename))
+        else:
+            fail("dependency loop in {} not detected".format(filename))
+
+    # Check the most complicated message completely
+    try:
+        Kconfig("Kconfiglib/tests/Kdeploop10")
+    except KconfigError as e:
+        verify_equal(str(e), """
+Dependency loop
+===============
+
+A (defined at Kconfiglib/tests/Kdeploop10:1), with definition...
+
+config A
+	bool
+	depends on B
+
+...depends on B (defined at Kconfiglib/tests/Kdeploop10:5), with definition...
+
+config B
+	bool
+	depends on C = 7
+
+...depends on C (defined at Kconfiglib/tests/Kdeploop10:9), with definition...
+
+config C
+	int
+	range D 8
+
+...depends on D (defined at Kconfiglib/tests/Kdeploop10:13), with definition...
+
+config D
+	int
+	default 3 if E
+	default 8
+
+...depends on E (defined at Kconfiglib/tests/Kdeploop10:18), with definition...
+
+config E
+	bool
+
+(select-related dependencies: F && G)
+
+...depends on G (defined at Kconfiglib/tests/Kdeploop10:25), with definition...
+
+config G
+	bool
+	depends on H
+
+...depends on the choice symbol H (defined at Kconfiglib/tests/Kdeploop10:32), with definition...
+
+config H
+	bool "H"
+	depends on I && <choice>
+
+...depends on the choice symbol I (defined at Kconfiglib/tests/Kdeploop10:41), with definition...
+
+config I
+	bool "I"
+	depends on <choice>
+
+...depends on <choice> (defined at Kconfiglib/tests/Kdeploop10:38), with definition...
+
+choice
+	bool "choice" if J
+
+...depends on J (defined at Kconfiglib/tests/Kdeploop10:46), with definition...
+
+config J
+	bool
+	depends on A
+
+...depends again on A (defined at Kconfiglib/tests/Kdeploop10:1)
+"""[:-1])
+    except:
+        fail("Loop detection message check raised wrong exception")
+    else:
+        fail("Loop detection message check did not raise exception")
+
+
+    print("Testing preprocessor")
+
+    os.environ["ENV_1"] = "env_1"
+    os.environ["ENV_2"] = "env_2"
+    os.environ["ENV_3"] = "env_3"
+    os.environ["ENV_4"] = "env_4"
+    os.environ["ENV_5"] = "n"
+    os.environ["ENV_6"] = "Kconfiglib/tests/empty"
+    os.environ["ENV_7"] = "env_7"
+    # We verify warnings manually
+    c = Kconfig("Kconfiglib/tests/Kpreprocess", warn_to_stderr=False)
+
+    def verify_variable(name, unexp_value, exp_value, recursive, *args):
+        var = c.variables[name]
+
+        verify(var.value == unexp_value,
+               "expected variable '{}' to have the unexpanded value '{}', had "
+               "the value '{}'".format(name, unexp_value, var.value))
+
+        if not args:
+            verify(var.expanded_value == exp_value,
+                   "expected expanded_value for {} to be '{}', was '{}'"
+                   .format(name, exp_value, var.expanded_value))
+
+        verify(var.expanded_value_w_args(*args) == exp_value,
+               "expected expanded_value_w_args() for '{}' to be '{}', was '{}'"
+               .format(name, exp_value, var.expanded_value_w_args(*args)))
+
+        verify(var.is_recursive == recursive,
+               "{} was {}, shouldn't be"
+               .format(name, "recursive" if var.is_recursive else "simple"))
+
+    verify_variable("simple-recursive", "foo", "foo", True)
+    verify_variable("simple-immediate", "bar", "bar", False)
+    verify_variable("simple-recursive-2", "baz", "baz", True)
+
+    verify_variable("whitespaced", "foo", "foo", True)
+
+    verify_variable("preserve-recursive", "foo bar", "foo bar", True)
+    verify_variable("preserve-immediate", "foo bar", "foo bar", False)
+
+    verify_variable("recursive",
+                    "$(foo) $(bar) $($(b-char)a$(z-char)) $(indir)",
+                    "abc def ghi jkl mno",
+                    True)
+
+    verify_variable("immediate", "foofoo", "foofoo", False)
+
+    verify_variable("messy-fn-res",
+                    "$($(fn-indir)-unused-arg, a  b (,) , c  d )",
+                    'surround-rev-quote " c  d " " a  b (,) " surround-rev-quote ',
+                    True)
+
+    verify_variable("special-chars-fn-res",
+                    "$(fn,$(comma)$(dollar)$(left-paren)foo$(right-paren))",
+                    '",$(foo)"',
+                    True)
+
+    verify_variable("quote", '"$(1)" "$(2)"', '"" ""', True)
+    verify_variable("quote", '"$(1)" "$(2)"', '"one" ""', True,
+                    "one")
+    verify_variable("quote", '"$(1)" "$(2)"', '"one" "two"', True,
+                    "one", "two")
+    verify_variable("quote", '"$(1)" "$(2)"', '"one" "two"', True,
+                    "one", "two", "three")
+
+    verify_str(c.syms["PRINT_ME"], r"""
+config PRINT_ME
+	string "env_1" if (FOO && BAR) || !BAZ || !QAZ
+	default "\"foo\"" if "foo \"bar\" baz" = ""
+""")
+
+    verify_str(c.syms["PRINT_ME_TOO"], r"""
+config PRINT_ME_TOO
+	bool "foo"
+	default FOOBARBAZQAZ if QAZ && QAZFOO && xxx
+""")
+
+    def verify_repr(name, s):
+        verify_equal(repr(c.variables[name]), s)
+
+    verify_repr(
+        "simple-immediate",
+        "<variable simple-immediate, immediate, value 'bar'>")
+
+    verify_repr(
+        "messy-fn-res",
+        "<variable messy-fn-res, recursive, value '$($(fn-indir)-unused-arg, a  b (,) , c  d )'>")
+
+    def verify_recursive(name):
+        try:
+            c.variables[name].expanded_value
+        except KconfigError:
+            pass
+        else:
+            fail("Expected '{}' expansion to flag recursive expansion, didn't"
+                 .format(name))
+
+    verify_recursive("rec-1")
+    # Indirectly verifies that it's not recursive
+    verify_variable("safe-fn-rec-res",
+                    "$(safe-fn-rec,safe-fn-rec-2)",
+                    "foo",
+                    True)
+    verify_recursive("unsafe-fn-rec")
+
+    verify_variable("foo-bar-baz", "$(rhs)", "value", True)
+
+    verify_variable("space-var-res", "$(foo bar)", "value", True)
+
+    verify_variable("shell-res",
+                    "$(shell,false && echo foo bar || echo baz qaz)",
+                    "baz qaz",
+                    True)
+
+    verify_variable("shell-stderr-res", "", "", False)
+
+    verify_variable("parens-res",
+                    "pre-$(shell,echo '(a,$(b-char),(c,d),e)')-post",
+                    "pre-(a,b,(c,d),e)-post",
+                    True)
+
+    verify_variable("location-res",
+                    "Kconfiglib/tests/Kpreprocess:129",
+                    "Kconfiglib/tests/Kpreprocess:129",
+                    False)
+
+    verify_variable("warning-res", "", "", False)
+    verify_variable("error-n-res", "", "", False)
+
+    try:
+        c.variables["error-y-res"].expanded_value
+    except KconfigError:
+        pass
+    else:
+        fail("expanding error-y-res didn't raise an exception")
+
+    # Check Kconfig.env_vars
+    verify_equal(c.env_vars,
+                 set(("ENV_1", "ENV_2", "ENV_3", "ENV_4", "ENV_5", "ENV_6")))
+
+    # Check that the expected warnings were generated
+    verify_equal(c.warnings, [
+        "Kconfiglib/tests/Kpreprocess:122: warning: 'echo message on stderr >&2' wrote to stderr: message on stderr",
+        "Kconfiglib/tests/Kpreprocess:134: warning: a warning"
+    ])
+
+
+    print("Testing user-defined preprocessor functions")
+
+    # Make Kconfiglib/tests/kconfigfunctions.py importable
+    sys.path.insert(0, "Kconfiglib/tests")
+
+    c = Kconfig("Kconfiglib/tests/Kuserfunctions")
+
+    verify_variable("add-zero",  "$(add)",          "0", True)
+    verify_variable("add-one",   "$(add,1)",        "1", True)
+    verify_variable("add-three", "$(add,1,-1,2,1)", "3", True)
+
+    verify_variable("one-one", "$(one,foo bar)", "onefoo barfoo bar", True)
+
+    verify_variable("one-or-more-one", "$(one-or-more,foo)", "foo + ", True)
+    verify_variable("one-or-more-three", "$(one-or-more,foo,bar,baz)",
+                    "foo + bar,baz", True)
+
+    verify_variable("location-1", "Kconfiglib/tests/Kuserfunctions:13",
+                    "Kconfiglib/tests/Kuserfunctions:13", False)
+    verify_variable("location-2", "Kconfiglib/tests/Kuserfunctions:14",
+                    "Kconfiglib/tests/Kuserfunctions:14", False)
+
+    def verify_bad_argno(name):
+        try:
+            c.variables[name].expanded_value
+        except KconfigError:
+            pass
+        else:
+            fail("Expected '{}' expansion to flag wrong number of arguments, "
+                 "didn't".format(name))
+
+    verify_bad_argno("one-zero")
+    verify_bad_argno("one-two")
+    verify_bad_argno("one-or-more-zero")
+
+    sys.path.pop(0)
+
+    # This test can fail on older Python 3.x versions, because they don't
+    # preserve dict insertion order during iteration. The output is still
+    # correct, just different.
+    if not (3, 0) <= sys.version_info <= (3, 5):
+        print("Testing KCONFIG_WARN_UNDEF")
+
+        os.environ["KCONFIG_WARN_UNDEF"] = "y"
+        c = Kconfig("Kconfiglib/tests/Kundef", warn_to_stderr=False)
+
+        verify_equal("\n".join(c.warnings), """
+warning: the int symbol INT (defined at Kconfiglib/tests/Kundef:8) has a non-int range [UNDEF_2 (undefined), 8 (undefined)]
+warning: undefined symbol UNDEF_1:
+
+- Referenced at Kconfiglib/tests/Kundef:4:
+
+config BOOL
+	bool "foo" if DEF || !UNDEF_1
+	default UNDEF_2
+
+- Referenced at Kconfiglib/tests/Kundef:19:
+
+menu "menu"
+	depends on UNDEF_1
+	visible if UNDEF_3
+warning: undefined symbol UNDEF_2:
+
+- Referenced at Kconfiglib/tests/Kundef:4:
+
+config BOOL
+	bool "foo" if DEF || !UNDEF_1
+	default UNDEF_2
+
+- Referenced at Kconfiglib/tests/Kundef:8:
+
+config INT
+	int
+	range UNDEF_2 8
+	range 5 15
+	default 10
+warning: undefined symbol UNDEF_3:
+
+- Referenced at Kconfiglib/tests/Kundef:19:
+
+menu "menu"
+	depends on UNDEF_1
+	visible if UNDEF_3
+"""[1:-1])
+
+        os.environ.pop("KCONFIG_WARN_UNDEF")
+
+
+    print("\nAll selftests passed\n" if all_passed else
+          "\nSome selftests failed\n")
+
+
+def run_compatibility_tests():
+    # Runs tests on configurations from the kernel. Tests compability with the
+    # C implementation by comparing outputs.
+
+    # Referenced inside the kernel Kconfig files.
+    #
+    # The str() makes the type of the value 'str' on both Python 2 and Python 3,
+    # which is nice for some later dictionary key sanity checks.
+
+    os.environ["KERNELVERSION"] = str(
+        subprocess.check_output("make kernelversion", shell=True)
+            .decode("utf-8").rstrip()
+    )
+
+    os.environ["CC_VERSION_TEXT"] = str(
+        subprocess.check_output("gcc --version | head -n1", shell=True)
+            .decode("utf-8").rstrip()
+    )
+
+    os.environ["srctree"] = "."
+    os.environ["CC"] = "gcc"
+    os.environ["LD"] = "ld"
+
+
+    if not os.path.exists("scripts/kconfig/conf"):
+        print("\nscripts/kconfig/conf does not exist -- running "
+              "'make allnoconfig' to build it...")
+        shell("make allnoconfig")
+
+
+    print("Running compatibility tests...\n")
+
+    test_fns = (test_defconfig,
+                # Fails for a few defconfigs due to a bug in the C tools. Will
+                # be enabled once patches get in.
+                #test_min_config,
+                test_alldefconfig,
+                test_allnoconfig,
+                test_allnoconfig_walk,
+                test_allmodconfig,
+                test_allyesconfig,
+                test_sanity)
+
+    for test_fn in test_fns:
+        # The test description is taken from the docstring of the corresponding
+        # function
+        print(textwrap.dedent(test_fn.__doc__))
+
+        for arch, srcarch in all_arch_srcarch():
+            # Referenced inside the Kconfig files
+            os.environ["ARCH"] = arch
+            os.environ["SRCARCH"] = srcarch
+
+            rm_configs()
+
+            test_fn(arch, srcarch)
+
+    if all_passed:
+        print("All selftests and compatibility tests passed")
+    else:
+        sys.exit("Some tests failed")
+
+
+def all_arch_srcarch():
+    for srcarch in os.listdir("arch"):
+        # arc and h8300 are currently broken with the C tools on linux-next as
+        # well. Perhaps they require cross-compilers to be installed.
+        #
+        # User-mode Linux has an unorthodox Kconfig setup that would require a
+        # different testing setup. Skip it too.
+        if srcarch in ("arc", "h8300", "um"):
+            continue
+
+        if os.path.exists(os.path.join("arch", srcarch, "Kconfig")):
+            yield (srcarch, srcarch)
+
+    # Some arches define additional ARCH settings with ARCH != SRCARCH
+    # (search for "Additional ARCH settings for" in the top-level Makefile)
+
+    yield ("i386", "x86")
+    yield ("x86_64", "x86")
+
+    yield ("sparc32", "sparc")
+    yield ("sparc64", "sparc")
+
+    yield ("sh64", "sh")
+
+
+def test_allnoconfig(arch, srcarch):
+    """
+    Verify that allnoconfig.py generates the same .config as
+    'make allnoconfig', for each architecture. Runs the script via
+    'make scriptconfig'.
+    """
+    shell("make scriptconfig SCRIPT=Kconfiglib/allnoconfig.py "
+          "PYTHONCMD='{}'".format(sys.executable))
+    shell("mv .config ._config")
+    shell("scripts/kconfig/conf --allnoconfig Kconfig")
+
+    compare_configs(arch)
+
+
+def test_allnoconfig_walk(arch, srcarch):
+    """
+    Verify that examples/allnoconfig_walk.py generates the same .config as
+    'make allnoconfig', for each architecture. Runs the script via
+    'make scriptconfig'.
+    """
+    shell("make scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_walk.py "
+          "PYTHONCMD='{}'".format(sys.executable))
+    shell("mv .config ._config")
+    shell("scripts/kconfig/conf --allnoconfig Kconfig")
+
+    compare_configs(arch)
+
+
+def test_allmodconfig(arch, srcarch):
+    """
+    Verify that allmodconfig.py generates the same .config as
+    'make allmodconfig', for each architecture. Runs the script via
+    'make scriptconfig'.
+    """
+    shell("make scriptconfig SCRIPT=Kconfiglib/allmodconfig.py "
+          "PYTHONCMD='{}'".format(sys.executable))
+    shell("mv .config ._config")
+    shell("scripts/kconfig/conf --allmodconfig Kconfig")
+
+    compare_configs(arch)
+
+
+def test_allyesconfig(arch, srcarch):
+    """
+    Verify that allyesconfig.py generates the same .config as
+    'make allyesconfig', for each architecture. Runs the script via
+    'make scriptconfig'.
+    """
+    shell("make scriptconfig SCRIPT=Kconfiglib/allyesconfig.py "
+          "PYTHONCMD='{}'".format(sys.executable))
+    shell("mv .config ._config")
+    shell("scripts/kconfig/conf --allyesconfig Kconfig")
+
+    compare_configs(arch)
+
+
+def test_sanity(arch, srcarch):
+    """
+    Do sanity checks on each configuration and call all public methods on all
+    symbols, choices, and menu nodes for all architectures to make sure we
+    never crash or hang.
+    """
+    print("For {}...".format(arch))
+
+    kconf = Kconfig()
+
+    for sym in kconf.defined_syms:
+        verify(sym._visited == 2,
+               "{} has broken dependency loop detection (_visited = {})"
+               .format(sym.name, sym._visited))
+
+    kconf.modules
+    kconf.defconfig_list
+    kconf.defconfig_filename
+
+    # Legacy warning functions
+    kconf.enable_redun_warnings()
+    kconf.disable_redun_warnings()
+    kconf.enable_undef_warnings()
+    kconf.disable_undef_warnings()
+    kconf.enable_warnings()
+    kconf.disable_warnings()
+    kconf.enable_stderr_warnings()
+    kconf.disable_stderr_warnings()
+
+    kconf.mainmenu_text
+    kconf.unset_values()
+
+    kconf.write_autoconf("/dev/null")
+
+    # No tempfile.TemporaryDirectory in Python 2
+    tmpdir = tempfile.mkdtemp()
+    kconf.sync_deps(os.path.join(tmpdir, "deps"))  # Create
+    kconf.sync_deps(os.path.join(tmpdir, "deps"))  # Update
+    shutil.rmtree(tmpdir)
+
+    # Python 2/3 compatible
+    for key, sym in kconf.syms.items():
+        verify(isinstance(key, str), "weird key '{}' in syms dict".format(key))
+
+        verify(not sym.is_constant, sym.name + " in 'syms' and constant")
+
+        verify(sym not in kconf.const_syms,
+               sym.name + " in both 'syms' and 'const_syms'")
+
+        for dep in sym._dependents:
+            verify(not dep.is_constant,
+                   "the constant symbol {} depends on {}"
+                   .format(dep.name, sym.name))
+
+        sym.__repr__()
+        sym.__str__()
+        sym.assignable
+        kconf.disable_warnings()
+        sym.set_value(2)
+        sym.set_value("foo")
+        sym.unset_value()
+        kconf.enable_warnings()  # Legacy warning function
+        sym.str_value
+        sym.tri_value
+        sym.type
+        sym.user_value
+        sym.visibility
+
+    for sym in kconf.defined_syms:
+        verify(sym.nodes, sym.name + " is defined but lacks menu nodes")
+
+        verify(not (sym.orig_type not in (BOOL, TRISTATE) and sym.choice),
+               sym.name + " is a choice symbol but not bool/tristate")
+
+    for key, sym in kconf.const_syms.items():
+        verify(isinstance(key, str),
+               "weird key '{}' in const_syms dict".format(key))
+
+        verify(sym.is_constant,
+               '"{}" is in const_syms but not marked constant'
+               .format(sym.name))
+
+        verify(not sym.nodes,
+               '"{}" is constant but has menu nodes'.format(sym.name))
+
+        verify(not sym._dependents,
+               '"{}" is constant but is a dependency of some symbol'
+               .format(sym.name))
+
+        verify(not sym.choice,
+               '"{}" is constant and a choice symbol'.format(sym.name))
+
+        sym.__repr__()
+        sym.__str__()
+        sym.assignable
+        kconf.disable_warnings()
+        sym.set_value(2)
+        sym.set_value("foo")
+        sym.unset_value()
+        kconf.enable_warnings()  # Legacy warning function
+        sym.str_value
+        sym.tri_value
+        sym.type
+        sym.visibility
+
+    for choice in kconf.choices:
+        for sym in choice.syms:
+            verify(sym.choice is choice,
+                   "{0} is in choice.syms but 'sym.choice' is not the choice"
+                   .format(sym.name))
+
+            verify(sym.type in (BOOL, TRISTATE),
+                   "{} is a choice symbol but is not a bool/tristate"
+                   .format(sym.name))
+
+        choice.__str__()
+        choice.__repr__()
+        choice.str_value
+        choice.tri_value
+        choice.user_value
+        choice.assignable
+        choice.selection
+        choice.type
+        choice.visibility
+
+    # Menu nodes
+
+    node = kconf.top_node
+
+    while 1:
+        # Everything else should be well exercised elsewhere
+        node.__repr__()
+        node.__str__()
+        verify(isinstance(node.item, (Symbol, Choice)) or \
+               node.item in (MENU, COMMENT),
+               "'{}' appeared as a menu item".format(node.item))
+
+        if node.list is not None:
+            node = node.list
+
+        elif node.next is not None:
+            node = node.next
+
+        else:
+            while node.parent is not None:
+                node = node.parent
+                if node.next is not None:
+                    node = node.next
+                    break
+            else:
+                break
+
+
+def test_alldefconfig(arch, srcarch):
+    """
+    Verify that alldefconfig.py generates the same .config as
+    'make alldefconfig', for each architecture. Runs the script via
+    'make scriptconfig'.
+    """
+    shell("make scriptconfig SCRIPT=Kconfiglib/alldefconfig.py "
+          "PYTHONCMD='{}'".format(sys.executable))
+    shell("mv .config ._config")
+    shell("scripts/kconfig/conf --alldefconfig Kconfig")
+
+    compare_configs(arch)
+
+
+def test_defconfig(arch, srcarch):
+    """
+    Verify that Kconfiglib generates the same .config as scripts/kconfig/conf,
+    for each architecture/defconfig pair. In obsessive mode, this test includes
+    nonsensical groupings of arches with defconfigs from other arches (every
+    arch/defconfig combination) and takes an order of magnitude longer time to
+    run.
+
+    With logging enabled, this test appends any failures to a file
+    test_defconfig_fails in the root.
+    """
+    kconf = Kconfig()
+
+    if obsessive:
+        defconfigs = []
+
+        # Collect all defconfigs. This could be done once instead, but it's
+        # a speedy operation comparatively.
+        for srcarch_ in os.listdir("arch"):
+            defconfigs.extend(defconfig_files(srcarch_))
+    else:
+        defconfigs = defconfig_files(srcarch)
+
+    # Test architecture for each defconfig
+
+    for defconfig in defconfigs:
+        rm_configs()
+
+        kconf.load_config(defconfig)
+        kconf.write_config("._config")
+        shell("scripts/kconfig/conf --defconfig='{}' Kconfig".
+              format(defconfig))
+
+        arch_defconfig_str = "  {:14}with {:60} ".format(arch, defconfig)
+
+        if equal_configs():
+            print(arch_defconfig_str + "OK")
+        else:
+            print(arch_defconfig_str + "FAIL")
+            fail()
+            if log:
+                with open("test_defconfig_fails", "a") as fail_log:
+                    fail_log.write("{} with {} did not match\n"
+                                   .format(arch, defconfig))
+
+
+def test_min_config(arch, srcarch):
+    """
+    Verify that Kconfiglib generates the same .config as 'make savedefconfig'
+    for each architecture/defconfig pair.
+    """
+    kconf = Kconfig()
+
+    if obsessive_min_config:
+        defconfigs = []
+        for srcarch_ in os.listdir("arch"):
+            defconfigs.extend(defconfig_files(srcarch_))
+    else:
+        defconfigs = defconfig_files(srcarch)
+
+    for defconfig in defconfigs:
+        rm_configs()
+
+        kconf.load_config(defconfig)
+        kconf.write_min_config("._config")
+
+        shell("cp {} .config".format(defconfig))
+
+        shell("scripts/kconfig/conf --savedefconfig=.config Kconfig")
+
+        arch_defconfig_str = "  {:14}with {:60} ".format(arch, defconfig)
+
+        if equal_configs():
+            print(arch_defconfig_str + "OK")
+        else:
+            print(arch_defconfig_str + "FAIL")
+
+
+#
+# Helper functions
+#
+
+
+def defconfig_files(srcarch):
+    # Yields a list of defconfig file filenames for a particular srcarch
+    # subdirectory (arch/<srcarch>/)
+
+    srcarch_dir = os.path.join("arch", srcarch)
+
+    # Some arches have a defconfig in the root of their arch/<arch>/ directory
+    root_defconfig = os.path.join(srcarch_dir, "defconfig")
+    if os.path.exists(root_defconfig):
+        yield root_defconfig
+
+    # Assume all files in the arch/<arch>/configs/ directory (if it exists) are
+    # configurations
+    defconfigs_dir = os.path.join(srcarch_dir, "configs")
+
+    if not os.path.isdir(defconfigs_dir):
+        return
+
+    for dirpath, _, filenames in os.walk(defconfigs_dir):
+        for filename in filenames:
+            yield os.path.join(dirpath, filename)
+
+
+def rm_configs():
+    # Delete any old ".config" (generated by the C implementation) and
+    # "._config" (generated by us), if present.
+
+    def rm_if_exists(f):
+        if os.path.exists(f):
+            os.remove(f)
+
+    rm_if_exists(".config")
+    rm_if_exists("._config")
+
+
+def compare_configs(arch):
+    if equal_configs():
+        print("{:14}OK".format(arch))
+    else:
+        print("{:14}FAIL".format(arch))
+        fail()
+
+
+def equal_configs():
+    with open(".config") as f:
+        their = f.readlines()
+
+    # Strip the header generated by 'conf'
+    i = 0
+    for line in their:
+        if not line.startswith("#") or \
+           re.match(r"# CONFIG_(\w+) is not set", line):
+            break
+        i += 1
+    their = their[i:]
+
+    try:
+        f = open("._config")
+    except EnvironmentError as e:
+        if e.errno != errno.ENOENT:
+            raise
+        print("._config not found. Did you forget to apply the Makefile patch?")
+        return False
+    else:
+        with f:
+            our = f.readlines()
+
+    if their == our:
+        return True
+
+    # Print a unified diff to help debugging
+    print("Mismatched .config's! Unified diff:")
+    sys.stdout.writelines(difflib.unified_diff(their, our, fromfile="their",
+                                               tofile="our"))
+
+    return False
+
+
+if __name__ == "__main__":
+    run_tests()

+ 40 - 8
tools/menuconfig.py → tools/menukconfig.py

@@ -243,16 +243,16 @@ def exclude_utestcases(RTT_ROOT):
             if line.find('examples/utest/testcases/Kconfig') == -1:
             if line.find('examples/utest/testcases/Kconfig') == -1:
                 f.write(line)
                 f.write(line)
 
 
-# menuconfig for Linux
+# menuconfig for Linux and Windows
 def menuconfig(RTT_ROOT):
 def menuconfig(RTT_ROOT):
+    import menuconfig
 
 
     # Exclude utestcases
     # Exclude utestcases
     exclude_utestcases(RTT_ROOT)
     exclude_utestcases(RTT_ROOT)
 
 
-    kconfig_dir = os.path.join(RTT_ROOT, 'tools', 'kconfig-frontends')
-    os.system('scons -C ' + kconfig_dir)
+    if sys.platform != 'win32':
+        touch_env()
 
 
-    touch_env()
     env_dir = get_env_dir()
     env_dir = get_env_dir()
     if isinstance(env_dir, str):
     if isinstance(env_dir, str):
         os.environ['PKGS_ROOT'] = os.path.join(env_dir, 'packages')
         os.environ['PKGS_ROOT'] = os.path.join(env_dir, 'packages')
@@ -260,8 +260,8 @@ def menuconfig(RTT_ROOT):
     fn = '.config'
     fn = '.config'
     fn_old = '.config.old'
     fn_old = '.config.old'
 
 
-    kconfig_cmd = os.path.join(RTT_ROOT, 'tools', 'kconfig-frontends', 'kconfig-mconf')
-    os.system(kconfig_cmd + ' Kconfig')
+    sys.argv = ['menuconfig', 'Kconfig']
+    menuconfig._main()
 
 
     if os.path.isfile(fn):
     if os.path.isfile(fn):
         if os.path.isfile(fn_old):
         if os.path.isfile(fn_old):
@@ -278,7 +278,7 @@ def menuconfig(RTT_ROOT):
 
 
 # guiconfig for windows and linux
 # guiconfig for windows and linux
 def guiconfig(RTT_ROOT):
 def guiconfig(RTT_ROOT):
-    import pyguiconfig
+    import guiconfig
 
 
     # Exclude utestcases
     # Exclude utestcases
     exclude_utestcases(RTT_ROOT)
     exclude_utestcases(RTT_ROOT)
@@ -294,7 +294,7 @@ def guiconfig(RTT_ROOT):
     fn_old = '.config.old'
     fn_old = '.config.old'
 
 
     sys.argv = ['guiconfig', 'Kconfig']
     sys.argv = ['guiconfig', 'Kconfig']
-    pyguiconfig._main()
+    guiconfig._main()
 
 
     if os.path.isfile(fn):
     if os.path.isfile(fn):
         if os.path.isfile(fn_old):
         if os.path.isfile(fn_old):
@@ -331,3 +331,35 @@ def guiconfig_silent(RTT_ROOT):
 
 
     # silent mode, force to make rtconfig.h
     # silent mode, force to make rtconfig.h
     mk_rtconfig(fn)
     mk_rtconfig(fn)
+
+
+def genconfig() :
+    from SCons.Script import SCons
+
+    PreProcessor = SCons.cpp.PreProcessor()
+
+    try:
+        f = open('rtconfig.h', 'r')
+        contents = f.read()
+        f.close()
+    except :
+        print("Open rtconfig.h file failed.")
+
+    PreProcessor.process_contents(contents)
+    options = PreProcessor.cpp_namespace
+
+    try:
+        f = open('.config', 'w')
+        for (opt, value) in options.items():
+            if type(value) == type(1):
+                f.write("CONFIG_%s=%d\n" % (opt, value))
+
+            if type(value) == type('') and value == '':
+                f.write("CONFIG_%s=y\n" % opt)
+            elif type(value) == type('str'):
+                f.write("CONFIG_%s=%s\n" % (opt, value))
+
+        print("Generate .config done!")
+        f.close()
+    except:
+        print("Generate .config file failed.")

+ 7 - 8
tools/options.py

@@ -121,19 +121,18 @@ def AddOptions():
                 action = 'store_true',
                 action = 'store_true',
                 default = False,
                 default = False,
                 help = 'reset the project configurations to default')
                 help = 'reset the project configurations to default')
-    AddOption('--pyconfig',
+    AddOption('--pyconfig', '--guiconfig',
                 dest = 'pyconfig',
                 dest = 'pyconfig',
                 action = 'store_true',
                 action = 'store_true',
                 default = False,
                 default = False,
                 help = 'Python GUI menuconfig for RT-Thread BSP')
                 help = 'Python GUI menuconfig for RT-Thread BSP')
-    AddOption('--pyconfig-silent',
+    AddOption('--pyconfig-silent', '--defconfig',
                 dest = 'pyconfig-silent',
                 dest = 'pyconfig-silent',
                 action = 'store_true',
                 action = 'store_true',
                 default = False,
                 default = False,
                 help = 'Don`t show pyconfig window')
                 help = 'Don`t show pyconfig window')
-    if platform.system() != 'Windows':
-        AddOption('--menuconfig',
-                    dest = 'menuconfig',
-                    action = 'store_true',
-                    default = False,
-                    help = 'make menuconfig for RT-Thread BSP')
+    AddOption('--menuconfig',
+                dest = 'menuconfig',
+                action = 'store_true',
+                default = False,
+                help = 'make menuconfig for RT-Thread BSP')

部分文件因文件數量過多而無法顯示