[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [Minios-devel] [UNIKRAFT PATCH v3 14/23] kconfig: Import kconfiglib
Import Kconfiglib for providing python based front end to the kconfig. URL: https://github.com/ulfalizer/Kconfiglib TAG: v12.13.0 Signed-off-by: Sharan Santhanam <sharan.santhanam@xxxxxxxxx> --- support/kconfiglib/LICENSE.txt | 5 + support/kconfiglib/MANIFEST.in | 2 + support/kconfiglib/README.rst | 806 +++ support/kconfiglib/alldefconfig.py | 27 + support/kconfiglib/allmodconfig.py | 46 + support/kconfiglib/allnoconfig.py | 45 + support/kconfiglib/allyesconfig.py | 56 + support/kconfiglib/defconfig.py | 43 + support/kconfiglib/examples/Kmenuconfig | 102 + support/kconfiglib/examples/allnoconfig_walk.py | 66 + support/kconfiglib/examples/defconfig_oldconfig.py | 39 + support/kconfiglib/examples/dumpvars.py | 15 + support/kconfiglib/examples/eval_expr.py | 24 + support/kconfiglib/examples/find_symbol.py | 112 + support/kconfiglib/examples/help_grep.py | 64 + support/kconfiglib/examples/kconfiglib.py | 1 + support/kconfiglib/examples/list_undefined.py | 156 + support/kconfiglib/examples/menuconfig_example.py | 341 + support/kconfiglib/examples/merge_config.py | 133 + support/kconfiglib/examples/print_config_tree.py | 199 + support/kconfiglib/examples/print_sym_info.py | 54 + support/kconfiglib/examples/print_tree.py | 75 + support/kconfiglib/genconfig.py | 132 + support/kconfiglib/guiconfig.py | 2316 +++++++ support/kconfiglib/kconfiglib.py | 7018 ++++++++++++++++++++ support/kconfiglib/listnewconfig.py | 47 + support/kconfiglib/makefile.patch | 48 + support/kconfiglib/menuconfig.py | 3264 +++++++++ support/kconfiglib/oldconfig.py | 246 + support/kconfiglib/olddefconfig.py | 28 + support/kconfiglib/savedefconfig.py | 49 + support/kconfiglib/setconfig.py | 92 + support/kconfiglib/setup.cfg | 7 + support/kconfiglib/setup.py | 100 + support/kconfiglib/tests/Kappend | 12 + support/kconfiglib/tests/Kassignable | 230 + support/kconfiglib/tests/Kchoice | 198 + support/kconfiglib/tests/Kdefconfig_existent | 8 + support/kconfiglib/tests/Kdefconfig_existent_but_n | 10 + support/kconfiglib/tests/Kdefconfig_nonexistent | 5 + support/kconfiglib/tests/Kdefconfig_srctree | 5 + support/kconfiglib/tests/Kdepcopy | 173 + support/kconfiglib/tests/Kdeploop0 | 3 + support/kconfiglib/tests/Kdeploop1 | 3 + support/kconfiglib/tests/Kdeploop10 | 48 + support/kconfiglib/tests/Kdeploop2 | 3 + support/kconfiglib/tests/Kdeploop3 | 3 + support/kconfiglib/tests/Kdeploop4 | 7 + support/kconfiglib/tests/Kdeploop5 | 7 + support/kconfiglib/tests/Kdeploop6 | 6 + support/kconfiglib/tests/Kdeploop7 | 11 + support/kconfiglib/tests/Kdeploop8 | 8 + support/kconfiglib/tests/Kdeploop9 | 7 + support/kconfiglib/tests/Kdirdep | 30 + support/kconfiglib/tests/Kescape | 3 + support/kconfiglib/tests/Keval | 34 + support/kconfiglib/tests/Kexpr_items | 11 + support/kconfiglib/tests/Khelp | 50 + support/kconfiglib/tests/Kifremoval | 99 + support/kconfiglib/tests/Kimply | 145 + support/kconfiglib/tests/Kinclude_path | 12 + support/kconfiglib/tests/Kinclude_path_sourced_1 | 12 + support/kconfiglib/tests/Kinclude_path_sourced_2 | 11 + support/kconfiglib/tests/Kitemlists | 36 + support/kconfiglib/tests/Klocation | 72 + support/kconfiglib/tests/Klocation_sourced | 18 + support/kconfiglib/tests/Kmainmenu | 5 + support/kconfiglib/tests/Kmenuconfig | 37 + support/kconfiglib/tests/Kmisc | 82 + support/kconfiglib/tests/Kmissingrsource | 1 + support/kconfiglib/tests/Kmissingsource | 1 + support/kconfiglib/tests/Korder | 35 + support/kconfiglib/tests/Kpreprocess | 147 + support/kconfiglib/tests/Krange | 133 + support/kconfiglib/tests/Krecursive1 | 1 + support/kconfiglib/tests/Krecursive2 | 1 + support/kconfiglib/tests/Kreferenced | 63 + support/kconfiglib/tests/Krelation | 36 + support/kconfiglib/tests/Krepr | 64 + support/kconfiglib/tests/Kstr | 293 + support/kconfiglib/tests/Kundef | 23 + support/kconfiglib/tests/Kuserfunctions | 11 + support/kconfiglib/tests/Kvisibility | 342 + support/kconfiglib/tests/config_indented | 3 + support/kconfiglib/tests/config_set_bool | 1 + support/kconfiglib/tests/config_set_string | 1 + support/kconfiglib/tests/defconfig_1 | 0 support/kconfiglib/tests/defconfig_2 | 0 support/kconfiglib/tests/empty | 0 support/kconfiglib/tests/kconfigfunctions.py | 14 + support/kconfiglib/tests/reltest | 51 + support/kconfiglib/tests/sub/Kconfig_symlink_2 | 1 + support/kconfiglib/tests/sub/Kconfig_symlink_3 | 2 + support/kconfiglib/tests/sub/Klocation_grsourced1 | 1 + support/kconfiglib/tests/sub/Klocation_grsourced2 | 1 + support/kconfiglib/tests/sub/Klocation_gsourced1 | 1 + support/kconfiglib/tests/sub/Klocation_gsourced2 | 1 + support/kconfiglib/tests/sub/Klocation_rsourced | 2 + support/kconfiglib/tests/sub/defconfig_in_sub | 0 support/kconfiglib/tests/sub/sub/Kconfig_symlink_1 | 2 + support/kconfiglib/tests/symlink | 1 + support/kconfiglib/testsuite.py | 3136 +++++++++ 102 files changed, 21531 insertions(+) create mode 100644 support/kconfiglib/LICENSE.txt create mode 100644 support/kconfiglib/MANIFEST.in create mode 100644 support/kconfiglib/README.rst create mode 100755 support/kconfiglib/alldefconfig.py create mode 100755 support/kconfiglib/allmodconfig.py create mode 100755 support/kconfiglib/allnoconfig.py create mode 100755 support/kconfiglib/allyesconfig.py create mode 100755 support/kconfiglib/defconfig.py create mode 100644 support/kconfiglib/examples/Kmenuconfig create mode 100644 support/kconfiglib/examples/allnoconfig_walk.py create mode 100644 support/kconfiglib/examples/defconfig_oldconfig.py create mode 100644 support/kconfiglib/examples/dumpvars.py create mode 100644 support/kconfiglib/examples/eval_expr.py create mode 100644 support/kconfiglib/examples/find_symbol.py create mode 100644 support/kconfiglib/examples/help_grep.py create mode 120000 support/kconfiglib/examples/kconfiglib.py create mode 100644 support/kconfiglib/examples/list_undefined.py create mode 100755 support/kconfiglib/examples/menuconfig_example.py create mode 100755 support/kconfiglib/examples/merge_config.py create mode 100644 support/kconfiglib/examples/print_config_tree.py create mode 100644 support/kconfiglib/examples/print_sym_info.py create mode 100644 support/kconfiglib/examples/print_tree.py create mode 100755 support/kconfiglib/genconfig.py create mode 100755 support/kconfiglib/guiconfig.py create mode 100644 support/kconfiglib/kconfiglib.py create mode 100755 support/kconfiglib/listnewconfig.py create mode 100644 support/kconfiglib/makefile.patch create mode 100755 support/kconfiglib/menuconfig.py create mode 100755 support/kconfiglib/oldconfig.py create mode 100755 support/kconfiglib/olddefconfig.py create mode 100755 support/kconfiglib/savedefconfig.py create mode 100755 support/kconfiglib/setconfig.py create mode 100644 support/kconfiglib/setup.cfg create mode 100644 support/kconfiglib/setup.py create mode 100644 support/kconfiglib/tests/Kappend create mode 100644 support/kconfiglib/tests/Kassignable create mode 100644 support/kconfiglib/tests/Kchoice create mode 100644 support/kconfiglib/tests/Kdefconfig_existent create mode 100644 support/kconfiglib/tests/Kdefconfig_existent_but_n create mode 100644 support/kconfiglib/tests/Kdefconfig_nonexistent create mode 100644 support/kconfiglib/tests/Kdefconfig_srctree create mode 100644 support/kconfiglib/tests/Kdepcopy create mode 100644 support/kconfiglib/tests/Kdeploop0 create mode 100644 support/kconfiglib/tests/Kdeploop1 create mode 100644 support/kconfiglib/tests/Kdeploop10 create mode 100644 support/kconfiglib/tests/Kdeploop2 create mode 100644 support/kconfiglib/tests/Kdeploop3 create mode 100644 support/kconfiglib/tests/Kdeploop4 create mode 100644 support/kconfiglib/tests/Kdeploop5 create mode 100644 support/kconfiglib/tests/Kdeploop6 create mode 100644 support/kconfiglib/tests/Kdeploop7 create mode 100644 support/kconfiglib/tests/Kdeploop8 create mode 100644 support/kconfiglib/tests/Kdeploop9 create mode 100644 support/kconfiglib/tests/Kdirdep create mode 100644 support/kconfiglib/tests/Kescape create mode 100644 support/kconfiglib/tests/Keval create mode 100644 support/kconfiglib/tests/Kexpr_items create mode 100644 support/kconfiglib/tests/Khelp create mode 100644 support/kconfiglib/tests/Kifremoval create mode 100644 support/kconfiglib/tests/Kimply create mode 100644 support/kconfiglib/tests/Kinclude_path create mode 100644 support/kconfiglib/tests/Kinclude_path_sourced_1 create mode 100644 support/kconfiglib/tests/Kinclude_path_sourced_2 create mode 100644 support/kconfiglib/tests/Kitemlists create mode 100644 support/kconfiglib/tests/Klocation create mode 100644 support/kconfiglib/tests/Klocation_sourced create mode 100644 support/kconfiglib/tests/Kmainmenu create mode 100644 support/kconfiglib/tests/Kmenuconfig create mode 100644 support/kconfiglib/tests/Kmisc create mode 100644 support/kconfiglib/tests/Kmissingrsource create mode 100644 support/kconfiglib/tests/Kmissingsource create mode 100644 support/kconfiglib/tests/Korder create mode 100644 support/kconfiglib/tests/Kpreprocess create mode 100644 support/kconfiglib/tests/Krange create mode 100644 support/kconfiglib/tests/Krecursive1 create mode 100644 support/kconfiglib/tests/Krecursive2 create mode 100644 support/kconfiglib/tests/Kreferenced create mode 100644 support/kconfiglib/tests/Krelation create mode 100644 support/kconfiglib/tests/Krepr create mode 100644 support/kconfiglib/tests/Kstr create mode 100644 support/kconfiglib/tests/Kundef create mode 100644 support/kconfiglib/tests/Kuserfunctions create mode 100644 support/kconfiglib/tests/Kvisibility create mode 100644 support/kconfiglib/tests/config_indented create mode 100644 support/kconfiglib/tests/config_set_bool create mode 100644 support/kconfiglib/tests/config_set_string create mode 100644 support/kconfiglib/tests/defconfig_1 create mode 100644 support/kconfiglib/tests/defconfig_2 create mode 100644 support/kconfiglib/tests/empty create mode 100644 support/kconfiglib/tests/kconfigfunctions.py create mode 100755 support/kconfiglib/tests/reltest create mode 100644 support/kconfiglib/tests/sub/Kconfig_symlink_2 create mode 100644 support/kconfiglib/tests/sub/Kconfig_symlink_3 create mode 100644 support/kconfiglib/tests/sub/Klocation_grsourced1 create mode 100644 support/kconfiglib/tests/sub/Klocation_grsourced2 create mode 100644 support/kconfiglib/tests/sub/Klocation_gsourced1 create mode 100644 support/kconfiglib/tests/sub/Klocation_gsourced2 create mode 100644 support/kconfiglib/tests/sub/Klocation_rsourced create mode 100644 support/kconfiglib/tests/sub/defconfig_in_sub create mode 100644 support/kconfiglib/tests/sub/sub/Kconfig_symlink_1 create mode 120000 support/kconfiglib/tests/symlink create mode 100644 support/kconfiglib/testsuite.py diff --git a/support/kconfiglib/LICENSE.txt b/support/kconfiglib/LICENSE.txt new file mode 100644 index 0000000..8b31efc --- /dev/null +++ b/support/kconfiglib/LICENSE.txt @@ -0,0 +1,5 @@ +Copyright (c) 2011-2019, Ulf Magnusson <ulfalizer@xxxxxxxxx> + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/support/kconfiglib/MANIFEST.in b/support/kconfiglib/MANIFEST.in new file mode 100644 index 0000000..881a794 --- /dev/null +++ b/support/kconfiglib/MANIFEST.in @@ -0,0 +1,2 @@ +# Include the license file in source distributions +include LICENSE.txt diff --git a/support/kconfiglib/README.rst b/support/kconfiglib/README.rst new file mode 100644 index 0000000..bdbd457 --- /dev/null +++ b/support/kconfiglib/README.rst @@ -0,0 +1,806 @@ +.. contents:: Table of contents + :backlinks: none + +Overview +-------- + +Kconfiglib is a `Kconfig +<https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-language.rst>`__ +implementation in Python 2/3. It started out as a helper library, but now has a +enough functionality to also work well as a standalone Kconfig implementation +(including `terminal and GUI menuconfig interfaces <Menuconfig interfaces_>`_ +and `Kconfig extensions`_). + +The entire library is contained in `kconfiglib.py +<https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_. The +bundled scripts are implemented on top of it. Implementing your own scripts +should be relatively easy, if needed. + +Kconfiglib is used exclusively by e.g. the `Zephyr <https://www.zephyrproject.org/>`_ +and `ACRN <https://projectacrn.org/>`_ projects. It is also used for many small helper +scripts in various projects. + +Since Kconfiglib is based around a library, it can be used e.g. to generate a +`Kconfig cross-reference <https://docs.zephyrproject.org/latest/reference/kconfig/index.html>`_ (note: heavy page), +using the same robust Kconfig parser used for other Kconfig tools, instead of brittle ad-hoc parsing. The documentation generation script can be found `here <https://github.com/zephyrproject-rtos/zephyr/blob/master/doc/scripts/genrest.py>`__. + +Kconfiglib implements the recently added `Kconfig preprocessor +<https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-macro-language.rst>`__. +For backwards compatibility, environment variables can be referenced both as +``$(FOO)`` (the new syntax) and as ``$FOO`` (the old syntax). The old syntax is +deprecated, but will probably be supported for a long time, as its needed to +stay compatible with older Linux kernels. The major version will be increased +if support is ever dropped. Using the old syntax with an undefined environment +variable keeps the string as is. + +Note: See `this issue <https://github.com/ulfalizer/Kconfiglib/issues/47>`_ if you run into +a "macro expanded to blank string" error with kernel 4.18+. + +See `this page +<https://docs.zephyrproject.org/latest/guides/kconfig/index.html>`__ for some +Kconfig tips and best practices. + +Installation +------------ + +Installation with pip +~~~~~~~~~~~~~~~~~~~~~ + +Kconfiglib is available on `PyPI <https://pypi.python.org/pypi/kconfiglib/>`_ and can be +installed with e.g. + +.. code:: + + $ pip(3) install kconfiglib + +Microsoft Windows is supported. + +The ``pip`` installation will give you both the base library and the following +executables. All but two (``genconfig`` and ``setconfig``) mirror functionality +available in the C tools. + +- `menuconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_ + +- `guiconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/guiconfig.py>`_ + +- `oldconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/oldconfig.py>`_ + +- `olddefconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/olddefconfig.py>`_ + +- `savedefconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/savedefconfig.py>`_ + +- `defconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/defconfig.py>`_ + +- `alldefconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/alldefconfig.py>`_ + +- `allnoconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/allnoconfig.py>`_ + +- `allmodconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/allmodconfig.py>`_ + +- `allyesconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/allyesconfig.py>`_ + +- `listnewconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/listnewconfig.py>`_ + +- `genconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/genconfig.py>`_ + +- `setconfig <https://github.com/ulfalizer/Kconfiglib/blob/master/setconfig.py>`_ + +``genconfig`` is intended to be run at build time. It generates a C header from +the configuration and (optionally) information that can be used to rebuild only +files that reference Kconfig symbols that have changed value. + +Starting with Kconfiglib version 12.2.0, all utilities are compatible with both +Python 2 and Python 3. Previously, ``menuconfig.py`` only ran under Python 3 +(i.e., it's now more backwards compatible than before). + +**Note:** If you install Kconfiglib with ``pip``'s ``--user`` flag, make sure +that your ``PATH`` includes the directory where the executables end up. You can +list the installed files with ``pip(3) show -f kconfiglib``. + +All releases have a corresponding tag in the git repository, e.g. ``v12.13.0`` +(the latest version). + +`Semantic versioning <http://semver.org/>`_ is used. There's been +ten small changes (`1 <https://github.com/ulfalizer/Kconfiglib/commit/e8b4ecb6ff6ccc1c7be0818314fbccda2ef2b2ee>`_, +`2 <https://github.com/ulfalizer/Kconfiglib/commit/db633015a4d7b0ba1e882f665e191f350932b2af>`_, +`3 <https://github.com/ulfalizer/Kconfiglib/commit/8983f7eb297dd614faf0beee3129559bc8ba338e>`_, +`4 <https://github.com/ulfalizer/Kconfiglib/commit/cbf32e29a130d22bc734b7778e6304ac9df2a3e8>`_, +`5 <https://github.com/ulfalizer/Kconfiglib/commit/eb6c21a9b33a2d6e2bed9882d4f930d0cab2f03b>`_, +`6 <https://github.com/ulfalizer/Kconfiglib/commit/c19fc11355b13d75d97286402c7a933fb23d3b70>`_, +`7 <https://github.com/ulfalizer/Kconfiglib/commit/7a428aa415606820a44291f475248b08e3952c4b>`_, +`8 <https://github.com/ulfalizer/Kconfiglib/commit/f247ddf618ad29718e5efd3e69f8baf75d4d347b>`_, +`9 <https://github.com/ulfalizer/Kconfiglib/commit/4fed39d9271ceb68be4157ab3f96a45b94f77dc0>`_, +`10 <https://github.com/ulfalizer/Kconfiglib/commit/55bc8c380869ea663092212e8fe388ad7abae596>`_) +to the behavior of the API, which is why the major version is at 12 rather than +2. I do major version bumps for all behavior changes, even tiny ones, and most of these were +fixes for baby issues in the early days of the Kconfiglib 2 API. + +Manual installation +~~~~~~~~~~~~~~~~~~~ + +Just drop ``kconfiglib.py`` and the scripts you want somewhere. There are no +third-party dependencies (except for the `windows-curses +<https://github.com/zephyrproject-rtos/windows-curses>`_ package on Windows, +when running the terminal ``menuconfig`` implementation). + +Installation for the Linux kernel +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +See the module docstring at the top of `kconfiglib.py <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_. + +Python version compatibility (2.7/3.2+) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Kconfiglib and all utilities run under both Python 2.7 and Python 3.2 and +later. The code mostly uses basic Python features and has no third-party +dependencies, so keeping it backwards-compatible is pretty low effort. + +The 3.2 requirement comes from ``argparse``. ``format()`` with unnumbered +``{}`` is used as well. + +A recent Python 3 version is recommended if you have a choice, as it'll give +you better Unicode handling. + +Getting started +--------------- + +1. `Install <Installation_>`_ the library and the utilities. + +2. Write `Kconfig + <https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt>`__ + files that describe the available configuration options. + +3. Generate an initial configuration with e.g. ``menuconfig``/``guiconfig`` or + ``alldefconfig``. The configuration is saved as ``.config`` by default. + + For more advanced projects, the ``defconfig`` utility can be used to + generate the initial configuration from an existing configuration file. + Usually, this existing configuration file would be a minimal configuration + file, as generated by e.g. ``savedefconfig``. + +4. Run ``genconfig`` to generate a header file. By default, it is saved as + ``config.h``. + + Normally, ``genconfig`` would be run automatically as part of the build. + + Before writing a header file or other configuration output, Kconfiglib + compares the old contents of the file against the new contents. If there's + no change, the write is skipped. This avoids updating file metadata like the + modification time, and might save work depending on your build setup. + + Adding new configuration output formats should be relatively straightforward. + See the implementation of ``write_config()`` in `kconfiglib.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_. + The documentation for the ``Symbol.config_string`` property has some tips as + well. + +5. To update an old ``.config`` file after the Kconfig files have changed (e.g. + to add new options), run ``oldconfig`` (prompts for values for new options) + or ``olddefconfig`` (gives new options their default value). Entering the + ``menuconfig`` or ``guiconfig`` interface and saving the configuration will + also update it (the configuration interfaces always prompt for saving + on exit if it would modify the contents of the ``.config`` file). + + Due to Kconfig semantics, simply loading an old ``.config`` file performs an + implicit ``olddefconfig``, so building will normally not be affected by + having an outdated configuration. + +Whenever ``.config`` is overwritten, the previous version of the file is saved +to ``.config.old`` (or, more generally, to ``$KCONFIG_CONFIG.old``). + +For some general Kconfig advice, see `this page +<https://docs.zephyrproject.org/latest/guides/kconfig/index.html>`__. + +Using ``.config`` files as Make input +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``.config`` files use Make syntax and can be included directly in Makefiles to +read configuration values from there. This is why ``n``-valued +``bool``/``tristate`` values are written out as ``# CONFIG_FOO is not set`` (a +Make comment) in ``.config``, allowing them to be tested with ``ifdef`` in +Make. + +If you make use of this, you might want to pass ``--config-out <filename>`` to +``genconfig`` and include the configuration file it generates instead of +including ``.config`` directly. This has the advantage that the generated +configuration file will always be a "full" configuration file, even if +``.config`` is outdated. Otherwise, it might be necessary to run +``old(def)config`` or ``menuconfig``/``guiconfig`` before rebuilding with an +outdated ``.config``. + +If you use ``--sync-deps`` to generate incremental build information, you can +include ``deps/auto.conf`` instead, which is also a full configuration file. + +Useful helper macros +~~~~~~~~~~~~~~~~~~~~ + +The `include/linux/kconfig.h +<https://github.com/torvalds/linux/blob/master/include/linux/kconfig.h>`_ +header in the Linux kernel defines some useful helper macros for testing +Kconfig configuration values. + +``IS_ENABLED()`` is generally useful, allowing configuration values to be +tested in ``if`` statements with no runtime overhead. + +Incremental building +~~~~~~~~~~~~~~~~~~~~ + +See the docstring for ``Kconfig.sync_deps()`` in `kconfiglib.py +<https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_ for hints +on implementing incremental builds (rebuilding just source files that reference +changed configuration values). + +Running the ``scripts/basic/fixdep.c`` tool from the kernel on the output of +``gcc -MD <source file>`` might give you an idea of how it all fits together. + +Library documentation +--------------------- + +Kconfiglib comes with extensive documentation in the form of docstrings. To view it, run e.g. +the following command: + +.. code:: sh + + $ pydoc(3) kconfiglib + +For HTML output, add ``-w``: + +.. code:: sh + + $ pydoc(3) -w kconfiglib + +This will also work after installing Kconfiglib with ``pip(3)``. + +Documentation for the ``menuconfig`` and ``guiconfig`` interfaces can be viewed +in the same way: + +.. code:: sh + + $ pydoc3 menuconfig/guiconfig + +A good starting point for learning the library is to read the module docstring +(which you could also just read directly at the beginning of `kconfiglib.py +<https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_). It +gives an introduction to symbol values, the menu tree, and expressions. + +After reading the module docstring, a good next step is to read the ``Kconfig`` +class documentation, and then the documentation for the ``Symbol``, ``Choice``, +and ``MenuNode`` classes. + +Please tell me if something is unclear or can be explained better. + +Library features +---------------- + +Kconfiglib can do the following, among other things: + +- **Programmatically get and set symbol values** + + See `allnoconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/allnoconfig.py>`_ and + `allyesconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/allyesconfig.py>`_, + which are automatically verified to produce identical output to the standard + ``make allnoconfig`` and ``make allyesconfig``. + +- **Read and write .config and defconfig files** + + The generated ``.config`` and ``defconfig`` (minimal configuration) files are + character-for-character identical to what the C implementation would generate + (except for the header comment). The test suite relies on this, as it + compares the generated files. + +- **Write C headers** + + The generated headers use the same format as ``include/generated/autoconf.h`` + from the Linux kernel. + +- **Implement incremental builds** + + This uses the same scheme as the ``include/config`` directory in the kernel: + Symbols are translated into files that are touched when the symbol's value + changes between builds, which can be used to avoid having to do a full + rebuild whenever the configuration is changed. + + See the ``sync_deps()`` function for more information. + +- **Inspect symbols** + + Printing a symbol or other item (which calls ``__str__()``) returns its + definition in Kconfig format. This also works for symbols defined in multiple + locations. + + A helpful ``__repr__()`` is on all objects too. + + All ``__str__()`` and ``__repr__()`` methods are deliberately implemented + with just public APIs, so all symbol information can be fetched separately as + well. + +- **Inspect expressions** + + Expressions use a simple tuple-based format that can be processed manually + if needed. Expression printing and evaluation functions are provided, + implemented with public APIs. + +- **Inspect the menu tree** + + The underlying menu tree is exposed, including submenus created implicitly + from symbols depending on preceding symbols. This can be used e.g. to + implement menuconfig-like functionality. + + See `menuconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_/`guiconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/guiconfig.py>`_ and the + minimalistic `menuconfig_example.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/menuconfig_example.py>`_ + example. + +Kconfig extensions +~~~~~~~~~~~~~~~~~~ + +The following Kconfig extensions are available: + +- ``source`` supports glob patterns and includes each matching file. A pattern + is required to match at least one file. + + A separate ``osource`` statement is available for cases where it's okay for + the pattern to match no files (in which case ``osource`` turns into a no-op). + +- A relative ``source`` statement (``rsource``) is available, where file paths + are specified relative to the directory of the current Kconfig file. An + ``orsource`` statement is available as well, analogous to ``osource``. + +- Preprocessor user functions can be defined in Python, which makes it simple + to integrate information from existing Python tools into Kconfig (e.g. to + have Kconfig symbols depend on hardware information stored in some other + format). + + See the *Kconfig extensions* section in the + `kconfiglib.py <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_ + module docstring for more information. + +- ``def_int``, ``def_hex``, and ``def_string`` are available in addition to + ``def_bool`` and ``def_tristate``, allowing ``int``, ``hex``, and ``string`` + symbols to be given a type and a default at the same time. + + These can be useful in projects that make use of symbols defined in multiple + locations, and remove some Kconfig inconsistency. + +- Environment variables are expanded directly in e.g. ``source`` and + ``mainmenu`` statements, meaning ``option env`` symbols are redundant. + + This is the standard behavior with the new `Kconfig preprocessor + <https://github.com/torvalds/linux/blob/master/Documentation/kbuild/kconfig-macro-language.txt>`__, + which Kconfiglib implements. + + ``option env`` symbols are accepted but ignored, which leads the caveat that + they must have the same name as the environment variables they reference + (Kconfiglib warns if the names differ). This keeps Kconfiglib compatible with + older Linux kernels, where the name of the ``option env`` symbol always + matched the environment variable. Compatibility with older Linux kernels is + the main reason ``option env`` is still supported. + + The C tools have dropped support for ``option env``. + +- Two extra optional warnings can be enabled by setting environment variables, + covering cases that are easily missed when making changes to Kconfig files: + + * ``KCONFIG_WARN_UNDEF``: If set to ``y``, warnings will be generated for all + references to undefined symbols within Kconfig files. The only gotcha is + that all hex literals must be prefixed with ``0x`` or ``0X``, to make it + possible to distinguish them from symbol references. + + Some projects (e.g. the Linux kernel) use multiple Kconfig trees with many + shared Kconfig files, leading to some safe undefined symbol references. + ``KCONFIG_WARN_UNDEF`` is useful in projects that only have a single + Kconfig tree though. + + ``KCONFIG_STRICT`` is an older alias for this environment variable, + supported for backwards compatibility. + + * ``KCONFIG_WARN_UNDEF_ASSIGN``: If set to ``y``, warnings will be generated + for all assignments to undefined symbols within ``.config`` files. By + default, no such warnings are generated. + + This warning can also be enabled/disabled by setting + ``Kconfig.warn_assign_undef`` to ``True``/``False``. + +Other features +-------------- + +- **Single-file implementation** + + The entire library is contained in `kconfiglib.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/kconfiglib.py>`_. + + The tools implemented on top of it are one file each. + +- **Robust and highly compatible with the C Kconfig tools** + + The `test suite <https://github.com/ulfalizer/Kconfiglib/blob/master/testsuite.py>`_ + automatically compares output from Kconfiglib and the C tools + by diffing the generated ``.config`` files for the real kernel Kconfig and + defconfig files, for all ARCHes. + + This currently involves comparing the output for 36 ARCHes and 498 defconfig + files (or over 18000 ARCH/defconfig combinations in "obsessive" test suite + mode). All tests are expected to pass. + + A comprehensive suite of selftests is included as well. + +- **Not horribly slow despite being a pure Python implementation** + + The `allyesconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/allyesconfig.py>`_ + script currently runs in about 1.3 seconds on the Linux kernel on a Core i7 + 2600K (with a warm file cache), including the ``make`` overhead from ``make + scriptconfig``. Note that the Linux kernel Kconfigs are absolutely massive + (over 14k symbols for x86) compared to most projects, and also have overhead + from running shell commands via the Kconfig preprocessor. + + Kconfiglib is especially speedy in cases where multiple ``.config`` files + need to be processed, because the ``Kconfig`` files will only need to be parsed + once. + + For long-running jobs, `PyPy <https://pypy.org/>`_ gives a big performance + boost. CPython is faster for short-running jobs as PyPy needs some time to + warm up. + + Kconfiglib also works well with the + `multiprocessing <https://docs.python.org/3/library/multiprocessing.html>`_ + module. No global state is kept. + +- **Generates more warnings than the C implementation** + + Generates the same warnings as the C implementation, plus additional ones. + Also detects dependency and ``source`` loops. + + All warnings point out the location(s) in the ``Kconfig`` files where a + symbol is defined, where applicable. + +- **Unicode support** + + Unicode characters in string literals in ``Kconfig`` and ``.config`` files are + correctly handled. This support mostly comes for free from Python. + +- **Windows support** + + Nothing Linux-specific is used. Universal newlines mode is used for both + Python 2 and Python 3. + + The `Zephyr <https://www.zephyrproject.org/>`_ project uses Kconfiglib to + generate ``.config`` files and C headers on Linux as well as Windows. + +- **Internals that (mostly) mirror the C implementation** + + While being simpler to understand and tweak. + +Menuconfig interfaces +--------------------- + +Three configuration interfaces are currently available: + +- `menuconfig.py <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_ + is a terminal-based configuration interface implemented using the standard + Python ``curses`` module. ``xconfig`` features like showing invisible symbols and + showing symbol names are included, and it's possible to jump directly to a symbol + in the menu tree (even if it's currently invisible). + + .. image:: https://raw.githubusercontent.com/ulfalizer/Kconfiglib/screenshots/screenshots/menuconfig.gif + + *There is now also a show-help mode that shows the help text of the currently + selected symbol in the help window at the bottom.* + + Starting with Kconfiglib 12.2.0, ``menuconfig.py`` runs under both Python 2 + and Python 3 (previously, it only ran under Python 3, so this was a + backport). Running it under Python 3 provides better support for Unicode text + entry (``get_wch()`` is not available in the ``curses`` module on Python 2). + + There are no third-party dependencies on \*nix. On Windows, + the ``curses`` modules is not available by default, but support + can be added by installing the ``windows-curses`` package (which is + installed automatically when Kconfiglib is installed via ``pip`` + on Windows): + + .. code-block:: shell + + $ pip install windows-curses + + This uses wheels built from `this repository + <https://github.com/zephyrproject-rtos/windows-curses>`_, which is in turn + based on Christoph Gohlke's `Python Extension Packages for Windows + <https://www.lfd.uci.edu/~gohlke/pythonlibs/#curses>`_. + + See the docstring at the top of `menuconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/menuconfig.py>`_ for + more information about the terminal menuconfig implementation. + +- `guiconfig.py + <https://github.com/ulfalizer/Kconfiglib/blob/master/guiconfig.py>`_ is a + graphical configuration interface written in `Tkinter + <https://docs.python.org/3/library/tkinter.html>`_. Like ``menuconfig.py``, + it supports showing all symbols (with invisible symbols in red) and jumping + directly to symbols. Symbol values can also be changed directly from the + jump-to dialog. + + When single-menu mode is enabled, a single menu is shown at a time, like in + the terminal menuconfig. Only this mode distinguishes between symbols defined + with ``config`` and symbols defined with ``menuconfig``. + + ``guiconfig.py`` has been tested on X11, Windows, and macOS, and is + compatible with both Python 2 and Python 3. + + Despite being part of the Python standard library, ``tkinter`` often isn't + included by default in Python installations on Linux. These commands will + install it on a few different distributions: + + - Ubuntu: ``sudo apt install python-tk``/``sudo apt install python3-tk`` + + - Fedora: ``dnf install python2-tkinter``/``dnf install python3-tkinter`` + + - Arch: ``sudo pacman -S tk`` + + - Clear Linux: ``sudo swupd bundle-add python3-tcl`` + + Screenshot below, with show-all mode enabled and the jump-to dialog open: + + .. image:: https://raw.githubusercontent.com/ulfalizer/Kconfiglib/screenshots/screenshots/guiconfig.png + + To avoid having to carry around a bunch of GIFs, the image data is embedded + in ``guiconfig.py``. To use separate GIF files instead, change + ``_USE_EMBEDDED_IMAGES`` to ``False`` in ``guiconfig.py``. The image files + can be found in the `screenshots + <https://github.com/ulfalizer/Kconfiglib/tree/screenshots/guiconfig>`_ + branch. + + I did my best with the images, but some are definitely only art adjacent. + Touch-ups are welcome. :) + +- `pymenuconfig <https://github.com/RomaVis/pymenuconfig>`_, built by `RomaVis + <https://github.com/RomaVis>`_, is an older portable Python 2/3 TkInter + menuconfig implementation. + + Screenshot below: + + .. image:: https://raw.githubusercontent.com/RomaVis/pymenuconfig/master/screenshot.PNG + + While working on the terminal menuconfig implementation, I added a few APIs + to Kconfiglib that turned out to be handy. ``pymenuconfig`` predates + ``menuconfig.py`` and ``guiconfig.py``, and so didn't have them available. + Blame me for any workarounds. + +Examples +-------- + +Example scripts +~~~~~~~~~~~~~~~ + +The `examples/ <https://github.com/ulfalizer/Kconfiglib/blob/master/examples>`_ directory contains some simple example scripts. Among these are the following ones. Make sure you run them with the latest version of Kconfiglib, as they might make use of newly added features. + +- `eval_expr.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/eval_expr.py>`_ evaluates an expression in the context of a configuration. + +- `find_symbol.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/find_symbol.py>`_ searches through expressions to find references to a symbol, also printing a "backtrace" with parents for each reference found. + +- `help_grep.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/help_grep.py>`_ searches for a string in all help texts. + +- `print_tree.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/print_tree.py>`_ prints a tree of all configuration items. + +- `print_config_tree.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/print_config_tree.py>`_ is similar to ``print_tree.py``, but dumps the tree as it would appear in ``menuconfig``, including values. This can be handy for visually diffing between ``.config`` files and different versions of ``Kconfig`` files. + +- `list_undefined.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/list_undefined.py>`_ finds references to symbols that are not defined by any architecture in the Linux kernel. + +- `merge_config.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/merge_config.py>`_ merges configuration fragments to produce a complete .config, similarly to ``scripts/kconfig/merge_config.sh`` from the kernel. + +- `menuconfig_example.py <https://github.com/ulfalizer/Kconfiglib/blob/master/examples/menuconfig_example.py>`_ implements a configuration interface that uses notation similar to ``make menuconfig``. It's deliberately kept as simple as possible to demonstrate just the core concepts. + +Real-world examples +~~~~~~~~~~~~~~~~~~~ + +- `kconfig.py + <https://github.com/zephyrproject-rtos/zephyr/blob/master/scripts/kconfig/kconfig.py>`_ + from the `Zephyr <https://www.zephyrproject.org/>`_ project handles + ``.config`` and header file generation, also doing configuration fragment + merging + +- `genrest.py + <https://github.com/zephyrproject-rtos/zephyr/blob/master/doc/scripts/genrest.py>`_ + generates a Kconfig symbol cross-reference, which can be viewed `here + <http://docs.zephyrproject.org/reference/kconfig/index.html>`__ + +- `CMake and IDE integration + <https://github.com/espressif/esp-idf/tree/master/tools/kconfig_new>`_ from + the ESP-IDF project, via a configuration server program. + +- `A script for turning on USB-related options + <https://github.com/google/syzkaller/blob/master/dashboard/config/kconfiglib-merge-usb-configs.py>`_, + from the `syzkaller <https://github.com/google/syzkaller>`_ project. + +- `Various automated checks + <https://github.com/zephyrproject-rtos/ci-tools/blob/master/scripts/check_compliance.py>`_, + including a check for references to undefined Kconfig symbols in source code. + See the ``KconfigCheck`` class. + +- `Various utilities + <https://github.com/projectacrn/acrn-hypervisor/tree/master/scripts/kconfig>`_ + from the `ACRN <https://projectacrn.org/>`_ project + +These use the older Kconfiglib 1 API, which was clunkier and not as general +(functions instead of properties, no direct access to the menu structure or +properties, uglier ``__str__()`` output): + +- `genboardscfg.py <http://git.denx.de/?p=u-boot.git;a=blob;f=tools/genboardscfg.py;hb=HEAD>`_ from `Das U-Boot <http://www.denx.de/wiki/U-Boot>`_ generates some sort of legacy board database by pulling information from a newly added Kconfig-based configuration system (as far as I understand it :). + +- `gen-manual-lists.py <https://git.busybox.net/buildroot/tree/support/scripts/gen-manual-lists.py?id=5676a2deea896f38123b99781da0a612865adeb0>`_ generated listings for an appendix in the `Buildroot <https://buildroot.org>`_ manual. (The listing has since been removed.) + +- `gen_kconfig_doc.py <https://github.com/espressif/esp-idf/blob/master/docs/gen-kconfig-doc.py>`_ from the `esp-idf <https://github.com/espressif/esp-idf>`_ project generates documentation from Kconfig files. + +- `SConf <https://github.com/CoryXie/SConf>`_ builds an interactive configuration interface (like ``menuconfig``) on top of Kconfiglib, for use e.g. with `SCons <scons.org>`_. + +- `kconfig-diff.py <https://gist.github.com/dubiousjim/5638961>`_ -- a script by `dubiousjim <https://github.com/dubiousjim>`_ that compares kernel configurations. + +- Originally, Kconfiglib was used in chapter 4 of my `master's thesis <http://liu.diva-portal.org/smash/get/diva2:473038/FULLTEXT01.pdf>`_ to automatically generate a "minimal" kernel for a given system. Parts of it bother me a bit now, but that's how it goes with old work. + +Sample ``make iscriptconfig`` session +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following log should give some idea of the functionality available in the API: + +.. code-block:: + + $ make iscriptconfig + A Kconfig instance 'kconf' for the architecture x86 has been created. + >>> kconf # Calls Kconfig.__repr__() + <configuration with 13711 symbols, main menu prompt "Linux/x86 4.14.0-rc7 Kernel Configuration", srctree ".", config symbol prefix "CONFIG_", warnings enabled, undef. symbol assignment warnings disabled> + >>> kconf.mainmenu_text # Expanded main menu text + 'Linux/x86 4.14.0-rc7 Kernel Configuration' + >>> kconf.top_node # The implicit top-level menu + <menu node for menu, prompt "Linux/x86 4.14.0-rc7 Kernel Configuration" (visibility y), deps y, 'visible if' deps y, has child, Kconfig:5> + >>> kconf.top_node.list # First child menu node + <menu node for symbol SRCARCH, deps y, has next, Kconfig:7> + >>> print(kconf.top_node.list) # Calls MenuNode.__str__() + config SRCARCH + string + option env="SRCARCH" + default "x86" + >>> sym = kconf.top_node.list.next.item # Item contained in next menu node + >>> print(sym) # Calls Symbol.__str__() + config 64BIT + bool "64-bit kernel" if ARCH = "x86" + default ARCH != "i386" + help + Say yes to build a 64-bit kernel - formerly known as x86_64 + Say no to build a 32-bit kernel - formerly known as i386 + >>> sym # Calls Symbol.__repr__() + <symbol 64BIT, bool, "64-bit kernel", value y, visibility y, direct deps y, arch/x86/Kconfig:2> + >>> sym.assignable # Currently assignable values (0, 1, 2 = n, m, y) + (0, 2) + >>> sym.set_value(0) # Set it to n + True + >>> sym.tri_value # Check the new value + 0 + >>> sym = kconf.syms["X86_MPPARSE"] # Look up symbol by name + >>> print(sym) + config X86_MPPARSE + bool "Enable MPS table" if (ACPI || SFI) && X86_LOCAL_APIC + default y if X86_LOCAL_APIC + help + For old smp systems that do not have proper acpi support. Newer systems + (esp with 64bit cpus) with acpi support, MADT and DSDT will override it + >>> default = sym.defaults[0] # Fetch its first default + >>> sym = default[1] # Fetch the default's condition (just a Symbol here) + >>> print(sym) + config X86_LOCAL_APIC + bool + default y + select IRQ_DOMAIN_HIERARCHY + select PCI_MSI_IRQ_DOMAIN if PCI_MSI + depends on X86_64 || SMP || X86_32_NON_STANDARD || X86_UP_APIC || PCI_MSI + >>> sym.nodes # Show the MenuNode(s) associated with it + [<menu node for symbol X86_LOCAL_APIC, deps n, has next, arch/x86/Kconfig:1015>] + >>> kconfiglib.expr_str(sym.defaults[0][1]) # Print the default's condition + 'X86_64 || SMP || X86_32_NON_STANDARD || X86_UP_APIC || PCI_MSI' + >>> kconfiglib.expr_value(sym.defaults[0][1]) # Evaluate it (0 = n) + 0 + >>> kconf.syms["64BIT"].set_value(2) + True + >>> kconfiglib.expr_value(sym.defaults[0][1]) # Evaluate it again (2 = y) + 2 + >>> kconf.write_config("myconfig") # Save a .config + >>> ^D + $ cat myconfig + # Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib) + CONFIG_64BIT=y + CONFIG_X86_64=y + CONFIG_X86=y + CONFIG_INSTRUCTION_DECODER=y + CONFIG_OUTPUT_FORMAT="elf64-x86-64" + CONFIG_ARCH_DEFCONFIG="arch/x86/configs/x86_64_defconfig" + CONFIG_LOCKDEP_SUPPORT=y + CONFIG_STACKTRACE_SUPPORT=y + CONFIG_MMU=y + ... + +Test suite +---------- + +The test suite is run with + +.. code:: + + $ python(3) Kconfiglib/testsuite.py + +`pypy <https://pypy.org/>`_ works too, and is much speedier for everything except ``allnoconfig.py``/``allnoconfig_simpler.py``/``allyesconfig.py``, where it doesn't have time to warm up since +the scripts are run via ``make scriptconfig``. + +The test suite must be run from the top-level kernel directory. It requires that the +Kconfiglib git repository has been cloned into it and that the makefile patch has been applied. + +To get rid of warnings generated for the kernel ``Kconfig`` files, add ``2>/dev/null`` to the command to +discard ``stderr``. + +**NOTE: Forgetting to apply the Makefile patch will cause some tests that compare generated configurations to fail** + +**NOTE: The test suite overwrites .config in the kernel root, so make sure to back it up.** + +The test suite consists of a set of selftests and a set of compatibility tests that +compare configurations generated by Kconfiglib with +configurations generated by the C tools, for a number of cases. See +`testsuite.py <https://github.com/ulfalizer/Kconfiglib/blob/master/testsuite.py>`_ +for the available options. + +The `tests/reltest <https://github.com/ulfalizer/Kconfiglib/blob/master/tests/reltest>`_ script runs the test suite +and all the example scripts for both Python 2 and Python 3, verifying that everything works. + +Rarely, the output from the C tools is changed slightly (most recently due to a +`change <https://www.spinics.net/lists/linux-kbuild/msg17074.html>`_ I added). +If you get test suite failures, try running the test suite again against the +`linux-next tree <https://www.kernel.org/doc/man-pages/linux-next.html>`_, +which has all the latest changes. I will make it clear if any +non-backwards-compatible changes appear. + +A lot of time is spent waiting around for ``make`` and the C utilities (which need to reparse all the +Kconfig files for each defconfig test). Adding some multiprocessing to the test suite would make sense +too. + +Notes +----- + +* This is version 2 of Kconfiglib, which is not backwards-compatible with + Kconfiglib 1. A summary of changes between Kconfiglib 1 and Kconfiglib + 2 can be found `here + <https://github.com/ulfalizer/Kconfiglib/blob/screenshots/kconfiglib-2-changes.txt>`__. + +* I sometimes see people add custom output formats, which is pretty + straightforward to do (see the implementations of ``write_autoconf()`` and + ``write_config()`` for a template, and also the documentation of the + ``Symbol.config_string`` property). If you come up with something you think + might be useful to other people, I'm happy to take it in upstream. Batteries + included and all that. + +* Kconfiglib assumes the modules symbol is ``MODULES``, which is backwards-compatible. + A warning is printed by default if ``option modules`` is set on some other symbol. + + Let me know if you need proper ``option modules`` support. It wouldn't be that + hard to add. + +Thanks +------ + +- To `RomaVis <https://github.com/RomaVis>`_, for making + `pymenuconfig <https://github.com/RomaVis/pymenuconfig>`_ and suggesting + the ``rsource`` keyword. + +- To `Mitja Horvat <https://github.com/pinkfluid>`_, for adding support + for user-defined styles to the terminal menuconfig. + +- To `Philip Craig <https://github.com/philipc>`_ for adding + support for the ``allnoconfig_y`` option and fixing an obscure issue + with ``comment``\s inside ``choice``\s (that didn't affect correctness but + made outputs differ). ``allnoconfig_y`` is used to force certain symbols + to ``y`` during ``make allnoconfig`` to improve coverage. + +License +------- + +See `LICENSE.txt <https://github.com/ulfalizer/Kconfiglib/blob/master/LICENSE.txt>`_. SPDX license identifiers are used in the +source code. diff --git a/support/kconfiglib/alldefconfig.py b/support/kconfiglib/alldefconfig.py new file mode 100755 index 0000000..5082fcb --- /dev/null +++ b/support/kconfiglib/alldefconfig.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where all symbols are set to their their default +values. + +The default output filename is '.config'. A different filename can be passed in +the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/alldefconfig.py +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig() + kconfiglib.load_allconfig(kconf, "alldef.config") + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/allmodconfig.py b/support/kconfiglib/allmodconfig.py new file mode 100755 index 0000000..9ae0b3b --- /dev/null +++ b/support/kconfiglib/allmodconfig.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where as many symbols as possible are set to 'm'. + +The default output filename is '.config'. A different filename can be passed +in the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/allmodconfig.py +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig() + + # See allnoconfig.py + kconf.warn = False + + for sym in kconf.unique_defined_syms: + if sym.orig_type == kconfiglib.BOOL: + # 'bool' choice symbols get their default value, as determined by + # e.g. 'default's on the choice + if not sym.choice: + # All other bool symbols get set to 'y', like for allyesconfig + sym.set_value(2) + elif sym.orig_type == kconfiglib.TRISTATE: + sym.set_value(1) + + for choice in kconf.unique_choices: + choice.set_value(2 if choice.orig_type == kconfiglib.BOOL else 1) + + kconf.warn = True + + kconfiglib.load_allconfig(kconf, "allmod.config") + + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/allnoconfig.py b/support/kconfiglib/allnoconfig.py new file mode 100755 index 0000000..e162ccb --- /dev/null +++ b/support/kconfiglib/allnoconfig.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where as many symbols as possible are set to 'n'. + +The default output filename is '.config'. A different filename can be passed +in the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/allmodconfig.py + +See the examples/allnoconfig_walk.py example script for another way to +implement this script. +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig() + + # Avoid warnings that would otherwise get printed by Kconfiglib for the + # following: + # + # 1. Assigning a value to a symbol without a prompt, which never has any + # effect + # + # 2. Assigning values invalid for the type (only bool/tristate symbols + # accept 0/1/2, for n/m/y). The assignments will be ignored for other + # symbol types, which is what we want. + kconf.warn = False + for sym in kconf.unique_defined_syms: + sym.set_value(2 if sym.is_allnoconfig_y else 0) + kconf.warn = True + + kconfiglib.load_allconfig(kconf, "allno.config") + + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/allyesconfig.py b/support/kconfiglib/allyesconfig.py new file mode 100755 index 0000000..e19ab31 --- /dev/null +++ b/support/kconfiglib/allyesconfig.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Writes a configuration file where as many symbols as possible are set to 'y'. + +The default output filename is '.config'. A different filename can be passed +in the KCONFIG_CONFIG environment variable. + +Usage for the Linux kernel: + + $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/allmodconfig.py +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig() + + # See allnoconfig.py + kconf.warn = False + + # Try to set all symbols to 'y'. Dependencies might truncate the value down + # later, but this will at least give the highest possible value. + # + # Assigning 0/1/2 to non-bool/tristate symbols has no effect (int/hex + # symbols still take a string, because they preserve formatting). + for sym in kconf.unique_defined_syms: + # Set choice symbols to 'm'. This value will be ignored for choices in + # 'y' mode (the "normal" mode), which will instead just get their + # default selection, but will set all symbols in m-mode choices to 'm', + # which is as high as they can go. + # + # Here's a convoluted example of how you might get an m-mode choice + # even during allyesconfig: + # + # choice + # tristate "weird choice" + # depends on m + sym.set_value(1 if sym.choice else 2) + + # Set all choices to the highest possible mode + for choice in kconf.unique_choices: + choice.set_value(2) + + kconf.warn = True + + kconfiglib.load_allconfig(kconf, "allyes.config") + + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/defconfig.py b/support/kconfiglib/defconfig.py new file mode 100755 index 0000000..d1b1e4e --- /dev/null +++ b/support/kconfiglib/defconfig.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Reads a specified configuration file, then writes a new configuration file. +This can be used to initialize the configuration from e.g. an arch-specific +configuration file. This input configuration file would usually be a minimal +configuration file, as generated by e.g. savedefconfig. + +The default output filename is '.config'. A different filename can be passed in +the KCONFIG_CONFIG environment variable. +""" +import argparse + +import kconfiglib + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--kconfig", + default="Kconfig", + help="Base Kconfig file (default: Kconfig)") + + parser.add_argument( + "config", + metavar="CONFIGURATION", + help="Input configuration file") + + args = parser.parse_args() + + kconf = kconfiglib.Kconfig(args.kconfig) + print(kconf.load_config(args.config)) + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/examples/Kmenuconfig b/support/kconfiglib/examples/Kmenuconfig new file mode 100644 index 0000000..f1cb67b --- /dev/null +++ b/support/kconfiglib/examples/Kmenuconfig @@ -0,0 +1,102 @@ +mainmenu "Example Kconfig configuration" + +config MODULES + bool "Enable loadable module support" + option modules + default y + +menu "Bool and tristate symbols" + +config BOOL + bool "Bool symbol" + default y + +config BOOL_DEP + bool "Dependent bool symbol" + depends on BOOL + +# Mix it up a bit with an 'if' instead of a 'depends on' +if BOOL + +config TRI_DEP + tristate "Dependent tristate symbol" + select SELECTED_BY_TRI_DEP + imply IMPLIED_BY_TRI_DEP + +endif + +config TWO_MENU_NODES + bool "First prompt" + depends on BOOL + +config TRI + tristate "Tristate symbol" + +config TWO_MENU_NODES + bool "Second prompt" + +comment "These are selected by TRI_DEP" + +config SELECTED_BY_TRI_DEP + tristate "Tristate selected by TRI_DEP" + +config IMPLIED_BY_TRI_DEP + tristate "Tristate implied by TRI_DEP" + +endmenu + + +menu "String, int, and hex symbols" + +config STRING + string "String symbol" + default "foo" + +config INT + int "Int symbol" + default 747 + +config HEX + hex "Hex symbol" + default 0xABC + +endmenu + + +menu "Various choices" + +choice BOOL_CHOICE + bool "Bool choice" + +config BOOL_CHOICE_SYM_1 + bool "Bool choice sym 1" + +config BOOL_CHOICE_SYM_2 + bool "Bool choice sym 2" + +endchoice + +choice TRI_CHOICE + tristate "Tristate choice" + +config TRI_CHOICE_SYM_1 + tristate "Tristate choice sym 1" + +config TRI_CHOICE_SYM_2 + tristate "Tristate choice sym 2" + +endchoice + +choice OPT_BOOL_CHOICE + bool "Optional bool choice" + optional + +config OPT_BOOL_CHOICE_SYM_1 + bool "Optional bool choice sym 1" + +config OPT_BOOL_CHOICE_SYM_2 + bool "Optional bool choice sym 2" + +endchoice + +endmenu diff --git a/support/kconfiglib/examples/allnoconfig_walk.py b/support/kconfiglib/examples/allnoconfig_walk.py new file mode 100644 index 0000000..5a8cc23 --- /dev/null +++ b/support/kconfiglib/examples/allnoconfig_walk.py @@ -0,0 +1,66 @@ +# This is tree-walking version of allnoconfig.py, for demonstration purposes. +# Verified by the test suite to generate identical output to 'make allnoconfig' +# for all ARCHes. +# +# Note: A more practical version would use Kconfig.node_iter(). The manual tree +# walking is for demonstration purposes. +# +# Usage for the Linux kernel: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_walk.py + +import sys + +from kconfiglib import Kconfig, Symbol + + +def do_allnoconfig(node): + global changed + + # Walk the tree of menu nodes. You can imagine this as going down/into menu + # entries in the menuconfig interface, setting each to n (or the lowest + # assignable value). + + while node: + if isinstance(node.item, Symbol): + sym = node.item + + # Is the symbol a non-allnoconfig_y symbol that can be set to a + # lower value than its current value? + if (not sym.is_allnoconfig_y and + sym.assignable and + sym.assignable[0] < sym.tri_value): + + # Yup, lower it + sym.set_value(sym.assignable[0]) + changed = True + + # Recursively lower children + if node.list: + do_allnoconfig(node.list) + + node = node.next + + +# Parse the Kconfig files +kconf = Kconfig(sys.argv[1]) + +# Do an initial pass to set 'option allnoconfig_y' symbols to y +for sym in kconf.unique_defined_syms: + if sym.is_allnoconfig_y: + sym.set_value(2) + +while True: + # Changing later symbols in the configuration can sometimes allow earlier + # symbols to be lowered, e.g. if a later symbol 'select's an earlier + # symbol. To handle such situations, we do additional passes over the tree + # until we're no longer able to change the value of any symbol in a pass. + changed = False + + do_allnoconfig(kconf.top_node) + + # Did the pass change any symbols? + if not changed: + break + +print(kconf.write_config()) diff --git a/support/kconfiglib/examples/defconfig_oldconfig.py b/support/kconfiglib/examples/defconfig_oldconfig.py new file mode 100644 index 0000000..68336c6 --- /dev/null +++ b/support/kconfiglib/examples/defconfig_oldconfig.py @@ -0,0 +1,39 @@ +# Produces exactly the same output as the following script: +# +# make defconfig +# echo CONFIG_ETHERNET=n >> .config +# make oldconfig +# echo CONFIG_ETHERNET=y >> .config +# yes n | make oldconfig +# +# This came up in https://github.com/ulfalizer/Kconfiglib/issues/15. +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/defconfig_oldconfig.py + +import sys + +import kconfiglib + + +kconf = kconfiglib.Kconfig(sys.argv[1]) + +# Mirrors defconfig +kconf.load_config("arch/x86/configs/x86_64_defconfig") +kconf.write_config() + +# Mirrors the first oldconfig +kconf.load_config() +kconf.syms["ETHERNET"].set_value(0) +kconf.write_config() + +# Mirrors the second oldconfig +kconf.load_config() +kconf.syms["ETHERNET"].set_value(2) +for s in kconf.unique_defined_syms: + if s.user_value is None and 0 in s.assignable: + s.set_value(0) + +# Write the final configuration +print(kconf.write_config()) diff --git a/support/kconfiglib/examples/dumpvars.py b/support/kconfiglib/examples/dumpvars.py new file mode 100644 index 0000000..0f8ab43 --- /dev/null +++ b/support/kconfiglib/examples/dumpvars.py @@ -0,0 +1,15 @@ +# Prints all (set) environment variables referenced in the Kconfig files +# together with their values, as a list of assignments. +# +# Note: This only works for environment variables referenced via the $(FOO) +# preprocessor syntax. The older $FOO syntax is maintained for backwards +# compatibility. + +import os +import sys + +import kconfiglib + + +print(" ".join("{}='{}'".format(var, os.environ[var]) + for var in kconfiglib.Kconfig(sys.argv[1]).env_vars)) diff --git a/support/kconfiglib/examples/eval_expr.py b/support/kconfiglib/examples/eval_expr.py new file mode 100644 index 0000000..23eedb4 --- /dev/null +++ b/support/kconfiglib/examples/eval_expr.py @@ -0,0 +1,24 @@ +# Evaluates an expression (e.g. "X86_64 || (X86_32 && X86_LOCAL_APIC)") in the +# context of a configuration. Note that this always yields a tristate value (n, +# m, or y). +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/eval_expr.py SCRIPT_ARG=<expr> + +import sys + +import kconfiglib + + +if len(sys.argv) < 3: + sys.exit("Pass the expression to evaluate with SCRIPT_ARG=<expression>") + +kconf = kconfiglib.Kconfig(sys.argv[1]) +expr = sys.argv[2] + +# Enable modules so that m doesn't get demoted to n +kconf.modules.set_value(2) + +print("the expression '{}' evaluates to {}" + .format(expr, kconf.eval_string(expr))) diff --git a/support/kconfiglib/examples/find_symbol.py b/support/kconfiglib/examples/find_symbol.py new file mode 100644 index 0000000..f747103 --- /dev/null +++ b/support/kconfiglib/examples/find_symbol.py @@ -0,0 +1,112 @@ +# Prints all menu nodes that reference a given symbol any of their properties +# or property conditions, along with their parent menu nodes. +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/find_symbol.py SCRIPT_ARG=<name> +# +# Example output for SCRIPT_ARG=X86: +# +# Found 470 locations that reference X86: +# +# ========== Location 1 (init/Kconfig:1108) ========== +# +# config SGETMASK_SYSCALL +# bool +# prompt "sgetmask/ssetmask syscalls support" if EXPERT +# default PARISC || M68K || PPC || MIPS || X86 || SPARC || MICROBLAZE || SUPERH +# help +# sys_sgetmask and sys_ssetmask are obsolete system calls +# no longer supported in libc but still enabled by default in some +# architectures. +# +# If unsure, leave the default option here. +# +# ---------- Parent 1 (init/Kconfig:1077) ---------- +# +# menuconfig EXPERT +# bool +# prompt "Configure standard kernel features (expert users)" +# select DEBUG_KERNEL +# help +# This option allows certain base kernel options and settings +# to be disabled or tweaked. This is for specialized +# environments which can tolerate a "non-standard" kernel. +# Only use this if you really know what you are doing. +# +# ---------- Parent 2 (init/Kconfig:39) ---------- +# +# menu "General setup" +# +# ========== Location 2 (arch/Kconfig:29) ========== +# +# config OPROFILE_EVENT_MULTIPLEX +# bool +# prompt "OProfile multiplexing support (EXPERIMENTAL)" +# default "n" +# depends on OPROFILE && X86 +# help +# The number of hardware counters is limited. The multiplexing +# feature enables OProfile to gather more events than counters +# are provided by the hardware. This is realized by switching +# between events at a user specified time interval. +# +# If unsure, say N. +# +# ---------- Parent 1 (arch/Kconfig:16) ---------- +# +# config OPROFILE +# tristate +# prompt "OProfile system profiling" +# select RING_BUFFER +# select RING_BUFFER_ALLOW_SWAP +# depends on PROFILING && HAVE_OPROFILE +# help +# OProfile is a profiling system capable of profiling the +# whole system, include the kernel, kernel modules, libraries, +# and applications. +# +# If unsure, say N. +# +# ---------- Parent 2 (init/Kconfig:39) ---------- +# +# menu "General setup" +# +# ... (tons more) + +import sys + +import kconfiglib + + +if len(sys.argv) < 3: + sys.exit('Pass symbol name (without "CONFIG_" prefix) with SCRIPT_ARG=<name>') + +kconf = kconfiglib.Kconfig(sys.argv[1]) +sym_name = sys.argv[2] +if sym_name not in kconf.syms: + print("No symbol {} exists in the configuration".format(sym_name)) + sys.exit(0) + +referencing = [node for node in kconf.node_iter() + if kconf.syms[sym_name] in node.referenced] +if not referencing: + print("No references to {} found".format(sym_name)) + sys.exit(0) + +print("Found {} locations that reference {}:\n" + .format(len(referencing), sym_name)) + +for i, node in enumerate(referencing, 1): + print("========== Location {} ({}:{}) ==========\n\n{}" + .format(i, node.filename, node.linenr, node)) + + # Print the parents of the menu node too + + node = node.parent + parent_i = 1 + while node is not kconf.top_node: + print("---------- Parent {} ({}:{}) ----------\n\n{}" + .format(parent_i, node.filename, node.linenr, node)) + node = node.parent + parent_i += 1 diff --git a/support/kconfiglib/examples/help_grep.py b/support/kconfiglib/examples/help_grep.py new file mode 100644 index 0000000..157d8f2 --- /dev/null +++ b/support/kconfiglib/examples/help_grep.py @@ -0,0 +1,64 @@ +# Does a case-insensitive search for a regular expression in the help texts of +# symbols and choices and the prompts of menus and comments. Prints the +# matching items together with their locations and the matching text. +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/help_grep.py SCRIPT_ARG=<regex> +# +# Shortened example output for SCRIPT_ARG=general: +# +# menu "General setup" +# location: init/Kconfig:39 +# +# config SYSVIPC +# bool +# prompt "System V IPC" +# help +# ... +# exchange information. It is generally considered to be a good thing, +# ... +# +# location: init/Kconfig:233 +# +# config BSD_PROCESS_ACCT +# bool +# prompt "BSD Process Accounting" if MULTIUSER +# help +# ... +# information. This is generally a good idea, so say Y. +# +# location: init/Kconfig:403 +# +# ... + + +import re +import sys + +from kconfiglib import Kconfig, Symbol, Choice, MENU, COMMENT + + +if len(sys.argv) < 3: + sys.exit("Pass the regex with SCRIPT_ARG=<regex>") + +search = re.compile(sys.argv[2], re.IGNORECASE).search + +for node in Kconfig(sys.argv[1]).node_iter(): + match = False + + if isinstance(node.item, (Symbol, Choice)) and \ + node.help is not None and search(node.help): + print(node.item) + match = True + + elif node.item == MENU and search(node.prompt[0]): + print('menu "{}"'.format(node.prompt[0])) + match = True + + elif node.item == COMMENT and search(node.prompt[0]): + print('comment "{}"'.format(node.prompt[0])) + match = True + + if match: + print("location: {}:{}\n".format(node.filename, node.linenr)) diff --git a/support/kconfiglib/examples/kconfiglib.py b/support/kconfiglib/examples/kconfiglib.py new file mode 120000 index 0000000..b9dfb64 --- /dev/null +++ b/support/kconfiglib/examples/kconfiglib.py @@ -0,0 +1 @@ +../kconfiglib.py \ No newline at end of file diff --git a/support/kconfiglib/examples/list_undefined.py b/support/kconfiglib/examples/list_undefined.py new file mode 100644 index 0000000..4a3bc9b --- /dev/null +++ b/support/kconfiglib/examples/list_undefined.py @@ -0,0 +1,156 @@ +# Prints a list of symbols that are referenced in the Kconfig files of some +# architecture but not defined by the Kconfig files of any architecture. +# +# A Kconfig file might be shared between many architectures and legitimately +# reference undefined symbols for some of them, but if no architecture defines +# the symbol, it usually indicates a problem or potential cleanup. +# +# This script could be sped up a lot if needed. See the comment near the +# referencing_nodes() call. +# +# Run with the following command in the kernel root: +# +# $ python(3) Kconfiglib/examples/list_undefined.py +# +# Example output: +# +# Registering defined and undefined symbols for all arches +# Processing mips +# Processing ia64 +# Processing metag +# ... +# +# Finding references to each undefined symbol +# Processing mips +# Processing ia64 +# Processing metag +# ... +# +# The following globally undefined symbols were found, listed here +# together with the locations of the items that reference them. +# References might come from enclosing menus and ifs. +# +# ARM_ERRATA_753970: arch/arm/mach-mvebu/Kconfig:56, arch/arm/mach-mvebu/Kconfig:39 +# SUNXI_CCU_MP: drivers/clk/sunxi-ng/Kconfig:14 +# SUNXI_CCU_DIV: drivers/clk/sunxi-ng/Kconfig:14 +# AC97: sound/ac97/Kconfig:6 +# ... + +import os +import subprocess + +from kconfiglib import Kconfig + + +# Referenced inside the Kconfig files +os.environ["KERNELVERSION"] = str( + subprocess.check_output(("make", "kernelversion")).decode("utf-8").rstrip() +) + + +def all_arch_srcarch_pairs(): + """ + Generates all valid (ARCH, SRCARCH) tuples for the kernel, corresponding to + different architectures. SRCARCH holds the arch/ subdirectory. + """ + for srcarch in os.listdir("arch"): + # Each subdirectory of arch/ containing a Kconfig file corresponds to + # an architecture + if os.path.exists(os.path.join("arch", srcarch, "Kconfig")): + yield (srcarch, srcarch) + + # Some architectures define additional ARCH settings with ARCH != SRCARCH + # (search for "Additional ARCH settings for" in the top-level Makefile) + + yield ("i386", "x86") + yield ("x86_64", "x86") + + yield ("sparc32", "sparc") + yield ("sparc64", "sparc") + + yield ("sh64", "sh") + + yield ("um", "um") + + +def all_arch_srcarch_kconfigs(): + """ + Generates Kconfig instances for all the architectures in the kernel + """ + + os.environ["srctree"] = "." + os.environ["HOSTCC"] = "gcc" + os.environ["HOSTCXX"] = "g++" + os.environ["CC"] = "gcc" + os.environ["LD"] = "ld" + + for arch, srcarch in all_arch_srcarch_pairs(): + print(" Processing " + arch) + + os.environ["ARCH"] = arch + os.environ["SRCARCH"] = srcarch + + # um (User Mode Linux) uses a different base Kconfig file + yield Kconfig("Kconfig" if arch != "um" else "arch/x86/um/Kconfig", + warn=False) + + +print("Registering defined and undefined symbols for all arches") + +# Sets holding the names of all defined and undefined symbols, for all +# architectures +defined = set() +undefined = set() + +for kconf in all_arch_srcarch_kconfigs(): + for name, sym in kconf.syms.items(): + if sym.nodes: + # If the symbol has a menu node, it is defined + defined.add(name) + else: + # Undefined symbol. We skip some of the uninteresting ones. + + # Due to how Kconfig works, integer literals show up as symbols + # (from e.g. 'default 1'). Skip those. + try: + int(name, 0) + continue + except ValueError: + # Interesting undefined symbol + undefined.add(name) + + +print("\nFinding references to each undefined symbol") + +def referencing_nodes(kconf, name): + # Returns a list of all menu nodes that reference a symbol named 'name' in + # any of their properties or property conditions + res = [] + + for node in kconf.node_iter(): + for ref in node.referenced: + if ref.name == name: + res.append(node) + + return res + + +# Maps each globally undefined symbol to the menu nodes that reference it +undef_sym_refs = [(name, set()) for name in undefined - defined] + +for kconf in all_arch_srcarch_kconfigs(): + for name, refs in undef_sym_refs: + # This means that we search the entire configuration tree for each + # undefined symbol, which is terribly inefficient. We could speed + # things up by tweaking referencing_nodes() to compare each symbol to + # multiple symbols while walking the configuration tree. + for node in referencing_nodes(kconf, name): + refs.add("{}:{}".format(node.filename, node.linenr)) + + +print("\nThe following globally undefined symbols were found, listed here\n" + "together with the locations of the items that reference them.\n" + "References might come from enclosing menus and ifs.\n") + +for name, refs in undef_sym_refs: + print(" {}: {}".format(name, ", ".join(refs))) diff --git a/support/kconfiglib/examples/menuconfig_example.py b/support/kconfiglib/examples/menuconfig_example.py new file mode 100755 index 0000000..a06b05e --- /dev/null +++ b/support/kconfiglib/examples/menuconfig_example.py @@ -0,0 +1,341 @@ +#!/usr/bin/env python + +# Implements a simple configuration interface on top of Kconfiglib to +# demonstrate concepts for building a menuconfig-like. Emulates how the +# standard menuconfig prints menu entries. +# +# Always displays the entire Kconfig tree to keep things as simple as possible +# (all symbols, choices, menus, and comments). +# +# Usage: +# +# $ python(3) Kconfiglib/examples/menuconfig.py <Kconfig file> +# +# A sample Kconfig is available in Kconfiglib/examples/Kmenuconfig. +# +# Here's a notation guide. The notation matches the one used by menuconfig +# (scripts/kconfig/mconf): +# +# [ ] prompt - Bool +# < > prompt - Tristate +# {M} prompt - Tristate selected to m. Can only be set to m or y. +# -*- prompt - Bool/tristate selected to y, pinning it +# -M- prompt - Tristate selected to m that also has m visibility, +# pinning it to m +# (foo) prompt - String/int/hex symbol with value "foo" +# --> prompt - The selected symbol in a choice in y mode. This +# syntax is unique to this example. +# +# When modules are disabled, the .type attribute of TRISTATE symbols and +# choices automatically changes to BOOL. This trick is used by the C +# implementation as well, and gives the expected behavior without having to do +# anything extra here. The original type is available in .orig_type if needed. +# +# The Kconfiglib/examples/Kmenuconfig example uses named choices to be able to +# refer to choices by name. Named choices are supported in the C tools too, but +# I don't think I've ever seen them used in the wild. +# +# Sample session: +# +# $ python Kconfiglib/examples/menuconfig.py Kconfiglib/examples/Kmenuconfig +# +# ======== Example Kconfig configuration ======== +# +# [*] Enable loadable module support (MODULES) +# Bool and tristate symbols +# [*] Bool symbol (BOOL) +# [ ] Dependent bool symbol (BOOL_DEP) +# < > Dependent tristate symbol (TRI_DEP) +# [ ] First prompt (TWO_MENU_NODES) +# < > Tristate symbol (TRI) +# [ ] Second prompt (TWO_MENU_NODES) +# *** These are selected by TRI_DEP *** +# < > Tristate selected by TRI_DEP (SELECTED_BY_TRI_DEP) +# < > Tristate implied by TRI_DEP (IMPLIED_BY_TRI_DEP) +# String, int, and hex symbols +# (foo) String symbol (STRING) +# (747) Int symbol (INT) +# (0xABC) Hex symbol (HEX) +# Various choices +# -*- Bool choice (BOOL_CHOICE) +# --> Bool choice sym 1 (BOOL_CHOICE_SYM_1) +# Bool choice sym 2 (BOOL_CHOICE_SYM_2) +# {M} Tristate choice (TRI_CHOICE) +# < > Tristate choice sym 1 (TRI_CHOICE_SYM_1) +# < > Tristate choice sym 2 (TRI_CHOICE_SYM_2) +# [ ] Optional bool choice (OPT_BOOL_CHOICE) +# +# Enter a symbol/choice name, "load_config", or "write_config" (or press CTRL+D to exit): BOOL +# Value for BOOL (available: n, y): n +# +# ======== Example Kconfig configuration ======== +# +# [*] Enable loadable module support (MODULES) +# Bool and tristate symbols +# [ ] Bool symbol (BOOL) +# < > Tristate symbol (TRI) +# [ ] Second prompt (TWO_MENU_NODES) +# *** These are selected by TRI_DEP *** +# < > Tristate selected by TRI_DEP (SELECTED_BY_TRI_DEP) +# < > Tristate implied by TRI_DEP (IMPLIED_BY_TRI_DEP) +# String, int, and hex symbols +# (foo) String symbol (STRING) +# (747) Int symbol (INT) +# (0xABC) Hex symbol (HEX) +# Various choices +# -*- Bool choice (BOOL_CHOICE) +# --> Bool choice sym 1 (BOOL_CHOICE_SYM_1) +# Bool choice sym 2 (BOOL_CHOICE_SYM_2) +# {M} Tristate choice (TRI_CHOICE) +# < > Tristate choice sym 1 (TRI_CHOICE_SYM_1) +# < > Tristate choice sym 2 (TRI_CHOICE_SYM_2) +# [ ] Optional bool choice (OPT_BOOL_CHOICE) +# +# Enter a symbol/choice name, "load_config", or "write_config" (or press CTRL+D to exit): MODULES +# Value for MODULES (available: n, y): n +# +# ======== Example Kconfig configuration ======== +# +# [ ] Enable loadable module support (MODULES) +# Bool and tristate symbols +# [ ] Bool symbol (BOOL) +# [ ] Tristate symbol (TRI) +# [ ] Second prompt (TWO_MENU_NODES) +# *** These are selected by TRI_DEP *** +# [ ] Tristate selected by TRI_DEP (SELECTED_BY_TRI_DEP) +# [ ] Tristate implied by TRI_DEP (IMPLIED_BY_TRI_DEP) +# String, int, and hex symbols +# (foo) String symbol (STRING) +# (747) Int symbol (INT) +# (0xABC) Hex symbol (HEX) +# Various choices +# -*- Bool choice (BOOL_CHOICE) +# --> Bool choice sym 1 (BOOL_CHOICE_SYM_1) +# Bool choice sym 2 (BOOL_CHOICE_SYM_2) +# -*- Tristate choice (TRI_CHOICE) +# --> Tristate choice sym 1 (TRI_CHOICE_SYM_1) +# Tristate choice sym 2 (TRI_CHOICE_SYM_2) +# [ ] Optional bool choice (OPT_BOOL_CHOICE) +# +# Enter a symbol/choice name, "load_config", or "write_config" (or press CTRL+D to exit): ^D + +from __future__ import print_function +import readline +import sys + +from kconfiglib import Kconfig, \ + Symbol, MENU, COMMENT, \ + BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN, \ + expr_value, \ + TRI_TO_STR + + +# Python 2/3 compatibility hack +if sys.version_info[0] < 3: + input = raw_input + + +def indent_print(s, indent): + print(indent*" " + s) + + +def value_str(sc): + """ + Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu entry. + + sc: Symbol or Choice. + """ + if sc.type in (STRING, INT, HEX): + return "({})".format(sc.str_value) + + # BOOL or TRISTATE + + # The choice mode is an upper bound on the visibility of choice symbols, so + # we can check the choice symbols' own visibility to see if the choice is + # in y mode + if isinstance(sc, Symbol) and sc.choice and sc.visibility == 2: + # For choices in y mode, print '-->' next to the selected symbol + return "-->" if sc.choice.selection is sc else " " + + tri_val_str = (" ", "M", "*")[sc.tri_value] + + if len(sc.assignable) == 1: + # Pinned to a single value + return "-{}-".format(tri_val_str) + + if sc.type == BOOL: + return "[{}]".format(tri_val_str) + + if sc.type == TRISTATE: + if sc.assignable == (1, 2): + # m and y available + return "{" + tri_val_str + "}" # Gets a bit confusing with .format() + return "<{}>".format(tri_val_str) + + +def node_str(node): + """ + Returns the complete menu entry text for a menu node, or "" for invisible + menu nodes. Invisible menu nodes are those that lack a prompt or that do + not have a satisfied prompt condition. + + Example return value: "[*] Bool symbol (BOOL)" + + The symbol name is printed in parentheses to the right of the prompt. This + is so that symbols can easily be referred to in the configuration + interface. + """ + if not node.prompt: + return "" + + # Even for menu nodes for symbols and choices, it's wrong to check + # Symbol.visibility / Choice.visibility here. The reason is that a symbol + # (and a choice, in theory) can be defined in multiple locations, giving it + # multiple menu nodes, which do not necessarily all have the same prompt + # visibility. Symbol.visibility / Choice.visibility is calculated as the OR + # of the visibility of all the prompts. + prompt, prompt_cond = node.prompt + if not expr_value(prompt_cond): + return "" + + if node.item == MENU: + return " " + prompt + + if node.item == COMMENT: + return " *** {} ***".format(prompt) + + # Symbol or Choice + + sc = node.item + + if sc.type == UNKNOWN: + # Skip symbols defined without a type (these are obscure and generate + # a warning) + return "" + + # {:3} sets the field width to three. Gives nice alignment for empty string + # values. + res = "{:3} {}".format(value_str(sc), prompt) + + # Don't print the name for unnamed choices (the normal kind) + if sc.name is not None: + res += " ({})".format(sc.name) + + return res + + +def print_menuconfig_nodes(node, indent): + """ + Prints a tree with all the menu entries rooted at 'node'. Child menu + entries are indented. + """ + while node: + string = node_str(node) + if string: + indent_print(string, indent) + + if node.list: + print_menuconfig_nodes(node.list, indent + 8) + + node = node.next + + +def print_menuconfig(kconf): + """ + Prints all menu entries for the configuration. + """ + # Print the expanded mainmenu text at the top. This is the same as + # kconf.top_node.prompt[0], but with variable references expanded. + print("\n======== {} ========\n".format(kconf.mainmenu_text)) + + print_menuconfig_nodes(kconf.top_node.list, 0) + print("") + + +def get_value_from_user(sc): + """ + Prompts the user for a value for the symbol or choice 'sc'. For + bool/tristate symbols and choices, provides a list of all the assignable + values. + """ + if not sc.visibility: + print(sc.name + " is not currently visible") + return False + + prompt = "Value for {}".format(sc.name) + if sc.type in (BOOL, TRISTATE): + prompt += " (available: {})" \ + .format(", ".join(TRI_TO_STR[val] for val in sc.assignable)) + prompt += ": " + + val = input(prompt) + + # Automatically add a "0x" prefix for hex symbols, like the menuconfig + # interface does. This isn't done when loading .config files, hence why + # set_value() doesn't do it automatically. + if sc.type == HEX and not val.startswith(("0x", "0X")): + val = "0x" + val + + # Let Kconfiglib itself print a warning here if the value is invalid. We + # could also disable warnings temporarily with 'kconf.warn = False' and + # print our own warning. + return sc.set_value(val) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit("usage: menuconfig.py <Kconfig file>") + + # Load Kconfig configuration files + kconf = Kconfig(sys.argv[1]) + + # Print the initial configuration tree + print_menuconfig(kconf) + + while True: + try: + cmd = input('Enter a symbol/choice name, "load_config", or ' + '"write_config" (or press CTRL+D to exit): ').strip() + except EOFError: + print("") + break + + if cmd == "load_config": + config_filename = input(".config file to load: ") + try: + # Returns a message telling which file got loaded + print(kconf.load_config(config_filename)) + except EnvironmentError as e: + print(e, file=sys.stderr) + + print_menuconfig(kconf) + continue + + if cmd == "write_config": + config_filename = input("To this file: ") + try: + # Returns a message telling which file got saved + print(kconf.write_config(config_filename)) + except EnvironmentError as e: + print(e, file=sys.stderr) + + continue + + # Assume 'cmd' is the name of a symbol or choice if it isn't one of the + # commands above, prompt the user for a value for it, and print the new + # configuration tree + + if cmd in kconf.syms: + if get_value_from_user(kconf.syms[cmd]): + print_menuconfig(kconf) + + continue + + if cmd in kconf.named_choices: + if get_value_from_user(kconf.named_choices[cmd]): + print_menuconfig(kconf) + + continue + + print("No symbol/choice named '{}' in the configuration".format(cmd), + file=sys.stderr) diff --git a/support/kconfiglib/examples/merge_config.py b/support/kconfiglib/examples/merge_config.py new file mode 100755 index 0000000..ef11d79 --- /dev/null +++ b/support/kconfiglib/examples/merge_config.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python + +# This script functions similarly to scripts/kconfig/merge_config.sh from the +# kernel tree, merging multiple configurations fragments to produce a complete +# .config, with unspecified values filled in as for alldefconfig. +# +# The generated .config respects symbol dependencies, and a warning is printed +# if any symbol gets a different value from the assigned value. +# +# For a real-world merging example based on this script, see +# https://github.com/zephyrproject-rtos/zephyr/blob/master/scripts/kconfig/kconfig.py. +# +# Here's a demo: +# +# Kconfig contents: +# +# config FOO +# bool "FOO" +# +# config BAR +# bool "BAR" +# +# config BAZ +# string "BAZ" +# +# config QAZ +# bool "QAZ" if n +# +# +# conf1 contents: +# +# CONFIG_FOO=y +# +# +# conf2 contents: +# +# CONFIG_BAR=y +# +# +# conf3 contents: +# +# # Assigned twice (would generate warning if 'warn_assign_override' was +# # True) +# # CONFIG_FOO is not set +# +# # Ops... this symbol doesn't exist +# CONFIG_OPS=y +# +# CONFIG_BAZ="baz string" +# +# +# conf4 contents: +# +# CONFIG_QAZ=y +# +# +# Running: +# +# $ python(3) merge_config.py Kconfig merged conf1 conf2 conf3 conf4 +# Merged configuration 'conf1' +# Merged configuration 'conf2' +# conf3:5: warning: attempt to assign the value 'y' to the undefined symbol OPS +# Merged configuration 'conf3' +# Merged configuration 'conf4' +# Configuration saved to 'merged' +# warning: QAZ (defined at Kconfig:10) was assigned the value 'y' but got the value 'n' -- check dependencies +# $ cat merged +# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib) +# # CONFIG_FOO is not set +# CONFIG_BAR=y +# CONFIG_BAZ="baz string" + +from __future__ import print_function +import sys + +from kconfiglib import Kconfig, BOOL, TRISTATE, TRI_TO_STR + + +if len(sys.argv) < 4: + sys.exit("usage: merge_config.py Kconfig merged_config config1 [config2 ...]") + +kconf = Kconfig(sys.argv[1]) + +# Enable warnings for assignments to undefined symbols +kconf.warn_assign_undef = True + +# (This script uses alldefconfig as the base. Other starting states could be +# set up here as well. The approach in examples/allnoconfig_simpler.py could +# provide an allnoconfig starting state for example.) + +# Disable warnings generated for multiple assignments to the same symbol within +# a (set of) configuration files. Assigning a symbol multiple times might be +# done intentionally when merging configuration files. +kconf.warn_assign_override = False +kconf.warn_assign_redun = False + +# Create a merged configuration by loading the fragments with replace=False. +# load_config() and write_config() returns a message to print. +for config in sys.argv[3:]: + print(kconf.load_config(config, replace=False)) + +# Write the merged configuration +print(kconf.write_config(sys.argv[2])) + +# Print warnings for symbols whose actual value doesn't match the assigned +# value + +def name_and_loc(sym): + # Helper for printing symbol names and Kconfig file location(s) in warnings + + if not sym.nodes: + return sym.name + " (undefined)" + + return "{} (defined at {})".format( + sym.name, + ", ".join("{}:{}".format(node.filename, node.linenr) + for node in sym.nodes)) + +for sym in kconf.defined_syms: + # Was the symbol assigned to? + if sym.user_value is not None: + # Tristate values are represented as 0, 1, 2. Having them as + # "n", "m", "y" is more convenient here, so convert. + if sym.type in (BOOL, TRISTATE): + user_value = TRI_TO_STR[sym.user_value] + else: + user_value = sym.user_value + + if user_value != sym.str_value: + print("warning: {} was assigned the value '{}' but got the " + "value '{}' -- check dependencies".format( + name_and_loc(sym), user_value, sym.str_value), + file=sys.stderr) diff --git a/support/kconfiglib/examples/print_config_tree.py b/support/kconfiglib/examples/print_config_tree.py new file mode 100644 index 0000000..dc81d9d --- /dev/null +++ b/support/kconfiglib/examples/print_config_tree.py @@ -0,0 +1,199 @@ +# Prints menu entries as a tree with its value in the .config file. This can be +# handy e.g. for diffing between different .config files or versions of Kconfig files. +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=print_config_tree.py [SCRIPT_ARG=<.config>] +# +# If the variable WITH_HELP_DESC is modified to 'True', the help is added +# to the symbols. +# +# Here's a notation guide. The notation matches the one used by menuconfig +# (scripts/kconfig/mconf): +# +# [ ] prompt - Bool +# < > prompt - Tristate +# {M} prompt - Tristate selected to m. Can only be set to m or y. +# -*- prompt - Bool/tristate selected to y, pinning it +# -M- prompt - Tristate selected to m that also has m visibility, +# pinning it to m +# (foo) prompt - String/int/hex symbol with value "foo" +# --> prompt - The selected symbol in a choice in y mode. This +# syntax is unique to this example. +# +# When modules are disabled, the .type attribute of TRISTATE symbols and +# choices automatically changes to BOOL. This trick is used by the C +# implementation as well, and gives the expected behavior without having to do +# anything extra here. The original type is available in .orig_type if needed. +# +# Example output: +# +# $ make scriptconfig SCRIPT=Kconfiglib/examples/print_config_tree.py [SCRIPT_ARG=<.config file>] +# +# ======== Linux/x86 4.9.82 Kernel Configuration ======== +# +# [*] 64-bit kernel (64BIT) +# General setup +# () Cross-compiler tool prefix (CROSS_COMPILE) +# [ ] Compile also drivers which will not load (COMPILE_TEST) +# () Local version - append to kernel release (LOCALVERSION) +# [*] Automatically append version information to the version string (LOCALVERSION_AUTO) +# -*- Kernel compression mode +# ... +# +# With the variable WITH_HELP_DESC modified to 'True': +# +# ======== Linux/x86 4.9.82 Kernel Configuration ======== +# +# [*] 64-bit kernel - Say yes to build a 64-bit kernel - formerly known as x86_64 Say no to build a 32-bit kernel - formerly known as i386 (64BIT) +# General setup +# () Cross-compiler tool prefix - Same as running 'make CROSS_COMPILE=prefix-' but stored for default make runs in this kernel build directory. You don't need to set this unless you want the configured kernel build directory to select the cross-compiler automatically. (CROSS_COMPILE) +# [ ] Compile also drivers which will not load - Some drivers can be compiled on a different platform than they are intended to be run on. Despite they cannot be loaded there (or even when they load they cannot be used due to missing HW support), developers still, opposing to distributors, might want to build such drivers to compile-test them. If you are a developer and want to build everything available, say Y here. If you are a user/distributor, say N here to exclude useless drivers to be distributed. (COMPILE_TEST) +# ... + +import sys + +from kconfiglib import Kconfig, \ + Symbol, MENU, COMMENT, \ + BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN, \ + expr_value + + +# Add help description to output +WITH_HELP_DESC = False + + +def indent_print(s, indent): + print(indent*" " + s) + + +def value_str(sc): + """ + Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu entry. + + sc: Symbol or Choice. + """ + if sc.type in (STRING, INT, HEX): + return "({})".format(sc.str_value) + + # BOOL or TRISTATE + + # The choice mode is an upper bound on the visibility of choice symbols, so + # we can check the choice symbols' own visibility to see if the choice is + # in y mode + if isinstance(sc, Symbol) and sc.choice and sc.visibility == 2: + # For choices in y mode, print '-->' next to the selected symbol + return "-->" if sc.choice.selection is sc else " " + + tri_val_str = (" ", "M", "*")[sc.tri_value] + + if len(sc.assignable) == 1: + # Pinned to a single value + return "-{}-".format(tri_val_str) + + if sc.type == BOOL: + return "[{}]".format(tri_val_str) + + if sc.type == TRISTATE: + if sc.assignable == (1, 2): + # m and y available + return "{" + tri_val_str + "}" # Gets a bit confusing with .format() + return "<{}>".format(tri_val_str) + + +def node_str(node): + """ + Returns the complete menu entry text for a menu node, or "" for invisible + menu nodes. Invisible menu nodes are those that lack a prompt or that do + not have a satisfied prompt condition. + + Example return value: "[*] Bool symbol (BOOL)" + + The symbol name is printed in parentheses to the right of the prompt. + """ + if not node.prompt: + return "" + + # Even for menu nodes for symbols and choices, it's wrong to check + # Symbol.visibility / Choice.visibility here. The reason is that a symbol + # (and a choice, in theory) can be defined in multiple locations, giving it + # multiple menu nodes, which do not necessarily all have the same prompt + # visibility. Symbol.visibility / Choice.visibility is calculated as the OR + # of the visibility of all the prompts. + prompt, prompt_cond = node.prompt + if not expr_value(prompt_cond): + return "" + + if node.item == MENU: + return " " + prompt + + if node.item == COMMENT: + return " *** {} ***".format(prompt) + + # Symbol or Choice + + sc = node.item + + if sc.type == UNKNOWN: + # Skip symbols defined without a type (these are obscure and generate + # a warning) + return "" + + # Add help text + if WITH_HELP_DESC: + prompt += ' - ' + str(node.help).replace('\n', ' ').replace('\r', '') + + # {:3} sets the field width to three. Gives nice alignment for empty string + # values. + res = "{:3} {}".format(value_str(sc), prompt) + + # Don't print the name for unnamed choices (the normal kind) + if sc.name is not None: + res += " ({})".format(sc.name) + + return res + + +def print_menuconfig_nodes(node, indent): + """ + Prints a tree with all the menu entries rooted at 'node'. Child menu + entries are indented. + """ + while node: + string = node_str(node) + if string: + indent_print(string, indent) + + if node.list: + print_menuconfig_nodes(node.list, indent + 8) + + node = node.next + + +def print_menuconfig(kconf): + """ + Prints all menu entries for the configuration. + """ + # Print the expanded mainmenu text at the top. This is the same as + # kconf.top_node.prompt[0], but with variable references expanded. + print("\n======== {} ========\n".format(kconf.mainmenu_text)) + + print_menuconfig_nodes(kconf.top_node.list, 0) + print("") + + +if __name__ == "__main__": + + # Load Kconfig configuration files + kconf = Kconfig(sys.argv[1]) + + # Set default .config file or load it from argv + if len(sys.argv) == 2: + config_filename = '.config' + else: + config_filename = sys.argv[2] + + kconf.load_config(config_filename) + + # Print the configuration tree + print_menuconfig(kconf) diff --git a/support/kconfiglib/examples/print_sym_info.py b/support/kconfiglib/examples/print_sym_info.py new file mode 100644 index 0000000..ea6fc72 --- /dev/null +++ b/support/kconfiglib/examples/print_sym_info.py @@ -0,0 +1,54 @@ +# Loads a Kconfig and a .config and prints a symbol. +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/print_sym_info.py SCRIPT_ARG=<name> +# +# Example output for SCRIPT_ARG=MODULES: +# +# menuconfig MODULES +# bool +# prompt "Enable loadable module support" +# option modules +# help +# Kernel modules are small pieces of compiled code which can +# be inserted in the running kernel, rather than being +# permanently built into the kernel. You use the "modprobe" +# tool to add (and sometimes remove) them. If you say Y here, +# many parts of the kernel can be built as modules (by +# answering M instead of Y where indicated): this is most +# useful for infrequently used options which are not required +# for booting. For more information, see the man pages for +# modprobe, lsmod, modinfo, insmod and rmmod. +# +# If you say Y here, you will need to run "make +# modules_install" to put the modules under /lib/modules/ +# where modprobe can find them (you may need to be root to do +# this). +# +# If unsure, say Y. +# +# value = n +# visibility = y +# currently assignable values: n, y +# defined at init/Kconfig:1674 + +import sys + +from kconfiglib import Kconfig, TRI_TO_STR + + +if len(sys.argv) < 3: + sys.exit('Pass symbol name (without "CONFIG_" prefix) with SCRIPT_ARG=<name>') + +kconf = Kconfig(sys.argv[1]) +sym = kconf.syms[sys.argv[2]] + +print(sym) +print("value = " + sym.str_value) +print("visibility = " + TRI_TO_STR[sym.visibility]) +print("currently assignable values: " + + ", ".join([TRI_TO_STR[v] for v in sym.assignable])) + +for node in sym.nodes: + print("defined at {}:{}".format(node.filename, node.linenr)) diff --git a/support/kconfiglib/examples/print_tree.py b/support/kconfiglib/examples/print_tree.py new file mode 100644 index 0000000..49cb954 --- /dev/null +++ b/support/kconfiglib/examples/print_tree.py @@ -0,0 +1,75 @@ +# Prints the menu tree of the configuration. Dependencies between symbols can +# sometimes implicitly alter the menu structure (see kconfig-language.txt), and +# that's implemented too. +# +# Note: See the Kconfig.node_iter() function as well, which provides a simpler +# interface for walking the menu tree. +# +# Usage: +# +# $ make [ARCH=<arch>] scriptconfig SCRIPT=Kconfiglib/examples/print_tree.py +# +# Example output: +# +# ... +# config HAVE_KERNEL_LZO +# config HAVE_KERNEL_LZ4 +# choice +# config KERNEL_GZIP +# config KERNEL_BZIP2 +# config KERNEL_LZMA +# config KERNEL_XZ +# config KERNEL_LZO +# config KERNEL_LZ4 +# config DEFAULT_HOSTNAME +# config SWAP +# config SYSVIPC +# config SYSVIPC_SYSCTL +# config POSIX_MQUEUE +# config POSIX_MQUEUE_SYSCTL +# config CROSS_MEMORY_ATTACH +# config FHANDLE +# config USELIB +# config AUDIT +# config HAVE_ARCH_AUDITSYSCALL +# config AUDITSYSCALL +# config AUDIT_WATCH +# config AUDIT_TREE +# menu "IRQ subsystem" +# config MAY_HAVE_SPARSE_IRQ +# config GENERIC_IRQ_LEGACY +# config GENERIC_IRQ_PROBE +# ... + +import sys + +from kconfiglib import Kconfig, Symbol, Choice, MENU, COMMENT + + +def indent_print(s, indent): + print(indent*" " + s) + + +def print_items(node, indent): + while node: + if isinstance(node.item, Symbol): + indent_print("config " + node.item.name, indent) + + elif isinstance(node.item, Choice): + indent_print("choice", indent) + + elif node.item == MENU: + indent_print('menu "{}"'.format(node.prompt[0]), indent) + + elif node.item == COMMENT: + indent_print('comment "{}"'.format(node.prompt[0]), indent) + + + if node.list: + print_items(node.list, indent + 2) + + node = node.next + + +kconf = Kconfig(sys.argv[1]) +print_items(kconf.top_node, 0) diff --git a/support/kconfiglib/genconfig.py b/support/kconfiglib/genconfig.py new file mode 100755 index 0000000..bb3e6ff --- /dev/null +++ b/support/kconfiglib/genconfig.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Generates a header file with #defines from the configuration, matching the +format of include/generated/autoconf.h in the Linux kernel. + +Optionally, also writes the configuration output as a .config file. See +--config-out. + +The --sync-deps, --file-list, and --env-list options generate information that +can be used to avoid needless rebuilds/reconfigurations. + +Before writing a header or configuration file, Kconfiglib compares the old +contents of the file against the new contents. If there's no change, the write +is skipped. This avoids updating file metadata like the modification time, and +might save work depending on your build setup. + +By default, the configuration is generated from '.config'. A different +configuration file can be passed in the KCONFIG_CONFIG environment variable. +""" +import argparse +import os +import sys + +import kconfiglib + + +DEFAULT_HEADER_PATH = "config.h" +DEFAULT_SYNC_DEPS_PATH = "deps/" + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--header-path", + metavar="HEADER_FILE", + default=DEFAULT_HEADER_PATH, + help="Path for the generated header file (default: {})" + .format(DEFAULT_HEADER_PATH)) + + parser.add_argument( + "--config-out", + metavar="CONFIG_FILE", + help=""" +Write the configuration to CONFIG_FILE. This is useful if you include .config +files in Makefiles, as the generated configuration file will be a full .config +file even if .config is outdated. The generated configuration matches what +olddefconfig would produce. If you use sync-deps, you can include +deps/auto.conf instead. --config-out is meant for cases where incremental build +information isn't needed. +""") + + parser.add_argument( + "--sync-deps", + metavar="OUTPUT_DIR", + nargs="?", + const=DEFAULT_SYNC_DEPS_PATH, + help=""" +Enable generation of symbol dependency information for incremental builds, +optionally specifying the output directory (default: {}). See the docstring of +Kconfig.sync_deps() in Kconfiglib for more information. +""".format(DEFAULT_SYNC_DEPS_PATH)) + + parser.add_argument( + "--file-list", + metavar="OUTPUT_FILE", + help=""" +Write a list of all Kconfig files to OUTPUT_FILE, with one file per line. The +paths are relative to $srctree (or to the current directory if $srctree is +unset). Files appear in the order they're 'source'd. +""") + + parser.add_argument( + "--env-list", + metavar="OUTPUT_FILE", + help=""" +Write a list of all environment variables referenced in Kconfig files to +OUTPUT_FILE, with one variable per line. Each line has the format NAME=VALUE. +Only environment variables referenced with the preprocessor $(VAR) syntax are +included, and not variables referenced with the older $VAR syntax (which is +only supported for backwards compatibility). +""") + + parser.add_argument( + "kconfig_filename", + metavar="KCONFIG_FILENAME", + nargs="?", + default="Kconfig", + help="Top-level Kconfig file (default: Kconfig)") + + args = parser.parse_args() + + + kconf = kconfiglib.Kconfig(args.kconfig_filename) + kconf.load_config() + + kconf.write_autoconf(args.header_path) + + if args.config_out is not None: + kconf.write_config(args.config_out, save_old=False) + + if args.sync_deps is not None: + kconf.sync_deps(args.sync_deps) + + if args.file_list is not None: + with _open_write(args.file_list) as f: + for path in kconf.kconfig_filenames: + f.write(path + "\n") + + if args.env_list is not None: + with _open_write(args.env_list) as f: + for env_var in kconf.env_vars: + f.write("{}={}\n".format(env_var, os.environ[env_var])) + + +def _open_write(path): + # Python 2/3 compatibility. io.open() is available on both, but makes + # write() expect 'unicode' strings on Python 2. + + if sys.version_info[0] < 3: + return open(path, "w") + return open(path, "w", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/guiconfig.py b/support/kconfiglib/guiconfig.py new file mode 100755 index 0000000..050789a --- /dev/null +++ b/support/kconfiglib/guiconfig.py @@ -0,0 +1,2316 @@ +#!/usr/bin/env python + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Overview +======== + +A Tkinter-based menuconfig implementation, based around a treeview control and +a help display. The interface should feel familiar to people used to qconf +('make xconfig'). Compatible with both Python 2 and Python 3. + +The display can be toggled between showing the full tree and showing just a +single menu (like menuconfig.py). Only single-menu mode distinguishes between +symbols defined with 'config' and symbols defined with 'menuconfig'. + +A show-all mode is available that shows invisible items in red. + +Supports both mouse and keyboard controls. The following keyboard shortcuts are +available: + + Ctrl-S : Save configuration + Ctrl-O : Open configuration + Ctrl-A : Toggle show-all mode + Ctrl-N : Toggle show-name mode + Ctrl-M : Toggle single-menu mode + Ctrl-F, /: Open jump-to dialog + ESC : Close + +Running +======= + +guiconfig.py can be run either as a standalone executable or by calling the +menuconfig() function with an existing Kconfig instance. The second option is a +bit inflexible in that it will still load and save .config, etc. + +When run in standalone mode, the top-level Kconfig file to load can be passed +as a command-line argument. With no argument, it defaults to "Kconfig". + +The KCONFIG_CONFIG environment variable specifies the .config file to load (if +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). + +$srctree is supported through Kconfiglib. +""" + +# Note: There's some code duplication with menuconfig.py below, especially for +# the help text. Maybe some of it could be moved into kconfiglib.py or a shared +# helper script, but OTOH it's pretty nice to have things standalone and +# customizable. + +import errno +import os +import sys + +_PY2 = sys.version_info[0] < 3 + +if _PY2: + # Python 2 + from Tkinter import * + import ttk + import tkFont as font + import tkFileDialog as filedialog + import tkMessageBox as messagebox +else: + # Python 3 + from tkinter import * + import tkinter.ttk as ttk + import tkinter.font as font + from tkinter import filedialog, messagebox + +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ + BOOL, TRISTATE, STRING, INT, HEX, \ + AND, OR, \ + expr_str, expr_value, split_expr, \ + standard_sc_expr_str, \ + TRI_TO_STR, TYPE_TO_STR, \ + standard_kconfig, standard_config_filename + + +# If True, use GIF image data embedded in this file instead of separate GIF +# files. See _load_images(). +_USE_EMBEDDED_IMAGES = True + + +# Help text for the jump-to dialog +_JUMP_TO_HELP = """\ +Type one or more strings/regexes and press Enter to list items that match all +of them. Python's regex flavor is used (see the 're' module). Double-clicking +an item will jump to it. Item values can be toggled directly within the dialog.\ +""" + + +def _main(): + menuconfig(standard_kconfig()) + + +# Global variables used below: +# +# _root: +# The Toplevel instance for the main window +# +# _tree: +# The Treeview in the main window +# +# _jump_to_tree: +# The Treeview in the jump-to dialog. None if the jump-to dialog isn't +# open. Doubles as a flag. +# +# _jump_to_matches: +# List of Nodes shown in the jump-to dialog +# +# _menupath: +# The Label that shows the menu path of the selected item +# +# _backbutton: +# The button shown in single-menu mode for jumping to the parent menu +# +# _status_label: +# Label with status text shown at the bottom of the main window +# ("Modified", "Saved to ...", etc.) +# +# _id_to_node: +# We can't use Node objects directly as Treeview item IDs, so we use their +# id()s instead. This dictionary maps Node id()s back to Nodes. (The keys +# are actually str(id(node)), just to simplify lookups.) +# +# _cur_menu: +# The current menu. Ignored outside single-menu mode. +# +# _show_all_var/_show_name_var/_single_menu_var: +# Tkinter Variable instances bound to the corresponding checkboxes +# +# _show_all/_single_menu: +# Plain Python bools that track _show_all_var and _single_menu_var, to +# speed up and simplify things a bit +# +# _conf_filename: +# File to save the configuration to +# +# _minconf_filename: +# File to save minimal configurations to +# +# _conf_changed: +# True if the configuration has been changed. If False, we don't bother +# showing the save-and-quit dialog. +# +# We reset this to False whenever the configuration is saved. +# +# _*_img: +# PhotoImage instances for images + + +def menuconfig(kconf): + """ + Launches the configuration interface, returning after the user exits. + + kconf: + Kconfig instance to be configured + """ + global _kconf + global _conf_filename + global _minconf_filename + global _jump_to_tree + global _cur_menu + + _kconf = kconf + + _jump_to_tree = None + + _create_id_to_node() + + _create_ui() + + # Filename to save configuration to + _conf_filename = standard_config_filename() + + # Load existing configuration and check if it's outdated + _set_conf_changed(_load_config()) + + # Filename to save minimal configuration to + _minconf_filename = "defconfig" + + # Current menu in single-menu mode + _cur_menu = _kconf.top_node + + # Any visible items in the top menu? + if not _shown_menu_nodes(kconf.top_node): + # Nothing visible. Start in show-all mode and try again. + _show_all_var.set(True) + if not _shown_menu_nodes(kconf.top_node): + # Give up and show an error. It's nice to be able to assume that + # the tree is non-empty in the rest of the code. + _root.wait_visibility() + messagebox.showerror( + "Error", + "Empty configuration -- nothing to configure.\n\n" + "Check that environment variables are set properly.") + _root.destroy() + return + + # Build the initial tree + _update_tree() + + # Select the first item and focus the Treeview, so that keyboard controls + # work immediately + _select(_tree, _tree.get_children()[0]) + _tree.focus_set() + + # Make geometry information available for centering the window. This + # indirectly creates the window, so hide it so that it's never shown at the + # old location. + _root.withdraw() + _root.update_idletasks() + + # Center the window + _root.geometry("+{}+{}".format( + (_root.winfo_screenwidth() - _root.winfo_reqwidth())//2, + (_root.winfo_screenheight() - _root.winfo_reqheight())//2)) + + # Show it + _root.deiconify() + + # Prevent the window from being automatically resized. Otherwise, it + # changes size when scrollbars appear/disappear before the user has + # manually resized it. + _root.geometry(_root.geometry()) + + _root.mainloop() + + +def _load_config(): + # Loads any existing .config file. See the Kconfig.load_config() docstring. + # + # Returns True if .config is missing or outdated. We always prompt for + # saving the configuration in that case. + + print(_kconf.load_config()) + if not os.path.exists(_conf_filename): + # No .config + return True + + return _needs_save() + + +def _needs_save(): + # Returns True if a just-loaded .config file is outdated (would get + # modified when saving) + + if _kconf.missing_syms: + # Assignments to undefined symbols in the .config + return True + + for sym in _kconf.unique_defined_syms: + if sym.user_value is None: + if sym.config_string: + # Unwritten symbol + return True + elif sym.orig_type in (BOOL, TRISTATE): + if sym.tri_value != sym.user_value: + # Written bool/tristate symbol, new value + return True + elif sym.str_value != sym.user_value: + # Written string/int/hex symbol, new value + return True + + # No need to prompt for save + return False + + +def _create_id_to_node(): + global _id_to_node + + _id_to_node = {str(id(node)): node for node in _kconf.node_iter()} + + +def _create_ui(): + # Creates the main window UI + + global _root + global _tree + + # Create the root window. This initializes Tkinter and makes e.g. + # PhotoImage available, so do it early. + _root = Tk() + + _load_images() + _init_misc_ui() + _fix_treeview_issues() + + _create_top_widgets() + # Create the pane with the Kconfig tree and description text + panedwindow, _tree = _create_kconfig_tree_and_desc(_root) + panedwindow.grid(column=0, row=1, sticky="nsew") + _create_status_bar() + + _root.columnconfigure(0, weight=1) + # Only the pane with the Kconfig tree and description grows vertically + _root.rowconfigure(1, weight=1) + + # Start with show-name disabled + _do_showname() + + _tree.bind("<Left>", _tree_left_key) + _tree.bind("<Right>", _tree_right_key) + # Note: Binding this for the jump-to tree as well would cause issues due to + # the Tk bug mentioned in _tree_open() + _tree.bind("<<TreeviewOpen>>", _tree_open) + # add=True to avoid overriding the description text update + _tree.bind("<<TreeviewSelect>>", _update_menu_path, add=True) + + _root.bind("<Control-s>", _save) + _root.bind("<Control-o>", _open) + _root.bind("<Control-a>", _toggle_showall) + _root.bind("<Control-n>", _toggle_showname) + _root.bind("<Control-m>", _toggle_tree_mode) + _root.bind("<Control-f>", _jump_to_dialog) + _root.bind("/", _jump_to_dialog) + _root.bind("<Escape>", _on_quit) + + +def _load_images(): + # Loads GIF images, creating the global _*_img PhotoImage variables. + # Base64-encoded images embedded in this script are used if + # _USE_EMBEDDED_IMAGES is True, and separate image files in the same + # directory as the script otherwise. + # + # Using a global variable indirectly prevents the image from being + # garbage-collected. Passing an image to a Tkinter function isn't enough to + # keep it alive. + + def load_image(name, data): + var_name = "_{}_img".format(name) + + if _USE_EMBEDDED_IMAGES: + globals()[var_name] = PhotoImage(data=data, format="gif") + else: + globals()[var_name] = PhotoImage( + file=os.path.join(os.path.dirname(__file__), name + ".gif"), + format="gif") + + # Note: Base64 data can be put on the clipboard with + # $ base64 -w0 foo.gif | xclip + + load_image("icon", "R0lGODlhMAAwAPEDAAAAAADQAO7u7v///yH5BAUKAAMALAAAAAAwADAAAAL/nI+gy+2Pokyv2jazuZxryQjiSJZmyXxHeLbumH6sEATvW8OLNtf5bfLZRLFITzgEipDJ4mYxYv6A0ubuqYhWk66tVTE4enHer7jcKvt0LLUw6P45lvEprT6c0+v7OBuqhYdHohcoqIbSAHc4ljhDwrh1UlgSydRCWWlp5wiYZvmSuSh4IzrqV6p4cwhkCsmY+nhK6uJ6t1mrOhuJqfu6+WYiCiwl7HtLjNSZZZis/MeM7NY3TaRKS40ooDeoiVqIultsrav92bi9c3a5KkkOsOJZpSS99m4k/0zPng4Gks9JSbB+8DIcoQfnjwpZCHv5W+ip4aQrKrB0uOikYhiMCBw1/uPoQUMBADs=") + load_image("n_bool", "R0lGODdhEAAQAPAAAAgICP///ywAAAAAEAAQAAACIISPacHtvp5kcb5qG85hZ2+BkyiRF8BBaEqtrKkqslEAADs=") + load_image("y_bool", "R0lGODdhEAAQAPEAAAgICADQAP///wAAACwAAAAAEAAQAAACMoSPacLtvlh4YrIYsst2cV19AvaVF9CUXBNJJoum7ymrsKuCnhiupIWjSSjAFuWhSCIKADs=") + load_image("n_tri", "R0lGODlhEAAQAPD/AAEBAf///yH5BAUKAAIALAAAAAAQABAAAAInlI+pBrAKQnCPSUlXvFhznlkfeGwjKZhnJ65h6nrfi6h0st2QXikFADs=") + load_image("m_tri", "R0lGODlhEAAQAPEDAAEBAeQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nI+pBrAWAhPCjYhiAJQCnWmdoElHGVBoiK5M21ofXFpXRIrgiecqxkuNciZIhNOZFRNI24PhfEoLADs=") + load_image("y_tri", "R0lGODlhEAAQAPEDAAICAgDQAP///wAAACH5BAUKAAMALAAAAAAQABAAAAI0nI+pBrAYBhDCRRUypfmergmgZ4xjMpmaw2zmxk7cCB+pWiVqp4MzDwn9FhGZ5WFjIZeGAgA7") + load_image("m_my", "R0lGODlhEAAQAPEDAAAAAOQMuv///wAAACH5BAUKAAMALAAAAAAQABAAAAI5nIGpxiAPI2ghxFinq/ZygQhc94zgZopmOLYf67anGr+oZdp02emfV5n9MEHN5QhqICETxkABbQ4KADs=") + load_image("y_my", "R0lGODlhEAAQAPH/AAAAAADQAAPRA////yH5BAUKAAQALAAAAAAQABAAAAM+SArcrhCMSSuIM9Q8rxxBWIXawIBkmWonupLd565Um9G1PIs59fKmzw8WnAlusBYR2SEIN6DmAmqBLBxYSAIAOw==") + load_image("n_locked", "R0lGODlhEAAQAPABAAAAAP///yH5BAUKAAEALAAAAAAQABAAAAIgjB8AyKwN04pu0vMutpqqz4Hih4ydlnUpyl2r23pxUAAAOw==") + load_image("m_locked", "R0lGODlhEAAQAPD/AAAAAOQMuiH5BAUKAAIALAAAAAAQABAAAAIylC8AyKwN04ohnGcqqlZmfXDWI26iInZoyiore05walolV39ftxsYHgL9QBBMBGFEFAAAOw==") + load_image("y_locked", "R0lGODlhEAAQAPD/AAAAAADQACH5BAUKAAIALAAAAAAQABAAAAIylC8AyKzNgnlCtoDTwvZwrHydIYpQmR3KWq4uK74IOnp0HQPmnD3cOVlUIAgKsShkFAAAOw==") + load_image("not_selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIrlA2px6IBw2IpWglOvTYhzmUbGD3kNZ5QqrKn2YrqigCxZoMelU6No9gdCgA7") + load_image("selected", "R0lGODlhEAAQAPD/AAAAAP///yH5BAUKAAIALAAAAAAQABAAAAIzlA2px6IBw2IpWglOvTah/kTZhimASJomiqonlLov1qptHTsgKSEzh9H8QI0QzNPwmRoFADs=") + load_image("edit", "R0lGODlhEAAQAPIFAAAAAKOLAMuuEPvXCvrxvgAAAAAAAAAAACH5BAUKAAUALAAAAAAQABAAAANCWLqw/gqMBp8cszJxcwVC2FEOEIAi5kVBi3IqWZhuCGMyfdpj2e4pnK+WAshmvxeAcETWlsxPkkBtsqBMa8TIBSQAADs=") + + +def _fix_treeview_issues(): + # Fixes some Treeview issues + + global _treeview_rowheight + + style = ttk.Style() + + # The treeview rowheight isn't adjusted automatically on high-DPI displays, + # so do it ourselves. The font will probably always be TkDefaultFont, but + # play it safe and look it up. + + _treeview_rowheight = font.Font(font=style.lookup("Treeview", "font")) \ + .metrics("linespace") + 2 + + style.configure("Treeview", rowheight=_treeview_rowheight) + + # Work around regression in https://core.tcl.tk/tk/tktview?name=509cafafae, + # which breaks tag background colors + + for option in "foreground", "background": + # Filter out any styles starting with ("!disabled", "!selected", ...). + # style.map() returns an empty list for missing options, so this should + # be future-safe. + style.map( + "Treeview", + **{option: [elm for elm in style.map("Treeview", query_opt=option) + if elm[:2] != ("!disabled", "!selected")]}) + + +def _init_misc_ui(): + # Does misc. UI initialization, like setting the title, icon, and theme + + _root.title(_kconf.mainmenu_text) + # iconphoto() isn't available in Python 2's Tkinter + _root.tk.call("wm", "iconphoto", _root._w, "-default", _icon_img) + # Reducing the width of the window to 1 pixel makes it move around, at + # least on GNOME. Prevent weird stuff like that. + _root.minsize(128, 128) + _root.protocol("WM_DELETE_WINDOW", _on_quit) + + # Use the 'clam' theme on *nix if it's available. It looks nicer than the + # 'default' theme. + if _root.tk.call("tk", "windowingsystem") == "x11": + style = ttk.Style() + if "clam" in style.theme_names(): + style.theme_use("clam") + + +def _create_top_widgets(): + # Creates the controls above the Kconfig tree in the main window + + global _show_all_var + global _show_name_var + global _single_menu_var + global _menupath + global _backbutton + + topframe = ttk.Frame(_root) + topframe.grid(column=0, row=0, sticky="ew") + + ttk.Button(topframe, text="Save", command=_save) \ + .grid(column=0, row=0, sticky="ew", padx=".05c", pady=".05c") + + ttk.Button(topframe, text="Save as...", command=_save_as) \ + .grid(column=1, row=0, sticky="ew") + + ttk.Button(topframe, text="Save minimal (advanced)...", + command=_save_minimal) \ + .grid(column=2, row=0, sticky="ew", padx=".05c") + + ttk.Button(topframe, text="Open...", command=_open) \ + .grid(column=3, row=0) + + ttk.Button(topframe, text="Jump to...", command=_jump_to_dialog) \ + .grid(column=4, row=0, padx=".05c") + + _show_name_var = BooleanVar() + ttk.Checkbutton(topframe, text="Show name", command=_do_showname, + variable=_show_name_var) \ + .grid(column=0, row=1, sticky="nsew", padx=".05c", pady="0 .05c", + ipady=".2c") + + _show_all_var = BooleanVar() + ttk.Checkbutton(topframe, text="Show all", command=_do_showall, + variable=_show_all_var) \ + .grid(column=1, row=1, sticky="nsew", pady="0 .05c") + + # Allow the show-all and single-menu status to be queried via plain global + # Python variables, which is faster and simpler + + def show_all_updated(*_): + global _show_all + _show_all = _show_all_var.get() + + _trace_write(_show_all_var, show_all_updated) + _show_all_var.set(False) + + _single_menu_var = BooleanVar() + ttk.Checkbutton(topframe, text="Single-menu mode", command=_do_tree_mode, + variable=_single_menu_var) \ + .grid(column=2, row=1, sticky="nsew", padx=".05c", pady="0 .05c") + + _backbutton = ttk.Button(topframe, text="<--", command=_leave_menu, + state="disabled") + _backbutton.grid(column=0, row=4, sticky="nsew", padx=".05c", pady="0 .05c") + + def tree_mode_updated(*_): + global _single_menu + _single_menu = _single_menu_var.get() + + if _single_menu: + _backbutton.grid() + else: + _backbutton.grid_remove() + + _trace_write(_single_menu_var, tree_mode_updated) + _single_menu_var.set(False) + + # Column to the right of the buttons that the menu path extends into, so + # that it can grow wider than the buttons + topframe.columnconfigure(5, weight=1) + + _menupath = ttk.Label(topframe) + _menupath.grid(column=0, row=3, columnspan=6, sticky="w", padx="0.05c", + pady="0 .05c") + + +def _create_kconfig_tree_and_desc(parent): + # Creates a Panedwindow with a Treeview that shows Kconfig nodes and a Text + # that shows a description of the selected node. Returns a tuple with the + # Panedwindow and the Treeview. This code is shared between the main window + # and the jump-to dialog. + + panedwindow = ttk.Panedwindow(parent, orient=VERTICAL) + + tree_frame, tree = _create_kconfig_tree(panedwindow) + desc_frame, desc = _create_kconfig_desc(panedwindow) + + panedwindow.add(tree_frame, weight=1) + panedwindow.add(desc_frame) + + def tree_select(_): + # The Text widget does not allow editing the text in its disabled + # state. We need to temporarily enable it. + desc["state"] = "normal" + + sel = tree.selection() + if not sel: + desc.delete("1.0", "end") + desc["state"] = "disabled" + return + + # Text.replace() is not available in Python 2's Tkinter + desc.delete("1.0", "end") + desc.insert("end", _info_str(_id_to_node[sel[0]])) + + desc["state"] = "disabled" + + tree.bind("<<TreeviewSelect>>", tree_select) + tree.bind("<1>", _tree_click) + tree.bind("<Double-1>", _tree_double_click) + tree.bind("<Return>", _tree_enter) + tree.bind("<KP_Enter>", _tree_enter) + tree.bind("<space>", _tree_toggle) + tree.bind("n", _tree_set_val(0)) + tree.bind("m", _tree_set_val(1)) + tree.bind("y", _tree_set_val(2)) + + return panedwindow, tree + + +def _create_kconfig_tree(parent): + # Creates a Treeview for showing Kconfig nodes + + frame = ttk.Frame(parent) + + tree = ttk.Treeview(frame, selectmode="browse", height=20, + columns=("name",)) + tree.heading("#0", text="Option", anchor="w") + tree.heading("name", text="Name", anchor="w") + + tree.tag_configure("n-bool", image=_n_bool_img) + tree.tag_configure("y-bool", image=_y_bool_img) + tree.tag_configure("m-tri", image=_m_tri_img) + tree.tag_configure("n-tri", image=_n_tri_img) + tree.tag_configure("m-tri", image=_m_tri_img) + tree.tag_configure("y-tri", image=_y_tri_img) + tree.tag_configure("m-my", image=_m_my_img) + tree.tag_configure("y-my", image=_y_my_img) + tree.tag_configure("n-locked", image=_n_locked_img) + tree.tag_configure("m-locked", image=_m_locked_img) + tree.tag_configure("y-locked", image=_y_locked_img) + tree.tag_configure("not-selected", image=_not_selected_img) + tree.tag_configure("selected", image=_selected_img) + tree.tag_configure("edit", image=_edit_img) + tree.tag_configure("invisible", foreground="red") + + tree.grid(column=0, row=0, sticky="nsew") + + _add_vscrollbar(frame, tree) + + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + # Create items for all menu nodes. These can be detached/moved later. + # Micro-optimize this a bit. + insert = tree.insert + id_ = id + Symbol_ = Symbol + for node in _kconf.node_iter(): + item = node.item + insert("", "end", iid=id_(node), + values=item.name if item.__class__ is Symbol_ else "") + + return frame, tree + + +def _create_kconfig_desc(parent): + # Creates a Text for showing the description of the selected Kconfig node + + frame = ttk.Frame(parent) + + desc = Text(frame, height=12, wrap="none", borderwidth=0, + state="disabled") + desc.grid(column=0, row=0, sticky="nsew") + + # Work around not being to Ctrl-C/V text from a disabled Text widget, with a + # tip found in https://stackoverflow.com/questions/3842155/is-there-a-way-to-make-the-tkinter-text-widget-read-only + desc.bind("<1>", lambda _: desc.focus_set()) + + _add_vscrollbar(frame, desc) + + frame.columnconfigure(0, weight=1) + frame.rowconfigure(0, weight=1) + + return frame, desc + + +def _add_vscrollbar(parent, widget): + # Adds a vertical scrollbar to 'widget' that's only shown as needed + + vscrollbar = ttk.Scrollbar(parent, orient="vertical", + command=widget.yview) + vscrollbar.grid(column=1, row=0, sticky="ns") + + def yscrollcommand(first, last): + # Only show the scrollbar when needed. 'first' and 'last' are + # strings. + if float(first) <= 0.0 and float(last) >= 1.0: + vscrollbar.grid_remove() + else: + vscrollbar.grid() + + vscrollbar.set(first, last) + + widget["yscrollcommand"] = yscrollcommand + + +def _create_status_bar(): + # Creates the status bar at the bottom of the main window + + global _status_label + + _status_label = ttk.Label(_root, anchor="e", padding="0 0 0.4c 0") + _status_label.grid(column=0, row=3, sticky="ew") + + +def _set_status(s): + # Sets the text in the status bar to 's' + + _status_label["text"] = s + + +def _set_conf_changed(changed): + # Updates the status re. whether there are unsaved changes + + global _conf_changed + + _conf_changed = changed + if changed: + _set_status("Modified") + + +def _update_tree(): + # Updates the Kconfig tree in the main window by first detaching all nodes + # and then updating and reattaching them. The tree structure might have + # changed. + + # If a selected/focused item is detached and later reattached, it stays + # selected/focused. That can give multiple selections even though + # selectmode=browse. Save and later restore the selection and focus as a + # workaround. + old_selection = _tree.selection() + old_focus = _tree.focus() + + # Detach all tree items before re-stringing them. This is relatively fast, + # luckily. + _tree.detach(*_id_to_node.keys()) + + if _single_menu: + _build_menu_tree() + else: + _build_full_tree(_kconf.top_node) + + _tree.selection_set(old_selection) + _tree.focus(old_focus) + + +def _build_full_tree(menu): + # Updates the tree starting from menu.list, in full-tree mode. To speed + # things up, only open menus are updated. The menu-at-a-time logic here is + # to deal with invisible items that can show up outside show-all mode (see + # _shown_full_nodes()). + + for node in _shown_full_nodes(menu): + _add_to_tree(node, _kconf.top_node) + + # _shown_full_nodes() includes nodes from menus rooted at symbols, so + # we only need to check "real" menus/choices here + if node.list and not isinstance(node.item, Symbol): + if _tree.item(id(node), "open"): + _build_full_tree(node) + else: + # We're just probing here, so _shown_menu_nodes() will work + # fine, and might be a bit faster + shown = _shown_menu_nodes(node) + if shown: + # Dummy element to make the open/closed toggle appear + _tree.move(id(shown[0]), id(shown[0].parent), "end") + + +def _shown_full_nodes(menu): + # Returns the list of menu nodes shown in 'menu' (a menu node for a menu) + # for full-tree mode. A tricky detail is that invisible items need to be + # shown if they have visible children. + + def rec(node): + res = [] + + while node: + if _visible(node) or _show_all: + res.append(node) + if node.list and isinstance(node.item, Symbol): + # Nodes from menu created from dependencies + res += rec(node.list) + + elif node.list and isinstance(node.item, Symbol): + # Show invisible symbols (defined with either 'config' and + # 'menuconfig') if they have visible children. This can happen + # for an m/y-valued symbol with an optional prompt + # ('prompt "foo" is COND') that is currently disabled. + shown_children = rec(node.list) + if shown_children: + res.append(node) + res += shown_children + + node = node.next + + return res + + return rec(menu.list) + + +def _build_menu_tree(): + # Updates the tree in single-menu mode. See _build_full_tree() as well. + + for node in _shown_menu_nodes(_cur_menu): + _add_to_tree(node, _cur_menu) + + +def _shown_menu_nodes(menu): + # Used for single-menu mode. Similar to _shown_full_nodes(), but doesn't + # include children of symbols defined with 'menuconfig'. + + def rec(node): + res = [] + + while node: + if _visible(node) or _show_all: + res.append(node) + if node.list and not node.is_menuconfig: + res += rec(node.list) + + elif node.list and isinstance(node.item, Symbol): + shown_children = rec(node.list) + if shown_children: + # Invisible item with visible children + res.append(node) + if not node.is_menuconfig: + res += shown_children + + node = node.next + + return res + + return rec(menu.list) + + +def _visible(node): + # Returns True if the node should appear in the menu (outside show-all + # mode) + + return node.prompt and expr_value(node.prompt[1]) and not \ + (node.item == MENU and not expr_value(node.visibility)) + + +def _add_to_tree(node, top): + # Adds 'node' to the tree, at the end of its menu. We rely on going through + # the nodes linearly to get the correct order. 'top' holds the menu that + # corresponds to the top-level menu, and can vary in single-menu mode. + + parent = node.parent + _tree.move(id(node), "" if parent is top else id(parent), "end") + _tree.item( + id(node), + text=_node_str(node), + # The _show_all test avoids showing invisible items in red outside + # show-all mode, which could look confusing/broken. Invisible symbols + # are shown outside show-all mode if an invisible symbol has visible + # children in an implicit menu. + tags=_img_tag(node) if _visible(node) or not _show_all else + _img_tag(node) + " invisible") + + +def _node_str(node): + # Returns the string shown to the right of the image (if any) for the node + + if node.prompt: + if node.item == COMMENT: + s = "*** {} ***".format(node.prompt[0]) + else: + s = node.prompt[0] + + if isinstance(node.item, Symbol): + sym = node.item + + # Print "(NEW)" next to symbols without a user value (from e.g. a + # .config), but skip it for choice symbols in choices in y mode, + # and for symbols of UNKNOWN type (which generate a warning though) + if sym.user_value is None and sym.type and not \ + (sym.choice and sym.choice.tri_value == 2): + + s += " (NEW)" + + elif isinstance(node.item, Symbol): + # Symbol without prompt (can show up in show-all) + s = "<{}>".format(node.item.name) + + else: + # Choice without prompt. Use standard_sc_expr_str() so that it shows up + # as '<choice (name if any)>'. + s = standard_sc_expr_str(node.item) + + + if isinstance(node.item, Symbol): + sym = node.item + if sym.orig_type == STRING: + s += ": " + sym.str_value + elif sym.orig_type in (INT, HEX): + s = "({}) {}".format(sym.str_value, s) + + elif isinstance(node.item, Choice) and node.item.tri_value == 2: + # Print the prompt of the selected symbol after the choice for + # choices in y mode + sym = node.item.selection + if sym: + for sym_node in sym.nodes: + # Use the prompt used at this choice location, in case the + # choice symbol is defined in multiple locations + if sym_node.parent is node and sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + else: + # If the symbol isn't defined at this choice location, then + # just use whatever prompt we can find for it + for sym_node in sym.nodes: + if sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + + # In single-menu mode, print "--->" next to nodes that have menus that can + # potentially be entered. Print "----" if the menu is empty. We don't allow + # those to be entered. + if _single_menu and node.is_menuconfig: + s += " --->" if _shown_menu_nodes(node) else " ----" + + return s + + +def _img_tag(node): + # Returns the tag for the image that should be shown next to 'node', or the + # empty string if it shouldn't have an image + + item = node.item + + if item in (MENU, COMMENT) or not item.orig_type: + return "" + + if item.orig_type in (STRING, INT, HEX): + return "edit" + + # BOOL or TRISTATE + + if _is_y_mode_choice_sym(item): + # Choice symbol in y-mode choice + return "selected" if item.choice.selection is item else "not-selected" + + if len(item.assignable) <= 1: + # Pinned to a single value + return "" if isinstance(item, Choice) else item.str_value + "-locked" + + if item.type == BOOL: + return item.str_value + "-bool" + + # item.type == TRISTATE + if item.assignable == (1, 2): + return item.str_value + "-my" + return item.str_value + "-tri" + + +def _is_y_mode_choice_sym(item): + # The choice mode is an upper bound on the visibility of choice symbols, so + # we can check the choice symbols' own visibility to see if the choice is + # in y mode + return isinstance(item, Symbol) and item.choice and item.visibility == 2 + + +def _tree_click(event): + # Click on the Kconfig Treeview + + tree = event.widget + if tree.identify_element(event.x, event.y) == "image": + item = tree.identify_row(event.y) + # Select the item before possibly popping up a dialog for + # string/int/hex items, so that its help is visible + _select(tree, item) + _change_node(_id_to_node[item], tree.winfo_toplevel()) + return "break" + + +def _tree_double_click(event): + # Double-click on the Kconfig treeview + + # Do an extra check to avoid weirdness when double-clicking in the tree + # heading area + if not _in_heading(event): + return _tree_enter(event) + + +def _in_heading(event): + # Returns True if 'event' took place in the tree heading + + tree = event.widget + return hasattr(tree, "identify_region") and \ + tree.identify_region(event.x, event.y) in ("heading", "separator") + + +def _tree_enter(event): + # Enter press or double-click within the Kconfig treeview. Prefer to + # open/close/enter menus, but toggle the value if that's not possible. + + tree = event.widget + sel = tree.focus() + if sel: + node = _id_to_node[sel] + + if tree.get_children(sel): + _tree_toggle_open(sel) + elif _single_menu_mode_menu(node, tree): + _enter_menu_and_select_first(node) + else: + _change_node(node, tree.winfo_toplevel()) + + return "break" + + +def _tree_toggle(event): + # Space press within the Kconfig treeview. Prefer to toggle the value, but + # open/close/enter the menu if that's not possible. + + tree = event.widget + sel = tree.focus() + if sel: + node = _id_to_node[sel] + + if _changeable(node): + _change_node(node, tree.winfo_toplevel()) + elif _single_menu_mode_menu(node, tree): + _enter_menu_and_select_first(node) + elif tree.get_children(sel): + _tree_toggle_open(sel) + + return "break" + + +def _tree_left_key(_): + # Left arrow key press within the Kconfig treeview + + if _single_menu: + # Leave the current menu in single-menu mode + _leave_menu() + return "break" + + # Otherwise, default action + + +def _tree_right_key(_): + # Right arrow key press within the Kconfig treeview + + sel = _tree.focus() + if sel: + node = _id_to_node[sel] + # If the node can be entered in single-menu mode, do it + if _single_menu_mode_menu(node, _tree): + _enter_menu_and_select_first(node) + return "break" + + # Otherwise, default action + + +def _single_menu_mode_menu(node, tree): + # Returns True if single-menu mode is on and 'node' is an (interface) + # menu that can be entered + + return _single_menu and tree is _tree and node.is_menuconfig and \ + _shown_menu_nodes(node) + + +def _changeable(node): + # Returns True if 'node' is a Symbol/Choice whose value can be changed + + sc = node.item + + if not isinstance(sc, (Symbol, Choice)): + return False + + # This will hit for invisible symbols, which appear in show-all mode and + # when an invisible symbol has visible children (which can happen e.g. for + # symbols with optional prompts) + if not (node.prompt and expr_value(node.prompt[1])): + return False + + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ + or _is_y_mode_choice_sym(sc) + + +def _tree_toggle_open(item): + # Opens/closes the Treeview item 'item' + + if _tree.item(item, "open"): + _tree.item(item, open=False) + else: + node = _id_to_node[item] + if not isinstance(node.item, Symbol): + # Can only get here in full-tree mode + _build_full_tree(node) + _tree.item(item, open=True) + + +def _tree_set_val(tri_val): + def tree_set_val(event): + # n/m/y press within the Kconfig treeview + + # Sets the value of the currently selected item to 'tri_val', if that + # value can be assigned + + sel = event.widget.focus() + if sel: + sc = _id_to_node[sel].item + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: + _set_val(sc, tri_val) + + return tree_set_val + + +def _tree_open(_): + # Lazily populates the Kconfig tree when menus are opened in full-tree mode + + if _single_menu: + # Work around https://core.tcl.tk/tk/tktview?name=368fa4561e + # ("ttk::treeview open/closed indicators can be toggled while hidden"). + # Clicking on the hidden indicator will call _build_full_tree() in + # single-menu mode otherwise. + return + + node = _id_to_node[_tree.focus()] + # _shown_full_nodes() includes nodes from menus rooted at symbols, so we + # only need to check "real" menus and choices here + if not isinstance(node.item, Symbol): + _build_full_tree(node) + + +def _update_menu_path(_): + # Updates the displayed menu path when nodes are selected in the Kconfig + # treeview + + sel = _tree.selection() + _menupath["text"] = _menu_path_info(_id_to_node[sel[0]]) if sel else "" + + +def _item_row(item): + # Returns the row number 'item' appears on within the Kconfig treeview, + # starting from the top of the tree. Used to preserve scrolling. + # + # ttkTreeview.c in the Tk sources defines a RowNumber() function that does + # the same thing, but it's not exposed. + + row = 0 + + while True: + prev = _tree.prev(item) + if prev: + item = prev + row += _n_rows(item) + else: + item = _tree.parent(item) + if not item: + return row + row += 1 + + +def _n_rows(item): + # _item_row() helper. Returns the number of rows occupied by 'item' and # + # its children. + + rows = 1 + + if _tree.item(item, "open"): + for child in _tree.get_children(item): + rows += _n_rows(child) + + return rows + + +def _attached(item): + # Heuristic for checking if a Treeview item is attached. Doesn't seem to be + # good APIs for this. Might fail for super-obscure cases with tiny trees, + # but you'd just get a small scroll mess-up. + + return bool(_tree.next(item) or _tree.prev(item) or _tree.parent(item)) + + +def _change_node(node, parent): + # Toggles/changes the value of 'node'. 'parent' is the parent window + # (either the main window or the jump-to dialog), in case we need to pop up + # a dialog. + + if not _changeable(node): + return + + # sc = symbol/choice + sc = node.item + + if sc.type in (INT, HEX, STRING): + s = _set_val_dialog(node, parent) + + # Tkinter can return 'unicode' strings on Python 2, which Kconfiglib + # can't deal with. UTF-8-encode the string to work around it. + if _PY2 and isinstance(s, unicode): + s = s.encode("utf-8", "ignore") + + if s is not None: + _set_val(sc, s) + + elif len(sc.assignable) == 1: + # Handles choice symbols for choices in y mode, which are a special + # case: .assignable can be (2,) while .tri_value is 0. + _set_val(sc, sc.assignable[0]) + + else: + # Set the symbol to the value after the current value in + # sc.assignable, with wrapping + val_index = sc.assignable.index(sc.tri_value) + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) + + +def _set_val(sc, val): + # Wrapper around Symbol/Choice.set_value() for updating the menu state and + # _conf_changed + + # Use the string representation of tristate values. This makes the format + # consistent for all symbol types. + if val in TRI_TO_STR: + val = TRI_TO_STR[val] + + if val != sc.str_value: + sc.set_value(val) + _set_conf_changed(True) + + # Update the tree and try to preserve the scroll. Do a cheaper variant + # than in the show-all case, that might mess up the scroll slightly in + # rare cases, but is fast and flicker-free. + + stayput = _loc_ref_item() # Item to preserve scroll for + old_row = _item_row(stayput) + + _update_tree() + + # If the reference item disappeared (can happen if the change was done + # from the jump-to dialog), then avoid messing with the scroll and hope + # for the best + if _attached(stayput): + _tree.yview_scroll(_item_row(stayput) - old_row, "units") + + if _jump_to_tree: + _update_jump_to_display() + + +def _set_val_dialog(node, parent): + # Pops up a dialog for setting the value of the string/int/hex + # symbol at node 'node'. 'parent' is the parent window. + + def ok(_=None): + # No 'nonlocal' in Python 2 + global _entry_res + + s = entry.get() + if sym.type == HEX and not s.startswith(("0x", "0X")): + s = "0x" + s + + if _check_valid(dialog, entry, sym, s): + _entry_res = s + dialog.destroy() + + def cancel(_=None): + global _entry_res + _entry_res = None + dialog.destroy() + + sym = node.item + + dialog = Toplevel(parent) + dialog.title("Enter {} value".format(TYPE_TO_STR[sym.type])) + dialog.resizable(False, False) + dialog.transient(parent) + dialog.protocol("WM_DELETE_WINDOW", cancel) + + ttk.Label(dialog, text=node.prompt[0] + ":") \ + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".3c", + pady=".2c .05c") + + entry = ttk.Entry(dialog, width=30) + # Start with the previous value in the editbox, selected + entry.insert(0, sym.str_value) + entry.selection_range(0, "end") + entry.grid(column=0, row=1, columnspan=2, sticky="ew", padx=".3c") + entry.focus_set() + + range_info = _range_info(sym) + if range_info: + ttk.Label(dialog, text=range_info) \ + .grid(column=0, row=2, columnspan=2, sticky="w", padx=".3c", + pady=".2c 0") + + ttk.Button(dialog, text="OK", command=ok) \ + .grid(column=0, row=4 if range_info else 3, sticky="e", padx=".3c", + pady=".4c") + + ttk.Button(dialog, text="Cancel", command=cancel) \ + .grid(column=1, row=4 if range_info else 3, padx="0 .3c") + + # Give all horizontal space to the grid cell with the OK button, so that + # Cancel moves to the right + dialog.columnconfigure(0, weight=1) + + _center_on_root(dialog) + + # Hack to scroll the entry so that the end of the text is shown, from + # https://stackoverflow.com/questions/29334544/why-does-tkinters-entry-xview-moveto-fail. + # Related Tk ticket: https://core.tcl.tk/tk/info/2513186fff + def scroll_entry(_): + _root.update_idletasks() + entry.unbind("<Expose>") + entry.xview_moveto(1) + entry.bind("<Expose>", scroll_entry) + + # The dialog must be visible before we can grab the input + dialog.wait_visibility() + dialog.grab_set() + + dialog.bind("<Return>", ok) + dialog.bind("<KP_Enter>", ok) + dialog.bind("<Escape>", cancel) + + # Wait for the user to be done with the dialog + parent.wait_window(dialog) + + # Regrab the input in the parent + parent.grab_set() + + return _entry_res + + +def _center_on_root(dialog): + # Centers 'dialog' on the root window. It often ends up at some bad place + # like the top-left corner of the screen otherwise. See the menuconfig() + # function, which has similar logic. + + dialog.withdraw() + _root.update_idletasks() + + dialog_width = dialog.winfo_reqwidth() + dialog_height = dialog.winfo_reqheight() + + screen_width = _root.winfo_screenwidth() + screen_height = _root.winfo_screenheight() + + x = _root.winfo_rootx() + (_root.winfo_width() - dialog_width)//2 + y = _root.winfo_rooty() + (_root.winfo_height() - dialog_height)//2 + + # Clamp so that no part of the dialog is outside the screen + if x + dialog_width > screen_width: + x = screen_width - dialog_width + elif x < 0: + x = 0 + if y + dialog_height > screen_height: + y = screen_height - dialog_height + elif y < 0: + y = 0 + + dialog.geometry("+{}+{}".format(x, y)) + + dialog.deiconify() + + +def _check_valid(dialog, entry, sym, s): + # Returns True if the string 's' is a well-formed value for 'sym'. + # Otherwise, pops up an error and returns False. + + if sym.type not in (INT, HEX): + # Anything goes for non-int/hex symbols + return True + + base = 10 if sym.type == INT else 16 + try: + int(s, base) + except ValueError: + messagebox.showerror( + "Bad value", + "'{}' is a malformed {} value".format( + s, TYPE_TO_STR[sym.type]), + parent=dialog) + entry.focus_set() + return False + + for low_sym, high_sym, cond in sym.ranges: + if expr_value(cond): + low_s = low_sym.str_value + high_s = high_sym.str_value + + if not int(low_s, base) <= int(s, base) <= int(high_s, base): + messagebox.showerror( + "Value out of range", + "{} is outside the range {}-{}".format(s, low_s, high_s), + parent=dialog) + entry.focus_set() + return False + + break + + return True + + +def _range_info(sym): + # Returns a string with information about the valid range for the symbol + # 'sym', or None if 'sym' doesn't have a range + + if sym.type in (INT, HEX): + for low, high, cond in sym.ranges: + if expr_value(cond): + return "Range: {}-{}".format(low.str_value, high.str_value) + + return None + + +def _save(_=None): + # Tries to save the configuration + + if _try_save(_kconf.write_config, _conf_filename, "configuration"): + _set_conf_changed(False) + + _tree.focus_set() + + +def _save_as(): + # Pops up a dialog for saving the configuration to a specific location + + global _conf_filename + + filename = _conf_filename + while True: + filename = filedialog.asksaveasfilename( + title="Save configuration as", + initialdir=os.path.dirname(filename), + initialfile=os.path.basename(filename), + parent=_root) + + if not filename: + break + + if _try_save(_kconf.write_config, filename, "configuration"): + _conf_filename = filename + break + + _tree.focus_set() + + +def _save_minimal(): + # Pops up a dialog for saving a minimal configuration (defconfig) to a + # specific location + + global _minconf_filename + + filename = _minconf_filename + while True: + filename = filedialog.asksaveasfilename( + title="Save minimal configuration as", + initialdir=os.path.dirname(filename), + initialfile=os.path.basename(filename), + parent=_root) + + if not filename: + break + + if _try_save(_kconf.write_min_config, filename, + "minimal configuration"): + + _minconf_filename = filename + break + + _tree.focus_set() + + +def _open(_=None): + # Pops up a dialog for loading a configuration + + global _conf_filename + + if _conf_changed and \ + not messagebox.askokcancel( + "Unsaved changes", + "You have unsaved changes. Load new configuration anyway?"): + + return + + filename = _conf_filename + while True: + filename = filedialog.askopenfilename( + title="Open configuration", + initialdir=os.path.dirname(filename), + initialfile=os.path.basename(filename), + parent=_root) + + if not filename: + break + + if _try_load(filename): + # Maybe something fancier could be done here later to try to + # preserve the scroll + + _conf_filename = filename + _set_conf_changed(_needs_save()) + + if _single_menu and not _shown_menu_nodes(_cur_menu): + # Turn on show-all if we're in single-menu mode and would end + # up with an empty menu + _show_all_var.set(True) + + _update_tree() + + break + + _tree.focus_set() + + +def _toggle_showname(_): + # Toggles show-name mode on/off + + _show_name_var.set(not _show_name_var.get()) + _do_showname() + + +def _do_showname(): + # Updates the UI for the current show-name setting + + # Columns do not automatically shrink/expand, so we have to update + # column widths ourselves + + tree_width = _tree.winfo_width() + + if _show_name_var.get(): + _tree["displaycolumns"] = ("name",) + _tree["show"] = "tree headings" + name_width = tree_width//3 + _tree.column("#0", width=max(tree_width - name_width, 1)) + _tree.column("name", width=name_width) + else: + _tree["displaycolumns"] = () + _tree["show"] = "tree" + _tree.column("#0", width=tree_width) + + _tree.focus_set() + + +def _toggle_showall(_): + # Toggles show-all mode on/off + + _show_all_var.set(not _show_all) + _do_showall() + + +def _do_showall(): + # Updates the UI for the current show-all setting + + # Don't allow turning off show-all if we're in single-menu mode and the + # current menu would become empty + if _single_menu and not _shown_menu_nodes(_cur_menu): + _show_all_var.set(True) + return + + # Save scroll information. old_scroll can end up negative here, if the + # reference item isn't shown (only invisible items on the screen, and + # show-all being turned off). + + stayput = _vis_loc_ref_item() + # Probe the middle of the first row, to play it safe. identify_row(0) seems + # to return the row before the top row. + old_scroll = _item_row(stayput) - \ + _item_row(_tree.identify_row(_treeview_rowheight//2)) + + _update_tree() + + if _show_all: + # Deep magic: Unless we call update_idletasks(), the scroll adjustment + # below is restricted to the height of the old tree, instead of the + # height of the new tree. Since the tree with show-all on is guaranteed + # to be taller, and we want the maximum range, we only call it when + # turning show-all on. + # + # Strictly speaking, something similar ought to be done when changing + # symbol values, but it causes annoying flicker, and in 99% of cases + # things work anyway there (with usually minor scroll mess-ups in the + # 1% case). + _root.update_idletasks() + + # Restore scroll + _tree.yview(_item_row(stayput) - old_scroll) + + _tree.focus_set() + + +def _toggle_tree_mode(_): + # Toggles single-menu mode on/off + + _single_menu_var.set(not _single_menu) + _do_tree_mode() + + +def _do_tree_mode(): + # Updates the UI for the current tree mode (full-tree or single-menu) + + loc_ref_node = _id_to_node[_loc_ref_item()] + + if not _single_menu: + # _jump_to() -> _enter_menu() already updates the tree, but + # _jump_to() -> load_parents() doesn't, because it isn't always needed. + # We always need to update the tree here, e.g. to add/remove "--->". + _update_tree() + + _jump_to(loc_ref_node) + _tree.focus_set() + + +def _enter_menu_and_select_first(menu): + # Enters the menu 'menu' and selects the first item. Used in single-menu + # mode. + + _enter_menu(menu) + _select(_tree, _tree.get_children()[0]) + + +def _enter_menu(menu): + # Enters the menu 'menu'. Used in single-menu mode. + + global _cur_menu + + _cur_menu = menu + _update_tree() + + _backbutton["state"] = "disabled" if menu is _kconf.top_node else "normal" + + +def _leave_menu(): + # Leaves the current menu. Used in single-menu mode. + + global _cur_menu + + if _cur_menu is not _kconf.top_node: + old_menu = _cur_menu + + _cur_menu = _parent_menu(_cur_menu) + _update_tree() + + _select(_tree, id(old_menu)) + + if _cur_menu is _kconf.top_node: + _backbutton["state"] = "disabled" + + _tree.focus_set() + + +def _select(tree, item): + # Selects, focuses, and see()s 'item' in 'tree' + + tree.selection_set(item) + tree.focus(item) + tree.see(item) + + +def _loc_ref_item(): + # Returns a Treeview item that can serve as a reference for the current + # scroll location. We try to make this item stay on the same row on the + # screen when updating the tree. + + # If the selected item is visible, use that + sel = _tree.selection() + if sel and _tree.bbox(sel[0]): + return sel[0] + + # Otherwise, use the middle item on the screen. If it doesn't exist, the + # tree is probably really small, so use the first item in the entire tree. + return _tree.identify_row(_tree.winfo_height()//2) or \ + _tree.get_children()[0] + + +def _vis_loc_ref_item(): + # Like _loc_ref_item(), but finds a visible item around the reference item. + # Used when changing show-all mode, where non-visible (red) items will + # disappear. + + item = _loc_ref_item() + + vis_before = _vis_before(item) + if vis_before and _tree.bbox(vis_before): + return vis_before + + vis_after = _vis_after(item) + if vis_after and _tree.bbox(vis_after): + return vis_after + + return vis_before or vis_after + + +def _vis_before(item): + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, + # searching backwards from 'item'. + + while item: + if not _tree.tag_has("invisible", item): + return item + + prev = _tree.prev(item) + item = prev if prev else _tree.parent(item) + + return None + + +def _vis_after(item): + # _vis_loc_ref_item() helper. Returns the first visible (not red) item, + # searching forwards from 'item'. + + while item: + if not _tree.tag_has("invisible", item): + return item + + next = _tree.next(item) + if next: + item = next + else: + item = _tree.parent(item) + if not item: + break + item = _tree.next(item) + + return None + + +def _on_quit(_=None): + # Called when the user wants to exit + + if not _conf_changed: + _quit("No changes to save (for '{}')".format(_conf_filename)) + return + + while True: + ync = messagebox.askyesnocancel("Quit", "Save changes?") + if ync is None: + return + + if not ync: + _quit("Configuration ({}) was not saved".format(_conf_filename)) + return + + if _try_save(_kconf.write_config, _conf_filename, "configuration"): + # _try_save() already prints the "Configuration saved to ..." + # message + _quit() + return + + +def _quit(msg=None): + # Quits the application + + # Do not call sys.exit() here, in case we're being run from a script + _root.destroy() + if msg: + print(msg) + + +def _try_save(save_fn, filename, description): + # Tries to save a configuration file. Pops up an error and returns False on + # failure. + # + # save_fn: + # Function to call with 'filename' to save the file + # + # description: + # String describing the thing being saved + + try: + # save_fn() returns a message to print + msg = save_fn(filename) + _set_status(msg) + print(msg) + return True + except EnvironmentError as e: + messagebox.showerror( + "Error saving " + description, + "Error saving {} to '{}': {} (errno: {})" + .format(description, e.filename, e.strerror, + errno.errorcode[e.errno])) + return False + + +def _try_load(filename): + # Tries to load a configuration file. Pops up an error and returns False on + # failure. + # + # filename: + # Configuration file to load + + try: + msg = _kconf.load_config(filename) + _set_status(msg) + print(msg) + return True + except EnvironmentError as e: + messagebox.showerror( + "Error loading configuration", + "Error loading '{}': {} (errno: {})" + .format(filename, e.strerror, errno.errorcode[e.errno])) + return False + + +def _jump_to_dialog(_=None): + # Pops up a dialog for jumping directly to a particular node. Symbol values + # can also be changed within the dialog. + # + # Note: There's nothing preventing this from doing an incremental search + # like menuconfig.py does, but currently it's a bit jerky for large Kconfig + # trees, at least when inputting the beginning of the search string. We'd + # need to somehow only update the tree items that are shown in the Treeview + # to fix it. + + global _jump_to_tree + + def search(_=None): + _update_jump_to_matches(msglabel, entry.get()) + + def jump_to_selected(event=None): + # Jumps to the selected node and closes the dialog + + # Ignore double clicks on the image and in the heading area + if event and (tree.identify_element(event.x, event.y) == "image" or + _in_heading(event)): + return + + sel = tree.selection() + if not sel: + return + + node = _id_to_node[sel[0]] + + if node not in _shown_menu_nodes(_parent_menu(node)): + _show_all_var.set(True) + if not _single_menu: + # See comment in _do_tree_mode() + _update_tree() + + _jump_to(node) + + dialog.destroy() + + def tree_select(_): + jumpto_button["state"] = "normal" if tree.selection() else "disabled" + + + dialog = Toplevel(_root) + dialog.geometry("+{}+{}".format( + _root.winfo_rootx() + 50, _root.winfo_rooty() + 50)) + dialog.title("Jump to symbol/choice/menu/comment") + dialog.minsize(128, 128) # See _create_ui() + dialog.transient(_root) + + ttk.Label(dialog, text=_JUMP_TO_HELP) \ + .grid(column=0, row=0, columnspan=2, sticky="w", padx=".1c", + pady=".1c") + + entry = ttk.Entry(dialog) + entry.grid(column=0, row=1, sticky="ew", padx=".1c", pady=".1c") + entry.focus_set() + + entry.bind("<Return>", search) + entry.bind("<KP_Enter>", search) + + ttk.Button(dialog, text="Search", command=search) \ + .grid(column=1, row=1, padx="0 .1c", pady="0 .1c") + + msglabel = ttk.Label(dialog) + msglabel.grid(column=0, row=2, sticky="w", pady="0 .1c") + + panedwindow, tree = _create_kconfig_tree_and_desc(dialog) + panedwindow.grid(column=0, row=3, columnspan=2, sticky="nsew") + + # Clear tree + tree.set_children("") + + _jump_to_tree = tree + + jumpto_button = ttk.Button(dialog, text="Jump to selected item", + state="disabled", command=jump_to_selected) + jumpto_button.grid(column=0, row=4, columnspan=2, sticky="ns", pady=".1c") + + dialog.columnconfigure(0, weight=1) + # Only the pane with the Kconfig tree and description grows vertically + dialog.rowconfigure(3, weight=1) + + # See the menuconfig() function + _root.update_idletasks() + dialog.geometry(dialog.geometry()) + + # The dialog must be visible before we can grab the input + dialog.wait_visibility() + dialog.grab_set() + + tree.bind("<Double-1>", jump_to_selected) + tree.bind("<Return>", jump_to_selected) + tree.bind("<KP_Enter>", jump_to_selected) + # add=True to avoid overriding the description text update + tree.bind("<<TreeviewSelect>>", tree_select, add=True) + + dialog.bind("<Escape>", lambda _: dialog.destroy()) + + # Wait for the user to be done with the dialog + _root.wait_window(dialog) + + _jump_to_tree = None + + _tree.focus_set() + + +def _update_jump_to_matches(msglabel, search_string): + # Searches for nodes matching the search string and updates + # _jump_to_matches. Puts a message in 'msglabel' if there are no matches, + # or regex errors. + + global _jump_to_matches + + _jump_to_tree.selection_set(()) + + try: + # We could use re.IGNORECASE here instead of lower(), but this is + # faster for regexes like '.*debug$' (though the '.*' is redundant + # there). Those probably have bad interactions with re.search(), which + # matches anywhere in the string. + regex_searches = [re.compile(regex).search + for regex in search_string.lower().split()] + except re.error as e: + msg = "Bad regular expression" + # re.error.msg was added in Python 3.5 + if hasattr(e, "msg"): + msg += ": " + e.msg + msglabel["text"] = msg + # Clear tree + _jump_to_tree.set_children("") + return + + _jump_to_matches = [] + add_match = _jump_to_matches.append + + for node in _sorted_sc_nodes(): + # Symbol/choice + sc = node.item + + for search in regex_searches: + # Both the name and the prompt might be missing, since + # we're searching both symbols and choices + + # Does the regex match either the symbol name or the + # prompt (if any)? + if not (sc.name and search(sc.name.lower()) or + node.prompt and search(node.prompt[0].lower())): + + # Give up on the first regex that doesn't match, to + # speed things up a bit when multiple regexes are + # entered + break + + else: + add_match(node) + + # Search menus and comments + + for node in _sorted_menu_comment_nodes(): + for search in regex_searches: + if not search(node.prompt[0].lower()): + break + else: + add_match(node) + + msglabel["text"] = "" if _jump_to_matches else "No matches" + + _update_jump_to_display() + + if _jump_to_matches: + item = id(_jump_to_matches[0]) + _jump_to_tree.selection_set(item) + _jump_to_tree.focus(item) + + +def _update_jump_to_display(): + # Updates the images and text for the items in _jump_to_matches, and sets + # them as the items of _jump_to_tree + + # Micro-optimize a bit + item = _jump_to_tree.item + id_ = id + node_str = _node_str + img_tag = _img_tag + visible = _visible + for node in _jump_to_matches: + item(id_(node), + text=node_str(node), + tags=img_tag(node) if visible(node) else + img_tag(node) + " invisible") + + _jump_to_tree.set_children("", *map(id, _jump_to_matches)) + + +def _jump_to(node): + # Jumps directly to 'node' and selects it + + if _single_menu: + _enter_menu(_parent_menu(node)) + else: + _load_parents(node) + + _select(_tree, id(node)) + + +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing +# to the same list. This avoids a global. +def _sorted_sc_nodes(cached_nodes=[]): + # Returns a sorted list of symbol and choice nodes to search. The symbol + # nodes appear first, sorted by name, and then the choice nodes, sorted by + # prompt and (secondarily) name. + + if not cached_nodes: + # Add symbol nodes + for sym in sorted(_kconf.unique_defined_syms, + key=lambda sym: sym.name): + # += is in-place for lists + cached_nodes += sym.nodes + + # Add choice nodes + + choices = sorted(_kconf.unique_choices, + key=lambda choice: choice.name or "") + + cached_nodes += sorted( + [node + for choice in choices + for node in choice.nodes], + key=lambda node: node.prompt[0] if node.prompt else "") + + return cached_nodes + + +def _sorted_menu_comment_nodes(cached_nodes=[]): + # Returns a list of menu and comment nodes to search, sorted by prompt, + # with the menus first + + if not cached_nodes: + def prompt_text(mc): + return mc.prompt[0] + + cached_nodes += sorted(_kconf.menus, key=prompt_text) + cached_nodes += sorted(_kconf.comments, key=prompt_text) + + return cached_nodes + + +def _load_parents(node): + # Menus are lazily populated as they're opened in full-tree mode, but + # jumping to an item needs its parent menus to be populated. This function + # populates 'node's parents. + + # Get all parents leading up to 'node', sorted with the root first + parents = [] + cur = node.parent + while cur is not _kconf.top_node: + parents.append(cur) + cur = cur.parent + parents.reverse() + + for i, parent in enumerate(parents): + if not _tree.item(id(parent), "open"): + # Found a closed menu. Populate it and all the remaining menus + # leading up to 'node'. + for parent in parents[i:]: + # We only need to populate "real" menus/choices. Implicit menus + # are populated when their parents menus are entered. + if not isinstance(parent.item, Symbol): + _build_full_tree(parent) + return + + +def _parent_menu(node): + # Returns the menu node of the menu that contains 'node'. In addition to + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. + # "Menu" here means a menu in the interface. + + menu = node.parent + while not menu.is_menuconfig: + menu = menu.parent + return menu + + +def _trace_write(var, fn): + # Makes fn() be called whenever the Tkinter Variable 'var' changes value + + # trace_variable() is deprecated according to the docstring, + # which recommends trace_add() + if hasattr(var, "trace_add"): + var.trace_add("write", fn) + else: + var.trace_variable("w", fn) + + +def _info_str(node): + # Returns information about the menu node 'node' as a string. + # + # The helper functions are responsible for adding newlines. This allows + # them to return "" if they don't want to add any output. + + if isinstance(node.item, Symbol): + sym = node.item + + return ( + _name_info(sym) + + _help_info(sym) + + _direct_dep_info(sym) + + _defaults_info(sym) + + _select_imply_info(sym) + + _kconfig_def_info(sym) + ) + + if isinstance(node.item, Choice): + choice = node.item + + return ( + _name_info(choice) + + _help_info(choice) + + 'Mode: {}\n\n'.format(choice.str_value) + + _choice_syms_info(choice) + + _direct_dep_info(choice) + + _defaults_info(choice) + + _kconfig_def_info(choice) + ) + + # node.item in (MENU, COMMENT) + return _kconfig_def_info(node) + + +def _name_info(sc): + # Returns a string with the name of the symbol/choice. Choices are shown as + # <choice (name if any)>. + + return (sc.name if sc.name else standard_sc_expr_str(sc)) + "\n\n" + + +def _value_info(sym): + # Returns a string showing 'sym's value + + # Only put quotes around the value for string symbols + return "Value: {}\n".format( + '"{}"'.format(sym.str_value) + if sym.orig_type == STRING + else sym.str_value) + + +def _choice_syms_info(choice): + # Returns a string listing the choice symbols in 'choice'. Adds + # "(selected)" next to the selected one. + + s = "Choice symbols:\n" + + for sym in choice.syms: + s += " - " + sym.name + if sym is choice.selection: + s += " (selected)" + s += "\n" + + return s + "\n" + + +def _help_info(sc): + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). + # Symbols and choices defined in multiple locations can have multiple help + # texts. + + s = "" + + for node in sc.nodes: + if node.help is not None: + s += node.help + "\n\n" + + return s + + +def _direct_dep_info(sc): + # Returns a string describing the direct dependencies of 'sc' (Symbol or + # Choice). The direct dependencies are the OR of the dependencies from each + # definition location. The dependencies at each definition location come + # from 'depends on' and dependencies inherited from parent items. + + return "" if sc.direct_dep is _kconf.y else \ + 'Direct dependencies (={}):\n{}\n' \ + .format(TRI_TO_STR[expr_value(sc.direct_dep)], + _split_expr_info(sc.direct_dep, 2)) + + +def _defaults_info(sc): + # Returns a string describing the defaults of 'sc' (Symbol or Choice) + + if not sc.defaults: + return "" + + s = "Default" + if len(sc.defaults) > 1: + s += "s" + s += ":\n" + + for val, cond in sc.orig_defaults: + s += " - " + if isinstance(sc, Symbol): + s += _expr_str(val) + + # Skip the tristate value hint if the expression is just a single + # symbol. _expr_str() already shows its value as a string. + # + # This also avoids showing the tristate value for string/int/hex + # defaults, which wouldn't make any sense. + if isinstance(val, tuple): + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) + else: + # Don't print the value next to the symbol name for choice + # defaults, as it looks a bit confusing + s += val.name + s += "\n" + + if cond is not _kconf.y: + s += " Condition (={}):\n{}" \ + .format(TRI_TO_STR[expr_value(cond)], + _split_expr_info(cond, 4)) + + return s + "\n" + + +def _split_expr_info(expr, indent): + # Returns a string with 'expr' split into its top-level && or || operands, + # with one operand per line, together with the operand's value. This is + # usually enough to get something readable for long expressions. A fancier + # recursive thingy would be possible too. + # + # indent: + # Number of leading spaces to add before the split expression. + + if len(split_expr(expr, AND)) > 1: + split_op = AND + op_str = "&&" + else: + split_op = OR + op_str = "||" + + s = "" + for i, term in enumerate(split_expr(expr, split_op)): + s += "{}{} {}".format(indent*" ", + " " if i == 0 else op_str, + _expr_str(term)) + + # Don't bother showing the value hint if the expression is just a + # single symbol. _expr_str() already shows its value. + if isinstance(term, tuple): + s += " (={})".format(TRI_TO_STR[expr_value(term)]) + + s += "\n" + + return s + + +def _select_imply_info(sym): + # Returns a string with information about which symbols 'select' or 'imply' + # 'sym'. The selecting/implying symbols are grouped according to which + # value they select/imply 'sym' to (n/m/y). + + def sis(expr, val, title): + # sis = selects/implies + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] + if not sis: + return "" + + res = title + for si in sis: + res += " - {}\n".format(split_expr(si, AND)[0].name) + return res + "\n" + + s = "" + + if sym.rev_dep is not _kconf.n: + s += sis(sym.rev_dep, 2, + "Symbols currently y-selecting this symbol:\n") + s += sis(sym.rev_dep, 1, + "Symbols currently m-selecting this symbol:\n") + s += sis(sym.rev_dep, 0, + "Symbols currently n-selecting this symbol (no effect):\n") + + if sym.weak_rev_dep is not _kconf.n: + s += sis(sym.weak_rev_dep, 2, + "Symbols currently y-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 1, + "Symbols currently m-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 0, + "Symbols currently n-implying this symbol (no effect):\n") + + return s + + +def _kconfig_def_info(item): + # Returns a string with the definition of 'item' in Kconfig syntax, + # together with the definition location(s) and their include and menu paths + + nodes = [item] if isinstance(item, MenuNode) else item.nodes + + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ + .format("s" if len(nodes) > 1 else "") + s += (len(s) - 1)*"=" + + for node in nodes: + s += "\n\n" \ + "At {}:{}\n" \ + "{}" \ + "Menu path: {}\n\n" \ + "{}" \ + .format(node.filename, node.linenr, + _include_path_info(node), + _menu_path_info(node), + node.custom_str(_name_and_val_str)) + + return s + + +def _include_path_info(node): + if not node.include_path: + # In the top-level Kconfig file + return "" + + return "Included via {}\n".format( + " -> ".join("{}:{}".format(filename, linenr) + for filename, linenr in node.include_path)) + + +def _menu_path_info(node): + # Returns a string describing the menu path leading up to 'node' + + path = "" + + while node.parent is not _kconf.top_node: + node = node.parent + + # Promptless choices might appear among the parents. Use + # standard_sc_expr_str() for them, so that they show up as + # '<choice (name if any)>'. + path = " -> " + (node.prompt[0] if node.prompt else + standard_sc_expr_str(node.item)) + path + + return "(Top)" + path + + +def _name_and_val_str(sc): + # Custom symbol/choice printer that shows symbol values after symbols + + # Show the values of non-constant (non-quoted) symbols that don't look like + # numbers. Things like 123 are actually symbol references, and only work as + # expected due to undefined symbols getting their name as their value. + # Showing the symbol value for those isn't helpful though. + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): + if not sc.nodes: + # Undefined symbol reference + return "{}(undefined/n)".format(sc.name) + + return '{}(={})'.format(sc.name, sc.str_value) + + # For other items, use the standard format + return standard_sc_expr_str(sc) + + +def _expr_str(expr): + # Custom expression printer that shows symbol values + return expr_str(expr, _name_and_val_str) + + +def _is_num(name): + # Heuristic to see if a symbol name looks like a number, for nicer output + # when printing expressions. Things like 16 are actually symbol names, only + # they get their name as their value when the symbol is undefined. + + try: + int(name) + except ValueError: + if not name.startswith(("0x", "0X")): + return False + + try: + int(name, 16) + except ValueError: + return False + + return True + + +if __name__ == "__main__": + _main() diff --git a/support/kconfiglib/kconfiglib.py b/support/kconfiglib/kconfiglib.py new file mode 100644 index 0000000..410db28 --- /dev/null +++ b/support/kconfiglib/kconfiglib.py @@ -0,0 +1,7018 @@ +# Copyright (c) 2011-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Overview +======== + +Kconfiglib is a Python 2/3 library for scripting and extracting information +from Kconfig (https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt) +configuration systems. + +See the homepage at https://github.com/ulfalizer/Kconfiglib for a longer +overview. + +Since Kconfiglib 12.0.0, the library version is available in +kconfiglib.VERSION, which is a (<major>, <minor>, <patch>) tuple, e.g. +(12, 0, 0). + + +Using Kconfiglib on the Linux kernel with the Makefile targets +============================================================== + +For the Linux kernel, a handy interface is provided by the +scripts/kconfig/Makefile patch, which can be applied with either 'git am' or +the 'patch' utility: + + $ wget -qO- https://raw.githubusercontent.com/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 kmenuconfig +---------------- + +This target runs the curses menuconfig interface with Python 3. As of +Kconfiglib 12.2.0, both Python 2 and Python 3 are supported (previously, only +Python 3 was supported, so this was a backport). + + +make guiconfig +-------------- + +This target runs the Tkinter menuconfig interface. Both Python 2 and Python 3 +are supported. To change the Python interpreter used, pass +PYTHONCMD=<executable> to 'make'. The default is 'python'. + + +make [ARCH=<arch>] iscriptconfig +-------------------------------- + +This target gives an interactive Python prompt where a Kconfig instance has +been preloaded and is available in 'kconf'. To change the Python interpreter +used, pass PYTHONCMD=<executable> to 'make'. The default is 'python'. + +To get a feel for the API, try evaluating and printing the symbols in +kconf.defined_syms, and explore the MenuNode menu tree starting at +kconf.top_node by following 'next' and 'list' pointers. + +The item contained in a menu node is found in MenuNode.item (note that this can +be one of the constants kconfiglib.MENU and kconfiglib.COMMENT), and all +symbols and choices have a 'nodes' attribute containing their menu nodes +(usually only one). Printing a menu node will print its item, in Kconfig +format. + +If you want to look up a symbol by name, use the kconf.syms dictionary. + + +make scriptconfig SCRIPT=<script> [SCRIPT_ARG=<arg>] +---------------------------------------------------- + +This target runs the Python script given by the SCRIPT parameter on the +configuration. sys.argv[1] holds the name of the top-level Kconfig file +(currently always "Kconfig" in practice), and sys.argv[2] holds the SCRIPT_ARG +argument, if given. + +See the examples/ subdirectory for example scripts. + + +make dumpvarsconfig +------------------- + +This target prints a list of all environment variables referenced from the +Kconfig files, together with their values. See the +Kconfiglib/examples/dumpvars.py script. + +Only environment variables that are referenced via the Kconfig preprocessor +$(FOO) syntax are included. The preprocessor was added in Linux 4.18. + + +Using Kconfiglib without the Makefile targets +============================================= + +The make targets are only needed to pick up environment variables exported from +the Kbuild makefiles and referenced inside Kconfig files, via e.g. +'source "arch/$(SRCARCH)/Kconfig" and commands run via '$(shell,...)'. + +These variables are referenced as of writing (Linux 4.18), together with sample +values: + + srctree (.) + ARCH (x86) + SRCARCH (x86) + KERNELVERSION (4.18.0) + CC (gcc) + HOSTCC (gcc) + HOSTCXX (g++) + CC_VERSION_TEXT (gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0) + +Older kernels only reference ARCH, SRCARCH, and KERNELVERSION. + +If your kernel is recent enough (4.18+), you can get a list of referenced +environment variables via 'make dumpvarsconfig' (see above). Note that this +command is added by the Makefile patch. + +To run Kconfiglib without the Makefile patch, set the environment variables +manually: + + $ srctree=. ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` ... python(3) + >>> import kconfiglib + >>> kconf = kconfiglib.Kconfig() # filename defaults to "Kconfig" + +Search the top-level Makefile for "Additional ARCH settings" to see other +possibilities for ARCH and SRCARCH. + + +Intro to symbol values +====================== + +Kconfiglib has the same assignment semantics as the C implementation. + +Any symbol can be assigned a value by the user (via Kconfig.load_config() or +Symbol.set_value()), but this user value is only respected if the symbol is +visible, which corresponds to it (currently) being visible in the menuconfig +interface. + +For symbols with prompts, the visibility of the symbol is determined by the +condition on the prompt. Symbols without prompts are never visible, so setting +a user value on them is pointless. A warning will be printed by default if +Symbol.set_value() is called on a promptless symbol. Assignments to promptless +symbols are normal within a .config file, so no similar warning will be printed +by load_config(). + +Dependencies from parents and 'if'/'depends on' are propagated to properties, +including prompts, so these two configurations are logically equivalent: + +(1) + + menu "menu" + depends on A + + if B + + config FOO + tristate "foo" if D + default y + depends on C + + endif + + endmenu + +(2) + + menu "menu" + depends on A + + config FOO + tristate "foo" if A && B && C && D + default y if A && B && C + + endmenu + +In this example, A && B && C && D (the prompt condition) needs to be non-n for +FOO to be visible (assignable). If its value is m, the symbol can only be +assigned the value m: The visibility sets an upper bound on the value that can +be assigned by the user, and any higher user value will be truncated down. + +'default' properties are independent of the visibility, though a 'default' will +often get the same condition as the prompt due to dependency propagation. +'default' properties are used if the symbol is not visible or has no user +value. + +Symbols with no user value (or that have a user value but are not visible) and +no (active) 'default' default to n for bool/tristate symbols, and to the empty +string for other symbol types. + +'select' works similarly to symbol visibility, but sets a lower bound on the +value of the symbol. The lower bound is determined by the value of the +select*ing* symbol. 'select' does not respect visibility, so non-visible +symbols can be forced to a particular (minimum) value by a select as well. + +For non-bool/tristate symbols, it only matters whether the visibility is n or +non-n: m visibility acts the same as y visibility. + +Conditions on 'default' and 'select' work in mostly intuitive ways. If the +condition is n, the 'default' or 'select' is disabled. If it is m, the +'default' or 'select' value (the value of the selecting symbol) is truncated +down to m. + +When writing a configuration with Kconfig.write_config(), only symbols that are +visible, have an (active) default, or are selected will get written out (note +that this includes all symbols that would accept user values). Kconfiglib +matches the .config format produced by the C implementations down to the +character. This eases testing. + +For a visible bool/tristate symbol FOO with value n, this line is written to +.config: + + # CONFIG_FOO is not set + +The point is to remember the user n selection (which might differ from the +default value the symbol would get), while at the same sticking to the rule +that undefined corresponds to n (.config uses Makefile format, making the line +above a comment). When the .config file is read back in, this line will be +treated the same as the following assignment: + + CONFIG_FOO=n + +In Kconfiglib, the set of (currently) assignable values for a bool/tristate +symbol appear in Symbol.assignable. For other symbol types, just check if +sym.visibility is non-0 (non-n) to see whether the user value will have an +effect. + + +Intro to the menu tree +====================== + +The menu structure, as seen in e.g. menuconfig, is represented by a tree of +MenuNode objects. The top node of the configuration corresponds to an implicit +top-level menu, the title of which is shown at the top in the standard +menuconfig interface. (The title is also available in Kconfig.mainmenu_text in +Kconfiglib.) + +The top node is found in Kconfig.top_node. From there, you can visit child menu +nodes by following the 'list' pointer, and any following menu nodes by +following the 'next' pointer. Usually, a non-None 'list' pointer indicates a +menu or Choice, but menu nodes for symbols can sometimes have a non-None 'list' +pointer too due to submenus created implicitly from dependencies. + +MenuNode.item is either a Symbol or a Choice object, or one of the constants +MENU and COMMENT. The prompt of the menu node can be found in MenuNode.prompt, +which also holds the title for menus and comments. For Symbol and Choice, +MenuNode.help holds the help text (if any, otherwise None). + +Most symbols will only have a single menu node. A symbol defined in multiple +locations will have one menu node for each location. The list of menu nodes for +a Symbol or Choice can be found in the Symbol/Choice.nodes attribute. + +Note that prompts and help texts for symbols and choices are stored in their +menu node(s) rather than in the Symbol or Choice objects themselves. This makes +it possible to define a symbol in multiple locations with a different prompt or +help text in each location. To get the help text or prompt for a symbol with a +single menu node, do sym.nodes[0].help and sym.nodes[0].prompt, respectively. +The prompt is a (text, condition) tuple, where condition determines the +visibility (see 'Intro to expressions' below). + +This organization mirrors the C implementation. MenuNode is called +'struct menu' there, but I thought "menu" was a confusing name. + +It is possible to give a Choice a name and define it in multiple locations, +hence why Choice.nodes is also a list. + +As a convenience, the properties added at a particular definition location are +available on the MenuNode itself, in e.g. MenuNode.defaults. This is helpful +when generating documentation, so that symbols/choices defined in multiple +locations can be shown with the correct properties at each location. + + +Intro to expressions +==================== + +Expressions can be evaluated with the expr_value() function and printed with +the expr_str() function (these are used internally as well). Evaluating an +expression always yields a tristate value, where n, m, and y are represented as +0, 1, and 2, respectively. + +The following table should help you figure out how expressions are represented. +A, B, C, ... are symbols (Symbol instances), NOT is the kconfiglib.NOT +constant, etc. + +Expression Representation +---------- -------------- +A A +"A" A (constant symbol) +!A (NOT, A) +A && B (AND, A, B) +A && B && C (AND, A, (AND, B, C)) +A || B (OR, A, B) +A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) +A = B (EQUAL, A, B) +A != "foo" (UNEQUAL, A, foo (constant symbol)) +A && B = C && D (AND, A, (AND, (EQUAL, B, C), D)) +n Kconfig.n (constant symbol) +m Kconfig.m (constant symbol) +y Kconfig.y (constant symbol) +"y" Kconfig.y (constant symbol) + +Strings like "foo" in 'default "foo"' or 'depends on SYM = "foo"' are +represented as constant symbols, so the only values that appear in expressions +are symbols***. This mirrors the C implementation. + +***For choice symbols, the parent Choice will appear in expressions as well, +but it's usually invisible as the value interfaces of Symbol and Choice are +identical. This mirrors the C implementation and makes different choice modes +"just work". + +Manual evaluation examples: + + - The value of A && B is min(A.tri_value, B.tri_value) + + - The value of A || B is max(A.tri_value, B.tri_value) + + - The value of !A is 2 - A.tri_value + + - The value of A = B is 2 (y) if A.str_value == B.str_value, and 0 (n) + otherwise. Note that str_value is used here instead of tri_value. + + For constant (as well as undefined) symbols, str_value matches the name of + the symbol. This mirrors the C implementation and explains why + 'depends on SYM = "foo"' above works as expected. + +n/m/y are automatically converted to the corresponding constant symbols +"n"/"m"/"y" (Kconfig.n/m/y) during parsing. + +Kconfig.const_syms is a dictionary like Kconfig.syms but for constant symbols. + +If a condition is missing (e.g., <cond> when the 'if <cond>' is removed from +'default A if <cond>'), it is actually Kconfig.y. The standard __str__() +functions just avoid printing 'if y' conditions to give cleaner output. + + +Kconfig extensions +================== + +Kconfiglib includes a couple of Kconfig extensions: + +'source' with relative path +--------------------------- + +The 'rsource' statement sources Kconfig files with a path relative to directory +of the Kconfig file containing the 'rsource' statement, instead of relative to +the project root. + +Consider following directory tree: + + Project + +--Kconfig + | + +--src + +--Kconfig + | + +--SubSystem1 + +--Kconfig + | + +--ModuleA + +--Kconfig + +In this example, assume that src/SubSystem1/Kconfig wants to source +src/SubSystem1/ModuleA/Kconfig. + +With 'source', this statement would be used: + + source "src/SubSystem1/ModuleA/Kconfig" + +With 'rsource', this turns into + + rsource "ModuleA/Kconfig" + +If an absolute path is given to 'rsource', it acts the same as 'source'. + +'rsource' can be used to create "position-independent" Kconfig trees that can +be moved around freely. + + +Globbing 'source' +----------------- + +'source' and 'rsource' accept glob patterns, sourcing all matching Kconfig +files. They require at least one matching file, raising a KconfigError +otherwise. + +For example, the following statement might source sub1/foofoofoo and +sub2/foobarfoo: + + source "sub[12]/foo*foo" + +The glob patterns accepted are the same as for the standard glob.glob() +function. + +Two additional statements are provided for cases where it's acceptable for a +pattern to match no files: 'osource' and 'orsource' (the o is for "optional"). + +For example, the following statements will be no-ops if neither "foo" nor any +files matching "bar*" exist: + + osource "foo" + osource "bar*" + +'orsource' does a relative optional source. + +'source' and 'osource' are analogous to 'include' and '-include' in Make. + + +Generalized def_* keywords +-------------------------- + +def_int, def_hex, and def_string are available in addition to def_bool and +def_tristate, allowing int, hex, and string symbols to be given a type and a +default at the same time. + + +Extra optional warnings +----------------------- + +Some optional warnings can be controlled via environment variables: + + - KCONFIG_WARN_UNDEF: If set to 'y', warnings will be generated for all + references to undefined symbols within Kconfig files. The only gotcha is + that all hex literals must be prefixed with "0x" or "0X", to make it + possible to distinguish them from symbol references. + + Some projects (e.g. the Linux kernel) use multiple Kconfig trees with many + shared Kconfig files, leading to some safe undefined symbol references. + KCONFIG_WARN_UNDEF is useful in projects that only have a single Kconfig + tree though. + + KCONFIG_STRICT is an older alias for this environment variable, supported + for backwards compatibility. + + - KCONFIG_WARN_UNDEF_ASSIGN: If set to 'y', warnings will be generated for + all assignments to undefined symbols within .config files. By default, no + such warnings are generated. + + This warning can also be enabled/disabled via the Kconfig.warn_assign_undef + variable. + + +Preprocessor user functions defined in Python +--------------------------------------------- + +Preprocessor functions can be defined in Python, which makes it simple to +integrate information from existing Python tools into Kconfig (e.g. to have +Kconfig symbols depend on hardware information stored in some other format). + +Putting a Python module named kconfigfunctions(.py) anywhere in sys.path will +cause it to be imported by Kconfiglib (in Kconfig.__init__()). Note that +sys.path can be customized via PYTHONPATH, and includes the directory of the +module being run by default, as well as installation directories. + +If the KCONFIG_FUNCTIONS environment variable is set, it gives a different +module name to use instead of 'kconfigfunctions'. + +The imported module is expected to define a global dictionary named 'functions' +that maps function names to Python functions, as follows: + + def my_fn(kconf, name, arg_1, arg_2, ...): + # kconf: + # Kconfig instance + # + # name: + # Name of the user-defined function ("my-fn"). Think argv[0]. + # + # arg_1, arg_2, ...: + # Arguments passed to the function from Kconfig (strings) + # + # Returns a string to be substituted as the result of calling the + # function + ... + + def my_other_fn(kconf, name, arg_1, arg_2, ...): + ... + + functions = { + "my-fn": (my_fn, <min.args>, <max.args>/None), + "my-other-fn": (my_other_fn, <min.args>, <max.args>/None), + ... + } + + ... + +<min.args> and <max.args> are the minimum and maximum number of arguments +expected by the function (excluding the implicit 'name' argument). If +<max.args> is None, there is no upper limit to the number of arguments. Passing +an invalid number of arguments will generate a KconfigError exception. + +Once defined, user functions can be called from Kconfig in the same way as +other preprocessor functions: + + config FOO + ... + depends on $(my-fn,arg1,arg2) + +If my_fn() returns "n", this will result in + + config FOO + ... + depends on n + +Warning +******* + +User-defined preprocessor functions are called as they're encountered at parse +time, before all Kconfig files have been processed, and before the menu tree +has been finalized. There are no guarantees that accessing Kconfig symbols or +the menu tree via the 'kconf' parameter will work, and it could potentially +lead to a crash. The 'kconf' parameter is provided for future extension (and +because the predefined functions take it anyway). + +Preferably, user-defined functions should be stateless. + + +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 importlib +import os +import re +import sys + +# Get rid of some attribute lookups. These are obvious in context. +from glob import iglob +from os.path import dirname, exists, expandvars, islink, join, realpath + + +VERSION = (12, 13, 0) + + +# File layout: +# +# Public classes +# Public functions +# Internal functions +# Global constants + +# Line length: 79 columns + + +# +# Public classes +# + + +class Kconfig(object): + """ + Represents a Kconfig configuration, e.g. for x86 or ARM. This is the set of + symbols, choices, and menu nodes appearing in the configuration. Creating + any number of Kconfig objects (including for different architectures) is + safe. Kconfiglib doesn't keep any global state. + + The following attributes are available. They should be treated as + read-only, and some are implemented through @property magic. + + syms: + A dictionary with all symbols in the configuration, indexed by name. Also + includes all symbols that are referenced in expressions but never + defined, except for constant (quoted) symbols. + + Undefined symbols can be recognized by Symbol.nodes being empty -- see + the 'Intro to the menu tree' section in the module docstring. + + const_syms: + A dictionary like 'syms' for constant (quoted) symbols + + named_choices: + A dictionary like 'syms' for named choices (choice FOO) + + defined_syms: + A list with all defined symbols, in the same order as they appear in the + Kconfig files. Symbols defined in multiple locations appear multiple + times. + + Note: You probably want to use 'unique_defined_syms' instead. This + attribute is mostly maintained for backwards compatibility. + + unique_defined_syms: + A list like 'defined_syms', but with duplicates removed. Just the first + instance is kept for symbols defined in multiple locations. Kconfig order + is preserved otherwise. + + Using this attribute instead of 'defined_syms' can save work, and + automatically gives reasonable behavior when writing configuration output + (symbols defined in multiple locations only generate output once, while + still preserving Kconfig order for readability). + + choices: + A list with all choices, in the same order as they appear in the Kconfig + files. + + Note: You probably want to use 'unique_choices' instead. This attribute + is mostly maintained for backwards compatibility. + + unique_choices: + Analogous to 'unique_defined_syms', for choices. Named choices can have + multiple definition locations. + + menus: + A list with all menus, in the same order as they appear in the Kconfig + files + + comments: + A list with all comments, in the same order as they appear in the Kconfig + files + + kconfig_filenames: + A list with the filenames of all Kconfig files included in the + configuration, relative to $srctree (or relative to the current directory + if $srctree isn't set), except absolute paths (e.g. + 'source "/foo/Kconfig"') are kept as-is. + + The files are listed in the order they are source'd, starting with the + top-level Kconfig file. If a file is source'd multiple times, it will + appear multiple times. Use set() to get unique filenames. + + Note that Kconfig.sync_deps() already indirectly catches any file + modifications that change configuration output. + + env_vars: + A set() with the names of all environment variables referenced in the + Kconfig files. + + Only environment variables referenced with the preprocessor $(FOO) syntax + will be registered. The older $FOO syntax is only supported for backwards + compatibility. + + Also note that $(FOO) won't be registered unless the environment variable + $FOO is actually set. If it isn't, $(FOO) is an expansion of an unset + preprocessor variable (which gives the empty string). + + Another gotcha is that environment variables referenced in the values of + recursively expanded preprocessor variables (those defined with =) will + only be registered if the variable is actually used (expanded) somewhere. + + The note from the 'kconfig_filenames' documentation applies here too. + + n/m/y: + The predefined constant symbols n/m/y. Also available in const_syms. + + modules: + The Symbol instance for the modules symbol. Currently hardcoded to + MODULES, which is backwards compatible. Kconfiglib will warn if + 'option modules' is set on some other symbol. Tell me if you need proper + 'option modules' support. + + 'modules' is never None. If the MODULES symbol is not explicitly defined, + its tri_value will be 0 (n), as expected. + + A simple way to enable modules is to do 'kconf.modules.set_value(2)' + (provided the MODULES symbol is defined and visible). Modules are + disabled by default in the kernel Kconfig files as of writing, though + nearly all defconfig files enable them (with 'CONFIG_MODULES=y'). + + defconfig_list: + The Symbol instance for the 'option defconfig_list' symbol, or None if no + defconfig_list symbol exists. The defconfig filename derived from this + symbol can be found in Kconfig.defconfig_filename. + + defconfig_filename: + The filename given by the defconfig_list symbol. This is taken from the + first 'default' with a satisfied condition where the specified file + exists (can be opened for reading). If a defconfig file foo/defconfig is + not found and $srctree was set when the Kconfig was created, + $srctree/foo/defconfig is looked up as well. + + 'defconfig_filename' is None if either no defconfig_list symbol exists, + or if the defconfig_list symbol has no 'default' with a satisfied + condition that specifies a file that exists. + + Gotcha: scripts/kconfig/Makefile might pass --defconfig=<defconfig> to + scripts/kconfig/conf when running e.g. 'make defconfig'. This option + overrides the defconfig_list symbol, meaning defconfig_filename might not + always match what 'make defconfig' would use. + + top_node: + The menu node (see the MenuNode class) of the implicit top-level menu. + Acts as the root of the menu tree. + + mainmenu_text: + The prompt (title) of the top menu (top_node). Defaults to "Main menu". + Can be changed with the 'mainmenu' statement (see kconfig-language.txt). + + variables: + A dictionary with all preprocessor variables, indexed by name. See the + Variable class. + + warn: + Set this variable to True/False to enable/disable warnings. See + Kconfig.__init__(). + + When 'warn' is False, the values of the other warning-related variables + are ignored. + + This variable as well as the other warn* variables can be read to check + the current warning settings. + + warn_to_stderr: + Set this variable to True/False to enable/disable warnings on stderr. See + Kconfig.__init__(). + + warn_assign_undef: + Set this variable to True to generate warnings for assignments to + undefined symbols in configuration files. + + This variable is False by default unless the KCONFIG_WARN_UNDEF_ASSIGN + environment variable was set to 'y' when the Kconfig instance was + created. + + warn_assign_override: + Set this variable to True to generate warnings for multiple assignments + to the same symbol in configuration files, where the assignments set + different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where the + last value would get used). + + This variable is True by default. Disabling it might be useful when + merging configurations. + + warn_assign_redun: + Like warn_assign_override, but for multiple assignments setting a symbol + to the same value. + + This variable is True by default. Disabling it might be useful when + merging configurations. + + warnings: + A list of strings containing all warnings that have been generated, for + cases where more flexibility is needed. + + See the 'warn_to_stderr' parameter to Kconfig.__init__() and the + Kconfig.warn_to_stderr variable as well. Note that warnings still get + added to Kconfig.warnings when 'warn_to_stderr' is True. + + Just as for warnings printed to stderr, only warnings that are enabled + will get added to Kconfig.warnings. See the various Kconfig.warn* + variables. + + missing_syms: + A list with (name, value) tuples for all assignments to undefined symbols + within the most recently loaded .config file(s). 'name' is the symbol + name without the 'CONFIG_' prefix. 'value' is a string that gives the + right-hand side of the assignment verbatim. + + See Kconfig.load_config() as well. + + srctree: + The value 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", + "_srctree_prefix", + "_unset_match", + "_warn_assign_no_prompt", + "choices", + "comments", + "config_prefix", + "const_syms", + "defconfig_list", + "defined_syms", + "env_vars", + "kconfig_filenames", + "m", + "menus", + "missing_syms", + "modules", + "n", + "named_choices", + "srctree", + "syms", + "top_node", + "unique_choices", + "unique_defined_syms", + "variables", + "warn", + "warn_assign_override", + "warn_assign_redun", + "warn_assign_undef", + "warn_to_stderr", + "warnings", + "y", + + # Parsing-related + "_parsing_kconfigs", + "_readline", + "_filename", + "_linenr", + "_include_path", + "_filestack", + "_line", + "_tokens", + "_tokens_i", + "_reuse_tokens", + ) + + # + # Public interface + # + + def __init__(self, filename="Kconfig", warn=True, warn_to_stderr=True, + encoding="utf-8"): + """ + Creates a new Kconfig object by parsing Kconfig files. + Note that Kconfig files are not the same as .config files (which store + configuration symbol values). + + See the module docstring for some environment variables that influence + default warning settings (KCONFIG_WARN_UNDEF and + KCONFIG_WARN_UNDEF_ASSIGN). + + Raises KconfigError on syntax/semantic errors, and OSError or (possibly + a subclass of) IOError on IO errors ('errno', 'strerror', and + 'filename' are available). Note that IOError is an alias for OSError on + Python 3, so it's enough to catch OSError there. If you need Python 2/3 + compatibility, it's easiest to catch EnvironmentError, which is a + common base class of OSError/IOError on Python 2 and an alias for + OSError on Python 3. + + filename (default: "Kconfig"): + The Kconfig file to load. For the Linux kernel, you'll want "Kconfig" + from the top-level directory, as environment variables will make sure + the right Kconfig is included from there (arch/$SRCARCH/Kconfig as of + writing). + + If $srctree is set, 'filename' will be looked up relative to it. + $srctree is also used to look up source'd files within Kconfig files. + See the class documentation. + + If you are using Kconfiglib via 'make scriptconfig', the filename of + the base base Kconfig file will be in sys.argv[1]. It's currently + always "Kconfig" in practice. + + warn (default: True): + True if warnings related to this configuration should be generated. + This can be changed later by setting Kconfig.warn to True/False. It + is provided as a constructor argument since warnings might be + generated during parsing. + + See the other Kconfig.warn_* variables as well, which enable or + suppress certain warnings when warnings are enabled. + + All generated warnings are added to the Kconfig.warnings list. See + the class documentation. + + warn_to_stderr (default: True): + True if warnings should be printed to stderr in addition to being + added to Kconfig.warnings. + + This can be changed later by setting Kconfig.warn_to_stderr to + True/False. + + encoding (default: "utf-8"): + The encoding to use when reading and writing files, and when decoding + output from commands run via $(shell). If None, the encoding + specified in the current locale will be used. + + The "utf-8" default avoids exceptions on systems that are configured + to use the C locale, which implies an ASCII encoding. + + This parameter has no effect on Python 2, due to implementation + issues (regular strings turning into Unicode strings, which are + distinct in Python 2). Python 2 doesn't decode regular strings + anyway. + + Related PEP: https://www.python.org/dev/peps/pep-0538/ + """ + self._encoding = encoding + + self.srctree = os.getenv("srctree", "") + # A prefix we can reliably strip from glob() results to get a filename + # relative to $srctree. relpath() can cause issues for symlinks, + # because it assumes symlink/../foo is the same as foo/. + self._srctree_prefix = realpath(self.srctree) + os.sep + + self.warn = warn + self.warn_to_stderr = warn_to_stderr + self.warn_assign_undef = os.getenv("KCONFIG_WARN_UNDEF_ASSIGN") == "y" + self.warn_assign_override = True + self.warn_assign_redun = True + self._warn_assign_no_prompt = True + + self.warnings = [] + + self.config_prefix = os.getenv("CONFIG_", "CONFIG_") + # Regular expressions for parsing .config files + self._set_match = _re_match(self.config_prefix + r"([^=]+)=(.*)") + self._unset_match = _re_match(r"# {}([^ ]+) is not set".format( + self.config_prefix)) + + self.syms = {} + self.const_syms = {} + self.defined_syms = [] + self.missing_syms = [] + self.named_choices = {} + self.choices = [] + self.menus = [] + self.comments = [] + + for nmy in "n", "m", "y": + sym = Symbol() + sym.kconfig = self + sym.name = nmy + sym.is_constant = True + sym.orig_type = TRISTATE + sym._cached_tri_val = STR_TO_TRI[nmy] + + self.const_syms[nmy] = sym + + self.n = self.const_syms["n"] + self.m = self.const_syms["m"] + self.y = self.const_syms["y"] + + # Make n/m/y well-formed symbols + for nmy in "n", "m", "y": + sym = self.const_syms[nmy] + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + # Maps preprocessor variables names to Variable instances + self.variables = {} + + # Predefined preprocessor functions, with min/max number of arguments + self._functions = { + "info": (_info_fn, 1, 1), + "error-if": (_error_if_fn, 2, 2), + "filename": (_filename_fn, 0, 0), + "lineno": (_lineno_fn, 0, 0), + "shell": (_shell_fn, 1, 1), + "warning-if": (_warning_if_fn, 2, 2), + } + + # Add any user-defined preprocessor functions + try: + self._functions.update( + importlib.import_module( + os.getenv("KCONFIG_FUNCTIONS", "kconfigfunctions") + ).functions) + except ImportError: + pass + + # This determines whether previously unseen symbols are registered. + # They shouldn't be if we parse expressions after parsing, as part of + # Kconfig.eval_string(). + self._parsing_kconfigs = True + + self.modules = self._lookup_sym("MODULES") + self.defconfig_list = None + + self.top_node = MenuNode() + self.top_node.kconfig = self + self.top_node.item = MENU + self.top_node.is_menuconfig = True + self.top_node.visibility = self.y + self.top_node.prompt = ("Main menu", self.y) + self.top_node.parent = None + self.top_node.dep = self.y + self.top_node.filename = filename + self.top_node.linenr = 1 + self.top_node.include_path = () + + # Parse the Kconfig files + + # Not used internally. Provided as a convenience. + self.kconfig_filenames = [filename] + self.env_vars = set() + + # Keeps track of the location in the parent Kconfig files. Kconfig + # files usually source other Kconfig files. See _enter_file(). + self._filestack = [] + self._include_path = () + + # The current parsing location + self._filename = filename + self._linenr = 0 + + # Used to avoid retokenizing lines when we discover that they're not + # part of the construct currently being parsed. This is kinda like an + # unget operation. + self._reuse_tokens = False + + # Open the top-level Kconfig file. Store the readline() method directly + # as a small optimization. + self._readline = self._open(join(self.srctree, filename), "r").readline + + try: + # Parse the Kconfig files + self._parse_block(None, self.top_node, self.top_node) + self.top_node.list = self.top_node.next + self.top_node.next = None + except UnicodeDecodeError as e: + _decoding_error(e, self._filename) + + # Close the top-level Kconfig file. __self__ fetches the 'file' object + # for the method. + self._readline.__self__.close() + + self._parsing_kconfigs = False + + # Do various menu tree post-processing + self._finalize_node(self.top_node, self.y) + + self.unique_defined_syms = _ordered_unique(self.defined_syms) + self.unique_choices = _ordered_unique(self.choices) + + # Do sanity checks. Some of these depend on everything being finalized. + self._check_sym_sanity() + self._check_choice_sanity() + + # KCONFIG_STRICT is an older alias for KCONFIG_WARN_UNDEF, supported + # for backwards compatibility + if os.getenv("KCONFIG_WARN_UNDEF") == "y" or \ + os.getenv("KCONFIG_STRICT") == "y": + + self._check_undef_syms() + + # Build Symbol._dependents for all symbols and choices + self._build_dep() + + # Check for dependency loops + check_dep_loop_sym = _check_dep_loop_sym # Micro-optimization + for sym in self.unique_defined_syms: + check_dep_loop_sym(sym, False) + + # Add extra dependencies from choices to choice symbols that get + # awkward during dependency loop detection + self._add_choice_deps() + + @property + def mainmenu_text(self): + """ + See the class documentation. + """ + return self.top_node.prompt[0] + + @property + def defconfig_filename(self): + """ + See the class documentation. + """ + if self.defconfig_list: + for filename, cond in self.defconfig_list.defaults: + if expr_value(cond): + try: + with self._open_config(filename.str_value) as f: + return f.name + except EnvironmentError: + continue + + return None + + def load_config(self, filename=None, replace=True, verbose=None): + """ + Loads symbol values from a file in the .config format. Equivalent to + calling Symbol.set_value() to set each of the values. + + "# CONFIG_FOO is not set" within a .config file sets the user value of + FOO to n. The C tools work the same way. + + For each symbol, the Symbol.user_value attribute holds the value the + symbol was assigned in the .config file (if any). The user value might + differ from Symbol.str/tri_value if there are unsatisfied dependencies. + + Calling this function also updates the Kconfig.missing_syms attribute + with a list of all assignments to undefined symbols within the + configuration file. Kconfig.missing_syms is cleared if 'replace' is + True, and appended to otherwise. See the documentation for + Kconfig.missing_syms as well. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + filename (default: None): + Path to load configuration from (a string). Respects $srctree if set + (see the class documentation). + + If 'filename' is None (the default), the configuration file to load + (if any) is calculated automatically, giving the behavior you'd + usually want: + + 1. If the KCONFIG_CONFIG environment variable is set, it gives the + path to the configuration file to load. Otherwise, ".config" is + used. See standard_config_filename(). + + 2. If the path from (1.) doesn't exist, the configuration file + given by kconf.defconfig_filename is loaded instead, which is + derived from the 'option defconfig_list' symbol. + + 3. If (1.) and (2.) fail to find a configuration file to load, no + configuration file is loaded, and symbols retain their current + values (e.g., their default values). This is not an error. + + See the return value as well. + + replace (default: True): + If True, all existing user values will be cleared before loading the + .config. Pass False to merge configurations. + + verbose (default: None): + Limited backwards compatibility to prevent crashes. A warning is + printed if anything but None is passed. + + Prior to Kconfiglib 12.0.0, this option enabled printing of messages + to stdout when 'filename' was None. A message is (always) returned + now instead, which is more flexible. + + Will probably be removed in some future version. + + Returns a string with a message saying which file got loaded (or + possibly that no file got loaded, when 'filename' is None). This is + meant to reduce boilerplate in tools, which can do e.g. + print(kconf.load_config()). The returned message distinguishes between + loading (replace == True) and merging (replace == False). + """ + if verbose is not None: + _warn_verbose_deprecated("load_config") + + msg = None + if filename is None: + filename = standard_config_filename() + if not exists(filename) and \ + not exists(join(self.srctree, filename)): + defconfig = self.defconfig_filename + if defconfig is None: + return "Using default symbol values (no '{}')" \ + .format(filename) + + msg = " default configuration '{}' (no '{}')" \ + .format(defconfig, filename) + filename = defconfig + + if not msg: + msg = " configuration '{}'".format(filename) + + # Disable the warning about assigning to symbols without prompts. This + # is normal and expected within a .config file. + self._warn_assign_no_prompt = False + + # This stub only exists to make sure _warn_assign_no_prompt gets + # reenabled + try: + self._load_config(filename, replace) + except UnicodeDecodeError as e: + _decoding_error(e, filename) + finally: + self._warn_assign_no_prompt = True + + return ("Loaded" if replace else "Merged") + msg + + def _load_config(self, filename, replace): + with self._open_config(filename) as f: + if replace: + self.missing_syms = [] + + # If we're replacing the configuration, keep track of which + # symbols and choices got set so that we can unset the rest + # later. This avoids invalidating everything and is faster. + # Another benefit is that invalidation must be rock solid for + # it to work, making it a good test. + + for sym in self.unique_defined_syms: + sym._was_set = False + + for choice in self.unique_choices: + choice._was_set = False + + # Small optimizations + set_match = self._set_match + unset_match = self._unset_match + get_sym = self.syms.get + + for linenr, line in enumerate(f, 1): + # The C tools ignore trailing whitespace + line = line.rstrip() + + match = set_match(line) + if match: + name, val = match.groups() + sym = get_sym(name) + if not sym or not sym.nodes: + self._undef_assign(name, val, filename, linenr) + continue + + if sym.orig_type in _BOOL_TRISTATE: + # The C implementation only checks the first character + # to the right of '=', for whatever reason + if not (sym.orig_type is BOOL + and val.startswith(("y", "n")) or + sym.orig_type is TRISTATE + and val.startswith(("y", "m", "n"))): + self._warn("'{}' is not a valid value for the {} " + "symbol {}. Assignment ignored." + .format(val, TYPE_TO_STR[sym.orig_type], + _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) + sym = get_sym(name) + if not sym or not sym.nodes: + self._undef_assign(name, "n", filename, linenr) + continue + + if sym.orig_type not in _BOOL_TRISTATE: + continue + + val = "n" + + # Done parsing the assignment. Set the value. + + if sym._was_set: + self._assigned_twice(sym, val, filename, linenr) + + sym.set_value(val) + + if replace: + # If we're replacing the configuration, unset the symbols that + # didn't get set + + for sym in self.unique_defined_syms: + if not sym._was_set: + sym.unset_value() + + for choice in self.unique_choices: + if not choice._was_set: + choice.unset_value() + + def _undef_assign(self, name, val, filename, linenr): + # Called for assignments to undefined symbols during .config loading + + self.missing_syms.append((name, val)) + if self.warn_assign_undef: + self._warn( + "attempt to assign the value '{}' to the undefined symbol {}" + .format(val, name), filename, linenr) + + def _assigned_twice(self, sym, new_val, filename, linenr): + # Called when a symbol is assigned more than once in a .config file + + # Use strings for bool/tristate user values in the warning + if sym.orig_type in _BOOL_TRISTATE: + user_val = TRI_TO_STR[sym.user_value] + else: + user_val = sym.user_value + + msg = '{} set more than once. Old value "{}", new value "{}".'.format( + _name_and_loc(sym), user_val, new_val) + + if user_val == new_val: + if self.warn_assign_redun: + self._warn(msg, filename, linenr) + elif self.warn_assign_override: + self._warn(msg, filename, linenr) + + def 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. + + If 'filename' exists and its contents is identical to what would get + written out, it is left untouched. This avoids updating file metadata + like the modification time and possibly triggering redundant work in + build tools. + + filename: + 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. + """ + self._write_if_changed(filename, self._autoconf_contents(header)) + + def _autoconf_contents(self, header): + # write_autoconf() helper. Returns the contents to write as a string, + # with 'header' at the beginning. + + # "".join()ed later + chunks = [header] + add = chunks.append + + for sym in self.unique_defined_syms: + # _write_to_conf is determined when the value is calculated. This + # is a hidden function call due to property magic. + # + # Note: In client code, you can check if sym.config_string is empty + # instead, to avoid accessing the internal _write_to_conf variable + # (though it's likely to keep working). + val = sym.str_value + if not sym._write_to_conf: + continue + + if sym.orig_type in _BOOL_TRISTATE: + if val == "y": + add("#define {}{} 1\n" + .format(self.config_prefix, sym.name)) + elif val == "m": + add("#define {}{}_MODULE 1\n" + .format(self.config_prefix, sym.name)) + + elif sym.orig_type is STRING: + add('#define {}{} "{}"\n' + .format(self.config_prefix, sym.name, escape(val))) + + else: # sym.orig_type in _INT_HEX: + if sym.orig_type is HEX and \ + not val.startswith(("0x", "0X")): + val = "0x" + val + + add("#define {}{} {}\n" + .format(self.config_prefix, sym.name, val)) + + return "".join(chunks) + + def write_config(self, filename=None, + header="# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n", + save_old=True, verbose=None): + r""" + Writes out symbol values in the .config format. The format matches the + C implementation, including ordering. + + Symbols appear in the same order in generated .config files as they do + in the Kconfig files. For symbols defined in multiple locations, a + single assignment is written out corresponding to the first location + where the symbol is defined. + + See the 'Intro to symbol values' section in the module docstring to + understand which symbols get written out. + + If 'filename' exists and its contents is identical to what would get + written out, it is left untouched. This avoids updating file metadata + like the modification time and possibly triggering redundant work in + build tools. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + filename (default: None): + Filename to save configuration to (a string). + + If None (the default), the filename in the environment variable + KCONFIG_CONFIG is used if set, and ".config" otherwise. See + standard_config_filename(). + + 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. + + save_old (default: True): + If True and <filename> already exists, a copy of it will be saved to + <filename>.old in the same directory before the new configuration is + written. + + Errors are silently ignored if <filename>.old cannot be written (e.g. + due to being a directory, or <filename> being something like + /dev/null). + + verbose (default: None): + Limited backwards compatibility to prevent crashes. A warning is + printed if anything but None is passed. + + Prior to Kconfiglib 12.0.0, this option enabled printing of messages + to stdout when 'filename' was None. A message is (always) returned + now instead, which is more flexible. + + Will probably be removed in some future version. + + Returns a string with a message saying which file got saved. This is + meant to reduce boilerplate in tools, which can do e.g. + print(kconf.write_config()). + """ + if verbose is not None: + _warn_verbose_deprecated("write_config") + + if filename is None: + filename = standard_config_filename() + + contents = self._config_contents(header) + if self._contents_eq(filename, contents): + return "No change to '{}'".format(filename) + + if save_old: + _save_old(filename) + + with self._open(filename, "w") as f: + f.write(contents) + + return "Configuration saved to '{}'".format(filename) + + def _config_contents(self, header): + # write_config() helper. Returns the contents to write as a string, + # with 'header' at the beginning. + # + # More memory friendly would be to 'yield' the strings and + # "".join(_config_contents()), but it was a bit slower on my system. + + # node_iter() was used here before commit 3aea9f7 ("Add '# end of + # <menu>' after menus in .config"). Those comments get tricky to + # implement with it. + + for sym in self.unique_defined_syms: + sym._visited = False + + # Did we just print an '# end of ...' comment? + after_end_comment = False + + # "".join()ed later + chunks = [header] + add = chunks.append + + node = self.top_node + while 1: + # Jump to the next node with an iterative tree walk + if node.list: + node = node.list + elif node.next: + node = node.next + else: + while node.parent: + node = node.parent + + # Add a comment when leaving visible menus + if node.item is MENU and expr_value(node.dep) and \ + expr_value(node.visibility) and \ + node is not self.top_node: + add("# end of {}\n".format(node.prompt[0])) + after_end_comment = True + + if node.next: + node = node.next + break + else: + # No more nodes + return "".join(chunks) + + # Generate configuration output for the node + + item = node.item + + if item.__class__ is Symbol: + if item._visited: + continue + item._visited = True + + conf_string = item.config_string + if not conf_string: + continue + + if after_end_comment: + # Add a blank line before the first symbol printed after an + # '# end of ...' comment + after_end_comment = False + add("\n") + add(conf_string) + + elif expr_value(node.dep) and \ + ((item is MENU and expr_value(node.visibility)) or + item is COMMENT): + + add("\n#\n# {}\n#\n".format(node.prompt[0])) + after_end_comment = False + + def write_min_config(self, filename, + header="# 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. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + 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. + + Returns a string with a message saying which file got saved. This is + meant to reduce boilerplate in tools, which can do e.g. + print(kconf.write_min_config()). + """ + contents = self._min_config_contents(header) + if self._contents_eq(filename, contents): + return "No change to '{}'".format(filename) + + with self._open(filename, "w") as f: + f.write(contents) + + return "Minimal configuration saved to '{}'".format(filename) + + def _min_config_contents(self, header): + # write_min_config() helper. Returns the contents to write as a string, + # with 'header' at the beginning. + + chunks = [header] + add = chunks.append + + for sym in self.unique_defined_syms: + # Skip symbols that cannot be changed. Only check + # non-choice symbols, as selects don't affect choice + # symbols. + if not sym.choice and \ + sym.visibility <= expr_value(sym.rev_dep): + continue + + # Skip symbols whose value matches their default + if sym.str_value == sym._str_default(): + continue + + # Skip symbols that would be selected by default in a + # choice, unless the choice is optional or the symbol type + # isn't bool (it might be possible to set the choice mode + # to n or the symbol to m in those cases). + if sym.choice and \ + not sym.choice.is_optional and \ + sym.choice._selection_from_defaults() is sym and \ + sym.orig_type is BOOL and \ + sym.tri_value == 2: + continue + + add(sym.config_string) + + return "".join(chunks) + + def sync_deps(self, path): + """ + Creates or updates a directory structure that can be used to avoid + doing a full rebuild whenever the configuration is changed, mirroring + include/config/ in the kernel. + + This function is intended to be called during each build, before + compiling source files that depend on configuration symbols. + + See the Kconfig.__init__() docstring for raised exceptions + (OSError/IOError). KconfigError is never raised here. + + path: + Path to directory + + sync_deps(path) does the following: + + 1. If the directory <path> does not exist, it is created. + + 2. If <path>/auto.conf exists, old symbol values are loaded from it, + which are then compared against the current symbol values. If a + symbol has changed value (would generate different output in + autoconf.h compared to before), the change is signaled by + touch'ing a file corresponding to the symbol. + + The first time sync_deps() is run on a directory, <path>/auto.conf + won't exist, and no old symbol values will be available. This + logically has the same effect as updating the entire + configuration. + + The path to a symbol's file is calculated from the symbol's name + by replacing all '_' with '/' and appending '.h'. For example, the + symbol FOO_BAR_BAZ gets the file <path>/foo/bar/baz.h, and FOO + gets the file <path>/foo.h. + + This scheme matches the C tools. The point is to avoid having a + single directory with a huge number of files, which the underlying + filesystem might not handle well. + + 3. A new auto.conf with the current symbol values is written, to keep + track of them for the next build. + + If auto.conf exists and its contents is identical to what would + get written out, it is left untouched. This avoids updating file + metadata like the modification time and possibly triggering + redundant work in build tools. + + + The last piece of the puzzle is knowing what symbols each source file + depends on. Knowing that, dependencies can be added from source files + to the files corresponding to the symbols they depends on. The source + file will then get recompiled (only) when the symbol value changes + (provided sync_deps() is run first during each build). + + The tool in the kernel that extracts symbol dependencies from source + files is scripts/basic/fixdep.c. Missing symbol files also correspond + to "not changed", which fixdep deals with by using the $(wildcard) Make + function when adding symbol prerequisites to source files. + + In case you need a different scheme for your project, the sync_deps() + implementation can be used as a template. + """ + if not exists(path): + os.mkdir(path, 0o755) + + # Load old values from auto.conf, if any + self._load_old_vals(path) + + for sym in self.unique_defined_syms: + # _write_to_conf is determined when the value is calculated. This + # is a hidden function call due to property magic. + # + # Note: In client code, you can check if sym.config_string is empty + # instead, to avoid accessing the internal _write_to_conf variable + # (though it's likely to keep working). + val = sym.str_value + + # n tristate values do not get written to auto.conf and autoconf.h, + # making a missing symbol logically equivalent to n + + if sym._write_to_conf: + if sym._old_val is None and \ + sym.orig_type in _BOOL_TRISTATE and \ + val == "n": + # No old value (the symbol was missing or n), new value n. + # No change. + continue + + if val == sym._old_val: + # New value matches old. No change. + continue + + elif sym._old_val is None: + # The symbol wouldn't appear in autoconf.h (because + # _write_to_conf is false), and it wouldn't have appeared in + # autoconf.h previously either (because it didn't appear in + # auto.conf). No change. + continue + + # 'sym' has a new value. Flag it. + _touch_dep_file(path, sym.name) + + # Remember the current values as the "new old" values. + # + # This call could go anywhere after the call to _load_old_vals(), but + # putting it last means _sync_deps() can be safely rerun if it fails + # before this point. + self._write_old_vals(path) + + def _load_old_vals(self, path): + # Loads old symbol values from auto.conf into a dedicated + # Symbol._old_val field. Mirrors load_config(). + # + # The extra field could be avoided with some trickery involving dumping + # symbol values and restoring them later, but this is simpler and + # faster. The C tools also use a dedicated field for this purpose. + + for sym in self.unique_defined_syms: + sym._old_val = None + + try: + auto_conf = self._open(join(path, "auto.conf"), "r") + except EnvironmentError as e: + if e.errno == errno.ENOENT: + # No old values + return + raise + + with auto_conf as f: + for line in f: + match = self._set_match(line) + if not match: + # We only expect CONFIG_FOO=... (and possibly a header + # comment) in auto.conf + continue + + name, val = match.groups() + if name in self.syms: + sym = self.syms[name] + + if sym.orig_type is STRING: + match = _conf_string_match(val) + if not match: + continue + val = unescape(match.group(1)) + + self.syms[name]._old_val = val + else: + # Flag that the symbol no longer exists, in + # case something still depends on it + _touch_dep_file(path, name) + + def _write_old_vals(self, path): + # Helper for writing auto.conf. Basically just a simplified + # write_config() that doesn't write any comments (including + # '# CONFIG_FOO is not set' comments). The format matches the C + # implementation, though the ordering is arbitrary there (depends on + # the hash table implementation). + # + # A separate helper function is neater than complicating write_config() + # by passing a flag to it, plus we only need to look at symbols here. + + self._write_if_changed( + os.path.join(path, "auto.conf"), + self._old_vals_contents()) + + def _old_vals_contents(self): + # _write_old_vals() helper. Returns the contents to write as a string. + + # Temporary list instead of generator makes this a bit faster + return "".join([ + sym.config_string for sym in self.unique_defined_syms + if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value) + ]) + + def node_iter(self, unique_syms=False): + """ + Returns a generator for iterating through all MenuNode's in the Kconfig + tree. The iteration is done in Kconfig definition order (each node is + visited before its children, and the children of a node are visited + before the next node). + + The Kconfig.top_node menu node is skipped. It contains an implicit menu + that holds the top-level items. + + As an example, the following code will produce a list equal to + Kconfig.defined_syms: + + defined_syms = [node.item for node in kconf.node_iter() + if isinstance(node.item, Symbol)] + + unique_syms (default: False): + If True, only the first MenuNode will be included for symbols defined + in multiple locations. + + Using kconf.node_iter(True) in the example above would give a list + equal to unique_defined_syms. + """ + if unique_syms: + for sym in self.unique_defined_syms: + sym._visited = False + + node = self.top_node + while 1: + # Jump to the next node with an iterative tree walk + if node.list: + node = node.list + elif node.next: + node = node.next + else: + while node.parent: + node = node.parent + if node.next: + node = node.next + break + else: + # No more nodes + return + + if unique_syms and node.item.__class__ is Symbol: + if node.item._visited: + continue + node.item._visited = True + + yield node + + def eval_string(self, s): + """ + Returns the tristate value of the expression 's', represented as 0, 1, + and 2 for n, m, and y, respectively. Raises KconfigError on syntax + errors. Warns if undefined symbols are referenced. + + As an example, if FOO and BAR are tristate symbols at least one of + which has the value y, then eval_string("y && (FOO || BAR)") returns + 2 (y). + + To get the string value of non-bool/tristate symbols, use + Symbol.str_value. eval_string() always returns a tristate value, and + all non-bool/tristate symbols have the tristate value 0 (n). + + The expression parsing is consistent with how parsing works for + conditional ('if ...') expressions in the configuration, and matches + the C implementation. m is rewritten to 'm && MODULES', so + eval_string("m") will return 0 (n) unless modules are enabled. + """ + # The parser is optimized to be fast when parsing Kconfig files (where + # an expression can never appear at the beginning of a line). We have + # to monkey-patch things a bit here to reuse it. + + self._filename = None + + self._tokens = self._tokenize("if " + s) + # Strip "if " to avoid giving confusing error messages + self._line = s + self._tokens_i = 1 # Skip the 'if' token + + return expr_value(self._expect_expr_and_eol()) + + def unset_values(self): + """ + Removes any user values from all symbols, as if Kconfig.load_config() + or Symbol.set_value() had never been called. + """ + self._warn_assign_no_prompt = False + try: + # set_value() already rejects undefined symbols, and they don't + # need to be invalidated (because their value never changes), so we + # can just iterate over defined symbols + for sym in self.unique_defined_syms: + sym.unset_value() + + for choice in self.unique_choices: + choice.unset_value() + finally: + self._warn_assign_no_prompt = True + + def enable_warnings(self): + """ + Do 'Kconfig.warn = True' instead. Maintained for backwards + compatibility. + """ + self.warn = True + + def disable_warnings(self): + """ + Do 'Kconfig.warn = False' instead. Maintained for backwards + compatibility. + """ + self.warn = False + + def enable_stderr_warnings(self): + """ + Do 'Kconfig.warn_to_stderr = True' instead. Maintained for backwards + compatibility. + """ + self.warn_to_stderr = True + + def disable_stderr_warnings(self): + """ + Do 'Kconfig.warn_to_stderr = False' instead. Maintained for backwards + compatibility. + """ + self.warn_to_stderr = False + + def enable_undef_warnings(self): + """ + Do 'Kconfig.warn_assign_undef = True' instead. Maintained for backwards + compatibility. + """ + self.warn_assign_undef = True + + def disable_undef_warnings(self): + """ + Do 'Kconfig.warn_assign_undef = False' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_undef = False + + def enable_override_warnings(self): + """ + Do 'Kconfig.warn_assign_override = True' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_override = True + + def disable_override_warnings(self): + """ + Do 'Kconfig.warn_assign_override = False' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_override = False + + def enable_redun_warnings(self): + """ + Do 'Kconfig.warn_assign_redun = True' instead. Maintained for backwards + compatibility. + """ + self.warn_assign_redun = True + + def disable_redun_warnings(self): + """ + Do 'Kconfig.warn_assign_redun = False' instead. Maintained for + backwards compatibility. + """ + self.warn_assign_redun = False + + def __repr__(self): + """ + Returns a string with information about the Kconfig object when it is + evaluated on e.g. the interactive Python prompt. + """ + def status(flag): + return "enabled" if flag else "disabled" + + return "<{}>".format(", ".join(( + "configuration with {} symbols".format(len(self.syms)), + 'main menu prompt "{}"'.format(self.mainmenu_text), + "srctree is current directory" if not self.srctree else + 'srctree "{}"'.format(self.srctree), + 'config symbol prefix "{}"'.format(self.config_prefix), + "warnings " + status(self.warn), + "printing of warnings to stderr " + status(self.warn_to_stderr), + "undef. symbol assignment warnings " + + status(self.warn_assign_undef), + "overriding symbol assignment warnings " + + status(self.warn_assign_override), + "redundant symbol assignment warnings " + + status(self.warn_assign_redun) + ))) + + # + # Private methods + # + + + # + # File reading + # + + def _open_config(self, filename): + # Opens a .config file. First tries to open 'filename', then + # '$srctree/filename' if $srctree was set when the configuration was + # loaded. + + try: + return self._open(filename, "r") + except EnvironmentError as e: + # This will try opening the same file twice if $srctree is unset, + # but it's not a big deal + try: + return self._open(join(self.srctree, filename), "r") + except EnvironmentError as e2: + # This is needed for Python 3, because e2 is deleted after + # the try block: + # + # https://docs.python.org/3/reference/compound_stmts.html#the-try-statement + e = e2 + + raise _KconfigIOError( + e, "Could not open '{}' ({}: {}). Check that the $srctree " + "environment variable ({}) is set correctly." + .format(filename, errno.errorcode[e.errno], e.strerror, + "set to '{}'".format(self.srctree) if self.srctree + else "unset or blank")) + + def _enter_file(self, filename): + # Jumps to the beginning of a sourced Kconfig file, saving the previous + # position and file object. + # + # filename: + # Absolute path to file + + # Path relative to $srctree, stored in e.g. self._filename + # (which makes it indirectly show up in MenuNode.filename). Equals + # 'filename' for absolute paths passed to 'source'. + if filename.startswith(self._srctree_prefix): + # Relative path (or a redundant absolute path to within $srctree, + # but it's probably fine to reduce those too) + rel_filename = filename[len(self._srctree_prefix):] + else: + # Absolute path + rel_filename = filename + + self.kconfig_filenames.append(rel_filename) + + # The parent Kconfig files are represented as a list of + # (<include path>, <Python 'file' object for Kconfig file>) tuples. + # + # <include path> is immutable and holds a *tuple* of + # (<filename>, <linenr>) tuples, giving the locations of the 'source' + # statements in the parent Kconfig files. The current include path is + # also available in Kconfig._include_path. + # + # The point of this redundant setup is to allow Kconfig._include_path + # to be assigned directly to MenuNode.include_path without having to + # copy it, sharing it wherever possible. + + # Save include path and 'file' object (via its 'readline' function) + # before entering the file + self._filestack.append((self._include_path, self._readline)) + + # _include_path is a tuple, so this rebinds the variable instead of + # doing in-place modification + self._include_path += ((self._filename, self._linenr),) + + # Check for recursive 'source' + for name, _ in self._include_path: + if name == rel_filename: + raise KconfigError( + "\n{}:{}: recursive 'source' of '{}' detected. Check that " + "environment variables are set correctly.\n" + "Include path:\n{}" + .format(self._filename, self._linenr, rel_filename, + "\n".join("{}:{}".format(name, linenr) + for name, linenr in self._include_path))) + + try: + self._readline = self._open(filename, "r").readline + except EnvironmentError as e: + # We already know that the file exists + raise _KconfigIOError( + e, "{}:{}: Could not open '{}' (in '{}') ({}: {})" + .format(self._filename, self._linenr, filename, + self._line.strip(), + errno.errorcode[e.errno], e.strerror)) + + self._filename = rel_filename + self._linenr = 0 + + def _leave_file(self): + # Returns from a Kconfig file to the file that sourced it. See + # _enter_file(). + + # Restore location from parent Kconfig file + self._filename, self._linenr = self._include_path[-1] + # Restore include path and 'file' object + self._readline.__self__.close() # __self__ fetches the 'file' object + self._include_path, self._readline = self._filestack.pop() + + def _next_line(self): + # Fetches and tokenizes the next line from the current Kconfig file. + # Returns False at EOF and True otherwise. + + # We might already have tokens from parsing a line and discovering that + # it's part of a different construct + if self._reuse_tokens: + self._reuse_tokens = False + # self._tokens_i is known to be 1 here, because _parse_properties() + # leaves it like that when it can't recognize a line (or parses + # a help text) + return True + + # readline() returns '' over and over at EOF, which we rely on for help + # texts at the end of files (see _line_after_help()) + line = self._readline() + if not line: + return False + self._linenr += 1 + + # Handle line joining + while line.endswith("\\\n"): + line = line[:-2] + self._readline() + self._linenr += 1 + + self._tokens = self._tokenize(line) + # Initialize to 1 instead of 0 to factor out code from _parse_block() + # and _parse_properties(). They immediately fetch self._tokens[0]. + self._tokens_i = 1 + + return True + + def _line_after_help(self, line): + # Tokenizes a line after a help text. This case is special in that the + # line has already been fetched (to discover that it isn't part of the + # help text). + # + # An earlier version used a _saved_line variable instead that was + # checked in _next_line(). This special-casing gets rid of it and makes + # _reuse_tokens alone sufficient to handle unget. + + # Handle line joining + while line.endswith("\\\n"): + line = line[:-2] + self._readline() + self._linenr += 1 + + self._tokens = self._tokenize(line) + self._reuse_tokens = True + + def _write_if_changed(self, filename, contents): + # Writes 'contents' into 'filename', but only if it differs from the + # current contents of the file. + # + # Another variant would be write a temporary file on the same + # filesystem, compare the files, and rename() the temporary file if it + # differs, but it breaks stuff like write_config("/dev/null"), which is + # used out there to force evaluation-related warnings to be generated. + # This simple version is pretty failsafe and portable. + + if not self._contents_eq(filename, contents): + with self._open(filename, "w") as f: + f.write(contents) + + def _contents_eq(self, filename, contents): + # Returns True if the contents of 'filename' is 'contents' (a string), + # and False otherwise (including if 'filename' can't be opened/read) + + try: + with self._open(filename, "r") as f: + # Robust re. things like encoding and line endings (mmap() + # trickery isn't) + return f.read(len(contents) + 1) == contents + except EnvironmentError: + # If the error here would prevent writing the file as well, we'll + # notice it later + return False + + # + # Tokenization + # + + def _lookup_sym(self, name): + # Fetches the symbol 'name' from the symbol table, creating and + # registering it if it does not exist. If '_parsing_kconfigs' is False, + # it means we're in eval_string(), and new symbols won't be registered. + + if name in self.syms: + return self.syms[name] + + sym = Symbol() + sym.kconfig = self + sym.name = name + sym.is_constant = False + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + if self._parsing_kconfigs: + self.syms[name] = sym + else: + self._warn("no symbol {} in configuration".format(name)) + + return sym + + def _lookup_const_sym(self, name): + # Like _lookup_sym(), for constant (quoted) symbols + + if name in self.const_syms: + return self.const_syms[name] + + sym = Symbol() + sym.kconfig = self + sym.name = name + sym.is_constant = True + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + if self._parsing_kconfigs: + self.const_syms[name] = sym + + return sym + + def _tokenize(self, s): + # Parses 's', returning a None-terminated list of tokens. Registers any + # new symbols encountered with _lookup(_const)_sym(). + # + # Tries to be reasonably speedy by processing chunks of text via + # regexes and string operations where possible. This is the biggest + # hotspot during parsing. + # + # It might be possible to rewrite this to 'yield' tokens instead, + # working across multiple lines. Lookback and compatibility with old + # janky versions of the C tools complicate things though. + + self._line = s # Used for error reporting + + # Initial token on the line + match = _command_match(s) + if not match: + if s.isspace() or s.lstrip().startswith("#"): + return (None,) + self._parse_error("unknown token at start of line") + + # Tricky implementation detail: While parsing a token, 'token' refers + # to the previous token. See _STRING_LEX for why this is needed. + token = _get_keyword(match.group(1)) + if not token: + # Backwards compatibility with old versions of the C tools, which + # (accidentally) accepted stuff like "--help--" and "-help---". + # This was fixed in the C tools by commit c2264564 ("kconfig: warn + # of unhandled characters in Kconfig commands"), committed in July + # 2015, but it seems people still run Kconfiglib on older kernels. + if s.strip(" \t\n-") == "help": + return (_T_HELP, None) + + # If the first token is not a keyword (and not a weird help token), + # we have a preprocessor variable assignment (or a bare macro on a + # line) + self._parse_assignment(s) + return (None,) + + tokens = [token] + # The current index in the string being tokenized + i = match.end() + + # Main tokenization loop (for tokens past the first one) + while i < len(s): + # Test for an identifier/keyword first. This is the most common + # case. + match = _id_keyword_match(s, i) + if match: + # We have an identifier or keyword + + # Check what it is. lookup_sym() will take care of allocating + # new symbols for us the first time we see them. Note that + # 'token' still refers to the previous token. + + name = match.group(1) + keyword = _get_keyword(name) + if keyword: + # It's a keyword + token = keyword + # Jump past it + i = match.end() + + elif token not in _STRING_LEX: + # It's a non-const symbol, except we translate n, m, and y + # into the corresponding constant symbols, like the C + # implementation + + if "$" in name: + # Macro expansion within symbol name + name, s, i = self._expand_name(s, i) + else: + i = match.end() + + token = self.const_syms[name] if name in STR_TO_TRI else \ + self._lookup_sym(name) + + else: + # It's a case of missing quotes. For example, the + # following is accepted: + # + # menu unquoted_title + # + # config A + # tristate unquoted_prompt + # + # endmenu + # + # Named choices ('choice FOO') also end up here. + + if token is not _T_CHOICE: + self._warn("style: quotes recommended around '{}' in '{}'" + .format(name, self._line.strip()), + self._filename, self._linenr) + + token = name + i = match.end() + + else: + # Neither a keyword nor a non-const symbol + + # We always strip whitespace after tokens, so it is safe to + # assume that s[i] is the start of a token here. + c = s[i] + + if c in "\"'": + if "$" not in s and "\\" not in s: + # Fast path for lines without $ and \. Find the + # matching quote. + end_i = s.find(c, i + 1) + 1 + if not end_i: + self._parse_error("unterminated string") + val = s[i + 1:end_i - 1] + i = end_i + else: + # Slow path + s, end_i = self._expand_str(s, i) + + # os.path.expandvars() and the $UNAME_RELEASE replace() + # is a backwards compatibility hack, which should be + # reasonably safe as expandvars() leaves references to + # undefined env. vars. as is. + # + # The preprocessor functionality changed how + # environment variables are referenced, to $(FOO). + val = expandvars(s[i + 1:end_i - 1] + .replace("$UNAME_RELEASE", + _UNAME_RELEASE)) + + i = end_i + + # This is the only place where we don't survive with a + # single token of lookback: 'option env="FOO"' does not + # refer to a constant symbol named "FOO". + token = \ + val if token in _STRING_LEX or tokens[0] is _T_OPTION \ + else self._lookup_const_sym(val) + + elif s.startswith("&&", i): + token = _T_AND + i += 2 + + elif s.startswith("||", i): + token = _T_OR + i += 2 + + elif c == "=": + token = _T_EQUAL + i += 1 + + elif s.startswith("!=", i): + token = _T_UNEQUAL + i += 2 + + elif c == "!": + token = _T_NOT + i += 1 + + elif c == "(": + token = _T_OPEN_PAREN + i += 1 + + elif c == ")": + token = _T_CLOSE_PAREN + i += 1 + + elif c == "#": + break + + + # Very rare + + elif s.startswith("<=", i): + token = _T_LESS_EQUAL + i += 2 + + elif c == "<": + token = _T_LESS + i += 1 + + elif s.startswith(">=", i): + token = _T_GREATER_EQUAL + i += 2 + + elif c == ">": + token = _T_GREATER + i += 1 + + + else: + self._parse_error("unknown tokens in line") + + + # Skip trailing whitespace + while i < len(s) and s[i].isspace(): + i += 1 + + + # Add the token + tokens.append(token) + + # None-terminating the token list makes token fetching simpler/faster + tokens.append(None) + + return tokens + + # Helpers for syntax checking and token fetching. See the + # 'Intro to expressions' section for what a constant symbol is. + # + # More of these could be added, but the single-use cases are inlined as an + # optimization. + + def _expect_sym(self): + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + + if token.__class__ is not Symbol: + self._parse_error("expected symbol") + + return token + + def _expect_nonconst_sym(self): + # Used for 'select' and 'imply' only. We know the token indices. + + token = self._tokens[1] + self._tokens_i = 2 + + if token.__class__ is not Symbol or token.is_constant: + self._parse_error("expected nonconstant symbol") + + return token + + def _expect_str_and_eol(self): + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + + if token.__class__ is not str: + self._parse_error("expected string") + + if self._tokens[self._tokens_i] is not None: + self._trailing_tokens_error() + + return token + + def _expect_expr_and_eol(self): + expr = self._parse_expr(True) + + if self._tokens[self._tokens_i] is not None: + self._trailing_tokens_error() + + return expr + + def _check_token(self, token): + # If the next token is 'token', removes it and returns True + + if self._tokens[self._tokens_i] is token: + self._tokens_i += 1 + return True + return False + + # + # Preprocessor logic + # + + def _parse_assignment(self, s): + # Parses a preprocessor variable assignment, registering the variable + # if it doesn't already exist. Also takes care of bare macros on lines + # (which are allowed, and can be useful for their side effects). + + # Expand any macros in the left-hand side of the assignment (the + # variable name) + s = s.lstrip() + i = 0 + while 1: + i = _assignment_lhs_fragment_match(s, i).end() + if s.startswith("$(", i): + s, i = self._expand_macro(s, i, ()) + else: + break + + if s.isspace(): + # We also accept a bare macro on a line (e.g. + # $(warning-if,$(foo),ops)), provided it expands to a blank string + return + + # Assigned variable + name = s[:i] + + + # Extract assignment operator (=, :=, or +=) and value + rhs_match = _assignment_rhs_match(s, i) + if not rhs_match: + self._parse_error("syntax error") + + op, val = rhs_match.groups() + + + if name in self.variables: + # Already seen variable + var = self.variables[name] + else: + # New variable + var = Variable() + var.kconfig = self + var.name = name + var._n_expansions = 0 + self.variables[name] = var + + # += acts like = on undefined variables (defines a recursive + # variable) + if op == "+=": + op = "=" + + if op == "=": + var.is_recursive = True + var.value = val + elif op == ":=": + var.is_recursive = False + var.value = self._expand_whole(val, ()) + else: # op == "+=" + # += does immediate expansion if the variable was last set + # with := + var.value += " " + (val if var.is_recursive else + self._expand_whole(val, ())) + + def _expand_whole(self, s, args): + # Expands preprocessor macros in all of 's'. Used whenever we don't + # have to worry about delimiters. See _expand_macro() re. the 'args' + # parameter. + # + # Returns the expanded string. + + i = 0 + while 1: + i = s.find("$(", i) + if i == -1: + break + s, i = self._expand_macro(s, i, args) + return s + + def _expand_name(self, s, i): + # Expands a symbol name starting at index 'i' in 's'. + # + # Returns the expanded name, the expanded 's' (including the part + # before the name), and the index of the first character in the next + # token after the name. + + s, end_i = self._expand_name_iter(s, i) + name = s[i:end_i] + # isspace() is False for empty strings + if not name.strip(): + # Avoid creating a Kconfig symbol with a blank name. It's almost + # guaranteed to be an error. + self._parse_error("macro expanded to blank string") + + # Skip trailing whitespace + while end_i < len(s) and s[end_i].isspace(): + end_i += 1 + + return name, s, end_i + + def _expand_name_iter(self, s, i): + # Expands a symbol name starting at index 'i' in 's'. + # + # Returns the expanded 's' (including the part before the name) and the + # index of the first character after the expanded name in 's'. + + while 1: + match = _name_special_search(s, i) + + if match.group() == "$(": + s, i = self._expand_macro(s, match.start(), ()) + else: + return (s, match.start()) + + def _expand_str(self, s, i): + # Expands a quoted string starting at index 'i' in 's'. Handles both + # backslash escapes and macro expansion. + # + # Returns the expanded 's' (including the part before the string) and + # the index of the first character after the expanded string in 's'. + + quote = s[i] + i += 1 # Skip over initial "/' + while 1: + match = _string_special_search(s, i) + if not match: + self._parse_error("unterminated string") + + + if match.group() == quote: + # Found the end of the string + return (s, match.end()) + + elif match.group() == "\\": + # Replace '\x' with 'x'. 'i' ends up pointing to the character + # after 'x', which allows macros to be canceled with '\$(foo)'. + i = match.end() + s = s[:match.start()] + s[i:] + + elif match.group() == "$(": + # A macro call within the string + s, i = self._expand_macro(s, match.start(), ()) + + else: + # A ' quote within " quotes or vice versa + i += 1 + + def _expand_macro(self, s, i, args): + # Expands a macro starting at index 'i' in 's'. If this macro resulted + # from the expansion of another macro, 'args' holds the arguments + # passed to that macro. + # + # Returns the expanded 's' (including the part before the macro) and + # the index of the first character after the expanded macro in 's'. + + 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 or user-defined function + + py_fn, min_arg, max_arg = self._functions[fn] + + if len(args) - 1 < min_arg or \ + (max_arg is not None and len(args) - 1 > max_arg): + + if min_arg == max_arg: + expected_args = min_arg + elif max_arg is None: + expected_args = "{} or more".format(min_arg) + else: + expected_args = "{}-{}".format(min_arg, max_arg) + + raise KconfigError("{}:{}: bad number of arguments in call " + "to {}, expected {}, got {}" + .format(self._filename, self._linenr, fn, + expected_args, len(args) - 1)) + + return py_fn(self, *args) + + # Environment variables are tried last + if fn in os.environ: + self.env_vars.add(fn) + return os.environ[fn] + + return "" + + # + # Parsing + # + + def _make_and(self, e1, e2): + # Constructs an AND (&&) expression. Performs trivial simplification. + + if e1 is self.y: + return e2 + + if e2 is self.y: + return e1 + + if e1 is self.n or e2 is self.n: + return self.n + + return (AND, e1, e2) + + def _make_or(self, e1, e2): + # Constructs an OR (||) expression. Performs trivial simplification. + + if e1 is self.n: + return e2 + + if e2 is self.n: + return e1 + + if e1 is self.y or e2 is self.y: + return self.y + + return (OR, e1, e2) + + def _parse_block(self, end_token, parent, prev): + # Parses a block, which is the contents of either a file or an if, + # menu, or choice statement. + # + # end_token: + # The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. + # None for files. + # + # parent: + # The parent menu node, corresponding to a menu, Choice, or 'if'. + # 'if's are flattened after parsing. + # + # prev: + # The previous menu node. New nodes will be added after this one (by + # modifying 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. + + while self._next_line(): + t0 = self._tokens[0] + + if t0 is _T_CONFIG or t0 is _T_MENUCONFIG: + # The tokenizer allocates Symbol objects for us + sym = self._tokens[1] + + if sym.__class__ is not Symbol or sym.is_constant: + self._parse_error("missing or bad symbol name") + + if self._tokens[2] is not None: + self._trailing_tokens_error() + + self.defined_syms.append(sym) + + node = MenuNode() + node.kconfig = self + node.item = sym + node.is_menuconfig = (t0 is _T_MENUCONFIG) + node.prompt = node.help = node.list = None + node.parent = parent + node.filename = self._filename + node.linenr = self._linenr + node.include_path = self._include_path + + sym.nodes.append(node) + + self._parse_properties(node) + + if node.is_menuconfig and not node.prompt: + self._warn("the menuconfig symbol {} has no prompt" + .format(_name_and_loc(sym))) + + # Equivalent to + # + # prev.next = node + # prev = node + # + # due to tricky Python semantics. The order matters. + prev.next = prev = node + + elif t0 is None: + # Blank line + continue + + elif t0 in _SOURCE_TOKENS: + pattern = self._expect_str_and_eol() + + if t0 in _REL_SOURCE_TOKENS: + # Relative source + pattern = join(dirname(self._filename), pattern) + + # - glob() doesn't support globbing relative to a directory, so + # we need to prepend $srctree to 'pattern'. Use join() + # instead of '+' so that an absolute path in 'pattern' is + # preserved. + # + # - Sort the glob results to ensure a consistent ordering of + # Kconfig symbols, which indirectly ensures a consistent + # ordering in e.g. .config files + filenames = sorted(iglob(join(self._srctree_prefix, pattern))) + + if not filenames and t0 in _OBL_SOURCE_TOKENS: + raise KconfigError( + "{}:{}: '{}' not found (in '{}'). Check that " + "environment variables are set correctly (e.g. " + "$srctree, which is {}). Also note that unset " + "environment variables expand to the empty string." + .format(self._filename, self._linenr, pattern, + self._line.strip(), + "set to '{}'".format(self.srctree) + if self.srctree else "unset or blank")) + + for filename in filenames: + self._enter_file(filename) + prev = self._parse_block(None, parent, prev) + self._leave_file() + + elif t0 is end_token: + # Reached the end of the block. Terminate the final node and + # return it. + + if self._tokens[1] is not None: + self._trailing_tokens_error() + + prev.next = None + return prev + + elif t0 is _T_IF: + node = MenuNode() + node.item = node.prompt = None + node.parent = parent + node.dep = self._expect_expr_and_eol() + + self._parse_block(_T_ENDIF, node, node) + node.list = node.next + + prev.next = prev = node + + elif t0 is _T_MENU: + node = MenuNode() + node.kconfig = self + node.item = t0 # _T_MENU == MENU + node.is_menuconfig = True + node.prompt = (self._expect_str_and_eol(), self.y) + node.visibility = self.y + node.parent = parent + node.filename = self._filename + node.linenr = self._linenr + node.include_path = self._include_path + + self.menus.append(node) + + self._parse_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 = t0 # _T_COMMENT == COMMENT + node.is_menuconfig = False + node.prompt = (self._expect_str_and_eol(), self.y) + node.list = None + node.parent = parent + node.filename = self._filename + node.linenr = self._linenr + node.include_path = self._include_path + + self.comments.append(node) + + self._parse_properties(node) + + prev.next = prev = node + + elif t0 is _T_CHOICE: + if self._tokens[1] is None: + choice = Choice() + choice.direct_dep = self.n + else: + # Named choice + name = self._expect_str_and_eol() + choice = self.named_choices.get(name) + if not choice: + choice = Choice() + choice.name = name + choice.direct_dep = self.n + self.named_choices[name] = choice + + self.choices.append(choice) + + node = MenuNode() + node.kconfig = choice.kconfig = self + node.item = choice + node.is_menuconfig = True + node.prompt = node.help = None + node.parent = parent + node.filename = self._filename + node.linenr = self._linenr + node.include_path = self._include_path + + choice.nodes.append(node) + + self._parse_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) + + else: + # A valid endchoice/endif/endmenu is caught by the 'end_token' + # check above + self._parse_error( + "no corresponding 'choice'" if t0 is _T_ENDCHOICE else + "no corresponding 'if'" if t0 is _T_ENDIF else + "no corresponding 'menu'" if t0 is _T_ENDMENU else + "unrecognized construct") + + # End of file reached. Terminate the final node and return it. + + if end_token: + raise KconfigError( + "expected '{}' at end of '{}'" + .format("endchoice" if end_token is _T_ENDCHOICE else + "endif" if end_token is _T_ENDIF else + "endmenu", + 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 + + expr = self._parse_expr(True) if self._check_token(_T_IF) else self.y + + if self._tokens[self._tokens_i] is not None: + self._trailing_tokens_error() + + return expr + + def _parse_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 e.g. + # _add_props_to_sym(). + # + # An older version of this code added properties directly to symbols + # and choices instead of to their menu nodes (and handled dependency + # propagation simultaneously), but that loses information on where a + # property is added when a symbol or choice is defined in multiple + # locations. Some Kconfig configuration systems rely heavily on such + # symbols, and better docs can be generated by keeping track of where + # properties are added. + # + # node: + # The menu node we're parsing properties on + + # Dependencies from 'depends on'. Will get propagated to the properties + # below. + node.dep = self.y + + while self._next_line(): + t0 = self._tokens[0] + + if t0 in _TYPE_TOKENS: + # Relies on '_T_BOOL is BOOL', etc., to save a conversion + self._set_type(node, t0) + if self._tokens[1] is not None: + self._parse_prompt(node) + + elif t0 is _T_DEPENDS: + if not self._check_token(_T_ON): + self._parse_error("expected 'on' after 'depends'") + + node.dep = self._make_and(node.dep, + self._expect_expr_and_eol()) + + elif t0 is _T_HELP: + self._parse_help(node) + + elif t0 is _T_SELECT: + if node.item.__class__ is not Symbol: + self._parse_error("only symbols can select") + + node.selects.append((self._expect_nonconst_sym(), + self._parse_cond())) + + elif t0 is None: + # Blank line + continue + + elif t0 is _T_DEFAULT: + node.defaults.append((self._parse_expr(False), + self._parse_cond())) + + elif t0 in _DEF_TOKEN_TO_TYPE: + self._set_type(node, _DEF_TOKEN_TO_TYPE[t0]) + node.defaults.append((self._parse_expr(False), + self._parse_cond())) + + elif t0 is _T_PROMPT: + self._parse_prompt(node) + + elif t0 is _T_RANGE: + node.ranges.append((self._expect_sym(), self._expect_sym(), + self._parse_cond())) + + elif t0 is _T_IMPLY: + if node.item.__class__ is not Symbol: + self._parse_error("only symbols can imply") + + node.implies.append((self._expect_nonconst_sym(), + self._parse_cond())) + + elif t0 is _T_VISIBLE: + if not self._check_token(_T_IF): + self._parse_error("expected 'if' after 'visible'") + + node.visibility = self._make_and(node.visibility, + self._expect_expr_and_eol()) + + elif t0 is _T_OPTION: + if self._check_token(_T_ENV): + if not self._check_token(_T_EQUAL): + self._parse_error("expected '=' after 'env'") + + env_var = self._expect_str_and_eol() + node.item.env_var = env_var + + if env_var in os.environ: + node.defaults.append( + (self._lookup_const_sym(os.environ[env_var]), + self.y)) + else: + self._warn("{1} has 'option env=\"{0}\"', " + "but the environment variable {0} is not " + "set".format(node.item.name, env_var), + self._filename, self._linenr) + + if env_var != node.item.name: + self._warn("Kconfiglib expands environment variables " + "in strings directly, meaning you do not " + "need 'option env=...' \"bounce\" symbols. " + "For compatibility with the C tools, " + "rename {} to {} (so that the symbol name " + "matches the environment variable name)." + .format(node.item.name, env_var), + self._filename, self._linenr) + + elif self._check_token(_T_DEFCONFIG_LIST): + if not self.defconfig_list: + self.defconfig_list = node.item + else: + self._warn("'option defconfig_list' set on multiple " + "symbols ({0} and {1}). Only {0} will be " + "used.".format(self.defconfig_list.name, + node.item.name), + self._filename, self._linenr) + + elif self._check_token(_T_MODULES): + # To reduce warning spam, only warn if 'option modules' is + # set on some symbol that isn't MODULES, which should be + # safe. I haven't run into any projects that make use + # modules besides the kernel yet, and there it's likely to + # keep being called "MODULES". + if node.item is not self.modules: + self._warn("the 'modules' option is not supported. " + "Let me know if this is a problem for you, " + "as it wouldn't be that hard to implement. " + "Note that modules are supported -- " + "Kconfiglib just assumes the symbol name " + "MODULES, like older versions of the C " + "implementation did when 'option modules' " + "wasn't used.", + self._filename, self._linenr) + + elif self._check_token(_T_ALLNOCONFIG_Y): + if node.item.__class__ is not Symbol: + self._parse_error("the 'allnoconfig_y' option is only " + "valid for symbols") + + node.item.is_allnoconfig_y = True + + else: + self._parse_error("unrecognized option") + + elif t0 is _T_OPTIONAL: + if node.item.__class__ is not Choice: + self._parse_error('"optional" is only valid for choices') + + node.item.is_optional = True + + else: + # Reuse the tokens for the non-property line later + self._reuse_tokens = True + return + + def _set_type(self, node, new_type): + # UNKNOWN is falsy + if node.item.orig_type and node.item.orig_type is not 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._tokens[1] + self._tokens_i = 2 + + if prompt.__class__ is not str: + self._parse_error("expected prompt string") + + 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): + 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") + + # Micro-optimization. This code is pretty hot. + readline = self._readline + + # Find first non-blank (not all-space) line and get its + # indentation + + while 1: + line = readline() + self._linenr += 1 + if not line: + self._empty_help(node, line) + return + if not line.isspace(): + break + + len_ = len # Micro-optimization + + # Use a separate 'expline' variable here and below to avoid stomping on + # any tabs people might've put deliberately into the first line after + # the help text + expline = line.expandtabs() + indent = len_(expline) - len_(expline.lstrip()) + if not indent: + self._empty_help(node, line) + return + + # The help text goes on till the first non-blank line with less indent + # than the first line + + # Add the first line + lines = [expline[indent:]] + add_line = lines.append # Micro-optimization + + while 1: + line = readline() + if line.isspace(): + # No need to preserve the exact whitespace in these + add_line("\n") + elif not line: + # End of file + break + else: + expline = line.expandtabs() + if len_(expline) - len_(expline.lstrip()) < indent: + break + add_line(expline[indent:]) + + self._linenr += len_(lines) + node.help = "".join(lines).rstrip() + if line: + self._line_after_help(line) + + def _empty_help(self, node, line): + self._warn(_name_and_loc(node.item) + + " has 'help' but empty help text") + node.help = "" + if line: + self._line_after_help(line) + + def _parse_expr(self, transform_m): + # Parses an expression from the tokens in Kconfig._tokens using a + # simple top-down approach. See the module docstring for the expression + # format. + # + # transform_m: + # True if m should be rewritten to m && MODULES. See the + # Kconfig.eval_string() documentation. + + # Grammar: + # + # expr: and_expr ['||' expr] + # and_expr: factor ['&&' and_expr] + # factor: <symbol> ['='/'!='/'<'/... <symbol>] + # '!' factor + # '(' expr ')' + # + # It helps to think of the 'expr: and_expr' case as a single-operand OR + # (no ||), and of the 'and_expr: factor' case as a single-operand AND + # (no &&). Parsing code is always a bit tricky. + + # Mind dump: parse_factor() and two nested loops for OR and AND would + # work as well. The straightforward implementation there gives a + # (op, (op, (op, A, B), C), D) parse for A op B op C op D. Representing + # expressions as (op, [list of operands]) instead goes nicely with that + # version, but is wasteful for short expressions and complicates + # expression evaluation and other code that works on expressions (more + # complicated code likely offsets any performance gain from less + # recursion too). If we also try to optimize the list representation by + # merging lists when possible (e.g. when ANDing two AND expressions), + # we end up allocating a ton of lists instead of reusing expressions, + # which is bad. + + and_expr = self._parse_and_expr(transform_m) + + # Return 'and_expr' directly if we have a "single-operand" OR. + # Otherwise, parse the expression on the right and make an OR node. + # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). + return and_expr if not self._check_token(_T_OR) else \ + (OR, and_expr, self._parse_expr(transform_m)) + + def _parse_and_expr(self, transform_m): + factor = self._parse_factor(transform_m) + + # Return 'factor' directly if we have a "single-operand" AND. + # Otherwise, parse the right operand and make an AND node. This turns + # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). + return factor if not self._check_token(_T_AND) else \ + (AND, factor, self._parse_and_expr(transform_m)) + + def _parse_factor(self, transform_m): + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + + if token.__class__ is Symbol: + # Plain symbol or relation + + if self._tokens[self._tokens_i] not in _RELATIONS: + # Plain symbol + + # For conditional expressions ('depends on <expr>', + # '... if <expr>', etc.), m is rewritten to m && MODULES. + if transform_m and token is self.m: + return (AND, self.m, self.modules) + + return token + + # Relation + # + # _T_EQUAL, _T_UNEQUAL, etc., deliberately have the same values as + # EQUAL, UNEQUAL, etc., so we can just use the token directly + self._tokens_i += 1 + return (self._tokens[self._tokens_i - 1], token, + self._expect_sym()) + + if token is _T_NOT: + # token == _T_NOT == NOT + return (token, self._parse_factor(transform_m)) + + if token is _T_OPEN_PAREN: + expr_parse = self._parse_expr(transform_m) + if self._check_token(_T_CLOSE_PAREN): + return expr_parse + + self._parse_error("malformed expression") + + # + # Caching and invalidation + # + + def _build_dep(self): + # Populates the Symbol/Choice._dependents sets, which contain all other + # items (symbols and choices) that immediately depend on the item in + # the sense that changing the value of the item might affect the value + # of the dependent items. This is used for caching/invalidation. + # + # The calculated sets might be larger than necessary as we don't do any + # complex analysis of the expressions. + + make_depend_on = _make_depend_on # Micro-optimization + + # Only calculate _dependents for defined symbols. Constant and + # undefined symbols could theoretically be selected/implied, but it + # wouldn't change their value, so it's not a true dependency. + for sym in self.unique_defined_syms: + # Symbols depend on the following: + + # The prompt conditions + for node in sym.nodes: + if node.prompt: + 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: + for sym in choice.syms: + sym._dependents.add(choice) + + def _invalidate_all(self): + # Undefined symbols never change value and don't need to be + # invalidated, so we can just iterate over defined symbols. + # Invalidating constant symbols would break things horribly. + for sym in self.unique_defined_syms: + sym._invalidate() + + for choice in self.unique_choices: + choice._invalidate() + + # + # Post-parsing menu tree processing, including dependency propagation and + # implicit submenu creation + # + + def _finalize_node(self, node, visible_if): + # Finalizes a menu node and its children: + # + # - Copies properties from menu nodes up to their contained + # symbols/choices + # + # - Propagates dependencies from parent to child nodes + # + # - Creates implicit menus (see kconfig-language.txt) + # + # - Removes 'if' nodes + # + # - Sets 'choice' types and registers choice symbols + # + # menu_finalize() in the C implementation is similar. + # + # node: + # The menu node to finalize. This node and its children will have + # been finalized when the function returns, and any implicit menus + # will have been created. + # + # visible_if: + # Dependencies from 'visible if' on parent menus. These are added to + # the prompts of symbols and choices. + + if node.item.__class__ is Symbol: + # Copy defaults, ranges, selects, and implies to the Symbol + self._add_props_to_sym(node) + + # Find any items that should go in an implicit menu rooted at the + # symbol + cur = node + while cur.next and _auto_menu_dep(node, cur.next): + # This makes implicit submenu creation work recursively, with + # implicit menus inside implicit menus + self._finalize_node(cur.next, visible_if) + cur = cur.next + cur.parent = node + + if cur is not node: + # Found symbols that should go in an implicit submenu. Tilt + # them up above us. + node.list = node.next + node.next = cur.next + cur.next = None + + elif node.list: + # The menu node is a choice, menu, or if. Finalize each child node. + + if node.item is MENU: + visible_if = self._make_and(visible_if, node.visibility) + + # Propagate the menu node's dependencies to each child menu node. + # + # This needs to go before the recursive _finalize_node() call so + # that implicit submenu creation can look ahead at dependencies. + self._propagate_deps(node, visible_if) + + # Finalize the children + cur = node.list + while cur: + self._finalize_node(cur, visible_if) + cur = cur.next + + if node.list: + # node's children have been individually finalized. Do final steps + # to finalize this "level" in the menu tree. + _flatten(node.list) + _remove_ifs(node) + + # Empty choices (node.list None) are possible, so this needs to go + # outside + if node.item.__class__ is Choice: + # Add the node's non-node-specific properties to the choice, like + # _add_props_to_sym() does + choice = node.item + choice.direct_dep = self._make_or(choice.direct_dep, node.dep) + choice.defaults += node.defaults + + _finalize_choice(node) + + def _propagate_deps(self, node, visible_if): + # Propagates 'node's dependencies to its child menu nodes + + # If the parent node holds a Choice, we use the Choice itself as the + # parent dependency. This makes sense as the value (mode) of the choice + # limits the visibility of the contained choice symbols. The C + # implementation works the same way. + # + # Due to the similar interface, Choice works as a drop-in replacement + # for Symbol here. + basedep = node.item if node.item.__class__ is Choice else node.dep + + cur = node.list + while cur: + dep = cur.dep = self._make_and(cur.dep, basedep) + + if cur.item.__class__ in _SYMBOL_CHOICE: + # Propagate 'visible if' and dependencies to the prompt + if cur.prompt: + cur.prompt = (cur.prompt[0], + self._make_and( + cur.prompt[1], + self._make_and(visible_if, dep))) + + # Propagate dependencies to defaults + if cur.defaults: + cur.defaults = [(default, self._make_and(cond, dep)) + for default, cond in cur.defaults] + + # Propagate dependencies to ranges + if cur.ranges: + cur.ranges = [(low, high, self._make_and(cond, dep)) + for low, high, cond in cur.ranges] + + # Propagate dependencies to selects + if cur.selects: + cur.selects = [(target, self._make_and(cond, dep)) + for target, cond in cur.selects] + + # Propagate dependencies to implies + if cur.implies: + cur.implies = [(target, self._make_and(cond, dep)) + for target, cond in cur.implies] + + elif cur.prompt: # Not a symbol/choice + # Propagate dependencies to the prompt. 'visible if' is only + # propagated to symbols/choices. + cur.prompt = (cur.prompt[0], + self._make_and(cur.prompt[1], dep)) + + cur = cur.next + + def _add_props_to_sym(self, node): + # Copies properties from the menu node 'node' up to its contained + # symbol, and adds (weak) reverse dependencies to selected/implied + # symbols. + # + # This can't be rolled into _propagate_deps(), because that function + # traverses the menu tree roughly breadth-first, meaning properties on + # symbols defined in multiple locations could end up in the wrong + # order. + + sym = node.item + + # See the Symbol class docstring + sym.direct_dep = self._make_or(sym.direct_dep, node.dep) + + sym.defaults += node.defaults + sym.ranges += node.ranges + sym.selects += node.selects + sym.implies += node.implies + + # Modify the reverse dependencies of the selected symbol + for target, cond in node.selects: + target.rev_dep = self._make_or( + target.rev_dep, + self._make_and(sym, cond)) + + # Modify the weak reverse dependencies of the implied + # symbol + for target, cond in node.implies: + target.weak_rev_dep = self._make_or( + target.weak_rev_dep, + self._make_and(sym, cond)) + + # + # Misc. + # + + def _check_sym_sanity(self): + # Checks various symbol properties that are handiest to check after + # parsing. Only generates errors and warnings. + + def num_ok(sym, type_): + # Returns True if the (possibly constant) symbol 'sym' is valid as a value + # for a symbol of type type_ (INT or HEX) + + # 'not sym.nodes' implies a constant or undefined symbol, e.g. a plain + # "123" + if not sym.nodes: + return _is_base_n(sym.name, _TYPE_TO_BASE[type_]) + + return sym.orig_type is type_ + + for sym in self.unique_defined_syms: + if sym.orig_type in _BOOL_TRISTATE: + # A helper function could be factored out here, but keep it + # speedy/straightforward + + for target_sym, _ in sym.selects: + if target_sym.orig_type not in _BOOL_TRISTATE_UNKNOWN: + self._warn("{} selects the {} symbol {}, which is not " + "bool or tristate" + .format(_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: + self._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: # STRING/INT/HEX + for default, _ in sym.defaults: + if default.__class__ is not Symbol: + raise KconfigError( + "the {} symbol {} has a malformed default {} -- expected " + "a single symbol" + .format(TYPE_TO_STR[sym.orig_type], _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). + self._warn("style: quotes recommended around " + "default value for string symbol " + + _name_and_loc(sym)) + + elif not num_ok(default, sym.orig_type): # INT/HEX + self._warn("the {0} symbol {1} has a non-{0} default {2}" + .format(TYPE_TO_STR[sym.orig_type], + _name_and_loc(sym), + _name_and_loc(default))) + + if sym.selects or sym.implies: + self._warn("the {} symbol {} has selects or implies" + .format(TYPE_TO_STR[sym.orig_type], + _name_and_loc(sym))) + + else: # UNKNOWN + self._warn("{} defined without a type" + .format(_name_and_loc(sym))) + + + if sym.ranges: + if sym.orig_type not in _INT_HEX: + self._warn( + "the {} symbol {} has ranges, but is not int or hex" + .format(TYPE_TO_STR[sym.orig_type], + _name_and_loc(sym))) + else: + for low, high, _ in sym.ranges: + if not num_ok(low, sym.orig_type) or \ + not num_ok(high, sym.orig_type): + + self._warn("the {0} symbol {1} has a non-{0} " + "range [{2}, {3}]" + .format(TYPE_TO_STR[sym.orig_type], + _name_and_loc(sym), + _name_and_loc(low), + _name_and_loc(high))) + + def _check_choice_sanity(self): + # Checks various choice properties that are handiest to check after + # parsing. Only generates errors and warnings. + + def warn_select_imply(sym, expr, expr_type): + msg = "the choice symbol {} is {} by the following symbols, but " \ + "select/imply has no effect on choice symbols" \ + .format(_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]) + + self._warn(msg) + + for choice in self.unique_choices: + if choice.orig_type not in _BOOL_TRISTATE: + self._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: + self._warn(_name_and_loc(choice) + " defined without a prompt") + + for default, _ in choice.defaults: + if default.__class__ is not Symbol: + raise KconfigError( + "{} has a malformed default {}" + .format(_name_and_loc(choice), expr_str(default))) + + if default.choice is not choice: + self._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: + self._warn("default on the choice symbol {} will have " + "no effect, as defaults do not affect choice " + "symbols".format(_name_and_loc(sym))) + + if sym.rev_dep is not sym.kconfig.n: + warn_select_imply(sym, sym.rev_dep, "selected") + + if sym.weak_rev_dep is not sym.kconfig.n: + warn_select_imply(sym, sym.weak_rev_dep, "implied") + + for node in sym.nodes: + if node.parent.item is choice: + if not node.prompt: + self._warn("the choice symbol {} has no prompt" + .format(_name_and_loc(sym))) + + elif node.prompt: + self._warn("the choice symbol {} is defined with a " + "prompt outside the choice" + .format(_name_and_loc(sym))) + + def _parse_error(self, msg): + raise KconfigError("{}couldn't parse '{}': {}".format( + "" if self._filename is None else + "{}:{}: ".format(self._filename, self._linenr), + self._line.strip(), msg)) + + def _trailing_tokens_error(self): + self._parse_error("extra tokens at end of line") + + def _open(self, filename, mode): + # open() wrapper: + # + # - Enable universal newlines mode on Python 2 to ease + # interoperability between Linux and Windows. It's already the + # default on Python 3. + # + # The "U" flag would currently work for both Python 2 and 3, but it's + # deprecated on Python 3, so play it future-safe. + # + # io.open() defaults to universal newlines on Python 2 (and is an + # alias for open() on Python 3), but it returns 'unicode' strings and + # slows things down: + # + # Parsing x86 Kconfigs on Python 2 + # + # with open(..., "rU"): + # + # real 0m0.930s + # user 0m0.905s + # sys 0m0.025s + # + # with io.open(): + # + # real 0m1.069s + # user 0m1.040s + # sys 0m0.029s + # + # There's no appreciable performance difference between "r" and + # "rU" for parsing performance on Python 2. + # + # - For Python 3, force the encoding. Forcing the encoding on Python 2 + # turns strings into Unicode strings, which gets messy. Python 2 + # doesn't decode regular strings anyway. + return open(filename, "rU" if mode == "r" else mode) if _IS_PY2 else \ + open(filename, mode, encoding=self._encoding) + + def _check_undef_syms(self): + # Prints warnings for all references to undefined symbols within the + # Kconfig files + + def is_num(s): + # Returns True if the string 's' looks like a number. + # + # Internally, all operands in Kconfig are symbols, only undefined symbols + # (which numbers usually are) get their name as their value. + # + # Only hex numbers that start with 0x/0X are classified as numbers. + # Otherwise, symbols whose names happen to contain only the letters A-F + # would trigger false positives. + + try: + int(s) + except ValueError: + if not s.startswith(("0x", "0X")): + return False + + try: + int(s, 16) + except ValueError: + return False + + return True + + for sym in (self.syms.viewvalues if _IS_PY2 else self.syms.values)(): + # - sym.nodes empty means the symbol is undefined (has no + # definition locations) + # + # - Due to Kconfig internals, numbers show up as undefined Kconfig + # symbols, but shouldn't be flagged + # + # - The MODULES symbol always exists + if not sym.nodes and not is_num(sym.name) and \ + sym.name != "MODULES": + + msg = "undefined symbol {}:".format(sym.name) + for node in self.node_iter(): + if sym in node.referenced: + msg += "\n\n- Referenced at {}:{}:\n\n{}" \ + .format(node.filename, node.linenr, node) + self._warn(msg) + + def _warn(self, msg, filename=None, linenr=None): + # For printing general warnings + + if not self.warn: + return + + msg = "warning: " + msg + if filename is not None: + msg = "{}:{}: {}".format(filename, linenr, msg) + + self.warnings.append(msg) + if self.warn_to_stderr: + sys.stderr.write(msg + "\n") + + +class Symbol(object): + """ + Represents a configuration symbol: + + (menu)config FOO + ... + + The following attributes are available. They should be viewed as read-only, + and some are implemented through @property magic (but are still efficient + to access due to internal caching). + + Note: Prompts, help texts, and locations are stored in the Symbol's + MenuNode(s) rather than in the Symbol itself. Check the MenuNode class and + the Symbol.nodes attribute. This organization matches the C tools. + + name: + The name of the symbol, e.g. "FOO" for 'config FOO'. + + type: + The type of the symbol. One of BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN. + UNKNOWN is for undefined symbols, (non-special) constant symbols, and + symbols defined without a type. + + When running without modules (MODULES having the value n), TRISTATE + symbols magically change type to BOOL. This also happens for symbols + within choices in "y" mode. This matches the C tools, and makes sense for + menuconfig-like functionality. + + orig_type: + The type as given in the Kconfig file, without any magic applied. Used + when printing the symbol. + + 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. This includes all non-n-valued bool/tristate + symbols, and all visible string/int/hex symbols. + + Symbols with the (no longer needed) 'option env=...' option generate no + configuration output, and neither does the special + 'option defconfig_list' symbol. + + Tip: This field is useful when generating custom configuration output, + even for non-.config-like formats. To write just the symbols that would + get written out to .config files, do this: + + if sym.config_string: + *Write symbol, e.g. by looking sym.str_value* + + This is a superset of the symbols written out by write_autoconf(). + That function skips all n-valued symbols. + + There usually won't be any great harm in just writing all symbols either, + though you might get some special symbols and possibly some "redundant" + n-valued symbol entries in there. + + nodes: + A list of MenuNodes for this symbol. Will contain a single MenuNode for + most symbols. Undefined and constant symbols have an empty nodes list. + Symbols defined in multiple locations get one node for each location. + + choice: + Holds the parent Choice for choice symbols, and None for non-choice + symbols. Doubles as a flag for whether a symbol is a choice symbol. + + defaults: + List of (default, cond) tuples for the symbol's 'default' properties. For + example, 'default A && B if C || D' is represented as + ((AND, A, B), (OR, C, D)). If no condition was given, 'cond' is + self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to + 'default' conditions. + + selects: + List of (symbol, cond) tuples for the symbol's 'select' properties. For + example, 'select A if B && C' is represented as (A, (AND, B, C)). If no + condition was given, 'cond' is self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to 'select' + conditions. + + implies: + Like 'selects', for imply. + + ranges: + List of (low, high, cond) tuples for the symbol's 'range' properties. For + example, 'range 1 2 if A' is represented as (1, 2, A). If there is no + condition, 'cond' is self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to 'range' + conditions. + + Gotcha: 1 and 2 above will be represented as (undefined) Symbols rather + than plain integers. Undefined symbols get their name as their string + value, so this works out. The C tools work the same way. + + orig_defaults: + orig_selects: + orig_implies: + orig_ranges: + See the corresponding attributes on the MenuNode class. + + rev_dep: + Reverse dependency expression from other symbols selecting this symbol. + Multiple selections get ORed together. A condition on a select is ANDed + with the selecting symbol. + + For example, if A has 'select FOO' and B has 'select FOO if C', then + FOO's rev_dep will be (OR, A, (AND, B, C)). + + weak_rev_dep: + Like rev_dep, for imply. + + direct_dep: + The direct ('depends on') dependencies for the symbol, or self.kconfig.y + if there are no direct dependencies. + + This attribute includes any dependencies from surrounding menus and ifs. + Those get propagated to the direct dependencies, and the resulting direct + dependencies in turn get propagated to the conditions of all properties. + + If the symbol is defined in multiple locations, the dependencies from the + different locations get ORed together. + + referenced: + A set() with all symbols and choices referenced in the properties and + property conditions of the symbol. + + Also includes dependencies from surrounding menus and ifs, because those + get propagated to the symbol (see the 'Intro to symbol values' section in + the module docstring). + + Choices appear in the dependencies of choice symbols. + + For the following definitions, only B and not C appears in A's + 'referenced'. To get transitive references, you'll have to recursively + expand 'references' until no new items appear. + + config A + bool + depends on B + + config B + bool + depends on C + + config C + bool + + See the Symbol.direct_dep attribute if you're only interested in the + direct dependencies of the symbol (its 'depends on'). You can extract the + symbols in it with the global expr_items() function. + + env_var: + If the Symbol has an 'option env="FOO"' option, this contains the name + ("FOO") of the environment variable. None for symbols without no + 'option env'. + + 'option env="FOO"' acts like a 'default' property whose value is the + value of $FOO. + + Symbols with 'option env' are never written out to .config files, even if + they are visible. env_var corresponds to a flag called SYMBOL_AUTO in the + C implementation. + + is_allnoconfig_y: + True if the symbol has 'option allnoconfig_y' set on it. This has no + effect internally (except when printing symbols), but can be checked by + scripts. + + is_constant: + True if the symbol is a constant (quoted) symbol. + + kconfig: + The Kconfig instance this symbol is from. + """ + __slots__ = ( + "_cached_assignable", + "_cached_str_val", + "_cached_tri_val", + "_cached_vis", + "_dependents", + "_old_val", + "_visited", + "_was_set", + "_write_to_conf", + "choice", + "defaults", + "direct_dep", + "env_var", + "implies", + "is_allnoconfig_y", + "is_constant", + "kconfig", + "name", + "nodes", + "orig_type", + "ranges", + "rev_dep", + "selects", + "user_value", + "weak_rev_dep", + ) + + # + # Public interface + # + + @property + def type(self): + """ + See the class documentation. + """ + if self.orig_type is TRISTATE and \ + (self.choice and self.choice.tri_value == 2 or + not self.kconfig.modules.tri_value): + + return BOOL + + return self.orig_type + + @property + def str_value(self): + """ + See the class documentation. + """ + if self._cached_str_val is not None: + return self._cached_str_val + + if self.orig_type in _BOOL_TRISTATE: + # Also calculates the visibility, so invalidation safe + self._cached_str_val = TRI_TO_STR[self.tri_value] + return self._cached_str_val + + # As a quirk of Kconfig, undefined symbols get their name as their + # string value. This is why things like "FOO = bar" work for seeing if + # FOO has the value "bar". + if not self.orig_type: # UNKNOWN + self._cached_str_val = self.name + return self.name + + val = "" + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + + self._write_to_conf = (vis != 0) + + if self.orig_type in _INT_HEX: + # The C implementation checks the user value against the range in a + # separate code path (post-processing after loading a .config). + # Checking all values here instead makes more sense for us. It + # requires that we check for a range first. + + base = _TYPE_TO_BASE[self.orig_type] + + # Check if a range is in effect + for low_expr, high_expr, cond in self.ranges: + if expr_value(cond): + has_active_range = True + + # The zeros are from the C implementation running strtoll() + # on empty strings + low = int(low_expr.str_value, base) if \ + _is_base_n(low_expr.str_value, base) else 0 + high = int(high_expr.str_value, base) if \ + _is_base_n(high_expr.str_value, base) else 0 + + break + else: + has_active_range = False + + # Defaults are used if the symbol is invisible, lacks a user value, + # or has an out-of-range user value + use_defaults = True + + if vis and self.user_value: + user_val = int(self.user_value, base) + if has_active_range and not low <= user_val <= high: + num2str = str if base == 10 else hex + self.kconfig._warn( + "user value {} on the {} symbol {} ignored due to " + "being outside the active range ([{}, {}]) -- falling " + "back on defaults" + .format(num2str(user_val), TYPE_TO_STR[self.orig_type], + _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 sym, cond in self.defaults: + if expr_value(cond): + has_default = self._write_to_conf = True + + val = sym.str_value + + if _is_base_n(val, base): + val_num = int(val, base) + else: + val_num = 0 # strtoll() on empty string + + break + else: + val_num = 0 # strtoll() on empty string + + # This clamping procedure runs even if there's no default + if has_active_range: + clamp = None + if val_num < low: + clamp = low + elif val_num > high: + clamp = high + + if clamp is not None: + # The value is rewritten to a standard form if it is + # clamped + val = str(clamp) \ + if self.orig_type is INT else \ + hex(clamp) + + if has_default: + num2str = str if base == 10 else hex + self.kconfig._warn( + "default value {} on {} clamped to {} due to " + "being outside the active range ([{}, {}])" + .format(val_num, _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 sym, cond in self.defaults: + if expr_value(cond): + val = sym.str_value + self._write_to_conf = True + break + + # env_var corresponds to SYMBOL_AUTO in the C implementation, and is + # also set on the defconfig_list symbol there. Test for the + # defconfig_list symbol explicitly instead here, to avoid a nonsensical + # env_var setting and the defconfig_list symbol being printed + # incorrectly. This code is pretty cold anyway. + if self.env_var is not None or self is self.kconfig.defconfig_list: + self._write_to_conf = False + + self._cached_str_val = val + return val + + @property + def tri_value(self): + """ + See the class documentation. + """ + if self._cached_tri_val is not None: + return self._cached_tri_val + + if self.orig_type not in _BOOL_TRISTATE: + if self.orig_type: # != UNKNOWN + # Would take some work to give the location here + self.kconfig._warn( + "The {} symbol {} is being evaluated in a logical context " + "somewhere. It will always evaluate to n." + .format(TYPE_TO_STR[self.orig_type], _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: + dep_val = expr_value(cond) + if dep_val: + val = min(expr_value(default), dep_val) + if val: + self._write_to_conf = True + break + + # Weak reverse dependencies are only considered if our + # direct dependencies are met + dep_val = expr_value(self.weak_rev_dep) + if dep_val and expr_value(self.direct_dep): + val = max(dep_val, val) + self._write_to_conf = True + + # Reverse (select-related) dependencies take precedence + dep_val = expr_value(self.rev_dep) + if dep_val: + if expr_value(self.direct_dep) < dep_val: + self._warn_select_unsatisfied_deps() + + val = max(dep_val, val) + self._write_to_conf = True + + # m is promoted to y for (1) bool symbols and (2) symbols with a + # weak_rev_dep (from imply) of y + if val == 1 and \ + (self.type is BOOL or expr_value(self.weak_rev_dep) == 2): + val = 2 + + elif vis == 2: + # Visible choice symbol in y-mode choice. The choice mode limits + # the visibility of choice symbols, so it's sufficient to just + # check the visibility of the choice symbols themselves. + val = 2 if self.choice.selection is self else 0 + + elif vis and self.user_value: + # Visible choice symbol in m-mode choice, with set non-0 user value + val = 1 + + self._cached_tri_val = val + return val + + @property + def assignable(self): + """ + See the class documentation. + """ + if self._cached_assignable is None: + self._cached_assignable = self._assignable() + return self._cached_assignable + + @property + def visibility(self): + """ + See the class documentation. + """ + if self._cached_vis is None: + self._cached_vis = _visibility(self) + return self._cached_vis + + @property + def config_string(self): + """ + See the class documentation. + """ + # _write_to_conf is determined when the value is calculated. This is a + # hidden function call due to property magic. + val = self.str_value + if not self._write_to_conf: + return "" + + if self.orig_type in _BOOL_TRISTATE: + return "{}{}={}\n" \ + .format(self.kconfig.config_prefix, self.name, val) \ + if val != "n" else \ + "# {}{} is not set\n" \ + .format(self.kconfig.config_prefix, self.name) + + if self.orig_type in _INT_HEX: + return "{}{}={}\n" \ + .format(self.kconfig.config_prefix, self.name, val) + + # sym.orig_type is STRING + return '{}{}="{}"\n' \ + .format(self.kconfig.config_prefix, self.name, escape(val)) + + 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. + + Note that the value for an int/hex symbol is passed as a string, e.g. + "123" or "0x0123". The format of this string is preserved in the + output. + + Values that are invalid for the type (such as "foo" or 1 (m) for a + BOOL or "0x123" for an INT) are ignored and won't be stored in + Symbol.user_value. Kconfiglib will print a warning by default for + invalid assignments, and set_value() will return False. + + Returns True if the value is valid for the type of the symbol, and + False otherwise. This only looks at the form of the value. For BOOL and + TRISTATE symbols, check the Symbol.assignable attribute to see what + values are currently in range and would actually be reflected in the + value of the symbol. For other symbol types, check whether the + visibility is non-n. + """ + if self.orig_type in _BOOL_TRISTATE and value in STR_TO_TRI: + value = STR_TO_TRI[value] + + # If the new user value matches the old, nothing changes, and we can + # avoid invalidating cached values. + # + # This optimization is skipped for choice symbols: Setting a choice + # symbol's user value to y might change the state of the choice, so it + # wouldn't be safe (symbol user values always match the values set in a + # .config file or via set_value(), and are never implicitly updated). + if value == self.user_value and not self.choice: + self._was_set = True + return True + + # Check if the value is valid for our type + if not (self.orig_type is BOOL and value in (2, 0) or + self.orig_type is TRISTATE and value in TRI_TO_STR or + value.__class__ is str and + (self.orig_type is STRING or + self.orig_type is INT and _is_base_n(value, 10) or + self.orig_type is HEX and _is_base_n(value, 16) + and int(value, 16) >= 0)): + + # Display tristate values as n, m, y in the warning + self.kconfig._warn( + "the value {} is invalid for {}, which has type {} -- " + "assignment ignored" + .format(TRI_TO_STR[value] if value in TRI_TO_STR else + "'{}'".format(value), + _name_and_loc(self), TYPE_TO_STR[self.orig_type])) + + return False + + self.user_value = value + self._was_set = True + + if self.choice and value == 2: + # Setting a choice symbol to y makes it the user selection of the + # choice. Like for symbol user values, the user selection is not + # guaranteed to match the actual selection of the choice, as + # dependencies come into play. + self.choice.user_selection = self + self.choice._was_set = True + self.choice._rec_invalidate() + else: + self._rec_invalidate_if_has_prompt() + + return True + + def unset_value(self): + """ + Removes any user value from the symbol, as if the symbol had never + gotten a user value via Kconfig.load_config() or Symbol.set_value(). + """ + if self.user_value is not None: + self.user_value = None + self._rec_invalidate_if_has_prompt() + + @property + def referenced(self): + """ + See the class documentation. + """ + return {item for node in self.nodes for item in node.referenced} + + @property + def orig_defaults(self): + """ + See the class documentation. + """ + return [d for node in self.nodes for d in node.orig_defaults] + + @property + def orig_selects(self): + """ + See the class documentation. + """ + return [s for node in self.nodes for s in node.orig_selects] + + @property + def orig_implies(self): + """ + See the class documentation. + """ + return [i for node in self.nodes for i in node.orig_implies] + + @property + def orig_ranges(self): + """ + See the class documentation. + """ + return [r for node in self.nodes for r in node.orig_ranges] + + def __repr__(self): + """ + Returns a string with information about the symbol (including its name, + value, visibility, and location(s)) when it is evaluated on e.g. the + interactive Python prompt. + """ + fields = ["symbol " + self.name, TYPE_TO_STR[self.type]] + add = fields.append + + for node in self.nodes: + if node.prompt: + add('"{}"'.format(node.prompt[0])) + + # Only add quotes for non-bool/tristate symbols + add("value " + (self.str_value if self.orig_type in _BOOL_TRISTATE + else '"{}"'.format(self.str_value))) + + if not self.is_constant: + # These aren't helpful to show for constant symbols + + if self.user_value is not None: + # Only add quotes for non-bool/tristate symbols + add("user value " + (TRI_TO_STR[self.user_value] + if self.orig_type in _BOOL_TRISTATE + else '"{}"'.format(self.user_value))) + + add("visibility " + TRI_TO_STR[self.visibility]) + + if self.choice: + add("choice symbol") + + if self.is_allnoconfig_y: + add("allnoconfig_y") + + if self is self.kconfig.defconfig_list: + add("is the defconfig_list symbol") + + if self.env_var is not None: + add("from environment variable " + self.env_var) + + if self is self.kconfig.modules: + add("is the modules symbol") + + add("direct deps " + TRI_TO_STR[expr_value(self.direct_dep)]) + + if self.nodes: + for node in self.nodes: + add("{}:{}".format(node.filename, node.linenr)) + else: + add("constant" if self.is_constant else "undefined") + + return "<{}>".format(", ".join(fields)) + + def __str__(self): + """ + Returns a string representation of the symbol when it is printed. + Matches the Kconfig format, with any parent dependencies propagated to + the 'depends on' condition. + + The string is constructed by joining the strings returned by + MenuNode.__str__() for each of the symbol's menu nodes, so symbols + defined in multiple locations will return a string with all + definitions. + + The returned string does not end in a newline. An empty string is + returned for undefined and constant symbols. + """ + return self.custom_str(standard_sc_expr_str) + + def custom_str(self, sc_expr_str_fn): + """ + Works like Symbol.__str__(), but allows a custom format to be used for + all symbol/choice references. See expr_str(). + """ + return "\n\n".join(node.custom_str(sc_expr_str_fn) + for node in self.nodes) + + # + # Private methods + # + + def __init__(self): + """ + Symbol constructor -- not intended to be called directly by Kconfiglib + clients. + """ + # These attributes are always set on the instance from outside and + # don't need defaults: + # kconfig + # direct_dep + # is_constant + # name + # rev_dep + # weak_rev_dep + + # - UNKNOWN == 0 + # - _visited is used during tree iteration and dep. loop detection + self.orig_type = self._visited = 0 + + self.nodes = [] + + self.defaults = [] + self.selects = [] + self.implies = [] + self.ranges = [] + + self.user_value = \ + self.choice = \ + self.env_var = \ + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ + self._cached_assignable = None + + # _write_to_conf is calculated along with the value. If True, the + # Symbol gets a .config entry. + + self.is_allnoconfig_y = \ + self._was_set = \ + self._write_to_conf = False + + # See Kconfig._build_dep() + self._dependents = set() + + def _assignable(self): + # Worker function for the 'assignable' attribute + + if self.orig_type not in _BOOL_TRISTATE: + return () + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + if not vis: + return () + + rev_dep_val = expr_value(self.rev_dep) + + if vis == 2: + if self.choice: + return (2,) + + if not rev_dep_val: + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: + return (0, 2) + return (0, 1, 2) + + if rev_dep_val == 2: + return (2,) + + # rev_dep_val == 1 + + if self.type is BOOL or expr_value(self.weak_rev_dep) == 2: + return (2,) + return (1, 2) + + # vis == 1 + + # Must be a tristate here, because bool m visibility gets promoted to y + + if not rev_dep_val: + return (0, 1) if expr_value(self.weak_rev_dep) != 2 else (0, 2) + + if rev_dep_val == 2: + return (2,) + + # vis == rev_dep_val == 1 + + return (1,) + + def _invalidate(self): + # Marks the symbol as needing to be recalculated + + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ + self._cached_assignable = None + + def _rec_invalidate(self): + # Invalidates the symbol and all items that (possibly) depend on it + + if self is self.kconfig.modules: + # Invalidating MODULES has wide-ranging effects + self.kconfig._invalidate_all() + else: + self._invalidate() + + for item in self._dependents: + # _cached_vis doubles as a flag that tells us whether 'item' + # has cached values, because it's calculated as a side effect + # of calculating all other (non-constant) cached values. + # + # If item._cached_vis is None, it means there can't be cached + # values on other items that depend on 'item', because if there + # were, some value on 'item' would have been calculated and + # item._cached_vis set as a side effect. It's therefore safe to + # stop the invalidation at symbols with _cached_vis None. + # + # This approach massively speeds up scripts that set a lot of + # values, vs simply invalidating all possibly dependent symbols + # (even when you already have a list of all the dependent + # symbols, because some symbols get huge dependency trees). + # + # This gracefully handles dependency loops too, which is nice + # for choices, where the choice depends on the choice symbols + # and vice versa. + if item._cached_vis is not None: + item._rec_invalidate() + + def _rec_invalidate_if_has_prompt(self): + # Invalidates the symbol and its dependent symbols, but only if the + # symbol has a prompt. User values never have an effect on promptless + # symbols, so we skip invalidation for them as an optimization. + # + # This also prevents constant (quoted) symbols from being invalidated + # if set_value() is called on them, which would make them lose their + # value and break things. + # + # Prints a warning if the symbol has no prompt. In some contexts (e.g. + # when loading a .config files) assignments to promptless symbols are + # normal and expected, so the warning can be disabled. + + for node in self.nodes: + if node.prompt: + self._rec_invalidate() + return + + if self.kconfig._warn_assign_no_prompt: + self.kconfig._warn(_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: # 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 select.__class__ is tuple: + msg += ", and select condition {} (value: {})" \ + .format(expr_str(select[2]), + TRI_TO_STR[expr_value(select[2])]) + + self.kconfig._warn(msg) + + +class Choice(object): + """ + Represents a choice statement: + + choice + ... + endchoice + + The following attributes are available on Choice instances. They should be + treated as read-only, and some are implemented through @property magic (but + are still efficient to access due to internal caching). + + Note: Prompts, help texts, and locations are stored in the Choice's + MenuNode(s) rather than in the Choice itself. Check the MenuNode class and + the Choice.nodes attribute. This organization matches the C tools. + + name: + The name of the choice, e.g. "FOO" for 'choice FOO', or None if the + Choice has no name. + + type: + The type of the choice. One of BOOL, TRISTATE, UNKNOWN. UNKNOWN is for + choices defined without a type where none of the contained symbols have a + type either (otherwise the choice inherits the type of the first symbol + defined with a type). + + When running without modules (CONFIG_MODULES=n), TRISTATE choices + magically change type to BOOL. This matches the C tools, and makes sense + for menuconfig-like functionality. + + orig_type: + The type as given in the Kconfig file, without any magic applied. Used + when printing the choice. + + tri_value: + The tristate value (mode) of the choice. A choice can be in one of three + modes: + + 0 (n) - The choice is disabled and no symbols can be selected. For + visible choices, this mode is only possible for choices with + the 'optional' flag set (see kconfig-language.txt). + + 1 (m) - Any number of choice symbols can be set to m, the rest will + be n. + + 2 (y) - One symbol will be y, the rest n. + + Only tristate choices can be in m mode. The visibility of the choice is + an upper bound on the mode, and the mode in turn is an upper bound on the + visibility of the choice symbols. + + To change the mode, use Choice.set_value(). + + Implementation note: + The C tools internally represent choices as a type of symbol, with + special-casing in many code paths. This is why there is a lot of + similarity to Symbol. The value (mode) of a choice is really just a + normal symbol value, and an implicit reverse dependency forces its + lower bound to m for visible non-optional choices (the reverse + dependency is 'm && <visibility>'). + + Symbols within choices get the choice propagated as a dependency to + their properties. This turns the mode of the choice into an upper bound + on e.g. the visibility of choice symbols, and explains the gotcha + related to printing choice symbols mentioned in the module docstring. + + Kconfiglib uses a separate Choice class only because it makes the code + and interface less confusing (especially in a user-facing interface). + Corresponding attributes have the same name in the Symbol and Choice + classes, for consistency and compatibility. + + 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. + + Obscure gotcha: If a symbol depends on the previous symbol within a + choice so that an implicit menu is created, it won't be a choice symbol, + and won't be included in 'syms'. + + nodes: + A list of MenuNodes for this choice. In practice, the list will probably + always contain a single MenuNode, but it is possible to give a choice a + name and define it in multiple locations. + + defaults: + List of (symbol, cond) tuples for the choice's 'defaults' properties. For + example, 'default A if B && C' is represented as (A, (AND, B, C)). If + there is no condition, 'cond' is self.kconfig.y. + + Note that 'depends on' and parent dependencies are propagated to + 'default' conditions. + + orig_defaults: + See the corresponding attribute on the MenuNode class. + + direct_dep: + See Symbol.direct_dep. + + referenced: + A set() with all symbols referenced in the properties and property + conditions of the choice. + + Also includes dependencies from surrounding menus and ifs, because those + get propagated to the choice (see the 'Intro to symbol values' section in + the module docstring). + + is_optional: + True if the choice has the 'optional' flag set on it and can be in + n mode. + + kconfig: + The Kconfig instance this choice is from. + """ + __slots__ = ( + "_cached_assignable", + "_cached_selection", + "_cached_vis", + "_dependents", + "_visited", + "_was_set", + "defaults", + "direct_dep", + "is_constant", + "is_optional", + "kconfig", + "name", + "nodes", + "orig_type", + "syms", + "user_selection", + "user_value", + ) + + # + # Public interface + # + + @property + def type(self): + """ + Returns the type of the choice. See Symbol.type. + """ + if self.orig_type is TRISTATE and not self.kconfig.modules.tri_value: + return BOOL + return self.orig_type + + @property + def str_value(self): + """ + See the class documentation. + """ + return TRI_TO_STR[self.tri_value] + + @property + def tri_value(self): + """ + See the class documentation. + """ + # This emulates a reverse dependency of 'm && visibility' for + # non-optional choices, which is how the C implementation does it + + val = 0 if self.is_optional else 1 + + if self.user_value is not None: + val = max(val, self.user_value) + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + val = min(val, self.visibility) + + # Promote m to y for boolean choices + return 2 if val == 1 and self.type is BOOL else val + + @property + def assignable(self): + """ + See the class documentation. + """ + if self._cached_assignable is None: + self._cached_assignable = self._assignable() + return self._cached_assignable + + @property + def visibility(self): + """ + See the class documentation. + """ + if self._cached_vis is None: + self._cached_vis = _visibility(self) + return self._cached_vis + + @property + def selection(self): + """ + See the class documentation. + """ + if self._cached_selection is _NO_CACHED_SELECTION: + self._cached_selection = self._selection() + return self._cached_selection + + def set_value(self, value): + """ + Sets the user value (mode) of the choice. Like for Symbol.set_value(), + the visibility might truncate the value. Choices without the 'optional' + attribute (is_optional) can never be in n mode, but 0/"n" is still + accepted since it's not a malformed value (though it will have no + effect). + + Returns True if the value is valid for the type of the choice, and + False otherwise. This only looks at the form of the value. Check the + Choice.assignable attribute to see what values are currently in range + and would actually be reflected in the mode of the choice. + """ + if value in STR_TO_TRI: + value = STR_TO_TRI[value] + + if value == self.user_value: + # We know the value must be valid if it was successfully set + # previously + self._was_set = True + return True + + if not (self.orig_type is BOOL and value in (2, 0) or + self.orig_type is TRISTATE and value in TRI_TO_STR): + + # Display tristate values as n, m, y in the warning + self.kconfig._warn( + "the value {} is invalid for {}, which has type {} -- " + "assignment ignored" + .format(TRI_TO_STR[value] if value in TRI_TO_STR else + "'{}'".format(value), + _name_and_loc(self), TYPE_TO_STR[self.orig_type])) + + return False + + self.user_value = value + self._was_set = True + self._rec_invalidate() + + return True + + def unset_value(self): + """ + Resets the user value (mode) and user selection of the Choice, as if + the user had never touched the mode or any of the choice symbols. + """ + if self.user_value is not None or self.user_selection: + self.user_value = self.user_selection = None + self._rec_invalidate() + + @property + def referenced(self): + """ + See the class documentation. + """ + return {item for node in self.nodes for item in node.referenced} + + @property + def orig_defaults(self): + """ + See the class documentation. + """ + return [d for node in self.nodes for d in node.orig_defaults] + + def __repr__(self): + """ + Returns a string with information about the choice when it is evaluated + on e.g. the interactive Python prompt. + """ + fields = ["choice " + self.name if self.name else "choice", + TYPE_TO_STR[self.type]] + add = fields.append + + for node in self.nodes: + if node.prompt: + add('"{}"'.format(node.prompt[0])) + + add("mode " + self.str_value) + + if self.user_value is not None: + add('user mode {}'.format(TRI_TO_STR[self.user_value])) + + if self.selection: + add("{} selected".format(self.selection.name)) + + if self.user_selection: + user_sel_str = "{} selected by user" \ + .format(self.user_selection.name) + + if self.selection is not self.user_selection: + user_sel_str += " (overridden)" + + add(user_sel_str) + + add("visibility " + TRI_TO_STR[self.visibility]) + + if self.is_optional: + add("optional") + + for node in self.nodes: + add("{}:{}".format(node.filename, node.linenr)) + + return "<{}>".format(", ".join(fields)) + + def __str__(self): + """ + Returns a string representation of the choice when it is printed. + Matches the Kconfig format (though without the contained choice + symbols), with any parent dependencies propagated to the 'depends on' + condition. + + The returned string does not end in a newline. + + See Symbol.__str__() as well. + """ + return self.custom_str(standard_sc_expr_str) + + def custom_str(self, sc_expr_str_fn): + """ + Works like Choice.__str__(), but allows a custom format to be used for + all symbol/choice references. See expr_str(). + """ + return "\n\n".join(node.custom_str(sc_expr_str_fn) + for node in self.nodes) + + # + # Private methods + # + + def __init__(self): + """ + Choice constructor -- not intended to be called directly by Kconfiglib + clients. + """ + # These attributes are always set on the instance from outside and + # don't need defaults: + # direct_dep + # kconfig + + # - UNKNOWN == 0 + # - _visited is used during dep. loop detection + self.orig_type = self._visited = 0 + + self.nodes = [] + + self.syms = [] + self.defaults = [] + + self.name = \ + self.user_value = self.user_selection = \ + self._cached_vis = self._cached_assignable = None + + self._cached_selection = _NO_CACHED_SELECTION + + # is_constant is checked by _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() + + def _assignable(self): + # Worker function for the 'assignable' attribute + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + vis = self.visibility + + if not vis: + return () + + if vis == 2: + if not self.is_optional: + return (2,) if self.type is BOOL else (1, 2) + return (0, 2) if self.type is BOOL else (0, 1, 2) + + # vis == 1 + + return (0, 1) if self.is_optional else (1,) + + def _selection(self): + # Worker function for the 'selection' attribute + + # Warning: See Symbol._rec_invalidate(), and note that this is a hidden + # function call (property magic) + if self.tri_value != 2: + # Not in y mode, so no selection + return None + + # Use the user selection if it's visible + if self.user_selection and self.user_selection.visibility: + return self.user_selection + + # Otherwise, check if we have a default + return self._selection_from_defaults() + + def _selection_from_defaults(self): + # Check if we have a default + for sym, cond in self.defaults: + # The default symbol must be visible too + if expr_value(cond) and sym.visibility: + return sym + + # Otherwise, pick the first visible symbol, if any + for sym in self.syms: + if sym.visibility: + return sym + + # Couldn't find a selection + return None + + def _invalidate(self): + self._cached_vis = self._cached_assignable = None + self._cached_selection = _NO_CACHED_SELECTION + + def _rec_invalidate(self): + # See Symbol._rec_invalidate() + + self._invalidate() + + for item in self._dependents: + if item._cached_vis is not None: + item._rec_invalidate() + + +class MenuNode(object): + """ + Represents a menu node in the configuration. This corresponds to an entry + in e.g. the 'make menuconfig' interface, though non-visible choices, menus, + and comments also get menu nodes. If a symbol or choice is defined in + multiple locations, it gets one menu node for each location. + + The top-level menu node, corresponding to the implicit top-level menu, is + available in Kconfig.top_node. + + The menu nodes for a Symbol or Choice can be found in the + Symbol/Choice.nodes attribute. Menus and comments are represented as plain + menu nodes, with their text stored in the prompt attribute (prompt[0]). + This mirrors the C implementation. + + The following attributes are available on MenuNode instances. They should + be viewed as read-only. + + item: + Either a Symbol, a Choice, or one of the constants MENU and COMMENT. + Menus and comments are represented as plain menu nodes. Ifs are collapsed + (matching the C implementation) and do not appear in the final menu tree. + + next: + The following menu node. None if there is no following node. + + list: + The first child menu node. None if there are no children. + + Choices and menus naturally have children, but Symbols can also have + children because of menus created automatically from dependencies (see + kconfig-language.txt). + + parent: + The parent menu node. None if there is no parent. + + prompt: + A (string, cond) tuple with the prompt for the menu node and its + conditional expression (which is self.kconfig.y if there is no + condition). None if there is no prompt. + + For symbols and choices, the prompt is stored in the MenuNode rather than + the Symbol or Choice instance. For menus and comments, the prompt holds + the text. + + defaults: + The 'default' properties for this particular menu node. See + symbol.defaults. + + When evaluating defaults, you should use Symbol/Choice.defaults instead, + as it include properties from all menu nodes (a symbol/choice can have + multiple definition locations/menu nodes). MenuNode.defaults is meant for + documentation generation. + + selects: + Like MenuNode.defaults, for selects. + + implies: + Like MenuNode.defaults, for implies. + + ranges: + Like MenuNode.defaults, for ranges. + + orig_prompt: + orig_defaults: + orig_selects: + orig_implies: + orig_ranges: + These work the like the corresponding attributes without orig_*, but omit + any dependencies propagated from 'depends on' and surrounding 'if's (the + direct dependencies, stored in MenuNode.dep). + + One use for this is generating less cluttered documentation, by only + showing the direct dependencies in one place. + + help: + The help text for the menu node for Symbols and Choices. None if there is + no help text. Always stored in the node rather than the Symbol or Choice. + It is possible to have a separate help text at each location if a symbol + is defined in multiple locations. + + Trailing whitespace (including a final newline) is stripped from the help + text. This was not the case before Kconfiglib 10.21.0, where the format + was undocumented. + + dep: + The direct ('depends on') dependencies for the menu node, or + self.kconfig.y if there are no direct dependencies. + + This attribute includes any dependencies from surrounding menus and ifs. + Those get propagated to the direct dependencies, and the resulting direct + dependencies in turn get propagated to the conditions of all properties. + + If a symbol or choice is defined in multiple locations, only the + properties defined at a particular location get the corresponding + MenuNode.dep dependencies propagated to them. + + visibility: + The 'visible if' dependencies for the menu node (which must represent a + menu), or self.kconfig.y if there are no 'visible if' dependencies. + 'visible if' dependencies are recursively propagated to the prompts of + symbols and choices within the menu. + + referenced: + A set() with all symbols and choices referenced in the properties and + property conditions of the menu node. + + Also includes dependencies inherited from surrounding menus and ifs. + Choices appear in the dependencies of choice symbols. + + is_menuconfig: + Set to True if the children of the menu node should be displayed in a + separate menu. This is the case for the following items: + + - Menus (node.item == MENU) + + - Choices + + - Symbols defined with the 'menuconfig' keyword. The children come from + implicitly created submenus, and should be displayed in a separate + menu rather than being indented. + + 'is_menuconfig' is just a hint on how to display the menu node. It's + ignored internally by Kconfiglib, except when printing symbols. + + filename/linenr: + The location where the menu node appears. The filename is relative to + $srctree (or to the current directory if $srctree isn't set), except + absolute paths are used for paths outside $srctree. + + include_path: + A tuple of (filename, linenr) tuples, giving the locations of the + 'source' statements via which the Kconfig file containing this menu node + was included. The first element is the location of the 'source' statement + in the top-level Kconfig file passed to Kconfig.__init__(), etc. + + Note that the Kconfig file of the menu node itself isn't included. Check + 'filename' and 'linenr' for that. + + kconfig: + The Kconfig instance the menu node is from. + """ + __slots__ = ( + "dep", + "filename", + "help", + "include_path", + "is_menuconfig", + "item", + "kconfig", + "linenr", + "list", + "next", + "parent", + "prompt", + "visibility", + + # Properties + "defaults", + "selects", + "implies", + "ranges", + ) + + def __init__(self): + # Properties defined on this particular menu node. A local 'depends on' + # only applies to these, in case a symbol is defined in multiple + # locations. + self.defaults = [] + self.selects = [] + self.implies = [] + self.ranges = [] + + @property + def orig_prompt(self): + """ + See the class documentation. + """ + if not self.prompt: + return None + return (self.prompt[0], self._strip_dep(self.prompt[1])) + + @property + def orig_defaults(self): + """ + See the class documentation. + """ + return [(default, self._strip_dep(cond)) + for default, cond in self.defaults] + + @property + def orig_selects(self): + """ + See the class documentation. + """ + return [(select, self._strip_dep(cond)) + for select, cond in self.selects] + + @property + def orig_implies(self): + """ + See the class documentation. + """ + return [(imply, self._strip_dep(cond)) + for imply, cond in self.implies] + + @property + def orig_ranges(self): + """ + See the class documentation. + """ + return [(low, high, self._strip_dep(cond)) + for low, high, cond in self.ranges] + + @property + def referenced(self): + """ + See the class documentation. + """ + # self.dep is included to catch dependencies from a lone 'depends on' + # when there are no properties to propagate it to + res = expr_items(self.dep) + + if self.prompt: + res |= expr_items(self.prompt[1]) + + if self.item is MENU: + res |= expr_items(self.visibility) + + for value, cond in self.defaults: + res |= expr_items(value) + res |= expr_items(cond) + + for value, cond in self.selects: + res.add(value) + res |= expr_items(cond) + + for value, cond in self.implies: + res.add(value) + res |= expr_items(cond) + + for low, high, cond in self.ranges: + res.add(low) + res.add(high) + res |= expr_items(cond) + + return res + + def __repr__(self): + """ + Returns a string with information about the menu node when it is + evaluated on e.g. the interactive Python prompt. + """ + fields = [] + add = fields.append + + if self.item.__class__ is Symbol: + add("menu node for symbol " + self.item.name) + + elif self.item.__class__ is Choice: + s = "menu node for choice" + if self.item.name is not None: + s += " " + self.item.name + add(s) + + elif self.item is MENU: + add("menu node for menu") + + else: # self.item is COMMENT + add("menu node for comment") + + if self.prompt: + add('prompt "{}" (visibility {})'.format( + self.prompt[0], TRI_TO_STR[expr_value(self.prompt[1])])) + + if self.item.__class__ is Symbol and self.is_menuconfig: + add("is menuconfig") + + add("deps " + TRI_TO_STR[expr_value(self.dep)]) + + if self.item is MENU: + add("'visible if' deps " + TRI_TO_STR[expr_value(self.visibility)]) + + if self.item.__class__ in _SYMBOL_CHOICE and self.help is not None: + add("has help") + + if self.list: + add("has child") + + if self.next: + add("has next") + + add("{}:{}".format(self.filename, self.linenr)) + + return "<{}>".format(", ".join(fields)) + + def __str__(self): + """ + Returns a string representation of the menu node. Matches the Kconfig + format, with any parent dependencies propagated to the 'depends on' + condition. + + The output could (almost) be fed back into a Kconfig parser to redefine + the object associated with the menu node. See the module documentation + for a gotcha related to choice symbols. + + For symbols and choices with multiple menu nodes (multiple definition + locations), properties that aren't associated with a particular menu + node are shown on all menu nodes ('option env=...', 'optional' for + choices, etc.). + + The returned string does not end in a newline. + """ + return self.custom_str(standard_sc_expr_str) + + def custom_str(self, sc_expr_str_fn): + """ + Works like MenuNode.__str__(), but allows a custom format to be used + for all symbol/choice references. See expr_str(). + """ + return self._menu_comment_node_str(sc_expr_str_fn) \ + if self.item in _MENU_COMMENT else \ + self._sym_choice_node_str(sc_expr_str_fn) + + def _menu_comment_node_str(self, sc_expr_str_fn): + s = '{} "{}"'.format("menu" if self.item is MENU else "comment", + self.prompt[0]) + + if self.dep is not self.kconfig.y: + s += "\n\tdepends on {}".format(expr_str(self.dep, sc_expr_str_fn)) + + if self.item is MENU and self.visibility is not self.kconfig.y: + s += "\n\tvisible if {}".format(expr_str(self.visibility, + sc_expr_str_fn)) + + return s + + def _sym_choice_node_str(self, sc_expr_str_fn): + def indent_add(s): + lines.append("\t" + s) + + def indent_add_cond(s, cond): + if cond is not self.kconfig.y: + s += " if " + expr_str(cond, sc_expr_str_fn) + indent_add(s) + + sc = self.item + + if sc.__class__ is Symbol: + lines = [("menuconfig " if self.is_menuconfig else "config ") + + sc.name] + else: + lines = ["choice " + sc.name if sc.name else "choice"] + + if sc.orig_type and not self.prompt: # sc.orig_type != UNKNOWN + # If there's a prompt, we'll use the '<type> "prompt"' shorthand + # instead + indent_add(TYPE_TO_STR[sc.orig_type]) + + if self.prompt: + if sc.orig_type: + prefix = TYPE_TO_STR[sc.orig_type] + else: + # Symbol defined without a type (which generates a warning) + prefix = "prompt" + + indent_add_cond(prefix + ' "{}"'.format(escape(self.prompt[0])), + self.orig_prompt[1]) + + if sc.__class__ is Symbol: + if sc.is_allnoconfig_y: + indent_add("option allnoconfig_y") + + if sc is sc.kconfig.defconfig_list: + indent_add("option defconfig_list") + + if sc.env_var is not None: + indent_add('option env="{}"'.format(sc.env_var)) + + if sc is sc.kconfig.modules: + indent_add("option modules") + + for low, high, cond in self.orig_ranges: + indent_add_cond( + "range {} {}".format(sc_expr_str_fn(low), + sc_expr_str_fn(high)), + cond) + + for default, cond in self.orig_defaults: + indent_add_cond("default " + expr_str(default, sc_expr_str_fn), + cond) + + if sc.__class__ is Choice and sc.is_optional: + indent_add("optional") + + if sc.__class__ is Symbol: + for select, cond in self.orig_selects: + indent_add_cond("select " + sc_expr_str_fn(select), cond) + + for imply, cond in self.orig_implies: + indent_add_cond("imply " + sc_expr_str_fn(imply), cond) + + if self.dep is not sc.kconfig.y: + indent_add("depends on " + expr_str(self.dep, sc_expr_str_fn)) + + if self.help is not None: + indent_add("help") + for line in self.help.splitlines(): + indent_add(" " + line) + + return "\n".join(lines) + + def _strip_dep(self, expr): + # Helper function for removing MenuNode.dep from 'expr'. Uses two + # pieces of internal knowledge: (1) Expressions are reused rather than + # copied, and (2) the direct dependencies always appear at the end. + + # ... if dep -> ... if y + if self.dep is expr: + return self.kconfig.y + + # (AND, X, dep) -> X + if expr.__class__ is tuple and expr[0] is AND and expr[2] is self.dep: + return expr[1] + + return expr + + +class Variable(object): + """ + Represents a preprocessor variable/function. + + The following attributes are available: + + name: + The name of the variable. + + value: + The unexpanded value of the variable. + + expanded_value: + The expanded value of the variable. For simple variables (those defined + with :=), this will equal 'value'. Accessing this property will raise a + KconfigError if the expansion seems to be stuck in a loop. + + Accessing this field is the same as calling expanded_value_w_args() with + no arguments. I hadn't considered function arguments when adding it. It + is retained for backwards compatibility though. + + is_recursive: + True if the variable is recursive (defined with =). + """ + __slots__ = ( + "_n_expansions", + "is_recursive", + "kconfig", + "name", + "value", + ) + + @property + def expanded_value(self): + """ + See the class documentation. + """ + return self.expanded_value_w_args() + + def expanded_value_w_args(self, *args): + """ + Returns the expanded value of the variable/function. Any arguments + passed will be substituted for $(1), $(2), etc. + + Raises a KconfigError if the expansion seems to be stuck in a loop. + """ + return self.kconfig._fn_val((self.name,) + args) + + def __repr__(self): + return "<variable {}, {}, value '{}'>" \ + .format(self.name, + "recursive" if self.is_recursive else "immediate", + self.value) + + +class KconfigError(Exception): + """ + Exception raised for Kconfig-related errors. + + KconfigError and KconfigSyntaxError are the same class. The + KconfigSyntaxError alias is only maintained for backwards compatibility. + """ + +KconfigSyntaxError = KconfigError # Backwards compatibility + + +class InternalError(Exception): + "Never raised. Kept around for backwards compatibility." + + +# Workaround: +# +# If 'errno' and 'strerror' are set on IOError, then __str__() always returns +# "[Errno <errno>] <strerror>", ignoring any custom message passed to the +# constructor. By defining our own subclass, we can use a custom message while +# also providing 'errno', 'strerror', and 'filename' to scripts. +class _KconfigIOError(IOError): + def __init__(self, ioerror, msg): + self.msg = msg + super(_KconfigIOError, self).__init__( + ioerror.errno, ioerror.strerror, ioerror.filename) + + def __str__(self): + return self.msg + + +# +# Public functions +# + + +def expr_value(expr): + """ + Evaluates the expression 'expr' to a tristate value. Returns 0 (n), 1 (m), + or 2 (y). + + 'expr' must be an already-parsed expression from a Symbol, Choice, or + MenuNode property. To evaluate an expression represented as a string, use + Kconfig.eval_string(). + + Passing subexpressions of expressions to this function works as expected. + """ + if expr.__class__ is not tuple: + return expr.tri_value + + if expr[0] is AND: + v1 = expr_value(expr[1]) + # Short-circuit the n case as an optimization (~5% faster + # allnoconfig.py and allyesconfig.py, as of writing) + return 0 if not v1 else min(v1, expr_value(expr[2])) + + if expr[0] is OR: + v1 = expr_value(expr[1]) + # Short-circuit the y case as an optimization + return 2 if v1 == 2 else max(v1, expr_value(expr[2])) + + if expr[0] is NOT: + return 2 - expr_value(expr[1]) + + # Relation + # + # Implements <, <=, >, >= comparisons as well. These were added to + # kconfig in 31847b67 (kconfig: allow use of relations other than + # (in)equality). + + rel, v1, v2 = expr + + # If both operands are strings... + if v1.orig_type is STRING and v2.orig_type is STRING: + # ...then compare them lexicographically + comp = _strcmp(v1.str_value, v2.str_value) + else: + # Otherwise, try to compare them as numbers + try: + comp = _sym_to_num(v1) - _sym_to_num(v2) + except ValueError: + # Fall back on a lexicographic comparison if the operands don't + # parse as numbers + comp = _strcmp(v1.str_value, v2.str_value) + + return 2*(comp == 0 if rel is EQUAL else + comp != 0 if rel is UNEQUAL else + comp < 0 if rel is LESS else + comp <= 0 if rel is LESS_EQUAL else + comp > 0 if rel is GREATER else + comp >= 0) + + +def standard_sc_expr_str(sc): + """ + Standard symbol/choice printing function. Uses plain Kconfig syntax, and + displays choices as <choice> (or <choice NAME>, for named choices). + + See expr_str(). + """ + if sc.__class__ is Symbol: + if sc.is_constant and sc.name not in STR_TO_TRI: + return '"{}"'.format(escape(sc.name)) + return sc.name + + return "<choice {}>".format(sc.name) if sc.name else "<choice>" + + +def expr_str(expr, sc_expr_str_fn=standard_sc_expr_str): + """ + Returns the string representation of the expression 'expr', as in a Kconfig + file. + + Passing subexpressions of expressions to this function works as expected. + + sc_expr_str_fn (default: standard_sc_expr_str): + This function is called for every symbol/choice (hence "sc") appearing in + the expression, with the symbol/choice as the argument. It is expected to + return a string to be used for the symbol/choice. + + This can be used e.g. to turn symbols/choices into links when generating + documentation, or for printing the value of each symbol/choice after it. + + Note that quoted values are represented as constants symbols + (Symbol.is_constant == True). + """ + if expr.__class__ is not tuple: + return sc_expr_str_fn(expr) + + if expr[0] is AND: + return "{} && {}".format(_parenthesize(expr[1], OR, sc_expr_str_fn), + _parenthesize(expr[2], OR, sc_expr_str_fn)) + + if expr[0] is OR: + # This turns A && B || C && D into "(A && B) || (C && D)", which is + # redundant, but more readable + return "{} || {}".format(_parenthesize(expr[1], AND, sc_expr_str_fn), + _parenthesize(expr[2], AND, sc_expr_str_fn)) + + if expr[0] is NOT: + if expr[1].__class__ is tuple: + return "!({})".format(expr_str(expr[1], sc_expr_str_fn)) + return "!" + sc_expr_str_fn(expr[1]) # Symbol + + # Relation + # + # Relation operands are always symbols (quoted strings are constant + # symbols) + return "{} {} {}".format(sc_expr_str_fn(expr[1]), REL_TO_STR[expr[0]], + sc_expr_str_fn(expr[2])) + + +def expr_items(expr): + """ + Returns a set() of all items (symbols and choices) that appear in the + expression 'expr'. + + Passing subexpressions of expressions to this function works as expected. + """ + res = set() + + def rec(subexpr): + if subexpr.__class__ is tuple: + # AND, OR, NOT, or relation + + rec(subexpr[1]) + + # NOTs only have a single operand + if subexpr[0] is not NOT: + rec(subexpr[2]) + + else: + # Symbol or choice + res.add(subexpr) + + rec(expr) + return res + + +def split_expr(expr, op): + """ + Returns a list containing the top-level AND or OR operands in the + expression 'expr', in the same (left-to-right) order as they appear in + the expression. + + This can be handy e.g. for splitting (weak) reverse dependencies + from 'select' and 'imply' into individual selects/implies. + + op: + Either AND to get AND operands, or OR to get OR operands. + + (Having this as an operand might be more future-safe than having two + hardcoded functions.) + + + Pseudo-code examples: + + split_expr( A , OR ) -> [A] + split_expr( A && B , OR ) -> [A && B] + split_expr( A || B , OR ) -> [A, B] + split_expr( A || B , AND ) -> [A || B] + split_expr( A || B || (C && D) , OR ) -> [A, B, C && D] + + # Second || is not at the top level + split_expr( A || (B && (C || D)) , OR ) -> [A, B && (C || D)] + + # Parentheses don't matter as long as we stay at the top level (don't + # encounter any non-'op' nodes) + split_expr( (A || B) || C , OR ) -> [A, B, C] + split_expr( A || (B || C) , OR ) -> [A, B, C] + """ + res = [] + + def rec(subexpr): + if subexpr.__class__ is tuple and subexpr[0] is op: + rec(subexpr[1]) + rec(subexpr[2]) + else: + res.append(subexpr) + + rec(expr) + return res + + +def escape(s): + r""" + Escapes the string 's' in the same fashion as is done for display in + Kconfig format and when writing strings to a .config file. " and \ are + replaced by \" and \\, respectively. + """ + # \ must be escaped before " to avoid double escaping + return s.replace("\\", r"\\").replace('"', r'\"') + + +def unescape(s): + r""" + Unescapes the string 's'. \ followed by any character is replaced with just + that character. Used internally when reading .config files. + """ + return _unescape_sub(r"\1", s) + +# unescape() helper +_unescape_sub = re.compile(r"\\(.)").sub + + +def standard_kconfig(): + """ + 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])) + + # Only show backtraces for unexpected exceptions + try: + return Kconfig("Kconfig" if len(sys.argv) < 2 else sys.argv[1]) + except (EnvironmentError, KconfigError) as e: + # Some long exception messages have extra newlines for better + # formatting when reported as an unhandled exception. Strip them here. + sys.exit(str(e).strip()) + + +def standard_config_filename(): + """ + Helper for tools. Returns the value of KCONFIG_CONFIG (which specifies the + .config file to load/save) if it is set, and ".config" otherwise. + + Calling load_config() with filename=None might give the behavior you want, + without having to use this function. + """ + return os.getenv("KCONFIG_CONFIG", ".config") + + +def load_allconfig(kconf, filename): + """ + Helper for all*config. Loads (merges) the configuration file specified by + KCONFIG_ALLCONFIG, if any. See Documentation/kbuild/kconfig.txt in the + Linux kernel. + + Disables warnings for duplicated assignments within configuration files for + the duration of the call (kconf.warn_assign_override/warn_assign_redun = False), + and restores the previous warning settings at the end. The + KCONFIG_ALLCONFIG configuration file is expected to override symbols. + + Exits with sys.exit() (which raises a SystemExit exception) and prints an + error to stderr if KCONFIG_ALLCONFIG is set but the configuration file + can't be opened. + + kconf: + Kconfig instance to load the configuration in. + + filename: + Command-specific configuration filename - "allyes.config", + "allno.config", etc. + """ + allconfig = os.getenv("KCONFIG_ALLCONFIG") + if allconfig is None: + return + + def std_msg(e): + # "Upcasts" a _KconfigIOError to an IOError, removing the custom + # __str__() message. The standard message is better here. + # + # This might also convert an OSError to an IOError in obscure cases, + # but it's probably not a big deal. The distinction is shaky (see + # PEP-3151). + return IOError(e.errno, e.strerror, e.filename) + + old_warn_assign_override = kconf.warn_assign_override + old_warn_assign_redun = kconf.warn_assign_redun + kconf.warn_assign_override = kconf.warn_assign_redun = False + + if allconfig in ("", "1"): + try: + print(kconf.load_config(filename, False)) + except EnvironmentError as e1: + try: + print(kconf.load_config("all.config", False)) + except EnvironmentError as e2: + sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} " + "nor all.config could be opened: {}, {}" + .format(filename, std_msg(e1), std_msg(e2))) + else: + try: + print(kconf.load_config(allconfig, False)) + except EnvironmentError as e: + sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which " + "could not be opened: {}" + .format(allconfig, std_msg(e))) + + kconf.warn_assign_override = old_warn_assign_override + kconf.warn_assign_redun = old_warn_assign_redun + + +# +# Internal functions +# + + +def _visibility(sc): + # Symbols and Choices have a "visibility" that acts as an upper bound on + # the values a user can set for them, corresponding to the visibility in + # e.g. 'make menuconfig'. This function calculates the visibility for the + # Symbol or Choice 'sc' -- the logic is nearly identical. + + vis = 0 + + for node in sc.nodes: + if node.prompt: + vis = max(vis, expr_value(node.prompt[1])) + + if sc.__class__ is Symbol and sc.choice: + if sc.choice.orig_type is TRISTATE and \ + sc.orig_type is not TRISTATE and sc.choice.tri_value != 2: + # Non-tristate choice symbols are only visible in y mode + return 0 + + if sc.orig_type is TRISTATE and vis == 1 and sc.choice.tri_value == 2: + # Choice symbols with m visibility are not visible in y mode + return 0 + + # Promote m to y if we're dealing with a non-tristate (possibly due to + # modules being disabled) + if vis == 1 and sc.type is not TRISTATE: + return 2 + + return vis + + +def _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 expr.__class__ is 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 expr.__class__ is tuple and expr[0] is type_: + return "({})".format(expr_str(expr, sc_expr_str_fn)) + return expr_str(expr, sc_expr_str_fn) + + +def _ordered_unique(lst): + # Returns 'lst' with any duplicates removed, preserving order. This hacky + # version seems to be a common idiom. It relies on short-circuit evaluation + # and set.add() returning None, which is falsy. + + seen = set() + seen_add = seen.add + return [x for x in lst if x not in seen and not seen_add(x)] + + +def _is_base_n(s, n): + try: + int(s, n) + return True + except ValueError: + return False + + +def _strcmp(s1, s2): + # strcmp()-alike that returns -1, 0, or 1 + + return (s1 > s2) - (s1 < s2) + + +def _sym_to_num(sym): + # expr_value() helper for converting a symbol to a number. Raises + # ValueError for symbols that can't be converted. + + # For BOOL and TRISTATE, n/m/y count as 0/1/2. This mirrors 9059a3493ef + # ("kconfig: fix relational operators for bool and tristate symbols") in + # the C implementation. + return sym.tri_value if sym.orig_type in _BOOL_TRISTATE else \ + int(sym.str_value, _TYPE_TO_BASE[sym.orig_type]) + + +def _touch_dep_file(path, sym_name): + # If sym_name is MY_SYM_NAME, touches my/sym/name.h. See the sync_deps() + # docstring. + + sym_path = path + os.sep + sym_name.lower().replace("_", os.sep) + ".h" + sym_path_dir = dirname(sym_path) + if not exists(sym_path_dir): + os.makedirs(sym_path_dir, 0o755) + + # A kind of truncating touch, mirroring the C tools + os.close(os.open( + sym_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)) + + +def _save_old(path): + # See write_config() + + def copy(src, dst): + # Import as needed, to save some startup time + import shutil + shutil.copyfile(src, dst) + + if islink(path): + # Preserve symlinks + copy_fn = copy + elif hasattr(os, "replace"): + # Python 3 (3.3+) only. Best choice when available, because it + # removes <filename>.old on both *nix and Windows. + copy_fn = os.replace + elif os.name == "posix": + # Removes <filename>.old on POSIX systems + copy_fn = os.rename + else: + # Fall back on copying + copy_fn = copy + + try: + copy_fn(path, path + ".old") + except Exception: + # Ignore errors from 'path' missing as well as other errors. + # <filename>.old file is usually more of a nice-to-have, and not worth + # erroring out over e.g. if <filename>.old happens to be a directory or + # <filename> is something like /dev/null. + pass + + +def _name_and_loc(sc): + # Helper for giving the symbol/choice name and location(s) in e.g. warnings + + # Reuse the expression format. That way choices show up as + # '<choice (name, if any)>' + name = standard_sc_expr_str(sc) + + 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 expr.__class__ is not tuple: + return expr is sym + + if expr[0] in _EQUAL_UNEQUAL: + # Check for one of the following: + # sym = m/y, m/y = sym, sym != n, n != sym + + left, right = expr[1:] + + if right is sym: + left, right = right, left + elif left is not sym: + return False + + return (expr[0] is EQUAL and right is sym.kconfig.m or + right is sym.kconfig.y) or \ + (expr[0] is UNEQUAL and right is sym.kconfig.n) + + return expr[0] is AND and \ + (_expr_depends_on(expr[1], sym) or + _expr_depends_on(expr[2], sym)) + + +def _auto_menu_dep(node1, node2): + # Returns True if node2 has an "automatic menu dependency" on node1. If + # node2 has a prompt, we check its condition. Otherwise, we look directly + # at node2.dep. + + return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep, + node1.item) + + +def _flatten(node): + # "Flattens" menu nodes without prompts (e.g. 'if' nodes and non-visible + # symbols with children from automatic menu creation) so that their + # children appear after them instead. This gives a clean menu structure + # with no unexpected "jumps" in the indentation. + # + # Do not flatten promptless choices (which can appear "legitimately" if a + # named choice is defined in multiple locations to add on symbols). It + # looks confusing, and the menuconfig already shows all choice symbols if + # you enter the choice at some location with a prompt. + + while node: + if node.list and not node.prompt and \ + node.item.__class__ is not Choice: + + last_node = node.list + while 1: + last_node.parent = node.parent + if not last_node.next: + break + last_node = last_node.next + + last_node.next = node.next + node.next = node.list + node.list = None + + node = node.next + + +def _remove_ifs(node): + # Removes 'if' nodes (which can be recognized by MenuNode.item being None), + # which are assumed to already have been flattened. The C implementation + # doesn't bother to do this, but we expose the menu tree directly, and it + # makes it nicer to work with. + + cur = node.list + while cur and not cur.item: + cur = cur.next + + node.list = cur + + while cur: + next = cur.next + while next and not next.item: + next = next.next + + # Equivalent to + # + # cur.next = next + # cur = next + # + # due to tricky Python semantics. The order matters. + cur.next = cur = next + + +def _finalize_choice(node): + # Finalizes a choice, marking each symbol whose menu node has the choice as + # the parent as a choice symbol, and automatically determining types if not + # specified. + + choice = node.item + + cur = node.list + while cur: + if cur.item.__class__ is Symbol: + cur.item.choice = choice + choice.syms.append(cur.item) + cur = cur.next + + # If no type is specified for the choice, its type is that of + # the first choice item with a specified type + if not choice.orig_type: + for item in choice.syms: + if item.orig_type: + choice.orig_type = item.orig_type + break + + # Each choice item of UNKNOWN type gets the type of the choice + for sym in choice.syms: + if not sym.orig_type: + sym.orig_type = choice.orig_type + + +def _check_dep_loop_sym(sym, ignore_choice): + # Detects dependency loops using depth-first search on the dependency graph + # (which is calculated earlier in Kconfig._build_dep()). + # + # Algorithm: + # + # 1. Symbols/choices start out with _visited = 0, meaning unvisited. + # + # 2. When a symbol/choice is first visited, _visited is set to 1, meaning + # "visited, potentially part of a dependency loop". The recursive + # search then continues from the symbol/choice. + # + # 3. If we run into a symbol/choice X with _visited already set to 1, + # there's a dependency loop. The loop is found on the call stack by + # recording symbols while returning ("on the way back") until X is seen + # again. + # + # 4. Once a symbol/choice and all its dependencies (or dependents in this + # case) have been checked recursively without detecting any loops, its + # _visited is set to 2, meaning "visited, not part of a dependency + # loop". + # + # This saves work if we run into the symbol/choice again in later calls + # to _check_dep_loop_sym(). We just return immediately. + # + # Choices complicate things, as every choice symbol depends on every other + # choice symbol in a sense. When a choice is "entered" via a choice symbol + # X, we visit all choice symbols from the choice except X, and prevent + # immediately revisiting the choice with a flag (ignore_choice). + # + # Maybe there's a better way to handle this (different flags or the + # like...) + + if not sym._visited: + # sym._visited == 0, unvisited + + sym._visited = 1 + + for dep in sym._dependents: + # Choices show up in Symbol._dependents when the choice has the + # symbol in a 'prompt' or 'default' condition (e.g. + # 'default ... if SYM'). + # + # Since we aren't entering the choice via a choice symbol, all + # choice symbols need to be checked, hence the None. + loop = _check_dep_loop_choice(dep, None) \ + if dep.__class__ is Choice \ + else _check_dep_loop_sym(dep, False) + + if loop: + # Dependency loop found + return _found_dep_loop(loop, sym) + + if sym.choice and not ignore_choice: + loop = _check_dep_loop_choice(sym.choice, sym) + if loop: + # Dependency loop found + return _found_dep_loop(loop, sym) + + # The symbol is not part of a dependency loop + sym._visited = 2 + + # No dependency loop found + return None + + if sym._visited == 2: + # The symbol was checked earlier and is already known to not be part of + # a dependency loop + return None + + # sym._visited == 1, found a dependency loop. Return the symbol as the + # first element in it. + return (sym,) + + +def _check_dep_loop_choice(choice, skip): + if not choice._visited: + # choice._visited == 0, unvisited + + choice._visited = 1 + + # Check for loops involving choice symbols. If we came here via a + # choice symbol, skip that one, as we'd get a false positive + # '<sym FOO> -> <choice> -> <sym FOO>' loop otherwise. + for sym in choice.syms: + if sym is not skip: + # Prevent the choice from being immediately re-entered via the + # "is a choice symbol" path by passing True + loop = _check_dep_loop_sym(sym, True) + if loop: + # Dependency loop found + return _found_dep_loop(loop, choice) + + # The choice is not part of a dependency loop + choice._visited = 2 + + # No dependency loop found + return None + + if choice._visited == 2: + # The choice was checked earlier and is already known to not be part of + # a dependency loop + return None + + # choice._visited == 1, found a dependency loop. Return the choice as the + # first element in it. + return (choice,) + + +def _found_dep_loop(loop, cur): + # Called "on the way back" when we know we have a loop + + # Is the symbol/choice 'cur' where the loop started? + if cur is not loop[0]: + # Nope, it's just a part of the loop + return loop + (cur,) + + # Yep, we have the entire loop. Throw an exception that shows it. + + msg = "\nDependency loop\n" \ + "===============\n\n" + + for item in loop: + if item is not loop[0]: + msg += "...depends on " + if item.__class__ is Symbol and item.choice: + msg += "the choice symbol " + + msg += "{}, with definition...\n\n{}\n\n" \ + .format(_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 item.__class__ is Symbol: + if item.rev_dep is not item.kconfig.n: + msg += "(select-related dependencies: {})\n\n" \ + .format(expr_str(item.rev_dep)) + + if item.weak_rev_dep is not item.kconfig.n: + msg += "(imply-related dependencies: {})\n\n" \ + .format(expr_str(item.rev_dep)) + + msg += "...depends again on {}".format(_name_and_loc(loop[0])) + + raise KconfigError(msg) + + +def _decoding_error(e, filename, macro_linenr=None): + # Gives the filename and context for UnicodeDecodeError's, which are a pain + # to debug otherwise. 'e' is the UnicodeDecodeError object. + # + # If the decoding error is for the output of a $(shell,...) command, + # macro_linenr holds the line number where it was run (the exact line + # number isn't available for decoding errors in files). + + raise KconfigError( + "\n" + "Malformed {} in {}\n" + "Context: {}\n" + "Problematic data: {}\n" + "Reason: {}".format( + e.encoding, + "'{}'".format(filename) if macro_linenr is None else + "output from macro at {}:{}".format(filename, macro_linenr), + e.object[max(e.start - 40, 0):e.end + 40], + e.object[e.start:e.end], + e.reason)) + + +def _warn_verbose_deprecated(fn_name): + sys.stderr.write( + "Deprecation warning: {0}()'s 'verbose' argument has no effect. Since " + "Kconfiglib 12.0.0, the message is returned from {0}() instead, " + "and is always generated. Do e.g. print(kconf.{0}()) if you want to " + "want to show a message like \"Loaded configuration '.config'\" on " + "stdout. The old API required ugly hacks to reuse messages in " + "configuration interfaces.\n".format(fn_name)) + + +# Predefined preprocessor functions + + +def _filename_fn(kconf, _): + return kconf._filename + + +def _lineno_fn(kconf, _): + return str(kconf._linenr) + + +def _info_fn(kconf, _, msg): + print("{}:{}: {}".format(kconf._filename, kconf._linenr, msg)) + + return "" + + +def _warning_if_fn(kconf, _, cond, msg): + if cond == "y": + kconf._warn(msg, kconf._filename, kconf._linenr) + + return "" + + +def _error_if_fn(kconf, _, cond, msg): + if cond == "y": + raise KconfigError("{}:{}: {}".format( + kconf._filename, kconf._linenr, msg)) + + return "" + + +def _shell_fn(kconf, _, command): + # Only import as needed, to save some startup time + import subprocess + + stdout, stderr = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ).communicate() + + if not _IS_PY2: + try: + stdout = stdout.decode(kconf._encoding) + stderr = stderr.decode(kconf._encoding) + except UnicodeDecodeError as e: + _decoding_error(e, kconf._filename, kconf._linenr) + + if stderr: + kconf._warn("'{}' wrote to stderr: {}".format( + command, "\n".join(stderr.splitlines())), + kconf._filename, kconf._linenr) + + # Universal newlines with splitlines() (to prevent e.g. stray \r's in + # command output on Windows), trailing newline removal, and + # newline-to-space conversion. + # + # On Python 3 versions before 3.6, it's not possible to specify the + # encoding when passing universal_newlines=True to Popen() (the 'encoding' + # parameter was added in 3.6), so we do this manual version instead. + return "\n".join(stdout.splitlines()).rstrip("\n").replace("\n", " ") + +# +# Global constants +# + +TRI_TO_STR = { + 0: "n", + 1: "m", + 2: "y", +} + +STR_TO_TRI = { + "n": 0, + "m": 1, + "y": 2, +} + +# Constant representing that there's no cached choice selection. This is +# distinct from a cached None (no selection). Any object that's not None or a +# Symbol will do. We test this with 'is'. +_NO_CACHED_SELECTION = 0 + +# Are we running on Python 2? +_IS_PY2 = sys.version_info[0] < 3 + +try: + _UNAME_RELEASE = os.uname()[2] +except AttributeError: + # Only import as needed, to save some startup time + import platform + _UNAME_RELEASE = platform.uname()[2] + +# The token and type constants below are safe to test with 'is', which is a bit +# faster (~30% faster on my machine, and a few % faster for total parsing +# time), even without assuming Python's small integer optimization (which +# caches small integer objects). The constants end up pointing to unique +# integer objects, and since we consistently refer to them via the names below, +# we always get the same object. +# +# Client code should use == though. + +# Tokens, with values 1, 2, ... . Avoiding 0 simplifies some checks by making +# all tokens except empty strings truthy. +( + _T_ALLNOCONFIG_Y, + _T_AND, + _T_BOOL, + _T_CHOICE, + _T_CLOSE_PAREN, + _T_COMMENT, + _T_CONFIG, + _T_DEFAULT, + _T_DEFCONFIG_LIST, + _T_DEF_BOOL, + _T_DEF_HEX, + _T_DEF_INT, + _T_DEF_STRING, + _T_DEF_TRISTATE, + _T_DEPENDS, + _T_ENDCHOICE, + _T_ENDIF, + _T_ENDMENU, + _T_ENV, + _T_EQUAL, + _T_GREATER, + _T_GREATER_EQUAL, + _T_HELP, + _T_HEX, + _T_IF, + _T_IMPLY, + _T_INT, + _T_LESS, + _T_LESS_EQUAL, + _T_MAINMENU, + _T_MENU, + _T_MENUCONFIG, + _T_MODULES, + _T_NOT, + _T_ON, + _T_OPEN_PAREN, + _T_OPTION, + _T_OPTIONAL, + _T_OR, + _T_ORSOURCE, + _T_OSOURCE, + _T_PROMPT, + _T_RANGE, + _T_RSOURCE, + _T_SELECT, + _T_SOURCE, + _T_STRING, + _T_TRISTATE, + _T_UNEQUAL, + _T_VISIBLE, +) = range(1, 51) + +# Keyword to token map, with the get() method assigned directly as a small +# optimization +_get_keyword = { + "---help---": _T_HELP, + "allnoconfig_y": _T_ALLNOCONFIG_Y, + "bool": _T_BOOL, + "boolean": _T_BOOL, + "choice": _T_CHOICE, + "comment": _T_COMMENT, + "config": _T_CONFIG, + "def_bool": _T_DEF_BOOL, + "def_hex": _T_DEF_HEX, + "def_int": _T_DEF_INT, + "def_string": _T_DEF_STRING, + "def_tristate": _T_DEF_TRISTATE, + "default": _T_DEFAULT, + "defconfig_list": _T_DEFCONFIG_LIST, + "depends": _T_DEPENDS, + "endchoice": _T_ENDCHOICE, + "endif": _T_ENDIF, + "endmenu": _T_ENDMENU, + "env": _T_ENV, + "grsource": _T_ORSOURCE, # Backwards compatibility + "gsource": _T_OSOURCE, # Backwards compatibility + "help": _T_HELP, + "hex": _T_HEX, + "if": _T_IF, + "imply": _T_IMPLY, + "int": _T_INT, + "mainmenu": _T_MAINMENU, + "menu": _T_MENU, + "menuconfig": _T_MENUCONFIG, + "modules": _T_MODULES, + "on": _T_ON, + "option": _T_OPTION, + "optional": _T_OPTIONAL, + "orsource": _T_ORSOURCE, + "osource": _T_OSOURCE, + "prompt": _T_PROMPT, + "range": _T_RANGE, + "rsource": _T_RSOURCE, + "select": _T_SELECT, + "source": _T_SOURCE, + "string": _T_STRING, + "tristate": _T_TRISTATE, + "visible": _T_VISIBLE, +}.get + +# The constants below match the value of the corresponding tokens to remove the +# need for conversion + +# Node types +MENU = _T_MENU +COMMENT = _T_COMMENT + +# Expression types +AND = _T_AND +OR = _T_OR +NOT = _T_NOT +EQUAL = _T_EQUAL +UNEQUAL = _T_UNEQUAL +LESS = _T_LESS +LESS_EQUAL = _T_LESS_EQUAL +GREATER = _T_GREATER +GREATER_EQUAL = _T_GREATER_EQUAL + +REL_TO_STR = { + EQUAL: "=", + UNEQUAL: "!=", + LESS: "<", + LESS_EQUAL: "<=", + GREATER: ">", + GREATER_EQUAL: ">=", +} + +# Symbol/choice types. UNKNOWN is 0 (falsy) to simplify some checks. +# Client code shouldn't rely on it though, as it was non-zero in +# older versions. +UNKNOWN = 0 +BOOL = _T_BOOL +TRISTATE = _T_TRISTATE +STRING = _T_STRING +INT = _T_INT +HEX = _T_HEX + +TYPE_TO_STR = { + UNKNOWN: "unknown", + BOOL: "bool", + TRISTATE: "tristate", + STRING: "string", + INT: "int", + HEX: "hex", +} + +# Used in comparisons. 0 means the base is inferred from the format of the +# string. +_TYPE_TO_BASE = { + HEX: 16, + INT: 10, + STRING: 0, + UNKNOWN: 0, +} + +# def_bool -> BOOL, etc. +_DEF_TOKEN_TO_TYPE = { + _T_DEF_BOOL: BOOL, + _T_DEF_HEX: HEX, + _T_DEF_INT: INT, + _T_DEF_STRING: STRING, + _T_DEF_TRISTATE: TRISTATE, +} + +# Tokens after which strings are expected. This is used to tell strings from +# constant symbol references during tokenization, both of which are enclosed in +# quotes. +# +# Identifier-like lexemes ("missing quotes") are also treated as strings after +# these tokens. _T_CHOICE is included to avoid symbols being registered for +# named choices. +_STRING_LEX = frozenset({ + _T_BOOL, + _T_CHOICE, + _T_COMMENT, + _T_HEX, + _T_INT, + _T_MAINMENU, + _T_MENU, + _T_ORSOURCE, + _T_OSOURCE, + _T_PROMPT, + _T_RSOURCE, + _T_SOURCE, + _T_STRING, + _T_TRISTATE, +}) + +# Various sets for quick membership tests. Gives a single global lookup and +# avoids creating temporary dicts/tuples. + +_TYPE_TOKENS = frozenset({ + _T_BOOL, + _T_TRISTATE, + _T_INT, + _T_HEX, + _T_STRING, +}) + +_SOURCE_TOKENS = frozenset({ + _T_SOURCE, + _T_RSOURCE, + _T_OSOURCE, + _T_ORSOURCE, +}) + +_REL_SOURCE_TOKENS = frozenset({ + _T_RSOURCE, + _T_ORSOURCE, +}) + +# Obligatory (non-optional) sources +_OBL_SOURCE_TOKENS = frozenset({ + _T_SOURCE, + _T_RSOURCE, +}) + +_BOOL_TRISTATE = frozenset({ + BOOL, + TRISTATE, +}) + +_BOOL_TRISTATE_UNKNOWN = frozenset({ + BOOL, + TRISTATE, + UNKNOWN, +}) + +_INT_HEX = frozenset({ + INT, + HEX, +}) + +_SYMBOL_CHOICE = frozenset({ + Symbol, + Choice, +}) + +_MENU_COMMENT = frozenset({ + MENU, + COMMENT, +}) + +_EQUAL_UNEQUAL = frozenset({ + EQUAL, + UNEQUAL, +}) + +_RELATIONS = frozenset({ + EQUAL, + UNEQUAL, + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, +}) + +# Helper functions for getting compiled regular expressions, with the needed +# matching function returned directly as a small optimization. +# +# Use ASCII regex matching on Python 3. It's already the default on Python 2. + + +def _re_match(regex): + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).match + + +def _re_search(regex): + return re.compile(regex, 0 if _IS_PY2 else re.ASCII).search + + +# Various regular expressions used during parsing + +# The initial token on a line. Also eats leading and trailing whitespace, so +# that we can jump straight to the next token (or to the end of the line if +# there is only one token). +# +# This regex will also fail to match for empty lines and comment lines. +# +# '$' is included to detect preprocessor variable assignments with macro +# expansions in the left-hand side. +_command_match = _re_match(r"\s*([A-Za-z0-9_$-]+)\s*") + +# An identifier/keyword after the first token. Also eats trailing whitespace. +# '$' is included to detect identifiers containing macro expansions. +_id_keyword_match = _re_match(r"([A-Za-z0-9_$/.-]+)\s*") + +# A fragment in the left-hand side of a preprocessor variable assignment. These +# are the portions between macro expansions ($(foo)). Macros are supported in +# the LHS (variable name). +_assignment_lhs_fragment_match = _re_match("[A-Za-z0-9_-]*") + +# The assignment operator and value (right-hand side) in a preprocessor +# variable assignment +_assignment_rhs_match = _re_match(r"\s*(=|:=|\+=)\s*(.*)") + +# Special characters/strings while expanding a macro (')', ',', and '$(') +_macro_special_search = _re_search(r"\)|,|\$\(") + +# Special characters/strings while expanding a string (quotes, '\', and '$(') +_string_special_search = _re_search(r'"|\'|\\|\$\(') + +# Special characters/strings while expanding a symbol name. Also includes +# end-of-line, in case the macro is the last thing on the line. +_name_special_search = _re_search(r'[^A-Za-z0-9_$/.-]|\$\(|$') + +# A valid right-hand side for an assignment to a string symbol in a .config +# file, including escaped characters. Extracts the contents. +_conf_string_match = _re_match(r'"((?:[^\\"]|\\.)*)"') diff --git a/support/kconfiglib/listnewconfig.py b/support/kconfiglib/listnewconfig.py new file mode 100755 index 0000000..59de141 --- /dev/null +++ b/support/kconfiglib/listnewconfig.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +List all user-modifiable symbols that are not given a value in the configuration +file. Usually, these are new symbols that have been added to the Kconfig files. + +The default configuration filename is '.config'. A different filename can be +passed in the KCONFIG_CONFIG environment variable. +""" +import sys + +from kconfiglib import standard_kconfig, BOOL, TRISTATE, INT, HEX, STRING, \ + TRI_TO_STR + + +def main(): + kconf = standard_kconfig() + # Make it possible to filter this message out + sys.stderr.write(kconf.load_config() + "\n") + + for sym in kconf.unique_defined_syms: + # Only show symbols that can be toggled. Choice symbols are a special + # case in that sym.assignable will be (2,) (length 1) for visible + # symbols in choices in y mode, but they can still be toggled by + # selecting some other symbol. + if sym.user_value is None and \ + (len(sym.assignable) > 1 or + (sym.visibility and (sym.orig_type in (INT, HEX, STRING) or + sym.choice))): + + # Don't reuse the 'config_string' format for bool/tristate symbols, + # to show n-valued symbols as 'CONFIG_FOO=n' instead of + # '# CONFIG_FOO is not set'. This matches the C tools. + if sym.orig_type in (BOOL, TRISTATE): + s = "{}{}={}\n".format(kconf.config_prefix, sym.name, + TRI_TO_STR[sym.tri_value]) + else: + s = sym.config_string + + sys.stdout.write(s) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/makefile.patch b/support/kconfiglib/makefile.patch new file mode 100644 index 0000000..a617ebd --- /dev/null +++ b/support/kconfiglib/makefile.patch @@ -0,0 +1,48 @@ +From 93daf46f309b0c8f86149ef58c4906387d054c22 Mon Sep 17 00:00:00 2001 +From: Ulf Magnusson <ulfalizer@xxxxxxxxx> +Date: Tue, 9 Jun 2015 13:01:34 +0200 +Subject: [PATCH] Kconfiglib scripts/kconfig/Makefile patch + +--- + scripts/kconfig/Makefile | 29 +++++++++++++++++++++++++++++ + 1 file changed, 29 insertions(+) + +diff --git a/scripts/kconfig/Makefile b/scripts/kconfig/Makefile +index 3f327e21f60e..8b7dd1292005 100644 +--- a/scripts/kconfig/Makefile ++++ b/scripts/kconfig/Makefile +@@ -27,2 +27,31 @@ gconfig: $(obj)/gconf + ++PHONY += scriptconfig iscriptconfig kmenuconfig guiconfig dumpvarsconfig ++ ++PYTHONCMD ?= python ++kpython := PYTHONPATH=$(srctree)/Kconfiglib:$$PYTHONPATH $(PYTHONCMD) ++ ++ifneq ($(filter scriptconfig,$(MAKECMDGOALS)),) ++ifndef SCRIPT ++$(error Use "make scriptconfig SCRIPT=<path to script> [SCRIPT_ARG=<argument>]") ++endif ++endif ++ ++scriptconfig: ++ $(Q)$(kpython) $(SCRIPT) $(Kconfig) $(if $(SCRIPT_ARG),"$(SCRIPT_ARG)") ++ ++iscriptconfig: ++ $(Q)$(kpython) -i -c \ ++ "import kconfiglib; \ ++ kconf = kconfiglib.Kconfig('$(Kconfig)'); \ ++ print('A Kconfig instance \'kconf\' for the architecture $(ARCH) has been created.')" ++ ++kmenuconfig: ++ $(Q)$(kpython) $(srctree)/Kconfiglib/menuconfig.py $(Kconfig) ++ ++guiconfig: ++ $(Q)$(kpython) $(srctree)/Kconfiglib/guiconfig.py $(Kconfig) ++ ++dumpvarsconfig: ++ $(Q)$(kpython) $(srctree)/Kconfiglib/examples/dumpvars.py $(Kconfig) ++ + menuconfig: $(obj)/mconf +-- +2.20.1 + diff --git a/support/kconfiglib/menuconfig.py b/support/kconfiglib/menuconfig.py new file mode 100755 index 0000000..4815c7f --- /dev/null +++ b/support/kconfiglib/menuconfig.py @@ -0,0 +1,3264 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Overview +======== + +A curses-based Python 2/3 menuconfig implementation. The interface should feel +familiar to people used to mconf ('make menuconfig'). + +Supports the same keys as mconf, and also supports a set of keybindings +inspired by Vi: + + J/K : Down/Up + L : Enter menu/Toggle item + H : Leave menu + Ctrl-D/U: Page Down/Page Up + G/End : Jump to end of list + g/Home : Jump to beginning of list + +[Space] toggles values if possible, and enters menus otherwise. [Enter] works +the other way around. + +The mconf feature where pressing a key jumps to a menu entry with that +character in it in the current menu isn't supported. A jump-to feature for +jumping directly to any symbol (including invisible symbols), choice, menu or +comment (as in a Kconfig 'comment "Foo"') is available instead. + +A few different modes are available: + + F: Toggle show-help mode, which shows the help text of the currently selected + item in the window at the bottom of the menu display. This is handy when + browsing through options. + + C: Toggle show-name mode, which shows the symbol name before each symbol menu + entry + + A: Toggle show-all mode, which shows all items, including currently invisible + items and items that lack a prompt. Invisible items are drawn in a different + style to make them stand out. + + +Running +======= + +menuconfig.py can be run either as a standalone executable or by calling the +menuconfig() function with an existing Kconfig instance. The second option is a +bit inflexible in that it will still load and save .config, etc. + +When run in standalone mode, the top-level Kconfig file to load can be passed +as a command-line argument. With no argument, it defaults to "Kconfig". + +The KCONFIG_CONFIG environment variable specifies the .config file to load (if +it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). + +$srctree is supported through Kconfiglib. + + +Color schemes +============= + +It is possible to customize the color scheme by setting the MENUCONFIG_STYLE +environment variable. For example, setting it to 'aquatic' will enable an +alternative, less yellow, more 'make menuconfig'-like color scheme, contributed +by Mitja Horvat (pinkfluid). + +This is the current list of built-in styles: + - default classic Kconfiglib theme with a yellow accent + - monochrome colorless theme (uses only bold and standout) attributes, + this style is used if the terminal doesn't support colors + - aquatic blue tinted style loosely resembling the lxdialog theme + +It is possible to customize the current style by changing colors of UI +elements on the screen. This is the list of elements that can be stylized: + + - path Top row in the main display, with the menu path + - separator Separator lines between windows. Also used for the top line + in the symbol information display. + - list List of items, e.g. the main display + - selection Style for the selected item + - inv-list Like list, but for invisible items. Used in show-all mode. + - inv-selection Like selection, but for invisible items. Used in show-all + mode. + - help Help text windows at the bottom of various fullscreen + dialogs + - show-help Window showing the help text in show-help mode + - frame Frame around dialog boxes + - body Body of dialog boxes + - edit Edit box in pop-up dialogs + - jump-edit Edit box in jump-to dialog + - text Symbol information text + +The color definition is a comma separated list of attributes: + + - fg:COLOR Set the foreground/background colors. COLOR can be one of + * or * the basic 16 colors (black, red, green, yellow, blue, + - bg:COLOR magenta, cyan, white and brighter versions, for example, + brightred). On terminals that support more than 8 colors, + you can also directly put in a color number, e.g. fg:123 + (hexadecimal and octal constants are accepted as well). + Colors outside the range -1..curses.COLORS-1 (which is + terminal-dependent) are ignored (with a warning). The COLOR + can be also specified using a RGB value in the HTML + notation, for example #RRGGBB. If the terminal supports + color changing, the color is rendered accurately. + Otherwise, the visually nearest color is used. + + If the background or foreground color of an element is not + specified, it defaults to -1, representing the default + terminal foreground or background color. + + Note: On some terminals a bright version of the color + implies bold. + - bold Use bold text + - underline Use underline text + - standout Standout text attribute (reverse color) + +More often than not, some UI elements share the same color definition. In such +cases the right value may specify an UI element from which the color definition +will be copied. For example, "separator=help" will apply the current color +definition for "help" to "separator". + +A keyword without the '=' is assumed to be a style template. The template name +is looked up in the built-in styles list and the style definition is expanded +in-place. With this, built-in styles can be used as basis for new styles. + +For example, take the aquatic theme and give it a red selection bar: + +MENUCONFIG_STYLE="aquatic selection=fg:white,bg:red" + +If there's an error in the style definition or if a missing style is assigned +to, the assignment will be ignored, along with a warning being printed on +stderr. + +The 'default' theme is always implicitly parsed first (or the 'monochrome' +theme if the terminal lacks colors), so the following two settings have the +same effect: + + MENUCONFIG_STYLE="selection=fg:white,bg:red" + MENUCONFIG_STYLE="default selection=fg:white,bg:red" + + +Other features +============== + + - Seamless terminal resizing + + - No dependencies on *nix, as the 'curses' module is in the Python standard + library + + - Unicode text entry + + - Improved information screen compared to mconf: + + * Expressions are split up by their top-level &&/|| operands to improve + readability + + * Undefined symbols in expressions are pointed out + + * Menus and comments have information displays + + * Kconfig definitions are printed + + * The include path is shown, listing the locations of the 'source' + statements that included the Kconfig file of the symbol (or other + item) + + +Limitations +=========== + +Doesn't work out of the box on Windows, but can be made to work with 'pip +install windows-curses'. See the +https://github.com/zephyrproject-rtos/windows-curses repository. + +'pip install kconfiglib' on Windows automatically installs windows-curses +to make the menuconfig usable. +""" +from __future__ import print_function + +import curses +import errno +import locale +import os +import re +import sys +import textwrap + +from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \ + BOOL, TRISTATE, STRING, INT, HEX, \ + AND, OR, \ + expr_str, expr_value, split_expr, \ + standard_sc_expr_str, \ + TRI_TO_STR, TYPE_TO_STR, \ + standard_kconfig, standard_config_filename + + +# +# Configuration variables +# + +# If True, try to change LC_CTYPE to a UTF-8 locale if it is set to the C +# locale (which implies ASCII). This fixes curses Unicode I/O issues on systems +# with bad defaults. ncurses configures itself from the locale settings. +# +# Related PEP: https://www.python.org/dev/peps/pep-0538/ +_CHANGE_C_LC_CTYPE_TO_UTF8 = True + +# How many steps an implicit submenu will be indented. Implicit submenus are +# created when an item depends on the symbol before it. Note that symbols +# defined with 'menuconfig' create a separate menu instead of indenting. +_SUBMENU_INDENT = 4 + +# Number of steps for Page Up/Down to jump +_PG_JUMP = 6 + +# Height of the help window in show-help mode +_SHOW_HELP_HEIGHT = 8 + +# How far the cursor needs to be from the edge of the window before it starts +# to scroll. Used for the main menu display, the information display, the +# search display, and for text boxes. +_SCROLL_OFFSET = 5 + +# Minimum width of dialogs that ask for text input +_INPUT_DIALOG_MIN_WIDTH = 30 + +# Number of arrows pointing up/down to draw when a window is scrolled +_N_SCROLL_ARROWS = 14 + +# Lines of help text shown at the bottom of the "main" display +_MAIN_HELP_LINES = """ +[Space/Enter] Toggle/enter [ESC] Leave menu [S] Save +[O] Load [?] Symbol info [/] Jump to symbol +[F] Toggle show-help mode [C] Toggle show-name mode [A] Toggle show-all mode +[Q] Quit (prompts for save) [D] Save minimal config (advanced) +"""[1:-1].split("\n") + +# Lines of help text shown at the bottom of the information dialog +_INFO_HELP_LINES = """ +[ESC/q] Return to menu [/] Jump to symbol +"""[1:-1].split("\n") + +# Lines of help text shown at the bottom of the search dialog +_JUMP_TO_HELP_LINES = """ +Type text to narrow the search. Regexes are supported (via Python's 're' +module). The up/down cursor keys step in the list. [Enter] jumps to the +selected symbol. [ESC] aborts the search. Type multiple space-separated +strings/regexes to find entries that match all of them. Type Ctrl-F to +view the help of the selected item without leaving the dialog. +"""[1:-1].split("\n") + +# +# Styling +# + +_STYLES = { + "default": """ + path=fg:black,bg:white,bold + separator=fg:black,bg:yellow,bold + list=fg:black,bg:white + selection=fg:white,bg:blue,bold + inv-list=fg:red,bg:white + inv-selection=fg:red,bg:blue + help=path + show-help=list + frame=fg:black,bg:yellow,bold + body=fg:white,bg:black + edit=fg:white,bg:blue + jump-edit=edit + text=list + """, + + # This style is forced on terminals that do no support colors + "monochrome": """ + path=bold + separator=bold,standout + list= + selection=bold,standout + inv-list=bold + inv-selection=bold,standout + help=bold + show-help= + frame=bold,standout + body= + edit=standout + jump-edit= + text= + """, + + # Blue tinted style loosely resembling lxdialog + "aquatic": """ + path=fg:cyan,bg:blue,bold + separator=fg:white,bg:cyan,bold + help=path + frame=fg:white,bg:cyan,bold + body=fg:brightwhite,bg:blue + edit=fg:black,bg:white + """ +} + +# Standard colors definition +_STYLE_STD_COLORS = { + # Basic colors + "black": curses.COLOR_BLACK, + "red": curses.COLOR_RED, + "green": curses.COLOR_GREEN, + "yellow": curses.COLOR_YELLOW, + "blue": curses.COLOR_BLUE, + "magenta": curses.COLOR_MAGENTA, + "cyan": curses.COLOR_CYAN, + "white": curses.COLOR_WHITE, + + # Bright versions + "brightblack": curses.COLOR_BLACK + 8, + "brightred": curses.COLOR_RED + 8, + "brightgreen": curses.COLOR_GREEN + 8, + "brightyellow": curses.COLOR_YELLOW + 8, + "brightblue": curses.COLOR_BLUE + 8, + "brightmagenta": curses.COLOR_MAGENTA + 8, + "brightcyan": curses.COLOR_CYAN + 8, + "brightwhite": curses.COLOR_WHITE + 8, + + # Aliases + "purple": curses.COLOR_MAGENTA, + "brightpurple": curses.COLOR_MAGENTA + 8, +} + + +def _rgb_to_6cube(rgb): + # Converts an 888 RGB color to a 3-tuple (nice in that it's hashable) + # representing the closest xterm 256-color 6x6x6 color cube color. + # + # The xterm 256-color extension uses a RGB color palette with components in + # the range 0-5 (a 6x6x6 cube). The catch is that the mapping is nonlinear. + # Index 0 in the 6x6x6 cube is mapped to 0, index 1 to 95, then 135, 175, + # etc., in increments of 40. See the links below: + # + # https://commons.wikimedia.org/wiki/File:Xterm_256color_chart.svg + # https://github.com/tmux/tmux/blob/master/colour.c + + # 48 is the middle ground between 0 and 95. + return tuple(0 if x < 48 else int(round(max(1, (x - 55)/40))) for x in rgb) + + +def _6cube_to_rgb(r6g6b6): + # Returns the 888 RGB color for a 666 xterm color cube index + + return tuple(0 if x == 0 else 40*x + 55 for x in r6g6b6) + + +def _rgb_to_gray(rgb): + # Converts an 888 RGB color to the index of an xterm 256-color grayscale + # color with approx. the same perceived brightness + + # Calculate the luminance (gray intensity) of the color. See + # https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color + # and + # https://www.w3.org/TR/AERT/#color-contrast + luma = 0.299*rgb[0] + 0.587*rgb[1] + 0.114*rgb[2] + + # Closest index in the grayscale palette, which starts at RGB 0x080808, + # with stepping 0x0A0A0A + index = int(round((luma - 8)/10)) + + # Clamp the index to 0-23, corresponding to 232-255 + return max(0, min(index, 23)) + + +def _gray_to_rgb(index): + # Convert a grayscale index to its closet single RGB component + + return 3*(10*index + 8,) # Returns a 3-tuple + + +# Obscure Python: We never pass a value for rgb2index, and it keeps pointing to +# the same dict. This avoids a global. +def _alloc_rgb(rgb, rgb2index={}): + # Initialize a new entry in the xterm palette to the given RGB color, + # returning its index. If the color has already been initialized, the index + # of the existing entry is returned. + # + # ncurses is palette-based, so we need to overwrite palette entries to make + # new colors. + # + # The colors from 0 to 15 are user-defined, and there's no way to query + # their RGB values, so we better leave them untouched. Also leave any + # hypothetical colors above 255 untouched (though we're unlikely to + # allocate that many colors anyway). + + if rgb in rgb2index: + return rgb2index[rgb] + + # Many terminals allow the user to customize the first 16 colors. Avoid + # changing their values. + color_index = 16 + len(rgb2index) + if color_index >= 256: + _warn("Unable to allocate new RGB color ", rgb, ". Too many colors " + "allocated.") + return 0 + + # Map each RGB component from the range 0-255 to the range 0-1000, which is + # what curses uses + curses.init_color(color_index, *(int(round(1000*x/255)) for x in rgb)) + rgb2index[rgb] = color_index + + return color_index + + +def _color_from_num(num): + # Returns the index of a color that looks like color 'num' in the xterm + # 256-color palette (but that might not be 'num', if we're redefining + # colors) + + # - _alloc_rgb() won't touch the first 16 colors or any (hypothetical) + # colors above 255, so we can always return them as-is + # + # - If the terminal doesn't support changing color definitions, or if + # curses.COLORS < 256, _alloc_rgb() won't touch any color, and all colors + # can be returned as-is + if num < 16 or num > 255 or not curses.can_change_color() or \ + curses.COLORS < 256: + return num + + # _alloc_rgb() might redefine colors, so emulate the xterm 256-color + # palette by allocating new colors instead of returning color numbers + # directly + + if num < 232: + num -= 16 + return _alloc_rgb(_6cube_to_rgb(((num//36)%6, (num//6)%6, num%6))) + + return _alloc_rgb(_gray_to_rgb(num - 232)) + + +def _color_from_rgb(rgb): + # Returns the index of a color matching the 888 RGB color 'rgb'. The + # returned color might be an ~exact match or an approximation, depending on + # terminal capabilities. + + # Calculates the Euclidean distance between two RGB colors + def dist(r1, r2): return sum((x - y)**2 for x, y in zip(r1, r2)) + + if curses.COLORS >= 256: + # Assume we're dealing with xterm's 256-color extension + + if curses.can_change_color(): + # Best case -- the terminal supports changing palette entries via + # curses.init_color(). Initialize an unused palette entry and + # return it. + return _alloc_rgb(rgb) + + # Second best case -- pick between the xterm 256-color extension colors + + # Closest 6-cube "color" color + c6 = _rgb_to_6cube(rgb) + # Closest gray color + gray = _rgb_to_gray(rgb) + + if dist(rgb, _6cube_to_rgb(c6)) < dist(rgb, _gray_to_rgb(gray)): + # Use the "color" color from the 6x6x6 color palette. Calculate the + # color number from the 6-cube index triplet. + return 16 + 36*c6[0] + 6*c6[1] + c6[2] + + # Use the color from the gray palette + return 232 + gray + + # Terminal not in xterm 256-color mode. This is probably the best we can + # do, or is it? Submit patches. :) + min_dist = float('inf') + best = -1 + for color in range(curses.COLORS): + # ncurses uses the range 0..1000. Scale that down to 0..255. + d = dist(rgb, tuple(int(round(255*c/1000)) + for c in curses.color_content(color))) + if d < min_dist: + min_dist = d + best = color + + return best + + +def _parse_style(style_str, parsing_default): + # Parses a string with '<element>=<style>' assignments. Anything not + # containing '=' is assumed to be a reference to a built-in style, which is + # treated as if all the assignments from the style were inserted at that + # point in the string. + # + # The parsing_default flag is set to True when we're implicitly parsing the + # 'default'/'monochrome' style, to prevent warnings. + + for sline in style_str.split(): + # Words without a "=" character represents a style template + if "=" in sline: + key, data = sline.split("=", 1) + + # The 'default' style template is assumed to define all keys. We + # run _style_to_curses() for non-existing keys as well, so that we + # print warnings for errors to the right of '=' for those too. + if key not in _style and not parsing_default: + _warn("Ignoring non-existent style", key) + + # If data is a reference to another key, copy its style + if data in _style: + _style[key] = _style[data] + else: + _style[key] = _style_to_curses(data) + + elif sline in _STYLES: + # Recursively parse style template. Ignore styles that don't exist, + # for backwards/forwards compatibility. + _parse_style(_STYLES[sline], parsing_default) + + else: + _warn("Ignoring non-existent style template", sline) + +# Dictionary mapping element types to the curses attributes used to display +# them +_style = {} + + +def _style_to_curses(style_def): + # Parses a style definition string (<element>=<style>), returning + # a (fg_color, bg_color, attributes) tuple. + + def parse_color(color_def): + color_def = color_def.split(":", 1)[1] + + if color_def in _STYLE_STD_COLORS: + return _color_from_num(_STYLE_STD_COLORS[color_def]) + + # HTML format, #RRGGBB + if re.match("#[A-Fa-f0-9]{6}", color_def): + return _color_from_rgb(( + int(color_def[1:3], 16), + int(color_def[3:5], 16), + int(color_def[5:7], 16))) + + try: + color_num = _color_from_num(int(color_def, 0)) + except ValueError: + _warn("Ignoring color ", color_def, "that's neither predefined " + "nor a number") + + return -1 + + if not -1 <= color_num < curses.COLORS: + _warn("Ignoring color {}, which is outside the range " + "-1..curses.COLORS-1 (-1..{})" + .format(color_def, curses.COLORS - 1)) + + return -1 + + return color_num + + fg_color = -1 + bg_color = -1 + attrs = 0 + + if style_def: + for field in style_def.split(","): + if field.startswith("fg:"): + fg_color = parse_color(field) + elif field.startswith("bg:"): + bg_color = parse_color(field) + elif field == "bold": + # A_BOLD tends to produce faint and hard-to-read text on the + # Windows console, especially with the old color scheme, before + # the introduction of + # https://blogs.msdn.microsoft.com/commandline/2017/08/02/updating-the-windows-console-colors/ + attrs |= curses.A_NORMAL if _IS_WINDOWS else curses.A_BOLD + elif field == "standout": + attrs |= curses.A_STANDOUT + elif field == "underline": + attrs |= curses.A_UNDERLINE + else: + _warn("Ignoring unknown style attribute", field) + + return _style_attr(fg_color, bg_color, attrs) + + +def _init_styles(): + if curses.has_colors(): + curses.use_default_colors() + + # Use the 'monochrome' style template as the base on terminals without + # color + _parse_style("default" if curses.has_colors() else "monochrome", True) + + # Add any user-defined style from the environment + if "MENUCONFIG_STYLE" in os.environ: + _parse_style(os.environ["MENUCONFIG_STYLE"], False) + + +# color_attribs holds the color pairs we've already created, indexed by a +# (<foreground color>, <background color>) tuple. +# +# Obscure Python: We never pass a value for color_attribs, and it keeps +# pointing to the same dict. This avoids a global. +def _style_attr(fg_color, bg_color, attribs, color_attribs={}): + # Returns an attribute with the specified foreground and background color + # and the attributes in 'attribs'. Reuses color pairs already created if + # possible, and creates a new color pair otherwise. + # + # Returns 'attribs' if colors aren't supported. + + if not curses.has_colors(): + return attribs + + if (fg_color, bg_color) not in color_attribs: + # Create new color pair. Color pair number 0 is hardcoded and cannot be + # changed, hence the +1s. + curses.init_pair(len(color_attribs) + 1, fg_color, bg_color) + color_attribs[(fg_color, bg_color)] = \ + curses.color_pair(len(color_attribs) + 1) + + return color_attribs[(fg_color, bg_color)] | attribs + + +# +# Main application +# + + +def _main(): + menuconfig(standard_kconfig()) + + +def menuconfig(kconf): + """ + Launches the configuration interface, returning after the user exits. + + kconf: + Kconfig instance to be configured + """ + global _kconf + global _conf_filename + global _conf_changed + global _minconf_filename + global _show_all + + _kconf = kconf + + # Filename to save configuration to + _conf_filename = standard_config_filename() + + # Load existing configuration and set _conf_changed True if it is outdated + _conf_changed = _load_config() + + # Filename to save minimal configuration to + _minconf_filename = "defconfig" + + # Any visible items in the top menu? + _show_all = False + if not _shown_nodes(kconf.top_node): + # Nothing visible. Start in show-all mode and try again. + _show_all = True + if not _shown_nodes(kconf.top_node): + # Give up. The implementation relies on always having a selected + # node. + print("Empty configuration -- nothing to configure.\n" + "Check that environment variables are set properly.") + return + + # Disable warnings. They get mangled in curses mode, and we deal with + # errors ourselves. + kconf.warn = False + + # Make curses use the locale settings specified in the environment + locale.setlocale(locale.LC_ALL, "") + + # Try to fix Unicode issues on systems with bad defaults + if _CHANGE_C_LC_CTYPE_TO_UTF8: + _change_c_lc_ctype_to_utf8() + + # Get rid of the delay between pressing ESC and jumping to the parent menu, + # unless the user has set ESCDELAY (see ncurses(3)). This makes the UI much + # smoother to work with. + # + # Note: This is strictly pretty iffy, since escape codes for e.g. cursor + # keys start with ESC, but I've never seen it cause problems in practice + # (probably because it's unlikely that the escape code for a key would get + # split up across read()s, at least with a terminal emulator). Please + # report if you run into issues. Some suitable small default value could be + # used here instead in that case. Maybe it's silly to not put in the + # smallest imperceptible delay here already, though I don't like guessing. + # + # (From a quick glance at the ncurses source code, ESCDELAY might only be + # relevant for mouse events there, so maybe escapes are assumed to arrive + # in one piece already...) + os.environ.setdefault("ESCDELAY", "0") + + # Enter curses mode. _menuconfig() returns a string to print on exit, after + # curses has been de-initialized. + print(curses.wrapper(_menuconfig)) + + +def _load_config(): + # Loads any existing .config file. See the Kconfig.load_config() docstring. + # + # Returns True if .config is missing or outdated. We always prompt for + # saving the configuration in that case. + + print(_kconf.load_config()) + if not os.path.exists(_conf_filename): + # No .config + return True + + return _needs_save() + + +def _needs_save(): + # Returns True if a just-loaded .config file is outdated (would get + # modified when saving) + + if _kconf.missing_syms: + # Assignments to undefined symbols in the .config + return True + + for sym in _kconf.unique_defined_syms: + if sym.user_value is None: + if sym.config_string: + # Unwritten symbol + return True + elif sym.orig_type in (BOOL, TRISTATE): + if sym.tri_value != sym.user_value: + # Written bool/tristate symbol, new value + return True + elif sym.str_value != sym.user_value: + # Written string/int/hex symbol, new value + return True + + # No need to prompt for save + return False + + +# Global variables used below: +# +# _stdscr: +# stdscr from curses +# +# _cur_menu: +# Menu node of the menu (or menuconfig symbol, or choice) currently being +# shown +# +# _shown: +# List of items in _cur_menu that are shown (ignoring scrolling). In +# show-all mode, this list contains all items in _cur_menu. Otherwise, it +# contains just the visible items. +# +# _sel_node_i: +# Index in _shown of the currently selected node +# +# _menu_scroll: +# Index in _shown of the top row of the main display +# +# _parent_screen_rows: +# List/stack of the row numbers that the selections in the parent menus +# appeared on. This is used to prevent the scrolling from jumping around +# when going in and out of menus. +# +# _show_help/_show_name/_show_all: +# If True, the corresponding mode is on. See the module docstring. +# +# _conf_filename: +# File to save the configuration to +# +# _minconf_filename: +# File to save minimal configurations to +# +# _conf_changed: +# True if the configuration has been changed. If False, we don't bother +# showing the save-and-quit dialog. +# +# We reset this to False whenever the configuration is saved explicitly +# from the save dialog. + + +def _menuconfig(stdscr): + # Logic for the main display, with the list of symbols, etc. + + global _stdscr + global _conf_filename + global _conf_changed + global _minconf_filename + global _show_help + global _show_name + + _stdscr = stdscr + + _init() + + while True: + _draw_main() + curses.doupdate() + + + c = _getch_compat(_menu_win) + + if c == curses.KEY_RESIZE: + _resize_main() + + elif c in (curses.KEY_DOWN, "j", "J"): + _select_next_menu_entry() + + elif c in (curses.KEY_UP, "k", "K"): + _select_prev_menu_entry() + + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D + # Keep it simple. This way we get sane behavior for small windows, + # etc., for free. + for _ in range(_PG_JUMP): + _select_next_menu_entry() + + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U + for _ in range(_PG_JUMP): + _select_prev_menu_entry() + + elif c in (curses.KEY_END, "G"): + _select_last_menu_entry() + + elif c in (curses.KEY_HOME, "g"): + _select_first_menu_entry() + + elif c == " ": + # Toggle the node if possible + sel_node = _shown[_sel_node_i] + if not _change_node(sel_node): + _enter_menu(sel_node) + + elif c in (curses.KEY_RIGHT, "\n", "l", "L"): + # Enter the node if possible + sel_node = _shown[_sel_node_i] + if not _enter_menu(sel_node): + _change_node(sel_node) + + elif c in ("n", "N"): + _set_sel_node_tri_val(0) + + elif c in ("m", "M"): + _set_sel_node_tri_val(1) + + elif c in ("y", "Y"): + _set_sel_node_tri_val(2) + + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, + "\x1B", "h", "H"): # \x1B = ESC + + if c == "\x1B" and _cur_menu is _kconf.top_node: + res = _quit_dialog() + if res: + return res + else: + _leave_menu() + + elif c in ("o", "O"): + _load_dialog() + + elif c in ("s", "S"): + filename = _save_dialog(_kconf.write_config, _conf_filename, + "configuration") + if filename: + _conf_filename = filename + _conf_changed = False + + elif c in ("d", "D"): + filename = _save_dialog(_kconf.write_min_config, _minconf_filename, + "minimal configuration") + if filename: + _minconf_filename = filename + + elif c == "/": + _jump_to_dialog() + # The terminal might have been resized while the fullscreen jump-to + # dialog was open + _resize_main() + + elif c == "?": + _info_dialog(_shown[_sel_node_i], False) + # The terminal might have been resized while the fullscreen info + # dialog was open + _resize_main() + + elif c in ("f", "F"): + _show_help = not _show_help + _set_style(_help_win, "show-help" if _show_help else "help") + _resize_main() + + elif c in ("c", "C"): + _show_name = not _show_name + + elif c in ("a", "A"): + _toggle_show_all() + + elif c in ("q", "Q"): + res = _quit_dialog() + if res: + return res + + +def _quit_dialog(): + if not _conf_changed: + return "No changes to save (for '{}')".format(_conf_filename) + + while True: + c = _key_dialog( + "Quit", + " Save configuration?\n" + "\n" + "(Y)es (N)o (C)ancel", + "ync") + + if c is None or c == "c": + return None + + if c == "y": + # Returns a message to print + msg = _try_save(_kconf.write_config, _conf_filename, "configuration") + if msg: + return msg + + elif c == "n": + return "Configuration ({}) was not saved".format(_conf_filename) + + +def _init(): + # Initializes the main display with the list of symbols, etc. Also does + # misc. global initialization that needs to happen after initializing + # curses. + + global _ERASE_CHAR + + global _path_win + global _top_sep_win + global _menu_win + global _bot_sep_win + global _help_win + + global _parent_screen_rows + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + + global _show_help + global _show_name + + # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes + # backspace work with TERM=vt100. That makes it likely to work in sane + # environments. + _ERASE_CHAR = curses.erasechar() + if sys.version_info[0] >= 3: + # erasechar() returns a one-byte bytes object on Python 3. This sets + # _ERASE_CHAR to a blank string if it can't be decoded, which should be + # harmless. + _ERASE_CHAR = _ERASE_CHAR.decode("utf-8", "ignore") + + _init_styles() + + # Hide the cursor + _safe_curs_set(0) + + # Initialize windows + + # Top row, with menu path + _path_win = _styled_win("path") + + # Separator below menu path, with title and arrows pointing up + _top_sep_win = _styled_win("separator") + + # List of menu entries with symbols, etc. + _menu_win = _styled_win("list") + _menu_win.keypad(True) + + # Row below menu list, with arrows pointing down + _bot_sep_win = _styled_win("separator") + + # Help window with keys at the bottom. Shows help texts in show-help mode. + _help_win = _styled_win("help") + + # The rows we'd like the nodes in the parent menus to appear on. This + # prevents the scroll from jumping around when going in and out of menus. + _parent_screen_rows = [] + + # Initial state + + _cur_menu = _kconf.top_node + _shown = _shown_nodes(_cur_menu) + _sel_node_i = _menu_scroll = 0 + + _show_help = _show_name = False + + # Give windows their initial size + _resize_main() + + +def _resize_main(): + # Resizes the main display, with the list of symbols, etc., to fill the + # terminal + + global _menu_scroll + + screen_height, screen_width = _stdscr.getmaxyx() + + _path_win.resize(1, screen_width) + _top_sep_win.resize(1, screen_width) + _bot_sep_win.resize(1, screen_width) + + help_win_height = _SHOW_HELP_HEIGHT if _show_help else \ + len(_MAIN_HELP_LINES) + + menu_win_height = screen_height - help_win_height - 3 + + if menu_win_height >= 1: + _menu_win.resize(menu_win_height, screen_width) + _help_win.resize(help_win_height, screen_width) + + _top_sep_win.mvwin(1, 0) + _menu_win.mvwin(2, 0) + _bot_sep_win.mvwin(2 + menu_win_height, 0) + _help_win.mvwin(2 + menu_win_height + 1, 0) + else: + # Degenerate case. Give up on nice rendering and just prevent errors. + + menu_win_height = 1 + + _menu_win.resize(1, screen_width) + _help_win.resize(1, screen_width) + + for win in _top_sep_win, _menu_win, _bot_sep_win, _help_win: + win.mvwin(0, 0) + + # Adjust the scroll so that the selected node is still within the window, + # if needed + if _sel_node_i - _menu_scroll >= menu_win_height: + _menu_scroll = _sel_node_i - menu_win_height + 1 + + +def _height(win): + # Returns the height of 'win' + + return win.getmaxyx()[0] + + +def _width(win): + # Returns the width of 'win' + + return win.getmaxyx()[1] + + +def _enter_menu(menu): + # Makes 'menu' the currently displayed menu. In addition to actual 'menu's, + # "menu" here includes choices and symbols defined with the 'menuconfig' + # keyword. + # + # Returns False if 'menu' can't be entered. + + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + + if not menu.is_menuconfig: + # Not a menu + return False + + shown_sub = _shown_nodes(menu) + # Never enter empty menus. We depend on having a current node. + if not shown_sub: + return False + + # Remember where the current node appears on the screen, so we can try + # to get it to appear in the same place when we leave the menu + _parent_screen_rows.append(_sel_node_i - _menu_scroll) + + # Jump into menu + _cur_menu = menu + _shown = shown_sub + _sel_node_i = _menu_scroll = 0 + + if isinstance(menu.item, Choice): + _select_selected_choice_sym() + + return True + + +def _select_selected_choice_sym(): + # Puts the cursor on the currently selected (y-valued) choice symbol, if + # any. Does nothing if if the choice has no selection (is not visible/in y + # mode). + + global _sel_node_i + + choice = _cur_menu.item + if choice.selection: + # Search through all menu nodes to handle choice symbols being defined + # in multiple locations + for node in choice.selection.nodes: + if node in _shown: + _sel_node_i = _shown.index(node) + _center_vertically() + return + + +def _jump_to(node): + # Jumps directly to the menu node 'node' + + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + global _show_all + global _parent_screen_rows + + # Clear remembered menu locations. We might not even have been in the + # parent menus before. + _parent_screen_rows = [] + + old_show_all = _show_all + jump_into = (isinstance(node.item, Choice) or node.item == MENU) and \ + node.list + + # If we're jumping to a non-empty choice or menu, jump to the first entry + # in it instead of jumping to its menu node + if jump_into: + _cur_menu = node + node = node.list + else: + _cur_menu = _parent_menu(node) + + _shown = _shown_nodes(_cur_menu) + if node not in _shown: + # The node wouldn't be shown. Turn on show-all to show it. + _show_all = True + _shown = _shown_nodes(_cur_menu) + + _sel_node_i = _shown.index(node) + + if jump_into and not old_show_all and _show_all: + # If we're jumping into a choice or menu and were forced to turn on + # show-all because the first entry wasn't visible, try turning it off. + # That will land us at the first visible node if there are visible + # nodes, and is a no-op otherwise. + _toggle_show_all() + + _center_vertically() + + # If we're jumping to a non-empty choice, jump to the selected symbol, if + # any + if jump_into and isinstance(_cur_menu.item, Choice): + _select_selected_choice_sym() + + +def _leave_menu(): + # Jumps to the parent menu of the current menu. Does nothing if we're in + # the top menu. + + global _cur_menu + global _shown + global _sel_node_i + global _menu_scroll + + if _cur_menu is _kconf.top_node: + return + + # Jump to parent menu + parent = _parent_menu(_cur_menu) + _shown = _shown_nodes(parent) + _sel_node_i = _shown.index(_cur_menu) + _cur_menu = parent + + # Try to make the menu entry appear on the same row on the screen as it did + # before we entered the menu. + + if _parent_screen_rows: + # The terminal might have shrunk since we were last in the parent menu + screen_row = min(_parent_screen_rows.pop(), _height(_menu_win) - 1) + _menu_scroll = max(_sel_node_i - screen_row, 0) + else: + # No saved parent menu locations, meaning we jumped directly to some + # node earlier + _center_vertically() + + +def _select_next_menu_entry(): + # Selects the menu entry after the current one, adjusting the scroll if + # necessary. Does nothing if we're already at the last menu entry. + + global _sel_node_i + global _menu_scroll + + if _sel_node_i < len(_shown) - 1: + # Jump to the next node + _sel_node_i += 1 + + # If the new node is sufficiently close to the edge of the menu window + # (as determined by _SCROLL_OFFSET), increase the scroll by one. This + # gives nice and non-jumpy behavior even when + # _SCROLL_OFFSET >= _height(_menu_win). + if _sel_node_i >= _menu_scroll + _height(_menu_win) - _SCROLL_OFFSET \ + and _menu_scroll < _max_scroll(_shown, _menu_win): + + _menu_scroll += 1 + + +def _select_prev_menu_entry(): + # Selects the menu entry before the current one, adjusting the scroll if + # necessary. Does nothing if we're already at the first menu entry. + + global _sel_node_i + global _menu_scroll + + if _sel_node_i > 0: + # Jump to the previous node + _sel_node_i -= 1 + + # See _select_next_menu_entry() + if _sel_node_i < _menu_scroll + _SCROLL_OFFSET: + _menu_scroll = max(_menu_scroll - 1, 0) + + +def _select_last_menu_entry(): + # Selects the last menu entry in the current menu + + global _sel_node_i + global _menu_scroll + + _sel_node_i = len(_shown) - 1 + _menu_scroll = _max_scroll(_shown, _menu_win) + + +def _select_first_menu_entry(): + # Selects the first menu entry in the current menu + + global _sel_node_i + global _menu_scroll + + _sel_node_i = _menu_scroll = 0 + + +def _toggle_show_all(): + # Toggles show-all mode on/off. If turning it off would give no visible + # items in the current menu, it is left on. + + global _show_all + global _shown + global _sel_node_i + global _menu_scroll + + # Row on the screen the cursor is on. Preferably we want the same row to + # stay highlighted. + old_row = _sel_node_i - _menu_scroll + + _show_all = not _show_all + # List of new nodes to be shown after toggling _show_all + new_shown = _shown_nodes(_cur_menu) + + # Find a good node to select. The selected node might disappear if show-all + # mode is turned off. + + # Select the previously selected node itself if it is still visible. If + # there are visible nodes before it, select the closest one. + for node in _shown[_sel_node_i::-1]: + if node in new_shown: + _sel_node_i = new_shown.index(node) + break + else: + # No visible nodes before the previously selected node. Select the + # closest visible node after it instead. + for node in _shown[_sel_node_i + 1:]: + if node in new_shown: + _sel_node_i = new_shown.index(node) + break + else: + # No visible nodes at all, meaning show-all was turned off inside + # an invisible menu. Don't allow that, as the implementation relies + # on always having a selected node. + _show_all = True + return + + _shown = new_shown + + # Try to make the cursor stay on the same row in the menu window. This + # might be impossible if too many nodes have disappeared above the node. + _menu_scroll = max(_sel_node_i - old_row, 0) + + +def _center_vertically(): + # Centers the selected node vertically, if possible + + global _menu_scroll + + _menu_scroll = min(max(_sel_node_i - _height(_menu_win)//2, 0), + _max_scroll(_shown, _menu_win)) + + +def _draw_main(): + # Draws the "main" display, with the list of symbols, the header, and the + # footer. + # + # This could be optimized to only update the windows that have actually + # changed, but keep it simple for now and let curses sort it out. + + term_width = _width(_stdscr) + + + # + # Update the separator row below the menu path + # + + _top_sep_win.erase() + + # Draw arrows pointing up if the symbol window is scrolled down. Draw them + # before drawing the title, so the title ends up on top for small windows. + if _menu_scroll > 0: + _safe_hline(_top_sep_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) + + # Add the 'mainmenu' text as the title, centered at the top + _safe_addstr(_top_sep_win, + 0, max((term_width - len(_kconf.mainmenu_text))//2, 0), + _kconf.mainmenu_text) + + _top_sep_win.noutrefresh() + + # Note: The menu path at the top is deliberately updated last. See below. + + # + # Update the symbol window + # + + _menu_win.erase() + + # Draw the _shown nodes starting from index _menu_scroll up to either as + # many as fit in the window, or to the end of _shown + for i in range(_menu_scroll, + min(_menu_scroll + _height(_menu_win), len(_shown))): + + node = _shown[i] + + # The 'not _show_all' test avoids showing invisible items in red + # outside show-all mode, which could look confusing/broken. Invisible + # symbols show up outside show-all mode if an invisible symbol has + # visible children in an implicit (indented) menu. + if _visible(node) or not _show_all: + style = _style["selection" if i == _sel_node_i else "list"] + else: + style = _style["inv-selection" if i == _sel_node_i else "inv-list"] + + _safe_addstr(_menu_win, i - _menu_scroll, 0, _node_str(node), style) + + _menu_win.noutrefresh() + + + # + # Update the bottom separator window + # + + _bot_sep_win.erase() + + # Draw arrows pointing down if the symbol window is scrolled up + if _menu_scroll < _max_scroll(_shown, _menu_win): + _safe_hline(_bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) + + # Indicate when show-name/show-help/show-all mode is enabled + enabled_modes = [] + if _show_help: + enabled_modes.append("show-help (toggle with [F])") + if _show_name: + enabled_modes.append("show-name") + if _show_all: + enabled_modes.append("show-all") + if enabled_modes: + s = " and ".join(enabled_modes) + " mode enabled" + _safe_addstr(_bot_sep_win, 0, max(term_width - len(s) - 2, 0), s) + + _bot_sep_win.noutrefresh() + + + # + # Update the help window, which shows either key bindings or help texts + # + + _help_win.erase() + + if _show_help: + node = _shown[_sel_node_i] + if isinstance(node.item, (Symbol, Choice)) and node.help: + help_lines = textwrap.wrap(node.help, _width(_help_win)) + for i in range(min(_height(_help_win), len(help_lines))): + _safe_addstr(_help_win, i, 0, help_lines[i]) + else: + _safe_addstr(_help_win, 0, 0, "(no help)") + else: + for i, line in enumerate(_MAIN_HELP_LINES): + _safe_addstr(_help_win, i, 0, line) + + _help_win.noutrefresh() + + + # + # Update the top row with the menu path. + # + # Doing this last leaves the cursor on the top row, which avoids some minor + # annoying jumpiness in gnome-terminal when reducing the height of the + # terminal. It seems to happen whenever the row with the cursor on it + # disappears. + # + + _path_win.erase() + + # Draw the menu path ("(Top) -> Menu -> Submenu -> ...") + + menu_prompts = [] + + menu = _cur_menu + while menu is not _kconf.top_node: + # Promptless choices can be entered in show-all mode. Use + # standard_sc_expr_str() for them, so they show up as + # '<choice (name if any)>'. + menu_prompts.append(menu.prompt[0] if menu.prompt else + standard_sc_expr_str(menu.item)) + menu = menu.parent + menu_prompts.append("(Top)") + menu_prompts.reverse() + + # Hack: We can't put ACS_RARROW directly in the string. Temporarily + # represent it with NULL. + menu_path_str = " \0 ".join(menu_prompts) + + # Scroll the menu path to the right if needed to make the current menu's + # title visible + if len(menu_path_str) > term_width: + menu_path_str = menu_path_str[len(menu_path_str) - term_width:] + + # Print the path with the arrows reinserted + split_path = menu_path_str.split("\0") + _safe_addstr(_path_win, split_path[0]) + for s in split_path[1:]: + _safe_addch(_path_win, curses.ACS_RARROW) + _safe_addstr(_path_win, s) + + _path_win.noutrefresh() + + +def _parent_menu(node): + # Returns the menu node of the menu that contains 'node'. In addition to + # proper 'menu's, this might also be a 'menuconfig' symbol or a 'choice'. + # "Menu" here means a menu in the interface. + + menu = node.parent + while not menu.is_menuconfig: + menu = menu.parent + return menu + + +def _shown_nodes(menu): + # Returns the list of menu nodes from 'menu' (see _parent_menu()) that + # would be shown when entering it + + def rec(node): + res = [] + + while node: + if _visible(node) or _show_all: + res.append(node) + if node.list and not node.is_menuconfig: + # Nodes from implicit menu created from dependencies. Will + # be shown indented. Note that is_menuconfig is True for + # menus and choices as well as 'menuconfig' symbols. + res += rec(node.list) + + elif node.list and isinstance(node.item, Symbol): + # Show invisible symbols if they have visible children. This + # can happen for an m/y-valued symbol with an optional prompt + # ('prompt "foo" is COND') that is currently disabled. Note + # that it applies to both 'config' and 'menuconfig' symbols. + shown_children = rec(node.list) + if shown_children: + res.append(node) + if not node.is_menuconfig: + res += shown_children + + node = node.next + + return res + + if isinstance(menu.item, Choice): + # For named choices defined in multiple locations, entering the choice + # at a particular menu node would normally only show the choice symbols + # defined there (because that's what the MenuNode tree looks like). + # + # That might look confusing, and makes extending choices by defining + # them in multiple locations less useful. Instead, gather all the child + # menu nodes for all the choices whenever a choice is entered. That + # makes all choice symbols visible at all locations. + # + # Choices can contain non-symbol items (people do all sorts of weird + # stuff with them), hence the generality here. We really need to + # preserve the menu tree at each choice location. + # + # Note: Named choices are pretty broken in the C tools, and this is + # super obscure, so you probably won't find much that relies on this. + # This whole 'if' could be deleted if you don't care about defining + # choices in multiple locations to add symbols (which will still work, + # just with things being displayed in a way that might be unexpected). + + # Do some additional work to avoid listing choice symbols twice if all + # or part of the choice is copied in multiple locations (e.g. by + # including some Kconfig file multiple times). We give the prompts at + # the current location precedence. + seen_syms = {node.item for node in rec(menu.list) + if isinstance(node.item, Symbol)} + res = [] + for choice_node in menu.item.nodes: + for node in rec(choice_node.list): + # 'choice_node is menu' checks if we're dealing with the + # current location + if node.item not in seen_syms or choice_node is menu: + res.append(node) + if isinstance(node.item, Symbol): + seen_syms.add(node.item) + return res + + return rec(menu.list) + + +def _visible(node): + # Returns True if the node should appear in the menu (outside show-all + # mode) + + return node.prompt and expr_value(node.prompt[1]) and not \ + (node.item == MENU and not expr_value(node.visibility)) + + +def _change_node(node): + # Changes the value of the menu node 'node' if it is a symbol. Bools and + # tristates are toggled, while other symbol types pop up a text entry + # dialog. + # + # Returns False if the value of 'node' can't be changed. + + if not _changeable(node): + return False + + # sc = symbol/choice + sc = node.item + + if sc.orig_type in (INT, HEX, STRING): + s = sc.str_value + + while True: + s = _input_dialog( + "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.orig_type]), + s, _range_info(sc)) + + if s is None: + break + + if sc.orig_type in (INT, HEX): + s = s.strip() + + # 'make menuconfig' does this too. Hex values not starting with + # '0x' are accepted when loading .config files though. + if sc.orig_type == HEX and not s.startswith(("0x", "0X")): + s = "0x" + s + + if _check_valid(sc, s): + _set_val(sc, s) + break + + elif len(sc.assignable) == 1: + # Handles choice symbols for choices in y mode, which are a special + # case: .assignable can be (2,) while .tri_value is 0. + _set_val(sc, sc.assignable[0]) + + else: + # Set the symbol to the value after the current value in + # sc.assignable, with wrapping + val_index = sc.assignable.index(sc.tri_value) + _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)]) + + + if _is_y_mode_choice_sym(sc) and not node.list: + # Immediately jump to the parent menu after making a choice selection, + # like 'make menuconfig' does, except if the menu node has children + # (which can happen if a symbol 'depends on' a choice symbol that + # immediately precedes it). + _leave_menu() + + + return True + + +def _changeable(node): + # Returns True if the value if 'node' can be changed + + sc = node.item + + if not isinstance(sc, (Symbol, Choice)): + return False + + # This will hit for invisible symbols, which appear in show-all mode and + # when an invisible symbol has visible children (which can happen e.g. for + # symbols with optional prompts) + if not (node.prompt and expr_value(node.prompt[1])): + return False + + return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \ + or _is_y_mode_choice_sym(sc) + + +def _set_sel_node_tri_val(tri_val): + # Sets the value of the currently selected menu entry to 'tri_val', if that + # value can be assigned + + sc = _shown[_sel_node_i].item + if isinstance(sc, (Symbol, Choice)) and tri_val in sc.assignable: + _set_val(sc, tri_val) + + +def _set_val(sc, val): + # Wrapper around Symbol/Choice.set_value() for updating the menu state and + # _conf_changed + + global _conf_changed + + # Use the string representation of tristate values. This makes the format + # consistent for all symbol types. + if val in TRI_TO_STR: + val = TRI_TO_STR[val] + + if val != sc.str_value: + sc.set_value(val) + _conf_changed = True + + # Changing the value of the symbol might have changed what items in the + # current menu are visible. Recalculate the state. + _update_menu() + + +def _update_menu(): + # Updates the current menu after the value of a symbol or choice has been + # changed. Changing a value might change which items in the menu are + # visible. + # + # If possible, preserves the location of the cursor on the screen when + # items are added/removed above the selected item. + + global _shown + global _sel_node_i + global _menu_scroll + + # Row on the screen the cursor was on + old_row = _sel_node_i - _menu_scroll + + sel_node = _shown[_sel_node_i] + + # New visible nodes + _shown = _shown_nodes(_cur_menu) + + # New index of selected node + _sel_node_i = _shown.index(sel_node) + + # Try to make the cursor stay on the same row in the menu window. This + # might be impossible if too many nodes have disappeared above the node. + _menu_scroll = max(_sel_node_i - old_row, 0) + + +def _input_dialog(title, initial_text, info_text=None): + # Pops up a dialog that prompts the user for a string + # + # title: + # Title to display at the top of the dialog window's border + # + # initial_text: + # Initial text to prefill the input field with + # + # info_text: + # String to show next to the input field. If None, just the input field + # is shown. + + win = _styled_win("body") + win.keypad(True) + + info_lines = info_text.split("\n") if info_text else [] + + # Give the input dialog its initial size + _resize_input_dialog(win, title, info_lines) + + _safe_curs_set(2) + + # Input field text + s = initial_text + + # Cursor position + i = len(initial_text) + + def edit_width(): + return _width(win) - 4 + + # Horizontal scroll offset + hscroll = max(i - edit_width() + 1, 0) + + while True: + # Draw the "main" display with the menu, etc., so that resizing still + # works properly. This is like a stack of windows, only hardcoded for + # now. + _draw_main() + _draw_input_dialog(win, title, info_lines, s, i, hscroll) + curses.doupdate() + + + c = _getch_compat(win) + + if c == curses.KEY_RESIZE: + # Resize the main display too. The dialog floats above it. + _resize_main() + _resize_input_dialog(win, title, info_lines) + + elif c == "\n": + _safe_curs_set(0) + return s + + elif c == "\x1B": # \x1B = ESC + _safe_curs_set(0) + return None + + else: + s, i, hscroll = _edit_text(c, s, i, hscroll, edit_width()) + + +def _resize_input_dialog(win, title, info_lines): + # Resizes the input dialog to a size appropriate for the terminal size + + screen_height, screen_width = _stdscr.getmaxyx() + + win_height = 5 + if info_lines: + win_height += len(info_lines) + 1 + win_height = min(win_height, screen_height) + + win_width = max(_INPUT_DIALOG_MIN_WIDTH, + len(title) + 4, + *(len(line) + 4 for line in info_lines)) + win_width = min(win_width, screen_width) + + win.resize(win_height, win_width) + win.mvwin((screen_height - win_height)//2, + (screen_width - win_width)//2) + + +def _draw_input_dialog(win, title, info_lines, s, i, hscroll): + edit_width = _width(win) - 4 + + win.erase() + + # Note: Perhaps having a separate window for the input field would be nicer + visible_s = s[hscroll:hscroll + edit_width] + _safe_addstr(win, 2, 2, visible_s + " "*(edit_width - len(visible_s)), + _style["edit"]) + + for linenr, line in enumerate(info_lines): + _safe_addstr(win, 4 + linenr, 2, line) + + # Draw the frame last so that it overwrites the body text for small windows + _draw_frame(win, title) + + _safe_move(win, 2, 2 + i - hscroll) + + win.noutrefresh() + + +def _load_dialog(): + # Dialog for loading a new configuration + + global _conf_changed + global _conf_filename + global _show_all + + if _conf_changed: + c = _key_dialog( + "Load", + "You have unsaved changes. Load new\n" + "configuration anyway?\n" + "\n" + " (O)K (C)ancel", + "oc") + + if c is None or c == "c": + return + + filename = _conf_filename + while True: + filename = _input_dialog("File to load", filename, _load_save_info()) + if filename is None: + return + + filename = os.path.expanduser(filename) + + if _try_load(filename): + _conf_filename = filename + _conf_changed = _needs_save() + + # Turn on show-all mode if the selected node is not visible after + # loading the new configuration. _shown still holds the old state. + if _shown[_sel_node_i] not in _shown_nodes(_cur_menu): + _show_all = True + + _update_menu() + + # The message dialog indirectly updates the menu display, so _msg() + # must be called after the new state has been initialized + _msg("Success", "Loaded " + filename) + return + + +def _try_load(filename): + # Tries to load a configuration file. Pops up an error and returns False on + # failure. + # + # filename: + # Configuration file to load + + try: + _kconf.load_config(filename) + return True + except EnvironmentError as e: + _error("Error loading '{}'\n\n{} (errno: {})" + .format(filename, e.strerror, errno.errorcode[e.errno])) + return False + + +def _save_dialog(save_fn, default_filename, description): + # Dialog for saving the current configuration + # + # save_fn: + # Function to call with 'filename' to save the file + # + # default_filename: + # Prefilled filename in the input field + # + # description: + # String describing the thing being saved + # + # Return value: + # The path to the saved file, or None if no file was saved + + filename = default_filename + while True: + filename = _input_dialog("Filename to save {} to".format(description), + filename, _load_save_info()) + if filename is None: + return None + + filename = os.path.expanduser(filename) + + msg = _try_save(save_fn, filename, description) + if msg: + _msg("Success", msg) + return filename + + +def _try_save(save_fn, filename, description): + # Tries to save a configuration file. Returns a message to print on + # success. + # + # save_fn: + # Function to call with 'filename' to save the file + # + # description: + # String describing the thing being saved + # + # Return value: + # A message to print on success, and None on failure + + try: + # save_fn() returns a message to print + return save_fn(filename) + except EnvironmentError as e: + _error("Error saving {} to '{}'\n\n{} (errno: {})" + .format(description, e.filename, e.strerror, + errno.errorcode[e.errno])) + return None + + +def _key_dialog(title, text, keys): + # Pops up a dialog that can be closed by pressing a key + # + # title: + # Title to display at the top of the dialog window's border + # + # text: + # Text to show in the dialog + # + # keys: + # List of keys that will close the dialog. Other keys (besides ESC) are + # ignored. The caller is responsible for providing a hint about which + # keys can be pressed in 'text'. + # + # Return value: + # The key that was pressed to close the dialog. Uppercase characters are + # converted to lowercase. ESC will always close the dialog, and returns + # None. + + win = _styled_win("body") + win.keypad(True) + + _resize_key_dialog(win, text) + + while True: + # See _input_dialog() + _draw_main() + _draw_key_dialog(win, title, text) + curses.doupdate() + + + c = _getch_compat(win) + + if c == curses.KEY_RESIZE: + # Resize the main display too. The dialog floats above it. + _resize_main() + _resize_key_dialog(win, text) + + elif c == "\x1B": # \x1B = ESC + return None + + elif isinstance(c, str): + c = c.lower() + if c in keys: + return c + + +def _resize_key_dialog(win, text): + # Resizes the key dialog to a size appropriate for the terminal size + + screen_height, screen_width = _stdscr.getmaxyx() + + lines = text.split("\n") + + win_height = min(len(lines) + 4, screen_height) + win_width = min(max(len(line) for line in lines) + 4, screen_width) + + win.resize(win_height, win_width) + win.mvwin((screen_height - win_height)//2, + (screen_width - win_width)//2) + + +def _draw_key_dialog(win, title, text): + win.erase() + + for i, line in enumerate(text.split("\n")): + _safe_addstr(win, 2 + i, 2, line) + + # Draw the frame last so that it overwrites the body text for small windows + _draw_frame(win, title) + + win.noutrefresh() + + +def _draw_frame(win, title): + # Draw a frame around the inner edges of 'win', with 'title' at the top + + win_height, win_width = win.getmaxyx() + + win.attron(_style["frame"]) + + # Draw top/bottom edge + _safe_hline(win, 0, 0, " ", win_width) + _safe_hline(win, win_height - 1, 0, " ", win_width) + + # Draw left/right edge + _safe_vline(win, 0, 0, " ", win_height) + _safe_vline(win, 0, win_width - 1, " ", win_height) + + # Draw title + _safe_addstr(win, 0, max((win_width - len(title))//2, 0), title) + + win.attroff(_style["frame"]) + + +def _jump_to_dialog(): + # Implements the jump-to dialog, where symbols can be looked up via + # incremental search and jumped to. + # + # Returns True if the user jumped to a symbol, and False if the dialog was + # canceled. + + s = "" # Search text + prev_s = None # Previous search text + s_i = 0 # Search text cursor position + hscroll = 0 # Horizontal scroll offset + + sel_node_i = 0 # Index of selected row + scroll = 0 # Index in 'matches' of the top row of the list + + # Edit box at the top + edit_box = _styled_win("jump-edit") + edit_box.keypad(True) + + # List of matches + matches_win = _styled_win("list") + + # Bottom separator, with arrows pointing down + bot_sep_win = _styled_win("separator") + + # Help window with instructions at the bottom + help_win = _styled_win("help") + + # Give windows their initial size + _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll) + + _safe_curs_set(2) + + # Logic duplication with _select_{next,prev}_menu_entry(), except we do a + # functional variant that returns the new (sel_node_i, scroll) values to + # avoid 'nonlocal'. TODO: Can this be factored out in some nice way? + + def select_next_match(): + if sel_node_i == len(matches) - 1: + return sel_node_i, scroll + + if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \ + and scroll < _max_scroll(matches, matches_win): + + return sel_node_i + 1, scroll + 1 + + return sel_node_i + 1, scroll + + def select_prev_match(): + if sel_node_i == 0: + return sel_node_i, scroll + + if sel_node_i - 1 < scroll + _SCROLL_OFFSET: + return sel_node_i - 1, max(scroll - 1, 0) + + return sel_node_i - 1, scroll + + while True: + if s != prev_s: + # The search text changed. Find new matching nodes. + + prev_s = s + + try: + # We could use re.IGNORECASE here instead of lower(), but this + # is noticeably less jerky while inputting regexes like + # '.*debug$' (though the '.*' is redundant there). Those + # probably have bad interactions with re.search(), which + # matches anywhere in the string. + # + # It's not horrible either way. Just a bit smoother. + regex_searches = [re.compile(regex).search + for regex in s.lower().split()] + + # No exception thrown, so the regexes are okay + bad_re = None + + # List of matching nodes + matches = [] + add_match = matches.append + + # Search symbols and choices + + for node in _sorted_sc_nodes(): + # Symbol/choice + sc = node.item + + for search in regex_searches: + # Both the name and the prompt might be missing, since + # we're searching both symbols and choices + + # Does the regex match either the symbol name or the + # prompt (if any)? + if not (sc.name and search(sc.name.lower()) or + node.prompt and search(node.prompt[0].lower())): + + # Give up on the first regex that doesn't match, to + # speed things up a bit when multiple regexes are + # entered + break + + else: + add_match(node) + + # Search menus and comments + + for node in _sorted_menu_comment_nodes(): + for search in regex_searches: + if not search(node.prompt[0].lower()): + break + else: + add_match(node) + + except re.error as e: + # Bad regex. Remember the error message so we can show it. + bad_re = "Bad regular expression" + # re.error.msg was added in Python 3.5 + if hasattr(e, "msg"): + bad_re += ": " + e.msg + + matches = [] + + # Reset scroll and jump to the top of the list of matches + sel_node_i = scroll = 0 + + _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + s, s_i, hscroll, + bad_re, matches, sel_node_i, scroll) + curses.doupdate() + + + c = _getch_compat(edit_box) + + if c == "\n": + if matches: + _jump_to(matches[sel_node_i]) + _safe_curs_set(0) + return True + + elif c == "\x1B": # \x1B = ESC + _safe_curs_set(0) + return False + + elif c == curses.KEY_RESIZE: + # We adjust the scroll so that the selected node stays visible in + # the list when the terminal is resized, hence the 'scroll' + # assignment + scroll = _resize_jump_to_dialog( + edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll) + + elif c == "\x06": # \x06 = Ctrl-F + if matches: + _safe_curs_set(0) + _info_dialog(matches[sel_node_i], True) + _safe_curs_set(2) + + scroll = _resize_jump_to_dialog( + edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll) + + elif c == curses.KEY_DOWN: + sel_node_i, scroll = select_next_match() + + elif c == curses.KEY_UP: + sel_node_i, scroll = select_prev_match() + + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D + # Keep it simple. This way we get sane behavior for small windows, + # etc., for free. + for _ in range(_PG_JUMP): + sel_node_i, scroll = select_next_match() + + # Page Up (no Ctrl-U, as it's already used by the edit box) + elif c == curses.KEY_PPAGE: + for _ in range(_PG_JUMP): + sel_node_i, scroll = select_prev_match() + + elif c == curses.KEY_END: + sel_node_i = len(matches) - 1 + scroll = _max_scroll(matches, matches_win) + + elif c == curses.KEY_HOME: + sel_node_i = scroll = 0 + + else: + s, s_i, hscroll = _edit_text(c, s, s_i, hscroll, + _width(edit_box) - 2) + + +# Obscure Python: We never pass a value for cached_nodes, and it keeps pointing +# to the same list. This avoids a global. +def _sorted_sc_nodes(cached_nodes=[]): + # Returns a sorted list of symbol and choice nodes to search. The symbol + # nodes appear first, sorted by name, and then the choice nodes, sorted by + # prompt and (secondarily) name. + + if not cached_nodes: + # Add symbol nodes + for sym in sorted(_kconf.unique_defined_syms, + key=lambda sym: sym.name): + # += is in-place for lists + cached_nodes += sym.nodes + + # Add choice nodes + + choices = sorted(_kconf.unique_choices, + key=lambda choice: choice.name or "") + + cached_nodes += sorted( + [node + for choice in choices + for node in choice.nodes], + key=lambda node: node.prompt[0] if node.prompt else "") + + return cached_nodes + + +def _sorted_menu_comment_nodes(cached_nodes=[]): + # Returns a list of menu and comment nodes to search, sorted by prompt, + # with the menus first + + if not cached_nodes: + def prompt_text(mc): + return mc.prompt[0] + + cached_nodes += sorted(_kconf.menus, key=prompt_text) + cached_nodes += sorted(_kconf.comments, key=prompt_text) + + return cached_nodes + + +def _resize_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + sel_node_i, scroll): + # Resizes the jump-to dialog to fill the terminal. + # + # Returns the new scroll index. We adjust the scroll if needed so that the + # selected node stays visible. + + screen_height, screen_width = _stdscr.getmaxyx() + + bot_sep_win.resize(1, screen_width) + + help_win_height = len(_JUMP_TO_HELP_LINES) + matches_win_height = screen_height - help_win_height - 4 + + if matches_win_height >= 1: + edit_box.resize(3, screen_width) + matches_win.resize(matches_win_height, screen_width) + help_win.resize(help_win_height, screen_width) + + matches_win.mvwin(3, 0) + bot_sep_win.mvwin(3 + matches_win_height, 0) + help_win.mvwin(3 + matches_win_height + 1, 0) + else: + # Degenerate case. Give up on nice rendering and just prevent errors. + + matches_win_height = 1 + + edit_box.resize(screen_height, screen_width) + matches_win.resize(1, screen_width) + help_win.resize(1, screen_width) + + for win in matches_win, bot_sep_win, help_win: + win.mvwin(0, 0) + + # Adjust the scroll so that the selected row is still within the window, if + # needed + if sel_node_i - scroll >= matches_win_height: + return sel_node_i - matches_win_height + 1 + return scroll + + +def _draw_jump_to_dialog(edit_box, matches_win, bot_sep_win, help_win, + s, s_i, hscroll, + bad_re, matches, sel_node_i, scroll): + + edit_width = _width(edit_box) - 2 + + + # + # Update list of matches + # + + matches_win.erase() + + if matches: + for i in range(scroll, + min(scroll + _height(matches_win), len(matches))): + + node = matches[i] + + if isinstance(node.item, (Symbol, Choice)): + node_str = _name_and_val_str(node.item) + if node.prompt: + node_str += ' "{}"'.format(node.prompt[0]) + elif node.item == MENU: + node_str = 'menu "{}"'.format(node.prompt[0]) + else: # node.item == COMMENT + node_str = 'comment "{}"'.format(node.prompt[0]) + + _safe_addstr(matches_win, i - scroll, 0, node_str, + _style["selection" if i == sel_node_i else "list"]) + + else: + # bad_re holds the error message from the re.error exception on errors + _safe_addstr(matches_win, 0, 0, bad_re or "No matches") + + matches_win.noutrefresh() + + + # + # Update bottom separator line + # + + bot_sep_win.erase() + + # Draw arrows pointing down if the symbol list is scrolled up + if scroll < _max_scroll(matches, matches_win): + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) + + bot_sep_win.noutrefresh() + + + # + # Update help window at bottom + # + + help_win.erase() + + for i, line in enumerate(_JUMP_TO_HELP_LINES): + _safe_addstr(help_win, i, 0, line) + + help_win.noutrefresh() + + + # + # Update edit box. We do this last since it makes it handy to position the + # cursor. + # + + edit_box.erase() + + _draw_frame(edit_box, "Jump to symbol/choice/menu/comment") + + # Draw arrows pointing up if the symbol list is scrolled down + if scroll > 0: + # TODO: Bit ugly that _style["frame"] is repeated here + _safe_hline(edit_box, 2, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS, + _style["frame"]) + + visible_s = s[hscroll:hscroll + edit_width] + _safe_addstr(edit_box, 1, 1, visible_s) + + _safe_move(edit_box, 1, 1 + s_i - hscroll) + + edit_box.noutrefresh() + + +def _info_dialog(node, from_jump_to_dialog): + # Shows a fullscreen window with information about 'node'. + # + # If 'from_jump_to_dialog' is True, the information dialog was opened from + # within the jump-to-dialog. In this case, we make '/' from within the + # information dialog just return, to avoid a confusing recursive invocation + # of the jump-to-dialog. + + # Top row, with title and arrows point up + top_line_win = _styled_win("separator") + + # Text display + text_win = _styled_win("text") + text_win.keypad(True) + + # Bottom separator, with arrows pointing down + bot_sep_win = _styled_win("separator") + + # Help window with keys at the bottom + help_win = _styled_win("help") + + # Give windows their initial size + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) + + + # Get lines of help text + lines = _info_str(node).split("\n") + + # Index of first row in 'lines' to show + scroll = 0 + + while True: + _draw_info_dialog(node, lines, scroll, top_line_win, text_win, + bot_sep_win, help_win) + curses.doupdate() + + + c = _getch_compat(text_win) + + if c == curses.KEY_RESIZE: + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) + + elif c in (curses.KEY_DOWN, "j", "J"): + if scroll < _max_scroll(lines, text_win): + scroll += 1 + + elif c in (curses.KEY_NPAGE, "\x04"): # Page Down/Ctrl-D + scroll = min(scroll + _PG_JUMP, _max_scroll(lines, text_win)) + + elif c in (curses.KEY_PPAGE, "\x15"): # Page Up/Ctrl-U + scroll = max(scroll - _PG_JUMP, 0) + + elif c in (curses.KEY_END, "G"): + scroll = _max_scroll(lines, text_win) + + elif c in (curses.KEY_HOME, "g"): + scroll = 0 + + elif c in (curses.KEY_UP, "k", "K"): + if scroll > 0: + scroll -= 1 + + elif c == "/": + # Support starting a search from within the information dialog + + if from_jump_to_dialog: + # Avoid recursion + return + + if _jump_to_dialog(): + # Jumped to a symbol. Cancel the information dialog. + return + + # Stay in the information dialog if the jump-to dialog was + # canceled. Resize it in case the terminal was resized while the + # fullscreen jump-to dialog was open. + _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win) + + elif c in (curses.KEY_LEFT, curses.KEY_BACKSPACE, _ERASE_CHAR, + "\x1B", # \x1B = ESC + "q", "Q", "h", "H"): + + return + + +def _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win): + # Resizes the info dialog to fill the terminal + + screen_height, screen_width = _stdscr.getmaxyx() + + top_line_win.resize(1, screen_width) + bot_sep_win.resize(1, screen_width) + + help_win_height = len(_INFO_HELP_LINES) + text_win_height = screen_height - help_win_height - 2 + + if text_win_height >= 1: + text_win.resize(text_win_height, screen_width) + help_win.resize(help_win_height, screen_width) + + text_win.mvwin(1, 0) + bot_sep_win.mvwin(1 + text_win_height, 0) + help_win.mvwin(1 + text_win_height + 1, 0) + else: + # Degenerate case. Give up on nice rendering and just prevent errors. + + text_win.resize(1, screen_width) + help_win.resize(1, screen_width) + + for win in text_win, bot_sep_win, help_win: + win.mvwin(0, 0) + + +def _draw_info_dialog(node, lines, scroll, top_line_win, text_win, + bot_sep_win, help_win): + + text_win_height, text_win_width = text_win.getmaxyx() + + + # Note: The top row is deliberately updated last. See _draw_main(). + + # + # Update text display + # + + text_win.erase() + + for i, line in enumerate(lines[scroll:scroll + text_win_height]): + _safe_addstr(text_win, i, 0, line) + + text_win.noutrefresh() + + + # + # Update bottom separator line + # + + bot_sep_win.erase() + + # Draw arrows pointing down if the symbol window is scrolled up + if scroll < _max_scroll(lines, text_win): + _safe_hline(bot_sep_win, 0, 4, curses.ACS_DARROW, _N_SCROLL_ARROWS) + + bot_sep_win.noutrefresh() + + + # + # Update help window at bottom + # + + help_win.erase() + + for i, line in enumerate(_INFO_HELP_LINES): + _safe_addstr(help_win, i, 0, line) + + help_win.noutrefresh() + + + # + # Update top row + # + + top_line_win.erase() + + # Draw arrows pointing up if the information window is scrolled down. Draw + # them before drawing the title, so the title ends up on top for small + # windows. + if scroll > 0: + _safe_hline(top_line_win, 0, 4, curses.ACS_UARROW, _N_SCROLL_ARROWS) + + title = ("Symbol" if isinstance(node.item, Symbol) else + "Choice" if isinstance(node.item, Choice) else + "Menu" if node.item == MENU else + "Comment") + " information" + _safe_addstr(top_line_win, 0, max((text_win_width - len(title))//2, 0), + title) + + top_line_win.noutrefresh() + + +def _info_str(node): + # Returns information about the menu node 'node' as a string. + # + # The helper functions are responsible for adding newlines. This allows + # them to return "" if they don't want to add any output. + + if isinstance(node.item, Symbol): + sym = node.item + + return ( + _name_info(sym) + + _prompt_info(sym) + + "Type: {}\n".format(TYPE_TO_STR[sym.type]) + + _value_info(sym) + + _help_info(sym) + + _direct_dep_info(sym) + + _defaults_info(sym) + + _select_imply_info(sym) + + _kconfig_def_info(sym) + ) + + if isinstance(node.item, Choice): + choice = node.item + + return ( + _name_info(choice) + + _prompt_info(choice) + + "Type: {}\n".format(TYPE_TO_STR[choice.type]) + + 'Mode: {}\n'.format(choice.str_value) + + _help_info(choice) + + _choice_syms_info(choice) + + _direct_dep_info(choice) + + _defaults_info(choice) + + _kconfig_def_info(choice) + ) + + # node.item in (MENU, COMMENT) + return _kconfig_def_info(node) + + +def _name_info(sc): + # Returns a string with the name of the symbol/choice. Names are optional + # for choices. + + return "Name: {}\n".format(sc.name) if sc.name else "" + + +def _prompt_info(sc): + # Returns a string listing the prompts of 'sc' (Symbol or Choice) + + s = "" + + for node in sc.nodes: + if node.prompt: + s += "Prompt: {}\n".format(node.prompt[0]) + + return s + + +def _value_info(sym): + # Returns a string showing 'sym's value + + # Only put quotes around the value for string symbols + return "Value: {}\n".format( + '"{}"'.format(sym.str_value) + if sym.orig_type == STRING + else sym.str_value) + + +def _choice_syms_info(choice): + # Returns a string listing the choice symbols in 'choice'. Adds + # "(selected)" next to the selected one. + + s = "Choice symbols:\n" + + for sym in choice.syms: + s += " - " + sym.name + if sym is choice.selection: + s += " (selected)" + s += "\n" + + return s + "\n" + + +def _help_info(sc): + # Returns a string with the help text(s) of 'sc' (Symbol or Choice). + # Symbols and choices defined in multiple locations can have multiple help + # texts. + + s = "\n" + + for node in sc.nodes: + if node.help is not None: + s += "Help:\n\n{}\n\n".format(_indent(node.help, 2)) + + return s + + +def _direct_dep_info(sc): + # Returns a string describing the direct dependencies of 'sc' (Symbol or + # Choice). The direct dependencies are the OR of the dependencies from each + # definition location. The dependencies at each definition location come + # from 'depends on' and dependencies inherited from parent items. + + return "" if sc.direct_dep is _kconf.y else \ + 'Direct dependencies (={}):\n{}\n' \ + .format(TRI_TO_STR[expr_value(sc.direct_dep)], + _split_expr_info(sc.direct_dep, 2)) + + +def _defaults_info(sc): + # Returns a string describing the defaults of 'sc' (Symbol or Choice) + + if not sc.defaults: + return "" + + s = "Default" + if len(sc.defaults) > 1: + s += "s" + s += ":\n" + + for val, cond in sc.orig_defaults: + s += " - " + if isinstance(sc, Symbol): + s += _expr_str(val) + + # Skip the tristate value hint if the expression is just a single + # symbol. _expr_str() already shows its value as a string. + # + # This also avoids showing the tristate value for string/int/hex + # defaults, which wouldn't make any sense. + if isinstance(val, tuple): + s += ' (={})'.format(TRI_TO_STR[expr_value(val)]) + else: + # Don't print the value next to the symbol name for choice + # defaults, as it looks a bit confusing + s += val.name + s += "\n" + + if cond is not _kconf.y: + s += " Condition (={}):\n{}" \ + .format(TRI_TO_STR[expr_value(cond)], + _split_expr_info(cond, 4)) + + return s + "\n" + + +def _split_expr_info(expr, indent): + # Returns a string with 'expr' split into its top-level && or || operands, + # with one operand per line, together with the operand's value. This is + # usually enough to get something readable for long expressions. A fancier + # recursive thingy would be possible too. + # + # indent: + # Number of leading spaces to add before the split expression. + + if len(split_expr(expr, AND)) > 1: + split_op = AND + op_str = "&&" + else: + split_op = OR + op_str = "||" + + s = "" + for i, term in enumerate(split_expr(expr, split_op)): + s += "{}{} {}".format(indent*" ", + " " if i == 0 else op_str, + _expr_str(term)) + + # Don't bother showing the value hint if the expression is just a + # single symbol. _expr_str() already shows its value. + if isinstance(term, tuple): + s += " (={})".format(TRI_TO_STR[expr_value(term)]) + + s += "\n" + + return s + + +def _select_imply_info(sym): + # Returns a string with information about which symbols 'select' or 'imply' + # 'sym'. The selecting/implying symbols are grouped according to which + # value they select/imply 'sym' to (n/m/y). + + def sis(expr, val, title): + # sis = selects/implies + sis = [si for si in split_expr(expr, OR) if expr_value(si) == val] + if not sis: + return "" + + res = title + for si in sis: + res += " - {}\n".format(split_expr(si, AND)[0].name) + return res + "\n" + + s = "" + + if sym.rev_dep is not _kconf.n: + s += sis(sym.rev_dep, 2, + "Symbols currently y-selecting this symbol:\n") + s += sis(sym.rev_dep, 1, + "Symbols currently m-selecting this symbol:\n") + s += sis(sym.rev_dep, 0, + "Symbols currently n-selecting this symbol (no effect):\n") + + if sym.weak_rev_dep is not _kconf.n: + s += sis(sym.weak_rev_dep, 2, + "Symbols currently y-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 1, + "Symbols currently m-implying this symbol:\n") + s += sis(sym.weak_rev_dep, 0, + "Symbols currently n-implying this symbol (no effect):\n") + + return s + + +def _kconfig_def_info(item): + # Returns a string with the definition of 'item' in Kconfig syntax, + # together with the definition location(s) and their include and menu paths + + nodes = [item] if isinstance(item, MenuNode) else item.nodes + + s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" \ + .format("s" if len(nodes) > 1 else "") + s += (len(s) - 1)*"=" + + for node in nodes: + s += "\n\n" \ + "At {}:{}\n" \ + "{}" \ + "Menu path: {}\n\n" \ + "{}" \ + .format(node.filename, node.linenr, + _include_path_info(node), + _menu_path_info(node), + _indent(node.custom_str(_name_and_val_str), 2)) + + return s + + +def _include_path_info(node): + if not node.include_path: + # In the top-level Kconfig file + return "" + + return "Included via {}\n".format( + " -> ".join("{}:{}".format(filename, linenr) + for filename, linenr in node.include_path)) + + +def _menu_path_info(node): + # Returns a string describing the menu path leading up to 'node' + + path = "" + + while node.parent is not _kconf.top_node: + node = node.parent + + # Promptless choices might appear among the parents. Use + # standard_sc_expr_str() for them, so that they show up as + # '<choice (name if any)>'. + path = " -> " + (node.prompt[0] if node.prompt else + standard_sc_expr_str(node.item)) + path + + return "(Top)" + path + + +def _indent(s, n): + # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not + # available in Python 2 (it's 3.3+). + + return "\n".join(n*" " + line for line in s.split("\n")) + + +def _name_and_val_str(sc): + # Custom symbol/choice printer that shows symbol values after symbols + + # Show the values of non-constant (non-quoted) symbols that don't look like + # numbers. Things like 123 are actually symbol references, and only work as + # expected due to undefined symbols getting their name as their value. + # Showing the symbol value for those isn't helpful though. + if isinstance(sc, Symbol) and not sc.is_constant and not _is_num(sc.name): + if not sc.nodes: + # Undefined symbol reference + return "{}(undefined/n)".format(sc.name) + + return '{}(={})'.format(sc.name, sc.str_value) + + # For other items, use the standard format + return standard_sc_expr_str(sc) + + +def _expr_str(expr): + # Custom expression printer that shows symbol values + return expr_str(expr, _name_and_val_str) + + +def _styled_win(style): + # Returns a new curses window with style 'style' and space as the fill + # character. The initial dimensions are (1, 1), so the window needs to be + # sized and positioned separately. + + win = curses.newwin(1, 1) + _set_style(win, style) + return win + + +def _set_style(win, style): + # Changes the style of an existing window + + win.bkgdset(" ", _style[style]) + + +def _max_scroll(lst, win): + # Assuming 'lst' is a list of items to be displayed in 'win', + # returns the maximum number of steps 'win' can be scrolled down. + # We stop scrolling when the bottom item is visible. + + return max(0, len(lst) - _height(win)) + + +def _edit_text(c, s, i, hscroll, width): + # Implements text editing commands for edit boxes. Takes a character (which + # could also be e.g. curses.KEY_LEFT) and the edit box state, and returns + # the new state after the character has been processed. + # + # c: + # Character from user + # + # s: + # Current contents of string + # + # i: + # Current cursor index in string + # + # hscroll: + # Index in s of the leftmost character in the edit box, for horizontal + # scrolling + # + # width: + # Width in characters of the edit box + # + # Return value: + # An (s, i, hscroll) tuple for the new state + + if c == curses.KEY_LEFT: + if i > 0: + i -= 1 + + elif c == curses.KEY_RIGHT: + if i < len(s): + i += 1 + + elif c in (curses.KEY_HOME, "\x01"): # \x01 = CTRL-A + i = 0 + + elif c in (curses.KEY_END, "\x05"): # \x05 = CTRL-E + i = len(s) + + elif c in (curses.KEY_BACKSPACE, _ERASE_CHAR): + if i > 0: + s = s[:i-1] + s[i:] + i -= 1 + + elif c == curses.KEY_DC: + s = s[:i] + s[i+1:] + + elif c == "\x17": # \x17 = CTRL-W + # The \W removes characters like ',' one at a time + new_i = re.search(r"(?:\w*|\W)\s*$", s[:i]).start() + s = s[:new_i] + s[i:] + i = new_i + + elif c == "\x0B": # \x0B = CTRL-K + s = s[:i] + + elif c == "\x15": # \x15 = CTRL-U + s = s[i:] + i = 0 + + elif isinstance(c, str): + # Insert character + s = s[:i] + c + s[i:] + i += 1 + + # Adjust the horizontal scroll so that the cursor never touches the left or + # right edges of the edit box, except when it's at the beginning or the end + # of the string + if i < hscroll + _SCROLL_OFFSET: + hscroll = max(i - _SCROLL_OFFSET, 0) + elif i >= hscroll + width - _SCROLL_OFFSET: + max_scroll = max(len(s) - width + 1, 0) + hscroll = min(i - width + _SCROLL_OFFSET + 1, max_scroll) + + + return s, i, hscroll + + +def _load_save_info(): + # Returns an information string for load/save dialog boxes + + return "(Relative to {})\n\nRefer to your home directory with ~" \ + .format(os.path.join(os.getcwd(), "")) + + +def _msg(title, text): + # Pops up a message dialog that can be dismissed with Space/Enter/ESC + + _key_dialog(title, text, " \n") + + +def _error(text): + # Pops up an error dialog that can be dismissed with Space/Enter/ESC + + _msg("Error", text) + + +def _node_str(node): + # Returns the complete menu entry text for a menu node. + # + # Example return value: "[*] Support for X" + + # Calculate the indent to print the item with by checking how many levels + # above it the closest 'menuconfig' item is (this includes menus and + # choices as well as menuconfig symbols) + indent = 0 + parent = node.parent + while not parent.is_menuconfig: + indent += _SUBMENU_INDENT + parent = parent.parent + + # This approach gives nice alignment for empty string symbols ("() Foo") + s = "{:{}}".format(_value_str(node), 3 + indent) + + if _should_show_name(node): + if isinstance(node.item, Symbol): + s += " <{}>".format(node.item.name) + else: + # For choices, use standard_sc_expr_str(). That way they show up as + # '<choice (name if any)>'. + s += " " + standard_sc_expr_str(node.item) + + if node.prompt: + if node.item == COMMENT: + s += " *** {} ***".format(node.prompt[0]) + else: + s += " " + node.prompt[0] + + if isinstance(node.item, Symbol): + sym = node.item + + # Print "(NEW)" next to symbols without a user value (from e.g. a + # .config), but skip it for choice symbols in choices in y mode, + # and for symbols of UNKNOWN type (which generate a warning though) + if sym.user_value is None and sym.orig_type and \ + not (sym.choice and sym.choice.tri_value == 2): + + s += " (NEW)" + + if isinstance(node.item, Choice) and node.item.tri_value == 2: + # Print the prompt of the selected symbol after the choice for + # choices in y mode + sym = node.item.selection + if sym: + for sym_node in sym.nodes: + # Use the prompt used at this choice location, in case the + # choice symbol is defined in multiple locations + if sym_node.parent is node and sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + else: + # If the symbol isn't defined at this choice location, then + # just use whatever prompt we can find for it + for sym_node in sym.nodes: + if sym_node.prompt: + s += " ({})".format(sym_node.prompt[0]) + break + + # Print "--->" next to nodes that have menus that can potentially be + # entered. Print "----" if the menu is empty. We don't allow those to be + # entered. + if node.is_menuconfig: + s += " --->" if _shown_nodes(node) else " ----" + + return s + + +def _should_show_name(node): + # Returns True if 'node' is a symbol or choice whose name should shown (if + # any, as names are optional for choices) + + # The 'not node.prompt' case only hits in show-all mode, for promptless + # symbols and choices + return not node.prompt or \ + (_show_name and isinstance(node.item, (Symbol, Choice))) + + +def _value_str(node): + # Returns the value part ("[*]", "<M>", "(foo)" etc.) of a menu node + + item = node.item + + if item in (MENU, COMMENT): + return "" + + # Wouldn't normally happen, and generates a warning + if not item.orig_type: + return "" + + if item.orig_type in (STRING, INT, HEX): + return "({})".format(item.str_value) + + # BOOL or TRISTATE + + if _is_y_mode_choice_sym(item): + return "(X)" if item.choice.selection is item else "( )" + + tri_val_str = (" ", "M", "*")[item.tri_value] + + if len(item.assignable) <= 1: + # Pinned to a single value + return "" if isinstance(item, Choice) else "-{}-".format(tri_val_str) + + if item.type == BOOL: + return "[{}]".format(tri_val_str) + + # item.type == TRISTATE + if item.assignable == (1, 2): + return "{{{}}}".format(tri_val_str) # {M}/{*} + return "<{}>".format(tri_val_str) + + +def _is_y_mode_choice_sym(item): + # The choice mode is an upper bound on the visibility of choice symbols, so + # we can check the choice symbols' own visibility to see if the choice is + # in y mode + return isinstance(item, Symbol) and item.choice and item.visibility == 2 + + +def _check_valid(sym, s): + # Returns True if the string 's' is a well-formed value for 'sym'. + # Otherwise, displays an error and returns False. + + if sym.orig_type not in (INT, HEX): + # Anything goes for non-int/hex symbols + return True + + base = 10 if sym.orig_type == INT else 16 + try: + int(s, base) + except ValueError: + _error("'{}' is a malformed {} value" + .format(s, TYPE_TO_STR[sym.orig_type])) + return False + + for low_sym, high_sym, cond in sym.ranges: + if expr_value(cond): + low_s = low_sym.str_value + high_s = high_sym.str_value + + if not int(low_s, base) <= int(s, base) <= int(high_s, base): + _error("{} is outside the range {}-{}" + .format(s, low_s, high_s)) + return False + + break + + return True + + +def _range_info(sym): + # Returns a string with information about the valid range for the symbol + # 'sym', or None if 'sym' doesn't have a range + + if sym.orig_type in (INT, HEX): + for low, high, cond in sym.ranges: + if expr_value(cond): + return "Range: {}-{}".format(low.str_value, high.str_value) + + return None + + +def _is_num(name): + # Heuristic to see if a symbol name looks like a number, for nicer output + # when printing expressions. Things like 16 are actually symbol names, only + # they get their name as their value when the symbol is undefined. + + try: + int(name) + except ValueError: + if not name.startswith(("0x", "0X")): + return False + + try: + int(name, 16) + except ValueError: + return False + + return True + + +def _getch_compat(win): + # Uses get_wch() if available (Python 3.3+) and getch() otherwise. Also + # handles a PDCurses resizing quirk. + + if hasattr(win, "get_wch"): + c = win.get_wch() + else: + c = win.getch() + if 0 <= c <= 255: + c = chr(c) + + # Decent resizing behavior on PDCurses requires calling resize_term(0, 0) + # after receiving KEY_RESIZE, while ncurses (usually) handles terminal + # resizing automatically in get(_w)ch() (see the end of the + # resizeterm(3NCURSES) man page). + # + # resize_term(0, 0) reliably fails and does nothing on ncurses, so this + # hack gives ncurses/PDCurses compatibility for resizing. I don't know + # whether it would cause trouble for other implementations. + if c == curses.KEY_RESIZE: + try: + curses.resize_term(0, 0) + except curses.error: + pass + + return c + + +def _warn(*args): + # Temporarily returns from curses to shell mode and prints a warning to + # stderr. The warning would get lost in curses mode. + curses.endwin() + print("menuconfig warning: ", end="", file=sys.stderr) + print(*args, file=sys.stderr) + curses.doupdate() + + +# Ignore exceptions from some functions that might fail, e.g. for small +# windows. They usually do reasonable things anyway. + + +def _safe_curs_set(visibility): + try: + curses.curs_set(visibility) + except curses.error: + pass + + +def _safe_addstr(win, *args): + # Clip the line to avoid wrapping to the next line, which looks glitchy. + # addchstr() would do it for us, but it's not available in the 'curses' + # module. + + attr = None + if isinstance(args[0], str): + y, x = win.getyx() + s = args[0] + if len(args) == 2: + attr = args[1] + else: + y, x, s = args[:3] + if len(args) == 4: + attr = args[3] + + maxlen = _width(win) - x + s = s.expandtabs() + + try: + # The 'curses' module uses wattr_set() internally if you pass 'attr', + # overwriting the background style, so setting 'attr' to 0 in the first + # case won't do the right thing + if attr is None: + win.addnstr(y, x, s, maxlen) + else: + win.addnstr(y, x, s, maxlen, attr) + except curses.error: + pass + + +def _safe_addch(win, *args): + try: + win.addch(*args) + except curses.error: + pass + + +def _safe_hline(win, *args): + try: + win.hline(*args) + except curses.error: + pass + + +def _safe_vline(win, *args): + try: + win.vline(*args) + except curses.error: + pass + + +def _safe_move(win, *args): + try: + win.move(*args) + except curses.error: + pass + + +def _change_c_lc_ctype_to_utf8(): + # See _CHANGE_C_LC_CTYPE_TO_UTF8 + + if _IS_WINDOWS: + # Windows rarely has issues here, and the PEP 538 implementation avoids + # changing the locale on it. None of the UTF-8 locales below were + # supported from some quick testing either. Play it safe. + return + + def try_set_locale(loc): + try: + locale.setlocale(locale.LC_CTYPE, loc) + return True + except locale.Error: + return False + + # Is LC_CTYPE set to the C locale? + if locale.setlocale(locale.LC_CTYPE) == "C": + # This list was taken from the PEP 538 implementation in the CPython + # code, in Python/pylifecycle.c + for loc in "C.UTF-8", "C.utf8", "UTF-8": + if try_set_locale(loc): + # LC_CTYPE successfully changed + return + + +# Are we running on Windows? +_IS_WINDOWS = os.name == "nt" + +if __name__ == "__main__": + _main() diff --git a/support/kconfiglib/oldconfig.py b/support/kconfiglib/oldconfig.py new file mode 100755 index 0000000..042ab44 --- /dev/null +++ b/support/kconfiglib/oldconfig.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Implements oldconfig functionality. + + 1. Loads existing .config + 2. Prompts for the value of all modifiable symbols/choices that + aren't already set in the .config + 3. Writes an updated .config + +The default input/output filename is '.config'. A different filename can be +passed in the KCONFIG_CONFIG environment variable. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). + +Entering '?' displays the help text of the symbol/choice, if any. + +Unlike 'make oldconfig', this script doesn't print menu titles and comments, +but gives Kconfig definition locations. Printing menus and comments would be +pretty easy to add: Look at the parents of each item, and print all menu +prompts and comments unless they have already been printed (assuming you want +to skip "irrelevant" menus). +""" +from __future__ import print_function + +import sys + +from kconfiglib import Symbol, Choice, BOOL, TRISTATE, HEX, standard_kconfig + + +# Python 2/3 compatibility hack +if sys.version_info[0] < 3: + input = raw_input + + +def _main(): + # Earlier symbols in Kconfig files might depend on later symbols and become + # visible if their values change. This flag is set to True if the value of + # any symbol changes, in which case we rerun the oldconfig to check for new + # visible symbols. + global conf_changed + + kconf = standard_kconfig() + print(kconf.load_config()) + + while True: + conf_changed = False + + for node in kconf.node_iter(): + oldconfig(node) + + if not conf_changed: + break + + print(kconf.write_config()) + + +def oldconfig(node): + """ + Prompts the user for a value if node.item is a visible symbol/choice with + no user value. + """ + # See main() + global conf_changed + + # Only symbols and choices can be configured + if not isinstance(node.item, (Symbol, Choice)): + return + + # Skip symbols and choices that aren't visible + if not node.item.visibility: + return + + # Skip symbols and choices that don't have a prompt (at this location) + if not node.prompt: + return + + if isinstance(node.item, Symbol): + sym = node.item + + # Skip symbols that already have a user value + if sym.user_value is not None: + return + + # Skip symbols that can only have a single value, due to selects + if len(sym.assignable) == 1: + return + + # Skip symbols in choices in y mode. We ask once for the entire choice + # instead. + if sym.choice and sym.choice.tri_value == 2: + return + + # Loop until the user enters a valid value or enters a blank string + # (for the default value) + while True: + val = input("{} ({}) [{}] ".format( + node.prompt[0], _name_and_loc_str(sym), + _default_value_str(sym))) + + if val == "?": + _print_help(node) + continue + + # Substitute a blank string with the default value the symbol + # would get + if not val: + val = sym.str_value + + # Automatically add a "0x" prefix for hex symbols, like the + # menuconfig interface does. This isn't done when loading .config + # files, hence why set_value() doesn't do it automatically. + if sym.type == HEX and not val.startswith(("0x", "0X")): + val = "0x" + val + + old_str_val = sym.str_value + + # Kconfiglib itself will print a warning here if the value + # is invalid, so we don't need to bother + if sym.set_value(val): + # Valid value input. We're done with this node. + + if sym.str_value != old_str_val: + conf_changed = True + + return + + else: + choice = node.item + + # Skip choices that already have a visible user selection... + if choice.user_selection and choice.user_selection.visibility == 2: + # ...unless there are new visible symbols in the choice. (We know + # they have y (2) visibility in that case, because m-visible + # symbols get demoted to n-visibility in y-mode choices, and the + # user-selected symbol had visibility y.) + for sym in choice.syms: + if sym is not choice.user_selection and sym.visibility and \ + sym.user_value is None: + # New visible symbols in the choice + break + else: + # No new visible symbols in the choice + return + + # Get a list of available selections. The mode of the choice limits + # the visibility of the choice value symbols, so this will indirectly + # skip choices in n and m mode. + options = [sym for sym in choice.syms if sym.visibility == 2] + + if not options: + # No y-visible choice value symbols + return + + # Loop until the user enters a valid selection or a blank string (for + # the default selection) + while True: + print("{} ({})".format(node.prompt[0], _name_and_loc_str(choice))) + + for i, sym in enumerate(options, 1): + print("{} {}. {} ({})".format( + ">" if sym is choice.selection else " ", + i, + # Assume people don't define choice symbols with multiple + # prompts. That generates a warning anyway. + sym.nodes[0].prompt[0], + sym.name)) + + sel_index = input("choice[1-{}]: ".format(len(options))) + + if sel_index == "?": + _print_help(node) + continue + + # Pick the default selection if the string is blank + if not sel_index: + choice.selection.set_value(2) + break + + try: + sel_index = int(sel_index) + except ValueError: + print("Bad index", file=sys.stderr) + continue + + if not 1 <= sel_index <= len(options): + print("Bad index", file=sys.stderr) + continue + + # Valid selection + + if options[sel_index - 1].tri_value != 2: + conf_changed = True + + options[sel_index - 1].set_value(2) + break + + # Give all of the non-selected visible choice symbols the user value n. + # This makes it so that the choice is no longer considered new once we + # do additional passes, if the reason that it was considered new was + # that it had new visible choice symbols. + # + # Only giving visible choice symbols the user value n means we will + # prompt for the choice again if later user selections make more new + # choice symbols visible, which is correct. + for sym in choice.syms: + if sym is not choice.user_selection and sym.visibility: + sym.set_value(0) + + +def _name_and_loc_str(sc): + # Helper for printing the name of the symbol/choice 'sc' along with the + # location(s) in the Kconfig files where it is defined. Unnamed choices + # return "choice" instead of the name. + + return "{}, defined at {}".format( + sc.name or "choice", + ", ".join("{}:{}".format(node.filename, node.linenr) + for node in sc.nodes)) + + +def _print_help(node): + print("\n" + (node.help or "No help text\n")) + + +def _default_value_str(sym): + # Returns the "m/M/y" string in e.g. + # + # TRISTATE_SYM prompt (TRISTATE_SYM, defined at Kconfig:9) [n/M/y]: + # + # For string/int/hex, returns the default value as-is. + + if sym.type in (BOOL, TRISTATE): + return "/".join(("NMY" if sym.tri_value == tri else "nmy")[tri] + for tri in sym.assignable) + + # string/int/hex + return sym.str_value + + +if __name__ == "__main__": + _main() diff --git a/support/kconfiglib/olddefconfig.py b/support/kconfiglib/olddefconfig.py new file mode 100755 index 0000000..a59a7d4 --- /dev/null +++ b/support/kconfiglib/olddefconfig.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +# Copyright (c) 2018-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Updates an old .config file or creates a new one, by filling in default values +for all new symbols. This is the same as picking the default selection for all +symbols in oldconfig, or entering the menuconfig interface and immediately +saving. + +The default input/output filename is '.config'. A different filename can be +passed in the KCONFIG_CONFIG environment variable. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). +""" +import kconfiglib + + +def main(): + kconf = kconfiglib.standard_kconfig() + print(kconf.load_config()) + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/savedefconfig.py b/support/kconfiglib/savedefconfig.py new file mode 100755 index 0000000..c388f1a --- /dev/null +++ b/support/kconfiglib/savedefconfig.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Saves a minimal configuration file that only lists symbols that differ in value +from their defaults. Loading such a configuration file is equivalent to loading +the "full" configuration file. + +Minimal configuration files are handy to start from when editing configuration +files by hand. + +The default input configuration file is '.config'. A different input filename +can be passed in the KCONFIG_CONFIG environment variable. + +Note: Minimal configurations can also be generated from within the menuconfig +interface. +""" +import argparse + +import kconfiglib + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--kconfig", + default="Kconfig", + help="Base Kconfig file (default: Kconfig)") + + parser.add_argument( + "--out", + metavar="MINIMAL_CONFIGURATION", + default="defconfig", + help="Output filename for minimal configuration (default: defconfig)") + + args = parser.parse_args() + + kconf = kconfiglib.Kconfig(args.kconfig) + print(kconf.load_config()) + print(kconf.write_min_config(args.out)) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/setconfig.py b/support/kconfiglib/setconfig.py new file mode 100755 index 0000000..b689754 --- /dev/null +++ b/support/kconfiglib/setconfig.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python + +# Copyright (c) 2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +""" +Simple utility for setting configuration values from the command line. + +Sample usage: + + $ setconfig FOO_SUPPORT=y BAR_BITS=8 + +Note: Symbol names should not be prefixed with 'CONFIG_'. + +The exit status on errors is 1. + +The default input/output configuration file is '.config'. A different filename +can be passed in the KCONFIG_CONFIG environment variable. + +When overwriting a configuration file, the old version is saved to +<filename>.old (e.g. .config.old). +""" +import argparse +import sys + +import kconfiglib + + +def main(): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=__doc__) + + parser.add_argument( + "--kconfig", + default="Kconfig", + help="Base Kconfig file (default: Kconfig)") + + parser.add_argument( + "--no-check-exists", + dest="check_exists", + action="store_false", + help="Ignore assignments to non-existent symbols instead of erroring " + "out") + + parser.add_argument( + "--no-check-value", + dest="check_value", + action="store_false", + help="Ignore assignments that didn't \"take\" (where the symbol got a " + "different value, e.g. due to unsatisfied dependencies) instead " + "of erroring out") + + parser.add_argument( + "assignments", + metavar="ASSIGNMENT", + nargs="*", + help="A 'NAME=value' assignment") + + args = parser.parse_args() + + kconf = kconfiglib.Kconfig(args.kconfig) + print(kconf.load_config()) + + for arg in args.assignments: + if "=" not in arg: + sys.exit("error: no '=' in assignment: '{}'".format(arg)) + name, value = arg.split("=", 1) + + if name not in kconf.syms: + if not args.check_exists: + continue + sys.exit("error: no symbol '{}' in configuration".format(name)) + + sym = kconf.syms[name] + + if not sym.set_value(value): + sys.exit("error: '{}' is an invalid value for the {} symbol {}" + .format(value, kconfiglib.TYPE_TO_STR[sym.orig_type], + name)) + + if args.check_value and sym.str_value != value: + sys.exit("error: {} was assigned the value '{}', but got the " + "value '{}'. Check the symbol's dependencies, and make " + "sure that it has a prompt." + .format(name, value, sym.str_value)) + + print(kconf.write_config()) + + +if __name__ == "__main__": + main() diff --git a/support/kconfiglib/setup.cfg b/support/kconfiglib/setup.cfg new file mode 100644 index 0000000..185845e --- /dev/null +++ b/support/kconfiglib/setup.cfg @@ -0,0 +1,7 @@ +[bdist_wheel] +# We support both Python 2 and Python 3 +universal = 1 + +[metadata] +# Include the license file in wheels +license_file = LICENSE.txt diff --git a/support/kconfiglib/setup.py b/support/kconfiglib/setup.py new file mode 100644 index 0000000..9a3947e --- /dev/null +++ b/support/kconfiglib/setup.py @@ -0,0 +1,100 @@ +import io +import os + +import setuptools + + +setuptools.setup( + name="kconfiglib", + # MAJOR.MINOR.PATCH, per http://semver.org + version="12.13.0", + description="A flexible Python Kconfig implementation", + + # Make sure that README.rst decodes on Python 3 in environments that use + # the C locale (which implies ASCII), by explicitly giving the encoding. + # + # io.open() has the 'encoding' parameter on both Python 2 and 3. open() + # doesn't have it on Python 2. This lets us use the same code for both. + long_description=io.open( + os.path.join(os.path.dirname(__file__), "README.rst"), + encoding="utf-8" + ).read(), + + url="https://github.com/ulfalizer/Kconfiglib", + author='Ulf "Ulfalizer" Magnusson', + author_email="ulfalizer@xxxxxxxxx", + keywords="kconfig, kbuild, menuconfig, configuration-management", + license="ISC", + + py_modules=( + "kconfiglib", + "menuconfig", + "guiconfig", + "genconfig", + "oldconfig", + "olddefconfig", + "savedefconfig", + "defconfig", + "alldefconfig", + "allnoconfig", + "allmodconfig", + "allyesconfig", + "listnewconfig", + "setconfig", + ), + + entry_points={ + "console_scripts": ( + "menuconfig = menuconfig:_main", + "guiconfig = guiconfig:_main", + "genconfig = genconfig:main", + "oldconfig = oldconfig:_main", + "olddefconfig = olddefconfig:main", + "savedefconfig = savedefconfig:main", + "defconfig = defconfig:main", + "alldefconfig = alldefconfig:main", + "allnoconfig = allnoconfig:main", + "allmodconfig = allmodconfig:main", + "allyesconfig = allyesconfig:main", + "listnewconfig = listnewconfig:main", + "setconfig = setconfig:main", + ) + }, + + # The terminal menuconfig implementation uses the standard Python 'curses' + # module. The windows-curses package makes it available on Windows. See + # https://github.com/zephyrproject-rtos/windows-curses. + install_requires=( + 'windows-curses; sys_platform == "win32"', + ), + + # Needs support for unnumbered {} in format() and argparse + python_requires=">=2.7,!=3.0.*,!=3.1.*", + + project_urls={ + "GitHub repository": "https://github.com/ulfalizer/Kconfiglib", + "Examples": "https://github.com/ulfalizer/Kconfiglib/tree/master/examples", + }, + + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + "Topic :: System :: Operating System Kernels :: Linux", + "License :: OSI Approved :: ISC License (ISCL)", + "Operating System :: POSIX", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + ] +) diff --git a/support/kconfiglib/tests/Kappend b/support/kconfiglib/tests/Kappend new file mode 100644 index 0000000..ce1478f --- /dev/null +++ b/support/kconfiglib/tests/Kappend @@ -0,0 +1,12 @@ +config MODULES + def_bool y + +config BOOL + bool "bool 1" + +config STRING + string "string" + +config IGNOREME + bool "ignore me" + default y diff --git a/support/kconfiglib/tests/Kassignable b/support/kconfiglib/tests/Kassignable new file mode 100644 index 0000000..26b4177 --- /dev/null +++ b/support/kconfiglib/tests/Kassignable @@ -0,0 +1,230 @@ +config MODULES + bool "modules" + option modules + + +# Things that should never be .assignable + +if UNDEFINED && "const" +endif + +config NO_PROMPT + bool + +config STRING + string "string" + +config INT + int "int" + +config HEX + hex "hex" + + +# Non-selected symbols + +config Y_VIS_BOOL + bool "y-vis bool" + +config M_VIS_BOOL + bool "m-vis bool" if m + +config N_VIS_BOOL + bool "n-vis bool" if n + +config Y_VIS_TRI + tristate "y-vis tri" + +config M_VIS_TRI + tristate "m-vis tri" if m + +config N_VIS_TRI + tristate "n-vis tri" if n + + +# Symbols selected to y + +config Y_SELECTOR + def_tristate y + + select Y_SEL_Y_VIS_BOOL + select Y_SEL_M_VIS_BOOL + select Y_SEL_N_VIS_BOOL + + select Y_SEL_Y_VIS_TRI + select Y_SEL_M_VIS_TRI + select Y_SEL_N_VIS_TRI + +config Y_SEL_Y_VIS_BOOL + bool "y-sel y-vis bool" + +config Y_SEL_M_VIS_BOOL + bool "y-sel m-vis bool" if m + +config Y_SEL_N_VIS_BOOL + bool "y-sel n-vis bool" if n + +config Y_SEL_Y_VIS_TRI + tristate "y-sel y-vis tri" + +config Y_SEL_M_VIS_TRI + tristate "y-sel m-vis tri" if m + +config Y_SEL_N_VIS_TRI + tristate "y-sel n-vis tri" if n + + +# Symbols selected to m + +config M_SELECTOR + def_tristate m + + select M_SEL_Y_VIS_BOOL + select M_SEL_M_VIS_BOOL + select M_SEL_N_VIS_BOOL + + select M_SEL_Y_VIS_TRI + select M_SEL_M_VIS_TRI + select M_SEL_N_VIS_TRI + +config M_SEL_Y_VIS_BOOL + bool "m-sel y-vis bool" + +config M_SEL_M_VIS_BOOL + bool "m-sel m-vis bool" if m + +config M_SEL_N_VIS_BOOL + bool "m-sel n-vis bool" if n + +config M_SEL_Y_VIS_TRI + tristate "m-sel y-vis tri" + +config M_SEL_M_VIS_TRI + tristate "m-sel m-vis tri" if m + +config M_SEL_N_VIS_TRI + tristate "m-sel n-vis tri" if n + + +# Symbols implied to y + +config Y_IMPLIER + def_tristate y + + imply Y_IMP_Y_VIS_BOOL + imply Y_IMP_M_VIS_BOOL + imply Y_IMP_N_VIS_BOOL + + imply Y_IMP_Y_VIS_TRI + imply Y_IMP_M_VIS_TRI + imply Y_IMP_N_VIS_TRI + +config Y_IMP_Y_VIS_BOOL + bool "y-imp y-vis bool" + +config Y_IMP_M_VIS_BOOL + bool "y-imp m-vis bool" if m + +config Y_IMP_N_VIS_BOOL + bool "y-imp n-vis bool" if n + +config Y_IMP_Y_VIS_TRI + tristate "y-imp y-vis tri" + +config Y_IMP_M_VIS_TRI + tristate "y-imp m-vis tri" if m + +config Y_IMP_N_VIS_TRI + tristate "y-imp n-vis tri" if n + + +# Symbols implied to m (never affects assignable values) + +config M_IMPLIER + def_tristate m + + imply M_IMP_Y_VIS_BOOL + imply M_IMP_M_VIS_BOOL + imply M_IMP_N_VIS_BOOL + + imply M_IMP_Y_VIS_TRI + imply M_IMP_M_VIS_TRI + imply M_IMP_N_VIS_TRI + +config M_IMP_Y_VIS_BOOL + bool "m-imp y-vis bool" + +config M_IMP_M_VIS_BOOL + bool "m-imp m-vis bool" if m + +config M_IMP_N_VIS_BOOL + bool "m-imp n-vis bool" if n + +config M_IMP_Y_VIS_TRI + tristate "m-imp y-vis tri" + +config M_IMP_M_VIS_TRI + tristate "m-imp m-vis tri" if m + +config M_IMP_N_VIS_TRI + tristate "m-imp n-vis tri" if n + + +# Symbols in y-mode choice + +choice Y_CHOICE + bool "y-mode choice" + +config Y_CHOICE_BOOL + bool "y-mode choice bool" + +config Y_CHOICE_TRISTATE + tristate "y-mode choice tristate" + +config Y_CHOICE_N_VIS_TRISTATE + tristate "y-mode choice tristate invisible" if n + +endchoice + + +# Symbols in m/y-mode choice + +choice MY_CHOICE + tristate "m/y-mode choice" + +config MY_CHOICE_BOOL + bool "m/y-mode choice bool" + +config MY_CHOICE_TRISTATE + tristate "m/y-mode choice tristate" + +config MY_CHOICE_N_VIS_TRISTATE + tristate "m/y-mode choice tristate invisible" if n + +endchoice + + +# Choices with some other possible modes + +choice NMY_CHOICE + tristate "n/m/y-mode choice" + optional +endchoice + +choice NY_CHOICE + bool "n/y-mode choice" + optional +endchoice + +choice NM_CHOICE + tristate "n/m-mode choice" if m + optional +endchoice + +choice M_CHOICE + tristate "m-mode choice" if m +endchoice + +choice N_CHOICE + tristate "n-mode choice" if n +endchoice diff --git a/support/kconfiglib/tests/Kchoice b/support/kconfiglib/tests/Kchoice new file mode 100644 index 0000000..16b38d4 --- /dev/null +++ b/support/kconfiglib/tests/Kchoice @@ -0,0 +1,198 @@ +config MODULES + bool "modules" + +# bool/tristate and optional + +choice BOOL + bool "bool" +config B_1 + tristate "B_1" +config B_2 + tristate "B_2" +endchoice + +choice BOOL_OPT + bool "bool optional" + optional +config BO_1 + tristate "BO_1" +config BO_2 + tristate "BO_2" +endchoice + +choice TRISTATE + tristate "tristate" +config T_1 + tristate "T_1" +config T_2 + tristate "T_2" +endchoice + +choice TRISTATE_OPT + tristate "tristate optional" + optional +config TO_1 + tristate "TO_1" +config TO_2 + tristate "TO_2" +endchoice + +# m-visibility + +choice BOOL_M + bool "bool m" if m +config BM_1 + tristate "BM_1" +config BM_2 + tristate "BM_2" +endchoice + +choice TRISTATE_M + tristate "tristate m" if m +config TM_1 + tristate "TM_1" +config TM_2 + tristate "TM_2" +endchoice + +# Defaults + +config TRISTATE_SYM + tristate "tristate" + +choice DEFAULTS + bool "defaults" + default OPT_1 if n + default OPT_2 if TRISTATE_SYM + default OPT_4 +config OPT_1 + tristate "OPT_1" +config OPT_2 + tristate "OPT_2" +config OPT_3 + tristate "OPT_3" +config OPT_4 + tristate "OPT_4" +endchoice + +choice DEFAULTS_NOT_VISIBLE + bool "defaults not visible" + # Skipped due to condition + default OPT_6 if n + # Skipped because OPT_7 is not visible + default OPT_7 + # This one should apply + default OPT_8 +config OPT_5 + tristate "OPT_5" +config OPT_6 + tristate "OPT_6" +config OPT_7 + tristate "OPT_7" if n +config OPT_8 + tristate "OPT_8" +config OPT_9 + tristate "OPT_9" +endchoice + +# Choices without an explicitly specified type should get the type of the first +# symbol with a type + +choice NO_TYPE_BOOL + prompt "no type bool" +config NTB_1 + bool "NTB_1" +config NTB_2 + tristate "NTB_2" +endchoice + +choice NO_TYPE_TRISTATE + prompt "no type tristate" +config NTT_1 +config NTT_2 + tristate "NTB_2" +config NTT_3 + bool "NTT_3" +endchoice + +# Choice items without an explicitly specified type should get the type of the +# choice + +choice MISSING_MEMBER_TYPES_1 + bool "missing member types" +config MMT_1 +config MMT_2 +config MMT_3 + tristate +endchoice + +choice MISSING_MEMBER_TYPES_2 +config MMT_4 +config MMT_5 + bool +endchoice + +# Choice where the default selection (the first symbol) depends on another +# symbol. If that symbol becomes 'n', the default selection should change to +# the first visible symbol in the choice. + +choice DEFAULT_WITH_DEP + bool "default with dep" + +config A + bool "A" + depends on DEP + +config B + bool "B" + +endchoice + +config DEP + bool "dep" + +# Choice with symbols that shouldn't be considered choice symbols because they +# depend on the preceding symbol. This might be a kconfig bug, but some things +# use it, so we need to emulate it. + +choice WEIRD_SYMS + bool "weird symbols that aren't considered part of the choice" + +# Only WS1 is part of the choice +config WS1 + bool "WS1" + +config WS2 + bool "WS2" + depends on WS1 + +config WS3 + bool + depends on WS2 + +config WS4 + bool + depends on WS1 + +config WS5 + bool "WS5" if WS1 + +# 'if' has the same effect, so only WS6 is part of the choice +config WS6 + bool "WS6" + +if WS6 + +config WS7 + bool + +config WS8 + bool "WS8" + +endif + +# Should also be part of the choice +config WS9 + bool "WS9" + +endchoice diff --git a/support/kconfiglib/tests/Kdefconfig_existent b/support/kconfiglib/tests/Kdefconfig_existent new file mode 100644 index 0000000..304cae6 --- /dev/null +++ b/support/kconfiglib/tests/Kdefconfig_existent @@ -0,0 +1,8 @@ +# $FOO is "defconfig_2" + +config A + string + option defconfig_list + default "Kconfiglib/tests/defconfig_1" if y && !n && n + default "Kconfiglib/tests/$FOO" + default "Kconfiglib/tests/defconfig_1" diff --git a/support/kconfiglib/tests/Kdefconfig_existent_but_n b/support/kconfiglib/tests/Kdefconfig_existent_but_n new file mode 100644 index 0000000..2fdaaa9 --- /dev/null +++ b/support/kconfiglib/tests/Kdefconfig_existent_but_n @@ -0,0 +1,10 @@ +# $FOO is "defconfig_2" +# Should produce None due to the "depends on n" + +config A + string + depends on n + option defconfig_list + default "Kconfiglib/tests/defconfig_1" if y && !n && n + default "Kconfiglib/tests/$FOO" + default "Kconfiglib/tests/defconfig_1" diff --git a/support/kconfiglib/tests/Kdefconfig_nonexistent b/support/kconfiglib/tests/Kdefconfig_nonexistent new file mode 100644 index 0000000..5b7230f --- /dev/null +++ b/support/kconfiglib/tests/Kdefconfig_nonexistent @@ -0,0 +1,5 @@ +config A + string + option defconfig_list + default "Kconfiglib/tests/non_existent_1" + default "Kconfiglib/tests/non_existent_2" diff --git a/support/kconfiglib/tests/Kdefconfig_srctree b/support/kconfiglib/tests/Kdefconfig_srctree new file mode 100644 index 0000000..d2591fa --- /dev/null +++ b/support/kconfiglib/tests/Kdefconfig_srctree @@ -0,0 +1,5 @@ +config A + string + option defconfig_list + default "sub/defconfig_in_sub" # Assume this doesn't exist + default "Kconfiglib/tests/defconfig_2" diff --git a/support/kconfiglib/tests/Kdepcopy b/support/kconfiglib/tests/Kdepcopy new file mode 100644 index 0000000..6abe898 --- /dev/null +++ b/support/kconfiglib/tests/Kdepcopy @@ -0,0 +1,173 @@ +# We verify that the properties below end up in definition order + +config MULTIDEF + bool + default A + default B + select AA + imply AA + +if FOO + +config MULTIDEF + default C + default D + select BB + imply BB + +if BAR + +config MULTIDEF + default E + default F + select CC + imply CC + +menu "menu" + +config MULTIDEF + default G + default H + select DD + imply DD + +config MULTIDEF + default I + default J + select EE + imply EE + +endmenu + +config MULTIDEF + default K + default L + select FF + imply FF + +config MULTIDEF + default M + default N + select GG + imply GG + +endif + +config MULTIDEF + default O + default P + select HH + select II + imply HH + imply II + +endif + +config MULTIDEF + default Q + default R + select JJ + imply JJ + + +# Same test with choice symbols involved + +config MULTIDEF_CHOICE + bool + select A + +choice + bool "choice" + +config MULTIDEF_CHOICE + bool "multidef choice" + select B + +endchoice + +config MULTIDEF_CHOICE + bool + select C + + +# Same test with ranges involved + +config MULTIDEF_RANGE + int + range A _ + +menu "menu" + +config MULTIDEF_RANGE + int + range B _ + +if FOO + +config MULTIDEF_RANGE + int + range C _ + +endif + +config MULTIDEF_RANGE + int + range D _ + +endmenu + +config MULTIDEF_RANGE + int + range E _ + +config MULTIDEF_RANGE + int + range F _ + + +# Same test for a choice + +choice MULTICHOICE + bool "choice" + default A + +config A + bool "A" + +config B + bool "B" + +config C + bool "C" + +config D + bool "C" + +config E + bool "C" + +endchoice + +if FOO + +choice MULTICHOICE + default B +endchoice + +menu "menu" + +choice MULTICHOICE + default C +endchoice + +endmenu + +choice MULTICHOICE + default D +endchoice + +endif + +choice MULTICHOICE + default E +endchoice diff --git a/support/kconfiglib/tests/Kdeploop0 b/support/kconfiglib/tests/Kdeploop0 new file mode 100644 index 0000000..98d3e3c --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop0 @@ -0,0 +1,3 @@ +config FOO + bool + depends on FOO diff --git a/support/kconfiglib/tests/Kdeploop1 b/support/kconfiglib/tests/Kdeploop1 new file mode 100644 index 0000000..134cd29 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop1 @@ -0,0 +1,3 @@ +config FOO + bool + select FOO diff --git a/support/kconfiglib/tests/Kdeploop10 b/support/kconfiglib/tests/Kdeploop10 new file mode 100644 index 0000000..2e616ae --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop10 @@ -0,0 +1,48 @@ +config A + bool + depends on B + +config B + bool + depends on C = 7 + +config C + int + range D 8 + +config D + int + default 3 if E + default 8 + +config E + bool + +config F + bool + select E if G + +config G + bool + depends on H + +choice + bool "choice" + +config H + bool "H" + depends on I + +endchoice + +choice + bool "choice" if J + +config I + bool "I" + +endchoice + +config J + bool + depends on A diff --git a/support/kconfiglib/tests/Kdeploop2 b/support/kconfiglib/tests/Kdeploop2 new file mode 100644 index 0000000..c997243 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop2 @@ -0,0 +1,3 @@ +config FOO + bool + default FOO diff --git a/support/kconfiglib/tests/Kdeploop3 b/support/kconfiglib/tests/Kdeploop3 new file mode 100644 index 0000000..90c83d5 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop3 @@ -0,0 +1,3 @@ +config FOO + bool + default y if FOO diff --git a/support/kconfiglib/tests/Kdeploop4 b/support/kconfiglib/tests/Kdeploop4 new file mode 100644 index 0000000..789d8b7 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop4 @@ -0,0 +1,7 @@ +config FOO + bool + depends on BAR + +config BAR + bool + depends on FOO diff --git a/support/kconfiglib/tests/Kdeploop5 b/support/kconfiglib/tests/Kdeploop5 new file mode 100644 index 0000000..f12fe6b --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop5 @@ -0,0 +1,7 @@ +config FOO + bool + select BAR + +config BAR + bool + select FOO diff --git a/support/kconfiglib/tests/Kdeploop6 b/support/kconfiglib/tests/Kdeploop6 new file mode 100644 index 0000000..cb1e701 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop6 @@ -0,0 +1,6 @@ +config FOO + bool + +config BAR + bool + select FOO if FOO diff --git a/support/kconfiglib/tests/Kdeploop7 b/support/kconfiglib/tests/Kdeploop7 new file mode 100644 index 0000000..63d2c57 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop7 @@ -0,0 +1,11 @@ +choice + bool "choice" + +config FOO + bool "foo" + depends on BAR + +config BAR + bool "bar" + +endchoice diff --git a/support/kconfiglib/tests/Kdeploop8 b/support/kconfiglib/tests/Kdeploop8 new file mode 100644 index 0000000..84efd8d --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop8 @@ -0,0 +1,8 @@ +choice + bool "choice" + default FOO if FOO + +config FOO + bool "foo" + +endchoice diff --git a/support/kconfiglib/tests/Kdeploop9 b/support/kconfiglib/tests/Kdeploop9 new file mode 100644 index 0000000..939f7f4 --- /dev/null +++ b/support/kconfiglib/tests/Kdeploop9 @@ -0,0 +1,7 @@ +choice + bool "choice" if FOO + +config FOO + bool "foo" + +endchoice diff --git a/support/kconfiglib/tests/Kdirdep b/support/kconfiglib/tests/Kdirdep new file mode 100644 index 0000000..cbb88b9 --- /dev/null +++ b/support/kconfiglib/tests/Kdirdep @@ -0,0 +1,30 @@ +config NO_DEP_SYM + bool + +config DEP_SYM + bool + depends on A + +config DEP_SYM + depends on B && C + +config DEP_SYM + depends on !D + + +choice NO_DEP_CHOICE + bool "no dep. choice" +endchoice + +choice DEP_CHOICE + bool "dep. choice" + depends on A +endchoice + +choice DEP_CHOICE + depends on B +endchoice + +choice DEP_CHOICE + depends on C +endchoice diff --git a/support/kconfiglib/tests/Kescape b/support/kconfiglib/tests/Kescape new file mode 100644 index 0000000..ee6b2b6 --- /dev/null +++ b/support/kconfiglib/tests/Kescape @@ -0,0 +1,3 @@ +config STRING + string "string" + default "\"\\" diff --git a/support/kconfiglib/tests/Keval b/support/kconfiglib/tests/Keval new file mode 100644 index 0000000..1e64b83 --- /dev/null +++ b/support/kconfiglib/tests/Keval @@ -0,0 +1,34 @@ +# Enabled/disabled in the test +config MODULES + bool "modules" + option modules + +config N + def_tristate n + +config M + def_tristate m + +menuconfig Y + def_tristate y + prompt "foo" + +config Y_STRING + string + default "y" + +config FOO_BAR_STRING + string + default "foo bar" + +config INT_37 + int + default 37 + +config HEX_0X37 + hex + default 0x37 + +config HEX_37 + hex + default 37 diff --git a/support/kconfiglib/tests/Kexpr_items b/support/kconfiglib/tests/Kexpr_items new file mode 100644 index 0000000..fb97ace --- /dev/null +++ b/support/kconfiglib/tests/Kexpr_items @@ -0,0 +1,11 @@ +config TEST + bool + default A && (B || !C && D = "E") || F > G || !!!H + +choice CHOICE + bool "choice" + +config TEST_CHOICE + bool "test choice" if A + +endchoice diff --git a/support/kconfiglib/tests/Khelp b/support/kconfiglib/tests/Khelp new file mode 100644 index 0000000..b80c2eb --- /dev/null +++ b/support/kconfiglib/tests/Khelp @@ -0,0 +1,50 @@ +config TWO_HELP_STRINGS + bool + help + first help string + + + + +config TWO_HELP_STRINGS + help + second help string + +config NO_BLANK_AFTER_HELP + bool + help + help for + NO_BLANK_AFTER_HELP +choice CHOICE_HELP + bool "choice with help" + help + help for + CHOICE_HELP +endchoice + +config HELP_TERMINATED_BY_COMMENT + bool + help + a + b + c +# + +config TRICKY_HELP + bool + -help--- + + + a + b + c + + d + e + f + + + g + h + i +# diff --git a/support/kconfiglib/tests/Kifremoval b/support/kconfiglib/tests/Kifremoval new file mode 100644 index 0000000..f94472c --- /dev/null +++ b/support/kconfiglib/tests/Kifremoval @@ -0,0 +1,99 @@ +# Test some tricky cases that give consecutive 'if' nodes even after +# flattening. Simple cases are exercised a ton elsewhere. + +if X +endif +if X +endif + +config A + +if X +endif +if X +endif + +config B + +if X +endif +if X +endif +if X +endif + +config C + +if X + if X + if X + endif + if X + endif + endif + if X + if X + endif + if X + endif + endif + config D +endif +if X +endif + +menu "E" + if X + if X + endif + endif + if X + if X + endif + endif +endmenu + +menu "F" + if X + endif + if X + endif + if X + if X + endif + if X + endif + menu "G" + endmenu + endif +endmenu + +choice H + if X + if X + endif + endif + if X + if X + endif + endif +endchoice + +choice I + if X + endif + if X + endif + if X + if X + endif + if X + endif + config J + endif +endchoice + +if X +endif +if X +endif diff --git a/support/kconfiglib/tests/Kimply b/support/kconfiglib/tests/Kimply new file mode 100644 index 0000000..3ce346f --- /dev/null +++ b/support/kconfiglib/tests/Kimply @@ -0,0 +1,145 @@ +config MODULES + def_bool y + option modules + +# +# Implied symbols with unmet and met direct dependencies +# + +config IMPLY_DIRECT_DEPS + def_tristate y + imply UNMET_DIRECT_1 + imply UNMET_DIRECT_2 + imply UNMET_DIRECT_3 + imply MET_DIRECT_1 + imply MET_DIRECT_2 + imply MET_DIRECT_3 + imply MET_DIRECT_4 + +config UNMET_DIRECT_1 + tristate + depends on n + +if n +config UNMET_DIRECT_2 + tristate +endif + +menu "menu" + depends on n + +config UNMET_DIRECT_3 + tristate + +endmenu + +config MET_DIRECT_1 + tristate + +config MET_DIRECT_2 + depends on y + tristate + +if y +config MET_DIRECT_3 + tristate +endif + +menu "menu" + depends on y + +config MET_DIRECT_4 + tristate + +endmenu + +# +# 'imply' with condition +# + +config IMPLY_COND + def_tristate y + tristate + imply IMPLIED_N_COND if n + imply IMPLIED_M_COND if m + imply IMPLIED_Y_COND if y + +config IMPLIED_N_COND + tristate + +config IMPLIED_M_COND + tristate + +config IMPLIED_Y_COND + tristate + +# +# Implying from symbol with value n +# + +# Will default to 'n' +config IMPLY_N_1 + tristate + imply IMPLIED_FROM_N_1 + +# This test also disables the imply, so it's kinda redundant, but why not +if n +config IMPLY_N_2 + tristate + imply IMPLIED_FROM_N_2 +endif + +config IMPLIED_FROM_N_1 + tristate + +config IMPLIED_FROM_N_2 + tristate + +# +# Implying from symbol with value m +# + +config IMPLY_M + def_tristate m + imply IMPLIED_M + # Implying a bool to 'm' makes it default to 'y' + imply IMPLIED_M_BOOL + +config IMPLIED_M + tristate + +config IMPLIED_M_BOOL + bool + +# +# 'imply' which should raise an 'm' default to 'y' +# + +config IMPLY_M_TO_Y + tristate + default y + imply IMPLIED_M_TO_Y + +config IMPLIED_M_TO_Y + tristate + default m + +# +# Used for testing user values +# + +config DIRECT_DEP + tristate "direct dep" + +config IMPLY + tristate "imply" + imply IMPLIED_TRISTATE + imply IMPLIED_BOOL + +config IMPLIED_TRISTATE + tristate "implied tristate" + depends on DIRECT_DEP + +config IMPLIED_BOOL + bool "implied bool" + depends on DIRECT_DEP diff --git a/support/kconfiglib/tests/Kinclude_path b/support/kconfiglib/tests/Kinclude_path new file mode 100644 index 0000000..7a3badb --- /dev/null +++ b/support/kconfiglib/tests/Kinclude_path @@ -0,0 +1,12 @@ +config TOP + bool + +source "Kinclude_path_sourced_1" + +config TOP + bool + +source "Kinclude_path_sourced_1" + +config TOP + bool diff --git a/support/kconfiglib/tests/Kinclude_path_sourced_1 b/support/kconfiglib/tests/Kinclude_path_sourced_1 new file mode 100644 index 0000000..f4dee98 --- /dev/null +++ b/support/kconfiglib/tests/Kinclude_path_sourced_1 @@ -0,0 +1,12 @@ +config ONE_DOWN + bool + +source "Kinclude_path_sourced_2" + +config ONE_DOWN + bool + +source "Kinclude_path_sourced_2" + +config ONE_DOWN + bool diff --git a/support/kconfiglib/tests/Kinclude_path_sourced_2 b/support/kconfiglib/tests/Kinclude_path_sourced_2 new file mode 100644 index 0000000..068f18d --- /dev/null +++ b/support/kconfiglib/tests/Kinclude_path_sourced_2 @@ -0,0 +1,11 @@ +config TWO_DOWN + bool + +menu "menu" +endmenu + +comment "comment" + +choice + bool "choice" +endchoice diff --git a/support/kconfiglib/tests/Kitemlists b/support/kconfiglib/tests/Kitemlists new file mode 100644 index 0000000..8aa7107 --- /dev/null +++ b/support/kconfiglib/tests/Kitemlists @@ -0,0 +1,36 @@ +comment "comment 1" + +choice + bool "choice 1" +endchoice + +menu "menu 1" + +choice NAMED + bool "choice 2" +endchoice + +menu "menu 2" +menu "menu 3" +comment "comment 2" +endmenu + +choice + bool "choice 3" +endchoice + +choice NAMED + bool +endchoice + +endmenu + +menu "menu 4" +endmenu + +comment "comment 3" + +endmenu + +menu "menu 5" +endmenu diff --git a/support/kconfiglib/tests/Klocation b/support/kconfiglib/tests/Klocation new file mode 100644 index 0000000..f997055 --- /dev/null +++ b/support/kconfiglib/tests/Klocation @@ -0,0 +1,72 @@ +if UNDEFINED +endif + +config SINGLE_DEF + bool + +config MULTI_DEF + bool + +# Throw in some line continuations too to make sure it doesn't mess up the line +# numbers +if y && \ + y +if y && \ + y && \ + y + +# Throw in some help texts too + +config HELP_1 + bool "help 1" + help +config HELP_2 + bool "help 2" + help + foo + bar + + baz + +config HELP_3 + help + foo + bar + bool +config \ +MULTI_DEF + +config MULTI_DEF + +endif +endif + +# Expands to "tests/Klocation_sourced" +source "$TESTS_DIR_FROM_ENV/Klocation$_SOURCED" + +# Expands to "sub/Klocation_rsourced" +rsource "$SUB_DIR_FROM_ENV/Klocation$_RSOURCED" + +# Expands to "tests/*ub/Klocation_gsourced[12]", matching +# tests/sub/Klocation_gsourced{1,2} +source "$TESTS_DIR_FROM_ENV/*ub/Klocation$_GSOURCED[12]" +# Test old syntax too +gsource "$TESTS_DIR_FROM_ENV/*ub/Klocation$_GSOURCED[12]" + +# Expands to "sub/Klocation_grsourced[12]", matching +# tests/sub/Klocation_grsourced{1,2} +rsource "$SUB_DIR_FROM_ENV/Klocation$_GRSOURCED[12]" +# Test old syntax too +grsource "$SUB_DIR_FROM_ENV/Klocation$_GRSOURCED[12]" + +# No-ops +osource "nonexistent" +osource "nonexistent*" +gsource "nonexistent" +gsource "nonexistent*" +orsource "nonexistent" +orsource "nonexistent*" +grsource "nonexistent" +grsource "nonexistent*" + +config MULTI_DEF diff --git a/support/kconfiglib/tests/Klocation_sourced b/support/kconfiglib/tests/Klocation_sourced new file mode 100644 index 0000000..6e19f76 --- /dev/null +++ b/support/kconfiglib/tests/Klocation_sourced @@ -0,0 +1,18 @@ + + +config MULTI_DEF + +choice CHOICE + bool "choice" +endchoice + +config MENU_HOOK + bool + +menu "menu" +endmenu + +config COMMENT_HOOK + bool + +comment "comment" diff --git a/support/kconfiglib/tests/Kmainmenu b/support/kconfiglib/tests/Kmainmenu new file mode 100644 index 0000000..80713c0 --- /dev/null +++ b/support/kconfiglib/tests/Kmainmenu @@ -0,0 +1,5 @@ +config FOO + string + option env="FOO" + +mainmenu "---$FOO---" diff --git a/support/kconfiglib/tests/Kmenuconfig b/support/kconfiglib/tests/Kmenuconfig new file mode 100644 index 0000000..9a4cc11 --- /dev/null +++ b/support/kconfiglib/tests/Kmenuconfig @@ -0,0 +1,37 @@ +# Menu nodes with is_menuconfig False + +config NOT_MENUCONFIG_1 + bool + +config NOT_MENUCONFIG_2 + bool "not menuconfig 2" + +config MENUCONFIG_MULTI_DEF + bool "menuconfig multi def 1" + +config COMMENT_HOOK + bool + +comment "not menuconfig 3" + + +# Menu nodes with is_menuconfig True + +menuconfig MENUCONFIG_1 + bool "menuconfig 1" + +menuconfig MENUCONFIG_MULTI_DEF + bool "menuconfig multi def 2" + +config MENU_HOOK + bool + +menu "menuconfig 2" +endmenu + +config CHOICE_HOOK + bool + +choice + bool "menuconfig 3" +endchoice diff --git a/support/kconfiglib/tests/Kmisc b/support/kconfiglib/tests/Kmisc new file mode 100644 index 0000000..d9a799d --- /dev/null +++ b/support/kconfiglib/tests/Kmisc @@ -0,0 +1,82 @@ +# For testing various minor APIs + +# optional choices + +choice NOT_OPTIONAL + bool "not optional" +config A + bool "A" +config B + bool "B" +endchoice + +choice OPTIONAL + tristate "optional" + optional +config C + tristate "C" +config D + tristate "D" +# Quirky symbols - not proper choice symbol + +config Q1 + tristate "Q1" + depends on D + +config Q2 + tristate "Q2" + depends on Q1 + +config Q3 + tristate "Q3" + depends on D + +endchoice + +# User values + +config BOOL + bool "bool" if NOT_DEFINED_1 + +config TRISTATE + tristate # Visibility should not affect user value + +config STRING + string "string" + +config INT + int # Visibility should not affect user value + +config HEX + hex "hex" + depends on NOT_DEFINED_2 + +config COMMENT_HOOK +comment "comment" + +config MENU_HOOK +menu "menu" + depends on NOT_DEFINED_3 || NOT_DEFINED_2 + depends on !NOT_DEFINED_4 +endmenu + +config FROM_ENV + string "from env" + option env="ENV_VAR" + +config FROM_ENV_MISSING + string "from env missing" + option env="MISSING_ENV_VAR" + default "missing" + +config FROM_ENV_WEIRD + string + default "weird" + option env="ENV_VAR" + +config NOT_ALLNOCONFIG_Y + bool "not allnoconfig_y" + +config ALLNOCONFIG_Y + bool "allnoconfig_y" + option allnoconfig_y diff --git a/support/kconfiglib/tests/Kmissingrsource b/support/kconfiglib/tests/Kmissingrsource new file mode 100644 index 0000000..924b0b3 --- /dev/null +++ b/support/kconfiglib/tests/Kmissingrsource @@ -0,0 +1 @@ +rsource "nonexistent" diff --git a/support/kconfiglib/tests/Kmissingsource b/support/kconfiglib/tests/Kmissingsource new file mode 100644 index 0000000..a3a6c25 --- /dev/null +++ b/support/kconfiglib/tests/Kmissingsource @@ -0,0 +1 @@ +source "nonexistent" diff --git a/support/kconfiglib/tests/Korder b/support/kconfiglib/tests/Korder new file mode 100644 index 0000000..3a8dffa --- /dev/null +++ b/support/kconfiglib/tests/Korder @@ -0,0 +1,35 @@ +config O + int "O" + default 0 + +config R + int "R" + default 1 + +config D + int "D" + default 2 + +config E + int "E" + default 3 + +# Defined twice +config R + int "R" + +config R2 + int "R2" + default 4 + +config I + int "I" + default 5 + +config N + int "N" + default 6 + +config G + int "G" + default 7 diff --git a/support/kconfiglib/tests/Kpreprocess b/support/kconfiglib/tests/Kpreprocess new file mode 100644 index 0000000..e30b389 --- /dev/null +++ b/support/kconfiglib/tests/Kpreprocess @@ -0,0 +1,147 @@ +# Simple assignments (with bad formatting, as an additional test) + +simple-recursive=foo +simple-immediate:=bar +# Should become recursive +simple-recursive-2+=baz + + whitespaced = foo + + +# Simple += test. += should preserve the flavor of the variable (simple vs. +# recursive). + +preserve-recursive = foo +preserve-recursive += bar + +preserve-immediate := foo +preserve-immediate += bar + + +# Recursive substitution + +recursive = $(foo) $(bar) $($(b-char)a$(z-char)) +recursive += $(indir) + +foo = abc +bar = def +baz = ghi + +b-char = b +z-char = z + +indir = jkl $(indir-2) +indir-2 = mno + + +# Immediate substitution + +def = foo +immediate := $(undef)$(def)$(undef)$(def) +def = bar +undef = bar + + +# Function calls + +# Chained function call +quote = "$(1)" "$(2)" +rev-quote = $(quote,$(2),$(1)) +surround-rev-quote = $(0) $(rev-quote,$(1),$(2)) $(0) +surround-rev-quote-unused-arg = $(surround-rev-quote,$(1),$(2)) $(3) +# No value is passed for $(3), so it expands to nothing +fn-indir = surround-rev-quote +messy-fn-res = $($(fn-indir)-unused-arg, a b , c d ) + +# Special characters in function call +comma = , +right-paren = ) +dollar = $ +left-paren = ( +fn = "$(1)" +special-chars-fn-res = $(fn,$(comma)$(dollar)$(left-paren)foo$(right-paren)) + + +# Variable expansions in various locations (verified by checking how the symbol +# prints) + +qaz = QAZ +echo = $(1) +ignore-first = $(2) + +config PRINT_ME + string "$(ENV_1)" if ($(echo,FOO) && $(echo,BAR)) || !$(echo,BAZ) || !(($(qaz))) + default "$(echo,"foo")" if "foo $(echo,"bar") baz" = "$(undefined)" + +# Expansion within a symbol token, with deliberate sloppiness +config PRINT_$(ignore-first, ,ME)_TOO + bool "foo" + default FOO$(ignore-first, ,BAR)BAZ$(qaz) if $(qaz)&&$(qaz)FOO&&x$(ignore-first, ,xx) + + +# Recursive expansion (throws an exception) + +rec-1 = x $(rec-2) y +rec-2 = x $(rec-3) y +rec-3 = x $(rec-1) y + +# Functions are allowed to reference themselves, but an exception is thrown if +# the function seems to be stuck (the recursion gets too deep) +safe-fn-rec = $($(1)) +safe-fn-rec-2 = $(safe-fn-rec,safe-fn-rec-3) +safe-fn-rec-3 = foo +safe-fn-rec-res = $(safe-fn-rec,safe-fn-rec-2) + +unsafe-fn-rec = $(unsafe-fn-rec,$(1)) + + +# Expansion in the left-hand side of assignments + +dummy-arg-fn = bar +lhs-indir-1 = lhs-indir-2 +lhs-indir-2 = -baz +rhs = value +# LHS expands to foo-bar-baz +foo-$(dummy-arg-fn, ignored argument )$($(lhs-indir-1)) = $(rhs) +# Expands to empty string, accepted + $(undefined) + +# Variable with a space in its name +empty = +space = $(empty) $(empty) +foo$(space)bar = value +space-var-res = $(foo bar) + + +# Built-in functions + +# Expands to "baz qaz" +shell-res = $(shell,false && echo foo bar || echo baz qaz) + +# Warns about output on stderr, expands to nothing +shell-stderr-res := $(shell,echo message on stderr >&2) + +# Expands to the current location +location-res := $(filename):$(lineno) + +# Adds one warning, expands to nothing +$(warning-if,,no warning) +$(warning-if,n,no warning) +warning-res := $(warning-if,y,a warning) + +# Does not cause an error, expands to nothing +error-n-res := $(error-if,n,oops) + +# Causes an error when expanded +error-y-res = $(error-if,y,oops) + + +# Environment variables (for testing Kconfig.env_vars). ENV_1 is already +# referenced above. +env_ref_1 := xxx $(ENV_2) xxx +env_ref_2 := $(shell,echo $(ENV_3)) +env_ref_3 := +env_ref_3 += $(ENV_4) +$(warning-if,$(ENV_5),$(ENV_UNDEFINED)) +source "$(ENV_6)" +env_ref_4 = $(ENV_7) # Never evaluated diff --git a/support/kconfiglib/tests/Krange b/support/kconfiglib/tests/Krange new file mode 100644 index 0000000..3057483 --- /dev/null +++ b/support/kconfiglib/tests/Krange @@ -0,0 +1,133 @@ +# +# No ranges +# + +config HEX_NO_RANGE + hex "hex no range" + +config INT_NO_RANGE + int "int no range" + +# +# Disabled ranges +# + +config HEX_ALL_RANGES_DISABLED + hex "hex all ranges disabled" + range 0x10 0x20 if n + range 0x30 0x40 if n + +config INT_ALL_RANGES_DISABLED + int "int all ranges disabled" + range 10 20 if n + range 30 40 if n + +# +# Ranges with defaults +# + +# hex + +config HEX_RANGE_10_20_LOW_DEFAULT + hex "int range 10-20 low default" + range 0x10 0x20 + default 0x9 + +config HEX_RANGE_10_20_HIGH_DEFAULT + hex "int range 10-20 high default" + range 0x10 0x20 + default 0x21 + +config HEX_RANGE_10_20_OK_DEFAULT + hex "int range 10-20 ok default" + range 0x10 0x20 + default 0x15 + +config HEX_RANGE_10_20_OK_DEFAULT_ALTERNATE + hex "int range 10-20 ok default alternate" + range 0x10 0x20 + default 15 + +# int + +config INT_RANGE_10_20_LOW_DEFAULT + int "int range 10-20 low default" + range 10 20 + default 9 + +config INT_RANGE_10_20_HIGH_DEFAULT + int "int range 10-20 high default" + range 10 20 + default 21 + +config INT_RANGE_10_20_OK_DEFAULT + int "int range 10-20 ok default" + range 10 20 + default 15 + +# +# Ranges with no defaults (should default to low end of range if > 0) +# + +config HEX_RANGE_10_20 + hex "hex range 10-20" + range 0x10 0x20 + +config HEX_RANGE_0_10 + hex "hex range 0-10" + range 0x0 0x10 + +config INT_RANGE_10_20 + int "int range 10-20" + range 10 20 + +config INT_RANGE_0_10 + int "int range 0-10" + range 0 10 + +config INT_RANGE_NEG_10_10 + int "int range -10-10" + range -10 10 + +# +# Dependent ranges +# + +config HEX_40 + hex + default 40 + +config HEX_RANGE_10_40_DEPENDENT + hex "hex range 10-40 dependent" + range HEX_RANGE_10_20 HEX_40 + +config INT_40 + int + default 40 + +config INT_RANGE_10_40_DEPENDENT + int "int range 10-40 dependent" + range INT_RANGE_10_20 INT_40 + +# +# Ranges on symbols defined in multiple locations +# + +if n +config INACTIVE_RANGE + range 0 1 +endif + +config INACTIVE_RANGE + int + # Default will apply and should not get clamped, + # because the range does not apply + default 2 + +config ACTIVE_RANGE + range 0 1 + +config ACTIVE_RANGE + int + # Default will apply and should be clamped to 1 + default 2 diff --git a/support/kconfiglib/tests/Krecursive1 b/support/kconfiglib/tests/Krecursive1 new file mode 100644 index 0000000..35b521a --- /dev/null +++ b/support/kconfiglib/tests/Krecursive1 @@ -0,0 +1 @@ +source "tests/Krecursive2" diff --git a/support/kconfiglib/tests/Krecursive2 b/support/kconfiglib/tests/Krecursive2 new file mode 100644 index 0000000..9f9f00e --- /dev/null +++ b/support/kconfiglib/tests/Krecursive2 @@ -0,0 +1 @@ +source "tests/Krecursive1" diff --git a/support/kconfiglib/tests/Kreferenced b/support/kconfiglib/tests/Kreferenced new file mode 100644 index 0000000..9da94c8 --- /dev/null +++ b/support/kconfiglib/tests/Kreferenced @@ -0,0 +1,63 @@ +config NO_REFS + bool + +config JUST_DEPENDS_ON_REFS + bool + depends on A && B + +if A + +menu "menu" + depends on B + visible if C + visible if D + +config LOTS_OF_REFS + bool "lots" if C || D + default E || F if G || H + default I || J if K || L + select M if N || O + select P if Q || R + imply S if T || U + imply V if W || X + depends on Y || Z + +endmenu + +endif + +config INT_REFS + int "int" + range A B if C && D + range E F if G && H + +choice CHOICE + bool "choice" + +config CHOICE_REF + bool "choice ref" + +endchoice + +comment "comment" + depends on A || B + + +config MULTI_DEF_SYM + def_bool A && B + +config MULTI_DEF_SYM + depends on C + + +choice MULTI_DEF_CHOICE + bool "choice" + depends on A && B + +endchoice + +choice MULTI_DEF_CHOICE + bool "choice" + depends on C + +endchoice diff --git a/support/kconfiglib/tests/Krelation b/support/kconfiglib/tests/Krelation new file mode 100644 index 0000000..940eff8 --- /dev/null +++ b/support/kconfiglib/tests/Krelation @@ -0,0 +1,36 @@ +config A + bool + depends on UNDEFINED + +choice CHOICE_1 + bool "C" +config B + bool "B" +config C + bool "C" if B +config D + bool "D" +endchoice + +menu "m1" +config E + bool +menu "m2" +config F + bool +choice CHOICE_2 + tristate "foo" +config G + bool "g" +config H + bool "h" +endchoice +endmenu +config I + bool +endmenu + +menu "m3" +endmenu +menu "m4" +endmenu diff --git a/support/kconfiglib/tests/Krepr b/support/kconfiglib/tests/Krepr new file mode 100644 index 0000000..fe6d8f0 --- /dev/null +++ b/support/kconfiglib/tests/Krepr @@ -0,0 +1,64 @@ +config MODULES + bool + option modules + default y + +if UNDEFINED +endif + +config BASIC + bool + default y + ---help--- + +config VISIBLE + bool "visible" + +config STRING + string "visible" + +config DIR_DEP_N + depends on n + +config OPTIONS + option allnoconfig_y + option defconfig_list + option env="ENV" + +config MULTI_DEF +config MULTI_DEF + +menuconfig MENUCONFIG + +choice CHOICE + tristate "choice" + +config CHOICE_1 + tristate "choice sym" + +config CHOICE_2 + tristate "choice sym" + +endchoice + +config CHOICE_HOOK + +choice + tristate "optional choice" if n + optional +endchoice + +config NO_VISIBLE_IF_HOOK + +menu "no visible if" +endmenu + +config VISIBLE_IF_HOOK + +menu "visible if" + visible if m +endmenu + +config COMMENT_HOOK + +comment "comment" diff --git a/support/kconfiglib/tests/Kstr b/support/kconfiglib/tests/Kstr new file mode 100644 index 0000000..f55c830 --- /dev/null +++ b/support/kconfiglib/tests/Kstr @@ -0,0 +1,293 @@ +if UNDEFINED +endif + +config NO_TYPE + +config BASIC_NO_PROMPT + bool + help + blah blah + + blah blah blah + + blah + +config BASIC_PROMPT + bool "basic" + +config ADVANCED + tristate "prompt" if DEP + default DEFAULT_1 + default DEFAULT_2 if DEP + select SELECTED_1 + select SELECTED_2 if DEP + imply IMPLIED_1 + imply IMPLIED_2 if DEP + help + first help text + +config ADVANCED + prompt "prompt 2" + +menuconfig ADVANCED + prompt "prompt 3" + +if DEP3 + +menu "foo" + depends on DEP4 + visible if VIS + +config ADVANCED + help + second help text + depends on A || !B || (C && D) || !(E && F) || G = H || \ + (I && !J && (K || L) && !(M || N) && O = P) + +config ADVANCED + # Used to verify that the direct dependencies appear to the right of VIS + prompt "prompt 4" + +endmenu + +endif + +config ONLY_DIRECT_DEPS + int + depends on DEP1 + depends on DEP2 + +config STRING + def_string "foo" + default "bar" if DEP + default STRING2 + default STRING3 if DEP + +config INT + def_int 7 if DEP + range 1 2 + range FOO BAR + range BAZ QAZ if DEP + +config HEX + def_hex 0x123 + range 0x100 0x200 + range FOO BAR + range BAZ QAZ if DEP + +config MODULES + bool "MODULES" + option modules + +config OPTIONS + option allnoconfig_y + option defconfig_list + option env="ENV" + +if LOC_1 +config CORRECT_PROP_LOCS_BOOL + prompt "prompt 1" + default DEFAULT_1 + default DEFAULT_2 + select SELECT_1 + select SELECT_2 + imply IMPLY_1 + imply IMPLY_2 + help + help 1 +endif + +if LOC_2 +menuconfig CORRECT_PROP_LOCS_BOOL + bool "prompt 2" + default DEFAULT_3 + default DEFAULT_4 + select SELECT_3 + select SELECT_4 + imply IMPLY_3 + imply IMPLY_4 + help + help 2 +endif + +if LOC_3 +config CORRECT_PROP_LOCS_BOOL + prompt "prompt 3" + default DEFAULT_5 + default DEFAULT_6 + select SELECT_5 + select SELECT_6 + imply IMPLY_5 + imply IMPLY_6 + help + help 2 +endif + +if LOC_1 +config CORRECT_PROP_LOCS_INT + int + range 1 2 + range 3 4 +endif + +if LOC_2 +config CORRECT_PROP_LOCS_INT + range 5 6 + range 7 8 +endif + +choice CHOICE + tristate "foo" + default CHOICE_1 + default CHOICE_2 if dep + +config CHOICE_1 + tristate "choice 1" + +config CHOICE_2 + tristate "choice 2" + +endchoice + +choice + tristate "no name" + optional +endchoice + +if LOC_1 +choice CORRECT_PROP_LOCS_CHOICE + bool + default CHOICE_3 + +config CHOICE_3 + bool "choice 3" + +config CHOICE_4 + bool "choice 3" + +config CHOICE_5 + bool "choice 3" + +endchoice +endif + +if LOC_2 +choice CORRECT_PROP_LOCS_CHOICE + default CHOICE_4 +endchoice +endif + +if LOC_3 +choice CORRECT_PROP_LOCS_CHOICE + default CHOICE_5 +endchoice +endif + +config SIMPLE_MENU_HOOK +menu "simple menu" +endmenu + +config ADVANCED_MENU_HOOK +menu "advanced menu" + depends on A + visible if B + visible if C || D +endmenu + +config SIMPLE_COMMENT_HOOK +comment "simple comment" + +config ADVANCED_COMMENT_HOOK +comment "advanced comment" + depends on A + depends on B + +# Corner cases when removing direct dependencies + +config DEP_REM_CORNER_CASES + bool + default A + depends on n + +config DEP_REM_CORNER_CASES + default B if n + +config DEP_REM_CORNER_CASES + default C + depends on m + +config DEP_REM_CORNER_CASES + default D if A && y + depends on y + +config DEP_REM_CORNER_CASES + default E if !E1 + default F if F1 = F2 + default G if G1 || H1 + depends on !H + +config DEP_REM_CORNER_CASES + default H + depends on "foo" = "bar" + +menu "menu" + visible if FOO || BAR + +config DEP_REM_CORNER_CASES + prompt "prompt" + depends on BAZ && QAZ + +endmenu + +# Only prompt, no type +config PROMPT_ONLY + prompt "prompt only" + +# {Symbol,Choice}.orig_* + +if BASE_DEP + +config BOOL_SYM_ORIG + bool + default D1 if DEP + default D2 + select S1 + select S2 if DEP + imply I1 + imply I1 + +config BOOL_SYM_ORIG + default D3 + select S3 + imply I3 if DEP + +config INT_SYM_ORIG + int + range 1 2 if DEP + range 3 4 + +config INT_SYM_ORIG + range 5 6 if DEP + +choice CHOICE_ORIG + bool "choice orig" + default A + default B if DEP + +config A + bool + +config B + bool + +endchoice + +choice CHOICE_ORIG + default C if DEP + +config C + bool + +endchoice + +endif diff --git a/support/kconfiglib/tests/Kundef b/support/kconfiglib/tests/Kundef new file mode 100644 index 0000000..fae521a --- /dev/null +++ b/support/kconfiglib/tests/Kundef @@ -0,0 +1,23 @@ +config DEF + bool + +config BOOL + bool "foo" if DEF || !UNDEF_1 + default UNDEF_2 + +config INT + int + range UNDEF_2 8 + default 10 + range 5 15 + +config HEX + hex + range 0x123 0X456 + default 0x200 + +menu "menu" + depends on UNDEF_1 + visible if UNDEF_3 + +endmenu diff --git a/support/kconfiglib/tests/Kuserfunctions b/support/kconfiglib/tests/Kuserfunctions new file mode 100644 index 0000000..89b4442 --- /dev/null +++ b/support/kconfiglib/tests/Kuserfunctions @@ -0,0 +1,11 @@ +add-zero = $(add) +add-one = $(add,1) +add-three = $(add,1,-1,2,1) + +one-zero = $(one) +one-one = $(one,foo bar) +one-two = $(one,foo bar,baz) + +one-or-more-zero = $(one-or-more) +one-or-more-one = $(one-or-more,foo) +one-or-more-three = $(one-or-more,foo,bar,baz) diff --git a/support/kconfiglib/tests/Kvisibility b/support/kconfiglib/tests/Kvisibility new file mode 100644 index 0000000..91def0a --- /dev/null +++ b/support/kconfiglib/tests/Kvisibility @@ -0,0 +1,342 @@ +config MODULES + bool "MODULES" + option modules + +# +# Symbol visibility +# + +config NO_PROMPT + bool + +# Not rewritten, so MOD will have the value 'y' when running without modules +config MOD + def_tristate m + +config BOOL_N + bool "bool n" if n + +config BOOL_M + # Rewritten to m && MODULES + bool "bool m" if m + +config BOOL_MOD + bool "bool MOD" + # Not rewritten + depends on MOD + +config BOOL_Y + bool "bool y" + # Rewritten to m && MODULES + depends on y || m + +config TRISTATE_N + tristate "tristate n" if n + +config TRISTATE_M + # Rewritten to m && MODULES + tristate "tristate m" if m + +config TRISTATE_MOD + tristate "tristate MOD" + # Not rewritten + depends on MOD + +config TRISTATE_Y + bool "tristate y" + # Rewritten to m && MODULES + depends on y || m + +# Symbols nested in 'if' + +if n + +config BOOL_IF_N + bool "bool if n" + +config TRISTATE_IF_N + tristate "tristate if n" + +endif + +if m + +config BOOL_IF_M + bool "bool if m" + +config TRISTATE_IF_M + tristate "tristate if n" + +endif + +if y + +config BOOL_IF_Y + bool "bool if y" + +config TRISTATE_IF_Y + tristate "tristate if y" + +endif + +# Symbols nested in 'menu' + +menu "menu 1" + depends on n + +config BOOL_MENU_N + bool "bool menu n" + +config TRISTATE_MENU_N + tristate "tristate menu n" + +endmenu + +menu "menu 2" + depends on m + +config BOOL_MENU_M + bool "bool menu m" + +config TRISTATE_MENU_M + tristate "tristate menu n" + +endmenu + +menu "menu 3" + depends on y + +config BOOL_MENU_Y + bool "bool menu y" + +config TRISTATE_MENU_Y + tristate "tristate menu y" + +endmenu + +# Symbols nested in choices + +choice C1 + tristate "choice n" if n + +config BOOL_CHOICE_N + bool "bool choice n" + +config TRISTATE_CHOICE_N + tristate "tristate choice n" + +endchoice + +choice C2 + tristate "choice m" if m + +config BOOL_CHOICE_M + bool "bool choice m" + +config TRISTATE_CHOICE_M + tristate "tristate choice n" + +endchoice + +choice C3 + tristate "choice y" if y + +config BOOL_CHOICE_Y + bool "bool choice y" + +config TRISTATE_CHOICE_Y + tristate "tristate choice y" + +endchoice + +# +# Choice visibility +# + +choice BOOL_CHOICE_N + bool "bool choice n" if n +endchoice + +choice BOOL_CHOICE_M + bool "bool choice m" if m +endchoice + +choice BOOL_CHOICE_Y + bool "bool choice y" if y +endchoice + +choice TRISTATE_CHOICE_N + tristate "tristate choice n" if n +endchoice + +choice TRISTATE_CHOICE_M + tristate "tristate choice m" if m +endchoice + +choice TRISTATE_CHOICE_Y + tristate "tristate choice y" if y + +config K + tristate "K" + +config L + tristate "L" + +endchoice + +if m +choice TRISTATE_CHOICE_IF_M_AND_Y + tristate "tristate choice if m and y" if y + +config M + bool "M" + +config N + bool "N" + +endchoice +endif + +menu "choice-containing menu" + depends on n && y + +choice TRISTATE_CHOICE_MENU_N_AND_Y + tristate "tristate choice if n and y" + +config O + tristate "O" + +config P + tristate "P" + +endchoice + +endmenu + +# +# Menu visibility +# + +menu "menu n" + depends on n +endmenu + +menu "menu m" + depends on m +endmenu + +menu "menu y" + depends on y +endmenu + +if n +menu "menu if n" +endmenu +endif + +if m +menu "menu if m" +endmenu +endif + +if y +menu "menu if y" +endmenu +endif + +if m +menu "menu if m and y" + depends on y +endmenu +endif + +# +# Comment visibility +# + +comment "comment n" + depends on n +comment "comment m" + depends on m +comment "comment y" + depends on y + +if n +comment "comment if n" +endif + +if m +comment "comment if m" +endif + +if y +comment "comment if y" +endif + +if "y" + +menu "menu with comment" + depends on m + +comment "double-nested m comment" + depends on y + +endmenu + +endif + +# Used to verify that string/int/hex symbols with m visibility accept a user +# value + +if m + +config STRING_m + string "string" + +config INT_m + int "int" + +config HEX_m + hex "hex" + +endif + +# Menu 'visible if' visibility + +menu "n-visible menu" + visible if n + +config VISIBLE_IF_N + tristate "visible if n" + +endmenu + +menu "m-visible menu" + visible if m + +config VISIBLE_IF_M + tristate "visible if m" + +endmenu + +menu "y-visible menu" + visible if y + +config VISIBLE_IF_Y + tristate "visible if m" + +endmenu + +menu "m-visible menu 2" + visible if y || n + visible if m && y + visible if y + +if y + +config VISIBLE_IF_M_2 + tristate "visible if m 2" + +endif + +endmenu diff --git a/support/kconfiglib/tests/config_indented b/support/kconfiglib/tests/config_indented new file mode 100644 index 0000000..d57d8b4 --- /dev/null +++ b/support/kconfiglib/tests/config_indented @@ -0,0 +1,3 @@ +# Indented assignments should be ignored to be compatible with the C +# implementation + CONFIG_IGNOREME=n diff --git a/support/kconfiglib/tests/config_set_bool b/support/kconfiglib/tests/config_set_bool new file mode 100644 index 0000000..d2dccbf --- /dev/null +++ b/support/kconfiglib/tests/config_set_bool @@ -0,0 +1 @@ +CONFIG_BOOL=y diff --git a/support/kconfiglib/tests/config_set_string b/support/kconfiglib/tests/config_set_string new file mode 100644 index 0000000..3a1250a --- /dev/null +++ b/support/kconfiglib/tests/config_set_string @@ -0,0 +1 @@ +CONFIG_STRING="foo bar" diff --git a/support/kconfiglib/tests/defconfig_1 b/support/kconfiglib/tests/defconfig_1 new file mode 100644 index 0000000..e69de29 diff --git a/support/kconfiglib/tests/defconfig_2 b/support/kconfiglib/tests/defconfig_2 new file mode 100644 index 0000000..e69de29 diff --git a/support/kconfiglib/tests/empty b/support/kconfiglib/tests/empty new file mode 100644 index 0000000..e69de29 diff --git a/support/kconfiglib/tests/kconfigfunctions.py b/support/kconfiglib/tests/kconfigfunctions.py new file mode 100644 index 0000000..e760468 --- /dev/null +++ b/support/kconfiglib/tests/kconfigfunctions.py @@ -0,0 +1,14 @@ +def add(kconf, name, *args): + return str(sum(map(int, args))) + +def one(kconf, name, s): + return name + 2*s + +def one_or_more(kconf, name, arg, *args): + return arg + " + " + ",".join(args) + +functions = { + "add": (add, 0, None), + "one": (one, 1, 1), + "one-or-more": (one_or_more, 1, None), +} diff --git a/support/kconfiglib/tests/reltest b/support/kconfiglib/tests/reltest new file mode 100755 index 0000000..3e09e5f --- /dev/null +++ b/support/kconfiglib/tests/reltest @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +# Runs the test suite and all examples scripts with Python 2 and Python 3, +# bailing immediately if anything fails. For the examples that aren't tested in +# the test suite, we just confirm that they at least run. +# +# Should be run from the kernel root with $ Kconfiglib/tests/reltest + +test_script() { + echo -e "\n================= $1 with $py =================\n" + if (($# == 1)); then + make scriptconfig PYTHONCMD=$py SCRIPT=$1 + else + make scriptconfig PYTHONCMD=$py SCRIPT=$1 SCRIPT_ARG="$2" + fi + + if (($?)); then + echo "$1 failed to run with $py" + exit 1 + fi +} + +for py in python2 python3; do + echo -e "\n================= Test suite with $py =================\n" + + if ! $py Kconfiglib/testsuite.py; then + echo "test suite failed for $py" + exit 1 + fi + + # Check that the example scripts that aren't tested by the test suite run + # at least + + test_script Kconfiglib/examples/defconfig_oldconfig.py + test_script Kconfiglib/examples/eval_expr.py MODULES + test_script Kconfiglib/examples/find_symbol.py X86 + test_script Kconfiglib/examples/help_grep.py general + test_script Kconfiglib/examples/print_sym_info.py MODULES + test_script Kconfiglib/examples/print_tree.py + + $py Kconfiglib/examples/menuconfig_example.py Kconfiglib/examples/Kmenuconfig <<END +BOOL +n +END + if (($?)); then + echo "menuconfig_example.py failed with $py" + exit 1 + fi +done + +echo "everything okay" diff --git a/support/kconfiglib/tests/sub/Kconfig_symlink_2 b/support/kconfiglib/tests/sub/Kconfig_symlink_2 new file mode 100644 index 0000000..aeba985 --- /dev/null +++ b/support/kconfiglib/tests/sub/Kconfig_symlink_2 @@ -0,0 +1 @@ +rsource "Kconfig_symlink_3" diff --git a/support/kconfiglib/tests/sub/Kconfig_symlink_3 b/support/kconfiglib/tests/sub/Kconfig_symlink_3 new file mode 100644 index 0000000..20b4e06 --- /dev/null +++ b/support/kconfiglib/tests/sub/Kconfig_symlink_3 @@ -0,0 +1,2 @@ +config FOUNDME + bool diff --git a/support/kconfiglib/tests/sub/Klocation_grsourced1 b/support/kconfiglib/tests/sub/Klocation_grsourced1 new file mode 100644 index 0000000..49dac36 --- /dev/null +++ b/support/kconfiglib/tests/sub/Klocation_grsourced1 @@ -0,0 +1 @@ +config MULTI_DEF diff --git a/support/kconfiglib/tests/sub/Klocation_grsourced2 b/support/kconfiglib/tests/sub/Klocation_grsourced2 new file mode 100644 index 0000000..49dac36 --- /dev/null +++ b/support/kconfiglib/tests/sub/Klocation_grsourced2 @@ -0,0 +1 @@ +config MULTI_DEF diff --git a/support/kconfiglib/tests/sub/Klocation_gsourced1 b/support/kconfiglib/tests/sub/Klocation_gsourced1 new file mode 100644 index 0000000..49dac36 --- /dev/null +++ b/support/kconfiglib/tests/sub/Klocation_gsourced1 @@ -0,0 +1 @@ +config MULTI_DEF diff --git a/support/kconfiglib/tests/sub/Klocation_gsourced2 b/support/kconfiglib/tests/sub/Klocation_gsourced2 new file mode 100644 index 0000000..49dac36 --- /dev/null +++ b/support/kconfiglib/tests/sub/Klocation_gsourced2 @@ -0,0 +1 @@ +config MULTI_DEF diff --git a/support/kconfiglib/tests/sub/Klocation_rsourced b/support/kconfiglib/tests/sub/Klocation_rsourced new file mode 100644 index 0000000..c6f185d --- /dev/null +++ b/support/kconfiglib/tests/sub/Klocation_rsourced @@ -0,0 +1,2 @@ + +config MULTI_DEF diff --git a/support/kconfiglib/tests/sub/defconfig_in_sub b/support/kconfiglib/tests/sub/defconfig_in_sub new file mode 100644 index 0000000..e69de29 diff --git a/support/kconfiglib/tests/sub/sub/Kconfig_symlink_1 b/support/kconfiglib/tests/sub/sub/Kconfig_symlink_1 new file mode 100644 index 0000000..bceec8b --- /dev/null +++ b/support/kconfiglib/tests/sub/sub/Kconfig_symlink_1 @@ -0,0 +1,2 @@ +# Sources tests/sub/Kconfig_symlink_2, with an absolute path +source "$(KCONFIG_SYMLINK_2)" diff --git a/support/kconfiglib/tests/symlink b/support/kconfiglib/tests/symlink new file mode 120000 index 0000000..565623e --- /dev/null +++ b/support/kconfiglib/tests/symlink @@ -0,0 +1 @@ +sub/sub \ No newline at end of file diff --git a/support/kconfiglib/testsuite.py b/support/kconfiglib/testsuite.py new file mode 100644 index 0000000..3a6a3a6 --- /dev/null +++ b/support/kconfiglib/testsuite.py @@ -0,0 +1,3136 @@ +# Copyright (c) 2011-2019, Ulf Magnusson +# SPDX-License-Identifier: ISC + +# This is the Kconfiglib test suite. It runs selftests on Kconfigs provided by +# us and tests compatibility with the C Kconfig implementation by comparing the +# output of Kconfiglib with the output of the scripts/kconfig/*conf utilities +# for different targets and defconfigs. It should be run from the top-level +# kernel directory with +# +# $ python Kconfiglib/testsuite.py +# +# Some additional options can be turned on by passing them as arguments. They +# default to off. +# +# - obsessive: +# By default, only valid arch/defconfig pairs are tested. In obsessive mode, +# every arch will be tested with every defconfig. Increases the testing time +# by an order of magnitude. Occasionally finds (usually obscure) bugs, and I +# make sure everything passes with it. +# +# - obsessive-min-config: +# Like obsessive, for the minimal configuation (defconfig) tests. +# +# - log: +# Log timestamped defconfig test failures to the file test_defconfig_fails. +# Handy in obsessive mode. +# +# For example, this commands runs the test suite in obsessive mode with logging +# enabled: +# +# $ python(3) Kconfiglib/testsuite.py obsessive log +# +# pypy works too, and runs most tests much faster than CPython. +# +# All tests should pass. Report regressions to ulfalizer a.t Google's email +# service. + +import difflib +import errno +import os +import platform +import re +import shutil +import subprocess +import sys +import tempfile +import textwrap + +from kconfiglib import Kconfig, Symbol, Choice, COMMENT, MENU, MenuNode, \ + BOOL, TRISTATE, HEX, STRING, \ + TRI_TO_STR, \ + escape, unescape, \ + expr_str, expr_value, expr_items, split_expr, \ + _ordered_unique, \ + OR, AND, \ + KconfigError + + +def shell(cmd): + with open(os.devnull, "w") as devnull: + subprocess.call(cmd, shell=True, stdout=devnull, stderr=devnull) + + +all_passed = True + + +def fail(msg=None): + global all_passed + all_passed = False + if msg is not None: + print("fail: " + msg) + + +def verify(cond, msg): + if not cond: + fail(msg) + + +def verify_equal(x, y): + if x != y: + fail("'{}' does not equal '{}'".format(x, y)) + + +# Prevent accidental loading of configuration files by removing +# KCONFIG_ALLCONFIG from the environment +os.environ.pop("KCONFIG_ALLCONFIG", None) + +obsessive = False +obsessive_min_config = False +log = False + + +def run_tests(): + global obsessive, log + for s in sys.argv[1:]: + if s == "obsessive": + obsessive = True + print("Obsessive mode enabled") + elif s == "obsessive-min-config": + obsessive_min_config = True + print("Obsessive minimal config mode enabled") + elif s == "log": + log = True + print("Log mode enabled") + else: + print("Unrecognized option '{}'".format(s)) + return + + run_selftests() + run_compatibility_tests() + + +def run_selftests(): + # + # Common helper functions. These all expect 'c' to hold the current + # configuration. + # + + def verify_value(sym_name, val): + # Verifies that a symbol has a particular value. + + if isinstance(val, int): + val = TRI_TO_STR[val] + + sym = c.syms[sym_name] + verify(sym.str_value == val, + 'expected {} to have the value "{}", had the value "{}"' + .format(sym_name, val, sym.str_value)) + + def assign_and_verify_value(sym_name, val, new_val): + # Assigns 'val' to a symbol and verifies that its value becomes + # 'new_val'. Assumes (and tests) that 'val' is valid for the + # symbol type. + + if isinstance(new_val, int): + new_val = TRI_TO_STR[new_val] + + sym = c.syms[sym_name] + old_val = sym.str_value + verify(sym.set_value(val), + "assigning '{}' to {} unexpectedly failed" + .format(val, sym_name)) + verify(sym.str_value == new_val, + "expected {} to have the value '{}' after being assigned the " + "value '{}'. Instead, the value is '{}'. The old value was " + "'{}'." + .format(sym_name, new_val, val, sym.str_value, old_val)) + + def assign_and_verify(sym_name, user_val): + # Like assign_and_verify_value(), with the expected value being the + # value just set. + + assign_and_verify_value(sym_name, user_val, user_val) + + def assign_and_verify_user_value(sym_name, val, user_val, valid): + # Assigns a user value to the symbol and verifies the new user value. + # If valid is True, the user value is valid for the type, otherwise + # not. This is used to test the set_value() return value. + + sym = c.syms[sym_name] + sym_old_user_val = sym.user_value + + verify(sym.set_value(val) == valid, + "expected the user value '{}' to be {} for {}, was not" + .format(val, "valid" if valid else "invalid", sym_name)) + verify(sym.user_value == user_val, + "the assigned user value '{}' wasn't reflected in user_value " + "on the symbol {}. Instead, the new user_value was '{}'. The " + "old user value was '{}'." + .format(user_val, sym_name, sym.user_value, sym_old_user_val)) + + # + # Selftests + # + + print("Testing string literal lexing") + + # Dummy empty configuration just to get a Kconfig object + c = Kconfig("Kconfiglib/tests/empty") + + def verify_string_lex(s, expected): + # Verifies that a constant symbol with the name 'res' is produced from + # lexing 's' + + res = c._tokenize("if " + s)[1].name + verify(res == expected, + "expected <{}> to produced the constant symbol <{}>, " + 'produced <{}>'.format(s[1:-1], expected, res)) + + verify_string_lex(r""" "" """, "") + verify_string_lex(r""" '' """, "") + + verify_string_lex(r""" "a" """, "a") + verify_string_lex(r""" 'a' """, "a") + verify_string_lex(r""" "ab" """, "ab") + verify_string_lex(r""" 'ab' """, "ab") + verify_string_lex(r""" "abc" """, "abc") + verify_string_lex(r""" 'abc' """, "abc") + + verify_string_lex(r""" "'" """, "'") + verify_string_lex(r""" '"' """, '"') + + verify_string_lex(r""" "\"" """, '"') + verify_string_lex(r""" '\'' """, "'") + + verify_string_lex(r""" "\"\"" """, '""') + verify_string_lex(r""" '\'\'' """, "''") + + verify_string_lex(r""" "\'" """, "'") + verify_string_lex(r""" '\"' """, '"') + + verify_string_lex(r""" "\\" """, "\\") + verify_string_lex(r""" '\\' """, "\\") + + verify_string_lex(r""" "\a\\'\b\c\"'d" """, 'a\\\'bc"\'d') + verify_string_lex(r""" '\a\\"\b\c\'"d' """, "a\\\"bc'\"d") + + def verify_string_bad(s): + # Verifies that tokenizing 's' throws a KconfigError. Strips the first + # and last characters from 's' so we can use readable raw strings as + # input. + + try: + c.eval_string(s) + except KconfigError: + pass + else: + fail("expected tokenization of {} to fail, didn't".format(s[1:-1])) + + verify_string_bad(r""" " """) + verify_string_bad(r""" ' """) + verify_string_bad(r""" "' """) + verify_string_bad(r""" '" """) + verify_string_bad(r""" "\" """) + verify_string_bad(r""" '\' """) + verify_string_bad(r""" "foo """) + verify_string_bad(r""" 'foo """) + + + print("Testing escape() and unescape()") + + def verify_escape_unescape(s, sesc): + # Verify that 's' escapes to 'sesc' and that 'sesc' unescapes to 's' + verify_equal(escape(s), sesc) + verify_equal(unescape(sesc), s) + + verify_escape_unescape(r'' , r'' ) + verify_escape_unescape(r'foo' , r'foo' ) + verify_escape_unescape(r'"' , r'\"' ) + verify_escape_unescape(r'""' , r'\"\"' ) + verify_escape_unescape('\\' , r'\\' ) + verify_escape_unescape(r'\\' , r'\\\\' ) + verify_escape_unescape(r'\"' , r'\\\"' ) + verify_escape_unescape(r'"ab\cd"ef"', r'\"ab\\cd\"ef\"') + + # Backslashes before any character should be unescaped, not just before " + # and \ + verify_equal(unescape(r"\afoo\b\c\\d\\\e\\\\f"), r"afoobc\d\e\\f") + + + print("Testing _ordered_unique()") + + verify_equal(_ordered_unique([]), []) + verify_equal(_ordered_unique([1]), [1]) + verify_equal(_ordered_unique([1, 2]), [1, 2]) + verify_equal(_ordered_unique([1, 1]), [1]) + verify_equal(_ordered_unique([1, 1, 2]), [1, 2]) + verify_equal(_ordered_unique([1, 2, 1]), [1, 2]) + verify_equal(_ordered_unique([1, 2, 2]), [1, 2]) + verify_equal(_ordered_unique([1, 2, 3, 2, 1, 2, 3, 4, 3, 2, 1, 0]), + [1, 2, 3, 4, 0]) + + + print("Testing expression evaluation") + + c = Kconfig("Kconfiglib/tests/Keval", warn=False) + + def verify_eval(expr, val): + res = c.eval_string(expr) + verify(res == val, + "'{}' evaluated to {}, expected {}".format(expr, res, val)) + + # No modules + verify_eval("n", 0) + verify_eval("m", 0) + verify_eval("y", 2) + verify_eval("'n'", 0) + verify_eval("'m'", 0) + verify_eval("'y'", 2) + verify_eval("M", 2) + + # Modules + c.modules.set_value(2) + verify_eval("n", 0) + verify_eval("m", 1) + verify_eval("y", 2) + verify_eval("'n'", 0) + verify_eval("'m'", 1) + verify_eval("'y'", 2) + verify_eval("M", 1) + verify_eval("(Y || N) && (m && y)", 1) + + # Non-bool/non-tristate symbols are always n in a tristate sense + verify_eval("Y_STRING", 0) + verify_eval("Y_STRING || m", 1) + + # As are all constants besides y and m + verify_eval('"foo"', 0) + verify_eval('"foo" || "bar"', 0) + verify_eval('"foo" || m', 1) + + # Test equality for symbols + + verify_eval("N = N", 2) + verify_eval("N = n", 2) + verify_eval("N = 'n'", 2) + verify_eval("N != N", 0) + verify_eval("N != n", 0) + verify_eval("N != 'n'", 0) + + verify_eval("M = M", 2) + verify_eval("M = m", 2) + verify_eval("M = 'm'", 2) + verify_eval("M != M", 0) + verify_eval("M != m", 0) + verify_eval("M != 'm'", 0) + + verify_eval("Y = Y", 2) + verify_eval("Y = y", 2) + verify_eval("Y = 'y'", 2) + verify_eval("Y != Y", 0) + verify_eval("Y != y", 0) + verify_eval("Y != 'y'", 0) + + verify_eval("N != M", 2) + verify_eval("N != Y", 2) + verify_eval("M != Y", 2) + + verify_eval("Y_STRING = y", 2) + verify_eval("Y_STRING = 'y'", 2) + verify_eval('FOO_BAR_STRING = "foo bar"', 2) + verify_eval('FOO_BAR_STRING != "foo bar baz"', 2) + verify_eval('INT_37 = 37', 2) + verify_eval("INT_37 = '37'", 2) + verify_eval('HEX_0X37 = 0x37', 2) + verify_eval("HEX_0X37 = '0x37'", 2) + + # These should also hold after 31847b67 (kconfig: allow use of relations + # other than (in)equality) + verify_eval("HEX_0X37 = '0x037'", 2) + verify_eval("HEX_0X37 = '0x0037'", 2) + + # Constant symbol comparisons + verify_eval('"foo" != "bar"', 2) + verify_eval('"foo" = "bar"', 0) + verify_eval('"foo" = "foo"', 2) + + # Undefined symbols get their name as their value + c.warn = False + verify_eval("'not_defined' = not_defined", 2) + verify_eval("not_defined_2 = not_defined_2", 2) + verify_eval("not_defined_1 != not_defined_2", 2) + + # Test less than/greater than + + # Basic evaluation + verify_eval("INT_37 < 38", 2) + verify_eval("38 < INT_37", 0) + verify_eval("INT_37 < '38'", 2) + verify_eval("'38' < INT_37", 0) + verify_eval("INT_37 < 138", 2) + verify_eval("138 < INT_37", 0) + verify_eval("INT_37 < '138'", 2) + verify_eval("'138' < INT_37", 0) + verify_eval("INT_37 < -138", 0) + verify_eval("-138 < INT_37", 2) + verify_eval("INT_37 < '-138'", 0) + verify_eval("'-138' < INT_37", 2) + verify_eval("INT_37 < 37", 0) + verify_eval("37 < INT_37", 0) + verify_eval("INT_37 < 36", 0) + verify_eval("36 < INT_37", 2) + + # Different formats in comparison + verify_eval("INT_37 < 0x26", 2) # 38 + verify_eval("INT_37 < 0x25", 0) # 37 + verify_eval("INT_37 < 0x24", 0) # 36 + verify_eval("HEX_0X37 < 56", 2) # 0x38 + verify_eval("HEX_0X37 < 55", 0) # 0x37 + verify_eval("HEX_0X37 < 54", 0) # 0x36 + + # Other int comparisons + verify_eval("INT_37 <= 38", 2) + verify_eval("INT_37 <= 37", 2) + verify_eval("INT_37 <= 36", 0) + verify_eval("INT_37 > 38", 0) + verify_eval("INT_37 > 37", 0) + verify_eval("INT_37 > 36", 2) + verify_eval("INT_37 >= 38", 0) + verify_eval("INT_37 >= 37", 2) + verify_eval("INT_37 >= 36", 2) + + # Other hex comparisons + verify_eval("HEX_0X37 <= 0x38", 2) + verify_eval("HEX_0X37 <= 0x37", 2) + verify_eval("HEX_0X37 <= 0x36", 0) + verify_eval("HEX_0X37 > 0x38", 0) + verify_eval("HEX_0X37 > 0x37", 0) + verify_eval("HEX_0X37 > 0x36", 2) + verify_eval("HEX_0X37 >= 0x38", 0) + verify_eval("HEX_0X37 >= 0x37", 2) + verify_eval("HEX_0X37 >= 0x36", 2) + + # A hex holding a value without a "0x" prefix should still be treated as + # hexadecimal + verify_eval("HEX_37 < 0x38", 2) + verify_eval("HEX_37 < 0x37", 0) + verify_eval("HEX_37 < 0x36", 0) + + # Symbol comparisons + verify_eval("INT_37 < HEX_0X37", 2) + verify_eval("INT_37 > HEX_0X37", 0) + verify_eval("HEX_0X37 < INT_37 ", 0) + verify_eval("HEX_0X37 > INT_37 ", 2) + verify_eval("INT_37 < INT_37 ", 0) + verify_eval("INT_37 <= INT_37 ", 2) + verify_eval("INT_37 > INT_37 ", 0) + verify_eval("INT_37 <= INT_37 ", 2) + + # Tristate value comparisons + verify_eval("n < n", 0) + verify_eval("n < m", 2) + verify_eval("n < y", 2) + verify_eval("n < N", 0) + verify_eval("n < M", 2) + verify_eval("n < Y", 2) + verify_eval("0 > n", 0) + verify_eval("1 > n", 2) + verify_eval("2 > n", 2) + verify_eval("m < n", 0) + verify_eval("m < m", 0) + verify_eval("m < y", 2) + + # Strings compare lexicographically + verify_eval("'aa' < 'ab'", 2) + verify_eval("'aa' > 'ab'", 0) + verify_eval("'ab' < 'aa'", 0) + verify_eval("'ab' > 'aa'", 2) + + # Comparisons where one of the operands doesn't parse as a number also give + # a lexicographic comparison + verify_eval("INT_37 < '37a' ", 2) + verify_eval("'37a' > INT_37", 2) + verify_eval("INT_37 <= '37a' ", 2) + verify_eval("'37a' >= INT_37", 2) + verify_eval("INT_37 >= '37a' ", 0) + verify_eval("INT_37 > '37a' ", 0) + verify_eval("'37a' < INT_37", 0) + verify_eval("'37a' <= INT_37", 0) + + def verify_eval_bad(expr): + try: + c.eval_string(expr) + except KconfigError: + pass + else: + fail('expected eval_string("{}") to throw KconfigError, ' + "didn't".format(expr)) + + # Verify that some bad stuff throws KconfigError's + verify_eval_bad("") + verify_eval_bad("&") + verify_eval_bad("|") + verify_eval_bad("!") + verify_eval_bad("(") + verify_eval_bad(")") + verify_eval_bad("=") + verify_eval_bad("(X") + verify_eval_bad("X)") + verify_eval_bad("X X") + verify_eval_bad("!X X") + verify_eval_bad("X !X") + verify_eval_bad("(X) X") + verify_eval_bad("X &&") + verify_eval_bad("&& X") + verify_eval_bad("X && && X") + verify_eval_bad("X && !&&") + verify_eval_bad("X ||") + verify_eval_bad("|| X") + + + print("Testing Symbol.__str__()/custom_str() and def_{int,hex,string}") + + def verify_str(item, s): + verify_equal(str(item), s[1:-1]) + + def verify_custom_str(item, s): + verify_equal(item.custom_str(lambda sc: "[{}]".format(sc.name)), + s[1:-1]) + + c = Kconfig("Kconfiglib/tests/Kstr", warn=False) + + c.modules.set_value(2) + + verify_str(c.syms["UNDEFINED"], """ +""") + + verify_str(c.syms["BASIC_NO_PROMPT"], """ +config BASIC_NO_PROMPT + bool + help + blah blah + + blah blah blah + + blah +""") + + verify_str(c.syms["BASIC_PROMPT"], """ +config BASIC_PROMPT + bool "basic" +""") + + verify_str(c.syms["ADVANCED"], """ +config ADVANCED + tristate "prompt" if DEP + default DEFAULT_1 + default DEFAULT_2 if DEP + select SELECTED_1 + select SELECTED_2 if DEP + imply IMPLIED_1 + imply IMPLIED_2 if DEP + help + first help text + +config ADVANCED + tristate "prompt 2" + +menuconfig ADVANCED + tristate "prompt 3" + +config ADVANCED + tristate + depends on (A || !B || (C && D) || !(E && F) || G = H || (I && !J && (K || L) && !(M || N) && O = P)) && DEP4 && DEP3 + help + second help text + +config ADVANCED + tristate "prompt 4" if VIS + depends on DEP4 && DEP3 +""") + + verify_custom_str(c.syms["ADVANCED"], """ +config ADVANCED + tristate "prompt" if [DEP] + default [DEFAULT_1] + default [DEFAULT_2] if [DEP] + select [SELECTED_1] + select [SELECTED_2] if [DEP] + imply [IMPLIED_1] + imply [IMPLIED_2] if [DEP] + help + first help text + +config ADVANCED + tristate "prompt 2" + +menuconfig ADVANCED + tristate "prompt 3" + +config ADVANCED + tristate + depends on ([A] || ![B] || ([C] && [D]) || !([E] && [F]) || [G] = [H] || ([I] && ![J] && ([K] || [L]) && !([M] || [N]) && [O] = [P])) && [DEP4] && [DEP3] + help + second help text + +config ADVANCED + tristate "prompt 4" if [VIS] + depends on [DEP4] && [DEP3] +""") + + + verify_str(c.syms["ONLY_DIRECT_DEPS"], """ +config ONLY_DIRECT_DEPS + int + depends on DEP1 && DEP2 +""") + + verify_str(c.syms["STRING"], """ +config STRING + string + default "foo" + default "bar" if DEP + default STRING2 + default STRING3 if DEP +""") + + verify_str(c.syms["INT"], """ +config INT + int + range 1 2 + range FOO BAR + range BAZ QAZ if DEP + default 7 if DEP +""") + + verify_str(c.syms["HEX"], """ +config HEX + hex + range 0x100 0x200 + range FOO BAR + range BAZ QAZ if DEP + default 0x123 +""") + + verify_str(c.modules, """ +config MODULES + bool "MODULES" + option modules +""") + + verify_str(c.syms["OPTIONS"], """ +config OPTIONS + option allnoconfig_y + option defconfig_list + option env="ENV" +""") + + verify_str(c.syms["CORRECT_PROP_LOCS_BOOL"], """ +config CORRECT_PROP_LOCS_BOOL + bool "prompt 1" + default DEFAULT_1 + default DEFAULT_2 + select SELECT_1 + select SELECT_2 + imply IMPLY_1 + imply IMPLY_2 + depends on LOC_1 + help + help 1 + +menuconfig CORRECT_PROP_LOCS_BOOL + bool "prompt 2" + default DEFAULT_3 + default DEFAULT_4 + select SELECT_3 + select SELECT_4 + imply IMPLY_3 + imply IMPLY_4 + depends on LOC_2 + help + help 2 + +config CORRECT_PROP_LOCS_BOOL + bool "prompt 3" + default DEFAULT_5 + default DEFAULT_6 + select SELECT_5 + select SELECT_6 + imply IMPLY_5 + imply IMPLY_6 + depends on LOC_3 + help + help 2 +""") + + verify_str(c.syms["CORRECT_PROP_LOCS_INT"], """ +config CORRECT_PROP_LOCS_INT + int + range 1 2 + range 3 4 + depends on LOC_1 + +config CORRECT_PROP_LOCS_INT + int + range 5 6 + range 7 8 + depends on LOC_2 +""") + + verify_str(c.syms["PROMPT_ONLY"], """ +config PROMPT_ONLY + prompt "prompt only" +""") + + verify_custom_str(c.syms["CORRECT_PROP_LOCS_INT"], """ +config CORRECT_PROP_LOCS_INT + int + range [1] [2] + range [3] [4] + depends on [LOC_1] + +config CORRECT_PROP_LOCS_INT + int + range [5] [6] + range [7] [8] + depends on [LOC_2] +""") + + + + print("Testing Choice.__str__()/custom_str()") + + verify_str(c.named_choices["CHOICE"], """ +choice CHOICE + tristate "foo" + default CHOICE_1 + default CHOICE_2 if dep +""") + + verify_str(c.named_choices["CHOICE"].nodes[0].next.item, """ +choice + tristate "no name" + optional +""") + + verify_str(c.named_choices["CORRECT_PROP_LOCS_CHOICE"], """ +choice CORRECT_PROP_LOCS_CHOICE + bool + default CHOICE_3 + depends on LOC_1 + +choice CORRECT_PROP_LOCS_CHOICE + bool + default CHOICE_4 + depends on LOC_2 + +choice CORRECT_PROP_LOCS_CHOICE + bool + default CHOICE_5 + depends on LOC_3 +""") + + verify_custom_str(c.named_choices["CORRECT_PROP_LOCS_CHOICE"], """ +choice CORRECT_PROP_LOCS_CHOICE + bool + default [CHOICE_3] + depends on [LOC_1] + +choice CORRECT_PROP_LOCS_CHOICE + bool + default [CHOICE_4] + depends on [LOC_2] + +choice CORRECT_PROP_LOCS_CHOICE + bool + default [CHOICE_5] + depends on [LOC_3] +""") + + + print("Testing MenuNode.__str__()/custom_str() for menus and comments") + + verify_str(c.syms["SIMPLE_MENU_HOOK"].nodes[0].next, """ +menu "simple menu" +""") + + verify_str(c.syms["ADVANCED_MENU_HOOK"].nodes[0].next, """ +menu "advanced menu" + depends on A + visible if B && (C || D) +""") + + verify_custom_str(c.syms["ADVANCED_MENU_HOOK"].nodes[0].next, """ +menu "advanced menu" + depends on [A] + visible if [B] && ([C] || [D]) +""") + + verify_str(c.syms["SIMPLE_COMMENT_HOOK"].nodes[0].next, """ +comment "simple comment" +""") + + verify_str(c.syms["ADVANCED_COMMENT_HOOK"].nodes[0].next, """ +comment "advanced comment" + depends on A && B +""") + + verify_custom_str(c.syms["ADVANCED_COMMENT_HOOK"].nodes[0].next, """ +comment "advanced comment" + depends on [A] && [B] +""") + + + print("Testing {MenuNode,Symbol,Choice}.orig_*") + + # Just test some corner cases here re. MenuNode.orig_*. They are already + # indirectly tested above. Use MenuNode.__str__() as a proxy. + + verify_str(c.syms["DEP_REM_CORNER_CASES"], """ +config DEP_REM_CORNER_CASES + bool + default A + depends on n + +config DEP_REM_CORNER_CASES + bool + default B if n + +config DEP_REM_CORNER_CASES + bool + default C + depends on m && MODULES + +config DEP_REM_CORNER_CASES + bool + default D if A + +config DEP_REM_CORNER_CASES + bool + default E if !E1 + default F if F1 = F2 + default G if G1 || H1 + depends on !H + +config DEP_REM_CORNER_CASES + bool + default H + depends on "foo" = "bar" + +config DEP_REM_CORNER_CASES + bool "prompt" if FOO || BAR + depends on BAZ && QAZ +""") + + # Test {Symbol,Choice}.orig_* + + def verify_deps(elms, dep_index, expected): + verify_equal(" ".join(expr_str(elm[dep_index]) for elm in elms), + expected) + + verify_deps(c.syms["BOOL_SYM_ORIG"].orig_defaults, 1, "DEP y y") + verify_deps(c.syms["BOOL_SYM_ORIG"].orig_selects, 1, "y DEP y") + verify_deps(c.syms["BOOL_SYM_ORIG"].orig_implies, 1, "y y DEP") + verify_deps(c.syms["INT_SYM_ORIG"].orig_ranges, 2, "DEP y DEP") + verify_deps(c.named_choices["CHOICE_ORIG"].orig_defaults, 1, "y DEP DEP") + + + print("Testing Symbol.__repr__()") + + def verify_repr(item, s): + verify_equal(repr(item) + "\n", s[1:]) + + c = Kconfig("Kconfiglib/tests/Krepr", warn=False) + + verify_repr(c.n, """ +<symbol n, tristate, value n, constant> +""") + + verify_repr(c.m, """ +<symbol m, tristate, value m, constant> +""") + + verify_repr(c.y, """ +<symbol y, tristate, value y, constant> +""") + + verify_repr(c.syms["UNDEFINED"], """ +<symbol UNDEFINED, unknown, value "UNDEFINED", visibility n, direct deps n, undefined> +""") + + verify_repr(c.syms["BASIC"], """ +<symbol BASIC, bool, value y, visibility n, direct deps y, Kconfiglib/tests/Krepr:9> +""") + + verify_repr(c.syms["VISIBLE"], """ +<symbol VISIBLE, bool, "visible", value n, visibility y, direct deps y, Kconfiglib/tests/Krepr:14> +""") + + c.syms["VISIBLE"].set_value(2) + c.syms["STRING"].set_value("foo") + + verify_repr(c.syms["VISIBLE"], """ +<symbol VISIBLE, bool, "visible", value y, user value y, visibility y, direct deps y, Kconfiglib/tests/Krepr:14> +""") + + verify_repr(c.syms["STRING"], """ +<symbol STRING, string, "visible", value "foo", user value "foo", visibility y, direct deps y, Kconfiglib/tests/Krepr:17> +""") + + verify_repr(c.syms["DIR_DEP_N"], """ +<symbol DIR_DEP_N, unknown, value "DIR_DEP_N", visibility n, direct deps n, Kconfiglib/tests/Krepr:20> +""") + + verify_repr(c.syms["OPTIONS"], """ +<symbol OPTIONS, unknown, value "OPTIONS", visibility n, allnoconfig_y, is the defconfig_list symbol, from environment variable ENV, direct deps y, Kconfiglib/tests/Krepr:23> +""") + + verify_repr(c.syms["MULTI_DEF"], """ +<symbol MULTI_DEF, unknown, value "MULTI_DEF", visibility n, direct deps y, Kconfiglib/tests/Krepr:28, Kconfiglib/tests/Krepr:29> +""") + + verify_repr(c.syms["CHOICE_1"], """ +<symbol CHOICE_1, tristate, "choice sym", value n, visibility m, choice symbol, direct deps m, Kconfiglib/tests/Krepr:36> +""") + + verify_repr(c.modules, """ +<symbol MODULES, bool, value y, visibility n, is the modules symbol, direct deps y, Kconfiglib/tests/Krepr:1> +""") + + + print("Testing Choice.__repr__()") + + verify_repr(c.named_choices["CHOICE"], """ +<choice CHOICE, tristate, "choice", mode m, visibility y, Kconfiglib/tests/Krepr:33> +""") + + c.named_choices["CHOICE"].set_value(2) + + verify_repr(c.named_choices["CHOICE"], """ +<choice CHOICE, tristate, "choice", mode y, user mode y, CHOICE_1 selected, visibility y, Kconfiglib/tests/Krepr:33> +""") + + c.syms["CHOICE_2"].set_value(2) + + verify_repr(c.named_choices["CHOICE"], """ +<choice CHOICE, tristate, "choice", mode y, user mode y, CHOICE_2 selected, CHOICE_2 selected by user, visibility y, Kconfiglib/tests/Krepr:33> +""") + + c.named_choices["CHOICE"].set_value(1) + + verify_repr(c.named_choices["CHOICE"], """ +<choice CHOICE, tristate, "choice", mode m, user mode m, CHOICE_2 selected by user (overridden), visibility y, Kconfiglib/tests/Krepr:33> +""") + + verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next.item, """ +<choice, tristate, "optional choice", mode n, visibility n, optional, Kconfiglib/tests/Krepr:46> +""") + + + print("Testing MenuNode.__repr__()") + + verify_repr(c.syms["BASIC"].nodes[0], """ +<menu node for symbol BASIC, deps y, has help, has next, Kconfiglib/tests/Krepr:9> +""") + + verify_repr(c.syms["DIR_DEP_N"].nodes[0], """ +<menu node for symbol DIR_DEP_N, deps n, has next, Kconfiglib/tests/Krepr:20> +""") + + verify_repr(c.syms["MULTI_DEF"].nodes[0], """ +<menu node for symbol MULTI_DEF, deps y, has next, Kconfiglib/tests/Krepr:28> +""") + + verify_repr(c.syms["MULTI_DEF"].nodes[1], """ +<menu node for symbol MULTI_DEF, deps y, has next, Kconfiglib/tests/Krepr:29> +""") + + verify_repr(c.syms["MENUCONFIG"].nodes[0], """ +<menu node for symbol MENUCONFIG, is menuconfig, deps y, has next, Kconfiglib/tests/Krepr:31> +""") + + verify_repr(c.named_choices["CHOICE"].nodes[0], """ +<menu node for choice CHOICE, prompt "choice" (visibility y), deps y, has child, has next, Kconfiglib/tests/Krepr:33> +""") + + verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next, """ +<menu node for choice, prompt "optional choice" (visibility n), deps y, has next, Kconfiglib/tests/Krepr:46> +""") + + verify_repr(c.syms["NO_VISIBLE_IF_HOOK"].nodes[0].next, """ +<menu node for menu, prompt "no visible if" (visibility y), deps y, 'visible if' deps y, has next, Kconfiglib/tests/Krepr:53> +""") + + verify_repr(c.syms["VISIBLE_IF_HOOK"].nodes[0].next, """ +<menu node for menu, prompt "visible if" (visibility y), deps y, 'visible if' deps m, has next, Kconfiglib/tests/Krepr:58> +""") + + verify_repr(c.syms["COMMENT_HOOK"].nodes[0].next, """ +<menu node for comment, prompt "comment" (visibility y), deps y, Kconfiglib/tests/Krepr:64> +""") + + + print("Testing Kconfig.__repr__()") + + verify_repr(c, """ +<configuration with 15 symbols, main menu prompt "Main menu", srctree is current directory, config symbol prefix "CONFIG_", warnings disabled, printing of warnings to stderr enabled, undef. symbol assignment warnings disabled, overriding symbol assignment warnings enabled, redundant symbol assignment warnings enabled> +""") + + os.environ["srctree"] = "Kconfiglib" + os.environ["CONFIG_"] = "CONFIG_ value" + + c = Kconfig("tests/Krepr", warn=False) + c.warn = True + c.warn_to_stderr = False + c.warn_assign_override = False + c.warn_assign_redun = False + c.warn_assign_undef = True + + verify_repr(c, """ +<configuration with 15 symbols, main menu prompt "Main menu", srctree "Kconfiglib", config symbol prefix "CONFIG_ value", warnings enabled, printing of warnings to stderr disabled, undef. symbol assignment warnings enabled, overriding symbol assignment warnings disabled, redundant symbol assignment warnings disabled> +""") + + os.environ.pop("srctree", None) + os.environ.pop("CONFIG_", None) + + + print("Testing tricky help strings") + + c = Kconfig("Kconfiglib/tests/Khelp") + + def verify_help(node, s): + verify_equal(node.help, s[1:-1]) + + verify_help(c.syms["TWO_HELP_STRINGS"].nodes[0], """ +first help string +""") + + verify_help(c.syms["TWO_HELP_STRINGS"].nodes[1], """ +second help string +""") + + verify_help(c.syms["NO_BLANK_AFTER_HELP"].nodes[0], """ +help for +NO_BLANK_AFTER_HELP +""") + + verify_help(c.named_choices["CHOICE_HELP"].nodes[0], """ +help for +CHOICE_HELP +""") + + verify_help(c.syms["HELP_TERMINATED_BY_COMMENT"].nodes[0], """ +a +b +c +""") + + verify_help(c.syms["TRICKY_HELP"].nodes[0], """ +a + b + c + + d + e + f + + +g + h + i +""") + + + print("Testing locations, source/rsource/gsource/grsource, and " + "Kconfig.kconfig_filenames") + + def verify_locations(nodes, *expected_locs): + verify(len(nodes) == len(expected_locs), + "Wrong number of locations for " + repr(nodes)) + + for node, expected_loc in zip(nodes, expected_locs): + node_loc = "{}:{}".format(node.filename, node.linenr) + verify(node_loc == expected_loc, + "expected {} to have the location {}, had the location {}" + .format(repr(node), expected_loc, node_loc)) + + # Expanded in the 'source' statement in Klocation + + os.environ["TESTS_DIR_FROM_ENV"] = "tests" + os.environ["SUB_DIR_FROM_ENV"] = "sub" + + os.environ["_SOURCED"] = "_sourced" + os.environ["_RSOURCED"] = "_rsourced" + os.environ["_GSOURCED"] = "_gsourced" + os.environ["_GRSOURCED"] = "_grsourced" + + # Test twice, with $srctree as a relative and an absolute path, + # respectively + for srctree in "Kconfiglib", os.path.abspath("Kconfiglib"): + os.environ["srctree"] = srctree + + # Has symbol with empty help text, so disable warnings + c = Kconfig("tests/Klocation", warn=False) + + verify_locations(c.syms["SINGLE_DEF"].nodes, "tests/Klocation:4") + + verify_locations(c.syms["MULTI_DEF"].nodes, + "tests/Klocation:7", + "tests/Klocation:37", + "tests/Klocation:39", + "tests/Klocation_sourced:3", + "tests/sub/Klocation_rsourced:2", + "tests/sub/Klocation_gsourced1:1", + "tests/sub/Klocation_gsourced2:1", + "tests/sub/Klocation_gsourced1:1", + "tests/sub/Klocation_gsourced2:1", + "tests/sub/Klocation_grsourced1:1", + "tests/sub/Klocation_grsourced2:1", + "tests/sub/Klocation_grsourced1:1", + "tests/sub/Klocation_grsourced2:1", + "tests/Klocation:72") + + verify_locations(c.named_choices["CHOICE"].nodes, + "tests/Klocation_sourced:5") + + verify_locations([c.syms["MENU_HOOK"].nodes[0].next], + "tests/Klocation_sourced:12") + + verify_locations([c.syms["COMMENT_HOOK"].nodes[0].next], + "tests/Klocation_sourced:18") + + # Test Kconfig.kconfig_filenames + + verify_equal(c.kconfig_filenames, [ + "tests/Klocation", + "tests/Klocation_sourced", + "tests/sub/Klocation_rsourced", + "tests/sub/Klocation_gsourced1", + "tests/sub/Klocation_gsourced2", + "tests/sub/Klocation_gsourced1", + "tests/sub/Klocation_gsourced2", + "tests/sub/Klocation_grsourced1", + "tests/sub/Klocation_grsourced2", + "tests/sub/Klocation_grsourced1", + "tests/sub/Klocation_grsourced2" + ]) + + # Test recursive 'source' detection + + try: + Kconfig("tests/Krecursive1") + except KconfigError as e: + verify_equal(str(e), """ +tests/Krecursive2:1: recursive 'source' of 'tests/Krecursive1' detected. Check that environment variables are set correctly. +Include path: +tests/Krecursive1:1 +tests/Krecursive2:1 +"""[:-1]) + except: + fail("recursive 'source' raised wrong exception") + else: + fail("recursive 'source' did not raise exception") + + # Verify that source and rsource throw exceptions for missing files + + # TODO: Make an exception test helper + + try: + Kconfig("tests/Kmissingsource") + except KconfigError as e: + if "not found" not in str(e): + fail("'source' with missing file raised wrong KconfigError") + except: + fail("'source' with missing file raised wrong exception") + else: + fail("'source' with missing file did not raise exception") + + try: + Kconfig("tests/Kmissingrsource") + except KconfigError as e: + if "not found" not in str(e): + fail("'rsource' with missing file raised wrong KconfigError") + except: + fail("'rsource' with missing file raised wrong exception") + else: + fail("'rsource' with missing file did not raise exception") + + # Test a tricky case involving symlinks. $srctree is tests/symlink, which + # points to tests/sub/sub, meaning tests/symlink/.. != tests/. Previously, + # using 'rsource' from a file sourced with an absolute path triggered an + # unsafe relpath() with tests/symlink/.. in it, crashing. + + os.environ["srctree"] = "Kconfiglib/tests/symlink" + os.environ["KCONFIG_SYMLINK_2"] = os.path.abspath( + "Kconfiglib/tests/sub/Kconfig_symlink_2") + if not os.path.isabs( + Kconfig("Kconfig_symlink_1").syms["FOUNDME"].nodes[0].filename): + + fail("Symlink + rsource issues") + + + print("Testing Kconfig.node_iter()") + + # Reuse tests/Klocation. The node_iter(unique_syms=True) case already gets + # plenty of testing from write_config() as well. + + os.environ["srctree"] = "Kconfiglib" + c = Kconfig("tests/Klocation", warn=False) + + verify_equal( + [node.item.name for node in c.node_iter() + if isinstance(node.item, Symbol)], + ["SINGLE_DEF", "MULTI_DEF", "HELP_1", "HELP_2", "HELP_3", "MULTI_DEF", + "MULTI_DEF", "MULTI_DEF", "MENU_HOOK", "COMMENT_HOOK"] + \ + 10*["MULTI_DEF"]) + + verify_equal( + [node.item.name for node in c.node_iter(True) + if isinstance(node.item, Symbol)], + ["SINGLE_DEF", "MULTI_DEF", "HELP_1", "HELP_2", "HELP_3", "MENU_HOOK", + "COMMENT_HOOK"]) + + verify_equal( + [node.prompt[0] for node in c.node_iter() + if not isinstance(node.item, Symbol)], + ["choice", "menu", "comment"]) + + verify_equal( + [node.prompt[0] for node in c.node_iter(True) + if not isinstance(node.item, Symbol)], + ["choice", "menu", "comment"]) + + + print("Testing MenuNode.include_path") + + os.environ["srctree"] = "Kconfiglib/tests" + + c = Kconfig("Kinclude_path") + + def verify_node_path(node, *expected): + if node.include_path != expected: + fail("Wrong include path for node {!r}. Got {}, expected {}." + .format(node, node.include_path, expected)) + + def verify_sym_path(sym_name, node_i, *expected): + verify_node_path(c.syms[sym_name].nodes[node_i], *expected) + + verify_sym_path("TOP", 0) + verify_sym_path("TOP", 1) + verify_sym_path("TOP", 2) + + verify_sym_path("ONE_DOWN", 0, ("Kinclude_path", 4)) + verify_sym_path("ONE_DOWN", 1, ("Kinclude_path", 4)) + verify_sym_path("ONE_DOWN", 2, ("Kinclude_path", 4)) + verify_sym_path("ONE_DOWN", 3, ("Kinclude_path", 9)) + verify_sym_path("ONE_DOWN", 4, ("Kinclude_path", 9)) + verify_sym_path("ONE_DOWN", 5, ("Kinclude_path", 9)) + + verify_sym_path("TWO_DOWN", 0, + ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4)) + verify_sym_path("TWO_DOWN", 1, + ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 9)) + verify_sym_path("TWO_DOWN", 2, + ("Kinclude_path", 9), ("Kinclude_path_sourced_1", 4)) + verify_sym_path("TWO_DOWN", 3, + ("Kinclude_path", 9), ("Kinclude_path_sourced_1", 9)) + + verify_node_path(c.top_node) + verify_node_path(c.menus[0], ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4)) + verify_node_path(c.comments[0], ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4)) + verify_node_path(c.choices[0].nodes[0], ("Kinclude_path", 4), ("Kinclude_path_sourced_1", 4)) + + os.environ.pop("srctree", None) + + + print("Testing Kconfig.choices/menus/comments") + + c = Kconfig("Kconfiglib/tests/Kitemlists") + + def verify_prompts(items, *expected_prompts): + verify(len(items) == len(expected_prompts), + "Wrong number of prompts for {}".format(items)) + + for item, expected_prompt in zip(items, expected_prompts): + if not isinstance(item, MenuNode): + item = item.nodes[0] + + verify(item.prompt[0] == expected_prompt, + "Wrong prompt for {}, expected '{}'" + .format(repr(item), expected_prompt)) + + verify_prompts(c.choices, "choice 1", "choice 2", "choice 3", "choice 2") + verify_prompts(c.menus, "menu 1", "menu 2", "menu 3", "menu 4", "menu 5") + verify_prompts(c.comments, "comment 1", "comment 2", "comment 3") + + + print("Testing Symbol/Choice.direct_dep") + + c = Kconfig("Kconfiglib/tests/Kdirdep") + + verify_equal(expr_str(c.syms["NO_DEP_SYM"].direct_dep), 'y') + verify_equal(expr_str(c.syms["DEP_SYM"].direct_dep), "A || (B && C) || !D") + + verify_equal(expr_str(c.named_choices["NO_DEP_CHOICE"].direct_dep), 'y') + verify_equal(expr_str(c.named_choices["DEP_CHOICE"].direct_dep), + "A || B || C") + + + print("Testing expr_items()") + + c = Kconfig("Kconfiglib/tests/Kexpr_items") + + def verify_expr_items(expr, *sym_names): + verify_equal(tuple(sorted(item.name for item in expr_items(expr))), + sym_names) + + verify_expr_items( + c.syms["TEST"].defaults[0][0], + "A", "B", "C", "D", "E", "F", "G", "H" + ) + + verify_expr_items( + c.syms["TEST_CHOICE"].nodes[0].prompt[1], + "A", "CHOICE" + ) + + + print("Testing MenuNode/Symbol/Choice.referenced") + + c = Kconfig("Kconfiglib/tests/Kreferenced", warn=False) + + def verify_deps(item, *dep_names): + verify_equal(tuple(sorted(item.name for item in item.referenced)), + dep_names) + + verify_deps(c.top_node, "y") + + verify_deps(c.syms["NO_REFS"].nodes[0], "y") + + verify_deps(c.syms["JUST_DEPENDS_ON_REFS"].nodes[0], "A", "B") + + verify_deps(c.syms["LOTS_OF_REFS"].nodes[0], + *(chr(n) for n in range(ord("A"), ord("Z") + 1))) + + verify_deps(c.syms["INT_REFS"].nodes[0], + "A", "B", "C", "D", "E", "F", "G", "H", "y") + + verify_deps(c.syms["CHOICE_REF"].nodes[0], "CHOICE") + + verify_deps(c.menus[0], "A", "B", "C", "D") + + verify_deps(c.comments[0], "A", "B") + + verify_deps(c.syms["MULTI_DEF_SYM"], "A", "B", "C", "y") + verify_deps(c.named_choices["MULTI_DEF_CHOICE"], "A", "B", "C") + + + print("Testing split_expr()") + + c = Kconfig("Kconfiglib/tests/empty") + c.warn = False + + def verify_split(to_split, op, operand_strs): + # The same hackage as in Kconfig.eval_string() + c._tokens = c._tokenize("if " + to_split)[1:] + c._tokens_i = 0 + + operands = split_expr(c._parse_expr(False), op) + + verify(len(operands) == len(operand_strs), + "Wrong number of operands when {} was split by {}" + .format(to_split, "OR" if op == OR else "AND")) + + for operand, operand_str in zip(operands, operand_strs): + verify_equal(expr_str(operand), operand_str) + + verify_split("A", OR, ("A", )) + verify_split("!A", OR, ("!A", )) + verify_split("A = B", OR, ("A = B", )) + verify_split("A && B", OR, ("A && B", )) + verify_split("A || B", OR, ("A", "B" )) + verify_split("(A || B) || C", OR, ("A", "B", "C" )) + verify_split("A || (B || C)", OR, ("A", "B", "C" )) + verify_split("A || !(B || C)", OR, ("A", "!(B || C)" )) + verify_split("A || (B && (C || D))", OR, ("A", "B && (C || D)")) + verify_split("(A && (B || C)) || D", OR, ("A && (B || C)", "D")) + + verify_split("A", AND, ("A", )) + verify_split("!A", AND, ("!A", )) + verify_split("A = B", AND, ("A = B", )) + verify_split("A || B", AND, ("A || B", )) + verify_split("A && B", AND, ("A", "B" )) + verify_split("(A && B) && C", AND, ("A", "B", "C" )) + verify_split("A && (B && C)", AND, ("A", "B", "C" )) + verify_split("A && !(B && C)", AND, ("A", "!(B && C)" )) + verify_split("A && (B || (C && D))", AND, ("A", "B || (C && D)")) + verify_split("(A || (B && C)) && D", AND, ("A || (B && C)", "D")) + + + print("Testing visibility") + + c = Kconfig("Kconfiglib/tests/Kvisibility") + + def verify_visibility(item, no_module_vis, module_vis): + c.modules.set_value(0) + verify(item.visibility == no_module_vis, + "expected {} to have visibility {} without modules, had " + "visibility {}". + format(repr(item), no_module_vis, item.visibility)) + + c.modules.set_value(2) + verify(item.visibility == module_vis, + "expected {} to have visibility {} with modules, had " + "visibility {}". + format(repr(item), module_vis, item.visibility)) + + # Symbol visibility + + verify_visibility(c.syms["NO_PROMPT"], 0, 0) + verify_visibility(c.syms["BOOL_N"], 0, 0) + verify_visibility(c.syms["BOOL_M"], 0, 2) + verify_visibility(c.syms["BOOL_MOD"], 2, 2) + verify_visibility(c.syms["BOOL_Y"], 2, 2) + verify_visibility(c.syms["TRISTATE_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_MOD"], 2, 1) + verify_visibility(c.syms["TRISTATE_Y"], 2, 2) + verify_visibility(c.syms["BOOL_IF_N"], 0, 0) + verify_visibility(c.syms["BOOL_IF_M"], 0, 2) + verify_visibility(c.syms["BOOL_IF_Y"], 2, 2) + verify_visibility(c.syms["BOOL_MENU_N"], 0, 0) + verify_visibility(c.syms["BOOL_MENU_M"], 0, 2) + verify_visibility(c.syms["BOOL_MENU_Y"], 2, 2) + verify_visibility(c.syms["BOOL_CHOICE_N"], 0, 0) + + # Non-tristate symbols in tristate choices are only visible if the choice + # is in y mode + + # The choice can't be brought to y mode because of the 'if m' + verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0) + c.syms["BOOL_CHOICE_M"].choice.set_value(2) + verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0) + + # The choice gets y mode only when running without modules, because it + # defaults to m mode + verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 0) + c.syms["BOOL_CHOICE_Y"].choice.set_value(2) + # When set to y mode, the choice symbol becomes visible both with and + # without modules + verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 2) + + verify_visibility(c.syms["TRISTATE_IF_N"], 0, 0) + verify_visibility(c.syms["TRISTATE_IF_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_IF_Y"], 2, 2) + verify_visibility(c.syms["TRISTATE_MENU_N"], 0, 0) + verify_visibility(c.syms["TRISTATE_MENU_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_MENU_Y"], 2, 2) + verify_visibility(c.syms["TRISTATE_CHOICE_N"], 0, 0) + verify_visibility(c.syms["TRISTATE_CHOICE_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_CHOICE_Y"], 2, 2) + + verify_visibility(c.named_choices["BOOL_CHOICE_N"], 0, 0) + verify_visibility(c.named_choices["BOOL_CHOICE_M"], 0, 2) + verify_visibility(c.named_choices["BOOL_CHOICE_Y"], 2, 2) + verify_visibility(c.named_choices["TRISTATE_CHOICE_N"], 0, 0) + verify_visibility(c.named_choices["TRISTATE_CHOICE_M"], 0, 1) + verify_visibility(c.named_choices["TRISTATE_CHOICE_Y"], 2, 2) + + verify_visibility(c.named_choices["TRISTATE_CHOICE_IF_M_AND_Y"], 0, 1) + verify_visibility(c.named_choices["TRISTATE_CHOICE_MENU_N_AND_Y"], 0, 0) + + # Verify that 'visible if' visibility gets propagated to prompts + + verify_visibility(c.syms["VISIBLE_IF_N"], 0, 0) + verify_visibility(c.syms["VISIBLE_IF_M"], 0, 1) + verify_visibility(c.syms["VISIBLE_IF_Y"], 2, 2) + verify_visibility(c.syms["VISIBLE_IF_M_2"], 0, 1) + + # Verify that string/int/hex symbols with m visibility accept a user value + + assign_and_verify("STRING_m", "foo bar") + assign_and_verify("INT_m", "123") + assign_and_verify("HEX_m", "0x123") + + + print("Testing .assignable") + + c = Kconfig("Kconfiglib/tests/Kassignable") + + def verify_assignable_imp(item, assignable_no_modules, assignable_modules): + # Verifies the assignable values for 'item', with and without modules. + + for modules_val, assignable in (0, assignable_no_modules), \ + (2, assignable_modules): + + c.modules.set_value(modules_val) + module_msg = "without modules" if modules_val == 0 else \ + "with modules" + + verify(item.assignable == assignable, + "Incorrect assignable values for {} {}. Should be {}, " + "was {}." + .format(item.name, module_msg, assignable, item.assignable)) + + # Verify that the values can actually be assigned too + + for val in item.assignable: + item.set_value(val) + verify(item.tri_value == val, + "Unable to set {} to {} {}, even though it was in " + ".assignable".format(item.name, val, module_msg)) + + def verify_assignable(sym_name, assignable_no_modules, assignable_modules): + verify_assignable_imp(c.syms[sym_name], + assignable_no_modules, + assignable_modules) + + def verify_const_unassignable(sym_name): + verify_assignable_imp(c.const_syms[sym_name], (), ()) + + # Things that shouldn't be .assignable + verify_const_unassignable("n") + verify_const_unassignable("m") + verify_const_unassignable("y") + verify_const_unassignable("const") + verify_assignable("UNDEFINED", (), ()) + verify_assignable("NO_PROMPT", (), ()) + verify_assignable("STRING", (), ()) + verify_assignable("INT", (), ()) + verify_assignable("HEX", (), ()) + + # Non-selected symbols + verify_assignable("Y_VIS_BOOL", (0, 2), (0, 2)) + verify_assignable("M_VIS_BOOL", ( ), (0, 2)) # Vis. promoted + verify_assignable("N_VIS_BOOL", ( ), ( )) + verify_assignable("Y_VIS_TRI", (0, 2), (0, 1, 2)) + verify_assignable("M_VIS_TRI", ( ), (0, 1 )) + verify_assignable("N_VIS_TRI", ( ), ( )) + + # Symbols selected to y + verify_assignable("Y_SEL_Y_VIS_BOOL", (2,), (2,)) + verify_assignable("Y_SEL_M_VIS_BOOL", ( ), (2,)) # Vis. promoted + verify_assignable("Y_SEL_N_VIS_BOOL", ( ), ( )) + verify_assignable("Y_SEL_Y_VIS_TRI", (2,), (2,)) + verify_assignable("Y_SEL_M_VIS_TRI", ( ), (2,)) + verify_assignable("Y_SEL_N_VIS_TRI", ( ), ( )) + + # Symbols selected to m + verify_assignable("M_SEL_Y_VIS_BOOL", (2,), ( 2,)) # Value promoted + verify_assignable("M_SEL_M_VIS_BOOL", ( ), ( 2,)) # Vis./value promoted + verify_assignable("M_SEL_N_VIS_BOOL", ( ), ( )) + verify_assignable("M_SEL_Y_VIS_TRI", (2,), (1, 2 )) + verify_assignable("M_SEL_M_VIS_TRI", ( ), (1, )) + verify_assignable("M_SEL_N_VIS_TRI", ( ), ( )) + + # Symbols implied to y + verify_assignable("Y_IMP_Y_VIS_BOOL", (0, 2), (0, 2)) + verify_assignable("Y_IMP_M_VIS_BOOL", ( ), (0, 2)) # Vis. promoted + verify_assignable("Y_IMP_N_VIS_BOOL", ( ), ( )) + verify_assignable("Y_IMP_Y_VIS_TRI", (0, 2), (0, 2)) # m removed by imply + verify_assignable("Y_IMP_M_VIS_TRI", ( ), (0, 2)) # m promoted to y by imply + verify_assignable("Y_IMP_N_VIS_TRI", ( ), ( )) + + # Symbols implied to m (never affects assignable values) + verify_assignable("M_IMP_Y_VIS_BOOL", (0, 2), (0, 2)) + verify_assignable("M_IMP_M_VIS_BOOL", ( ), (0, 2)) # Vis. promoted + verify_assignable("M_IMP_N_VIS_BOOL", ( ), ( )) + verify_assignable("M_IMP_Y_VIS_TRI", (0, 2), (0, 1, 2)) + verify_assignable("M_IMP_M_VIS_TRI", ( ), (0, 1 )) + verify_assignable("M_IMP_N_VIS_TRI", ( ), ( )) + + # Symbols in y-mode choice + verify_assignable("Y_CHOICE_BOOL", (2,), (2,)) + verify_assignable("Y_CHOICE_TRISTATE", (2,), (2,)) + verify_assignable("Y_CHOICE_N_VIS_TRISTATE", ( ), ( )) + + # Symbols in m/y-mode choice, starting out in m mode, or y mode when + # running without modules + verify_assignable("MY_CHOICE_BOOL", (2,), ( )) + verify_assignable("MY_CHOICE_TRISTATE", (2,), (0, 1)) + verify_assignable("MY_CHOICE_N_VIS_TRISTATE", ( ), ( )) + + c.named_choices["MY_CHOICE"].set_value(2) + + # Symbols in m/y-mode choice, now in y mode + verify_assignable("MY_CHOICE_BOOL", (2,), (2,)) + verify_assignable("MY_CHOICE_TRISTATE", (2,), (2,)) + verify_assignable("MY_CHOICE_N_VIS_TRISTATE", ( ), ( )) + + def verify_choice_assignable(choice_name, assignable_no_modules, + assignable_modules): + verify_assignable_imp(c.named_choices[choice_name], + assignable_no_modules, + assignable_modules) + + # Choices with various possible modes + verify_choice_assignable("Y_CHOICE", (2, ), ( 2,)) + verify_choice_assignable("MY_CHOICE", (2, ), ( 1, 2 )) + verify_choice_assignable("NMY_CHOICE", (0, 2), (0, 1, 2 )) + verify_choice_assignable("NY_CHOICE", (0, 2), (0, 2 )) + verify_choice_assignable("NM_CHOICE", ( ), (0, 1 )) + verify_choice_assignable("M_CHOICE", ( ), ( 1, )) + verify_choice_assignable("N_CHOICE", ( ), ( )) + + + print("Testing object relations") + + c = Kconfig("Kconfiglib/tests/Krelation") + + verify(c.syms["A"].nodes[0].parent is c.top_node, + "A's parent should be the top node") + + verify(c.syms["B"].nodes[0].parent.item is c.named_choices["CHOICE_1"], + "B's parent should be the first choice") + + verify(c.syms["C"].nodes[0].parent.item is c.syms["B"], + "C's parent should be B (due to auto menus)") + + verify(c.syms["E"].nodes[0].parent.item == MENU, + "E's parent should be a menu") + + verify(c.syms["E"].nodes[0].parent.parent is c.top_node, + "E's grandparent should be the top node") + + verify(c.syms["G"].nodes[0].parent.item is c.named_choices["CHOICE_2"], + "G's parent should be the second choice") + + verify(c.syms["G"].nodes[0].parent.parent.item == MENU, + "G's grandparent should be a menu") + + + print("Testing hex/int ranges") + + c = Kconfig("Kconfiglib/tests/Krange", warn=False) + + for sym_name in "HEX_NO_RANGE", "INT_NO_RANGE", "HEX_40", "INT_40": + sym = c.syms[sym_name] + verify(not sym.ranges, + "{} should not have ranges".format(sym_name)) + + for sym_name in "HEX_ALL_RANGES_DISABLED", "INT_ALL_RANGES_DISABLED", \ + "HEX_RANGE_10_20_LOW_DEFAULT", \ + "INT_RANGE_10_20_LOW_DEFAULT": + sym = c.syms[sym_name] + verify(sym.ranges, "{} should have ranges".format(sym_name)) + + # hex/int symbols without defaults should get no default value + verify_value("HEX_NO_RANGE", "") + verify_value("INT_NO_RANGE", "") + # And neither if all ranges are disabled + verify_value("HEX_ALL_RANGES_DISABLED", "") + verify_value("INT_ALL_RANGES_DISABLED", "") + # Make sure they are assignable though, and test that the form of the user + # value is reflected in the value for hex symbols + assign_and_verify("HEX_NO_RANGE", "0x123") + assign_and_verify("HEX_NO_RANGE", "123") + assign_and_verify("INT_NO_RANGE", "123") + + # Defaults outside of the valid range should be clamped + verify_value("HEX_RANGE_10_20_LOW_DEFAULT", "0x10") + verify_value("HEX_RANGE_10_20_HIGH_DEFAULT", "0x20") + verify_value("INT_RANGE_10_20_LOW_DEFAULT", "10") + verify_value("INT_RANGE_10_20_HIGH_DEFAULT", "20") + # Defaults inside the valid range should be preserved. For hex symbols, + # they should additionally use the same form as in the assignment. + verify_value("HEX_RANGE_10_20_OK_DEFAULT", "0x15") + verify_value("HEX_RANGE_10_20_OK_DEFAULT_ALTERNATE", "15") + verify_value("INT_RANGE_10_20_OK_DEFAULT", "15") + + # hex/int symbols with no defaults but valid ranges should default to the + # lower end of the range if it's > 0 + verify_value("HEX_RANGE_10_20", "0x10") + verify_value("HEX_RANGE_0_10", "") + verify_value("INT_RANGE_10_20", "10") + verify_value("INT_RANGE_0_10", "") + verify_value("INT_RANGE_NEG_10_10", "") + + # User values and dependent ranges + + # Avoid warnings for assigning values outside the active range + c.warn = False + + def verify_range(sym_name, low, high, default): + # Verifies that all values in the range 'low'-'high' can be assigned, + # and that assigning values outside the range reverts the value back to + # 'default' (None if it should revert back to ""). + + is_hex = (c.syms[sym_name].type == HEX) + + for i in range(low, high + 1): + assign_and_verify_user_value(sym_name, str(i), str(i), True) + if is_hex: + # The form of the user value should be preserved for hex + # symbols + assign_and_verify_user_value(sym_name, hex(i), hex(i), True) + + # Verify that assigning a user value just outside the range causes + # defaults to be used + + if default is None: + default_str = "" + else: + default_str = hex(default) if is_hex else str(default) + + if is_hex: + too_low_str = hex(low - 1) + too_high_str = hex(high + 1) + else: + too_low_str = str(low - 1) + too_high_str = str(high + 1) + + assign_and_verify_value(sym_name, too_low_str, default_str) + assign_and_verify_value(sym_name, too_high_str, default_str) + + verify_range("HEX_RANGE_10_20_LOW_DEFAULT", 0x10, 0x20, 0x10) + verify_range("HEX_RANGE_10_20_HIGH_DEFAULT", 0x10, 0x20, 0x20) + verify_range("HEX_RANGE_10_20_OK_DEFAULT", 0x10, 0x20, 0x15) + + verify_range("INT_RANGE_10_20_LOW_DEFAULT", 10, 20, 10) + verify_range("INT_RANGE_10_20_HIGH_DEFAULT", 10, 20, 20) + verify_range("INT_RANGE_10_20_OK_DEFAULT", 10, 20, 15) + + verify_range("HEX_RANGE_10_20", 0x10, 0x20, 0x10) + + verify_range("INT_RANGE_10_20", 10, 20, 10) + verify_range("INT_RANGE_0_10", 0, 10, None) + verify_range("INT_RANGE_NEG_10_10", -10, 10, None) + + # Dependent ranges + + verify_value("HEX_40", "40") + verify_value("INT_40", "40") + + c.syms["HEX_RANGE_10_20"].unset_value() + c.syms["INT_RANGE_10_20"].unset_value() + verify_value("HEX_RANGE_10_40_DEPENDENT", "0x10") + verify_value("INT_RANGE_10_40_DEPENDENT", "10") + c.syms["HEX_RANGE_10_20"].set_value("15") + c.syms["INT_RANGE_10_20"].set_value("15") + verify_value("HEX_RANGE_10_40_DEPENDENT", "0x15") + verify_value("INT_RANGE_10_40_DEPENDENT", "15") + c.unset_values() + verify_range("HEX_RANGE_10_40_DEPENDENT", 0x10, 0x40, 0x10) + verify_range("INT_RANGE_10_40_DEPENDENT", 10, 40, 10) + + # Ranges and symbols defined in multiple locations + + verify_value("INACTIVE_RANGE", "2") + verify_value("ACTIVE_RANGE", "1") + + + print("Testing defconfig_filename") + + c = Kconfig("Kconfiglib/tests/empty") + verify(c.defconfig_filename is None, + "defconfig_filename should be None with no defconfig_list symbol") + + c = Kconfig("Kconfiglib/tests/Kdefconfig_nonexistent") + verify(c.defconfig_filename is None, + "defconfig_filename should be None when none of the files in the " + "defconfig_list symbol exist") + + # Referenced in Kdefconfig_existent(_but_n) + os.environ["FOO"] = "defconfig_2" + + c = Kconfig("Kconfiglib/tests/Kdefconfig_existent_but_n") + verify(c.defconfig_filename is None, + "defconfig_filename should be None when the condition is n for all " + "the defaults") + + c = Kconfig("Kconfiglib/tests/Kdefconfig_existent") + verify(c.defconfig_filename == "Kconfiglib/tests/defconfig_2", + "defconfig_filename should return the existing file " + "Kconfiglib/tests/defconfig_2") + + # Should also look relative to $srctree if the specified defconfig is a + # relative path and can't be opened + + c = Kconfig("Kconfiglib/tests/Kdefconfig_srctree") + verify(c.defconfig_filename == "Kconfiglib/tests/defconfig_2", + "defconfig_filename gave wrong file with $srctree unset") + + os.environ["srctree"] = "Kconfiglib/tests" + c = Kconfig("Kdefconfig_srctree") + verify(c.defconfig_filename == "Kconfiglib/tests/sub/defconfig_in_sub", + "defconfig_filename gave wrong file with $srctree set") + + os.environ.pop("srctree", None) + + + print("Testing mainmenu_text") + + c = Kconfig("Kconfiglib/tests/empty") + verify(c.mainmenu_text == "Main menu", + "An empty Kconfig should get a default main menu prompt") + + # Expanded in the mainmenu text + os.environ["FOO"] = "bar baz" + c = Kconfig("Kconfiglib/tests/Kmainmenu") + verify(c.mainmenu_text == "---bar baz---", + "Wrong mainmenu text") + + + print("Testing user_value") + + # References undefined env. var. Disable warnings. + c = Kconfig("Kconfiglib/tests/Kmisc", warn=False) + + # Avoid warnings from assigning invalid user values and assigning user + # values to symbols without prompts + c.warn = False + + syms = [c.syms[name] for name in + ("BOOL", "TRISTATE", "STRING", "INT", "HEX")] + + for sym in syms: + verify(sym.user_value is None, + "{} should not have a user value to begin with") + + # Assign valid values for the types + + assign_and_verify_user_value("BOOL", 0, 0, True) + assign_and_verify_user_value("BOOL", 2, 2, True) + assign_and_verify_user_value("TRISTATE", 0, 0, True) + assign_and_verify_user_value("TRISTATE", 1, 1, True) + assign_and_verify_user_value("TRISTATE", 2, 2, True) + assign_and_verify_user_value("STRING", "foo bar", "foo bar", True) + assign_and_verify_user_value("INT", "123", "123", True) + assign_and_verify_user_value("HEX", "0x123", "0x123", True) + + # Assign invalid values for the types. They should retain their old user + # value. + + assign_and_verify_user_value("BOOL", 1, 2, False) + assign_and_verify_user_value("BOOL", "foo", 2, False) + assign_and_verify_user_value("BOOL", "1", 2, False) + assign_and_verify_user_value("TRISTATE", "foo", 2, False) + assign_and_verify_user_value("TRISTATE", "1", 2, False) + assign_and_verify_user_value("STRING", 0, "foo bar", False) + assign_and_verify_user_value("INT", "foo", "123", False) + assign_and_verify_user_value("INT", 0, "123", False) + assign_and_verify_user_value("HEX", "foo", "0x123", False) + assign_and_verify_user_value("HEX", 0, "0x123", False) + assign_and_verify_user_value("HEX", "-0x1", "0x123", False) + + for s in syms: + s.unset_value() + verify(s.user_value is None, + "{} should not have a user value after being reset". + format(s.name)) + + + print("Testing is_menuconfig") + + c = Kconfig("Kconfiglib/tests/Kmenuconfig") + + for not_menuconfig in c.syms["NOT_MENUCONFIG_1"].nodes[0], \ + c.syms["NOT_MENUCONFIG_2"].nodes[0], \ + c.syms["MENUCONFIG_MULTI_DEF"].nodes[0], \ + c.syms["COMMENT_HOOK"].nodes[0].next: + + verify(not not_menuconfig.is_menuconfig, + "'{}' should have is_menuconfig False".format(not_menuconfig)) + + for menuconfig in c.top_node, \ + c.syms["MENUCONFIG_1"].nodes[0], \ + c.syms["MENUCONFIG_MULTI_DEF"].nodes[1], \ + c.syms["MENU_HOOK"].nodes[0].next, \ + c.syms["CHOICE_HOOK"].nodes[0].next: + + verify(menuconfig.is_menuconfig, + "'{}' should have is_menuconfig True".format(menuconfig)) + + + print("Testing 'option env' semantics") + + os.environ["ENV_VAR"] = "ENV_VAR value" + + # References undefined env. var., so disable warnings + c = Kconfig("Kconfiglib/tests/Kmisc", warn=False) + + # Verify that 'option env' is treated like a default + verify_value("FROM_ENV", "ENV_VAR value") + verify_value("FROM_ENV_MISSING", "missing") + + verify_value("FROM_ENV_WEIRD", "weird") + + + print("Testing defined vs undefined symbols") + + for name in "A", "B", "C", "D", "BOOL", "TRISTATE", "STRING", "INT", "HEX": + verify(c.syms[name].nodes, + "{} should be defined".format(name)) + + for name in "NOT_DEFINED_1", "NOT_DEFINED_2", "NOT_DEFINED_3", \ + "NOT_DEFINED_4": + sym = c.syms[name] + verify(not c.syms[name].nodes, + "{} should not be defined".format(name)) + + + print("Testing Symbol.choice") + + for name in "A", "B", "C", "D": + verify(c.syms[name].choice is not None, + "{} should be a choice symbol".format(name)) + + for name in "Q1", "Q2", "Q3", "BOOL", "TRISTATE", "STRING", "INT", "HEX", \ + "FROM_ENV", "FROM_ENV_MISSING", "NOT_DEFINED_1", \ + "NOT_DEFINED_2", "NOT_DEFINED_3", "NOT_DEFINED_4": + verify(c.syms[name].choice is None, + "{} should not be a choice symbol".format(name)) + + + print("Testing is_allnoconfig_y") + + verify(not c.syms["NOT_ALLNOCONFIG_Y"].is_allnoconfig_y, + "NOT_ALLNOCONFIG_Y should not be allnoconfig_y") + verify(c.syms["ALLNOCONFIG_Y"].is_allnoconfig_y, + "ALLNOCONFIG_Y should be allnoconfig_y") + + + print("Testing .config reading and writing") + + config_test_file = "Kconfiglib/tests/config_test" + + def verify_file_contents(fname, contents): + with open(fname, "r") as f: + file_contents = f.read() + verify(file_contents == contents, + "{} contains '{}'. Expected '{}'." + .format(fname, file_contents, contents)) + + # Writing/reading strings with characters that need to be escaped + + c = Kconfig("Kconfiglib/tests/Kescape") + + # Test the default value + c.write_config(config_test_file + "_from_def", header="") + verify_file_contents(config_test_file + "_from_def", + r'''CONFIG_STRING="\"\\"''' "\n") + # Write our own value + c.syms["STRING"].set_value(r'''\"a'\\''') + c.write_config(config_test_file + "_from_user", header="") + verify_file_contents(config_test_file + "_from_user", + r'''CONFIG_STRING="\\\"a'\\\\"''' "\n") + + # Read back the two configs and verify the respective values + c.load_config(config_test_file + "_from_def") + verify_value("STRING", '"\\') + c.load_config(config_test_file + "_from_user") + verify_value("STRING", r'''\"a'\\''') + + # Appending values from a .config + + c = Kconfig("Kconfiglib/tests/Kappend") + + # Values before assigning + verify_value("BOOL", "n") + verify_value("STRING", "") + + # Assign BOOL + c.load_config("Kconfiglib/tests/config_set_bool", replace=False) + verify_value("BOOL", "y") + verify_value("STRING", "") + + # Assign STRING + c.load_config("Kconfiglib/tests/config_set_string", replace=False) + verify_value("BOOL", "y") + verify_value("STRING", "foo bar") + + # Reset BOOL + c.load_config("Kconfiglib/tests/config_set_string") + verify_value("BOOL", "n") + verify_value("STRING", "foo bar") + + # Loading a completely empty .config should reset values + c.load_config("Kconfiglib/tests/empty") + verify_value("STRING", "") + + # An indented assignment in a .config should be ignored + c.load_config("Kconfiglib/tests/config_indented") + verify_value("IGNOREME", "y") + + # Symbol order in headers and minimal configuration files should match + # definition order, like in .config files + + c = Kconfig("Kconfiglib/tests/Korder") + + c.write_autoconf(config_test_file, header="") + verify_file_contents(config_test_file, """ +#define CONFIG_O 0 +#define CONFIG_R 1 +#define CONFIG_D 2 +#define CONFIG_E 3 +#define CONFIG_R2 4 +#define CONFIG_I 5 +#define CONFIG_N 6 +#define CONFIG_G 7 +"""[1:]) + + # Differs from defaults + c.syms["O"].set_value("-1") + c.syms["R"].set_value("-1") + c.syms["E"].set_value("-1") + c.syms["R2"].set_value("-1") + c.syms["N"].set_value("-1") + c.syms["G"].set_value("-1") + c.write_min_config(config_test_file, header="") + verify_file_contents(config_test_file, """ +CONFIG_O=-1 +CONFIG_R=-1 +CONFIG_E=-1 +CONFIG_R2=-1 +CONFIG_N=-1 +CONFIG_G=-1 +"""[1:]) + + + print("Testing Kconfig fetching and separation") + + for c in Kconfig("Kconfiglib/tests/Kmisc", warn=False), \ + Kconfig("Kconfiglib/tests/Kmisc", warn=False): + for item in c.syms["BOOL"], \ + c.syms["BOOL"].nodes[0], \ + c.named_choices["OPTIONAL"], \ + c.named_choices["OPTIONAL"].nodes[0], \ + c.syms["MENU_HOOK"].nodes[0].next, \ + c.syms["COMMENT_HOOK"].nodes[0].next: + verify(item.kconfig is c, + ".kconfig not properly set for " + repr(item)) + + + print("Testing imply semantics") + + c = Kconfig("Kconfiglib/tests/Kimply") + + verify_value("IMPLY_DIRECT_DEPS", "y") + verify_value("UNMET_DIRECT_1", "n") + verify_value("UNMET_DIRECT_2", "n") + verify_value("UNMET_DIRECT_3", "n") + verify_value("MET_DIRECT_1", "y") + verify_value("MET_DIRECT_2", "y") + verify_value("MET_DIRECT_3", "y") + verify_value("MET_DIRECT_4", "y") + + verify_value("IMPLY_COND", "y") + verify_value("IMPLIED_N_COND", "n") + verify_value("IMPLIED_M_COND", "m") + verify_value("IMPLIED_Y_COND", "y") + + verify_value("IMPLY_N_1", "n") + verify_value("IMPLY_N_2", "n") + verify_value("IMPLIED_FROM_N_1", "n") + verify_value("IMPLIED_FROM_N_2", "n") + + verify_value("IMPLY_M", "m") + verify_value("IMPLIED_M", "m") + verify_value("IMPLIED_M_BOOL", "y") + + verify_value("IMPLY_M_TO_Y", "y") + verify_value("IMPLIED_M_TO_Y", "y") + + # Test user value semantics + + # Verify that IMPLIED_TRISTATE is invalidated if the direct + # dependencies change + + assign_and_verify("IMPLY", 2) + assign_and_verify("DIRECT_DEP", 2) + verify_value("IMPLIED_TRISTATE", 2) + assign_and_verify("DIRECT_DEP", 0) + verify_value("IMPLIED_TRISTATE", 0) + # Set back for later tests + assign_and_verify("DIRECT_DEP", 2) + + # Verify that IMPLIED_TRISTATE can be set to anything when IMPLY has value + # n, and that it gets the value n by default (for non-imply-related + # reasons) + + assign_and_verify("IMPLY", 0) + assign_and_verify("IMPLIED_TRISTATE", 0) + assign_and_verify("IMPLIED_TRISTATE", 1) + assign_and_verify("IMPLIED_TRISTATE", 2) + c.syms["IMPLIED_TRISTATE"].unset_value() + verify_value("IMPLIED_TRISTATE", "n") + + # Same as above for m. Anything still goes, but m by default now. + + assign_and_verify("IMPLY", 1) + assign_and_verify("IMPLIED_TRISTATE", 0) + assign_and_verify("IMPLIED_TRISTATE", 1) + assign_and_verify("IMPLIED_TRISTATE", 2) + c.syms["IMPLIED_TRISTATE"].unset_value() + verify_value("IMPLIED_TRISTATE", 1) + + # Same as above for y. Only n and y should be accepted. m gets promoted to + # y. Default should be y. + + assign_and_verify("IMPLY", 2) + assign_and_verify("IMPLIED_TRISTATE", 0) + assign_and_verify_value("IMPLIED_TRISTATE", 1, 2) + assign_and_verify("IMPLIED_TRISTATE", 2) + c.syms["IMPLIED_TRISTATE"].unset_value() + verify_value("IMPLIED_TRISTATE", 2) + + # Being implied to either m or y should give a bool the value y + + c.syms["IMPLY"].unset_value() + verify_value("IMPLIED_BOOL", 0) + assign_and_verify("IMPLY", 0) + verify_value("IMPLIED_BOOL", 0) + assign_and_verify("IMPLY", 1) + verify_value("IMPLIED_BOOL", 2) + assign_and_verify("IMPLY", 2) + verify_value("IMPLIED_BOOL", 2) + + # A bool implied to m or y can take the values n and y + + c.syms["IMPLY"].set_value(1) + assign_and_verify("IMPLIED_BOOL", 0) + assign_and_verify("IMPLIED_BOOL", 2) + + c.syms["IMPLY"].set_value(2) + assign_and_verify("IMPLIED_BOOL", 0) + assign_and_verify("IMPLIED_BOOL", 2) + + + print("Testing choice semantics") + + # Would warn for choice value symbols defined without a type, even + # though the type is automatically derived. This is probably more + # helpful than ignoring those cases, as this feature isn't used + # deliberately anywhere from what I've seen. + c = Kconfig("Kconfiglib/tests/Kchoice", warn=False) + + for name in "BOOL", "BOOL_OPT", "BOOL_M", "DEFAULTS": + verify(c.named_choices[name].orig_type == BOOL, + "choice {} should have type bool".format(name)) + + for name in "TRISTATE", "TRISTATE_OPT", "TRISTATE_M": + verify(c.named_choices[name].orig_type == TRISTATE, + "choice {} should have type tristate".format(name)) + + def select_and_verify(sym): + choice = sym.nodes[0].parent.item + choice.set_value(2) + + sym.set_value(2) + + verify(sym.choice.selection is sym, + sym.name + " should be the selected symbol") + + verify(choice.user_selection is sym, + sym.name + " should be the user selection of the choice") + + verify(sym.tri_value == 2, + sym.name + " should have value y when selected") + + verify(sym.user_value == 2, + sym.name + " should have user value y when selected") + + for sibling in choice.syms: + if sibling is not sym: + verify(sibling.tri_value == 0, + sibling.name + " should be n when not selected") + + def select_and_verify_all(choice_name): + choice = c.named_choices[choice_name] + + # Select in forward order + for sym in choice.syms: + select_and_verify(sym) + + # Select in reverse order + for sym in reversed(choice.syms): + select_and_verify(sym) + + def verify_mode(choice_name, no_modules_mode, modules_mode): + choice = c.named_choices[choice_name] + + c.modules.set_value(0) + verify(choice.tri_value == no_modules_mode, + 'Wrong mode for choice {} with no modules. Expected {}, got {}.' + .format(choice.name, no_modules_mode, choice.tri_value)) + + c.modules.set_value(2) + verify(choice.tri_value == modules_mode, + 'Wrong mode for choice {} with modules. Expected {}, got {}.' + .format(choice.name, modules_mode, choice.tri_value)) + + verify_mode("BOOL", 2, 2) + verify_mode("BOOL_OPT", 0, 0) + verify_mode("TRISTATE", 2, 1) + verify_mode("TRISTATE_OPT", 0, 0) + verify_mode("BOOL_M", 0, 2) + verify_mode("TRISTATE_M", 0, 1) + + # Test defaults + + choice = c.named_choices["DEFAULTS"] + + c.syms["TRISTATE_SYM"].set_value(0) + verify(choice.selection is c.syms["OPT_4"], + "Wrong choice default with TRISTATE_SYM = n") + + c.syms["TRISTATE_SYM"].set_value(2) + verify(choice.selection is c.syms["OPT_2"], + "Wrong choice default with TRISTATE_SYM = y") + + c.syms["OPT_1"].set_value(2) + verify(choice.selection is c.syms["OPT_1"], + "User selection should override defaults") + + verify(c.named_choices["DEFAULTS_NOT_VISIBLE"].selection + is c.syms["OPT_8"], + "Non-visible choice symbols should cause the next default to be " + "considered") + + # Test y mode selection + + c.modules.set_value(2) + + select_and_verify_all("BOOL") + select_and_verify_all("BOOL_OPT") + select_and_verify_all("TRISTATE") + select_and_verify_all("TRISTATE_OPT") + # For BOOL_M, the mode should have been promoted + select_and_verify_all("BOOL_M") + + # Test m mode selection + + c.named_choices["TRISTATE"].set_value(1) + + verify(c.named_choices["TRISTATE"].tri_value == 1, + "TRISTATE choice should have mode m after explicit mode assignment") + + assign_and_verify_value("T_1", 0, 0) + assign_and_verify_value("T_2", 0, 0) + assign_and_verify_value("T_1", 1, 1) + assign_and_verify_value("T_2", 1, 1) + assign_and_verify_value("T_1", 2, 1) + assign_and_verify_value("T_2", 2, 1) + + # Switching to y mode should cause T_2 to become selected + c.named_choices["TRISTATE"].set_value(2) + verify_value("T_1", 0) + verify_value("T_2", 2) + + # Verify that choices with no explicitly specified type get the type of the + # first contained symbol with a type + + verify(c.named_choices["NO_TYPE_BOOL"].orig_type == BOOL, + "Expected first choice without explicit type to have type bool") + + verify(c.named_choices["NO_TYPE_TRISTATE"].orig_type == TRISTATE, + "Expected second choice without explicit type to have type " + "tristate") + + # Verify that symbols without a type in the choice get the type of the + # choice + + for name in "MMT_1", "MMT_2", "MMT_4", "MMT_5": + verify(c.syms[name].orig_type == BOOL, + "Expected {} to get type bool".format(name)) + + verify(c.syms["MMT_3"].orig_type == TRISTATE, + "Expected MMT_3 to have type tristate") + + # Verify that the default selection can change depending on the + # visibility of the choice symbols + + default_with_dep_choice = c.named_choices["DEFAULT_WITH_DEP"] + + verify(default_with_dep_choice.selection is c.syms["B"], + "Wrong choice default with unsatisfied deps on default") + + c.syms["DEP"].set_value("y") + + verify(default_with_dep_choice.selection is c.syms["A"], + "Wrong choice default with satisfied deps on default") + + c.syms["DEP"].set_value("n") + + verify(default_with_dep_choice.selection is c.syms["B"], + "Wrong choice default with unsatisfied deps on default (round two)") + + # Verify that symbols in choices that depend on the preceding symbol aren't + # considered choice symbols + + weird_choice = c.named_choices["WEIRD_SYMS"] + + def verify_is_normal_choice_symbol(name): + sym = c.syms[name] + verify(sym.choice is not None and + sym in weird_choice.syms and + sym.nodes[0].parent.item is weird_choice, + "{} should be a normal choice symbol".format(sym.name)) + + def verify_is_weird_choice_symbol(name): + sym = c.syms[name] + verify(sym.choice is None and + sym not in weird_choice.syms, + "{} should be a weird (non-)choice symbol" + .format(sym.name)) + + verify_is_normal_choice_symbol("WS1") + verify_is_weird_choice_symbol("WS2") + verify_is_weird_choice_symbol("WS3") + verify_is_weird_choice_symbol("WS4") + verify_is_weird_choice_symbol("WS5") + verify_is_normal_choice_symbol("WS6") + verify_is_weird_choice_symbol("WS7") + verify_is_weird_choice_symbol("WS8") + verify_is_normal_choice_symbol("WS9") + + + print("Testing 'if' node removal") + + c = Kconfig("Kconfiglib/tests/Kifremoval", warn=False) + + nodes = tuple(c.node_iter()) + verify_equal(nodes[0].item.name, "A") + verify_equal(nodes[1].item.name, "B") + verify_equal(nodes[2].item.name, "C") + verify_equal(nodes[3].item.name, "D") + verify_equal(nodes[4].prompt[0], "E") + verify_equal(nodes[5].prompt[0], "F") + verify_equal(nodes[6].prompt[0], "G") + verify_equal(nodes[7].item.name, "H") + verify_equal(nodes[8].item.name, "I") + verify_equal(nodes[9].item.name, "J") + verify(len(nodes) == 10, + "Wrong number of nodes after 'if' removal") + + + print("Testing multi.def. property copying") + + c = Kconfig("Kconfiglib/tests/Kdepcopy", warn=False) + + def verify_props(desc, props, prop_names): + actual = [prop[0].name for prop in props] + expected = prop_names.split() + + verify(actual == expected, + "Wrong {} properties, expected '{}', got '{}'" + .format(desc, expected, actual)) + + verify_props("default", c.syms["MULTIDEF"].defaults, + "A B C D E F G H I J K L M N O P Q R") + + verify_props("select", c.syms["MULTIDEF"].selects, + "AA BB CC DD EE FF GG HH II JJ") + + verify_props("imply", c.syms["MULTIDEF"].selects, + "AA BB CC DD EE FF GG HH II JJ") + + verify_props("select", c.syms["MULTIDEF_CHOICE"].selects, + "A B C") + + verify_props("range", c.syms["MULTIDEF_RANGE"].ranges, + "A B C D E F") + + verify_props("default", c.choices[1].defaults, + "A B C D E") + + + print("Testing dependency loop detection") + + # These are all expected to raise dependency loop errors + for i in range(11): + filename = "Kconfiglib/tests/Kdeploop" + str(i) + try: + Kconfig(filename) + except KconfigError as e: + if "Dependency loop" not in str(e): + fail("dependency loop in {} raised wrong KconfigError" + .format(filename)) + except: + fail("dependency loop in {} raised wrong exception" + .format(filename)) + else: + fail("dependency loop in {} not detected".format(filename)) + + # Check the most complicated message completely + try: + Kconfig("Kconfiglib/tests/Kdeploop10") + except KconfigError as e: + verify_equal(str(e), """ +Dependency loop +=============== + +A (defined at Kconfiglib/tests/Kdeploop10:1), with definition... + +config A + bool + depends on B + +...depends on B (defined at Kconfiglib/tests/Kdeploop10:5), with definition... + +config B + bool + depends on C = 7 + +...depends on C (defined at Kconfiglib/tests/Kdeploop10:9), with definition... + +config C + int + range D 8 + +...depends on D (defined at Kconfiglib/tests/Kdeploop10:13), with definition... + +config D + int + default 3 if E + default 8 + +...depends on E (defined at Kconfiglib/tests/Kdeploop10:18), with definition... + +config E + bool + +(select-related dependencies: F && G) + +...depends on G (defined at Kconfiglib/tests/Kdeploop10:25), with definition... + +config G + bool + depends on H + +...depends on the choice symbol H (defined at Kconfiglib/tests/Kdeploop10:32), with definition... + +config H + bool "H" + depends on I && <choice> + +...depends on the choice symbol I (defined at Kconfiglib/tests/Kdeploop10:41), with definition... + +config I + bool "I" + depends on <choice> + +...depends on <choice> (defined at Kconfiglib/tests/Kdeploop10:38), with definition... + +choice + bool "choice" if J + +...depends on J (defined at Kconfiglib/tests/Kdeploop10:46), with definition... + +config J + bool + depends on A + +...depends again on A (defined at Kconfiglib/tests/Kdeploop10:1) +"""[:-1]) + except: + fail("Loop detection message check raised wrong exception") + else: + fail("Loop detection message check did not raise exception") + + + print("Testing preprocessor") + + os.environ["ENV_1"] = "env_1" + os.environ["ENV_2"] = "env_2" + os.environ["ENV_3"] = "env_3" + os.environ["ENV_4"] = "env_4" + os.environ["ENV_5"] = "n" + os.environ["ENV_6"] = "Kconfiglib/tests/empty" + os.environ["ENV_7"] = "env_7" + # We verify warnings manually + c = Kconfig("Kconfiglib/tests/Kpreprocess", warn_to_stderr=False) + + def verify_variable(name, unexp_value, exp_value, recursive, *args): + var = c.variables[name] + + verify(var.value == unexp_value, + "expected variable '{}' to have the unexpanded value '{}', had " + "the value '{}'".format(name, unexp_value, var.value)) + + if not args: + verify(var.expanded_value == exp_value, + "expected expanded_value for {} to be '{}', was '{}'" + .format(name, exp_value, var.expanded_value)) + + verify(var.expanded_value_w_args(*args) == exp_value, + "expected expanded_value_w_args() for '{}' to be '{}', was '{}'" + .format(name, exp_value, var.expanded_value_w_args(*args))) + + verify(var.is_recursive == recursive, + "{} was {}, shouldn't be" + .format(name, "recursive" if var.is_recursive else "simple")) + + verify_variable("simple-recursive", "foo", "foo", True) + verify_variable("simple-immediate", "bar", "bar", False) + verify_variable("simple-recursive-2", "baz", "baz", True) + + verify_variable("whitespaced", "foo", "foo", True) + + verify_variable("preserve-recursive", "foo bar", "foo bar", True) + verify_variable("preserve-immediate", "foo bar", "foo bar", False) + + verify_variable("recursive", + "$(foo) $(bar) $($(b-char)a$(z-char)) $(indir)", + "abc def ghi jkl mno", + True) + + verify_variable("immediate", "foofoo", "foofoo", False) + + verify_variable("messy-fn-res", + "$($(fn-indir)-unused-arg, a b , c d )", + 'surround-rev-quote " c d " " a b " surround-rev-quote ', + True) + + verify_variable("special-chars-fn-res", + "$(fn,$(comma)$(dollar)$(left-paren)foo$(right-paren))", + '",$(foo)"', + True) + + verify_variable("quote", '"$(1)" "$(2)"', '"" ""', True) + verify_variable("quote", '"$(1)" "$(2)"', '"one" ""', True, + "one") + verify_variable("quote", '"$(1)" "$(2)"', '"one" "two"', True, + "one", "two") + verify_variable("quote", '"$(1)" "$(2)"', '"one" "two"', True, + "one", "two", "three") + + verify_str(c.syms["PRINT_ME"], r""" +config PRINT_ME + string "env_1" if (FOO && BAR) || !BAZ || !QAZ + default "\"foo\"" if "foo \"bar\" baz" = "" +""") + + verify_str(c.syms["PRINT_ME_TOO"], r""" +config PRINT_ME_TOO + bool "foo" + default FOOBARBAZQAZ if QAZ && QAZFOO && xxx +""") + + def verify_repr(name, s): + verify_equal(repr(c.variables[name]), s) + + verify_repr( + "simple-immediate", + "<variable simple-immediate, immediate, value 'bar'>") + + verify_repr( + "messy-fn-res", + "<variable messy-fn-res, recursive, value '$($(fn-indir)-unused-arg, a b , c d )'>") + + def verify_recursive(name): + try: + c.variables[name].expanded_value + except KconfigError: + pass + else: + fail("Expected '{}' expansion to flag recursive expansion, didn't" + .format(name)) + + verify_recursive("rec-1") + # Indirectly verifies that it's not recursive + verify_variable("safe-fn-rec-res", + "$(safe-fn-rec,safe-fn-rec-2)", + "foo", + True) + verify_recursive("unsafe-fn-rec") + + verify_variable("foo-bar-baz", "$(rhs)", "value", True) + + verify_variable("space-var-res", "$(foo bar)", "value", True) + + verify_variable("shell-res", + "$(shell,false && echo foo bar || echo baz qaz)", + "baz qaz", + True) + + verify_variable("shell-stderr-res", "", "", False) + + verify_variable("location-res", + "Kconfiglib/tests/Kpreprocess:125", + "Kconfiglib/tests/Kpreprocess:125", + False) + + verify_variable("warning-res", "", "", False) + verify_variable("error-n-res", "", "", False) + + try: + c.variables["error-y-res"].expanded_value + except KconfigError: + pass + else: + fail("expanding error-y-res didn't raise an exception") + + # Check Kconfig.env_vars + verify_equal(c.env_vars, + set(("ENV_1", "ENV_2", "ENV_3", "ENV_4", "ENV_5", "ENV_6"))) + + # Check that the expected warnings were generated + verify_equal(c.warnings, [ + "Kconfiglib/tests/Kpreprocess:122: warning: 'echo message on stderr >&2' wrote to stderr: message on stderr", + "Kconfiglib/tests/Kpreprocess:130: warning: a warning" + ]) + + + print("Testing user-defined preprocessor functions") + + # Make Kconfiglib/tests/kconfigfunctions.py importable + sys.path.insert(0, "Kconfiglib/tests") + + c = Kconfig("Kconfiglib/tests/Kuserfunctions") + + verify_variable("add-zero", "$(add)", "0", True) + verify_variable("add-one", "$(add,1)", "1", True) + verify_variable("add-three", "$(add,1,-1,2,1)", "3", True) + + verify_variable("one-one", "$(one,foo bar)", "onefoo barfoo bar", True) + + verify_variable("one-or-more-one", "$(one-or-more,foo)", "foo + ", True) + verify_variable("one-or-more-three", "$(one-or-more,foo,bar,baz)", + "foo + bar,baz", True) + + def verify_bad_argno(name): + try: + c.variables[name].expanded_value + except KconfigError: + pass + else: + fail("Expected '{}' expansion to flag wrong number of arguments, " + "didn't".format(name)) + + verify_bad_argno("one-zero") + verify_bad_argno("one-two") + verify_bad_argno("one-or-more-zero") + + sys.path.pop(0) + + # This test can fail on older Python 3.x versions, because they don't + # preserve dict insertion order during iteration. The output is still + # correct, just different. + if not (3, 0) <= sys.version_info <= (3, 5): + print("Testing KCONFIG_WARN_UNDEF") + + os.environ["KCONFIG_WARN_UNDEF"] = "y" + c = Kconfig("Kconfiglib/tests/Kundef", warn_to_stderr=False) + + verify_equal("\n".join(c.warnings), """ +warning: the int symbol INT (defined at Kconfiglib/tests/Kundef:8) has a non-int range [UNDEF_2 (undefined), 8 (undefined)] +warning: undefined symbol UNDEF_1: + +- Referenced at Kconfiglib/tests/Kundef:4: + +config BOOL + bool "foo" if DEF || !UNDEF_1 + default UNDEF_2 + +- Referenced at Kconfiglib/tests/Kundef:19: + +menu "menu" + depends on UNDEF_1 + visible if UNDEF_3 +warning: undefined symbol UNDEF_2: + +- Referenced at Kconfiglib/tests/Kundef:4: + +config BOOL + bool "foo" if DEF || !UNDEF_1 + default UNDEF_2 + +- Referenced at Kconfiglib/tests/Kundef:8: + +config INT + int + range UNDEF_2 8 + range 5 15 + default 10 +warning: undefined symbol UNDEF_3: + +- Referenced at Kconfiglib/tests/Kundef:19: + +menu "menu" + depends on UNDEF_1 + visible if UNDEF_3 +"""[1:-1]) + + os.environ.pop("KCONFIG_WARN_UNDEF") + + + print("\nAll selftests passed\n" if all_passed else + "\nSome selftests failed\n") + + +def run_compatibility_tests(): + # Runs tests on configurations from the kernel. Tests compability with the + # C implementation by comparing outputs. + + # Referenced inside the kernel Kconfig files. + # + # The str() makes the type of the value 'str' on both Python 2 and Python 3, + # which is nice for some later dictionary key sanity checks. + + os.environ["KERNELVERSION"] = str( + subprocess.check_output("make kernelversion", shell=True) + .decode("utf-8").rstrip() + ) + + os.environ["CC_VERSION_TEXT"] = str( + subprocess.check_output("gcc --version | head -n1", shell=True) + .decode("utf-8").rstrip() + ) + + os.environ["srctree"] = "." + os.environ["CC"] = "gcc" + os.environ["LD"] = "ld" + + + if not os.path.exists("scripts/kconfig/conf"): + print("\nscripts/kconfig/conf does not exist -- running " + "'make allnoconfig' to build it...") + shell("make allnoconfig") + + + print("Running compatibility tests...\n") + + test_fns = (test_defconfig, + # Fails for a few defconfigs due to a bug in the C tools. Will + # be enabled once patches get in. + #test_min_config, + test_alldefconfig, + test_allnoconfig, + test_allnoconfig_walk, + test_allmodconfig, + test_allyesconfig, + test_sanity) + + for test_fn in test_fns: + # The test description is taken from the docstring of the corresponding + # function + print(textwrap.dedent(test_fn.__doc__)) + + for arch, srcarch in all_arch_srcarch(): + # Referenced inside the Kconfig files + os.environ["ARCH"] = arch + os.environ["SRCARCH"] = srcarch + + rm_configs() + + test_fn(arch, srcarch) + + if all_passed: + print("All selftests and compatibility tests passed") + else: + sys.exit("Some tests failed") + + +def all_arch_srcarch(): + for srcarch in os.listdir("arch"): + # arc and h8300 are currently broken with the C tools on linux-next as + # well. Perhaps they require cross-compilers to be installed. + # + # User-mode Linux has an unorthodox Kconfig setup that would require a + # different testing setup. Skip it too. + if srcarch in ("arc", "h8300", "um"): + continue + + if os.path.exists(os.path.join("arch", srcarch, "Kconfig")): + yield (srcarch, srcarch) + + # Some arches define additional ARCH settings with ARCH != SRCARCH + # (search for "Additional ARCH settings for" in the top-level Makefile) + + yield ("i386", "x86") + yield ("x86_64", "x86") + + yield ("sparc32", "sparc") + yield ("sparc64", "sparc") + + yield ("sh64", "sh") + + +def test_allnoconfig(arch, srcarch): + """ + Verify that allnoconfig.py generates the same .config as + 'make allnoconfig', for each architecture. Runs the script via + 'make scriptconfig'. + """ + shell("make scriptconfig SCRIPT=Kconfiglib/allnoconfig.py " + "PYTHONCMD='{}'".format(sys.executable)) + shell("mv .config ._config") + shell("scripts/kconfig/conf --allnoconfig Kconfig") + + compare_configs(arch) + + +def test_allnoconfig_walk(arch, srcarch): + """ + Verify that examples/allnoconfig_walk.py generates the same .config as + 'make allnoconfig', for each architecture. Runs the script via + 'make scriptconfig'. + """ + shell("make scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_walk.py " + "PYTHONCMD='{}'".format(sys.executable)) + shell("mv .config ._config") + shell("scripts/kconfig/conf --allnoconfig Kconfig") + + compare_configs(arch) + + +def test_allmodconfig(arch, srcarch): + """ + Verify that allmodconfig.py generates the same .config as + 'make allmodconfig', for each architecture. Runs the script via + 'make scriptconfig'. + """ + shell("make scriptconfig SCRIPT=Kconfiglib/allmodconfig.py " + "PYTHONCMD='{}'".format(sys.executable)) + shell("mv .config ._config") + shell("scripts/kconfig/conf --allmodconfig Kconfig") + + compare_configs(arch) + + +def test_allyesconfig(arch, srcarch): + """ + Verify that allyesconfig.py generates the same .config as + 'make allyesconfig', for each architecture. Runs the script via + 'make scriptconfig'. + """ + shell("make scriptconfig SCRIPT=Kconfiglib/allyesconfig.py " + "PYTHONCMD='{}'".format(sys.executable)) + shell("mv .config ._config") + shell("scripts/kconfig/conf --allyesconfig Kconfig") + + compare_configs(arch) + + +def test_sanity(arch, srcarch): + """ + Do sanity checks on each configuration and call all public methods on all + symbols, choices, and menu nodes for all architectures to make sure we + never crash or hang. + """ + print("For {}...".format(arch)) + + kconf = Kconfig() + + for sym in kconf.defined_syms: + verify(sym._visited == 2, + "{} has broken dependency loop detection (_visited = {})" + .format(sym.name, sym._visited)) + + kconf.modules + kconf.defconfig_list + kconf.defconfig_filename + + # Legacy warning functions + kconf.enable_redun_warnings() + kconf.disable_redun_warnings() + kconf.enable_undef_warnings() + kconf.disable_undef_warnings() + kconf.enable_warnings() + kconf.disable_warnings() + kconf.enable_stderr_warnings() + kconf.disable_stderr_warnings() + + kconf.mainmenu_text + kconf.unset_values() + + kconf.write_autoconf("/dev/null") + + # No tempfile.TemporaryDirectory in Python 2 + tmpdir = tempfile.mkdtemp() + kconf.sync_deps(os.path.join(tmpdir, "deps")) # Create + kconf.sync_deps(os.path.join(tmpdir, "deps")) # Update + shutil.rmtree(tmpdir) + + # Python 2/3 compatible + for key, sym in kconf.syms.items(): + verify(isinstance(key, str), "weird key '{}' in syms dict".format(key)) + + verify(not sym.is_constant, sym.name + " in 'syms' and constant") + + verify(sym not in kconf.const_syms, + sym.name + " in both 'syms' and 'const_syms'") + + for dep in sym._dependents: + verify(not dep.is_constant, + "the constant symbol {} depends on {}" + .format(dep.name, sym.name)) + + sym.__repr__() + sym.__str__() + sym.assignable + kconf.disable_warnings() + sym.set_value(2) + sym.set_value("foo") + sym.unset_value() + kconf.enable_warnings() # Legacy warning function + sym.str_value + sym.tri_value + sym.type + sym.user_value + sym.visibility + + for sym in kconf.defined_syms: + verify(sym.nodes, sym.name + " is defined but lacks menu nodes") + + verify(not (sym.orig_type not in (BOOL, TRISTATE) and sym.choice), + sym.name + " is a choice symbol but not bool/tristate") + + for key, sym in kconf.const_syms.items(): + verify(isinstance(key, str), + "weird key '{}' in const_syms dict".format(key)) + + verify(sym.is_constant, + '"{}" is in const_syms but not marked constant' + .format(sym.name)) + + verify(not sym.nodes, + '"{}" is constant but has menu nodes'.format(sym.name)) + + verify(not sym._dependents, + '"{}" is constant but is a dependency of some symbol' + .format(sym.name)) + + verify(not sym.choice, + '"{}" is constant and a choice symbol'.format(sym.name)) + + sym.__repr__() + sym.__str__() + sym.assignable + kconf.disable_warnings() + sym.set_value(2) + sym.set_value("foo") + sym.unset_value() + kconf.enable_warnings() # Legacy warning function + sym.str_value + sym.tri_value + sym.type + sym.visibility + + for choice in kconf.choices: + for sym in choice.syms: + verify(sym.choice is choice, + "{0} is in choice.syms but 'sym.choice' is not the choice" + .format(sym.name)) + + verify(sym.type in (BOOL, TRISTATE), + "{} is a choice symbol but is not a bool/tristate" + .format(sym.name)) + + choice.__str__() + choice.__repr__() + choice.str_value + choice.tri_value + choice.user_value + choice.assignable + choice.selection + choice.type + choice.visibility + + # Menu nodes + + node = kconf.top_node + + while 1: + # Everything else should be well exercised elsewhere + node.__repr__() + node.__str__() + verify(isinstance(node.item, (Symbol, Choice)) or \ + node.item in (MENU, COMMENT), + "'{}' appeared as a menu item".format(node.item)) + + if node.list is not None: + node = node.list + + elif node.next is not None: + node = node.next + + else: + while node.parent is not None: + node = node.parent + if node.next is not None: + node = node.next + break + else: + break + + +def test_alldefconfig(arch, srcarch): + """ + Verify that alldefconfig.py generates the same .config as + 'make alldefconfig', for each architecture. Runs the script via + 'make scriptconfig'. + """ + shell("make scriptconfig SCRIPT=Kconfiglib/alldefconfig.py " + "PYTHONCMD='{}'".format(sys.executable)) + shell("mv .config ._config") + shell("scripts/kconfig/conf --alldefconfig Kconfig") + + compare_configs(arch) + + +def test_defconfig(arch, srcarch): + """ + Verify that Kconfiglib generates the same .config as scripts/kconfig/conf, + for each architecture/defconfig pair. In obsessive mode, this test includes + nonsensical groupings of arches with defconfigs from other arches (every + arch/defconfig combination) and takes an order of magnitude longer time to + run. + + With logging enabled, this test appends any failures to a file + test_defconfig_fails in the root. + """ + kconf = Kconfig() + + if obsessive: + defconfigs = [] + + # Collect all defconfigs. This could be done once instead, but it's + # a speedy operation comparatively. + for srcarch_ in os.listdir("arch"): + defconfigs.extend(defconfig_files(srcarch_)) + else: + defconfigs = defconfig_files(srcarch) + + # Test architecture for each defconfig + + for defconfig in defconfigs: + rm_configs() + + kconf.load_config(defconfig) + kconf.write_config("._config") + shell("scripts/kconfig/conf --defconfig='{}' Kconfig". + format(defconfig)) + + arch_defconfig_str = " {:14}with {:60} ".format(arch, defconfig) + + if equal_configs(): + print(arch_defconfig_str + "OK") + else: + print(arch_defconfig_str + "FAIL") + fail() + if log: + with open("test_defconfig_fails", "a") as fail_log: + fail_log.write("{} with {} did not match\n" + .format(arch, defconfig)) + + +def test_min_config(arch, srcarch): + """ + Verify that Kconfiglib generates the same .config as 'make savedefconfig' + for each architecture/defconfig pair. + """ + kconf = Kconfig() + + if obsessive_min_config: + defconfigs = [] + for srcarch_ in os.listdir("arch"): + defconfigs.extend(defconfig_files(srcarch_)) + else: + defconfigs = defconfig_files(srcarch) + + for defconfig in defconfigs: + rm_configs() + + kconf.load_config(defconfig) + kconf.write_min_config("._config") + + shell("cp {} .config".format(defconfig)) + + shell("scripts/kconfig/conf --savedefconfig=.config Kconfig") + + arch_defconfig_str = " {:14}with {:60} ".format(arch, defconfig) + + if equal_configs(): + print(arch_defconfig_str + "OK") + else: + print(arch_defconfig_str + "FAIL") + + +# +# Helper functions +# + + +def defconfig_files(srcarch): + # Yields a list of defconfig file filenames for a particular srcarch + # subdirectory (arch/<srcarch>/) + + srcarch_dir = os.path.join("arch", srcarch) + + # Some arches have a defconfig in the root of their arch/<arch>/ directory + root_defconfig = os.path.join(srcarch_dir, "defconfig") + if os.path.exists(root_defconfig): + yield root_defconfig + + # Assume all files in the arch/<arch>/configs/ directory (if it exists) are + # configurations + defconfigs_dir = os.path.join(srcarch_dir, "configs") + + if not os.path.isdir(defconfigs_dir): + return + + for dirpath, _, filenames in os.walk(defconfigs_dir): + for filename in filenames: + yield os.path.join(dirpath, filename) + + +def rm_configs(): + # Delete any old ".config" (generated by the C implementation) and + # "._config" (generated by us), if present. + + def rm_if_exists(f): + if os.path.exists(f): + os.remove(f) + + rm_if_exists(".config") + rm_if_exists("._config") + + +def compare_configs(arch): + if equal_configs(): + print("{:14}OK".format(arch)) + else: + print("{:14}FAIL".format(arch)) + fail() + + +def equal_configs(): + with open(".config") as f: + their = f.readlines() + + # Strip the header generated by 'conf' + i = 0 + for line in their: + if not line.startswith("#") or \ + re.match(r"# CONFIG_(\w+) is not set", line): + break + i += 1 + their = their[i:] + + try: + f = open("._config") + except EnvironmentError as e: + if e.errno != errno.ENOENT: + raise + print("._config not found. Did you forget to apply the Makefile patch?") + return False + else: + with f: + # [1:] strips the default header + our = f.readlines()[1:] + + if their == our: + return True + + # Print a unified diff to help debugging + print("Mismatched .config's! Unified diff:") + sys.stdout.writelines(difflib.unified_diff(their, our, fromfile="their", + tofile="our")) + + return False + + +if __name__ == "__main__": + run_tests() -- 2.7.4 _______________________________________________ Minios-devel mailing list Minios-devel@xxxxxxxxxxxxxxxxxxxx https://lists.xenproject.org/mailman/listinfo/minios-devel
|
Lists.xenproject.org is hosted with RackSpace, monitoring our |