浏览代码

Add scons --pyconfig command for configure project.

weety 6 年之前
父节点
当前提交
937e22fc8b
共有 4 个文件被更改,包括 7380 次插入16 次删除
  1. 10 0
      tools/building.py
  2. 6159 0
      tools/kconfiglib.py
  3. 108 16
      tools/menuconfig.py
  4. 1103 0
      tools/pymenuconfig.py

+ 10 - 0
tools/building.py

@@ -335,6 +335,16 @@ def PrepareBuilding(env, root_directory, has_libcpu=False, remove_components = [
             menuconfig(Rtt_Root)
             exit(0)
 
+    AddOption('--pyconfig',
+                dest = 'pyconfig',
+                action = 'store_true',
+                default = False,
+                help = 'make menuconfig for RT-Thread BSP')
+    if GetOption('pyconfig'):
+        from menuconfig import pyconfig
+        pyconfig(Rtt_Root)
+        exit(0)
+
     configfn = GetOption('useconfig')
     if configfn:
         from menuconfig import mk_rtconfig

+ 6159 - 0
tools/kconfiglib.py

@@ -0,0 +1,6159 @@
+# Copyright (c) 2011-2018, Ulf Magnusson
+# SPDX-License-Identifier: ISC
+
+"""
+Overview
+========
+
+Kconfiglib is a Python 2/3 library for scripting and extracting information
+from Kconfig (https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt)
+configuration systems.
+
+See the homepage at https://github.com/ulfalizer/Kconfiglib for a longer
+overview.
+
+Using Kconfiglib on the Linux kernel with the Makefile targets
+==============================================================
+
+For the Linux kernel, a handy interface is provided by the
+scripts/kconfig/Makefile patch, which can be applied with either 'git am' or
+the 'patch' utility:
+
+  $ wget -qO- https://raw.githubusercontent.com/ulfalizer/Kconfiglib/master/makefile.patch | git am
+  $ wget -qO- https://raw.githubusercontent.com/ulfalizer/Kconfiglib/master/makefile.patch | patch -p1
+
+Warning: Not passing -p1 to patch will cause the wrong file to be patched.
+
+Please tell me if the patch does not apply. It should be trivial to apply
+manually, as it's just a block of text that needs to be inserted near the other
+*conf: targets in scripts/kconfig/Makefile.
+
+Look further down for a motivation for the Makefile patch and for instructions
+on how you can use Kconfiglib without it.
+
+If you do not wish to install Kconfiglib via pip, the Makefile patch is set up
+so that you can also just clone Kconfiglib into the kernel root:
+
+  $ git clone git://github.com/ulfalizer/Kconfiglib.git
+  $ git am Kconfiglib/makefile.patch  (or 'patch -p1 < Kconfiglib/makefile.patch')
+
+Warning: The directory name Kconfiglib/ is significant in this case, because
+it's added to PYTHONPATH by the new targets in makefile.patch.
+
+The targets added by the Makefile patch are described in the following
+sections.
+
+
+make [ARCH=<arch>] iscriptconfig
+--------------------------------
+
+This target gives an interactive Python prompt where a Kconfig instance has
+been preloaded and is available in 'kconf'. To change the Python interpreter
+used, pass PYTHONCMD=<executable> to make. The default is "python".
+
+To get a feel for the API, try evaluating and printing the symbols in
+kconf.defined_syms, and explore the MenuNode menu tree starting at
+kconf.top_node by following 'next' and 'list' pointers.
+
+The item contained in a menu node is found in MenuNode.item (note that this can
+be one of the constants kconfiglib.MENU and kconfiglib.COMMENT), and all
+symbols and choices have a 'nodes' attribute containing their menu nodes
+(usually only one). Printing a menu node will print its item, in Kconfig
+format.
+
+If you want to look up a symbol by name, use the kconf.syms dictionary.
+
+
+make scriptconfig SCRIPT=<script> [SCRIPT_ARG=<arg>]
+----------------------------------------------------
+
+This target runs the Python script given by the SCRIPT parameter on the
+configuration. sys.argv[1] holds the name of the top-level Kconfig file
+(currently always "Kconfig" in practice), and sys.argv[2] holds the SCRIPT_ARG
+argument, if given.
+
+See the examples/ subdirectory for example scripts.
+
+
+Using Kconfiglib without the Makefile targets
+=============================================
+
+The make targets are only needed to pick up environment variables exported from
+the Kbuild makefiles and referenced inside Kconfig files, via e.g.
+'source "arch/$(SRCARCH)/Kconfig" and '$(shell,...)'.
+
+These variables are referenced as of writing (Linux 4.18), together with sample
+values:
+
+  srctree          (.)
+  ARCH             (x86)
+  SRCARCH          (x86)
+  KERNELVERSION    (4.18.0)
+  CC               (gcc)
+  HOSTCC           (gcc)
+  HOSTCXX          (g++)
+  CC_VERSION_TEXT  (gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0)
+
+To run Kconfiglib without the Makefile patch, set the environment variables
+manually:
+
+  $ srctree=. ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` ... python(3)
+  >>> import kconfiglib
+  >>> kconf = kconfiglib.Kconfig()  # filename defaults to "Kconfig"
+
+Search the top-level Makefile for "Additional ARCH settings" to see other
+possibilities for ARCH and SRCARCH.
+
+To see a list of all referenced environment variables together with their
+values, run this code from e.g. 'make iscriptconfig':
+
+  import os
+  for var in kconf.env_vars:
+      print(var, os.environ[var])
+
+
+Intro to symbol values
+======================
+
+Kconfiglib has the same assignment semantics as the C implementation.
+
+Any symbol can be assigned a value by the user (via Kconfig.load_config() or
+Symbol.set_value()), but this user value is only respected if the symbol is
+visible, which corresponds to it (currently) being visible in the menuconfig
+interface.
+
+For symbols with prompts, the visibility of the symbol is determined by the
+condition on the prompt. Symbols without prompts are never visible, so setting
+a user value on them is pointless. A warning will be printed by default if
+Symbol.set_value() is called on a promptless symbol. Assignments to promptless
+symbols are normal within a .config file, so no similar warning will be printed
+by load_config().
+
+Dependencies from parents and 'if'/'depends on' are propagated to properties,
+including prompts, so these two configurations are logically equivalent:
+
+(1)
+
+  menu "menu"
+  depends on A
+
+  if B
+
+  config FOO
+      tristate "foo" if D
+      default y
+      depends on C
+
+  endif
+
+  endmenu
+
+(2)
+
+  menu "menu"
+  depends on A
+
+  config FOO
+      tristate "foo" if A && B && C && D
+      default y if A && B && C
+
+  endmenu
+
+In this example, A && B && C && D (the prompt condition) needs to be non-n for
+FOO to be visible (assignable). If its value is m, the symbol can only be
+assigned the value m: The visibility sets an upper bound on the value that can
+be assigned by the user, and any higher user value will be truncated down.
+
+'default' properties are independent of the visibility, though a 'default' will
+often get the same condition as the prompt due to dependency propagation.
+'default' properties are used if the symbol is not visible or has no user
+value.
+
+Symbols with no user value (or that have a user value but are not visible) and
+no (active) 'default' default to n for bool/tristate symbols, and to the empty
+string for other symbol types.
+
+'select' works similarly to symbol visibility, but sets a lower bound on the
+value of the symbol. The lower bound is determined by the value of the
+select*ing* symbol. 'select' does not respect visibility, so non-visible
+symbols can be forced to a particular (minimum) value by a select as well.
+
+For non-bool/tristate symbols, it only matters whether the visibility is n or
+non-n: m visibility acts the same as y visibility.
+
+Conditions on 'default' and 'select' work in mostly intuitive ways. If the
+condition is n, the 'default' or 'select' is disabled. If it is m, the
+'default' or 'select' value (the value of the selecting symbol) is truncated
+down to m.
+
+When writing a configuration with Kconfig.write_config(), only symbols that are
+visible, have an (active) default, or are selected will get written out (note
+that this includes all symbols that would accept user values). Kconfiglib
+matches the .config format produced by the C implementations down to the
+character. This eases testing.
+
+For a visible bool/tristate symbol FOO with value n, this line is written to
+.config:
+
+    # CONFIG_FOO is not set
+
+The point is to remember the user n selection (which might differ from the
+default value the symbol would get), while at the same sticking to the rule
+that undefined corresponds to n (.config uses Makefile format, making the line
+above a comment). When the .config file is read back in, this line will be
+treated the same as the following assignment:
+
+    CONFIG_FOO=n
+
+In Kconfiglib, the set of (currently) assignable values for a bool/tristate
+symbol appear in Symbol.assignable. For other symbol types, just check if
+sym.visibility is non-0 (non-n) to see whether the user value will have an
+effect.
+
+
+Intro to the menu tree
+======================
+
+The menu structure, as seen in e.g. menuconfig, is represented by a tree of
+MenuNode objects. The top node of the configuration corresponds to an implicit
+top-level menu, the title of which is shown at the top in the standard
+menuconfig interface. (The title is also available in Kconfig.mainmenu_text in
+Kconfiglib.)
+
+The top node is found in Kconfig.top_node. From there, you can visit child menu
+nodes by following the 'list' pointer, and any following menu nodes by
+following the 'next' pointer. Usually, a non-None 'list' pointer indicates a
+menu or Choice, but menu nodes for symbols can sometimes have a non-None 'list'
+pointer too due to submenus created implicitly from dependencies.
+
+MenuNode.item is either a Symbol or a Choice object, or one of the constants
+MENU and COMMENT. The prompt of the menu node can be found in MenuNode.prompt,
+which also holds the title for menus and comments. For Symbol and Choice,
+MenuNode.help holds the help text (if any, otherwise None).
+
+Most symbols will only have a single menu node. A symbol defined in multiple
+locations will have one menu node for each location. The list of menu nodes for
+a Symbol or Choice can be found in the Symbol/Choice.nodes attribute.
+
+Note that prompts and help texts for symbols and choices are stored in their
+menu node(s) rather than in the Symbol or Choice objects themselves. This makes
+it possible to define a symbol in multiple locations with a different prompt or
+help text in each location. To get the help text or prompt for a symbol with a
+single menu node, do sym.nodes[0].help and sym.nodes[0].prompt, respectively.
+The prompt is a (text, condition) tuple, where condition determines the
+visibility (see 'Intro to expressions' below).
+
+This organization mirrors the C implementation. MenuNode is called
+'struct menu' there, but I thought "menu" was a confusing name.
+
+It is possible to give a Choice a name and define it in multiple locations,
+hence why Choice.nodes is also a list.
+
+As a convenience, the properties added at a particular definition location are
+available on the MenuNode itself, in e.g. MenuNode.defaults. This is helpful
+when generating documentation, so that symbols/choices defined in multiple
+locations can be shown with the correct properties at each location.
+
+
+Intro to expressions
+====================
+
+Expressions can be evaluated with the expr_value() function and printed with
+the expr_str() function (these are used internally as well). Evaluating an
+expression always yields a tristate value, where n, m, and y are represented as
+0, 1, and 2, respectively.
+
+The following table should help you figure out how expressions are represented.
+A, B, C, ... are symbols (Symbol instances), NOT is the kconfiglib.NOT
+constant, etc.
+
+Expression            Representation
+----------            --------------
+A                     A
+"A"                   A (constant symbol)
+!A                    (NOT, A)
+A && B                (AND, A, B)
+A && B && C           (AND, A, (AND, B, C))
+A || B                (OR, A, B)
+A || (B && C && D)    (OR, A, (AND, B, (AND, C, D)))
+A = B                 (EQUAL, A, B)
+A != "foo"            (UNEQUAL, A, foo (constant symbol))
+A && B = C && D       (AND, A, (AND, (EQUAL, B, C), D))
+n                     Kconfig.n (constant symbol)
+m                     Kconfig.m (constant symbol)
+y                     Kconfig.y (constant symbol)
+"y"                   Kconfig.y (constant symbol)
+
+Strings like "foo" in 'default "foo"' or 'depends on SYM = "foo"' are
+represented as constant symbols, so the only values that appear in expressions
+are symbols***. This mirrors the C implementation.
+
+***For choice symbols, the parent Choice will appear in expressions as well,
+but it's usually invisible as the value interfaces of Symbol and Choice are
+identical. This mirrors the C implementation and makes different choice modes
+"just work".
+
+Manual evaluation examples:
+
+  - The value of A && B is min(A.tri_value, B.tri_value)
+
+  - The value of A || B is max(A.tri_value, B.tri_value)
+
+  - The value of !A is 2 - A.tri_value
+
+  - The value of A = B is 2 (y) if A.str_value == B.str_value, and 0 (n)
+    otherwise. Note that str_value is used here instead of tri_value.
+
+    For constant (as well as undefined) symbols, str_value matches the name of
+    the symbol. This mirrors the C implementation and explains why
+    'depends on SYM = "foo"' above works as expected.
+
+n/m/y are automatically converted to the corresponding constant symbols
+"n"/"m"/"y" (Kconfig.n/m/y) during parsing.
+
+Kconfig.const_syms is a dictionary like Kconfig.syms but for constant symbols.
+
+If a condition is missing (e.g., <cond> when the 'if <cond>' is removed from
+'default A if <cond>'), it is actually Kconfig.y. The standard __str__()
+functions just avoid printing 'if y' conditions to give cleaner output.
+
+
+Kconfig extensions
+==================
+
+Kconfiglib implements two Kconfig extensions related to 'source':
+
+'source' with relative path
+---------------------------
+
+Kconfiglib supports a custom 'rsource' statement that sources Kconfig files
+with a path relative to directory of the Kconfig file containing the 'rsource'
+statement, instead of relative to the project root. This extension is not
+supported by Linux kernel tools as of writing.
+
+Consider following directory tree:
+
+  Project
+  +--Kconfig
+  |
+  +--src
+     +--Kconfig
+     |
+     +--SubSystem1
+        +--Kconfig
+        |
+        +--ModuleA
+           +--Kconfig
+
+In this example, assume that src/SubSystem1/Kconfig wants to source
+src/SubSystem1/ModuleA/Kconfig.
+
+With 'source', the following statement would be used:
+
+  source "src/SubSystem1/ModuleA/Kconfig"
+
+Using 'rsource', it can be rewritten as:
+
+  rsource "ModuleA/Kconfig"
+
+If an absolute path is given to 'rsource', it acts the same as 'source'.
+
+'rsource' can be used to create "position-independent" Kconfig trees that can
+be moved around freely.
+
+
+Globbed sourcing
+----------------
+
+'source' and 'rsource' accept glob patterns, sourcing all matching Kconfig
+files. They require at least one matching file, throwing a KconfigError
+otherwise.
+
+For example, the following statement might source sub1/foofoofoo and
+sub2/foobarfoo:
+
+  source "sub[12]/foo*foo"
+
+The glob patterns accepted are the same as for the standard glob.glob()
+function.
+
+Two additional statements are provided for cases where it's acceptable for a
+pattern to match no files: 'osource' and 'orsource' (the o is for "optional").
+
+For example, the following statements will be no-ops if neither "foo" nor any
+files matching "bar*" exist:
+
+  osource "foo"
+  osource "bar*"
+
+'orsource' does a relative optional source.
+
+'source' and 'osource' are analogous to 'include' and '-include' in Make.
+
+
+Feedback
+========
+
+Send bug reports, suggestions, and questions to ulfalizer a.t Google's email
+service, or open a ticket on the GitHub page.
+"""
+import errno
+import glob
+import os
+import platform
+import re
+import subprocess
+import sys
+import textwrap
+
+# File layout:
+#
+# Public classes
+# Public functions
+# Internal functions
+# Public global constants
+# Internal global constants
+
+# Line length: 79 columns
+
+#
+# Public classes
+#
+
+class Kconfig(object):
+    """
+    Represents a Kconfig configuration, e.g. for x86 or ARM. This is the set of
+    symbols, choices, and menu nodes appearing in the configuration. Creating
+    any number of Kconfig objects (including for different architectures) is
+    safe. Kconfiglib doesn't keep any global state.
+
+    The following attributes are available. They should be treated as
+    read-only, and some are implemented through @property magic.
+
+    syms:
+      A dictionary with all symbols in the configuration, indexed by name. Also
+      includes all symbols that are referenced in expressions but never
+      defined, except for constant (quoted) symbols.
+
+      Undefined symbols can be recognized by Symbol.nodes being empty -- see
+      the 'Intro to the menu tree' section in the module docstring.
+
+    const_syms:
+      A dictionary like 'syms' for constant (quoted) symbols
+
+    named_choices:
+      A dictionary like 'syms' for named choices (choice FOO)
+
+    defined_syms:
+      A list with all defined symbols, in the same order as they appear in the
+      Kconfig files. Symbols defined in multiple locations appear multiple
+      times.
+
+      Note: You probably want to use 'unique_defined_syms' instead. This
+      attribute is mostly maintained for backwards compatibility.
+
+    unique_defined_syms:
+      A list like 'defined_syms', but with duplicates removed. Just the first
+      instance is kept for symbols defined in multiple locations. Kconfig order
+      is preserved otherwise.
+
+      Using this attribute instead of 'defined_syms' can save work, and
+      automatically gives reasonable behavior when writing configuration output
+      (symbols defined in multiple locations only generate output once, while
+      still preserving Kconfig order for readability).
+
+    choices:
+      A list with all choices, in the same order as they appear in the Kconfig
+      files.
+
+      Note: You probably want to use 'unique_choices' instead. This attribute
+      is mostly maintained for backwards compatibility.
+
+    unique_choices:
+      Analogous to 'unique_defined_syms', for choices. Named choices can have
+      multiple definition locations.
+
+    menus:
+      A list with all menus, in the same order as they appear in the Kconfig
+      files
+
+    comments:
+      A list with all comments, in the same order as they appear in the Kconfig
+      files
+
+    kconfig_filenames:
+      A list with the filenames of all Kconfig files included in the
+      configuration, relative to $srctree (or relative to the current directory
+      if $srctree isn't set).
+
+      The files are listed in the order they are source'd, starting with the
+      top-level Kconfig file. If a file is source'd multiple times, it will
+      appear multiple times. Use set() to get unique filenames.
+
+      Note: Using this for incremental builds is redundant. Kconfig.sync_deps()
+      already indirectly catches any file modifications that change the
+      configuration output.
+
+    env_vars:
+      A set() with the names of all environment variables referenced in the
+      Kconfig files.
+
+      Only environment variables referenced with the preprocessor $(FOO) syntax
+      will be registered. The older $FOO syntax is only supported for backwards
+      compatibility.
+
+      Also note that $(FOO) won't be registered unless the environment variable
+      $FOO is actually set. If it isn't, $(FOO) is an expansion of an unset
+      preprocessor variable (which gives the empty string).
+
+      Another gotcha is that environment variables referenced in the values of
+      recursively expanded preprocessor variables (those defined with =) will
+      only be registered if the variable is actually used (expanded) somewhere.
+
+      The note from the 'kconfig_filenames' documentation applies here too.
+
+    n/m/y:
+      The predefined constant symbols n/m/y. Also available in const_syms.
+
+    modules:
+      The Symbol instance for the modules symbol. Currently hardcoded to
+      MODULES, which is backwards compatible. Kconfiglib will warn if
+      'option modules' is set on some other symbol. Tell me if you need proper
+      'option modules' support.
+
+      'modules' is never None. If the MODULES symbol is not explicitly defined,
+      its tri_value will be 0 (n), as expected.
+
+      A simple way to enable modules is to do 'kconf.modules.set_value(2)'
+      (provided the MODULES symbol is defined and visible). Modules are
+      disabled by default in the kernel Kconfig files as of writing, though
+      nearly all defconfig files enable them (with 'CONFIG_MODULES=y').
+
+    defconfig_list:
+      The Symbol instance for the 'option defconfig_list' symbol, or None if no
+      defconfig_list symbol exists. The defconfig filename derived from this
+      symbol can be found in Kconfig.defconfig_filename.
+
+    defconfig_filename:
+      The filename given by the defconfig_list symbol. This is taken from the
+      first 'default' with a satisfied condition where the specified file
+      exists (can be opened for reading). If a defconfig file foo/defconfig is
+      not found and $srctree was set when the Kconfig was created,
+      $srctree/foo/defconfig is looked up as well.
+
+      'defconfig_filename' is None if either no defconfig_list symbol exists,
+      or if the defconfig_list symbol has no 'default' with a satisfied
+      condition that specifies a file that exists.
+
+      Gotcha: scripts/kconfig/Makefile might pass --defconfig=<defconfig> to
+      scripts/kconfig/conf when running e.g. 'make defconfig'. This option
+      overrides the defconfig_list symbol, meaning defconfig_filename might not
+      always match what 'make defconfig' would use.
+
+    top_node:
+      The menu node (see the MenuNode class) of the implicit top-level menu.
+      Acts as the root of the menu tree.
+
+    mainmenu_text:
+      The prompt (title) of the top menu (top_node). Defaults to "Main menu".
+      Can be changed with the 'mainmenu' statement (see kconfig-language.txt).
+
+    variables:
+      A dictionary with all preprocessor variables, indexed by name. See the
+      Variable class.
+
+    warnings:
+      A list of strings containing all warnings that have been generated. This
+      allows flexibility in how warnings are printed and processed.
+
+      See the 'warn_to_stderr' parameter to Kconfig.__init__() and the
+      Kconfig.enable/disable_stderr_warnings() functions as well. Note that
+      warnings still get added to Kconfig.warnings when 'warn_to_stderr' is
+      True.
+
+      Just as for warnings printed to stderr, only optional warnings that are
+      enabled will get added to Kconfig.warnings. See the various
+      Kconfig.enable/disable_*_warnings() functions.
+
+    srctree:
+      The value of the $srctree environment variable when the configuration was
+      loaded, or the empty string if $srctree wasn't set. This gives nice
+      behavior with os.path.join(), which treats "" as the current directory,
+      without adding "./".
+
+      Kconfig files are looked up relative to $srctree (unless absolute paths
+      are used), and .config files are looked up relative to $srctree if they
+      are not found in the current directory. This is used to support
+      out-of-tree builds. The C tools use this environment variable in the same
+      way.
+
+      Changing $srctree after creating the Kconfig instance has no effect. Only
+      the value when the configuration is loaded matters. This avoids surprises
+      if multiple configurations are loaded with different values for $srctree.
+
+    config_prefix:
+      The value of the $CONFIG_ environment variable when the configuration was
+      loaded. This is the prefix used (and expected) on symbol names in .config
+      files and C headers. Defaults to "CONFIG_". Used in the same way in the C
+      tools.
+
+      Like for srctree, only the value of $CONFIG_ when the configuration is
+      loaded matters.
+    """
+    __slots__ = (
+        "_encoding",
+        "_functions",
+        "_set_match",
+        "_unset_match",
+        "_warn_for_no_prompt",
+        "_warn_for_redun_assign",
+        "_warn_for_undef_assign",
+        "_warn_to_stderr",
+        "_warnings_enabled",
+        "choices",
+        "comments",
+        "config_prefix",
+        "const_syms",
+        "defconfig_list",
+        "defined_syms",
+        "env_vars",
+        "kconfig_filenames",
+        "m",
+        "mainmenu_text",
+        "menus",
+        "modules",
+        "n",
+        "named_choices",
+        "srctree",
+        "syms",
+        "top_node",
+        "unique_choices",
+        "unique_defined_syms",
+        "variables",
+        "warnings",
+        "y",
+
+        # Parsing-related
+        "_parsing_kconfigs",
+        "_file",
+        "_filename",
+        "_linenr",
+        "_include_path",
+        "_filestack",
+        "_line",
+        "_saved_line",
+        "_tokens",
+        "_tokens_i",
+        "_has_tokens",
+    )
+
+    #
+    # Public interface
+    #
+
+    def __init__(self, filename="Kconfig", warn=True, warn_to_stderr=True,
+                 encoding="utf-8"):
+        """
+        Creates a new Kconfig object by parsing Kconfig files. Raises
+        KconfigError on syntax errors. Note that Kconfig files are not the same
+        as .config files (which store configuration symbol values).
+
+        If the environment variable KCONFIG_STRICT is set to "y", warnings will
+        be generated for all references to undefined symbols within Kconfig
+        files. The reason this isn't the default is that some projects (e.g.
+        the Linux kernel) use multiple Kconfig trees (one per architecture)
+        with many shared Kconfig files, leading to some safe references to
+        undefined symbols.
+
+        KCONFIG_STRICT relies on literal hex values being prefixed with 0x/0X.
+        They are indistinguishable from references to undefined symbols
+        otherwise.
+
+        KCONFIG_STRICT might enable other warnings that depend on there being
+        just a single Kconfig tree in the future.
+
+        filename (default: "Kconfig"):
+          The Kconfig file to load. For the Linux kernel, you'll want "Kconfig"
+          from the top-level directory, as environment variables will make sure
+          the right Kconfig is included from there (arch/$SRCARCH/Kconfig as of
+          writing).
+
+          If $srctree is set, 'filename' will be looked up relative to it.
+          $srctree is also used to look up source'd files within Kconfig files.
+          See the class documentation.
+
+          If you are using Kconfiglib via 'make scriptconfig', the filename of
+          the base base Kconfig file will be in sys.argv[1]. It's currently
+          always "Kconfig" in practice.
+
+        warn (default: True):
+          True if warnings related to this configuration should be generated.
+          This can be changed later with Kconfig.enable/disable_warnings(). It
+          is provided as a constructor argument since warnings might be
+          generated during parsing.
+
+          See the other Kconfig.enable_*_warnings() functions as well, which
+          enable or suppress certain warnings when warnings are enabled.
+
+          All generated warnings are added to the Kconfig.warnings list. See
+          the class documentation.
+
+        warn_to_stderr (default: True):
+          True if warnings should be printed to stderr in addition to being
+          added to Kconfig.warnings.
+
+          This can be changed later with
+          Kconfig.enable/disable_stderr_warnings().
+
+        encoding (default: "utf-8"):
+          The encoding to use when reading and writing files. If None, the
+          encoding specified in the current locale will be used.
+
+          The "utf-8" default avoids exceptions on systems that are configured
+          to use the C locale, which implies an ASCII encoding.
+
+          This parameter has no effect on Python 2, due to implementation
+          issues (regular strings turning into Unicode strings, which are
+          distinct in Python 2). Python 2 doesn't decode regular strings
+          anyway.
+
+          Related PEP: https://www.python.org/dev/peps/pep-0538/
+        """
+        self.srctree = os.environ.get("srctree", "")
+        self.config_prefix = os.environ.get("CONFIG_", "CONFIG_")
+
+        # Regular expressions for parsing .config files
+        self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)")
+        self._unset_match = \
+            _re_match(r"# {}([^ ]+) is not set".format(self.config_prefix))
+
+
+        self.warnings = []
+
+        self._warnings_enabled = warn
+        self._warn_to_stderr = warn_to_stderr
+        self._warn_for_undef_assign = False
+        self._warn_for_redun_assign = True
+
+
+        self._encoding = encoding
+
+
+        self.syms = {}
+        self.const_syms = {}
+        self.defined_syms = []
+
+        self.named_choices = {}
+        self.choices = []
+
+        self.menus = []
+        self.comments = []
+
+        for nmy in "n", "m", "y":
+            sym = Symbol()
+            sym.kconfig = self
+            sym.name = nmy
+            sym.is_constant = True
+            sym.orig_type = TRISTATE
+            sym._cached_tri_val = STR_TO_TRI[nmy]
+
+            self.const_syms[nmy] = sym
+
+        self.n = self.const_syms["n"]
+        self.m = self.const_syms["m"]
+        self.y = self.const_syms["y"]
+
+        # Make n/m/y well-formed symbols
+        for nmy in "n", "m", "y":
+            sym = self.const_syms[nmy]
+            sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n
+
+
+        # Maps preprocessor variables names to Variable instances
+        self.variables = {}
+
+        # Predefined preprocessor functions, with min/max number of arguments
+        self._functions = {
+            "info":       (_info_fn,       1, 1),
+            "error-if":   (_error_if_fn,   2, 2),
+            "filename":   (_filename_fn,   0, 0),
+            "lineno":     (_lineno_fn,     0, 0),
+            "shell":      (_shell_fn,      1, 1),
+            "warning-if": (_warning_if_fn, 2, 2),
+        }
+
+
+        # This is used to determine whether previously unseen symbols should be
+        # registered. They shouldn't be if we parse expressions after parsing,
+        # as part of Kconfig.eval_string().
+        self._parsing_kconfigs = True
+
+        self.modules = self._lookup_sym("MODULES")
+        self.defconfig_list = None
+
+        self.top_node = MenuNode()
+        self.top_node.kconfig = self
+        self.top_node.item = MENU
+        self.top_node.is_menuconfig = True
+        self.top_node.visibility = self.y
+        self.top_node.prompt = ("Main menu", self.y)
+        self.top_node.parent = None
+        self.top_node.dep = self.y
+        self.top_node.filename = filename
+        self.top_node.linenr = 1
+        self.top_node.include_path = ()
+
+        # Parse the Kconfig files
+
+        # Not used internally. Provided as a convenience.
+        self.kconfig_filenames = [filename]
+        self.env_vars = set()
+
+        # These implement a single line of "unget" for the parser
+        self._saved_line = None
+        self._has_tokens = False
+
+        # Keeps track of the location in the parent Kconfig files. Kconfig
+        # files usually source other Kconfig files. See _enter_file().
+        self._filestack = []
+        self._include_path = ()
+
+        # The current parsing location
+        self._filename = filename
+        self._linenr = 0
+
+        # Open the top-level Kconfig file
+        try:
+            self._file = self._open(os.path.join(self.srctree, filename), "r")
+        except IOError as e:
+            if self.srctree:
+                print(textwrap.fill(
+                    _INIT_SRCTREE_NOTE.format(self.srctree), 80))
+            raise
+
+        try:
+            # Parse everything
+            self._parse_block(None, self.top_node, self.top_node)
+        except UnicodeDecodeError as e:
+            _decoding_error(e, self._filename)
+
+        # Close the top-level Kconfig file
+        self._file.close()
+
+        self.top_node.list = self.top_node.next
+        self.top_node.next = None
+
+        self._parsing_kconfigs = False
+
+        self.unique_defined_syms = _ordered_unique(self.defined_syms)
+        self.unique_choices = _ordered_unique(self.choices)
+
+        # Do various post-processing of the menu tree
+        self._finalize_tree(self.top_node, self.y)
+
+
+        # Do sanity checks. Some of these depend on everything being
+        # finalized.
+
+        for sym in self.unique_defined_syms:
+            _check_sym_sanity(sym)
+
+        for choice in self.unique_choices:
+            _check_choice_sanity(choice)
+
+        if os.environ.get("KCONFIG_STRICT") == "y":
+            self._check_undef_syms()
+
+
+        # Build Symbol._dependents for all symbols and choices
+        self._build_dep()
+
+        # Check for dependency loops
+        for sym in self.unique_defined_syms:
+            _check_dep_loop_sym(sym, False)
+
+        # Add extra dependencies from choices to choice symbols that get
+        # awkward during dependency loop detection
+        self._add_choice_deps()
+
+
+        self._warn_for_no_prompt = True
+
+        self.mainmenu_text = self.top_node.prompt[0]
+
+    @property
+    def defconfig_filename(self):
+        """
+        See the class documentation.
+        """
+        if self.defconfig_list:
+            for filename, cond in self.defconfig_list.defaults:
+                if expr_value(cond):
+                    try:
+                        with self._open_config(filename.str_value) as f:
+                            return f.name
+                    except IOError:
+                        continue
+
+        return None
+
+    def load_config(self, filename, replace=True):
+        """
+        Loads symbol values from a file in the .config format. Equivalent to
+        calling Symbol.set_value() to set each of the values.
+
+        "# CONFIG_FOO is not set" within a .config file sets the user value of
+        FOO to n. The C tools work the same way.
+
+        The Symbol.user_value attribute can be inspected afterwards to see what
+        value the symbol was assigned in the .config file (if any). The user
+        value might differ from Symbol.str/tri_value if there are unsatisfied
+        dependencies.
+
+        filename:
+          The file to load. Respects $srctree if set (see the class
+          documentation).
+
+        replace (default: True):
+          True if all existing user values should be cleared before loading the
+          .config.
+        """
+        # Disable the warning about assigning to symbols without prompts. This
+        # is normal and expected within a .config file.
+        self._warn_for_no_prompt = False
+
+        # This stub only exists to make sure _warn_for_no_prompt gets reenabled
+        try:
+            self._load_config(filename, replace)
+        except UnicodeDecodeError as e:
+            _decoding_error(e, filename)
+        finally:
+            self._warn_for_no_prompt = True
+
+    def _load_config(self, filename, replace):
+        with self._open_config(filename) as f:
+            if replace:
+                # If we're replacing the configuration, keep track of which
+                # symbols and choices got set so that we can unset the rest
+                # later. This avoids invalidating everything and is faster.
+                # Another benefit is that invalidation must be rock solid for
+                # it to work, making it a good test.
+
+                for sym in self.unique_defined_syms:
+                    sym._was_set = False
+
+                for choice in self.unique_choices:
+                    choice._was_set = False
+
+            # Small optimizations
+            set_match = self._set_match
+            unset_match = self._unset_match
+            syms = self.syms
+
+            for linenr, line in enumerate(f, 1):
+                # The C tools ignore trailing whitespace
+                line = line.rstrip()
+
+                match = set_match(line)
+                if match:
+                    name, val = match.groups()
+                    if name not in syms:
+                        self._warn_undef_assign_load(name, val, filename,
+                                                     linenr)
+                        continue
+
+                    sym = syms[name]
+                    if not sym.nodes:
+                        self._warn_undef_assign_load(name, val, filename,
+                                                     linenr)
+                        continue
+
+                    if sym.orig_type in (BOOL, TRISTATE):
+                        # The C implementation only checks the first character
+                        # to the right of '=', for whatever reason
+                        if not ((sym.orig_type is BOOL and
+                                 val.startswith(("n", "y"))) or \
+                                (sym.orig_type is TRISTATE and
+                                 val.startswith(("n", "m", "y")))):
+                            self._warn("'{}' is not a valid value for the {} "
+                                       "symbol {}. Assignment ignored."
+                                       .format(val, TYPE_TO_STR[sym.orig_type],
+                                               _name_and_loc(sym)),
+                                       filename, linenr)
+                            continue
+
+                        val = val[0]
+
+                        if sym.choice and val != "n":
+                            # During .config loading, we infer the mode of the
+                            # choice from the kind of values that are assigned
+                            # to the choice symbols
+
+                            prev_mode = sym.choice.user_value
+                            if prev_mode is not None and \
+                               TRI_TO_STR[prev_mode] != val:
+
+                                self._warn("both m and y assigned to symbols "
+                                           "within the same choice",
+                                           filename, linenr)
+
+                            # Set the choice's mode
+                            sym.choice.set_value(val)
+
+                    elif sym.orig_type is STRING:
+                        match = _conf_string_match(val)
+                        if not match:
+                            self._warn("malformed string literal in "
+                                       "assignment to {}. Assignment ignored."
+                                       .format(_name_and_loc(sym)),
+                                       filename, linenr)
+                            continue
+
+                        val = unescape(match.group(1))
+
+                else:
+                    match = unset_match(line)
+                    if not match:
+                        # Print a warning for lines that match neither
+                        # set_match() nor unset_match() and that are not blank
+                        # lines or comments. 'line' has already been
+                        # rstrip()'d, so blank lines show up as "" here.
+                        if line and not line.lstrip().startswith("#"):
+                            self._warn("ignoring malformed line '{}'"
+                                       .format(line),
+                                       filename, linenr)
+
+                        continue
+
+                    name = match.group(1)
+                    if name not in syms:
+                        self._warn_undef_assign_load(name, "n", filename,
+                                                     linenr)
+                        continue
+
+                    sym = syms[name]
+                    if sym.orig_type not in (BOOL, TRISTATE):
+                        continue
+
+                    val = "n"
+
+                # Done parsing the assignment. Set the value.
+
+                if sym._was_set:
+                    # Use strings for bool/tristate user values in the warning
+                    if sym.orig_type in (BOOL, TRISTATE):
+                        display_user_val = TRI_TO_STR[sym.user_value]
+                    else:
+                        display_user_val = sym.user_value
+
+                    warn_msg = '{} set more than once. Old value: "{}", new value: "{}".'.format(
+                        _name_and_loc(sym), display_user_val, val
+                    )
+
+                    if display_user_val == val:
+                        self._warn_redun_assign(warn_msg, filename, linenr)
+                    else:
+                        self._warn(             warn_msg, filename, linenr)
+
+                sym.set_value(val)
+
+        if replace:
+            # If we're replacing the configuration, unset the symbols that
+            # didn't get set
+
+            for sym in self.unique_defined_syms:
+                if not sym._was_set:
+                    sym.unset_value()
+
+            for choice in self.unique_choices:
+                if not choice._was_set:
+                    choice.unset_value()
+
+    def write_autoconf(self, filename,
+                       header="/* Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib) */\n"):
+        r"""
+        Writes out symbol values as a C header file, matching the format used
+        by include/generated/autoconf.h in the kernel.
+
+        The ordering of the #defines matches the one generated by
+        write_config(). The order in the C implementation depends on the hash
+        table implementation as of writing, and so won't match.
+
+        filename:
+          Self-explanatory.
+
+        header (default: "/* Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib) */\n"):
+          Text that will be inserted verbatim at the beginning of the file. You
+          would usually want it enclosed in '/* */' to make it a C comment,
+          and include a final terminating newline.
+        """
+        with self._open(filename, "w") as f:
+            f.write(header)
+
+            for sym in self.unique_defined_syms:
+                # Note: _write_to_conf is determined when the value is
+                # calculated. This is a hidden function call due to
+                # property magic.
+                val = sym.str_value
+                if sym._write_to_conf:
+                    if sym.orig_type in (BOOL, TRISTATE):
+                        if val != "n":
+                            f.write("#define {}{}{} 1\n"
+                                    .format(self.config_prefix, sym.name,
+                                            "_MODULE" if val == "m" else ""))
+
+                    elif sym.orig_type is STRING:
+                        f.write('#define {}{} "{}"\n'
+                                .format(self.config_prefix, sym.name,
+                                        escape(val)))
+
+                    elif sym.orig_type in (INT, HEX):
+                        if sym.orig_type is HEX and \
+                           not val.startswith(("0x", "0X")):
+                            val = "0x" + val
+
+                        f.write("#define {}{} {}\n"
+                                .format(self.config_prefix, sym.name, val))
+
+                    else:
+                        _internal_error("Internal error while creating C "
+                                        'header: unknown type "{}".'
+                                        .format(sym.orig_type))
+
+    def write_config(self, filename,
+                     header="# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n"):
+        r"""
+        Writes out symbol values in the .config format. The format matches the
+        C implementation, including ordering.
+
+        Symbols appear in the same order in generated .config files as they do
+        in the Kconfig files. For symbols defined in multiple locations, a
+        single assignment is written out corresponding to the first location
+        where the symbol is defined.
+
+        See the 'Intro to symbol values' section in the module docstring to
+        understand which symbols get written out.
+
+        filename:
+          Self-explanatory.
+
+        header (default: "# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n"):
+          Text that will be inserted verbatim at the beginning of the file. You
+          would usually want each line to start with '#' to make it a comment,
+          and include a final terminating newline.
+        """
+        with self._open(filename, "w") as f:
+            f.write(header)
+
+            for node in self.node_iter(unique_syms=True):
+                item = node.item
+
+                if isinstance(item, Symbol):
+                    f.write(item.config_string)
+
+                elif expr_value(node.dep) and \
+                     ((item is MENU and expr_value(node.visibility)) or
+                       item is COMMENT):
+
+                    f.write("\n#\n# {}\n#\n".format(node.prompt[0]))
+
+    def write_min_config(self, filename,
+                         header="# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n"):
+        """
+        Writes out a "minimal" configuration file, omitting symbols whose value
+        matches their default value. The format matches the one produced by
+        'make savedefconfig'.
+
+        The resulting configuration file is incomplete, but a complete
+        configuration can be derived from it by loading it. Minimal
+        configuration files can serve as a more manageable configuration format
+        compared to a "full" .config file, especially when configurations files
+        are merged or edited by hand.
+
+        filename:
+          Self-explanatory.
+
+        header (default: "# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n"):
+          Text that will be inserted verbatim at the beginning of the file. You
+          would usually want each line to start with '#' to make it a comment,
+          and include a final terminating newline.
+        """
+        with self._open(filename, "w") as f:
+            f.write(header)
+
+            for sym in self.unique_defined_syms:
+                # Skip symbols that cannot be changed. Only check
+                # non-choice symbols, as selects don't affect choice
+                # symbols.
+                if not sym.choice and \
+                   sym.visibility <= expr_value(sym.rev_dep):
+                    continue
+
+                # Skip symbols whose value matches their default
+                if sym.str_value == sym._str_default():
+                    continue
+
+                # Skip symbols that would be selected by default in a
+                # choice, unless the choice is optional or the symbol type
+                # isn't bool (it might be possible to set the choice mode
+                # to n or the symbol to m in those cases).
+                if sym.choice and \
+                   not sym.choice.is_optional and \
+                   sym.choice._get_selection_from_defaults() is sym and \
+                   sym.orig_type is BOOL and \
+                   sym.tri_value == 2:
+                    continue
+
+                f.write(sym.config_string)
+
+    def sync_deps(self, path):
+        """
+        Creates or updates a directory structure that can be used to avoid
+        doing a full rebuild whenever the configuration is changed, mirroring
+        include/config/ in the kernel.
+
+        This function is intended to be called during each build, before
+        compiling source files that depend on configuration symbols.
+
+        path:
+          Path to directory
+
+        sync_deps(path) does the following:
+
+          1. If the directory <path> does not exist, it is created.
+
+          2. If <path>/auto.conf exists, old symbol values are loaded from it,
+             which are then compared against the current symbol values. If a
+             symbol has changed value (would generate different output in
+             autoconf.h compared to before), the change is signaled by
+             touch'ing a file corresponding to the symbol.
+
+             The first time sync_deps() is run on a directory, <path>/auto.conf
+             won't exist, and no old symbol values will be available. This
+             logically has the same effect as updating the entire
+             configuration.
+
+             The path to a symbol's file is calculated from the symbol's name
+             by replacing all '_' with '/' and appending '.h'. For example, the
+             symbol FOO_BAR_BAZ gets the file <path>/foo/bar/baz.h, and FOO
+             gets the file <path>/foo.h.
+
+             This scheme matches the C tools. The point is to avoid having a
+             single directory with a huge number of files, which the underlying
+             filesystem might not handle well.
+
+          3. A new auto.conf with the current symbol values is written, to keep
+             track of them for the next build.
+
+
+        The last piece of the puzzle is knowing what symbols each source file
+        depends on. Knowing that, dependencies can be added from source files
+        to the files corresponding to the symbols they depends on. The source
+        file will then get recompiled (only) when the symbol value changes
+        (provided sync_deps() is run first during each build).
+
+        The tool in the kernel that extracts symbol dependencies from source
+        files is scripts/basic/fixdep.c. Missing symbol files also correspond
+        to "not changed", which fixdep deals with by using the $(wildcard) Make
+        function when adding symbol prerequisites to source files.
+
+        In case you need a different scheme for your project, the sync_deps()
+        implementation can be used as a template."""
+        if not os.path.exists(path):
+            os.mkdir(path, 0o755)
+
+        # This setup makes sure that at least the current working directory
+        # gets reset if things fail
+        prev_dir = os.getcwd()
+        try:
+            # cd'ing into the symbol file directory simplifies
+            # _sync_deps() and saves some work
+            os.chdir(path)
+            self._sync_deps()
+        finally:
+            os.chdir(prev_dir)
+
+    def _sync_deps(self):
+        # Load old values from auto.conf, if any
+        self._load_old_vals()
+
+        for sym in self.unique_defined_syms:
+            # Note: _write_to_conf is determined when the value is
+            # calculated. This is a hidden function call due to
+            # property magic.
+            val = sym.str_value
+
+            # Note: n tristate values do not get written to auto.conf and
+            # autoconf.h, making a missing symbol logically equivalent to n
+
+            if sym._write_to_conf:
+                if sym._old_val is None and \
+                   sym.orig_type in (BOOL, TRISTATE) and \
+                   val == "n":
+                    # No old value (the symbol was missing or n), new value n.
+                    # No change.
+                    continue
+
+                if val == sym._old_val:
+                    # New value matches old. No change.
+                    continue
+
+            elif sym._old_val is None:
+                # The symbol wouldn't appear in autoconf.h (because
+                # _write_to_conf is false), and it wouldn't have appeared in
+                # autoconf.h previously either (because it didn't appear in
+                # auto.conf). No change.
+                continue
+
+            # 'sym' has a new value. Flag it.
+
+            sym_path = sym.name.lower().replace("_", os.sep) + ".h"
+            sym_path_dir = os.path.dirname(sym_path)
+            if sym_path_dir and not os.path.exists(sym_path_dir):
+                os.makedirs(sym_path_dir, 0o755)
+
+            # A kind of truncating touch, mirroring the C tools
+            os.close(os.open(
+                sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644))
+
+        # Remember the current values as the "new old" values.
+        #
+        # This call could go anywhere after the call to _load_old_vals(), but
+        # putting it last means _sync_deps() can be safely rerun if it fails
+        # before this point.
+        self._write_old_vals()
+
+    def _write_old_vals(self):
+        # Helper for writing auto.conf. Basically just a simplified
+        # write_config() that doesn't write any comments (including
+        # '# CONFIG_FOO is not set' comments). The format matches the C
+        # implementation, though the ordering is arbitrary there (depends on
+        # the hash table implementation).
+        #
+        # A separate helper function is neater than complicating write_config()
+        # by passing a flag to it, plus we only need to look at symbols here.
+
+        with self._open("auto.conf", "w") as f:
+            for sym in self.unique_defined_syms:
+                if not (sym.orig_type in (BOOL, TRISTATE) and
+                        not sym.tri_value):
+                    f.write(sym.config_string)
+
+    def _load_old_vals(self):
+        # Loads old symbol values from auto.conf into a dedicated
+        # Symbol._old_val field. Mirrors load_config().
+        #
+        # The extra field could be avoided with some trickery involving dumping
+        # symbol values and restoring them later, but this is simpler and
+        # faster. The C tools also use a dedicated field for this purpose.
+
+        for sym in self.unique_defined_syms:
+            sym._old_val = None
+
+        if not os.path.exists("auto.conf"):
+            # No old values
+            return
+
+        with self._open("auto.conf", "r") as f:
+            for line in f:
+                match = self._set_match(line)
+                if not match:
+                    # We only expect CONFIG_FOO=... (and possibly a header
+                    # comment) in auto.conf
+                    continue
+
+                name, val = match.groups()
+                if name in self.syms:
+                    sym = self.syms[name]
+
+                    if sym.orig_type is STRING:
+                        match = _conf_string_match(val)
+                        if not match:
+                            continue
+                        val = unescape(match.group(1))
+
+                    self.syms[name]._old_val = val
+
+    def node_iter(self, unique_syms=False):
+        """
+        Returns a generator for iterating through all MenuNode's in the Kconfig
+        tree. The iteration is done in Kconfig definition order (the children
+        of a node are visited before the next node is visited).
+
+        The Kconfig.top_node menu node is skipped. It contains an implicit menu
+        that holds the top-level items.
+
+        As an example, the following code will produce a list equal to
+        Kconfig.defined_syms:
+
+          defined_syms = [node.item for node in kconf.node_iter()
+                          if isinstance(node.item, Symbol)]
+
+        unique_syms (default: False):
+          If True, only the first MenuNode will be included for symbols defined
+          in multiple locations.
+
+          Using kconf.node_iter(True) in the example above would give a list
+          equal to unique_defined_syms.
+        """
+        if unique_syms:
+            for sym in self.unique_defined_syms:
+                sym._visited = False
+
+        node = self.top_node
+        while 1:
+            # Jump to the next node with an iterative tree walk
+            if node.list:
+                node = node.list
+            elif node.next:
+                node = node.next
+            else:
+                while node.parent:
+                    node = node.parent
+                    if node.next:
+                        node = node.next
+                        break
+                else:
+                    # No more nodes
+                    return
+
+            if unique_syms and isinstance(node.item, Symbol):
+                if node.item._visited:
+                    continue
+                node.item._visited = True
+
+            yield node
+
+    def eval_string(self, s):
+        """
+        Returns the tristate value of the expression 's', represented as 0, 1,
+        and 2 for n, m, and y, respectively. Raises KconfigError if syntax
+        errors are detected in 's'. Warns if undefined symbols are referenced.
+
+        As an example, if FOO and BAR are tristate symbols at least one of
+        which has the value y, then config.eval_string("y && (FOO || BAR)")
+        returns 2 (y).
+
+        To get the string value of non-bool/tristate symbols, use
+        Symbol.str_value. eval_string() always returns a tristate value, and
+        all non-bool/tristate symbols have the tristate value 0 (n).
+
+        The expression parsing is consistent with how parsing works for
+        conditional ('if ...') expressions in the configuration, and matches
+        the C implementation. m is rewritten to 'm && MODULES', so
+        eval_string("m") will return 0 (n) unless modules are enabled.
+        """
+        # The parser is optimized to be fast when parsing Kconfig files (where
+        # an expression can never appear at the beginning of a line). We have
+        # to monkey-patch things a bit here to reuse it.
+
+        self._filename = None
+
+        # Don't include the "if " from below to avoid giving confusing error
+        # messages
+        self._line = s
+        # [1:] removes the _T_IF token
+        self._tokens = self._tokenize("if " + s)[1:]
+        self._tokens_i = -1
+
+        return expr_value(self._expect_expr_and_eol())  # transform_m
+
+    def unset_values(self):
+        """
+        Resets the user values of all symbols, as if Kconfig.load_config() or
+        Symbol.set_value() had never been called.
+        """
+        self._warn_for_no_prompt = False
+        try:
+            # set_value() already rejects undefined symbols, and they don't
+            # need to be invalidated (because their value never changes), so we
+            # can just iterate over defined symbols
+            for sym in self.unique_defined_syms:
+                sym.unset_value()
+
+            for choice in self.unique_choices:
+                choice.unset_value()
+        finally:
+            self._warn_for_no_prompt = True
+
+    def enable_warnings(self):
+        """
+        See Kconfig.__init__().
+        """
+        self._warnings_enabled = True
+
+    def disable_warnings(self):
+        """
+        See Kconfig.__init__().
+        """
+        self._warnings_enabled = False
+
+    def enable_stderr_warnings(self):
+        """
+        See Kconfig.__init__().
+        """
+        self._warn_to_stderr = True
+
+    def disable_stderr_warnings(self):
+        """
+        See Kconfig.__init__().
+        """
+        self._warn_to_stderr = False
+
+    def enable_undef_warnings(self):
+        """
+        Enables warnings for assignments to undefined symbols. Disabled by
+        default since they tend to be spammy for Kernel configurations (and
+        mostly suggests cleanups).
+        """
+        self._warn_for_undef_assign = True
+
+    def disable_undef_warnings(self):
+        """
+        See enable_undef_assign().
+        """
+        self._warn_for_undef_assign = False
+
+    def enable_redun_warnings(self):
+        """
+        Enables warnings for duplicated assignments in .config files that all
+        set the same value.
+
+        These warnings are enabled by default. Disabling them might be helpful
+        in certain cases when merging configurations.
+        """
+        self._warn_for_redun_assign = True
+
+    def disable_redun_warnings(self):
+        """
+        See enable_redun_warnings().
+        """
+        self._warn_for_redun_assign = False
+
+    def __repr__(self):
+        """
+        Returns a string with information about the Kconfig object when it is
+        evaluated on e.g. the interactive Python prompt.
+        """
+        return "<{}>".format(", ".join((
+            "configuration with {} symbols".format(len(self.syms)),
+            'main menu prompt "{}"'.format(self.mainmenu_text),
+            "srctree is current directory" if not self.srctree else
+                'srctree "{}"'.format(self.srctree),
+            'config symbol prefix "{}"'.format(self.config_prefix),
+            "warnings " +
+                ("enabled" if self._warnings_enabled else "disabled"),
+            "printing of warnings to stderr " +
+                ("enabled" if self._warn_to_stderr else "disabled"),
+            "undef. symbol assignment warnings " +
+                ("enabled" if self._warn_for_undef_assign else "disabled"),
+            "redundant symbol assignment warnings " +
+                ("enabled" if self._warn_for_redun_assign else "disabled")
+        )))
+
+    #
+    # Private methods
+    #
+
+
+    #
+    # File reading
+    #
+
+    def _open_config(self, filename):
+        # Opens a .config file. First tries to open 'filename', then
+        # '$srctree/filename' if $srctree was set when the configuration was
+        # loaded.
+
+        try:
+            return self._open(filename, "r")
+        except IOError as e:
+            # This will try opening the same file twice if $srctree is unset,
+            # but it's not a big deal
+            try:
+                return self._open(os.path.join(self.srctree, filename), "r")
+            except IOError as e2:
+                # This is needed for Python 3, because e2 is deleted after
+                # the try block:
+                #
+                # https://docs.python.org/3/reference/compound_stmts.html#the-try-statement
+                e = e2
+
+            raise IOError("\n" + textwrap.fill(
+                "Could not open '{}' ({}: {}){}".format(
+                    filename, errno.errorcode[e.errno], e.strerror,
+                    self._srctree_hint()),
+                80))
+
+    def _enter_file(self, full_filename, rel_filename):
+        # Jumps to the beginning of a sourced Kconfig file, saving the previous
+        # position and file object.
+        #
+        # full_filename:
+        #   Actual path to the file.
+        #
+        # rel_filename:
+        #   File path with $srctree prefix stripped, stored in e.g.
+        #   self._filename (which makes it indirectly show up in
+        #   MenuNode.filename). Equals full_filename for absolute paths.
+
+        self.kconfig_filenames.append(rel_filename)
+
+        # The parent Kconfig files are represented as a list of
+        # (<include path>, <Python 'file' object for Kconfig file>) tuples.
+        #
+        # <include path> is immutable and holds a *tuple* of
+        # (<filename>, <linenr>) tuples, giving the locations of the 'source'
+        # statements in the parent Kconfig files. The current include path is
+        # also available in Kconfig._include_path.
+        #
+        # The point of this redundant setup is to allow Kconfig._include_path
+        # to be assigned directly to MenuNode.include_path without having to
+        # copy it, sharing it wherever possible.
+
+        # Save include path and 'file' object before entering the file
+        self._filestack.append((self._include_path, self._file))
+
+        # _include_path is a tuple, so this rebinds the variable instead of
+        # doing in-place modification
+        self._include_path += ((self._filename, self._linenr),)
+
+        # Check for recursive 'source'
+        for name, _ in self._include_path:
+            if name == rel_filename:
+                raise KconfigError(
+                    "\n{}:{}: Recursive 'source' of '{}' detected. Check that "
+                    "environment variables are set correctly.\n"
+                    "Include path:\n{}"
+                    .format(self._filename, self._linenr, rel_filename,
+                            "\n".join("{}:{}".format(name, linenr)
+                                      for name, linenr in self._include_path)))
+
+        # Note: We already know that the file exists
+
+        try:
+            self._file = self._open(full_filename, "r")
+        except IOError as e:
+            raise IOError("{}:{}: Could not open '{}' ({}: {})".format(
+                self._filename, self._linenr, full_filename,
+                errno.errorcode[e.errno], e.strerror))
+
+        self._filename = rel_filename
+        self._linenr = 0
+
+    def _leave_file(self):
+        # Returns from a Kconfig file to the file that sourced it. See
+        # _enter_file().
+
+        self._file.close()
+        # Restore location from parent Kconfig file
+        self._filename, self._linenr = self._include_path[-1]
+        # Restore include path and 'file' object
+        self._include_path, self._file = self._filestack.pop()
+
+    def _next_line(self):
+        # Fetches and tokenizes the next line from the current Kconfig file.
+        # Returns False at EOF and True otherwise.
+
+        # _saved_line provides a single line of "unget", currently only used
+        # for help texts.
+        #
+        # This also works as expected if _saved_line is "", indicating EOF:
+        # "" is falsy, and readline() returns "" over and over at EOF.
+        if self._saved_line:
+            self._line = self._saved_line
+            self._saved_line = None
+        else:
+            self._line = self._file.readline()
+            if not self._line:
+                return False
+            self._linenr += 1
+
+        # Handle line joining
+        while self._line.endswith("\\\n"):
+            self._line = self._line[:-2] + self._file.readline()
+            self._linenr += 1
+
+        self._tokens = self._tokenize(self._line)
+        self._tokens_i = -1  # Token index (minus one)
+
+        return True
+
+
+    #
+    # Tokenization
+    #
+
+    def _lookup_sym(self, name):
+        # Fetches the symbol 'name' from the symbol table, creating and
+        # registering it if it does not exist. If '_parsing_kconfigs' is False,
+        # it means we're in eval_string(), and new symbols won't be registered.
+
+        if name in self.syms:
+            return self.syms[name]
+
+        sym = Symbol()
+        sym.kconfig = self
+        sym.name = name
+        sym.is_constant = False
+        sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n
+
+        if self._parsing_kconfigs:
+            self.syms[name] = sym
+        else:
+            self._warn("no symbol {} in configuration".format(name))
+
+        return sym
+
+    def _lookup_const_sym(self, name):
+        # Like _lookup_sym(), for constant (quoted) symbols
+
+        if name in self.const_syms:
+            return self.const_syms[name]
+
+        sym = Symbol()
+        sym.kconfig = self
+        sym.name = name
+        sym.is_constant = True
+        sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n
+
+        if self._parsing_kconfigs:
+            self.const_syms[name] = sym
+
+        return sym
+
+    def _tokenize(self, s):
+        # Parses 's', returning a None-terminated list of tokens. Registers any
+        # new symbols encountered with _lookup(_const)_sym().
+        #
+        # Tries to be reasonably speedy by processing chunks of text via
+        # regexes and string operations where possible. This is the biggest
+        # hotspot during parsing.
+        #
+        # Note: It might be possible to rewrite this to 'yield' tokens instead,
+        # working across multiple lines. The 'option env' lookback thing below
+        # complicates things though.
+
+        # Initial token on the line
+        match = _command_match(s)
+        if not match:
+            if s.isspace() or s.lstrip().startswith("#"):
+                return (None,)
+            self._parse_error("unknown token at start of line")
+
+        # Tricky implementation detail: While parsing a token, 'token' refers
+        # to the previous token. See _STRING_LEX for why this is needed.
+        token = _get_keyword(match.group(1))
+        if not token:
+            # Backwards compatibility with old versions of the C tools, which
+            # (accidentally) accepted stuff like "--help--" and "-help---".
+            # This was fixed in the C tools by commit c2264564 ("kconfig: warn
+            # of unhandled characters in Kconfig commands"), committed in July
+            # 2015, but it seems people still run Kconfiglib on older kernels.
+            if s.strip(" \t\n-") == "help":
+                return (_T_HELP, None)
+
+            # If the first token is not a keyword (and not a weird help token),
+            # we have a preprocessor variable assignment (or a bare macro on a
+            # line)
+            self._parse_assignment(s)
+            return (None,)
+
+        tokens = [token]
+        # The current index in the string being tokenized
+        i = match.end()
+
+        # Main tokenization loop (for tokens past the first one)
+        while i < len(s):
+            # Test for an identifier/keyword first. This is the most common
+            # case.
+            match = _id_keyword_match(s, i)
+            if match:
+                # We have an identifier or keyword
+
+                # Jump past it
+                i = match.end()
+
+                # Check what it is. lookup_sym() will take care of allocating
+                # new symbols for us the first time we see them. Note that
+                # 'token' still refers to the previous token.
+
+                name = match.group(1)
+                keyword = _get_keyword(name)
+                if keyword:
+                    # It's a keyword
+                    token = keyword
+
+                elif token not in _STRING_LEX:
+                    # It's a non-const symbol, except we translate n, m, and y
+                    # into the corresponding constant symbols, like the C
+                    # implementation
+                    token = self.const_syms[name] \
+                            if name in ("n", "m", "y") else \
+                            self._lookup_sym(name)
+
+                else:
+                    # It's a case of missing quotes. For example, the
+                    # following is accepted:
+                    #
+                    #   menu unquoted_title
+                    #
+                    #   config A
+                    #       tristate unquoted_prompt
+                    #
+                    #   endmenu
+                    token = name
+
+            else:
+                # Neither a keyword nor a non-const symbol (except
+                # $()-expansion might still yield a non-const symbol).
+
+                # We always strip whitespace after tokens, so it is safe to
+                # assume that s[i] is the start of a token here.
+                c = s[i]
+
+                if c in "\"'":
+                    s, end_i = self._expand_str(s, i, c)
+
+                    # os.path.expandvars() and the $UNAME_RELEASE replace() is
+                    # a backwards compatibility hack, which should be
+                    # reasonably safe as expandvars() leaves references to
+                    # undefined env. vars. as is.
+                    #
+                    # The preprocessor functionality changed how environment
+                    # variables are referenced, to $(FOO).
+                    val = os.path.expandvars(
+                        s[i + 1:end_i - 1].replace("$UNAME_RELEASE",
+                                                   platform.uname()[2]))
+
+                    i = end_i
+
+                    # This is the only place where we don't survive with a
+                    # single token of lookback: 'option env="FOO"' does not
+                    # refer to a constant symbol named "FOO".
+                    token = val \
+                            if token in _STRING_LEX or \
+                                tokens[0] is _T_OPTION else \
+                            self._lookup_const_sym(val)
+
+                elif s.startswith("&&", i):
+                    token = _T_AND
+                    i += 2
+
+                elif s.startswith("||", i):
+                    token = _T_OR
+                    i += 2
+
+                elif c == "=":
+                    token = _T_EQUAL
+                    i += 1
+
+                elif s.startswith("!=", i):
+                    token = _T_UNEQUAL
+                    i += 2
+
+                elif c == "!":
+                    token = _T_NOT
+                    i += 1
+
+                elif c == "(":
+                    token = _T_OPEN_PAREN
+                    i += 1
+
+                elif c == ")":
+                    token = _T_CLOSE_PAREN
+                    i += 1
+
+                elif s.startswith("$(", i):
+                    s, end_i = self._expand_macro(s, i, ())
+                    val = s[i:end_i]
+                    # isspace() is False for empty strings
+                    if not val.strip():
+                        # Avoid creating a Kconfig symbol with a blank name.
+                        # It's almost guaranteed to be an error.
+                        self._parse_error("macro expanded to blank string")
+                    i = end_i
+
+                    # Compatibility with what the C implementation does. Might
+                    # be unexpected that you can reference non-constant symbols
+                    # this way though...
+                    token = self.const_syms[val] \
+                            if val in ("n", "m", "y") else \
+                            self._lookup_sym(val)
+
+                elif s.startswith("$", i):
+                    if token == _T_CONFIG:
+                        s.replace('$', ' ')
+                        i += 1
+                        continue
+                elif c == "#":
+                    break
+
+
+                # Very rare
+
+                elif s.startswith("<=", i):
+                    token = _T_LESS_EQUAL
+                    i += 2
+
+                elif c == "<":
+                    token = _T_LESS
+                    i += 1
+
+                elif s.startswith(">=", i):
+                    token = _T_GREATER_EQUAL
+                    i += 2
+
+                elif c == ">":
+                    token = _T_GREATER
+                    i += 1
+
+
+                else:
+                    self._parse_error("unknown tokens in line")
+
+
+                # Skip trailing whitespace
+                while i < len(s) and s[i].isspace():
+                    i += 1
+
+
+            # Add the token
+            tokens.append(token)
+
+        # None-terminating the token list makes the token fetching functions
+        # simpler/faster
+        tokens.append(None)
+
+        return tokens
+
+    def _next_token(self):
+        self._tokens_i += 1
+        return self._tokens[self._tokens_i]
+
+    def _peek_token(self):
+        return self._tokens[self._tokens_i + 1]
+
+    # The functions below are just _next_token() and _parse_expr() with extra
+    # syntax checking. Inlining _next_token() and _peek_token() into them saves
+    # a few % of parsing time.
+    #
+    # See the 'Intro to expressions' section for what a constant symbol is.
+
+    def _expect_sym(self):
+        self._tokens_i += 1
+        token = self._tokens[self._tokens_i]
+
+        if not isinstance(token, Symbol):
+            self._parse_error("expected symbol")
+
+        return token
+
+    def _expect_nonconst_sym(self):
+        self._tokens_i += 1
+        token = self._tokens[self._tokens_i]
+
+        if not isinstance(token, Symbol) or token.is_constant:
+            self._parse_error("expected nonconstant symbol")
+
+        return token
+
+    def _expect_nonconst_sym_and_eol(self):
+        self._tokens_i += 1
+        token = self._tokens[self._tokens_i]
+
+        if not isinstance(token, Symbol) or token.is_constant:
+            self._parse_error("expected nonconstant symbol")
+
+        if self._tokens[self._tokens_i + 1] is not None:
+            self._parse_error("extra tokens at end of line")
+
+        return token
+
+    def _expect_str(self):
+        self._tokens_i += 1
+        token = self._tokens[self._tokens_i]
+
+        if not isinstance(token, str):
+            self._parse_error("expected string")
+
+        return token
+
+    def _expect_str_and_eol(self):
+        self._tokens_i += 1
+        token = self._tokens[self._tokens_i]
+
+        if not isinstance(token, str):
+            self._parse_error("expected string")
+
+        if self._tokens[self._tokens_i + 1] is not None:
+            self._parse_error("extra tokens at end of line")
+
+        return token
+
+    def _expect_expr_and_eol(self):
+        expr = self._parse_expr(True)
+
+        if self._peek_token() is not None:
+            self._parse_error("extra tokens at end of line")
+
+        return expr
+
+    def _check_token(self, token):
+        # If the next token is 'token', removes it and returns True
+
+        if self._tokens[self._tokens_i + 1] is token:
+            self._tokens_i += 1
+            return True
+        return False
+
+
+    #
+    # Preprocessor logic
+    #
+
+    def _parse_assignment(self, s):
+        # Parses a preprocessor variable assignment, registering the variable
+        # if it doesn't already exist. Also takes care of bare macros on lines
+        # (which are allowed, and can be useful for their side effects).
+
+        # Expand any macros in the left-hand side of the assignment (the
+        # variable name)
+        s = s.lstrip()
+        i = 0
+        while 1:
+            i = _assignment_lhs_fragment_match(s, i).end()
+            if s.startswith("$(", i):
+                s, i = self._expand_macro(s, i, ())
+            else:
+                break
+
+        if s.isspace():
+            # We also accept a bare macro on a line (e.g.
+            # $(warning-if,$(foo),ops)), provided it expands to a blank string
+            return
+
+        # Assigned variable
+        name = s[:i]
+
+
+        # Extract assignment operator (=, :=, or +=) and value
+        rhs_match = _assignment_rhs_match(s, i)
+        if not rhs_match:
+            self._parse_error("syntax error")
+
+        op, val = rhs_match.groups()
+
+
+        if name in self.variables:
+            # Already seen variable
+            var = self.variables[name]
+        else:
+            # New variable
+            var = Variable()
+            var.kconfig = self
+            var.name = name
+            var._n_expansions = 0
+            self.variables[name] = var
+
+            # += acts like = on undefined variables (defines a recursive
+            # variable)
+            if op == "+=":
+                op = "="
+
+        if op == "=":
+            var.is_recursive = True
+            var.value = val
+        elif op == ":=":
+            var.is_recursive = False
+            var.value = self._expand_whole(val, ())
+        else:  # op == "+="
+            # += does immediate expansion if the variable was last set
+            # with :=
+            var.value += " " + (val if var.is_recursive else \
+                                self._expand_whole(val, ()))
+
+    def _expand_whole(self, s, args):
+        # Expands preprocessor macros in all of 's'. Used whenever we don't
+        # have to worry about delimiters. See _expand_macro() re. the 'args'
+        # parameter.
+        #
+        # Returns the expanded string.
+
+        i = 0
+        while 1:
+            i = s.find("$(", i)
+            if i == -1:
+                break
+            s, i = self._expand_macro(s, i, args)
+        return s
+
+    def _expand_str(self, s, i, quote):
+        # Expands a quoted string starting at index 'i' in 's'. Handles both
+        # backslash escapes and macro expansion.
+        #
+        # Returns the expanded 's' (including the part before the string) and
+        # the index of the first character after the expanded string in 's'.
+
+        i += 1  # Skip over initial "/'
+        while 1:
+            match = _string_special_search(s, i)
+            if not match:
+                self._parse_error("unterminated string")
+
+
+            if match.group() == quote:
+                # Found the end of the string
+                return (s, match.end())
+
+            elif match.group() == "\\":
+                # Replace '\x' with 'x'. 'i' ends up pointing to the character
+                # after 'x', which allows macros to be canceled with '\$(foo)'.
+                i = match.end()
+                s = s[:match.start()] + s[i:]
+
+            elif match.group() == "$(":
+                # A macro call within the string
+                s, i = self._expand_macro(s, match.start(), ())
+
+            else:
+                # A ' quote within " quotes or vice versa
+                i += 1
+
+    def _expand_macro(self, s, i, args):
+        # Expands a macro starting at index 'i' in 's'. If this macro resulted
+        # from the expansion of another macro, 'args' holds the arguments
+        # passed to that macro.
+        #
+        # Returns the expanded 's' (including the part before the macro) and
+        # the index of the first character after the expanded macro in 's'.
+
+        start = i
+        i += 2  # Skip over "$("
+
+        # Start of current macro argument
+        arg_start = i
+
+        # Arguments of this macro call
+        new_args = []
+
+        while 1:
+            match = _macro_special_search(s, i)
+            if not match:
+                self._parse_error("missing end parenthesis in macro expansion")
+
+
+            if match.group() == ")":
+                # Found the end of the macro
+
+                new_args.append(s[arg_start:match.start()])
+
+                prefix = s[:start]
+
+                # $(1) is replaced by the first argument to the function, etc.,
+                # provided at least that many arguments were passed
+
+                try:
+                    # Does the macro look like an integer, with a corresponding
+                    # argument? If so, expand it to the value of the argument.
+                    prefix += args[int(new_args[0])]
+                except (ValueError, IndexError):
+                    # Regular variables are just functions without arguments,
+                    # and also go through the function value path
+                    prefix += self._fn_val(new_args)
+
+                return (prefix + s[match.end():],
+                        len(prefix))
+
+            elif match.group() == ",":
+                # Found the end of a macro argument
+                new_args.append(s[arg_start:match.start()])
+                arg_start = i = match.end()
+
+            else:  # match.group() == "$("
+                # A nested macro call within the macro
+                s, i = self._expand_macro(s, match.start(), args)
+
+    def _fn_val(self, args):
+        # Returns the result of calling the function args[0] with the arguments
+        # args[1..len(args)-1]. Plain variables are treated as functions
+        # without arguments.
+
+        fn = args[0]
+
+        if fn in self.variables:
+            var = self.variables[fn]
+
+            if len(args) == 1:
+                # Plain variable
+                if var._n_expansions:
+                    self._parse_error("Preprocessor variable {} recursively "
+                                      "references itself".format(var.name))
+            elif var._n_expansions > 100:
+                # Allow functions to call themselves, but guess that functions
+                # that are overly recursive are stuck
+                self._parse_error("Preprocessor function {} seems stuck "
+                                  "in infinite recursion".format(var.name))
+
+            var._n_expansions += 1
+            res = self._expand_whole(self.variables[fn].value, args)
+            var._n_expansions -= 1
+            return res
+
+        if fn in self._functions:
+            # Built-in function
+
+            py_fn, min_arg, max_arg = self._functions[fn]
+
+            if not min_arg <= len(args) - 1 <= max_arg:
+                if min_arg == max_arg:
+                    expected_args = min_arg
+                else:
+                    expected_args = "{}-{}".format(min_arg, max_arg)
+
+                raise KconfigError("{}:{}: bad number of arguments in call "
+                                   "to {}, expected {}, got {}"
+                                   .format(self._filename, self._linenr, fn,
+                                           expected_args, len(args) - 1))
+
+            return py_fn(self, args)
+
+        # Environment variables are tried last
+        if fn in os.environ:
+            self.env_vars.add(fn)
+            return os.environ[fn]
+
+        return ""
+
+
+    #
+    # Parsing
+    #
+
+    def _make_and(self, e1, e2):
+        # Constructs an AND (&&) expression. Performs trivial simplification.
+
+        if e1 is self.y:
+            return e2
+
+        if e2 is self.y:
+            return e1
+
+        if e1 is self.n or e2 is self.n:
+            return self.n
+
+        return (AND, e1, e2)
+
+    def _make_or(self, e1, e2):
+        # Constructs an OR (||) expression. Performs trivial simplification.
+
+        if e1 is self.n:
+            return e2
+
+        if e2 is self.n:
+            return e1
+
+        if e1 is self.y or e2 is self.y:
+            return self.y
+
+        return (OR, e1, e2)
+
+    def _parse_block(self, end_token, parent, prev):
+        # Parses a block, which is the contents of either a file or an if,
+        # menu, or choice statement.
+        #
+        # end_token:
+        #   The token that ends the block, e.g. _T_ENDIF ("endif") for ifs.
+        #   None for files.
+        #
+        # parent:
+        #   The parent menu node, corresponding to a menu, Choice, or 'if'.
+        #   'if's are flattened after parsing.
+        #
+        # prev:
+        #   The previous menu node. New nodes will be added after this one (by
+        #   modifying their 'next' pointer).
+        #
+        #   'prev' is reused to parse a list of child menu nodes (for a menu or
+        #   Choice): After parsing the children, the 'next' pointer is assigned
+        #   to the 'list' pointer to "tilt up" the children above the node.
+        #
+        # Returns the final menu node in the block (or 'prev' if the block is
+        # empty). This allows chaining.
+
+        # We might already have tokens from parsing a line to check if it's a
+        # property and discovering it isn't. self._has_tokens functions as a
+        # kind of "unget".
+        while self._has_tokens or self._next_line():
+            self._has_tokens = False
+
+            t0 = self._next_token()
+            if t0 is None:
+                continue
+
+            if t0 in (_T_CONFIG, _T_MENUCONFIG):
+                # The tokenizer allocates Symbol objects for us
+                sym = self._expect_nonconst_sym_and_eol()
+                self.defined_syms.append(sym)
+
+                node = MenuNode()
+                node.kconfig = self
+                node.item = sym
+                node.is_menuconfig = (t0 is _T_MENUCONFIG)
+                node.prompt = node.help = node.list = None
+                node.parent = parent
+                node.filename = self._filename
+                node.linenr = self._linenr
+                node.include_path = self._include_path
+
+                sym.nodes.append(node)
+
+                self._parse_properties(node)
+
+                if node.item.env_var:
+                    if node.item.env_var in os.environ:
+                        os.environ[node.item.name] = os.environ[node.item.env_var]
+                    else:
+                        os.environ[node.item.name] = ((node.defaults[0])[0]).name
+
+                if node.is_menuconfig and not node.prompt:
+                    self._warn("the menuconfig symbol {} has no prompt"
+                               .format(_name_and_loc(sym)))
+
+                # Tricky Python semantics: This assigns prev.next before prev
+                prev.next = prev = node
+
+            elif t0 in (_T_SOURCE, _T_RSOURCE, _T_OSOURCE, _T_ORSOURCE):
+                pattern = self._expect_str_and_eol()
+
+                # Check if the pattern is absolute and avoid stripping srctree
+                # from it below in that case. We must do the check before
+                # join()'ing, as srctree might be an absolute path.
+                isabs = os.path.isabs(pattern)
+
+                if t0 in (_T_RSOURCE, _T_ORSOURCE):
+                    # Relative source
+                    pattern = os.path.join(os.path.dirname(self._filename),
+                                           pattern)
+
+                # Sort the glob results to ensure a consistent ordering of
+                # Kconfig symbols, which indirectly ensures a consistent
+                # ordering in e.g. .config files
+                filenames = \
+                    sorted(glob.iglob(os.path.join(self.srctree, pattern)))
+
+                if not filenames and t0 in (_T_SOURCE, _T_RSOURCE):
+                    raise KconfigError("\n" + textwrap.fill(
+                        "{}:{}: '{}' does not exist{}".format(
+                            self._filename, self._linenr, pattern,
+                            self._srctree_hint()),
+                        80))
+
+                for filename in filenames:
+                    self._enter_file(
+                        filename,
+                        # Unless an absolute path is passed to *source, strip
+                        # the $srctree prefix from the filename. That way it
+                        # appears without a $srctree prefix in
+                        # MenuNode.filename, which is nice e.g. when generating
+                        # documentation.
+                        filename if isabs else
+                            os.path.relpath(filename, self.srctree))
+
+                    prev = self._parse_block(None, parent, prev)
+
+                    self._leave_file()
+
+            elif t0 is end_token:
+                # We have reached the end of the block. Terminate the final
+                # node and return it.
+                prev.next = None
+                return prev
+
+            elif t0 is _T_IF:
+                node = MenuNode()
+                node.item = node.prompt = None
+                node.parent = parent
+                node.filename = self._filename
+                node.linenr = self._linenr
+
+                node.dep = self._expect_expr_and_eol()
+
+                self._parse_block(_T_ENDIF, node, node)
+                node.list = node.next
+
+                prev.next = prev = node
+
+            elif t0 is _T_MENU:
+                node = MenuNode()
+                node.kconfig = self
+                node.item = MENU
+                node.is_menuconfig = True
+                node.prompt = (self._expect_str_and_eol(), self.y)
+                node.visibility = self.y
+                node.parent = parent
+                node.filename = self._filename
+                node.linenr = self._linenr
+                node.include_path = self._include_path
+
+                self.menus.append(node)
+
+                self._parse_properties(node)
+                self._parse_block(_T_ENDMENU, node, node)
+                node.list = node.next
+
+                prev.next = prev = node
+
+            elif t0 is _T_COMMENT:
+                node = MenuNode()
+                node.kconfig = self
+                node.item = COMMENT
+                node.is_menuconfig = False
+                node.prompt = (self._expect_str_and_eol(), self.y)
+                node.list = None
+                node.parent = parent
+                node.filename = self._filename
+                node.linenr = self._linenr
+                node.include_path = self._include_path
+
+                self.comments.append(node)
+
+                self._parse_properties(node)
+
+                prev.next = prev = node
+
+            elif t0 is _T_CHOICE:
+                if self._peek_token() is None:
+                    choice = Choice()
+                    choice.direct_dep = self.n
+
+                    self.choices.append(choice)
+                else:
+                    # Named choice
+                    name = self._expect_str_and_eol()
+                    choice = self.named_choices.get(name)
+                    if not choice:
+                        choice = Choice()
+                        choice.name = name
+                        choice.direct_dep = self.n
+
+                        self.choices.append(choice)
+                        self.named_choices[name] = choice
+
+                choice.kconfig = self
+
+                node = MenuNode()
+                node.kconfig = self
+                node.item = choice
+                node.is_menuconfig = True
+                node.prompt = node.help = None
+                node.parent = parent
+                node.filename = self._filename
+                node.linenr = self._linenr
+                node.include_path = self._include_path
+
+                choice.nodes.append(node)
+
+                self._parse_properties(node)
+                self._parse_block(_T_ENDCHOICE, node, node)
+                node.list = node.next
+
+                prev.next = prev = node
+
+            elif t0 is _T_MAINMENU:
+                self.top_node.prompt = (self._expect_str_and_eol(), self.y)
+                self.top_node.filename = self._filename
+                self.top_node.linenr = self._linenr
+
+            else:
+                self._parse_error("unrecognized construct")
+
+        # End of file reached. Terminate the final node and return it.
+
+        if end_token:
+            raise KconfigError("Unexpected end of file " + self._filename)
+
+        prev.next = None
+        return prev
+
+    def _parse_cond(self):
+        # Parses an optional 'if <expr>' construct and returns the parsed
+        # <expr>, or self.y if the next token is not _T_IF
+
+        return self._expect_expr_and_eol() if self._check_token(_T_IF) \
+            else self.y
+
+    def _parse_properties(self, node):
+        # Parses and adds properties to the MenuNode 'node' (type, 'prompt',
+        # 'default's, etc.) Properties are later copied up to symbols and
+        # choices in a separate pass after parsing, in _add_props_to_sc().
+        #
+        # An older version of this code added properties directly to symbols
+        # and choices instead of to their menu nodes (and handled dependency
+        # propagation simultaneously), but that loses information on where a
+        # property is added when a symbol or choice is defined in multiple
+        # locations. Some Kconfig configuration systems rely heavily on such
+        # symbols, and better docs can be generated by keeping track of where
+        # properties are added.
+        #
+        # node:
+        #   The menu node we're parsing properties on
+
+        # Dependencies from 'depends on'. Will get propagated to the properties
+        # below.
+        node.dep = self.y
+
+        while self._next_line():
+            t0 = self._next_token()
+            if t0 is None:
+                continue
+
+            if t0 in _TYPE_TOKENS:
+                self._set_type(node, _TOKEN_TO_TYPE[t0])
+                if self._peek_token() is not None:
+                    self._parse_prompt(node)
+
+            elif t0 is _T_DEPENDS:
+                if not self._check_token(_T_ON):
+                    self._parse_error('expected "on" after "depends"')
+
+                node.dep = self._make_and(node.dep,
+                                          self._expect_expr_and_eol())
+
+            elif t0 is _T_HELP:
+                self._parse_help(node)
+
+            elif t0 is _T_SELECT:
+                if not isinstance(node.item, Symbol):
+                    self._parse_error("only symbols can select")
+
+                node.selects.append((self._expect_nonconst_sym(),
+                                     self._parse_cond()))
+
+            elif t0 is _T_IMPLY:
+                if not isinstance(node.item, Symbol):
+                    self._parse_error("only symbols can imply")
+
+                node.implies.append((self._expect_nonconst_sym(),
+                                     self._parse_cond()))
+
+            elif t0 is _T_DEFAULT:
+                node.defaults.append((self._parse_expr(False),
+                                      self._parse_cond()))
+
+            elif t0 in (_T_DEF_BOOL, _T_DEF_TRISTATE, _T_DEF_INT, _T_DEF_HEX,
+                        _T_DEF_STRING):
+                self._set_type(node, _TOKEN_TO_TYPE[t0])
+                node.defaults.append((self._parse_expr(False),
+                                      self._parse_cond()))
+
+            elif t0 is _T_PROMPT:
+                self._parse_prompt(node)
+
+            elif t0 is _T_RANGE:
+                node.ranges.append((self._expect_sym(),
+                                    self._expect_sym(),
+                                    self._parse_cond()))
+
+            elif t0 is _T_OPTION:
+                if self._check_token(_T_ENV):
+                    if not self._check_token(_T_EQUAL):
+                        self._parse_error('expected "=" after "env"')
+
+                    env_var = self._expect_str_and_eol()
+                    node.item.env_var = env_var
+
+                    if env_var in os.environ:
+                        node.defaults.append(
+                            (self._lookup_const_sym(os.environ[env_var]),
+                             self.y))
+                    else:
+                        self._warn("{1} has 'option env=\"{0}\"', "
+                                   "but the environment variable {0} is not "
+                                   "set".format(node.item.name, env_var),
+                                   self._filename, self._linenr)
+
+                    if env_var != node.item.name:
+                        self._warn("Kconfiglib expands environment variables "
+                                   "in strings directly, meaning you do not "
+                                   "need 'option env=...' \"bounce\" symbols. "
+                                   "For compatibility with the C tools, "
+                                   "rename {} to {} (so that the symbol name "
+                                   "matches the environment variable name)."
+                                   .format(node.item.name, env_var),
+                                   self._filename, self._linenr)
+
+                elif self._check_token(_T_DEFCONFIG_LIST):
+                    if not self.defconfig_list:
+                        self.defconfig_list = node.item
+                    else:
+                        self._warn("'option defconfig_list' set on multiple "
+                                   "symbols ({0} and {1}). Only {0} will be "
+                                   "used.".format(self.defconfig_list.name,
+                                                  node.item.name),
+                                   self._filename, self._linenr)
+
+                elif self._check_token(_T_MODULES):
+                    # To reduce warning spam, only warn if 'option modules' is
+                    # set on some symbol that isn't MODULES, which should be
+                    # safe. I haven't run into any projects that make use
+                    # modules besides the kernel yet, and there it's likely to
+                    # keep being called "MODULES".
+                    if node.item is not self.modules:
+                        self._warn("the 'modules' option is not supported. "
+                                   "Let me know if this is a problem for you, "
+                                   "as it wouldn't be that hard to implement. "
+                                   "Note that modules are supported -- "
+                                   "Kconfiglib just assumes the symbol name "
+                                   "MODULES, like older versions of the C "
+                                   "implementation did when 'option modules' "
+                                   "wasn't used.",
+                                   self._filename, self._linenr)
+
+                elif self._check_token(_T_ALLNOCONFIG_Y):
+                    if not isinstance(node.item, Symbol):
+                        self._parse_error("the 'allnoconfig_y' option is only "
+                                          "valid for symbols")
+
+                    node.item.is_allnoconfig_y = True
+
+                else:
+                    self._parse_error("unrecognized option")
+
+            elif t0 is _T_VISIBLE:
+                if not self._check_token(_T_IF):
+                    self._parse_error('expected "if" after "visible"')
+
+                node.visibility = self._make_and(node.visibility,
+                                                 self._expect_expr_and_eol())
+
+            elif t0 is _T_OPTIONAL:
+                if not isinstance(node.item, Choice):
+                    self._parse_error('"optional" is only valid for choices')
+
+                node.item.is_optional = True
+
+            else:
+                # Reuse the tokens for the non-property line later
+                self._has_tokens = True
+                self._tokens_i = -1
+                return
+
+    def _set_type(self, node, new_type):
+        if node.item.orig_type not in (UNKNOWN, new_type):
+            self._warn("{} defined with multiple types, {} will be used"
+                       .format(_name_and_loc(node.item),
+                               TYPE_TO_STR[new_type]))
+
+        node.item.orig_type = new_type
+
+    def _parse_prompt(self, node):
+        # 'prompt' properties override each other within a single definition of
+        # a symbol, but additional prompts can be added by defining the symbol
+        # multiple times
+        if node.prompt:
+            self._warn(_name_and_loc(node.item) +
+                       " defined with multiple prompts in single location")
+
+        prompt = self._expect_str()
+        if prompt != prompt.strip():
+            self._warn(_name_and_loc(node.item) +
+                       " has leading or trailing whitespace in its prompt")
+
+            # This avoid issues for e.g. reStructuredText documentation, where
+            # '*prompt *' is invalid
+            prompt = prompt.strip()
+
+        node.prompt = (prompt, self._parse_cond())
+
+    def _parse_help(self, node):
+        # Find first non-blank (not all-space) line and get its indentation
+
+        if node.help is not None:
+            self._warn(_name_and_loc(node.item) +
+                       " defined with more than one help text -- only the "
+                       "last one will be used")
+
+        # Small optimization. This code is pretty hot.
+        readline = self._file.readline
+
+        while 1:
+            line = readline()
+            self._linenr += 1
+            if not line or not line.isspace():
+                break
+
+        if not line:
+            self._warn(_name_and_loc(node.item) +
+                       " has 'help' but empty help text")
+
+            node.help = ""
+            return
+
+        indent = _indentation(line)
+        if indent == 0:
+            # If the first non-empty lines has zero indent, there is no help
+            # text
+            self._warn(_name_and_loc(node.item) +
+                       " has 'help' but empty help text")
+
+            node.help = ""
+            self._saved_line = line  # "Unget" the line
+            return
+
+        # The help text goes on till the first non-empty line with less indent
+        # than the first line
+
+        help_lines = []
+        # Small optimizations
+        add_help_line = help_lines.append
+        indentation = _indentation
+
+        while line and (line.isspace() or indentation(line) >= indent):
+            # De-indent 'line' by 'indent' spaces and rstrip() it to remove any
+            # newlines (which gets rid of other trailing whitespace too, but
+            # that's fine).
+            #
+            # This prepares help text lines in a speedy way: The [indent:]
+            # might already remove trailing newlines for lines shorter than
+            # indent (e.g. empty lines). The rstrip() makes it consistent,
+            # meaning we can join the lines with "\n" later.
+            add_help_line(line.expandtabs()[indent:].rstrip())
+
+            line = readline()
+
+        self._linenr += len(help_lines)
+
+        node.help = "\n".join(help_lines).rstrip() + "\n"
+        self._saved_line = line  # "Unget" the line
+
+    def _parse_expr(self, transform_m):
+        # Parses an expression from the tokens in Kconfig._tokens using a
+        # simple top-down approach. See the module docstring for the expression
+        # format.
+        #
+        # transform_m:
+        #   True if m should be rewritten to m && MODULES. See the
+        #   Kconfig.eval_string() documentation.
+
+        # Grammar:
+        #
+        #   expr:     and_expr ['||' expr]
+        #   and_expr: factor ['&&' and_expr]
+        #   factor:   <symbol> ['='/'!='/'<'/... <symbol>]
+        #             '!' factor
+        #             '(' expr ')'
+        #
+        # It helps to think of the 'expr: and_expr' case as a single-operand OR
+        # (no ||), and of the 'and_expr: factor' case as a single-operand AND
+        # (no &&). Parsing code is always a bit tricky.
+
+        # Mind dump: parse_factor() and two nested loops for OR and AND would
+        # work as well. The straightforward implementation there gives a
+        # (op, (op, (op, A, B), C), D) parse for A op B op C op D. Representing
+        # expressions as (op, [list of operands]) instead goes nicely with that
+        # version, but is wasteful for short expressions and complicates
+        # expression evaluation and other code that works on expressions (more
+        # complicated code likely offsets any performance gain from less
+        # recursion too). If we also try to optimize the list representation by
+        # merging lists when possible (e.g. when ANDing two AND expressions),
+        # we end up allocating a ton of lists instead of reusing expressions,
+        # which is bad.
+
+        and_expr = self._parse_and_expr(transform_m)
+
+        # Return 'and_expr' directly if we have a "single-operand" OR.
+        # Otherwise, parse the expression on the right and make an OR node.
+        # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))).
+        return and_expr \
+               if not self._check_token(_T_OR) else \
+               (OR, and_expr, self._parse_expr(transform_m))
+
+    def _parse_and_expr(self, transform_m):
+        factor = self._parse_factor(transform_m)
+
+        # Return 'factor' directly if we have a "single-operand" AND.
+        # Otherwise, parse the right operand and make an AND node. This turns
+        # A && B && C && D into (AND, A, (AND, B, (AND, C, D))).
+        return factor \
+               if not self._check_token(_T_AND) else \
+               (AND, factor, self._parse_and_expr(transform_m))
+
+    def _parse_factor(self, transform_m):
+        token = self._next_token()
+
+        if isinstance(token, Symbol):
+            # Plain symbol or relation
+
+            next_token = self._peek_token()
+            if next_token not in _RELATIONS:
+                # Plain symbol
+
+                # For conditional expressions ('depends on <expr>',
+                # '... if <expr>', etc.), m is rewritten to m && MODULES.
+                if transform_m and token is self.m:
+                    return (AND, self.m, self.modules)
+
+                return token
+
+            # Relation
+            #
+            # _T_EQUAL, _T_UNEQUAL, etc., deliberately have the same values as
+            # EQUAL, UNEQUAL, etc., so we can just use the token directly
+            return (self._next_token(), token, self._expect_sym())
+
+        if token is _T_NOT:
+            # token == _T_NOT == NOT
+            return (token, self._parse_factor(transform_m))
+
+        if token is _T_OPEN_PAREN:
+            expr_parse = self._parse_expr(transform_m)
+            if self._check_token(_T_CLOSE_PAREN):
+                return expr_parse
+
+        self._parse_error("malformed expression")
+
+    #
+    # Caching and invalidation
+    #
+
+    def _build_dep(self):
+        # Populates the Symbol/Choice._dependents sets, which contain all other
+        # items (symbols and choices) that immediately depend on the item in
+        # the sense that changing the value of the item might affect the value
+        # of the dependent items. This is used for caching/invalidation.
+        #
+        # The calculated sets might be larger than necessary as we don't do any
+        # complex analysis of the expressions.
+
+        # Only calculate _dependents for defined symbols. Constant and
+        # undefined symbols could theoretically be selected/implied, but it
+        # wouldn't change their value, so it's not a true dependency.
+        for sym in self.unique_defined_syms:
+            # Symbols depend on the following:
+
+            # The prompt conditions
+            for node in sym.nodes:
+                if node.prompt:
+                    _make_depend_on(sym, node.prompt[1])
+
+            # The default values and their conditions
+            for value, cond in sym.defaults:
+                _make_depend_on(sym, value)
+                _make_depend_on(sym, cond)
+
+            # The reverse and weak reverse dependencies
+            _make_depend_on(sym, sym.rev_dep)
+            _make_depend_on(sym, sym.weak_rev_dep)
+
+            # The ranges along with their conditions
+            for low, high, cond in sym.ranges:
+                _make_depend_on(sym, low)
+                _make_depend_on(sym, high)
+                _make_depend_on(sym, cond)
+
+            # The direct dependencies. This is usually redundant, as the direct
+            # dependencies get propagated to properties, but it's needed to get
+            # invalidation solid for 'imply', which only checks the direct
+            # dependencies (even if there are no properties to propagate it
+            # to).
+            _make_depend_on(sym, sym.direct_dep)
+
+            # In addition to the above, choice symbols depend on the choice
+            # they're in, but that's handled automatically since the Choice is
+            # propagated to the conditions of the properties before
+            # _build_dep() runs.
+
+        for choice in self.unique_choices:
+            # Choices depend on the following:
+
+            # The prompt conditions
+            for node in choice.nodes:
+                if node.prompt:
+                    _make_depend_on(choice, node.prompt[1])
+
+            # The default symbol conditions
+            for _, cond in choice.defaults:
+                _make_depend_on(choice, cond)
+
+    def _add_choice_deps(self):
+        # Choices also depend on the choice symbols themselves, because the
+        # y-mode selection of the choice might change if a choice symbol's
+        # visibility changes.
+        #
+        # We add these dependencies separately after dependency loop detection.
+        # The invalidation algorithm can handle the resulting
+        # <choice symbol> <-> <choice> dependency loops, but they make loop
+        # detection awkward.
+
+        for choice in self.unique_choices:
+            # The choice symbols themselves, because the y mode selection might
+            # change if a choice symbol's visibility changes
+            for sym in choice.syms:
+                sym._dependents.add(choice)
+
+    def _invalidate_all(self):
+        # Undefined symbols never change value and don't need to be
+        # invalidated, so we can just iterate over defined symbols.
+        # Invalidating constant symbols would break things horribly.
+        for sym in self.unique_defined_syms:
+            sym._invalidate()
+
+        for choice in self.unique_choices:
+            choice._invalidate()
+
+
+    #
+    # Post-parsing menu tree processing, including dependency propagation and
+    # implicit submenu creation
+    #
+
+    def _finalize_tree(self, node, visible_if):
+        # Propagates properties and dependencies, creates implicit menus (see
+        # kconfig-language.txt), removes 'if' nodes, and finalizes choices.
+        # This pretty closely mirrors menu_finalize() from the C
+        # implementation, with some minor tweaks (MenuNode holds lists of
+        # properties instead of each property having a MenuNode pointer, for
+        # example).
+        #
+        # node:
+        #   The current "parent" menu node, from which we propagate
+        #   dependencies
+        #
+        # visible_if:
+        #   Dependencies from 'visible if' on parent menus. These are added to
+        #   the prompts of symbols and choices.
+
+        if node.list:
+            # The menu node is a choice, menu, or if. Finalize each child in
+            # it.
+
+            if node.item is MENU:
+                visible_if = self._make_and(visible_if, node.visibility)
+
+            # Propagate the menu node's dependencies to each child menu node.
+            #
+            # The recursive _finalize_tree() calls assume that the current
+            # "level" in the tree has already had dependencies propagated. This
+            # makes e.g. implicit submenu creation easier, because it needs to
+            # look ahead.
+            self._propagate_deps(node, visible_if)
+
+            # Finalize the children
+            cur = node.list
+            while cur:
+                self._finalize_tree(cur, visible_if)
+                cur = cur.next
+
+        elif isinstance(node.item, Symbol):
+            # Add the node's non-node-specific properties (defaults, ranges,
+            # etc.) to the Symbol
+            self._add_props_to_sc(node)
+
+            # See if we can create an implicit menu rooted at the Symbol and
+            # finalize each child menu node in that menu if so, like for the
+            # choice/menu/if case above
+            cur = node
+            while cur.next and _auto_menu_dep(node, cur.next):
+                # This also makes implicit submenu creation work recursively,
+                # with implicit menus inside implicit menus
+                self._finalize_tree(cur.next, visible_if)
+                cur = cur.next
+                cur.parent = node
+
+            if cur is not node:
+                # Found symbols that should go in an implicit submenu. Tilt
+                # them up above us.
+                node.list = node.next
+                node.next = cur.next
+                cur.next = None
+
+
+        if node.list:
+            # We have a parent node with individually finalized child nodes. Do
+            # final steps to finalize this "level" in the menu tree.
+            _flatten(node.list)
+            _remove_ifs(node)
+
+        # Empty choices (node.list None) are possible, so this needs to go
+        # outside
+        if isinstance(node.item, Choice):
+            # Add the node's non-node-specific properties to the choice
+            self._add_props_to_sc(node)
+            _finalize_choice(node)
+
+    def _propagate_deps(self, node, visible_if):
+        # Propagates 'node's dependencies to its child menu nodes
+
+        # If the parent node holds a Choice, we use the Choice itself as the
+        # parent dependency. This makes sense as the value (mode) of the choice
+        # limits the visibility of the contained choice symbols. The C
+        # implementation works the same way.
+        #
+        # Due to the similar interface, Choice works as a drop-in replacement
+        # for Symbol here.
+        basedep = node.item if isinstance(node.item, Choice) else node.dep
+
+        cur = node.list
+        while cur:
+            cur.dep = dep = self._make_and(cur.dep, basedep)
+
+            # Propagate dependencies to prompt
+            if cur.prompt:
+                cur.prompt = (cur.prompt[0],
+                              self._make_and(cur.prompt[1], dep))
+
+            if isinstance(cur.item, (Symbol, Choice)):
+                sc = cur.item
+
+                # Propagate 'visible if' dependencies to the prompt
+                if cur.prompt:
+                    cur.prompt = (cur.prompt[0],
+                                  self._make_and(cur.prompt[1], visible_if))
+
+                # Propagate dependencies to defaults
+                if cur.defaults:
+                    cur.defaults = [(default, self._make_and(cond, dep))
+                                    for default, cond in cur.defaults]
+
+                # Propagate dependencies to ranges
+                if cur.ranges:
+                    cur.ranges = [(low, high, self._make_and(cond, dep))
+                                  for low, high, cond in cur.ranges]
+
+                # Propagate dependencies to selects
+                if cur.selects:
+                    cur.selects = [(target, self._make_and(cond, dep))
+                                   for target, cond in cur.selects]
+
+                # Propagate dependencies to implies
+                if cur.implies:
+                    cur.implies = [(target, self._make_and(cond, dep))
+                                   for target, cond in cur.implies]
+
+
+            cur = cur.next
+
+    def _add_props_to_sc(self, node):
+        # Copies properties from the menu node 'node' up to its contained
+        # symbol or choice.
+        #
+        # This can't be rolled into _propagate_deps(), because that function
+        # traverses the menu tree roughly breadth-first order, meaning
+        # properties on symbols and choices defined in multiple locations could
+        # end up in the wrong order.
+
+        # Symbol or choice
+        sc = node.item
+
+        # See the Symbol class docstring
+        sc.direct_dep = self._make_or(sc.direct_dep, node.dep)
+
+        sc.defaults += node.defaults
+
+        # The properties below aren't available on choices
+
+        if node.ranges:
+            sc.ranges += node.ranges
+
+        if node.selects:
+            sc.selects += node.selects
+
+            # Modify the reverse dependencies of the selected symbol
+            for target, cond in node.selects:
+                target.rev_dep = self._make_or(
+                    target.rev_dep,
+                    self._make_and(sc, cond))
+
+        if node.implies:
+            sc.implies += node.implies
+
+            # Modify the weak reverse dependencies of the implied
+            # symbol
+            for target, cond in node.implies:
+                target.weak_rev_dep = self._make_or(
+                    target.weak_rev_dep,
+                    self._make_and(sc, cond))
+
+
+    #
+    # Misc.
+    #
+
+    def _parse_error(self, msg):
+        if self._filename is None:
+            loc = ""
+        else:
+            loc = "{}:{}: ".format(self._filename, self._linenr)
+
+        raise KconfigError(
+            "{}couldn't parse '{}': {}".format(loc, self._line.rstrip(), msg))
+
+    def _open(self, filename, mode):
+        # open() wrapper:
+        #
+        # - Enable universal newlines mode on Python 2 to ease
+        #   interoperability between Linux and Windows. It's already the
+        #   default on Python 3.
+        #
+        #   The "U" flag would currently work for both Python 2 and 3, but it's
+        #   deprecated on Python 3, so play it future-safe.
+        #
+        #   A simpler solution would be to use io.open(), which defaults to
+        #   universal newlines on both Python 2 and 3 (and is an alias for
+        #   open() on Python 3), but it's appreciably slower on Python 2:
+        #
+        #     Parsing x86 Kconfigs on Python 2
+        #
+        #     with open(..., "rU"):
+        #
+        #       real  0m0.930s
+        #       user  0m0.905s
+        #       sys   0m0.025s
+        #
+        #     with io.open():
+        #
+        #       real  0m1.069s
+        #       user  0m1.040s
+        #       sys   0m0.029s
+        #
+        #   There's no appreciable performance difference between "r" and
+        #   "rU" for parsing performance on Python 2.
+        #
+        # - For Python 3, force the encoding. Forcing the encoding on Python 2
+        #   turns strings into Unicode strings, which gets messy. Python 2
+        #   doesn't decode regular strings anyway.
+        return open(filename, "rU" if mode == "r" else mode) if _IS_PY2 else \
+               open(filename, mode, encoding=self._encoding)
+
+    def _check_undef_syms(self):
+        # Prints warnings for all references to undefined symbols within the
+        # Kconfig files
+
+        for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)():
+            # - sym.nodes empty means the symbol is undefined (has no
+            #   definition locations)
+            #
+            # - Due to Kconfig internals, numbers show up as undefined Kconfig
+            #   symbols, but shouldn't be flagged
+            #
+            # - The MODULES symbol always exists
+            if not sym.nodes and not _is_num(sym.name) and \
+               sym.name != "MODULES":
+
+                msg = "undefined symbol {}:".format(sym.name)
+
+                for node in self.node_iter():
+                    if sym in node.referenced:
+                        msg += "\n\n- Referenced at {}:{}:\n\n{}" \
+                               .format(node.filename, node.linenr, node)
+
+                self._warn(msg)
+
+    def _warn(self, msg, filename=None, linenr=None):
+        # For printing general warnings
+
+        if self._warnings_enabled:
+            msg = "warning: " + msg
+            if filename is not None:
+                msg = "{}:{}: {}".format(filename, linenr, msg)
+
+            self.warnings.append(msg)
+            if self._warn_to_stderr:
+                sys.stderr.write(msg + "\n")
+
+    def _warn_undef_assign(self, msg, filename=None, linenr=None):
+        # See the class documentation
+
+        if self._warn_for_undef_assign:
+            self._warn(msg, filename, linenr)
+
+    def _warn_undef_assign_load(self, name, val, filename, linenr):
+        # Special version for load_config()
+
+        self._warn_undef_assign(
+            'attempt to assign the value "{}" to the undefined symbol {}'
+            .format(val, name), filename, linenr)
+
+    def _warn_redun_assign(self, msg, filename=None, linenr=None):
+        # See the class documentation
+
+        if self._warn_for_redun_assign:
+            self._warn(msg, filename, linenr)
+
+    def _srctree_hint(self):
+        # Hint printed when Kconfig files can't be found or .config files can't
+        # be opened
+
+        return ". Perhaps the $srctree environment variable ({}) " \
+               "is set incorrectly. Note that the current value of $srctree " \
+               "is saved when the Kconfig instance is created (for " \
+               "consistency and to cleanly separate instances)." \
+               .format("set to '{}'".format(self.srctree) if self.srctree
+                           else "unset or blank")
+
+class Symbol(object):
+    """
+    Represents a configuration symbol:
+
+      (menu)config FOO
+          ...
+
+    The following attributes are available. They should be viewed as read-only,
+    and some are implemented through @property magic (but are still efficient
+    to access due to internal caching).
+
+    Note: Prompts, help texts, and locations are stored in the Symbol's
+    MenuNode(s) rather than in the Symbol itself. Check the MenuNode class and
+    the Symbol.nodes attribute. This organization matches the C tools.
+
+    name:
+      The name of the symbol, e.g. "FOO" for 'config FOO'.
+
+    type:
+      The type of the symbol. One of BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN.
+      UNKNOWN is for undefined symbols, (non-special) constant symbols, and
+      symbols defined without a type.
+
+      When running without modules (MODULES having the value n), TRISTATE
+      symbols magically change type to BOOL. This also happens for symbols
+      within choices in "y" mode. This matches the C tools, and makes sense for
+      menuconfig-like functionality.
+
+    orig_type:
+      The type as given in the Kconfig file, without any magic applied. Used
+      when printing the symbol.
+
+    str_value:
+      The value of the symbol as a string. Gives the value for string/int/hex
+      symbols. For bool/tristate symbols, gives "n", "m", or "y".
+
+      This is the symbol value that's used in relational expressions
+      (A = B, A != B, etc.)
+
+      Gotcha: For int/hex symbols, the exact format of the value must often be
+      preserved (e.g., when writing a .config file), hence why you can't get it
+      directly as an int. Do int(int_sym.str_value) or
+      int(hex_sym.str_value, 16) to get the integer value.
+
+    tri_value:
+      The tristate value of the symbol as an integer. One of 0, 1, 2,
+      representing n, m, y. Always 0 (n) for non-bool/tristate symbols.
+
+      This is the symbol value that's used outside of relation expressions
+      (A, !A, A && B, A || B).
+
+    assignable:
+      A tuple containing the tristate user values that can currently be
+      assigned to the symbol (that would be respected), ordered from lowest (0,
+      representing n) to highest (2, representing y). This corresponds to the
+      selections available in the menuconfig interface. The set of assignable
+      values is calculated from the symbol's visibility and selects/implies.
+
+      Returns the empty set for non-bool/tristate symbols and for symbols with
+      visibility n. The other possible values are (0, 2), (0, 1, 2), (1, 2),
+      (1,), and (2,). A (1,) or (2,) result means the symbol is visible but
+      "locked" to m or y through a select, perhaps in combination with the
+      visibility. menuconfig represents this as -M- and -*-, respectively.
+
+      For string/hex/int symbols, check if Symbol.visibility is non-0 (non-n)
+      instead to determine if the value can be changed.
+
+      Some handy 'assignable' idioms:
+
+        # Is 'sym' an assignable (visible) bool/tristate symbol?
+        if sym.assignable:
+            # What's the highest value it can be assigned? [-1] in Python
+            # gives the last element.
+            sym_high = sym.assignable[-1]
+
+            # The lowest?
+            sym_low = sym.assignable[0]
+
+            # Can the symbol be set to at least m?
+            if sym.assignable[-1] >= 1:
+                ...
+
+        # Can the symbol be set to m?
+        if 1 in sym.assignable:
+            ...
+
+    visibility:
+      The visibility of the symbol. One of 0, 1, 2, representing n, m, y. See
+      the module documentation for an overview of symbol values and visibility.
+
+    user_value:
+      The user value of the symbol. None if no user value has been assigned
+      (via Kconfig.load_config() or Symbol.set_value()).
+
+      Holds 0, 1, or 2 for bool/tristate symbols, and a string for the other
+      symbol types.
+
+      WARNING: Do not assign directly to this. It will break things. Use
+      Symbol.set_value().
+
+    config_string:
+      The .config assignment string that would get written out for the symbol
+      by Kconfig.write_config(). Returns the empty string if no .config
+      assignment would get written out. In general, visible symbols, symbols
+      with (active) defaults, and selected symbols get written out.
+
+    nodes:
+      A list of MenuNodes for this symbol. Will contain a single MenuNode for
+      most symbols. Undefined and constant symbols have an empty nodes list.
+      Symbols defined in multiple locations get one node for each location.
+
+    choice:
+      Holds the parent Choice for choice symbols, and None for non-choice
+      symbols. Doubles as a flag for whether a symbol is a choice symbol.
+
+    defaults:
+      List of (default, cond) tuples for the symbol's 'default' properties. For
+      example, 'default A && B if C || D' is represented as
+      ((AND, A, B), (OR, C, D)). If no condition was given, 'cond' is
+      self.kconfig.y.
+
+      Note that 'depends on' and parent dependencies are propagated to
+      'default' conditions.
+
+    selects:
+      List of (symbol, cond) tuples for the symbol's 'select' properties. For
+      example, 'select A if B && C' is represented as (A, (AND, B, C)). If no
+      condition was given, 'cond' is self.kconfig.y.
+
+      Note that 'depends on' and parent dependencies are propagated to 'select'
+      conditions.
+
+    implies:
+      Like 'selects', for imply.
+
+    ranges:
+      List of (low, high, cond) tuples for the symbol's 'range' properties. For
+      example, 'range 1 2 if A' is represented as (1, 2, A). If there is no
+      condition, 'cond' is self.config.y.
+
+      Note that 'depends on' and parent dependencies are propagated to 'range'
+      conditions.
+
+      Gotcha: 1 and 2 above will be represented as (undefined) Symbols rather
+      than plain integers. Undefined symbols get their name as their string
+      value, so this works out. The C tools work the same way.
+
+    rev_dep:
+      Reverse dependency expression from other symbols selecting this symbol.
+      Multiple selections get ORed together. A condition on a select is ANDed
+      with the selecting symbol.
+
+      For example, if A has 'select FOO' and B has 'select FOO if C', then
+      FOO's rev_dep will be (OR, A, (AND, B, C)).
+
+    weak_rev_dep:
+      Like rev_dep, for imply.
+
+    direct_dep:
+      The 'depends on' dependencies. If a symbol is defined in multiple
+      locations, the dependencies at each location are ORed together.
+
+      Internally, this is used to implement 'imply', which only applies if the
+      implied symbol has expr_value(self.direct_dep) != 0. 'depends on' and
+      parent dependencies are automatically propagated to the conditions of
+      properties, so normally it's redundant to check the direct dependencies.
+
+    referenced:
+      A set() with all symbols and choices referenced in the properties and
+      property conditions of the symbol.
+
+      Also includes dependencies inherited from surrounding menus and if's.
+      Choices appear in the dependencies of choice symbols.
+
+    env_var:
+      If the Symbol has an 'option env="FOO"' option, this contains the name
+      ("FOO") of the environment variable. None for symbols without no
+      'option env'.
+
+      'option env="FOO"' acts like a 'default' property whose value is the
+      value of $FOO.
+
+      Symbols with 'option env' are never written out to .config files, even if
+      they are visible. env_var corresponds to a flag called SYMBOL_AUTO in the
+      C implementation.
+
+    is_allnoconfig_y:
+      True if the symbol has 'option allnoconfig_y' set on it. This has no
+      effect internally (except when printing symbols), but can be checked by
+      scripts.
+
+    is_constant:
+      True if the symbol is a constant (quoted) symbol.
+
+    kconfig:
+      The Kconfig instance this symbol is from.
+    """
+    __slots__ = (
+        "_cached_assignable",
+        "_cached_str_val",
+        "_cached_tri_val",
+        "_cached_vis",
+        "_dependents",
+        "_old_val",
+        "_visited",
+        "_was_set",
+        "_write_to_conf",
+        "choice",
+        "defaults",
+        "direct_dep",
+        "env_var",
+        "implies",
+        "is_allnoconfig_y",
+        "is_constant",
+        "kconfig",
+        "name",
+        "nodes",
+        "orig_type",
+        "ranges",
+        "rev_dep",
+        "selects",
+        "user_value",
+        "weak_rev_dep",
+    )
+
+    #
+    # Public interface
+    #
+
+    @property
+    def type(self):
+        """
+        See the class documentation.
+        """
+        if self.orig_type is TRISTATE and \
+           ((self.choice and self.choice.tri_value == 2) or
+            not self.kconfig.modules.tri_value):
+            return BOOL
+
+        return self.orig_type
+
+    @property
+    def str_value(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_str_val is not None:
+            return self._cached_str_val
+
+        if self.orig_type in (BOOL, TRISTATE):
+            # Also calculates the visibility, so invalidation safe
+            self._cached_str_val = TRI_TO_STR[self.tri_value]
+            return self._cached_str_val
+
+        # As a quirk of Kconfig, undefined symbols get their name as their
+        # string value. This is why things like "FOO = bar" work for seeing if
+        # FOO has the value "bar".
+        if self.orig_type is UNKNOWN:
+            self._cached_str_val = self.name
+            return self.name
+
+        val = ""
+        # Warning: See Symbol._rec_invalidate(), and note that this is a hidden
+        # function call (property magic)
+        vis = self.visibility
+
+        self._write_to_conf = (vis != 0)
+
+        if self.orig_type in (INT, HEX):
+            # The C implementation checks the user value against the range in a
+            # separate code path (post-processing after loading a .config).
+            # Checking all values here instead makes more sense for us. It
+            # requires that we check for a range first.
+
+            base = _TYPE_TO_BASE[self.orig_type]
+
+            # Check if a range is in effect
+            for low_expr, high_expr, cond in self.ranges:
+                if expr_value(cond):
+                    has_active_range = True
+
+                    # The zeros are from the C implementation running strtoll()
+                    # on empty strings
+                    low = int(low_expr.str_value, base) if \
+                      _is_base_n(low_expr.str_value, base) else 0
+                    high = int(high_expr.str_value, base) if \
+                      _is_base_n(high_expr.str_value, base) else 0
+
+                    break
+            else:
+                has_active_range = False
+
+            # Defaults are used if the symbol is invisible, lacks a user value,
+            # or has an out-of-range user value.
+            use_defaults = True
+
+            if vis and self.user_value:
+                user_val = int(self.user_value, base)
+                if has_active_range and not low <= user_val <= high:
+                    num2str = str if base == 10 else hex
+                    self.kconfig._warn(
+                        "user value {} on the {} symbol {} ignored due to "
+                        "being outside the active range ([{}, {}]) -- falling "
+                        "back on defaults"
+                        .format(num2str(user_val), TYPE_TO_STR[self.orig_type],
+                                _name_and_loc(self),
+                                num2str(low), num2str(high)))
+                else:
+                    # If the user value is well-formed and satisfies range
+                    # contraints, it is stored in exactly the same form as
+                    # specified in the assignment (with or without "0x", etc.)
+                    val = self.user_value
+                    use_defaults = False
+
+            if use_defaults:
+                # No user value or invalid user value. Look at defaults.
+
+                # Used to implement the warning below
+                has_default = False
+
+                for val_sym, cond in self.defaults:
+                    if expr_value(cond):
+                        has_default = self._write_to_conf = True
+
+                        val = val_sym.str_value
+
+                        if _is_base_n(val, base):
+                            val_num = int(val, base)
+                        else:
+                            val_num = 0  # strtoll() on empty string
+
+                        break
+                else:
+                    val_num = 0  # strtoll() on empty string
+
+                # This clamping procedure runs even if there's no default
+                if has_active_range:
+                    clamp = None
+                    if val_num < low:
+                        clamp = low
+                    elif val_num > high:
+                        clamp = high
+
+                    if clamp is not None:
+                        # The value is rewritten to a standard form if it is
+                        # clamped
+                        val = str(clamp) \
+                              if self.orig_type is INT else \
+                              hex(clamp)
+
+                        if has_default:
+                            num2str = str if base == 10 else hex
+                            self.kconfig._warn(
+                                "default value {} on {} clamped to {} due to "
+                                "being outside the active range ([{}, {}])"
+                                .format(val_num, _name_and_loc(self),
+                                        num2str(clamp), num2str(low),
+                                        num2str(high)))
+
+        elif self.orig_type is STRING:
+            if vis and self.user_value is not None:
+                # If the symbol is visible and has a user value, use that
+                val = self.user_value
+            else:
+                # Otherwise, look at defaults
+                for val_sym, cond in self.defaults:
+                    if expr_value(cond):
+                        val = val_sym.str_value
+                        self._write_to_conf = True
+                        break
+
+        # env_var corresponds to SYMBOL_AUTO in the C implementation, and is
+        # also set on the defconfig_list symbol there. Test for the
+        # defconfig_list symbol explicitly instead here, to avoid a nonsensical
+        # env_var setting and the defconfig_list symbol being printed
+        # incorrectly. This code is pretty cold anyway.
+        if self.env_var is not None or self is self.kconfig.defconfig_list:
+            self._write_to_conf = False
+
+        self._cached_str_val = val
+        return val
+
+    @property
+    def tri_value(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_tri_val is not None:
+            return self._cached_tri_val
+
+        if self.orig_type not in (BOOL, TRISTATE):
+            if self.orig_type is not UNKNOWN:
+                # Would take some work to give the location here
+                self.kconfig._warn(
+                    "The {} symbol {} is being evaluated in a logical context "
+                    "somewhere. It will always evaluate to n."
+                    .format(TYPE_TO_STR[self.orig_type], _name_and_loc(self)))
+
+            self._cached_tri_val = 0
+            return 0
+
+        # Warning: See Symbol._rec_invalidate(), and note that this is a hidden
+        # function call (property magic)
+        vis = self.visibility
+        self._write_to_conf = (vis != 0)
+
+        val = 0
+
+        if not self.choice:
+            # Non-choice symbol
+
+            if vis and self.user_value is not None:
+                # If the symbol is visible and has a user value, use that
+                val = min(self.user_value, vis)
+
+            else:
+                # Otherwise, look at defaults and weak reverse dependencies
+                # (implies)
+
+                for default, cond in self.defaults:
+                    cond_val = expr_value(cond)
+                    if cond_val:
+                        val = min(expr_value(default), cond_val)
+                        if val:
+                            self._write_to_conf = True
+                        break
+
+                # Weak reverse dependencies are only considered if our
+                # direct dependencies are met
+                weak_rev_dep_val = expr_value(self.weak_rev_dep)
+                if weak_rev_dep_val and expr_value(self.direct_dep):
+                    val = max(weak_rev_dep_val, val)
+                    self._write_to_conf = True
+
+            # Reverse (select-related) dependencies take precedence
+            rev_dep_val = expr_value(self.rev_dep)
+            if rev_dep_val:
+                if expr_value(self.direct_dep) < rev_dep_val:
+                    self._warn_select_unsatisfied_deps()
+
+                val = max(rev_dep_val, val)
+                self._write_to_conf = True
+
+            # m is promoted to y for (1) bool symbols and (2) symbols with a
+            # weak_rev_dep (from imply) of y
+            if val == 1 and \
+               (self.type is BOOL or expr_value(self.weak_rev_dep) == 2):
+                val = 2
+
+        elif vis == 2:
+            # Visible choice symbol in y-mode choice. The choice mode limits
+            # the visibility of choice symbols, so it's sufficient to just
+            # check the visibility of the choice symbols themselves.
+            val = 2 if self.choice.selection is self else 0
+
+        elif vis and self.user_value:
+            # Visible choice symbol in m-mode choice, with set non-0 user value
+            val = 1
+
+        self._cached_tri_val = val
+        return val
+
+    @property
+    def assignable(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_assignable is None:
+            self._cached_assignable = self._assignable()
+
+        return self._cached_assignable
+
+    @property
+    def visibility(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_vis is None:
+            self._cached_vis = _visibility(self)
+
+        return self._cached_vis
+
+    @property
+    def config_string(self):
+        """
+        See the class documentation.
+        """
+        # Note: _write_to_conf is determined when the value is calculated. This
+        # is a hidden function call due to property magic.
+        val = self.str_value
+        if not self._write_to_conf:
+            return ""
+
+        if self.orig_type in (BOOL, TRISTATE):
+            return "{}{}={}\n" \
+                   .format(self.kconfig.config_prefix, self.name, val) \
+                   if val != "n" else \
+                   "# {}{} is not set\n" \
+                   .format(self.kconfig.config_prefix, self.name)
+
+        if self.orig_type in (INT, HEX):
+            return "{}{}={}\n" \
+                   .format(self.kconfig.config_prefix, self.name, val)
+
+        if self.orig_type is STRING:
+            return '{}{}="{}"\n' \
+                   .format(self.kconfig.config_prefix, self.name, escape(val))
+
+        _internal_error("Internal error while creating .config: unknown "
+                        'type "{}".'.format(self.orig_type))
+
+    def set_value(self, value):
+        """
+        Sets the user value of the symbol.
+
+        Equal in effect to assigning the value to the symbol within a .config
+        file. For bool and tristate symbols, use the 'assignable' attribute to
+        check which values can currently be assigned. Setting values outside
+        'assignable' will cause Symbol.user_value to differ from
+        Symbol.str/tri_value (be truncated down or up).
+
+        Setting a choice symbol to 2 (y) sets Choice.user_selection to the
+        choice symbol in addition to setting Symbol.user_value.
+        Choice.user_selection is considered when the choice is in y mode (the
+        "normal" mode).
+
+        Other symbols that depend (possibly indirectly) on this symbol are
+        automatically recalculated to reflect the assigned value.
+
+        value:
+          The user value to give to the symbol. For bool and tristate symbols,
+          n/m/y can be specified either as 0/1/2 (the usual format for tristate
+          values in Kconfiglib) or as one of the strings "n"/"m"/"y". For other
+          symbol types, pass a string.
+
+          Values that are invalid for the type (such as "foo" or 1 (m) for a
+          BOOL or "0x123" for an INT) are ignored and won't be stored in
+          Symbol.user_value. Kconfiglib will print a warning by default for
+          invalid assignments, and set_value() will return False.
+
+        Returns True if the value is valid for the type of the symbol, and
+        False otherwise. This only looks at the form of the value. For BOOL and
+        TRISTATE symbols, check the Symbol.assignable attribute to see what
+        values are currently in range and would actually be reflected in the
+        value of the symbol. For other symbol types, check whether the
+        visibility is non-n.
+        """
+        # If the new user value matches the old, nothing changes, and we can
+        # save some work.
+        #
+        # This optimization is skipped for choice symbols: Setting a choice
+        # symbol's user value to y might change the state of the choice, so it
+        # wouldn't be safe (symbol user values always match the values set in a
+        # .config file or via set_value(), and are never implicitly updated).
+        if value == self.user_value and not self.choice:
+            self._was_set = True
+            return True
+
+        # Check if the value is valid for our type
+        if not (self.orig_type is BOOL     and value in (0, 2, "n", "y")         or
+                self.orig_type is TRISTATE and value in (0, 1, 2, "n", "m", "y") or
+                (isinstance(value, str)    and
+                 (self.orig_type is STRING                        or
+                  self.orig_type is INT and _is_base_n(value, 10) or
+                  self.orig_type is HEX and _is_base_n(value, 16)
+                                        and int(value, 16) >= 0))):
+
+            # Display tristate values as n, m, y in the warning
+            self.kconfig._warn(
+                "the value {} is invalid for {}, which has type {} -- "
+                "assignment ignored"
+                .format(TRI_TO_STR[value] if value in (0, 1, 2) else
+                            "'{}'".format(value),
+                        _name_and_loc(self), TYPE_TO_STR[self.orig_type]))
+
+            return False
+
+        if self.orig_type in (BOOL, TRISTATE) and value in ("n", "m", "y"):
+            value = STR_TO_TRI[value]
+
+        self.user_value = value
+        self._was_set = True
+
+        if self.choice and value == 2:
+            # Setting a choice symbol to y makes it the user selection of the
+            # choice. Like for symbol user values, the user selection is not
+            # guaranteed to match the actual selection of the choice, as
+            # dependencies come into play.
+            self.choice.user_selection = self
+            self.choice._was_set = True
+            self.choice._rec_invalidate()
+        else:
+            self._rec_invalidate_if_has_prompt()
+
+        return True
+
+    def unset_value(self):
+        """
+        Resets the user value of the symbol, as if the symbol had never gotten
+        a user value via Kconfig.load_config() or Symbol.set_value().
+        """
+        if self.user_value is not None:
+            self.user_value = None
+            self._rec_invalidate_if_has_prompt()
+
+    @property
+    def referenced(self):
+        """
+        See the class documentation.
+        """
+        res = set()
+        for node in self.nodes:
+            res |= node.referenced
+
+        return res
+
+    def __repr__(self):
+        """
+        Returns a string with information about the symbol (including its name,
+        value, visibility, and location(s)) when it is evaluated on e.g. the
+        interactive Python prompt.
+        """
+        fields = []
+
+        fields.append("symbol " + self.name)
+        fields.append(TYPE_TO_STR[self.type])
+
+        for node in self.nodes:
+            if node.prompt:
+                fields.append('"{}"'.format(node.prompt[0]))
+
+        # Only add quotes for non-bool/tristate symbols
+        fields.append("value " +
+                      (self.str_value
+                       if self.orig_type in (BOOL, TRISTATE) else
+                       '"{}"'.format(self.str_value)))
+
+        if not self.is_constant:
+            # These aren't helpful to show for constant symbols
+
+            if self.user_value is not None:
+                # Only add quotes for non-bool/tristate symbols
+                fields.append("user value " +
+                              (TRI_TO_STR[self.user_value]
+                               if self.orig_type in (BOOL, TRISTATE) else
+                               '"{}"'.format(self.user_value)))
+
+            fields.append("visibility " + TRI_TO_STR[self.visibility])
+
+            if self.choice:
+                fields.append("choice symbol")
+
+            if self.is_allnoconfig_y:
+                fields.append("allnoconfig_y")
+
+            if self is self.kconfig.defconfig_list:
+                fields.append("is the defconfig_list symbol")
+
+            if self.env_var is not None:
+                fields.append("from environment variable " + self.env_var)
+
+            if self is self.kconfig.modules:
+                fields.append("is the modules symbol")
+
+            fields.append("direct deps " +
+                          TRI_TO_STR[expr_value(self.direct_dep)])
+
+        if self.nodes:
+            for node in self.nodes:
+                fields.append("{}:{}".format(node.filename, node.linenr))
+        else:
+            if self.is_constant:
+                fields.append("constant")
+            else:
+                fields.append("undefined")
+
+        return "<{}>".format(", ".join(fields))
+
+    def __str__(self):
+        """
+        Returns a string representation of the symbol when it is printed,
+        matching the Kconfig format, with parent dependencies propagated.
+
+        The string is constructed by joining the strings returned by
+        MenuNode.__str__() for each of the symbol's menu nodes, so symbols
+        defined in multiple locations will return a string with all
+        definitions.
+
+        An empty string is returned for undefined and constant symbols.
+        """
+        return self.custom_str(standard_sc_expr_str)
+
+    def custom_str(self, sc_expr_str_fn):
+        """
+        Works like Symbol.__str__(), but allows a custom format to be used for
+        all symbol/choice references. See expr_str().
+        """
+        return "\n".join(node.custom_str(sc_expr_str_fn)
+                         for node in self.nodes)
+
+    #
+    # Private methods
+    #
+
+    def __init__(self):
+        """
+        Symbol constructor -- not intended to be called directly by Kconfiglib
+        clients.
+        """
+        # These attributes are always set on the instance from outside and
+        # don't need defaults:
+        #   kconfig
+        #   direct_dep
+        #   is_constant
+        #   name
+        #   rev_dep
+        #   weak_rev_dep
+
+        self.orig_type = UNKNOWN
+        self.defaults = []
+        self.selects = []
+        self.implies = []
+        self.ranges = []
+
+        self.nodes = []
+
+        self.user_value = \
+        self.choice = \
+        self.env_var = \
+        self._cached_str_val = self._cached_tri_val = self._cached_vis = \
+        self._cached_assignable = None
+
+        # _write_to_conf is calculated along with the value. If True, the
+        # Symbol gets a .config entry.
+
+        self.is_allnoconfig_y = \
+        self._was_set = \
+        self._write_to_conf = False
+
+        # See Kconfig._build_dep()
+        self._dependents = set()
+
+        # Used during dependency loop detection and (independently) in
+        # node_iter()
+        self._visited = 0
+
+    def _assignable(self):
+        # Worker function for the 'assignable' attribute
+
+        if self.orig_type not in (BOOL, TRISTATE):
+            return ()
+
+        # Warning: See Symbol._rec_invalidate(), and note that this is a hidden
+        # function call (property magic)
+        vis = self.visibility
+
+        if not vis:
+            return ()
+
+        rev_dep_val = expr_value(self.rev_dep)
+
+        if vis == 2:
+            if self.choice:
+                return (2,)
+
+            if not rev_dep_val:
+                if self.type is BOOL or expr_value(self.weak_rev_dep) == 2:
+                    return (0, 2)
+                return (0, 1, 2)
+
+            if rev_dep_val == 2:
+                return (2,)
+
+            # rev_dep_val == 1
+
+            if self.type is BOOL or expr_value(self.weak_rev_dep) == 2:
+                return (2,)
+            return (1, 2)
+
+        # vis == 1
+
+        # Must be a tristate here, because bool m visibility gets promoted to y
+
+        if not rev_dep_val:
+            return (0, 1) if expr_value(self.weak_rev_dep) != 2 else (0, 2)
+
+        if rev_dep_val == 2:
+            return (2,)
+
+        # vis == rev_dep_val == 1
+
+        return (1,)
+
+    def _invalidate(self):
+        # Marks the symbol as needing to be recalculated
+
+        self._cached_str_val = self._cached_tri_val = self._cached_vis = \
+            self._cached_assignable = None
+
+    def _rec_invalidate(self):
+        # Invalidates the symbol and all items that (possibly) depend on it
+
+        if self is self.kconfig.modules:
+            # Invalidating MODULES has wide-ranging effects
+            self.kconfig._invalidate_all()
+        else:
+            self._invalidate()
+
+            for item in self._dependents:
+                # _cached_vis doubles as a flag that tells us whether 'item'
+                # has cached values, because it's calculated as a side effect
+                # of calculating all other (non-constant) cached values.
+                #
+                # If item._cached_vis is None, it means there can't be cached
+                # values on other items that depend on 'item', because if there
+                # were, some value on 'item' would have been calculated and
+                # item._cached_vis set as a side effect. It's therefore safe to
+                # stop the invalidation at symbols with _cached_vis None.
+                #
+                # This approach massively speeds up scripts that set a lot of
+                # values, vs simply invalidating all possibly dependent symbols
+                # (even when you already have a list of all the dependent
+                # symbols, because some symbols get huge dependency trees).
+                #
+                # This gracefully handles dependency loops too, which is nice
+                # for choices, where the choice depends on the choice symbols
+                # and vice versa.
+                if item._cached_vis is not None:
+                    item._rec_invalidate()
+
+    def _rec_invalidate_if_has_prompt(self):
+        # Invalidates the symbol and its dependent symbols, but only if the
+        # symbol has a prompt. User values never have an effect on promptless
+        # symbols, so we skip invalidation for them as an optimization.
+        #
+        # This also prevents constant (quoted) symbols from being invalidated
+        # if set_value() is called on them, which would cause them to lose
+        # their value and break things.
+        #
+        # Prints a warning if the symbol has no prompt. In some contexts (e.g.
+        # when loading a .config files) assignments to promptless symbols are
+        # normal and expected, so the warning can be disabled.
+
+        for node in self.nodes:
+            if node.prompt:
+                self._rec_invalidate()
+                return
+
+        if self.kconfig._warn_for_no_prompt:
+            self.kconfig._warn(_name_and_loc(self) + " has no prompt, meaning "
+                               "user values have no effect on it")
+
+    def _str_default(self):
+        # write_min_config() helper function. Returns the value the symbol
+        # would get from defaults if it didn't have a user value. Uses exactly
+        # the same algorithm as the C implementation (though a bit cleaned up),
+        # for compatibility.
+
+        if self.orig_type in (BOOL, TRISTATE):
+            val = 0
+
+            # Defaults, selects, and implies do not affect choice symbols
+            if not self.choice:
+                for default, cond in self.defaults:
+                    cond_val = expr_value(cond)
+                    if cond_val:
+                        val = min(expr_value(default), cond_val)
+                        break
+
+                val = max(expr_value(self.rev_dep),
+                          expr_value(self.weak_rev_dep),
+                          val)
+
+                # Transpose mod to yes if type is bool (possibly due to modules
+                # being disabled)
+                if val == 1 and self.type is BOOL:
+                    val = 2
+
+            return TRI_TO_STR[val]
+
+        if self.orig_type in (STRING, INT, HEX):
+            for default, cond in self.defaults:
+                if expr_value(cond):
+                    return default.str_value
+
+        return ""
+
+    def _warn_select_unsatisfied_deps(self):
+        # Helper for printing an informative warning when a symbol with
+        # unsatisfied direct dependencies (dependencies from 'depends on', ifs,
+        # and menus) is selected by some other symbol. Also warn if a symbol
+        # whose direct dependencies evaluate to m is selected to y.
+
+        msg = "{} has direct dependencies {} with value {}, but is " \
+              "currently being {}-selected by the following symbols:" \
+              .format(_name_and_loc(self), expr_str(self.direct_dep),
+                      TRI_TO_STR[expr_value(self.direct_dep)],
+                      TRI_TO_STR[expr_value(self.rev_dep)])
+
+        # The reverse dependencies from each select are ORed together
+        for select in split_expr(self.rev_dep, OR):
+            if expr_value(select) <= expr_value(self.direct_dep):
+                # Only include selects that exceed the direct dependencies
+                continue
+
+            # - 'select A if B' turns into A && B
+            # - 'select A' just turns into A
+            #
+            # In both cases, we can split on AND and pick the first operand
+            selecting_sym = split_expr(select, AND)[0]
+
+            msg += "\n - {}, with value {}, direct dependencies {} " \
+                   "(value: {})" \
+                   .format(_name_and_loc(selecting_sym),
+                           selecting_sym.str_value,
+                           expr_str(selecting_sym.direct_dep),
+                           TRI_TO_STR[expr_value(selecting_sym.direct_dep)])
+
+            if isinstance(select, tuple):
+                msg += ", and select condition {} (value: {})" \
+                       .format(expr_str(select[2]),
+                               TRI_TO_STR[expr_value(select[2])])
+
+        self.kconfig._warn(msg)
+
+class Choice(object):
+    """
+    Represents a choice statement:
+
+      choice
+          ...
+      endchoice
+
+    The following attributes are available on Choice instances. They should be
+    treated as read-only, and some are implemented through @property magic (but
+    are still efficient to access due to internal caching).
+
+    Note: Prompts, help texts, and locations are stored in the Choice's
+    MenuNode(s) rather than in the Choice itself. Check the MenuNode class and
+    the Choice.nodes attribute. This organization matches the C tools.
+
+    name:
+      The name of the choice, e.g. "FOO" for 'choice FOO', or None if the
+      Choice has no name. I can't remember ever seeing named choices in
+      practice, but the C tools support them too.
+
+    type:
+      The type of the choice. One of BOOL, TRISTATE, UNKNOWN. UNKNOWN is for
+      choices defined without a type where none of the contained symbols have a
+      type either (otherwise the choice inherits the type of the first symbol
+      defined with a type).
+
+      When running without modules (CONFIG_MODULES=n), TRISTATE choices
+      magically change type to BOOL. This matches the C tools, and makes sense
+      for menuconfig-like functionality.
+
+    orig_type:
+      The type as given in the Kconfig file, without any magic applied. Used
+      when printing the choice.
+
+    tri_value:
+      The tristate value (mode) of the choice. A choice can be in one of three
+      modes:
+
+        0 (n) - The choice is disabled and no symbols can be selected. For
+                visible choices, this mode is only possible for choices with
+                the 'optional' flag set (see kconfig-language.txt).
+
+        1 (m) - Any number of choice symbols can be set to m, the rest will
+                be n.
+
+        2 (y) - One symbol will be y, the rest n.
+
+      Only tristate choices can be in m mode. The visibility of the choice is
+      an upper bound on the mode, and the mode in turn is an upper bound on the
+      visibility of the choice symbols.
+
+      To change the mode, use Choice.set_value().
+
+      Implementation note:
+        The C tools internally represent choices as a type of symbol, with
+        special-casing in many code paths. This is why there is a lot of
+        similarity to Symbol. The value (mode) of a choice is really just a
+        normal symbol value, and an implicit reverse dependency forces its
+        lower bound to m for visible non-optional choices (the reverse
+        dependency is 'm && <visibility>').
+
+        Symbols within choices get the choice propagated as a dependency to
+        their properties. This turns the mode of the choice into an upper bound
+        on e.g. the visibility of choice symbols, and explains the gotcha
+        related to printing choice symbols mentioned in the module docstring.
+
+        Kconfiglib uses a separate Choice class only because it makes the code
+        and interface less confusing (especially in a user-facing interface).
+        Corresponding attributes have the same name in the Symbol and Choice
+        classes, for consistency and compatibility.
+
+    assignable:
+      See the symbol class documentation. Gives the assignable values (modes).
+
+    visibility:
+      See the Symbol class documentation. Acts on the value (mode).
+
+    selection:
+      The Symbol instance of the currently selected symbol. None if the Choice
+      is not in y mode or has no selected symbol (due to unsatisfied
+      dependencies on choice symbols).
+
+      WARNING: Do not assign directly to this. It will break things. Call
+      sym.set_value(2) on the choice symbol you want to select instead.
+
+    user_value:
+      The value (mode) selected by the user through Choice.set_value(). Either
+      0, 1, or 2, or None if the user hasn't selected a mode. See
+      Symbol.user_value.
+
+      WARNING: Do not assign directly to this. It will break things. Use
+      Choice.set_value() instead.
+
+    user_selection:
+      The symbol selected by the user (by setting it to y). Ignored if the
+      choice is not in y mode, but still remembered so that the choice "snaps
+      back" to the user selection if the mode is changed back to y. This might
+      differ from 'selection' due to unsatisfied dependencies.
+
+      WARNING: Do not assign directly to this. It will break things. Call
+      sym.set_value(2) on the choice symbol to be selected instead.
+
+    syms:
+      List of symbols contained in the choice.
+
+      Gotcha: If a symbol depends on the previous symbol within a choice so
+      that an implicit menu is created, it won't be a choice symbol, and won't
+      be included in 'syms'. There are real-world examples of this, and it was
+      a PITA to support in older versions of Kconfiglib that didn't implement
+      the menu structure.
+
+    nodes:
+      A list of MenuNodes for this choice. In practice, the list will probably
+      always contain a single MenuNode, but it is possible to give a choice a
+      name and define it in multiple locations (I've never even seen a named
+      choice though).
+
+    defaults:
+      List of (symbol, cond) tuples for the choice's 'defaults' properties. For
+      example, 'default A if B && C' is represented as (A, (AND, B, C)). If
+      there is no condition, 'cond' is self.config.y.
+
+      Note that 'depends on' and parent dependencies are propagated to
+      'default' conditions.
+
+    direct_dep:
+      See Symbol.direct_dep.
+
+    referenced:
+      A set() with all symbols referenced in the properties and property
+      conditions of the choice.
+
+      Also includes dependencies inherited from surrounding menus and if's.
+
+    is_optional:
+      True if the choice has the 'optional' flag set on it and can be in
+      n mode.
+
+    kconfig:
+      The Kconfig instance this choice is from.
+    """
+    __slots__ = (
+        "_cached_assignable",
+        "_cached_selection",
+        "_cached_vis",
+        "_dependents",
+        "_visited",
+        "_was_set",
+        "defaults",
+        "direct_dep",
+        "is_constant",
+        "is_optional",
+        "kconfig",
+        "name",
+        "nodes",
+        "orig_type",
+        "syms",
+        "user_selection",
+        "user_value",
+    )
+
+    #
+    # Public interface
+    #
+
+    @property
+    def type(self):
+        """
+        Returns the type of the choice. See Symbol.type.
+        """
+        if self.orig_type is TRISTATE and not self.kconfig.modules.tri_value:
+            return BOOL
+
+        return self.orig_type
+
+    @property
+    def str_value(self):
+        """
+        See the class documentation.
+        """
+        return TRI_TO_STR[self.tri_value]
+
+    @property
+    def tri_value(self):
+        """
+        See the class documentation.
+        """
+        # This emulates a reverse dependency of 'm && visibility' for
+        # non-optional choices, which is how the C implementation does it
+
+        val = 0 if self.is_optional else 1
+
+        if self.user_value is not None:
+            val = max(val, self.user_value)
+
+        # Warning: See Symbol._rec_invalidate(), and note that this is a hidden
+        # function call (property magic)
+        val = min(val, self.visibility)
+
+        # Promote m to y for boolean choices
+        return 2 if val == 1 and self.type is BOOL else val
+
+    @property
+    def assignable(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_assignable is None:
+            self._cached_assignable = self._assignable()
+
+        return self._cached_assignable
+
+    @property
+    def visibility(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_vis is None:
+            self._cached_vis = _visibility(self)
+
+        return self._cached_vis
+
+    @property
+    def selection(self):
+        """
+        See the class documentation.
+        """
+        if self._cached_selection is _NO_CACHED_SELECTION:
+            self._cached_selection = self._selection()
+
+        return self._cached_selection
+
+    def set_value(self, value):
+        """
+        Sets the user value (mode) of the choice. Like for Symbol.set_value(),
+        the visibility might truncate the value. Choices without the 'optional'
+        attribute (is_optional) can never be in n mode, but 0/"n" is still
+        accepted since it's not a malformed value (though it will have no
+        effect).
+
+        Returns True if the value is valid for the type of the choice, and
+        False otherwise. This only looks at the form of the value. Check the
+        Choice.assignable attribute to see what values are currently in range
+        and would actually be reflected in the mode of the choice.
+        """
+        if value == self.user_value:
+            # We know the value must be valid if it was successfully set
+            # previously
+            self._was_set = True
+            return True
+
+        if not ((self.orig_type is BOOL     and value in (0, 2, "n", "y")        ) or
+                (self.orig_type is TRISTATE and value in (0, 1, 2, "n", "m", "y"))):
+
+            # Display tristate values as n, m, y in the warning
+            self.kconfig._warn(
+                "the value {} is invalid for {}, which has type {} -- "
+                "assignment ignored"
+                .format(TRI_TO_STR[value] if value in (0, 1, 2) else
+                            "'{}'".format(value),
+                        _name_and_loc(self),
+                        TYPE_TO_STR[self.orig_type]))
+
+            return False
+
+        if value in ("n", "m", "y"):
+            value = STR_TO_TRI[value]
+
+        self.user_value = value
+        self._was_set = True
+        self._rec_invalidate()
+
+        return True
+
+    def unset_value(self):
+        """
+        Resets the user value (mode) and user selection of the Choice, as if
+        the user had never touched the mode or any of the choice symbols.
+        """
+        if self.user_value is not None or self.user_selection:
+            self.user_value = self.user_selection = None
+            self._rec_invalidate()
+
+    @property
+    def referenced(self):
+        """
+        See the class documentation.
+        """
+        res = set()
+        for node in self.nodes:
+            res |= node.referenced
+
+        return res
+
+    def __repr__(self):
+        """
+        Returns a string with information about the choice when it is evaluated
+        on e.g. the interactive Python prompt.
+        """
+        fields = []
+
+        fields.append("choice " + self.name if self.name else "choice")
+        fields.append(TYPE_TO_STR[self.type])
+
+        for node in self.nodes:
+            if node.prompt:
+                fields.append('"{}"'.format(node.prompt[0]))
+
+        fields.append("mode " + self.str_value)
+
+        if self.user_value is not None:
+            fields.append('user mode {}'.format(TRI_TO_STR[self.user_value]))
+
+        if self.selection:
+            fields.append("{} selected".format(self.selection.name))
+
+        if self.user_selection:
+            user_sel_str = "{} selected by user" \
+                           .format(self.user_selection.name)
+
+            if self.selection is not self.user_selection:
+                user_sel_str += " (overridden)"
+
+            fields.append(user_sel_str)
+
+        fields.append("visibility " + TRI_TO_STR[self.visibility])
+
+        if self.is_optional:
+            fields.append("optional")
+
+        for node in self.nodes:
+            fields.append("{}:{}".format(node.filename, node.linenr))
+
+        return "<{}>".format(", ".join(fields))
+
+    def __str__(self):
+        """
+        Returns a string representation of the choice when it is printed,
+        matching the Kconfig format (though without the contained choice
+        symbols).
+
+        See Symbol.__str__() as well.
+        """
+        return self.custom_str(standard_sc_expr_str)
+
+    def custom_str(self, sc_expr_str_fn):
+        """
+        Works like Choice.__str__(), but allows a custom format to be used for
+        all symbol/choice references. See expr_str().
+        """
+        return "\n".join(node.custom_str(sc_expr_str_fn)
+                         for node in self.nodes)
+
+    #
+    # Private methods
+    #
+
+    def __init__(self):
+        """
+        Choice constructor -- not intended to be called directly by Kconfiglib
+        clients.
+        """
+        # These attributes are always set on the instance from outside and
+        # don't need defaults:
+        #   direct_dep
+        #   kconfig
+
+        self.orig_type = UNKNOWN
+        self.syms = []
+        self.defaults = []
+
+        self.nodes = []
+
+        self.name = \
+        self.user_value = self.user_selection = \
+        self._cached_vis = self._cached_assignable = None
+
+        self._cached_selection = _NO_CACHED_SELECTION
+
+        # is_constant is checked by _make_depend_on(). Just set it to avoid
+        # having to special-case choices.
+        self.is_constant = self.is_optional = False
+
+        # See Kconfig._build_dep()
+        self._dependents = set()
+
+        # Used during dependency loop detection
+        self._visited = 0
+
+    def _assignable(self):
+        # Worker function for the 'assignable' attribute
+
+        # Warning: See Symbol._rec_invalidate(), and note that this is a hidden
+        # function call (property magic)
+        vis = self.visibility
+
+        if not vis:
+            return ()
+
+        if vis == 2:
+            if not self.is_optional:
+                return (2,) if self.type is BOOL else (1, 2)
+            return (0, 2) if self.type is BOOL else (0, 1, 2)
+
+        # vis == 1
+
+        return (0, 1) if self.is_optional else (1,)
+
+    def _selection(self):
+        # Worker function for the 'selection' attribute
+
+        # Warning: See Symbol._rec_invalidate(), and note that this is a hidden
+        # function call (property magic)
+        if self.tri_value != 2:
+            # Not in y mode, so no selection
+            return None
+
+        # Use the user selection if it's visible
+        if self.user_selection and self.user_selection.visibility:
+            return self.user_selection
+
+        # Otherwise, check if we have a default
+        return self._get_selection_from_defaults()
+
+    def _get_selection_from_defaults(self):
+        # Check if we have a default
+        for sym, cond in self.defaults:
+            # The default symbol must be visible too
+            if expr_value(cond) and sym.visibility:
+                return sym
+
+        # Otherwise, pick the first visible symbol, if any
+        for sym in self.syms:
+            if sym.visibility:
+                return sym
+
+        # Couldn't find a selection
+        return None
+
+    def _invalidate(self):
+        self._cached_vis = self._cached_assignable = None
+        self._cached_selection = _NO_CACHED_SELECTION
+
+    def _rec_invalidate(self):
+        # See Symbol._rec_invalidate()
+
+        self._invalidate()
+
+        for item in self._dependents:
+            if item._cached_vis is not None:
+                item._rec_invalidate()
+
+class MenuNode(object):
+    """
+    Represents a menu node in the configuration. This corresponds to an entry
+    in e.g. the 'make menuconfig' interface, though non-visible choices, menus,
+    and comments also get menu nodes. If a symbol or choice is defined in
+    multiple locations, it gets one menu node for each location.
+
+    The top-level menu node, corresponding to the implicit top-level menu, is
+    available in Kconfig.top_node.
+
+    The menu nodes for a Symbol or Choice can be found in the
+    Symbol/Choice.nodes attribute. Menus and comments are represented as plain
+    menu nodes, with their text stored in the prompt attribute (prompt[0]).
+    This mirrors the C implementation.
+
+    The following attributes are available on MenuNode instances. They should
+    be viewed as read-only.
+
+    item:
+      Either a Symbol, a Choice, or one of the constants MENU and COMMENT.
+      Menus and comments are represented as plain menu nodes. Ifs are collapsed
+      (matching the C implementation) and do not appear in the final menu tree.
+
+    next:
+      The following menu node. None if there is no following node.
+
+    list:
+      The first child menu node. None if there are no children.
+
+      Choices and menus naturally have children, but Symbols can also have
+      children because of menus created automatically from dependencies (see
+      kconfig-language.txt).
+
+    parent:
+      The parent menu node. None if there is no parent.
+
+    prompt:
+      A (string, cond) tuple with the prompt for the menu node and its
+      conditional expression (which is self.kconfig.y if there is no
+      condition). None if there is no prompt.
+
+      For symbols and choices, the prompt is stored in the MenuNode rather than
+      the Symbol or Choice instance. For menus and comments, the prompt holds
+      the text.
+
+    defaults:
+      The 'default' properties for this particular menu node. See
+      symbol.defaults.
+
+      When evaluating defaults, you should use Symbol/Choice.defaults instead,
+      as it include properties from all menu nodes (a symbol/choice can have
+      multiple definition locations/menu nodes). MenuNode.defaults is meant for
+      documentation generation.
+
+    selects:
+      Like MenuNode.defaults, for selects.
+
+    implies:
+      Like MenuNode.defaults, for implies.
+
+    ranges:
+      Like MenuNode.defaults, for ranges.
+
+    help:
+      The help text for the menu node for Symbols and Choices. None if there is
+      no help text. Always stored in the node rather than the Symbol or Choice.
+      It is possible to have a separate help text at each location if a symbol
+      is defined in multiple locations.
+
+    dep:
+      The 'depends on' dependencies for the menu node, or self.kconfig.y if
+      there are no dependencies. Parent dependencies are propagated to this
+      attribute, and this attribute is then in turn propagated to the
+      properties of symbols and choices.
+
+      If a symbol or choice is defined in multiple locations, only the
+      properties defined at a particular location get the corresponding
+      MenuNode.dep dependencies propagated to them.
+
+    visibility:
+      The 'visible if' dependencies for the menu node (which must represent a
+      menu), or self.kconfig.y if there are no 'visible if' dependencies.
+      'visible if' dependencies are recursively propagated to the prompts of
+      symbols and choices within the menu.
+
+    referenced:
+      A set() with all symbols and choices referenced in the properties and
+      property conditions of the menu node.
+
+      Also includes dependencies inherited from surrounding menus and if's.
+      Choices appear in the dependencies of choice symbols.
+
+    is_menuconfig:
+      Set to True if the children of the menu node should be displayed in a
+      separate menu. This is the case for the following items:
+
+        - Menus (node.item == MENU)
+
+        - Choices
+
+        - Symbols defined with the 'menuconfig' keyword. The children come from
+          implicitly created submenus, and should be displayed in a separate
+          menu rather than being indented.
+
+      'is_menuconfig' is just a hint on how to display the menu node. It's
+      ignored internally by Kconfiglib, except when printing symbols.
+
+    filename/linenr:
+      The location where the menu node appears. The filename is relative to
+      $srctree (or to the current directory if $srctree isn't set), except
+      absolute paths passed to 'source' and Kconfig.__init__() are preserved.
+
+    include_path:
+      A tuple of (filename, linenr) tuples, giving the locations of the
+      'source' statements via which the Kconfig file containing this menu node
+      was included. The first element is the location of the 'source' statement
+      in the top-level Kconfig file passed to Kconfig.__init__(), etc.
+
+      Note that the Kconfig file of the menu node itself isn't included. Check
+      'filename' and 'linenr' for that.
+
+    kconfig:
+      The Kconfig instance the menu node is from.
+    """
+    __slots__ = (
+        "dep",
+        "filename",
+        "help",
+        "include_path",
+        "is_menuconfig",
+        "item",
+        "kconfig",
+        "linenr",
+        "list",
+        "next",
+        "parent",
+        "prompt",
+        "visibility",
+
+        # Properties
+        "defaults",
+        "selects",
+        "implies",
+        "ranges"
+    )
+
+    def __init__(self):
+        # Properties defined on this particular menu node. A local 'depends on'
+        # only applies to these, in case a symbol is defined in multiple
+        # locations.
+        self.defaults = []
+        self.selects = []
+        self.implies = []
+        self.ranges = []
+
+    @property
+    def referenced(self):
+        """
+        See the class documentation.
+        """
+        # self.dep is included to catch dependencies from a lone 'depends on'
+        # when there are no properties to propagate it to
+        res = expr_items(self.dep)
+
+        if self.prompt:
+            res |= expr_items(self.prompt[1])
+
+        if self.item is MENU:
+            res |= expr_items(self.visibility)
+
+        for value, cond in self.defaults:
+            res |= expr_items(value)
+            res |= expr_items(cond)
+
+        for value, cond in self.selects:
+            res.add(value)
+            res |= expr_items(cond)
+
+        for value, cond in self.implies:
+            res.add(value)
+            res |= expr_items(cond)
+
+        for low, high, cond in self.ranges:
+            res.add(low)
+            res.add(high)
+            res |= expr_items(cond)
+
+        return res
+
+    def __repr__(self):
+        """
+        Returns a string with information about the menu node when it is
+        evaluated on e.g. the interactive Python prompt.
+        """
+        fields = []
+
+        if isinstance(self.item, Symbol):
+            fields.append("menu node for symbol " + self.item.name)
+
+        elif isinstance(self.item, Choice):
+            s = "menu node for choice"
+            if self.item.name is not None:
+                s += " " + self.item.name
+            fields.append(s)
+
+        elif self.item is MENU:
+            fields.append("menu node for menu")
+
+        elif self.item is COMMENT:
+            fields.append("menu node for comment")
+
+        elif self.item is None:
+            fields.append("menu node for if (should not appear in the final "
+                          " tree)")
+
+        else:
+            _internal_error("unable to determine type in MenuNode.__repr__()")
+
+        if self.prompt:
+            fields.append('prompt "{}" (visibility {})'
+                          .format(self.prompt[0],
+                                  TRI_TO_STR[expr_value(self.prompt[1])]))
+
+        if isinstance(self.item, Symbol) and self.is_menuconfig:
+            fields.append("is menuconfig")
+
+        fields.append("deps " + TRI_TO_STR[expr_value(self.dep)])
+
+        if self.item is MENU:
+            fields.append("'visible if' deps " + \
+                          TRI_TO_STR[expr_value(self.visibility)])
+
+        if isinstance(self.item, (Symbol, Choice)) and self.help is not None:
+            fields.append("has help")
+
+        if self.list:
+            fields.append("has child")
+
+        if self.next:
+            fields.append("has next")
+
+        fields.append("{}:{}".format(self.filename, self.linenr))
+
+        return "<{}>".format(", ".join(fields))
+
+    def __str__(self):
+        """
+        Returns a string representation of the menu node, matching the Kconfig
+        format.
+
+        The output could (almost) be fed back into a Kconfig parser to redefine
+        the object associated with the menu node. See the module documentation
+        for a gotcha related to choice symbols.
+
+        For symbols and choices with multiple menu nodes (multiple definition
+        locations), properties that aren't associated with a particular menu
+        node are shown on all menu nodes ('option env=...', 'optional' for
+        choices, etc.).
+        """
+        return self.custom_str(standard_sc_expr_str)
+
+    def custom_str(self, sc_expr_str_fn):
+        """
+        Works like MenuNode.__str__(), but allows a custom format to be used
+        for all symbol/choice references. See expr_str().
+        """
+        return self._menu_comment_node_str(sc_expr_str_fn) \
+               if self.item in (MENU, COMMENT) else \
+               self._sym_choice_node_str(sc_expr_str_fn)
+
+    def _menu_comment_node_str(self, sc_expr_str_fn):
+        s = '{} "{}"\n'.format("menu" if self.item is MENU else "comment",
+                               self.prompt[0])
+
+        if self.dep is not self.kconfig.y:
+            s += "\tdepends on {}\n".format(expr_str(self.dep, sc_expr_str_fn))
+
+        if self.item is MENU and self.visibility is not self.kconfig.y:
+            s += "\tvisible if {}\n".format(expr_str(self.visibility,
+                                                     sc_expr_str_fn))
+
+        return s
+
+    def _sym_choice_node_str(self, sc_expr_str_fn):
+        lines = []
+
+        def indent_add(s):
+            lines.append("\t" + s)
+
+        def indent_add_cond(s, cond):
+            if cond is not self.kconfig.y:
+                s += " if " + expr_str(cond, sc_expr_str_fn)
+            indent_add(s)
+
+        sc = self.item
+
+        if isinstance(sc, Symbol):
+            lines.append(
+                ("menuconfig " if self.is_menuconfig else "config ")
+                + sc.name)
+        else:
+            lines.append("choice " + sc.name if sc.name else "choice")
+
+        if sc.orig_type is not UNKNOWN:
+            indent_add(TYPE_TO_STR[sc.orig_type])
+
+        if self.prompt:
+            indent_add_cond(
+                'prompt "{}"'.format(escape(self.prompt[0])),
+                self.prompt[1])
+
+        if isinstance(sc, Symbol):
+            if sc.is_allnoconfig_y:
+                indent_add("option allnoconfig_y")
+
+            if sc is sc.kconfig.defconfig_list:
+                indent_add("option defconfig_list")
+
+            if sc.env_var is not None:
+                indent_add('option env="{}"'.format(sc.env_var))
+
+            if sc is sc.kconfig.modules:
+                indent_add("option modules")
+
+            for low, high, cond in self.ranges:
+                indent_add_cond(
+                    "range {} {}".format(sc_expr_str_fn(low),
+                                         sc_expr_str_fn(high)),
+                    cond)
+
+        for default, cond in self.defaults:
+            indent_add_cond("default " + expr_str(default, sc_expr_str_fn),
+                            cond)
+
+        if isinstance(sc, Choice) and sc.is_optional:
+            indent_add("optional")
+
+        if isinstance(sc, Symbol):
+            for select, cond in self.selects:
+                indent_add_cond("select " + sc_expr_str_fn(select), cond)
+
+            for imply, cond in self.implies:
+                indent_add_cond("imply " + sc_expr_str_fn(imply), cond)
+
+        if self.dep is not sc.kconfig.y:
+            indent_add("depends on " + expr_str(self.dep, sc_expr_str_fn))
+
+        if self.help is not None:
+            indent_add("help")
+            for line in self.help.splitlines():
+                indent_add("  " + line)
+
+        return "\n".join(lines) + "\n"
+
+class Variable(object):
+    """
+    Represents a preprocessor variable/function.
+
+    The following attributes are available:
+
+    name:
+      The name of the variable.
+
+    value:
+      The unexpanded value of the variable.
+
+    expanded_value:
+      The expanded value of the variable. For simple variables (those defined
+      with :=), this will equal 'value'. Accessing this property will raise a
+      KconfigError if any variable in the expansion expands to itself.
+
+    is_recursive:
+      True if the variable is recursive (defined with =).
+    """
+    __slots__ = (
+        "_n_expansions",
+        "is_recursive",
+        "kconfig",
+        "name",
+        "value",
+    )
+
+    @property
+    def expanded_value(self):
+        """
+        See the class documentation.
+        """
+        return self.kconfig._expand_whole(self.value, ())
+
+class KconfigError(Exception):
+    """
+    Exception raised for Kconfig-related errors.
+    """
+
+# Backwards compatibility
+KconfigSyntaxError = KconfigError
+
+class InternalError(Exception):
+    """
+    Exception raised for internal errors.
+    """
+
+#
+# Public functions
+#
+
+def expr_value(expr):
+    """
+    Evaluates the expression 'expr' to a tristate value. Returns 0 (n), 1 (m),
+    or 2 (y).
+
+    'expr' must be an already-parsed expression from a Symbol, Choice, or
+    MenuNode property. To evaluate an expression represented as a string, use
+    Kconfig.eval_string().
+
+    Passing subexpressions of expressions to this function works as expected.
+    """
+    if not isinstance(expr, tuple):
+        return expr.tri_value
+
+    if expr[0] is AND:
+        v1 = expr_value(expr[1])
+        # Short-circuit the n case as an optimization (~5% faster
+        # allnoconfig.py and allyesconfig.py, as of writing)
+        return 0 if not v1 else min(v1, expr_value(expr[2]))
+
+    if expr[0] is OR:
+        v1 = expr_value(expr[1])
+        # Short-circuit the y case as an optimization
+        return 2 if v1 == 2 else max(v1, expr_value(expr[2]))
+
+    if expr[0] is NOT:
+        return 2 - expr_value(expr[1])
+
+    if expr[0] in _RELATIONS:
+        # Implements <, <=, >, >= comparisons as well. These were added to
+        # kconfig in 31847b67 (kconfig: allow use of relations other than
+        # (in)equality).
+
+        oper, op1, op2 = expr
+
+        # If both operands are strings...
+        if op1.orig_type is STRING and op2.orig_type is STRING:
+            # ...then compare them lexicographically
+            comp = _strcmp(op1.str_value, op2.str_value)
+        else:
+            # Otherwise, try to compare them as numbers
+            try:
+                comp = _sym_to_num(op1) - _sym_to_num(op2)
+            except ValueError:
+                # Fall back on a lexicographic comparison if the operands don't
+                # parse as numbers
+                comp = _strcmp(op1.str_value, op2.str_value)
+
+        if   oper is EQUAL:         res = comp == 0
+        elif oper is UNEQUAL:       res = comp != 0
+        elif oper is LESS:          res = comp < 0
+        elif oper is LESS_EQUAL:    res = comp <= 0
+        elif oper is GREATER:       res = comp > 0
+        elif oper is GREATER_EQUAL: res = comp >= 0
+
+        return 2*res
+
+    _internal_error("Internal error while evaluating expression: "
+                    "unknown operation {}.".format(expr[0]))
+
+def standard_sc_expr_str(sc):
+    """
+    Standard symbol/choice printing function. Uses plain Kconfig syntax, and
+    displays choices as <choice> (or <choice NAME>, for named choices).
+
+    See expr_str().
+    """
+    if isinstance(sc, Symbol):
+        return '"{}"'.format(escape(sc.name)) if sc.is_constant else sc.name
+
+    # Choice
+    return "<choice {}>".format(sc.name) if sc.name else "<choice>"
+
+def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str):
+    """
+    Returns the string representation of the expression 'expr', as in a Kconfig
+    file.
+
+    Passing subexpressions of expressions to this function works as expected.
+
+    sc_expr_str_fn (default: standard_sc_expr_str):
+      This function is called for every symbol/choice (hence "sc") appearing in
+      the expression, with the symbol/choice as the argument. It is expected to
+      return a string to be used for the symbol/choice.
+
+      This can be used e.g. to turn symbols/choices into links when generating
+      documentation, or for printing the value of each symbol/choice after it.
+
+      Note that quoted values are represented as constants symbols
+      (Symbol.is_constant == True).
+    """
+    if not isinstance(expr, tuple):
+        return sc_expr_str_fn(expr)
+
+    if expr[0] is AND:
+        return "{} && {}".format(_parenthesize(expr[1], OR, sc_expr_str_fn),
+                                 _parenthesize(expr[2], OR, sc_expr_str_fn))
+
+    if expr[0] is OR:
+        # This turns A && B || C && D into "(A && B) || (C && D)", which is
+        # redundant, but more readable
+        return "{} || {}".format(_parenthesize(expr[1], AND, sc_expr_str_fn),
+                                 _parenthesize(expr[2], AND, sc_expr_str_fn))
+
+    if expr[0] is NOT:
+        if isinstance(expr[1], tuple):
+            return "!({})".format(expr_str(expr[1], sc_expr_str_fn))
+        return "!" + sc_expr_str_fn(expr[1])  # Symbol
+
+    # Relation
+    #
+    # Relation operands are always symbols (quoted strings are constant
+    # symbols)
+    return "{} {} {}".format(sc_expr_str_fn(expr[1]), _REL_TO_STR[expr[0]],
+                             sc_expr_str_fn(expr[2]))
+
+def expr_items(expr):
+    """
+    Returns a set() of all items (symbols and choices) that appear in the
+    expression 'expr'.
+    """
+
+    res = set()
+
+    def rec(subexpr):
+        if isinstance(subexpr, tuple):
+            # AND, OR, NOT, or relation
+
+            rec(subexpr[1])
+
+            # NOTs only have a single operand
+            if subexpr[0] is not NOT:
+                rec(subexpr[2])
+
+        else:
+            # Symbol or choice
+            res.add(subexpr)
+
+    rec(expr)
+    return res
+
+def split_expr(expr, op):
+    """
+    Returns a list containing the top-level AND or OR operands in the
+    expression 'expr', in the same (left-to-right) order as they appear in
+    the expression.
+
+    This can be handy e.g. for splitting (weak) reverse dependencies
+    from 'select' and 'imply' into individual selects/implies.
+
+    op:
+      Either AND to get AND operands, or OR to get OR operands.
+
+      (Having this as an operand might be more future-safe than having two
+      hardcoded functions.)
+
+
+    Pseudo-code examples:
+
+      split_expr( A                    , OR  )  ->  [A]
+      split_expr( A && B               , OR  )  ->  [A && B]
+      split_expr( A || B               , OR  )  ->  [A, B]
+      split_expr( A || B               , AND )  ->  [A || B]
+      split_expr( A || B || (C && D)   , OR  )  ->  [A, B, C && D]
+
+      # Second || is not at the top level
+      split_expr( A || (B && (C || D)) , OR )  ->  [A, B && (C || D)]
+
+      # Parentheses don't matter as long as we stay at the top level (don't
+      # encounter any non-'op' nodes)
+      split_expr( (A || B) || C        , OR )  ->  [A, B, C]
+      split_expr( A || (B || C)        , OR )  ->  [A, B, C]
+    """
+    res = []
+
+    def rec(subexpr):
+        if isinstance(subexpr, tuple) and subexpr[0] is op:
+            rec(subexpr[1])
+            rec(subexpr[2])
+        else:
+            res.append(subexpr)
+
+    rec(expr)
+    return res
+
+def escape(s):
+    r"""
+    Escapes the string 's' in the same fashion as is done for display in
+    Kconfig format and when writing strings to a .config file. " and \ are
+    replaced by \" and \\, respectively.
+    """
+    # \ must be escaped before " to avoid double escaping
+    return s.replace("\\", r"\\").replace('"', r'\"')
+
+# unescape() helper
+_unescape_sub = re.compile(r"\\(.)").sub
+
+def unescape(s):
+    r"""
+    Unescapes the string 's'. \ followed by any character is replaced with just
+    that character. Used internally when reading .config files.
+    """
+    return _unescape_sub(r"\1", s)
+
+def standard_kconfig():
+    """
+    Helper for tools. Loads the top-level Kconfig specified as the first
+    command-line argument, or "Kconfig" if there are no command-line arguments.
+    Returns the Kconfig instance.
+
+    Exits with sys.exit() (which raises a SystemExit exception) and prints a
+    usage note to stderr if more than one command-line argument is passed.
+    """
+    if len(sys.argv) > 2:
+        sys.exit("usage: {} [Kconfig]".format(sys.argv[0]))
+
+    return Kconfig("Kconfig" if len(sys.argv) < 2 else sys.argv[1])
+
+def standard_config_filename():
+    """
+    Helper for tools. Returns the value of KCONFIG_CONFIG (which specifies the
+    .config file to load/save) if it is set, and ".config" otherwise.
+    """
+    return os.environ.get("KCONFIG_CONFIG", ".config")
+
+#
+# Internal functions
+#
+
+def _visibility(sc):
+    # Symbols and Choices have a "visibility" that acts as an upper bound on
+    # the values a user can set for them, corresponding to the visibility in
+    # e.g. 'make menuconfig'. This function calculates the visibility for the
+    # Symbol or Choice 'sc' -- the logic is nearly identical.
+
+    vis = 0
+
+    for node in sc.nodes:
+        if node.prompt:
+            vis = max(vis, expr_value(node.prompt[1]))
+
+    if isinstance(sc, Symbol) and sc.choice:
+        if sc.choice.orig_type is TRISTATE and \
+           sc.orig_type is not TRISTATE and sc.choice.tri_value != 2:
+            # Non-tristate choice symbols are only visible in y mode
+            return 0
+
+        if sc.orig_type is TRISTATE and vis == 1 and sc.choice.tri_value == 2:
+            # Choice symbols with m visibility are not visible in y mode
+            return 0
+
+    # Promote m to y if we're dealing with a non-tristate (possibly due to
+    # modules being disabled)
+    if vis == 1 and sc.type is not TRISTATE:
+        return 2
+
+    return vis
+
+def _make_depend_on(sc, expr):
+    # Adds 'sc' (symbol or choice) as a "dependee" to all symbols in 'expr'.
+    # Constant symbols in 'expr' are skipped as they can never change value
+    # anyway.
+
+    if isinstance(expr, tuple):
+        # AND, OR, NOT, or relation
+
+        _make_depend_on(sc, expr[1])
+
+        # NOTs only have a single operand
+        if expr[0] is not NOT:
+            _make_depend_on(sc, expr[2])
+
+    elif not expr.is_constant:
+        # Non-constant symbol, or choice
+        expr._dependents.add(sc)
+
+def _parenthesize(expr, type_, sc_expr_str_fn):
+    # expr_str() helper. Adds parentheses around expressions of type 'type_'.
+
+    if isinstance(expr, tuple) and expr[0] is type_:
+        return "({})".format(expr_str(expr, sc_expr_str_fn))
+    return expr_str(expr, sc_expr_str_fn)
+
+def _indentation(line):
+    # Returns the length of the line's leading whitespace, treating tab stops
+    # as being spaced 8 characters apart.
+
+    line = line.expandtabs()
+    return len(line) - len(line.lstrip())
+
+def _ordered_unique(lst):
+    # Returns 'lst' with any duplicates removed, preserving order. This hacky
+    # version seems to be a common idiom. It relies on short-circuit evaluation
+    # and set.add() returning None, which is falsy.
+
+    seen = set()
+    seen_add = seen.add
+    return [x for x in lst if x not in seen and not seen_add(x)]
+
+def _is_base_n(s, n):
+    try:
+        int(s, n)
+        return True
+    except ValueError:
+        return False
+
+def _strcmp(s1, s2):
+    # strcmp()-alike that returns -1, 0, or 1
+
+    return (s1 > s2) - (s1 < s2)
+
+def _is_num(s):
+    # Returns True if the string 's' looks like a number.
+    #
+    # Internally, all operands in Kconfig are symbols, only undefined symbols
+    # (which numbers usually are) get their name as their value.
+    #
+    # Only hex numbers that start with 0x/0X are classified as numbers.
+    # Otherwise, symbols whose names happen to contain only the letters A-F
+    # would trigger false positives.
+
+    try:
+        int(s)
+    except ValueError:
+        if not s.startswith(("0x", "0X")):
+            return False
+
+        try:
+            int(s, 16)
+        except ValueError:
+            return False
+
+    return True
+
+def _sym_to_num(sym):
+    # expr_value() helper for converting a symbol to a number. Raises
+    # ValueError for symbols that can't be converted.
+
+    # For BOOL and TRISTATE, n/m/y count as 0/1/2. This mirrors 9059a3493ef
+    # ("kconfig: fix relational operators for bool and tristate symbols") in
+    # the C implementation.
+    return sym.tri_value if sym.orig_type in (BOOL, TRISTATE) else \
+           int(sym.str_value, _TYPE_TO_BASE[sym.orig_type])
+
+def _internal_error(msg):
+    raise InternalError(
+        msg +
+        "\nSorry! You may want to send an email to ulfalizer a.t Google's "
+        "email service to tell me about this. Include the message above and "
+        "the stack trace and describe what you were doing.")
+
+def _decoding_error(e, filename, macro_linenr=None):
+    # Gives the filename and context for UnicodeDecodeError's, which are a pain
+    # to debug otherwise. 'e' is the UnicodeDecodeError object.
+    #
+    # If the decoding error is for the output of a $(shell,...) command,
+    # macro_linenr holds the line number where it was run (the exact line
+    # number isn't available for decoding errors in files).
+
+    if macro_linenr is None:
+        loc = filename
+    else:
+        loc = "output from macro at {}:{}".format(filename, macro_linenr)
+
+    raise KconfigError(
+        "\n"
+        "Malformed {} in {}\n"
+        "Context: {}\n"
+        "Problematic data: {}\n"
+        "Reason: {}".format(
+            e.encoding, loc,
+            e.object[max(e.start - 40, 0):e.end + 40],
+            e.object[e.start:e.end],
+            e.reason))
+
+def _name_and_loc(sc):
+    # Helper for giving the symbol/choice name and location(s) in e.g. warnings
+
+    name = sc.name or "<choice>"
+
+    if not sc.nodes:
+        return name + " (undefined)"
+
+    return "{} (defined at {})".format(
+        name,
+        ", ".join("{}:{}".format(node.filename, node.linenr)
+                  for node in sc.nodes))
+
+
+# Menu manipulation
+
+def _expr_depends_on(expr, sym):
+    # Reimplementation of expr_depends_symbol() from mconf.c. Used to determine
+    # if a submenu should be implicitly created. This also influences which
+    # items inside choice statements are considered choice items.
+
+    if not isinstance(expr, tuple):
+        return expr is sym
+
+    if expr[0] in (EQUAL, UNEQUAL):
+        # Check for one of the following:
+        # sym = m/y, m/y = sym, sym != n, n != sym
+
+        left, right = expr[1:]
+
+        if right is sym:
+            left, right = right, left
+        elif left is not sym:
+            return False
+
+        return (expr[0] is EQUAL and right is sym.kconfig.m or \
+                                     right is sym.kconfig.y) or \
+               (expr[0] is UNEQUAL and right is sym.kconfig.n)
+
+    return expr[0] is AND and \
+           (_expr_depends_on(expr[1], sym) or
+            _expr_depends_on(expr[2], sym))
+
+def _auto_menu_dep(node1, node2):
+    # Returns True if node2 has an "automatic menu dependency" on node1. If
+    # node2 has a prompt, we check its condition. Otherwise, we look directly
+    # at node2.dep.
+
+    # If node2 has no prompt, use its menu node dependencies instead
+    return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep,
+                            node1.item)
+
+def _flatten(node):
+    # "Flattens" menu nodes without prompts (e.g. 'if' nodes and non-visible
+    # symbols with children from automatic menu creation) so that their
+    # children appear after them instead. This gives a clean menu structure
+    # with no unexpected "jumps" in the indentation.
+    #
+    # Do not flatten promptless choices (which can appear "legitimitely" if a
+    # named choice is defined in multiple locations to add on symbols). It
+    # looks confusing, and the menuconfig already shows all choice symbols if
+    # you enter the choice at some location with a prompt.
+
+    while node:
+        if node.list and not node.prompt and \
+           not isinstance(node.item, Choice):
+
+            last_node = node.list
+            while 1:
+                last_node.parent = node.parent
+                if not last_node.next:
+                    break
+                last_node = last_node.next
+
+            last_node.next = node.next
+            node.next = node.list
+            node.list = None
+
+        node = node.next
+
+def _remove_ifs(node):
+    # Removes 'if' nodes (which can be recognized by MenuNode.item being None),
+    # which are assumed to already have been flattened. The C implementation
+    # doesn't bother to do this, but we expose the menu tree directly, and it
+    # makes it nicer to work with.
+
+    first = node.list
+    while first and first.item is None:
+        first = first.next
+
+    cur = first
+    while cur:
+        if cur.next and cur.next.item is None:
+            cur.next = cur.next.next
+        cur = cur.next
+
+    node.list = first
+
+def _finalize_choice(node):
+    # Finalizes a choice, marking each symbol whose menu node has the choice as
+    # the parent as a choice symbol, and automatically determining types if not
+    # specified.
+
+    choice = node.item
+
+    cur = node.list
+    while cur:
+        if isinstance(cur.item, Symbol):
+            cur.item.choice = choice
+            choice.syms.append(cur.item)
+        cur = cur.next
+
+    # If no type is specified for the choice, its type is that of
+    # the first choice item with a specified type
+    if choice.orig_type is UNKNOWN:
+        for item in choice.syms:
+            if item.orig_type is not UNKNOWN:
+                choice.orig_type = item.orig_type
+                break
+
+    # Each choice item of UNKNOWN type gets the type of the choice
+    for sym in choice.syms:
+        if sym.orig_type is UNKNOWN:
+            sym.orig_type = choice.orig_type
+
+def _check_dep_loop_sym(sym, ignore_choice):
+    # Detects dependency loops using depth-first search on the dependency graph
+    # (which is calculated earlier in Kconfig._build_dep()).
+    #
+    # Algorithm:
+    #
+    #  1. Symbols/choices start out with _visited = 0, meaning unvisited.
+    #
+    #  2. When a symbol/choice is first visited, _visited is set to 1, meaning
+    #     "visited, potentially part of a dependency loop". The recursive
+    #     search then continues from the symbol/choice.
+    #
+    #  3. If we run into a symbol/choice X with _visited already set to 1,
+    #     there's a dependency loop. The loop is found on the call stack by
+    #     recording symbols while returning ("on the way back") until X is seen
+    #     again.
+    #
+    #  4. Once a symbol/choice and all its dependencies (or dependents in this
+    #     case) have been checked recursively without detecting any loops, its
+    #     _visited is set to 2, meaning "visited, not part of a dependency
+    #     loop".
+    #
+    #     This saves work if we run into the symbol/choice again in later calls
+    #     to _check_dep_loop_sym(). We just return immediately.
+    #
+    # Choices complicate things, as every choice symbol depends on every other
+    # choice symbol in a sense. When a choice is "entered" via a choice symbol
+    # X, we visit all choice symbols from the choice except X, and prevent
+    # immediately revisiting the choice with a flag (ignore_choice).
+    #
+    # Maybe there's a better way to handle this (different flags or the
+    # like...)
+
+    if not sym._visited:
+        # sym._visited == 0, unvisited
+
+        sym._visited = 1
+
+        for dep in sym._dependents:
+            # Choices show up in Symbol._dependents when the choice has the
+            # symbol in a 'prompt' or 'default' condition (e.g.
+            # 'default ... if SYM').
+            #
+            # Since we aren't entering the choice via a choice symbol, all
+            # choice symbols need to be checked, hence the None.
+            loop = _check_dep_loop_choice(dep, None) \
+                   if isinstance(dep, Choice) \
+                   else _check_dep_loop_sym(dep, False)
+
+            if loop:
+                # Dependency loop found
+                return _found_dep_loop(loop, sym)
+
+        if sym.choice and not ignore_choice:
+            loop = _check_dep_loop_choice(sym.choice, sym)
+            if loop:
+                # Dependency loop found
+                return _found_dep_loop(loop, sym)
+
+        # The symbol is not part of a dependency loop
+        sym._visited = 2
+
+        # No dependency loop found
+        return None
+
+    if sym._visited == 2:
+        # The symbol was checked earlier and is already known to not be part of
+        # a dependency loop
+        return None
+
+    # sym._visited == 1, found a dependency loop. Return the symbol as the
+    # first element in it.
+    return (sym,)
+
+def _check_dep_loop_choice(choice, skip):
+    if not choice._visited:
+        # choice._visited == 0, unvisited
+
+        choice._visited = 1
+
+        # Check for loops involving choice symbols. If we came here via a
+        # choice symbol, skip that one, as we'd get a false positive
+        # '<sym FOO> -> <choice> -> <sym FOO>' loop otherwise.
+        for sym in choice.syms:
+            if sym is not skip:
+                # Prevent the choice from being immediately re-entered via the
+                # "is a choice symbol" path by passing True
+                loop = _check_dep_loop_sym(sym, True)
+                if loop:
+                    # Dependency loop found
+                    return _found_dep_loop(loop, choice)
+
+        # The choice is not part of a dependency loop
+        choice._visited = 2
+
+        # No dependency loop found
+        return None
+
+    if choice._visited == 2:
+        # The choice was checked earlier and is already known to not be part of
+        # a dependency loop
+        return None
+
+    # choice._visited == 1, found a dependency loop. Return the choice as the
+    # first element in it.
+    return (choice,)
+
+def _found_dep_loop(loop, cur):
+    # Called "on the way back" when we know we have a loop
+
+    # Is the symbol/choice 'cur' where the loop started?
+    if cur is not loop[0]:
+        # Nope, it's just a part of the loop
+        return loop + (cur,)
+
+    # Yep, we have the entire loop. Throw an exception that shows it.
+
+    msg = "\nDependency loop\n" \
+            "===============\n\n"
+
+    for item in loop:
+        if item is not loop[0]:
+            msg += "...depends on "
+            if isinstance(item, Symbol) and item.choice:
+                msg += "the choice symbol "
+
+        msg += "{}, with definition...\n\n{}\n" \
+               .format(_name_and_loc(item), item)
+
+        # Small wart: Since we reuse the already calculated
+        # Symbol/Choice._dependents sets for recursive dependency detection, we
+        # lose information on whether a dependency came from a 'select'/'imply'
+        # condition or e.g. a 'depends on'.
+        #
+        # This might cause selecting symbols to "disappear". For example,
+        # a symbol B having 'select A if C' gives a direct dependency from A to
+        # C, since it corresponds to a reverse dependency of B && C.
+        #
+        # Always print reverse dependencies for symbols that have them to make
+        # sure information isn't lost. I wonder if there's some neat way to
+        # improve this.
+
+        if isinstance(item, Symbol):
+            if item.rev_dep is not item.kconfig.n:
+                msg += "(select-related dependencies: {})\n\n" \
+                       .format(expr_str(item.rev_dep))
+
+            if item.weak_rev_dep is not item.kconfig.n:
+                msg += "(imply-related dependencies: {})\n\n" \
+                       .format(expr_str(item.rev_dep))
+
+    msg += "...depends again on {}".format(_name_and_loc(loop[0]))
+
+    raise KconfigError(msg)
+
+def _check_sym_sanity(sym):
+    # Checks various symbol properties that are handiest to check after
+    # parsing. Only generates errors and warnings.
+
+    if sym.orig_type in (BOOL, TRISTATE):
+        # A helper function could be factored out here, but keep it
+        # speedy/straightforward for now. bool/tristate symbols are by far the
+        # most common, and most lack selects and implies.
+
+        for target_sym, _ in sym.selects:
+            if target_sym.orig_type not in (BOOL, TRISTATE, UNKNOWN):
+                sym.kconfig._warn("{} selects the {} symbol {}, which is not "
+                                  "bool or tristate"
+                                  .format(_name_and_loc(sym),
+                                          TYPE_TO_STR[target_sym.orig_type],
+                                          _name_and_loc(target_sym)))
+
+        for target_sym, _ in sym.implies:
+            if target_sym.orig_type not in (BOOL, TRISTATE, UNKNOWN):
+                sym.kconfig._warn("{} implies the {} symbol {}, which is not "
+                                  "bool or tristate"
+                                  .format(_name_and_loc(sym),
+                                          TYPE_TO_STR[target_sym.orig_type],
+                                          _name_and_loc(target_sym)))
+
+    elif sym.orig_type in (STRING, INT, HEX):
+        for default, _ in sym.defaults:
+            if not isinstance(default, Symbol):
+                raise KconfigError(
+                    "the {} symbol {} has a malformed default {} -- expected "
+                    "a single symbol"
+                    .format(TYPE_TO_STR[sym.orig_type], _name_and_loc(sym),
+                            expr_str(default)))
+
+            if sym.orig_type is STRING:
+                if not default.is_constant and not default.nodes and \
+                   not default.name.isupper():
+                    # 'default foo' on a string symbol could be either a symbol
+                    # reference or someone leaving out the quotes. Guess that
+                    # the quotes were left out if 'foo' isn't all-uppercase
+                    # (and no symbol named 'foo' exists).
+                    sym.kconfig._warn("style: quotes recommended around "
+                                      "default value for string symbol "
+                                      + _name_and_loc(sym))
+
+            elif sym.orig_type in (INT, HEX) and \
+               not _int_hex_ok(default, sym.orig_type):
+
+                sym.kconfig._warn("the {0} symbol {1} has a non-{0} default {2}"
+                                  .format(TYPE_TO_STR[sym.orig_type],
+                                          _name_and_loc(sym),
+                                          _name_and_loc(default)))
+
+        if sym.selects or sym.implies:
+            sym.kconfig._warn("the {} symbol {} has selects or implies"
+                              .format(TYPE_TO_STR[sym.orig_type],
+                                      _name_and_loc(sym)))
+
+    else:  # UNKNOWN
+        sym.kconfig._warn("{} defined without a type"
+                          .format(_name_and_loc(sym)))
+
+
+    if sym.ranges:
+        if sym.orig_type not in (INT, HEX):
+            sym.kconfig._warn(
+                "the {} symbol {} has ranges, but is not int or hex"
+                .format(TYPE_TO_STR[sym.orig_type], _name_and_loc(sym)))
+        else:
+            for low, high, _ in sym.ranges:
+                if not _int_hex_ok(low, sym.orig_type) or \
+                   not _int_hex_ok(high, sym.orig_type):
+
+                   sym.kconfig._warn("the {0} symbol {1} has a non-{0} range "
+                                     "[{2}, {3}]"
+                                     .format(TYPE_TO_STR[sym.orig_type],
+                                             _name_and_loc(sym),
+                                             _name_and_loc(low),
+                                             _name_and_loc(high)))
+
+
+def _int_hex_ok(sym, type_):
+    # Returns True if the (possibly constant) symbol 'sym' is valid as a value
+    # for a symbol of type type_ (INT or HEX)
+
+    # 'not sym.nodes' implies a constant or undefined symbol, e.g. a plain
+    # "123"
+    if not sym.nodes:
+        return _is_base_n(sym.name, _TYPE_TO_BASE[type_])
+
+    return sym.orig_type is type_
+
+def _check_choice_sanity(choice):
+    # Checks various choice properties that are handiest to check after
+    # parsing. Only generates errors and warnings.
+
+    if choice.orig_type not in (BOOL, TRISTATE):
+        choice.kconfig._warn("{} defined with type {}"
+                             .format(_name_and_loc(choice),
+                                     TYPE_TO_STR[choice.orig_type]))
+
+    for node in choice.nodes:
+        if node.prompt:
+            break
+    else:
+        choice.kconfig._warn(_name_and_loc(choice) +
+                             " defined without a prompt")
+
+    for default, _ in choice.defaults:
+        if not isinstance(default, Symbol):
+            raise KconfigError(
+                "{} has a malformed default {}"
+                .format(_name_and_loc(choice), expr_str(default)))
+
+        if default.choice is not choice:
+            choice.kconfig._warn("the default selection {} of {} is not "
+                                 "contained in the choice"
+                                 .format(_name_and_loc(default),
+                                         _name_and_loc(choice)))
+
+    for sym in choice.syms:
+        if sym.defaults:
+            sym.kconfig._warn("default on the choice symbol {} will have "
+                              "no effect".format(_name_and_loc(sym)))
+
+        if sym.rev_dep is not sym.kconfig.n:
+            _warn_choice_select_imply(sym, sym.rev_dep, "selected")
+
+        if sym.weak_rev_dep is not sym.kconfig.n:
+            _warn_choice_select_imply(sym, sym.weak_rev_dep, "implied")
+
+        for node in sym.nodes:
+            if node.parent.item is choice:
+                if not node.prompt:
+                    sym.kconfig._warn("the choice symbol {} has no prompt"
+                                      .format(_name_and_loc(sym)))
+
+            elif node.prompt:
+                sym.kconfig._warn("the choice symbol {} is defined with a "
+                                  "prompt outside the choice"
+                                  .format(_name_and_loc(sym)))
+
+def _warn_choice_select_imply(sym, expr, expr_type):
+    msg = "the choice symbol {} is {} by the following symbols, which has " \
+          "no effect: ".format(_name_and_loc(sym), expr_type)
+
+    # si = select/imply
+    for si in split_expr(expr, OR):
+        msg += "\n - " + _name_and_loc(split_expr(si, AND)[0])
+
+    sym.kconfig._warn(msg)
+
+
+# Predefined preprocessor functions
+
+def _filename_fn(kconf, args):
+    return kconf._filename
+
+def _lineno_fn(kconf, args):
+    return str(kconf._linenr)
+
+def _info_fn(kconf, args):
+    print("{}:{}: {}".format(kconf._filename, kconf._linenr, args[1]))
+
+    return ""
+
+def _warning_if_fn(kconf, args):
+    if args[1] == "y":
+        kconf._warn(args[2], kconf._filename, kconf._linenr)
+
+    return ""
+
+def _error_if_fn(kconf, args):
+    if args[1] == "y":
+        raise KconfigError("{}:{}: {}".format(
+            kconf._filename, kconf._linenr, args[2]))
+
+    return ""
+
+def _shell_fn(kconf, args):
+    stdout, stderr = subprocess.Popen(
+        args[1], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
+    ).communicate()
+
+    if not _IS_PY2:
+        try:
+            stdout = stdout.decode(kconf._encoding)
+            stderr = stderr.decode(kconf._encoding)
+        except UnicodeDecodeError as e:
+            _decoding_error(e, kconf._filename, kconf._linenr)
+
+    if stderr:
+        kconf._warn("'{}' wrote to stderr: {}".format(
+                        args[1], "\n".join(stderr.splitlines())),
+                    kconf._filename, kconf._linenr)
+
+    # Manual universal newlines with splitlines() (to prevent e.g. stray \r's
+    # in command output on Windows), trailing newline removal, and
+    # newline-to-space conversion.
+    #
+    # On Python 3 versions before 3.6, it's not possible to specify the
+    # encoding when passing universal_newlines=True to Popen() (the 'encoding'
+    # parameter was added in 3.6), so we do this manual version instead.
+    return "\n".join(stdout.splitlines()).rstrip("\n").replace("\n", " ")
+
+
+#
+# Public global constants
+#
+
+# Integers representing symbol types
+(
+    BOOL,
+    HEX,
+    INT,
+    STRING,
+    TRISTATE,
+    UNKNOWN
+) = range(6)
+
+# Integers representing menu and comment menu nodes
+(
+    MENU,
+    COMMENT,
+) = range(2)
+
+# Converts a symbol/choice type to a string
+TYPE_TO_STR = {
+    UNKNOWN:  "unknown",
+    BOOL:     "bool",
+    TRISTATE: "tristate",
+    STRING:   "string",
+    HEX:      "hex",
+    INT:      "int",
+}
+
+TRI_TO_STR = {
+    0: "n",
+    1: "m",
+    2: "y",
+}
+
+STR_TO_TRI = {
+    "n": 0,
+    "m": 1,
+    "y": 2,
+}
+
+#
+# Internal global constants (plus public expression type
+# constants)
+#
+
+# Note:
+#
+# The token and type constants below are safe to test with 'is', which is a bit
+# faster (~30% faster in a microbenchmark with Python 3 on my machine, and a
+# few % faster for total parsing time), even without assuming Python's small
+# integer optimization (which caches small integer objects). The constants end
+# up pointing to unique integer objects, and since we consistently refer to
+# them via the names below, we always get the same object.
+#
+# Client code would also need to use the names below, because the integer
+# values can change e.g. when tokens get added. Client code would usually test
+# with == too, which would be safe even in super obscure cases involving e.g.
+# pickling (where 'is' would be a bad idea anyway) and no small-integer
+# optimization.
+
+# Are we running on Python 2?
+_IS_PY2 = sys.version_info[0] < 3
+
+# Tokens, with values 1, 2, ... . Avoiding 0 simplifies some checks by making
+# all tokens except empty strings truthy.
+(
+    _T_ALLNOCONFIG_Y,
+    _T_AND,
+    _T_BOOL,
+    _T_CHOICE,
+    _T_CLOSE_PAREN,
+    _T_COMMENT,
+    _T_CONFIG,
+    _T_DEFAULT,
+    _T_DEFCONFIG_LIST,
+    _T_DEF_BOOL,
+    _T_DEF_HEX,
+    _T_DEF_INT,
+    _T_DEF_STRING,
+    _T_DEF_TRISTATE,
+    _T_DEPENDS,
+    _T_ENDCHOICE,
+    _T_ENDIF,
+    _T_ENDMENU,
+    _T_ENV,
+    _T_EQUAL,
+    _T_GREATER,
+    _T_GREATER_EQUAL,
+    _T_HELP,
+    _T_HEX,
+    _T_IF,
+    _T_IMPLY,
+    _T_INT,
+    _T_LESS,
+    _T_LESS_EQUAL,
+    _T_MAINMENU,
+    _T_MENU,
+    _T_MENUCONFIG,
+    _T_MODULES,
+    _T_NOT,
+    _T_ON,
+    _T_OPEN_PAREN,
+    _T_OPTION,
+    _T_OPTIONAL,
+    _T_OR,
+    _T_ORSOURCE,
+    _T_OSOURCE,
+    _T_PROMPT,
+    _T_RANGE,
+    _T_RSOURCE,
+    _T_SELECT,
+    _T_SOURCE,
+    _T_STRING,
+    _T_TRISTATE,
+    _T_UNEQUAL,
+    _T_VISIBLE,
+) = range(1, 51)
+
+# Public integers representing expression types
+#
+# Having these match the value of the corresponding tokens removes the need
+# for conversion
+AND           = _T_AND
+OR            = _T_OR
+NOT           = _T_NOT
+EQUAL         = _T_EQUAL
+UNEQUAL       = _T_UNEQUAL
+LESS          = _T_LESS
+LESS_EQUAL    = _T_LESS_EQUAL
+GREATER       = _T_GREATER
+GREATER_EQUAL = _T_GREATER_EQUAL
+
+# Keyword to token map, with the get() method assigned directly as a small
+# optimization
+_get_keyword = {
+    "---help---":     _T_HELP,
+    "allnoconfig_y":  _T_ALLNOCONFIG_Y,
+    "bool":           _T_BOOL,
+    "boolean":        _T_BOOL,
+    "choice":         _T_CHOICE,
+    "comment":        _T_COMMENT,
+    "config":         _T_CONFIG,
+    "def_bool":       _T_DEF_BOOL,
+    "def_hex":        _T_DEF_HEX,
+    "def_int":        _T_DEF_INT,
+    "def_string":     _T_DEF_STRING,
+    "def_tristate":   _T_DEF_TRISTATE,
+    "default":        _T_DEFAULT,
+    "defconfig_list": _T_DEFCONFIG_LIST,
+    "depends":        _T_DEPENDS,
+    "endchoice":      _T_ENDCHOICE,
+    "endif":          _T_ENDIF,
+    "endmenu":        _T_ENDMENU,
+    "env":            _T_ENV,
+    "grsource":       _T_ORSOURCE,  # Backwards compatibility
+    "gsource":        _T_OSOURCE,   # Backwards compatibility
+    "help":           _T_HELP,
+    "hex":            _T_HEX,
+    "if":             _T_IF,
+    "imply":          _T_IMPLY,
+    "int":            _T_INT,
+    "mainmenu":       _T_MAINMENU,
+    "menu":           _T_MENU,
+    "menuconfig":     _T_MENUCONFIG,
+    "modules":        _T_MODULES,
+    "on":             _T_ON,
+    "option":         _T_OPTION,
+    "optional":       _T_OPTIONAL,
+    "orsource":       _T_ORSOURCE,
+    "osource":        _T_OSOURCE,
+    "prompt":         _T_PROMPT,
+    "range":          _T_RANGE,
+    "rsource":        _T_RSOURCE,
+    "select":         _T_SELECT,
+    "source":         _T_SOURCE,
+    "string":         _T_STRING,
+    "tristate":       _T_TRISTATE,
+    "visible":        _T_VISIBLE,
+}.get
+
+# Tokens after which strings are expected. This is used to tell strings from
+# constant symbol references during tokenization, both of which are enclosed in
+# quotes.
+#
+# Identifier-like lexemes ("missing quotes") are also treated as strings after
+# these tokens. _T_CHOICE is included to avoid symbols being registered for
+# named choices.
+_STRING_LEX = frozenset((
+    _T_BOOL,
+    _T_CHOICE,
+    _T_COMMENT,
+    _T_HEX,
+    _T_INT,
+    _T_MAINMENU,
+    _T_MENU,
+    _T_ORSOURCE,
+    _T_OSOURCE,
+    _T_PROMPT,
+    _T_RSOURCE,
+    _T_SOURCE,
+    _T_STRING,
+    _T_TRISTATE,
+))
+
+# Tokens for types, excluding def_bool, def_tristate, etc., for quick
+# checks during parsing
+_TYPE_TOKENS = frozenset((
+    _T_BOOL,
+    _T_TRISTATE,
+    _T_INT,
+    _T_HEX,
+    _T_STRING,
+))
+
+
+# Helper functions for getting compiled regular expressions, with the needed
+# matching function returned directly as a small optimization.
+#
+# Use ASCII regex matching on Python 3. It's already the default on Python 2.
+
+def _re_match(regex):
+    return re.compile(regex, 0 if _IS_PY2 else re.ASCII).match
+
+def _re_search(regex):
+    return re.compile(regex, 0 if _IS_PY2 else re.ASCII).search
+
+
+# Various regular expressions used during parsing
+
+# The initial token on a line. Also eats leading and trailing whitespace, so
+# that we can jump straight to the next token (or to the end of the line if
+# there is only one token).
+#
+# This regex will also fail to match for empty lines and comment lines.
+#
+# '$' is included to detect a variable assignment left-hand side with a $ in it
+# (which might be from a macro expansion).
+_command_match = _re_match(r"\s*([$A-Za-z0-9_-]+)\s*")
+
+# An identifier/keyword after the first token. Also eats trailing whitespace.
+_id_keyword_match = _re_match(r"([A-Za-z0-9_/.-]+)\s*")
+
+# A fragment in the left-hand side of a preprocessor variable assignment. These
+# are the portions between macro expansions ($(foo)). Macros are supported in
+# the LHS (variable name).
+_assignment_lhs_fragment_match = _re_match("[A-Za-z0-9_-]*")
+
+# The assignment operator and value (right-hand side) in a preprocessor
+# variable assignment
+_assignment_rhs_match = _re_match(r"\s*(=|:=|\+=)\s*(.*)")
+
+# Special characters/strings while expanding a macro (')', ',', and '$(')
+_macro_special_search = _re_search(r"\)|,|\$\(")
+
+# Special characters/strings while expanding a string (quotes, '\', and '$(')
+_string_special_search = _re_search(r'"|\'|\\|\$\(')
+
+# A valid right-hand side for an assignment to a string symbol in a .config
+# file, including escaped characters. Extracts the contents.
+_conf_string_match = _re_match(r'"((?:[^\\"]|\\.)*)"')
+
+
+# Token to type mapping
+_TOKEN_TO_TYPE = {
+    _T_BOOL:         BOOL,
+    _T_DEF_BOOL:     BOOL,
+    _T_DEF_HEX:      HEX,
+    _T_DEF_INT:      INT,
+    _T_DEF_STRING:   STRING,
+    _T_DEF_TRISTATE: TRISTATE,
+    _T_HEX:          HEX,
+    _T_INT:          INT,
+    _T_STRING:       STRING,
+    _T_TRISTATE:     TRISTATE,
+}
+
+# Constant representing that there's no cached choice selection. This is
+# distinct from a cached None (no selection). We create a unique object (any
+# will do) for it so we can test with 'is'.
+_NO_CACHED_SELECTION = object()
+
+# Used in comparisons. 0 means the base is inferred from the format of the
+# string.
+_TYPE_TO_BASE = {
+    HEX:      16,
+    INT:      10,
+    STRING:   0,
+    UNKNOWN:  0,
+}
+
+# Note: These constants deliberately equal the corresponding tokens (_T_EQUAL,
+# _T_UNEQUAL, etc.), which removes the need for conversion
+_RELATIONS = frozenset((
+    EQUAL,
+    UNEQUAL,
+    LESS,
+    LESS_EQUAL,
+    GREATER,
+    GREATER_EQUAL,
+))
+
+_REL_TO_STR = {
+    EQUAL:         "=",
+    UNEQUAL:       "!=",
+    LESS:          "<",
+    LESS_EQUAL:    "<=",
+    GREATER:       ">",
+    GREATER_EQUAL: ">=",
+}
+
+_INIT_SRCTREE_NOTE = """
+NOTE: Starting with Kconfiglib 10.0.0, the Kconfig filename passed to
+Kconfig.__init__() is looked up relative to $srctree (which is set to '{}')
+instead of relative to the working directory. Previously, $srctree only applied
+to files being source'd within Kconfig files. This change makes running scripts
+out-of-tree work seamlessly, with no special coding required. Sorry for the
+backwards compatibility break!
+"""[1:]

+ 108 - 16
tools/menuconfig.py

@@ -20,8 +20,12 @@
 # Change Logs:
 # Date           Author       Notes
 # 2017-12-29     Bernard      The first version
+# 2018-07-31.....weety        Support pyconfig
 
 import os
+import sys
+import shutil
+import pymenuconfig
 
 # make rtconfig.h from .config
 
@@ -32,7 +36,7 @@ def mk_rtconfig(filename):
         print('open config:%s failed' % filename)
         return
 
-    rtconfig = file('rtconfig.h', 'w')
+    rtconfig = file('rtconfig.h', 'wb')
     rtconfig.write('#ifndef RT_CONFIG_H__\n')
     rtconfig.write('#define RT_CONFIG_H__\n\n')
 
@@ -88,8 +92,12 @@ def get_env_dir():
     if os.environ.get('ENV_ROOT'):
         return os.environ.get('ENV_ROOT')
 
-    home_dir = os.environ['HOME']
-    env_dir  = os.path.join(home_dir, '.env')
+    if sys.platform == 'win32':
+        home_dir = os.environ['USERPROFILE']
+        env_dir  = os.path.join(home_dir, '.env')
+    else:
+        home_dir = os.environ['HOME']
+        env_dir  = os.path.join(home_dir, '.env')
 
     if not os.path.exists(env_dir):
         return None
@@ -97,23 +105,79 @@ def get_env_dir():
     return env_dir
 
 def touch_env():
-    home_dir = os.environ['HOME']
-    env_dir  = os.path.join(home_dir, '.env')
-    if not os.path.exists(env_dir):
-        os.mkdir(env_dir)
-        os.mkdir(os.path.join(env_dir, 'local_pkgs'))
-        os.mkdir(os.path.join(env_dir, 'packages'))
+    if sys.platform != 'win32':
+        home_dir = os.environ['HOME']
+        env_dir  = os.path.join(home_dir, '.env')
+        if not os.path.exists(env_dir):
+            os.mkdir(env_dir)
+            os.mkdir(os.path.join(env_dir, 'local_pkgs'))
+            os.mkdir(os.path.join(env_dir, 'packages'))
 
-        os.system('git clone https://github.com/RT-Thread/packages.git ~/.env/packages/packages')
+            os.system('git clone https://github.com/RT-Thread/packages.git ~/.env/packages/packages')
 
-        kconfig = file(os.path.join(env_dir, 'packages', 'Kconfig'), 'w')
-        kconfig.write('source "$PKGS_DIR/packages/Kconfig"')
+            kconfig = file(os.path.join(env_dir, 'packages', 'Kconfig'), 'w')
+            kconfig.write('source "$PKGS_DIR/packages/Kconfig"')
 
-        os.mkdir(os.path.join(env_dir, 'tools'))
-        os.system('git clone https://github.com/RT-Thread/env.git ~/.env/tools/scripts')
+            os.mkdir(os.path.join(env_dir, 'tools'))
+            os.system('git clone https://github.com/RT-Thread/env.git ~/.env/tools/scripts')
 
-        env_sh = file(os.path.join(env_dir, 'env.sh'), 'w')
-        env_sh.write('export PATH=~/.env/tools/scripts:$PATH')
+            env_sh = file(os.path.join(env_dir, 'env.sh'), 'w')
+            env_sh.write('export PATH=~/.env/tools/scripts:$PATH')
+    else:
+        home_dir = os.environ['USERPROFILE']
+        env_dir  = os.path.join(home_dir, '.env')
+        if not os.path.exists(env_dir):
+            os.mkdir(env_dir)
+            os.mkdir(os.path.join(env_dir, 'local_pkgs'))
+            os.mkdir(os.path.join(env_dir, 'packages'))
+            os.mkdir(os.path.join(env_dir, 'tools'))
+            kconfig = file(os.path.join(env_dir, 'packages', 'Kconfig'), 'wb')
+            kconfig.close()
+
+        if not os.path.exists(os.path.join(env_dir, 'packages', 'packages')):
+            try:
+                ret = os.system('git clone https://github.com/RT-Thread/packages.git %s' % os.path.join(env_dir, 'packages', 'packages'))
+                if ret != 0:
+                    shutil.rmtree(os.path.join(env_dir, 'packages', 'packages'))
+                    print("********************************************************************************\n"
+                          "* Run command error for \"git clone https://github.com/RT-Thread/packages.git\".\n"
+                          "* This error may have been caused by not found a git tool or network error.\n"
+                          "* If the git tool is not installed, install the git tool first.\n"
+                          "* If the git utility is installed, check whether the git command is added to the system PATH.\n"
+                          "* This error may cause the RT-Thread packages to not work properly.\n"
+                          "********************************************************************************\n")
+                else:
+                    kconfig = file(os.path.join(env_dir, 'packages', 'Kconfig'), 'wb')
+                    kconfig.write('source "$PKGS_DIR/packages/Kconfig"')
+                    kconfig.close()
+            except:
+                print("********************************************************************************\n"
+                      "* Run command error for \"git clone https://github.com/RT-Thread/packages.git\". \n"
+                      "* This error may have been caused by not found a git tool or git tool not in the system PATH. \n"
+                      "* This error may cause the RT-Thread packages to not work properly. \n"
+                      "********************************************************************************\n")
+
+        if not os.path.exists(os.path.join(env_dir, 'tools', 'scripts')):
+            try:
+                ret = os.system('git clone https://github.com/RT-Thread/env.git %s' % os.path.join(env_dir, 'tools', 'scripts'))
+                if ret != 0:
+                    shutil.rmtree(os.path.join(env_dir, 'tools', 'scripts'))
+                    print("********************************************************************************\n"
+                          "* Run command error for \"git clone https://github.com/RT-Thread/env.git\".\n"
+                          "* This error may have been caused by not found a git tool or network error.\n"
+                          "* If the git tool is not installed, install the git tool first.\n"
+                          "* If the git utility is installed, check whether the git command is added to the system PATH.\n"
+                          "* This error may cause script tools to fail to work properly.\n"
+                          "********************************************************************************\n")
+            except:
+                print("********************************************************************************\n"
+                      "* Run command error for \"git clone https://github.com/RT-Thread/env.git\". \n"
+                      "* This error may have been caused by not found a git tool or git tool not in the system PATH. \n"
+                      "* This error may cause script tools to fail to work properly. \n"
+                      "********************************************************************************\n")
+
+        if os.path.exists(os.path.join(env_dir, 'tools', 'scripts')):
+            os.environ["PATH"] = os.path.join(env_dir, 'tools', 'scripts') + ';' + os.environ["PATH"]
 
 # menuconfig for Linux
 def menuconfig(RTT_ROOT):
@@ -146,3 +210,31 @@ def menuconfig(RTT_ROOT):
     if mtime != mtime2:
         mk_rtconfig(fn)
 
+# pyconfig for windows and linux
+def pyconfig(RTT_ROOT):
+
+    env_dir = get_env_dir()
+    if not env_dir:
+        touch_env()
+        env_dir = get_env_dir()
+
+    os.environ['PKGS_ROOT'] = os.path.join(env_dir, 'packages')
+
+    fn = '.config'
+
+    if os.path.isfile(fn):
+        mtime = os.path.getmtime(fn)
+    else:
+        mtime = -1
+
+    pymenuconfig.main(['--kconfig', 'Kconfig', '--config', '.config'])
+
+    if os.path.isfile(fn):
+        mtime2 = os.path.getmtime(fn)
+    else:
+        mtime2 = -1
+
+    # make rtconfig.h
+    if mtime != mtime2:
+        mk_rtconfig(fn)
+

+ 1103 - 0
tools/pymenuconfig.py

@@ -0,0 +1,1103 @@
+# SPDX-License-Identifier: ISC
+# -*- coding: utf-8 -*-
+
+"""
+Overview
+========
+
+pymenuconfig is a small and simple frontend to Kconfiglib that's written
+entirely in Python using Tkinter as its GUI toolkit.
+
+Motivation
+==========
+
+Kconfig is a nice and powerful framework for build-time configuration and lots
+of projects already benefit from using it. Kconfiglib allows to utilize power of
+Kconfig by using scripts written in pure Python, without requiring one to build
+Linux kernel tools written in C (this can be quite tedious on anything that's
+not *nix). The aim of this project is to implement simple and small Kconfiglib
+GUI frontend that runs on as much systems as possible.
+
+Tkinter GUI toolkit is a natural choice if portability is considered, as it's
+a part of Python standard library and is available virtually in every CPython
+installation.
+
+
+User interface
+==============
+
+I've tried to replicate look and fill of Linux kernel 'menuconfig' tool that
+many users are used to, including keyboard-oriented control and textual
+representation of menus with fixed-width font.
+
+
+Usage
+=====
+
+The pymenuconfig module is executable and parses command-line args, so the
+most simple way to run menuconfig is to execute script directly:
+
+  python pymenuconfig.py --kconfig Kconfig
+
+As with most command-line tools list of options can be obtained with '--help':
+
+  python pymenuconfig.py --help
+
+If installed with setuptools, one can run it like this:
+
+  python -m pymenuconfig --kconfig Kconfig
+
+In case you're making a wrapper around menuconfig, you can either call main():
+
+  import pymenuconfig
+  pymenuconfig.main(['--kconfig', 'Kconfig'])
+
+Or import MenuConfig class, instantiate it and manually run Tkinter's mainloop:
+
+  import tkinter
+  import kconfiglib
+  from pymenuconfig import MenuConfig
+
+  kconfig = kconfiglib.Kconfig()
+  mconf = MenuConfig(kconfig)
+  tkinter.mainloop()
+
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+import argparse
+import kconfiglib
+
+# Tk is imported differently depending on python major version
+if sys.version_info[0] < 3:
+    import Tkinter as tk
+    import tkFont as font
+    import tkFileDialog as filedialog
+    import tkMessageBox as messagebox
+else:
+    import tkinter as tk
+    from tkinter import font
+    from tkinter import filedialog
+    from tkinter import messagebox
+
+
+class ListEntry(object):
+    """
+    Represents visible menu node and holds all information related to displaying
+    menu node in a Listbox.
+
+    Instances of this class also handle all interaction with main window.
+    A node is displayed as a single line of text:
+      PREFIX INDENT BODY POSTFIX
+    - The PREFIX is always 3 characters or more and can take following values:
+      '   ' comment, menu, bool choice, etc.
+      Inside menus:
+      '< >' bool symbol has value 'n'
+      '<*>' bool symbol has value 'y'
+      '[ ]' tristate symbol has value 'n'
+      '[M]' tristate symbol has value 'm'
+      '[*]' tristate symbol has value 'y'
+      '- -' symbol has value 'n' that's not editable
+      '-M-' symbol has value 'm' that's not editable
+      '-*-' symbol has value 'y' that's not editable
+      '(M)' tristate choice has value 'm'
+      '(*)' tristate choice has value 'y'
+      '(some value)' value of non-bool/tristate symbols
+      Inside choices:
+      '( )' symbol has value 'n'
+      '(M)' symbol has value 'm'
+      '(*)' symbol has value 'y'
+    - INDENT is a sequence of space characters. It's used in implicit menus, and
+      adds 2 spaces for each nesting level
+    - BODY is a menu node prompt. '***' is added if node is a comment
+    - POSTFIX adds '(NEW)', '--->' and selected choice symbol where applicable
+
+    Attributes:
+
+    node:
+      MenuNode instance this ListEntry is created for.
+
+    visible:
+      Whether entry should be shown in main window.
+
+    text:
+      String to display in a main window's Listbox.
+
+    refresh():
+      Updates .visible and .text attribute values.
+
+    set_tristate_value():
+      Set value for bool/tristate symbols, value should be one of 0,1,2 or None.
+      Usually it's called when user presses 'y', 'n', 'm' key.
+
+    set_str_value():
+      Set value for non-bool/tristate symbols, value is a string. Usually called
+      with a value returned by one of MenuConfig.ask_for_* methods.
+
+    toggle():
+      Toggle bool/tristate symbol value. Called when '<Space>' key is pressed in
+      a main window. Also selects choice value.
+
+    select():
+      Called when '<Return>' key is pressed in a main window with 'SELECT'
+      action selected. Displays submenu, choice selection menu, or just selects
+      choice value. For non-bool/tristate symbols asks MenuConfig window to
+      handle value input via one of MenuConfig.ask_for_* methods.
+
+    show_help():
+      Called when '<Return>' key is pressed in a main window with 'HELP' action
+      selected. Prepares text help and calls MenuConfig.show_text() to display
+      text window.
+    """
+
+    # How to display value of BOOL and TRISTATE symbols
+    TRI_TO_DISPLAY = {
+        0: ' ',
+        1: 'M',
+        2: '*'
+    }
+
+    def __init__(self, mconf, node, indent):
+        self.indent = indent
+        self.node = node
+        self.menuconfig = mconf
+        self.visible = False
+        self.text = None
+
+    def __str__(self):
+        return self.text
+
+    def _is_visible(self):
+        node = self.node
+        v = True
+        v = v and node.prompt is not None
+        # It should be enough to check if prompt expression is not false and
+        # for menu nodes whether 'visible if' is not false
+        v = v and kconfiglib.expr_value(node.prompt[1]) > 0
+        if node.item == kconfiglib.MENU:
+            v = v and kconfiglib.expr_value(node.visibility) > 0
+        # If node references Symbol, then we also account for symbol visibility
+        # TODO: need to re-think whether this is needed
+        if isinstance(node.item, kconfiglib.Symbol):
+            if node.item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
+                v = v and len(node.item.assignable) > 0
+            else:
+                v = v and node.item.visibility > 0
+        return v
+
+    def _get_text(self):
+        """
+        Compute textual representation of menu node (a line in ListView)
+        """
+        node = self.node
+        item = node.item
+        # Determine prefix
+        prefix = '   '
+        if (isinstance(item, kconfiglib.Symbol) and item.choice is None or
+            isinstance(item, kconfiglib.Choice) and item.type is kconfiglib.TRISTATE):
+            # The node is for either a symbol outside of choice statement
+            # or a tristate choice
+            if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
+                value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
+                if len(item.assignable) > 1:
+                    # Symbol is editable
+                    if 1 in item.assignable:
+                        prefix = '<{}>'.format(value)
+                    else:
+                        prefix = '[{}]'.format(value)
+                else:
+                    # Symbol is not editable
+                    prefix = '-{}-'.format(value)
+            else:
+                prefix = '({})'.format(item.str_value)
+        elif isinstance(item, kconfiglib.Symbol) and item.choice is not None:
+            # The node is for symbol inside choice statement
+            if item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
+                value = ListEntry.TRI_TO_DISPLAY[item.tri_value]
+                if len(item.assignable) > 0:
+                    # Symbol is editable
+                    prefix = '({})'.format(value)
+                else:
+                    # Symbol is not editable
+                    prefix = '-{}-'.format(value)
+            else:
+                prefix = '({})'.format(item.str_value)
+
+        # Prefix should be at least 3 chars long
+        if len(prefix) < 3:
+            prefix += ' ' * (3 - len(prefix))
+        # Body
+        body = ''
+        if node.prompt is not None:
+            if item is kconfiglib.COMMENT:
+                body = '*** {} ***'.format(node.prompt[0])
+            else:
+                body = node.prompt[0]
+        # Suffix
+        is_menu = False
+        is_new = False
+        if (item is kconfiglib.MENU
+            or isinstance(item, kconfiglib.Symbol) and node.is_menuconfig
+            or isinstance(item, kconfiglib.Choice)):
+            is_menu = True
+        if isinstance(item, kconfiglib.Symbol) and item.user_value is None:
+            is_new = True
+        # For symbol inside choice that has 'y' value, '(NEW)' is not displayed
+        if (isinstance(item, kconfiglib.Symbol)
+            and item.choice and item.choice.tri_value == 2):
+            is_new = False
+        # Choice selection - displayed only for choices which have 'y' value
+        choice_selection = None
+        if isinstance(item, kconfiglib.Choice) and node.item.str_value == 'y':
+            choice_selection = ''
+            if item.selection is not None:
+                sym = item.selection
+                if sym.nodes and sym.nodes[0].prompt is not None:
+                    choice_selection = sym.nodes[0].prompt[0]
+        text = '  {prefix} {indent}{body}{choice}{new}{menu}'.format(
+            prefix=prefix,
+            indent='  ' * self.indent,
+            body=body,
+            choice='' if choice_selection is None else ' ({})'.format(
+                choice_selection
+            ),
+            new=' (NEW)' if is_new else '',
+            menu=' --->' if is_menu else ''
+        )
+        return text
+
+    def refresh(self):
+        self.visible = self._is_visible()
+        self.text = self._get_text()
+
+    def set_tristate_value(self, value):
+        """
+        Call to change value of BOOL, TRISTATE symbols
+
+        It's preferred to use this instead of item.set_value as it handles
+        all necessary interaction with MenuConfig window when symbol value
+        changes
+
+        None value is accepted but ignored
+        """
+        item = self.node.item
+        if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
+            and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)
+            and value is not None):
+            if value in item.assignable:
+                item.set_value(value)
+            elif value == 2 and 1 in item.assignable:
+                print(
+                    'Symbol {} value is limited to \'m\'. Setting value \'m\' instead of \'y\''.format(item.name),
+                    file=sys.stderr
+                )
+                item.set_value(1)
+            self.menuconfig.mark_as_changed()
+            self.menuconfig.refresh_display()
+
+    def set_str_value(self, value):
+        """
+        Call to change value of HEX, INT, STRING symbols
+
+        It's preferred to use this instead of item.set_value as it handles
+        all necessary interaction with MenuConfig window when symbol value
+        changes
+
+        None value is accepted but ignored
+        """
+        item = self.node.item
+        if (isinstance(item, kconfiglib.Symbol)
+            and item.type in (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)
+            and value is not None):
+            item.set_value(value)
+            self.menuconfig.mark_as_changed()
+            self.menuconfig.refresh_display()
+
+    def toggle(self):
+        """
+        Called when <space> key is pressed
+        """
+        item = self.node.item
+        if (isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice))
+            and item.type in (kconfiglib.BOOL, kconfiglib.TRISTATE)):
+            value = item.tri_value
+            # Find next value in Symbol/Choice.assignable, or use assignable[0]
+            try:
+                it = iter(item.assignable)
+                while value != next(it):
+                    pass
+                self.set_tristate_value(next(it))
+            except StopIteration:
+                self.set_tristate_value(item.assignable[0])
+
+    def select(self):
+        """
+        Called when <Return> key is pressed and SELECT action is selected
+        """
+        item = self.node.item
+        # - Menu: dive into submenu
+        # - INT, HEX, STRING symbol: raise prompt to enter symbol value
+        # - BOOL, TRISTATE symbol inside 'y'-valued Choice: set 'y' value
+        if (item is kconfiglib.MENU
+            or isinstance(item, kconfiglib.Symbol) and self.node.is_menuconfig
+            or isinstance(item, kconfiglib.Choice)):
+            # Dive into submenu
+            self.menuconfig.show_submenu(self.node)
+        elif (isinstance(item, kconfiglib.Symbol) and item.type in
+              (kconfiglib.INT, kconfiglib.HEX, kconfiglib.STRING)):
+            # Raise prompt to enter symbol value
+            ident = self.node.prompt[0] if self.node.prompt is not None else None
+            title = 'Symbol: {}'.format(item.name)
+            if item.type is kconfiglib.INT:
+                # Find enabled ranges
+                ranges = [
+                    (int(start.str_value), int(end.str_value))
+                    for start, end, expr in item.ranges
+                    if kconfiglib.expr_value(expr) > 0
+                ]
+                # Raise prompt
+                self.set_str_value(str(self.menuconfig.ask_for_int(
+                    ident=ident,
+                    title=title,
+                    value=item.str_value,
+                    ranges=ranges
+                )))
+            elif item.type is kconfiglib.HEX:
+                # Find enabled ranges
+                ranges = [
+                    (int(start.str_value, base=16), int(end.str_value, base=16))
+                    for start, end, expr in item.ranges
+                    if kconfiglib.expr_value(expr) > 0
+                ]
+                # Raise prompt
+                self.set_str_value(hex(self.menuconfig.ask_for_hex(
+                    ident=ident,
+                    title=title,
+                    value=item.str_value,
+                    ranges=ranges
+                )))
+            elif item.type is kconfiglib.STRING:
+                # Raise prompt
+                self.set_str_value(self.menuconfig.ask_for_string(
+                    ident=ident,
+                    title=title,
+                    value=item.str_value
+                ))
+        elif (isinstance(item, kconfiglib.Symbol)
+              and item.choice is not None and item.choice.tri_value == 2):
+            # Symbol inside choice -> set symbol value to 'y'
+            self.set_tristate_value(2)
+
+    def show_help(self):
+        node = self.node
+        item = self.node.item
+        if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)):
+            title = 'Help for symbol: {}'.format(item.name)
+            if node.help:
+                help = node.help
+            else:
+                help = 'There is no help available for this option.\n'
+            lines = []
+            lines.append(help)
+            lines.append(
+                'Symbol: {} [={}]'.format(
+                    item.name if item.name else '<UNNAMED>', item.str_value
+                )
+            )
+            lines.append('Type  : {}'.format(kconfiglib.TYPE_TO_STR[item.type]))
+            for n in item.nodes:
+                lines.append('Prompt: {}'.format(n.prompt[0] if n.prompt else '<EMPTY>'))
+                lines.append('  Defined at {}:{}'.format(n.filename, n.linenr))
+                lines.append('  Depends on: {}'.format(kconfiglib.expr_str(n.dep)))
+            text = '\n'.join(lines)
+        else:
+            title = 'Help'
+            text = 'Help not available for this menu node.\n'
+        self.menuconfig.show_text(text, title)
+        self.menuconfig.refresh_display()
+
+
+class EntryDialog(object):
+    """
+    Creates modal dialog (top-level Tk window) with labels, entry box and two
+    buttons: OK and CANCEL.
+    """
+    def __init__(self, master, text, title, ident=None, value=None):
+        self.master = master
+        dlg = self.dlg = tk.Toplevel(master)
+        dlg.title(title)
+        # Identifier label
+        if ident is not None:
+            self.label_id = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
+            self.label_id['font'] = font.nametofont('TkFixedFont')
+            self.label_id['text'] = '# {}'.format(ident)
+            self.label_id.pack(fill=tk.X, padx=2, pady=2)
+        # Label
+        self.label = tk.Label(dlg, anchor=tk.W, justify=tk.LEFT)
+        self.label['font'] = font.nametofont('TkFixedFont')
+        self.label['text'] = text
+        self.label.pack(fill=tk.X, padx=10, pady=4)
+        # Entry box
+        self.entry = tk.Entry(dlg)
+        self.entry['font'] = font.nametofont('TkFixedFont')
+        self.entry.pack(fill=tk.X, padx=2, pady=2)
+        # Frame for buttons
+        self.frame = tk.Frame(dlg)
+        self.frame.pack(padx=2, pady=2)
+        # Button
+        self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
+        self.btn_accept['font'] = font.nametofont('TkFixedFont')
+        self.btn_accept.pack(side=tk.LEFT, padx=2)
+        self.btn_cancel = tk.Button(self.frame, text='< Cancel >', command=self.cancel)
+        self.btn_cancel['font'] = font.nametofont('TkFixedFont')
+        self.btn_cancel.pack(side=tk.LEFT, padx=2)
+        # Bind Enter and Esc keys
+        self.dlg.bind('<Return>', self.accept)
+        self.dlg.bind('<Escape>', self.cancel)
+        # Dialog is resizable only by width
+        self.dlg.resizable(1, 0)
+        # Set supplied value (if any)
+        if value is not None:
+            self.entry.insert(0, value)
+            self.entry.selection_range(0, tk.END)
+        # By default returned value is None. To caller this means that entry
+        # process was cancelled
+        self.value = None
+        # Modal dialog
+        dlg.transient(master)
+        dlg.grab_set()
+        # Center dialog window
+        _center_window_above_parent(master, dlg)
+        # Focus entry field
+        self.entry.focus_set()
+
+    def accept(self, ev=None):
+        self.value = self.entry.get()
+        self.dlg.destroy()
+
+    def cancel(self, ev=None):
+        self.dlg.destroy()
+
+
+class TextDialog(object):
+    def __init__(self, master, text, title):
+        self.master = master
+        dlg = self.dlg = tk.Toplevel(master)
+        dlg.title(title)
+        dlg.minsize(600,400)
+        # Text
+        self.text = tk.Text(dlg, height=1)
+        self.text['font'] = font.nametofont('TkFixedFont')
+        self.text.insert(tk.END, text)
+        # Make text read-only
+        self.text['state'] = tk.DISABLED
+        self.text.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
+        # Frame for buttons
+        self.frame = tk.Frame(dlg)
+        self.frame.pack(padx=2, pady=2)
+        # Button
+        self.btn_accept = tk.Button(self.frame, text='< Ok >', command=self.accept)
+        self.btn_accept['font'] = font.nametofont('TkFixedFont')
+        self.btn_accept.pack(side=tk.LEFT, padx=2)
+        # Bind Enter and Esc keys
+        self.dlg.bind('<Return>', self.accept)
+        self.dlg.bind('<Escape>', self.cancel)
+        # Modal dialog
+        dlg.transient(master)
+        dlg.grab_set()
+        # Center dialog window
+        _center_window_above_parent(master, dlg)
+        # Focus entry field
+        self.text.focus_set()
+
+    def accept(self, ev=None):
+        self.dlg.destroy()
+
+    def cancel(self, ev=None):
+        self.dlg.destroy()
+
+
+class MenuConfig(object):
+    (
+        ACTION_SELECT,
+        ACTION_EXIT,
+        ACTION_HELP,
+        ACTION_LOAD,
+        ACTION_SAVE,
+        ACTION_SAVE_AS
+    ) = range(6)
+
+    ACTIONS = (
+        ('Select', ACTION_SELECT),
+        ('Exit', ACTION_EXIT),
+        ('Help', ACTION_HELP),
+        ('Load', ACTION_LOAD),
+        ('Save', ACTION_SAVE),
+        ('Save as', ACTION_SAVE_AS),
+    )
+
+    def __init__(self, kconfig):
+        self.kconfig = kconfig
+
+        # Instantiate Tk widgets
+        self.root = tk.Tk()
+        dlg = self.root
+
+        # Window title
+        dlg.title('pymenuconfig')
+        # Some empirical window size
+        dlg.minsize(500, 300)
+        dlg.geometry('800x600')
+
+        # Label that shows position in menu tree
+        self.label_position = tk.Label(
+            dlg,
+            anchor=tk.W,
+            justify=tk.LEFT,
+            font=font.nametofont('TkFixedFont')
+        )
+        self.label_position.pack(fill=tk.X, padx=2)
+
+        # 'Tip' frame and text
+        self.frame_tip = tk.LabelFrame(
+            dlg,
+            text='Tip'
+        )
+        self.label_tip = tk.Label(
+            self.frame_tip,
+            anchor=tk.W,
+            justify=tk.LEFT,
+            font=font.nametofont('TkFixedFont')
+        )
+        self.label_tip['text'] = '\n'.join([
+            'Arrow keys navigate the menu. <Enter> performs selected operation (set of buttons at the bottom)',
+            'Pressing <Y> includes, <N> excludes, <M> modularizes features',
+            'Press <Esc> to go one level up. Press <Esc> at top level to exit',
+            'Legend: [*] built-in  [ ] excluded  <M> module  < > module capable'
+        ])
+        self.label_tip.pack(fill=tk.BOTH, expand=1, padx=4, pady=4)
+        self.frame_tip.pack(fill=tk.X, padx=2)
+
+        # Main ListBox where all the magic happens
+        self.list = tk.Listbox(
+            dlg,
+            selectmode=tk.SINGLE,
+            activestyle=tk.UNDERLINE,
+            font=font.nametofont('TkFixedFont'),
+            height=1,
+        )
+        # Make selection invisible
+        self.list['selectbackground'] = self.list['background']
+        self.list['selectforeground'] = self.list['foreground']
+        self.list.pack(fill=tk.BOTH, expand=1, padx=20, ipadx=2)
+
+        # Frame with radio buttons
+        self.frame_radio = tk.Frame(dlg)
+        self.radio_buttons = []
+        self.tk_selected_action = tk.IntVar()
+        for text, value in MenuConfig.ACTIONS:
+            btn = tk.Radiobutton(
+                self.frame_radio,
+                variable=self.tk_selected_action,
+                value=value
+            )
+            btn['text'] = '< {} >'.format(text)
+            btn['font'] = font.nametofont('TkFixedFont')
+            btn['indicatoron'] = 0
+            btn.pack(side=tk.LEFT)
+            self.radio_buttons.append(btn)
+        self.frame_radio.pack(anchor=tk.CENTER, pady=4)
+        # Label with status information
+        self.tk_status = tk.StringVar()
+        self.label_status = tk.Label(
+            dlg,
+            textvariable=self.tk_status,
+            anchor=tk.W,
+            justify=tk.LEFT,
+            font=font.nametofont('TkFixedFont')
+        )
+        self.label_status.pack(fill=tk.X, padx=4, pady=4)
+        # Center window
+        _center_window(self.root, dlg)
+        # Disable keyboard focus on all widgets ...
+        self._set_option_to_all_children(dlg, 'takefocus', 0)
+        # ... except for main ListBox
+        self.list['takefocus'] = 1
+        self.list.focus_set()
+        # Bind keys
+        dlg.bind('<Escape>', self.handle_keypress)
+        dlg.bind('<space>', self.handle_keypress)
+        dlg.bind('<Return>', self.handle_keypress)
+        dlg.bind('<Right>', self.handle_keypress)
+        dlg.bind('<Left>', self.handle_keypress)
+        dlg.bind('n', self.handle_keypress)
+        dlg.bind('m', self.handle_keypress)
+        dlg.bind('y', self.handle_keypress)
+        # Register callback that's called when window closes
+        dlg.wm_protocol('WM_DELETE_WINDOW', self._close_window)
+        # Init fields
+        self.node = None
+        self.node_stack = []
+        self.all_entries = []
+        self.shown_entries = []
+        self.config_path = None
+        self.unsaved_changes = False
+        self.status_string = 'NEW CONFIG'
+        self.update_status()
+        # Display first child of top level node (the top level node is 'mainmenu')
+        self.show_node(self.kconfig.top_node)
+
+    def _set_option_to_all_children(self, widget, option, value):
+        widget[option] = value
+        for n,c in widget.children.items():
+            self._set_option_to_all_children(c, option, value)
+
+    @property
+    def _selected_entry(self):
+        # type: (...) -> ListEntry
+        active_idx = self.list.index(tk.ACTIVE)
+        if active_idx >= 0 and active_idx < len(self.shown_entries):
+            return self.shown_entries[active_idx]
+        return None
+
+    def _select_node(self, node):
+        # type: (kconfiglib.MenuNode) -> None
+        """
+        Attempts to select entry that corresponds to given MenuNode in main listbox
+        """
+        idx = None
+        for i, e in enumerate(self.shown_entries):
+            if e.node is node:
+                idx = i
+                break
+        if idx is not None:
+            self.list.activate(idx)
+            self.list.see(idx)
+
+    def handle_keypress(self, ev):
+        keysym = ev.keysym
+        if keysym == 'Left':
+            self._select_action(prev=True)
+        elif keysym == 'Right':
+            self._select_action(prev=False)
+        elif keysym == 'space':
+            self._selected_entry.toggle()
+        elif keysym in ('n', 'm', 'y'):
+            self._selected_entry.set_tristate_value(kconfiglib.STR_TO_TRI[keysym])
+        elif keysym == 'Return':
+            action = self.tk_selected_action.get()
+            if action == self.ACTION_SELECT:
+                self._selected_entry.select()
+            elif action == self.ACTION_EXIT:
+                self._action_exit()
+            elif action == self.ACTION_HELP:
+                self._selected_entry.show_help()
+            elif action == self.ACTION_LOAD:
+                if self.prevent_losing_changes():
+                    self.open_config()
+            elif action == self.ACTION_SAVE:
+                self.save_config()
+            elif action == self.ACTION_SAVE_AS:
+                self.save_config(force_file_dialog=True)
+        elif keysym == 'Escape':
+            self._action_exit()
+        pass
+
+    def _close_window(self):
+        if self.prevent_losing_changes():
+            print('Exiting..')
+            self.root.destroy()
+
+    def _action_exit(self):
+        if self.node_stack:
+            self.show_parent()
+        else:
+            self._close_window()
+
+    def _select_action(self, prev=False):
+        # Determine the radio button that's activated
+        action = self.tk_selected_action.get()
+        if prev:
+            action -= 1
+        else:
+            action += 1
+        action %= len(MenuConfig.ACTIONS)
+        self.tk_selected_action.set(action)
+
+    def _collect_list_entries(self, start_node, indent=0):
+        """
+        Given first MenuNode of nodes list at some level in menu hierarchy,
+        collects nodes that may be displayed when viewing and editing that
+        hierarchy level. Includes implicit menu nodes, i.e. the ones dependent
+        on 'config' entry via 'if' statement which are internally represented
+        as children of their dependency
+        """
+        entries = []
+        n = start_node
+        while n is not None:
+            entries.append(ListEntry(self, n, indent))
+            # If node refers to a symbol (X) and has children, it is either
+            # 'config' or 'menuconfig'. The children are items inside 'if X'
+            # block that immediately follows 'config' or 'menuconfig' entry.
+            # If it's a 'menuconfig' then corresponding MenuNode is shown as a
+            # regular menu entry. But if it's a 'config', then its children need
+            # to be shown in the same list with their texts indented
+            if (n.list is not None
+                and isinstance(n.item, kconfiglib.Symbol)
+                and n.is_menuconfig == False):
+                entries.extend(
+                    self._collect_list_entries(n.list, indent=indent + 1)
+                )
+            n = n.next
+        return entries
+
+    def refresh_display(self, reset_selection=False):
+        # Refresh list entries' attributes
+        for e in self.all_entries:
+            e.refresh()
+        # Try to preserve selection upon refresh
+        selected_entry = self._selected_entry
+        # Also try to preserve listbox scroll offset
+        # If not preserved, the see() method will make wanted item to appear
+        # at the bottom of the list, even if previously it was in center
+        scroll_offset = self.list.yview()[0]
+        # Show only visible entries
+        self.shown_entries = [e for e in self.all_entries if e.visible]
+        # Refresh listbox contents
+        self.list.delete(0, tk.END)
+        self.list.insert(0, *self.shown_entries)
+        if selected_entry and not reset_selection:
+            # Restore scroll position
+            self.list.yview_moveto(scroll_offset)
+            # Activate previously selected node
+            self._select_node(selected_entry.node)
+        else:
+            # Select the topmost entry
+            self.list.activate(0)
+        # Select ACTION_SELECT on each refresh (mimic C menuconfig)
+        self.tk_selected_action.set(self.ACTION_SELECT)
+        # Display current location in configuration tree
+        pos = []
+        for n in self.node_stack + [self.node]:
+            pos.append(n.prompt[0] if n.prompt else '[none]')
+        self.label_position['text'] = u'# ' + u' → '.join(pos)
+
+    def show_node(self, node):
+        self.node = node
+        if node.list is not None:
+            self.all_entries = self._collect_list_entries(node.list)
+        else:
+            self.all_entries = []
+        self.refresh_display(reset_selection=True)
+
+    def show_submenu(self, node):
+        self.node_stack.append(self.node)
+        self.show_node(node)
+
+    def show_parent(self):
+        if self.node_stack:
+            select_node = self.node
+            parent_node = self.node_stack.pop()
+            self.show_node(parent_node)
+            # Restore previous selection
+            self._select_node(select_node)
+
+    def ask_for_string(self, ident=None, title='Enter string', value=None):
+        """
+        Raises dialog with text entry widget and asks user to enter string
+
+        Return:
+            - str - user entered string
+            - None - entry was cancelled
+        """
+        text = 'Please enter a string value\n' \
+               'User <Enter> key to accept the value\n' \
+               'Use <Esc> key to cancel entry\n'
+        d = EntryDialog(self.root, text, title, ident=ident, value=value)
+        self.root.wait_window(d.dlg)
+        self.list.focus_set()
+        return d.value
+
+    def ask_for_int(self, ident=None, title='Enter integer value', value=None, ranges=()):
+        """
+        Raises dialog with text entry widget and asks user to enter decimal number
+        Ranges should be iterable of tuples (start, end),
+        where 'start' and 'end' specify allowed value range (inclusively)
+
+        Return:
+            - int - when valid number that falls within any one of specified ranges is entered
+            - None - invalid number or entry was cancelled
+        """
+        text = 'Please enter a decimal value. Fractions will not be accepted\n' \
+               'User <Enter> key to accept the value\n' \
+               'Use <Esc> key to cancel entry\n'
+        d = EntryDialog(self.root, text, title, ident=ident, value=value)
+        self.root.wait_window(d.dlg)
+        self.list.focus_set()
+        ivalue = None
+        if d.value:
+            try:
+                ivalue = int(d.value)
+            except ValueError:
+                messagebox.showerror('Bad value', 'Entered value \'{}\' is not an integer'.format(d.value))
+            if ivalue is not None and ranges:
+                allowed = False
+                for start, end in ranges:
+                    allowed = allowed or start <= ivalue and ivalue <= end
+                if not allowed:
+                    messagebox.showerror(
+                        'Bad value',
+                        'Entered value \'{:d}\' is out of range\n'
+                        'Allowed:\n{}'.format(
+                            ivalue,
+                            '\n'.join(['  {:d} - {:d}'.format(s,e) for s,e in ranges])
+                        )
+                    )
+                    ivalue = None
+        return ivalue
+
+    def ask_for_hex(self, ident=None, title='Enter hexadecimal value', value=None, ranges=()):
+        """
+        Raises dialog with text entry widget and asks user to enter decimal number
+        Ranges should be iterable of tuples (start, end),
+        where 'start' and 'end' specify allowed value range (inclusively)
+
+        Return:
+            - int - when valid number that falls within any one of specified ranges is entered
+            - None - invalid number or entry was cancelled
+        """
+        text = 'Please enter a hexadecimal value\n' \
+               'User <Enter> key to accept the value\n' \
+               'Use <Esc> key to cancel entry\n'
+        d = EntryDialog(self.root, text, title, ident=ident, value=value)
+        self.root.wait_window(d.dlg)
+        self.list.focus_set()
+        hvalue = None
+        if d.value:
+            try:
+                hvalue = int(d.value, base=16)
+            except ValueError:
+                messagebox.showerror('Bad value', 'Entered value \'{}\' is not a hexadecimal value'.format(d.value))
+            if hvalue is not None and ranges:
+                allowed = False
+                for start, end in ranges:
+                    allowed = allowed or start <= hvalue and hvalue <= end
+                if not allowed:
+                    messagebox.showerror(
+                        'Bad value',
+                        'Entered value \'0x{:x}\' is out of range\n'
+                        'Allowed:\n{}'.format(
+                            hvalue,
+                            '\n'.join(['  0x{:x} - 0x{:x}'.format(s,e) for s,e in ranges])
+                        )
+                    )
+                    hvalue = None
+        return hvalue
+
+    def show_text(self, text, title='Info'):
+        """
+        Raises dialog with read-only text view that contains supplied text
+        """
+        d = TextDialog(self.root, text, title)
+        self.root.wait_window(d.dlg)
+        self.list.focus_set()
+
+    def mark_as_changed(self):
+        """
+        Marks current config as having unsaved changes
+        Should be called whenever config value is changed
+        """
+        self.unsaved_changes = True
+        self.update_status()
+
+    def set_status_string(self, status):
+        """
+        Sets status string displayed at the bottom of the window
+        """
+        self.status_string = status
+        self.update_status()
+
+    def update_status(self):
+        """
+        Updates status bar display
+        Status bar displays:
+        - unsaved status
+        - current config path
+        - status string (see set_status_string())
+        """
+        self.tk_status.set('{} [{}] {}'.format(
+            '<UNSAVED>' if self.unsaved_changes else '',
+            self.config_path if self.config_path else '',
+            self.status_string
+        ))
+
+    def prevent_losing_changes(self):
+        """
+        Checks if there are unsaved changes and asks user to save or discard them
+        This routine should be called whenever current config is going to be discarded
+
+        Raises the usual 'Yes', 'No', 'Cancel' prompt.
+
+        Return:
+            - True: caller may safely drop current config state
+            - False: user needs to continue work on current config ('Cancel' pressed or saving failed)
+        """
+        if not self.unsaved_changes:
+            return True
+        res = messagebox.askyesnocancel(
+            parent=self.root,
+            title='Unsaved changes',
+            message='Config has unsaved changes. Do you want to save them?'
+        )
+        if res is None:
+            return False
+        elif res is False:
+            return True
+        # Otherwise attempt to save config and succeed only if config has been saved successfully
+        saved = self.save_config()
+        return saved
+
+    def open_config(self, path=None):
+        if path is None:
+            # Create open dialog. Either existing file is selected or no file is selected as a result
+            path = filedialog.askopenfilename(
+                parent=self.root,
+                title='Open config..',
+                initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
+                filetypes=(('.config files', '*.config'), ('All files', '*.*'))
+            )
+            if not path or not os.path.isfile(path):
+                return False
+        path = os.path.abspath(path)
+        print('Loading config: \'{}\''.format(path))
+        # Try to open given path
+        # If path does not exist, we still set current config path to it but don't load anything
+        self.unsaved_changes = False
+        self.config_path = path
+        if not os.path.exists(path):
+            self.set_status_string('New config')
+            self.mark_as_changed()
+            return True
+        # Load config and set status accordingly
+        try:
+            self.kconfig.load_config(path)
+        except IOError as e:
+            self.set_status_string('Failed to load: \'{}\''.format(path))
+            self.refresh_display()
+            print('Failed to load config \'{}\': {}'.format(path, e))
+            return False
+        self.set_status_string('Opened config')
+        self.refresh_display()
+        return True
+
+    def save_config(self, force_file_dialog=False):
+        path = self.config_path
+        if path is None or force_file_dialog:
+            path = filedialog.asksaveasfilename(
+                parent=self.root,
+                title='Save config as..',
+                initialdir=os.path.dirname(self.config_path) if self.config_path else os.getcwd(),
+                initialfile=os.path.basename(self.config_path) if self.config_path else None,
+                defaultextension='.config',
+                filetypes=(('.config files', '*.config'), ('All files', '*.*'))
+            )
+        if not path:
+            return False
+        path = os.path.abspath(path)
+        print('Saving config: \'{}\''.format(path))
+        # Try to save config to selected path
+        try:
+            self.kconfig.write_config(path)
+            self.unsaved_changes = False
+            self.config_path = path
+            self.set_status_string('Saved config')
+        except IOError as e:
+            self.set_status_string('Failed to save: \'{}\''.format(path))
+            print('Save failed: {}'.format(e), file=sys.stderr)
+            return False
+        return True
+
+
+def _center_window(root, window):
+    # type: (tk.Tk, tk.Toplevel) -> None
+    """
+    Attempts to center window on screen
+    """
+    root.update_idletasks()
+    # root.eval('tk::PlaceWindow {!s} center'.format(
+    #     window.winfo_pathname(window.winfo_id())
+    # ))
+    w = window.winfo_width()
+    h = window.winfo_height()
+    ws = window.winfo_screenwidth()
+    hs = window.winfo_screenheight()
+    x = (ws / 2) - (w / 2)
+    y = (hs / 2) - (h / 2)
+    window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
+    window.lift()
+    window.focus_force()
+
+
+def _center_window_above_parent(root, window):
+    # type: (tk.Tk, tk.Toplevel) -> None
+    """
+    Attempts to center window above its parent window
+    """
+    # root.eval('tk::PlaceWindow {!s} center'.format(
+    #     window.winfo_pathname(window.winfo_id())
+    # ))
+    root.update_idletasks()
+    parent = window.master
+    w = window.winfo_width()
+    h = window.winfo_height()
+    px = parent.winfo_rootx()
+    py = parent.winfo_rooty()
+    pw = parent.winfo_width()
+    ph = parent.winfo_height()
+    x = px + (pw / 2) - (w / 2)
+    y = py + (ph / 2) - (h / 2)
+    window.geometry('+{:d}+{:d}'.format(int(x), int(y)))
+    window.lift()
+    window.focus_force()
+
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv[1:]
+    # Instantiate cmd options parser
+    parser = argparse.ArgumentParser(
+        description='Interactive Kconfig configuration editor'
+    )
+    parser.add_argument(
+        '--kconfig',
+        metavar='FILE',
+        type=str,
+        default='Kconfig',
+        help='path to root Kconfig file'
+    )
+    parser.add_argument(
+        '--config',
+        metavar='FILE',
+        type=str,
+        help='path to .config file to load'
+    )
+    args = parser.parse_args(argv)
+    kconfig_path = args.kconfig
+    config_path = args.config
+    # Verify that Kconfig file exists
+    if not os.path.isfile(kconfig_path):
+        raise RuntimeError('\'{}\': no such file'.format(kconfig_path))
+    # Parse Kconfig files
+    kconf = kconfiglib.Kconfig(filename=kconfig_path)
+    mc = MenuConfig(kconf)
+    # If config file was specified, load it
+    if config_path:
+        mc.open_config(config_path)
+    tk.mainloop()
+
+
+if __name__ == '__main__':
+    main()