mirror of
https://github.com/espressif/esp-idf.git
synced 2024-10-05 20:47:46 -04:00
tools: Move out kconfig_new in favour of using the esp-idf-kconfig package
This commit removes all kconfig_new files and references to them and adds esp-idf-kconfig as a dependency and adequate wrappers to avoid breaking changes.
This commit is contained in:
parent
f7f3c514df
commit
ebb6c2e77b
@ -5,7 +5,6 @@
|
||||
- "components/**/Kconfig*"
|
||||
- "components/**/CMakeList.txt"
|
||||
- "components/**/sdkconfig*"
|
||||
- "tools/kconfig_new/**/*"
|
||||
- "tools/tools.json"
|
||||
- "tools/idf_tools.py"
|
||||
- "CONTRIBUTING.rst"
|
||||
|
@ -150,25 +150,6 @@ test_certificate_bundle_on_host:
|
||||
- cd components/mbedtls/esp_crt_bundle/test_gen_crt_bundle/
|
||||
- ./test_gen_crt_bundle.py
|
||||
|
||||
test_confserver:
|
||||
extends: .host_test_template
|
||||
script:
|
||||
- cd tools/kconfig_new/test/confserver
|
||||
- ./test_confserver.py
|
||||
|
||||
test_gen_kconfig_doc:
|
||||
extends: .host_test_template
|
||||
script:
|
||||
- cd tools/kconfig_new/test/gen_kconfig_doc/
|
||||
- ./test_target_visibility.py
|
||||
- ./test_kconfig_out.py
|
||||
|
||||
test_confgen:
|
||||
extends: .host_test_template
|
||||
script:
|
||||
- cd tools/kconfig_new/test/confgen/
|
||||
- ./test_confgen.py
|
||||
|
||||
test_idf_monitor:
|
||||
extends: .host_test_template
|
||||
artifacts:
|
||||
|
@ -129,24 +129,6 @@ If using an IDE with CMake, setting the ``PYTHON`` value as a CMake cache overri
|
||||
|
||||
To manage the Python version more generally via the command line, check out the tools pyenv_ or virtualenv_. These let you change the default Python version.
|
||||
|
||||
Possible Issues
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The user of ``idf.py`` may sometimes experience ``ImportError`` described below.
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/Users/user_name/e/esp-idf/tools/kconfig_new/confgen.py", line 27, in <module>
|
||||
import kconfiglib
|
||||
ImportError: bad magic number in 'kconfiglib': b'\x03\xf3\r\n'
|
||||
|
||||
The exception is often caused by ``.pyc`` files generated by different Python versions. To solve the issue run the following command:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
idf.py python-clean
|
||||
|
||||
.. _example-project-structure:
|
||||
|
||||
Example Project
|
||||
@ -1359,11 +1341,11 @@ JSON Configuration Server
|
||||
|
||||
.. highlight :: json
|
||||
|
||||
A tool called ``confserver.py`` is provided to allow IDEs to easily integrate with the configuration system logic. ``confserver.py`` is designed to run in the background and interact with a calling process by reading and writing JSON over process stdin & stdout.
|
||||
A tool called ``kconfserver`` is provided to allow IDEs to easily integrate with the configuration system logic. ``kconfserver`` is designed to run in the background and interact with a calling process by reading and writing JSON over process stdin & stdout.
|
||||
|
||||
You can run ``confserver.py`` from a project via ``idf.py confserver`` or ``ninja confserver``, or a similar target triggered from a different build generator.
|
||||
You can run ``kconfserver`` from a project via ``idf.py confserver`` or ``ninja kconfserver``, or a similar target triggered from a different build generator.
|
||||
|
||||
For more information about ``confserver.py``, see :idf_file:`tools/kconfig_new/README.md`.
|
||||
For more information about ``kconfserver``, see the `esp-idf-kconfig documentation <https://github.com/espressif/esp-idf-kconfig/blob/master/docs/DOCUMENTATION.md>`_.
|
||||
|
||||
Build System Internals
|
||||
=======================
|
||||
|
@ -4,7 +4,7 @@ Project Configuration
|
||||
Introduction
|
||||
============
|
||||
|
||||
ESP-IDF uses kconfiglib_ which is a Python-based extension to the Kconfig_ system which provides a compile-time project configuration mechanism. Kconfig is based around options of several types: integer, string, boolean. Kconfig files specify dependencies between options, default values of the options, the way the options are grouped together, etc.
|
||||
ESP-IDF uses the esp-idf-kconfig_ package based on kconfiglib_ which is a Python-based extension to the Kconfig_ system. Kconfig provides a compile-time project configuration mechanism and is based around options of several types: integer, string, boolean. Kconfig files specify dependencies between options, default values of the options, the way the options are grouped together, etc.
|
||||
|
||||
For the complete list of available features please see Kconfig_ and `kconfiglib extentions`_.
|
||||
|
||||
@ -50,16 +50,16 @@ Backward Compatibility of Kconfig Options
|
||||
|
||||
The standard Kconfig_ tools ignore unknown options in ``sdkconfig``. So if a developer has custom settings for options which are renamed in newer ESP-IDF releases then the given setting for the option would be silently ignored. Therefore, several features have been adopted to avoid this:
|
||||
|
||||
1. ``confgen.py`` is used by the tool chain to pre-process ``sdkconfig`` files before anything else, for example
|
||||
1. ``kconfgen`` is used by the tool chain to pre-process ``sdkconfig`` files before anything else, for example
|
||||
``menuconfig``, would read them. As the consequence, the settings for old options will be kept and not ignored.
|
||||
2. ``confgen.py`` recursively finds all ``sdkconfig.rename`` files in ESP-IDF directory which contain old and new
|
||||
2. ``kconfgen`` recursively finds all ``sdkconfig.rename`` files in ESP-IDF directory which contain old and new
|
||||
``Kconfig`` option names. Old options are replaced by new ones in the ``sdkconfig`` file. Renames that should only appear for a single target can be placed in a target specific rename file: `sdkconfig.rename.TARGET`, where `TARGET` is the target name, e.g. `sdkconfig.rename.esp32s2`.
|
||||
3. ``confgen.py`` post-processes ``sdkconfig`` files and generates all build
|
||||
3. ``kconfgen`` post-processes ``sdkconfig`` files and generates all build
|
||||
outputs (``sdkconfig.h``, ``sdkconfig.cmake``, ``auto.conf``) by adding a list
|
||||
of compatibility statements, i.e. value of the old option is set the value of
|
||||
the new option (after modification). This is done in order to not break
|
||||
customer codes where old option might still be used.
|
||||
4. :ref:`configuration-deprecated-options` are automatically generated by ``confgen.py``.
|
||||
4. :ref:`configuration-deprecated-options` are automatically generated by ``kconfgen``.
|
||||
|
||||
.. _configuration-options-reference:
|
||||
|
||||
@ -74,5 +74,6 @@ By convention, all option names are upper case with underscores. When Kconfig ge
|
||||
.. include-build-file:: inc/kconfig.inc
|
||||
|
||||
.. _Kconfig: https://www.kernel.org/doc/Documentation/kbuild/kconfig-language.txt
|
||||
.. _esp-idf-kconfig: https://pypi.org/project/esp-idf-kconfig/
|
||||
.. _kconfiglib: https://github.com/ulfalizer/Kconfiglib
|
||||
.. _kconfiglib extentions: https://pypi.org/project/kconfiglib/#kconfig-extensions
|
||||
|
@ -129,24 +129,6 @@ ESP-IDF 适用于 Python 3.7 以上版本。
|
||||
|
||||
如果想在命令行中更优雅地管理 Python 的各个版本,请查看 pyenv_ 或 virtualenv_ 工具,它们会帮助您更改默认的 python 版本。
|
||||
|
||||
潜在问题
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
使用 ``idf.py`` 可能会出现如下 ``ImportError`` 错误:
|
||||
|
||||
.. code-block:: none
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "/Users/user_name/e/esp-idf/tools/kconfig_new/confgen.py"、 line 27、 in <module>
|
||||
import kconfiglib
|
||||
ImportError: bad magic number in 'kconfiglib': b'\x03\xf3\r\n'
|
||||
|
||||
该错误通常是由不同 Python 版本生成的 ``.pyc`` 文件引起的,可以通过运行以下命令解决该问题:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
idf.py python-clean
|
||||
|
||||
.. _example-project-structure:
|
||||
|
||||
示例项目
|
||||
@ -1359,11 +1341,11 @@ JSON 配置服务器
|
||||
|
||||
.. highlight :: json
|
||||
|
||||
``confserver.py`` 工具可以帮助 IDE 轻松地与配置系统的逻辑进行集成,它运行在后台,通过使用 stdin 和 stdout 读写 JSON 文件的方式与调用进程交互。
|
||||
``kconfserver`` 工具可以帮助 IDE 轻松地与配置系统的逻辑进行集成,它运行在后台,通过使用 stdin 和 stdout 读写 JSON 文件的方式与调用进程交互。
|
||||
|
||||
您可以通过 ``idf.py confserver`` 或 ``ninja confserver`` 从项目中运行 ``confserver.py``,也可以使用不同的构建生成器来触发类似的目标。
|
||||
您可以通过 ``idf.py confserver`` 或 ``ninja kconfserver`` 从项目中运行 ``kconfserver``,也可以使用不同的构建生成器来触发类似的目标。
|
||||
|
||||
有关 confserver.py 的更多信息,请参阅 :idf_file:`tools/kconfig_new/README.md`
|
||||
有关 kconfserver 的更多信息,请参阅 `esp-idf-kconfig documentation <https://github.com/espressif/esp-idf-kconfig/blob/master/docs/DOCUMENTATION.md>`_.
|
||||
|
||||
构建系统内部
|
||||
=======================
|
||||
|
@ -1749,11 +1749,6 @@ tools/catch/catch.hpp
|
||||
tools/find_build_apps/__init__.py
|
||||
tools/find_build_apps/cmake.py
|
||||
tools/find_build_apps/common.py
|
||||
tools/kconfig_new/confserver.py
|
||||
tools/kconfig_new/test/confgen/test_confgen.py
|
||||
tools/kconfig_new/test/confserver/test_confserver.py
|
||||
tools/kconfig_new/test/gen_kconfig_doc/test_kconfig_out.py
|
||||
tools/kconfig_new/test/gen_kconfig_doc/test_target_visibility.py
|
||||
tools/ldgen/entity.py
|
||||
tools/ldgen/ldgen_common.py
|
||||
tools/ldgen/output_commands.py
|
||||
|
@ -97,10 +97,6 @@ tools/idf_size.py
|
||||
tools/idf_tools.py
|
||||
tools/kconfig_new/confgen.py
|
||||
tools/kconfig_new/confserver.py
|
||||
tools/kconfig_new/test/confgen/test_confgen.py
|
||||
tools/kconfig_new/test/confserver/test_confserver.py
|
||||
tools/kconfig_new/test/gen_kconfig_doc/test_kconfig_out.py
|
||||
tools/kconfig_new/test/gen_kconfig_doc/test_target_visibility.py
|
||||
tools/ldgen/ldgen.py
|
||||
tools/ldgen/test/test_entity.py
|
||||
tools/ldgen/test/test_fragments.py
|
||||
|
@ -189,14 +189,6 @@ tools/esp_prov/transport/transport_console.py
|
||||
tools/esp_prov/transport/transport_http.py
|
||||
tools/gen_esp_err_to_name.py
|
||||
tools/gen_soc_caps_kconfig/test/test_gen_soc_caps_kconfig.py
|
||||
tools/kconfig_new/confgen.py
|
||||
tools/kconfig_new/confserver.py
|
||||
tools/kconfig_new/gen_kconfig_doc.py
|
||||
tools/kconfig_new/prepare_kconfig_files.py
|
||||
tools/kconfig_new/test/confgen/test_confgen.py
|
||||
tools/kconfig_new/test/confserver/test_confserver.py
|
||||
tools/kconfig_new/test/gen_kconfig_doc/test_kconfig_out.py
|
||||
tools/kconfig_new/test/gen_kconfig_doc/test_target_visibility.py
|
||||
tools/ldgen/ldgen.py
|
||||
tools/ldgen/ldgen/entity.py
|
||||
tools/ldgen/ldgen/fragments.py
|
||||
|
@ -691,7 +691,7 @@ endmenu\n" >> ${IDF_PATH}/Kconfig
|
||||
popd
|
||||
|
||||
|
||||
print_status "Confserver can be invoked by idf.py"
|
||||
print_status "Kconfserver can be invoked by idf.py"
|
||||
echo '{"version": 1}' | idf.py confserver || failure "Couldn't load confserver"
|
||||
|
||||
print_status "Check ccache is used to build"
|
||||
|
@ -127,8 +127,8 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
--list-separator=semicolon
|
||||
--env-file ${config_env_path})
|
||||
|
||||
set(confgen_basecommand
|
||||
${python} ${idf_path}/tools/kconfig_new/confgen.py
|
||||
set(kconfgen_basecommand
|
||||
${python} -m kconfgen
|
||||
--list-separator=semicolon
|
||||
--kconfig ${root_kconfig}
|
||||
--sdkconfig-rename ${root_sdkconfig_rename}
|
||||
@ -153,7 +153,7 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
execute_process(
|
||||
COMMAND ${prepare_kconfig_files_command})
|
||||
execute_process(
|
||||
COMMAND ${confgen_basecommand}
|
||||
COMMAND ${kconfgen_basecommand}
|
||||
--output header ${sdkconfig_header}
|
||||
--output cmake ${sdkconfig_cmake}
|
||||
--output json ${sdkconfig_json}
|
||||
@ -164,7 +164,7 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
execute_process(
|
||||
COMMAND ${prepare_kconfig_files_command})
|
||||
execute_process(
|
||||
COMMAND ${confgen_basecommand}
|
||||
COMMAND ${kconfgen_basecommand}
|
||||
--output header ${sdkconfig_header}
|
||||
--output cmake ${sdkconfig_cmake}
|
||||
--output json ${sdkconfig_json}
|
||||
@ -173,7 +173,7 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
endif()
|
||||
|
||||
if(config_result)
|
||||
message(FATAL_ERROR "Failed to run confgen.py (${confgen_basecommand}). Error ${config_result}")
|
||||
message(FATAL_ERROR "Failed to run kconfgen (${kconfgen_basecommand}). Error ${config_result}")
|
||||
endif()
|
||||
|
||||
# Add the generated config header to build specifications.
|
||||
@ -207,7 +207,7 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
${menuconfig_depends}
|
||||
# create any missing config file, with defaults if necessary
|
||||
COMMAND ${prepare_kconfig_files_command}
|
||||
COMMAND ${confgen_basecommand}
|
||||
COMMAND ${kconfgen_basecommand}
|
||||
--env "IDF_TARGET=${idf_target}"
|
||||
--env "IDF_ENV_FPGA=${idf_env_fpga}"
|
||||
--dont-write-deprecated
|
||||
@ -221,18 +221,18 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
"IDF_ENV_FPGA=${idf_env_fpga}"
|
||||
${MENUCONFIG_CMD} ${root_kconfig}
|
||||
USES_TERMINAL
|
||||
# additional run of confgen esures that the deprecated options will be inserted into sdkconfig (for backward
|
||||
# additional run of kconfgen esures that the deprecated options will be inserted into sdkconfig (for backward
|
||||
# compatibility)
|
||||
COMMAND ${confgen_basecommand}
|
||||
COMMAND ${kconfgen_basecommand}
|
||||
--env "IDF_TARGET=${idf_target}"
|
||||
--env "IDF_ENV_FPGA=${idf_env_fpga}"
|
||||
--output config ${sdkconfig}
|
||||
)
|
||||
|
||||
# Custom target to run confserver.py from the build tool
|
||||
# Custom target to run kconfserver from the build tool
|
||||
add_custom_target(confserver
|
||||
COMMAND ${prepare_kconfig_files_command}
|
||||
COMMAND ${PYTHON} ${IDF_PATH}/tools/kconfig_new/confserver.py
|
||||
COMMAND ${python} -m kconfserver
|
||||
--env-file ${config_env_path}
|
||||
--kconfig ${IDF_PATH}/Kconfig
|
||||
--sdkconfig-rename ${root_sdkconfig_rename}
|
||||
@ -242,7 +242,7 @@ function(__kconfig_generate_config sdkconfig sdkconfig_defaults)
|
||||
|
||||
add_custom_target(save-defconfig
|
||||
COMMAND ${prepare_kconfig_files_command}
|
||||
COMMAND ${confgen_basecommand}
|
||||
COMMAND ${kconfgen_basecommand}
|
||||
--dont-write-deprecated
|
||||
--output savedefconfig ${CMAKE_SOURCE_DIR}/sdkconfig.defaults
|
||||
USES_TERMINAL
|
||||
|
@ -1,120 +0,0 @@
|
||||
# kconfig_new
|
||||
|
||||
kconfig_new is the kconfig support used by the CMake-based build system.
|
||||
|
||||
It depends on the [kconfiglib](https://github.com/ulfalizer/Kconfiglib) package.
|
||||
|
||||
## confserver.py
|
||||
|
||||
confserver.py is a small Python program intended to support IDEs and other clients who want to allow editing sdkconfig, without needing to reproduce all of the kconfig logic in a particular program.
|
||||
|
||||
After launching confserver.py (which can be done via `idf.py confserver` command or `confserver` build target in ninja/make), the confserver communicates via JSON sent/received on stdout. Out-of-band errors are logged via stderr.
|
||||
|
||||
### Configuration Structure
|
||||
|
||||
During cmake run, the CMake-based build system produces a number of metadata files including `build/config/kconfig_menus.json`, which is a JSON representation of all the menu items in the project configuration and their structure.
|
||||
|
||||
This format is currently undocumented, however running CMake with an IDF project will give an indication of the format. The format is expected to be stable.
|
||||
|
||||
### Initial Process
|
||||
|
||||
After initializing, the server will print "Server running, waiting for requests on stdin..." on stderr.
|
||||
|
||||
Then it will print a JSON dictionary on stdout, representing the initial state of sdkconfig:
|
||||
|
||||
```
|
||||
{
|
||||
"version": 2,
|
||||
"ranges": {
|
||||
"TEST_CONDITIONAL_RANGES": [0, 10] },
|
||||
"visible": { "TEST_CONDITIONAL_RANGES": true,
|
||||
"CHOICE_A": true,
|
||||
"test-config-submenu": true },
|
||||
"values": { "TEST_CONDITIONAL_RANGES": 1,
|
||||
"CHOICE_A": true },
|
||||
}
|
||||
```
|
||||
|
||||
(Note: actual output is not pretty-printed and will print on a single line. Order of dictionary keys is undefined.)
|
||||
|
||||
* "version" key is the protocol version in use.
|
||||
* "ranges" holds is a dictionary for any config symbol which has a valid integer range. The array value has two values for min/max.
|
||||
* "visible" holds a dictionary showing initial visibility status of config symbols (identified by the config symbol name) and menus (which don't represent a symbol but are represented as an id 'slug'). Both these names (symbol name and menu slug) correspond to the 'id' key in kconfig_menus.json.
|
||||
* "values" holds a dictionary showing initial values of all config symbols. Invisible symbols are not included here.
|
||||
|
||||
### Interaction
|
||||
|
||||
Interaction consists of the client sending JSON dictionary "requests" to the server one at a time. The server will respond to each request with a JSON dictionary response. Interaction is done when the client closes stdout (at this point the server will exit).
|
||||
|
||||
Requests look like:
|
||||
|
||||
```
|
||||
{ "version": 2,
|
||||
"set": { "TEST_CHILD_STR": "New value",
|
||||
"TEST_BOOL": true }
|
||||
}
|
||||
```
|
||||
|
||||
Note: Requests don't need to be pretty-printed, they just need to be valid JSON.
|
||||
|
||||
The `version` key *must* be present in the request and must match a protocol version supported by confserver.
|
||||
|
||||
The `set` key is optional. If present, its value must be a dictionary of new values to set on kconfig symbols.
|
||||
|
||||
Additional optional keys:
|
||||
|
||||
* `load`: If this key is set, sdkconfig file will be reloaded from filesystem before any values are set applied. The value of this key can be a filename, in which case configuration will be loaded from this file. If the value of this key is `null`, configuration will be loaded from the last used file. The response to a "load" command is always the full set of config values and ranges, the same as when the server is initially started.
|
||||
|
||||
* `save`: If this key is set, sdkconfig file will be saved after any values are set. Similar to `load`, the value of this key can be a filename to save to a particular file, or `null` to reuse the last used file.
|
||||
|
||||
After a request is processed, a response is printed to stdout similar to this:
|
||||
|
||||
```
|
||||
{ "version": 2,
|
||||
"ranges": {},
|
||||
"visible": { "test-config-submenu": false},
|
||||
"values": { "SUBMENU_TRIGGER": false }
|
||||
}
|
||||
```
|
||||
|
||||
* `version` is the protocol version used by the server.
|
||||
* `ranges` contains any changed ranges, where the new range of the config symbol has changed (due to some other configuration change or because a new sdkconfig has been loaded).
|
||||
* `visible` contains any visibility changes, where the visible config symbols have changed.
|
||||
* `values` contains any value changes, where a config symbol value has changed. This may be due to an explicit change (ie the client `set` this value), or a change caused by some other change in the config system. Note that a change which is set by the client may not be reflected exactly the same in the response, due to restrictions on allowed values which are enforced by the config server. Invalid changes are ignored by the config server.
|
||||
|
||||
If setting a value also changes the possible range of values that an item can have, this is also represented with a dictionary `ranges` that contains key/value pairs of config items to their new ranges:
|
||||
|
||||
```
|
||||
{ "version": 2,
|
||||
"values": {"OTHER_NAME": true },
|
||||
"visible": { },
|
||||
"ranges" : { "HAS_RANGE" : [ 3, 4 ] } }
|
||||
```
|
||||
|
||||
Note: The configuration server does not automatically load any changes which are applied externally to the `sdkconfig` file. Send a "load" command or restart the server if the file is externally edited.
|
||||
|
||||
Note: The configuration server does not re-run CMake to regenerate other build files or metadata files after `sdkconfig` is updated. This will happen automatically the next time ``CMake`` or ``idf.py`` is run.
|
||||
|
||||
### KConfig Item Types
|
||||
|
||||
* `string` types are represented as JSON strings.
|
||||
* `bool` and `tristate` types are represented as JSON Booleans, the third `tristate` state is not supported.
|
||||
* `int` types are represented as JSON integers
|
||||
* `hex` types are also represented as JSON integers, clients should read the separate metadata file to know if the UI representation is `int` or `hex`. It is possible to set a `hex` item by sending the server a JSON string of hex digits (no prefix) as the value, but the server always sends `hex` values as JSON integers.
|
||||
|
||||
### Error Responses
|
||||
|
||||
In some cases, a request may lead to an error message. In this case, the error message is printed to stderr but an array of errors is also returned in the `error` key of the response:
|
||||
|
||||
```
|
||||
{ "version": 777,
|
||||
"error": [ "Unsupported request version 777. Server supports versions 1-2" ]
|
||||
}
|
||||
```
|
||||
|
||||
These error messages are intended to be human readable, not machine parseable.
|
||||
|
||||
### Protocol Version Changes
|
||||
|
||||
* V2: Added the `visible` key to the response. Invisible items are no longer represented as having value null.
|
||||
* V2: `load` now sends changes compared to values before the load, not the whole list of config items.
|
591
tools/kconfig_new/confgen.py
Executable file → Normal file
591
tools/kconfig_new/confgen.py
Executable file → Normal file
@ -1,594 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Command line tool to take in ESP-IDF sdkconfig files with project
|
||||
# settings and output data in multiple formats (update config, generate
|
||||
# header file, generate .cmake include file, documentation, etc).
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
#
|
||||
# Used internally by the ESP-IDF build system. But designed to be
|
||||
# non-IDF-specific.
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2018-2021 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
from collections import defaultdict
|
||||
|
||||
import gen_kconfig_doc
|
||||
import kconfiglib
|
||||
|
||||
__version__ = '0.1'
|
||||
|
||||
|
||||
class DeprecatedOptions(object):
|
||||
_REN_FILE = 'sdkconfig.rename'
|
||||
_DEP_OP_BEGIN = '# Deprecated options for backward compatibility'
|
||||
_DEP_OP_END = '# End of deprecated options'
|
||||
_RE_DEP_OP_BEGIN = re.compile(_DEP_OP_BEGIN)
|
||||
_RE_DEP_OP_END = re.compile(_DEP_OP_END)
|
||||
|
||||
def __init__(self, config_prefix, path_rename_files=[]):
|
||||
self.config_prefix = config_prefix
|
||||
# r_dic maps deprecated options to new options; rev_r_dic maps in the opposite direction
|
||||
self.r_dic, self.rev_r_dic = self._parse_replacements(path_rename_files)
|
||||
|
||||
# note the '=' at the end of regex for not getting partial match of configs.
|
||||
# Also match if the config option is followed by a whitespace, this is the case
|
||||
# in sdkconfig.defaults files contaning "# CONFIG_MMM_NNN is not set".
|
||||
self._RE_CONFIG = re.compile(r'{}(\w+)(=|\s+)'.format(self.config_prefix))
|
||||
|
||||
def _parse_replacements(self, repl_paths):
|
||||
rep_dic = {}
|
||||
rev_rep_dic = defaultdict(list)
|
||||
|
||||
def remove_config_prefix(string):
|
||||
if string.startswith(self.config_prefix):
|
||||
return string[len(self.config_prefix):]
|
||||
raise RuntimeError('Error in {} (line {}): Config {} is not prefixed with {}'
|
||||
''.format(rep_path, line_number, string, self.config_prefix))
|
||||
|
||||
for rep_path in repl_paths:
|
||||
with open(rep_path) as f_rep:
|
||||
for line_number, line in enumerate(f_rep, start=1):
|
||||
sp_line = line.split()
|
||||
if len(sp_line) == 0 or sp_line[0].startswith('#'):
|
||||
# empty line or comment
|
||||
continue
|
||||
if len(sp_line) != 2 or not all(x.startswith(self.config_prefix) for x in sp_line):
|
||||
raise RuntimeError('Syntax error in {} (line {})'.format(rep_path, line_number))
|
||||
if sp_line[0] in rep_dic:
|
||||
raise RuntimeError('Error in {} (line {}): Replacement {} exist for {} and new '
|
||||
'replacement {} is defined'.format(rep_path, line_number,
|
||||
rep_dic[sp_line[0]], sp_line[0],
|
||||
sp_line[1]))
|
||||
|
||||
(dep_opt, new_opt) = (remove_config_prefix(x) for x in sp_line)
|
||||
rep_dic[dep_opt] = new_opt
|
||||
rev_rep_dic[new_opt].append(dep_opt)
|
||||
return rep_dic, rev_rep_dic
|
||||
|
||||
def get_deprecated_option(self, new_option):
|
||||
return self.rev_r_dic.get(new_option, [])
|
||||
|
||||
def get_new_option(self, deprecated_option):
|
||||
return self.r_dic.get(deprecated_option, None)
|
||||
|
||||
def replace(self, sdkconfig_in, sdkconfig_out):
|
||||
replace_enabled = True
|
||||
with open(sdkconfig_in, 'r') as f_in, open(sdkconfig_out, 'w') as f_out:
|
||||
for line_num, line in enumerate(f_in, start=1):
|
||||
if self._RE_DEP_OP_BEGIN.search(line):
|
||||
replace_enabled = False
|
||||
elif self._RE_DEP_OP_END.search(line):
|
||||
replace_enabled = True
|
||||
elif replace_enabled:
|
||||
m = self._RE_CONFIG.search(line)
|
||||
if m and m.group(1) in self.r_dic:
|
||||
depr_opt = self.config_prefix + m.group(1)
|
||||
new_opt = self.config_prefix + self.r_dic[m.group(1)]
|
||||
line = line.replace(depr_opt, new_opt)
|
||||
print('{}:{} {} was replaced with {}'.format(sdkconfig_in, line_num, depr_opt, new_opt))
|
||||
f_out.write(line)
|
||||
|
||||
def append_doc(self, config, visibility, path_output):
|
||||
|
||||
def option_was_written(opt):
|
||||
# named choices were written if any of the symbols in the choice were visible
|
||||
if new_opt in config.named_choices:
|
||||
syms = config.named_choices[new_opt].syms
|
||||
for s in syms:
|
||||
if any(visibility.visible(node) for node in s.nodes):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
# otherwise if any of the nodes associated with the option was visible
|
||||
return any(visibility.visible(node) for node in config.syms[opt].nodes)
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if len(self.r_dic) > 0:
|
||||
with open(path_output, 'a') as f_o:
|
||||
header = 'Deprecated options and their replacements'
|
||||
f_o.write('.. _configuration-deprecated-options:\n\n{}\n{}\n\n'.format(header, '-' * len(header)))
|
||||
for dep_opt in sorted(self.r_dic):
|
||||
new_opt = self.r_dic[dep_opt]
|
||||
if option_was_written(new_opt) and (new_opt not in config.syms or config.syms[new_opt].choice is None):
|
||||
# everything except config for a choice (no link reference for those in the docs)
|
||||
f_o.write('- {}{} (:ref:`{}{}`)\n'.format(config.config_prefix, dep_opt,
|
||||
config.config_prefix, new_opt))
|
||||
|
||||
if new_opt in config.named_choices:
|
||||
# here are printed config options which were filtered out
|
||||
syms = config.named_choices[new_opt].syms
|
||||
for sym in syms:
|
||||
if sym.name in self.rev_r_dic:
|
||||
# only if the symbol has been renamed
|
||||
dep_names = self.rev_r_dic[sym.name]
|
||||
dep_names = [config.config_prefix + name for name in dep_names]
|
||||
# config options doesn't have references
|
||||
f_o.write(' - {}\n'.format(', '.join(dep_names)))
|
||||
|
||||
def append_config(self, config, path_output):
|
||||
tmp_list = []
|
||||
|
||||
def append_config_node_process(node):
|
||||
item = node.item
|
||||
if isinstance(item, kconfiglib.Symbol) and item.env_var is None:
|
||||
if item.name in self.rev_r_dic:
|
||||
c_string = item.config_string
|
||||
if c_string:
|
||||
for dep_name in self.rev_r_dic[item.name]:
|
||||
tmp_list.append(c_string.replace(self.config_prefix + item.name,
|
||||
self.config_prefix + dep_name))
|
||||
|
||||
for n in config.node_iter():
|
||||
append_config_node_process(n)
|
||||
|
||||
if len(tmp_list) > 0:
|
||||
with open(path_output, 'a') as f_o:
|
||||
f_o.write('\n{}\n'.format(self._DEP_OP_BEGIN))
|
||||
f_o.writelines(tmp_list)
|
||||
f_o.write('{}\n'.format(self._DEP_OP_END))
|
||||
|
||||
def append_header(self, config, path_output):
|
||||
def _opt_defined(opt):
|
||||
if opt.orig_type in (kconfiglib.BOOL, kconfiglib.TRISTATE) and opt.str_value != 'n':
|
||||
opt_defined = True
|
||||
elif opt.orig_type in (kconfiglib.INT, kconfiglib.STRING, kconfiglib.HEX) and opt.str_value != '':
|
||||
opt_defined = True
|
||||
else:
|
||||
opt_defined = False
|
||||
return opt_defined
|
||||
|
||||
if len(self.r_dic) > 0:
|
||||
with open(path_output, 'a') as f_o:
|
||||
f_o.write('\n/* List of deprecated options */\n')
|
||||
for dep_opt in sorted(self.r_dic):
|
||||
new_opt = self.r_dic[dep_opt]
|
||||
if new_opt in config.syms and _opt_defined(config.syms[new_opt]):
|
||||
f_o.write('#define {}{} {}{}\n'.format(self.config_prefix, dep_opt, self.config_prefix, new_opt))
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='confgen.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0]))
|
||||
|
||||
parser.add_argument('--config',
|
||||
help='Project configuration settings',
|
||||
nargs='?',
|
||||
default=None)
|
||||
|
||||
parser.add_argument('--defaults',
|
||||
help='Optional project defaults file, used if --config file doesn\'t exist. '
|
||||
'Multiple files can be specified using multiple --defaults arguments.',
|
||||
nargs='?',
|
||||
default=[],
|
||||
action='append')
|
||||
|
||||
parser.add_argument('--kconfig',
|
||||
help='KConfig file with config item definitions',
|
||||
required=True)
|
||||
|
||||
parser.add_argument('--sdkconfig-rename',
|
||||
help='File with deprecated Kconfig options',
|
||||
required=False)
|
||||
|
||||
parser.add_argument('--dont-write-deprecated',
|
||||
help='Do not write compatibility statements for deprecated values',
|
||||
action='store_true')
|
||||
|
||||
parser.add_argument('--output', nargs=2, action='append',
|
||||
help='Write output file (format and output filename)',
|
||||
metavar=('FORMAT', 'FILENAME'),
|
||||
default=[])
|
||||
|
||||
parser.add_argument('--env', action='append', default=[],
|
||||
help='Environment to set when evaluating the config file', metavar='NAME=VAL')
|
||||
|
||||
parser.add_argument('--env-file', type=argparse.FileType('r'),
|
||||
help='Optional file to load environment variables from. Contents '
|
||||
'should be a JSON object where each key/value pair is a variable.')
|
||||
|
||||
parser.add_argument('--list-separator', choices=['space', 'semicolon'],
|
||||
default='space',
|
||||
help='Separator used in environment list variables (COMPONENT_SDKCONFIG_RENAMES)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
for fmt, filename in args.output:
|
||||
if fmt not in OUTPUT_FORMATS.keys():
|
||||
print("Format '%s' not recognised. Known formats: %s" % (fmt, OUTPUT_FORMATS.keys()))
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
args.env = [(name,value) for (name,value) in (e.split('=',1) for e in args.env)]
|
||||
except ValueError:
|
||||
print("--env arguments must each contain =. To unset an environment variable, use 'ENV='")
|
||||
sys.exit(1)
|
||||
|
||||
for name, value in args.env:
|
||||
os.environ[name] = value
|
||||
|
||||
if args.env_file is not None:
|
||||
env = json.load(args.env_file)
|
||||
os.environ.update(env)
|
||||
|
||||
config = kconfiglib.Kconfig(args.kconfig)
|
||||
config.warn_assign_redun = False
|
||||
config.warn_assign_override = False
|
||||
|
||||
sdkconfig_renames_sep = ';' if args.list_separator == 'semicolon' else ' '
|
||||
|
||||
sdkconfig_renames = [args.sdkconfig_rename] if args.sdkconfig_rename else []
|
||||
sdkconfig_renames_from_env = os.environ.get('COMPONENT_SDKCONFIG_RENAMES')
|
||||
if sdkconfig_renames_from_env:
|
||||
sdkconfig_renames += sdkconfig_renames_from_env.split(sdkconfig_renames_sep)
|
||||
deprecated_options = DeprecatedOptions(config.config_prefix, path_rename_files=sdkconfig_renames)
|
||||
|
||||
if len(args.defaults) > 0:
|
||||
def _replace_empty_assignments(path_in, path_out):
|
||||
with open(path_in, 'r') as f_in, open(path_out, 'w') as f_out:
|
||||
for line_num, line in enumerate(f_in, start=1):
|
||||
line = line.strip()
|
||||
if line.endswith('='):
|
||||
line += 'n'
|
||||
print('{}:{} line was updated to {}'.format(path_out, line_num, line))
|
||||
f_out.write(line)
|
||||
f_out.write('\n')
|
||||
|
||||
# always load defaults first, so any items which are not defined in that config
|
||||
# will have the default defined in the defaults file
|
||||
for name in args.defaults:
|
||||
print('Loading defaults file %s...' % name)
|
||||
if not os.path.exists(name):
|
||||
raise RuntimeError('Defaults file not found: %s' % name)
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(prefix='confgen_tmp', delete=False) as f:
|
||||
temp_file1 = f.name
|
||||
with tempfile.NamedTemporaryFile(prefix='confgen_tmp', delete=False) as f:
|
||||
temp_file2 = f.name
|
||||
deprecated_options.replace(sdkconfig_in=name, sdkconfig_out=temp_file1)
|
||||
_replace_empty_assignments(temp_file1, temp_file2)
|
||||
config.load_config(temp_file2, replace=False)
|
||||
finally:
|
||||
try:
|
||||
os.remove(temp_file1)
|
||||
os.remove(temp_file2)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# If config file previously exists, load it
|
||||
if args.config and os.path.exists(args.config):
|
||||
# ... but replace deprecated options before that
|
||||
with tempfile.NamedTemporaryFile(prefix='confgen_tmp', delete=False) as f:
|
||||
temp_file = f.name
|
||||
try:
|
||||
deprecated_options.replace(sdkconfig_in=args.config, sdkconfig_out=temp_file)
|
||||
config.load_config(temp_file, replace=False)
|
||||
update_if_changed(temp_file, args.config)
|
||||
finally:
|
||||
try:
|
||||
os.remove(temp_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if args.dont_write_deprecated:
|
||||
# The deprecated object was useful until now for replacements. Now it will be redefined with no configurations
|
||||
# and as the consequence, it won't generate output with deprecated statements.
|
||||
deprecated_options = DeprecatedOptions('', path_rename_files=[])
|
||||
|
||||
# Output the files specified in the arguments
|
||||
for output_type, filename in args.output:
|
||||
with tempfile.NamedTemporaryFile(prefix='confgen_tmp', delete=False) as f:
|
||||
temp_file = f.name
|
||||
try:
|
||||
output_function = OUTPUT_FORMATS[output_type]
|
||||
output_function(deprecated_options, config, temp_file)
|
||||
update_if_changed(temp_file, filename)
|
||||
finally:
|
||||
try:
|
||||
os.remove(temp_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def write_config(deprecated_options, config, filename):
|
||||
CONFIG_HEADING = """#
|
||||
# Automatically generated file. DO NOT EDIT.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Configuration
|
||||
#
|
||||
"""
|
||||
config.write_config(filename, header=CONFIG_HEADING)
|
||||
deprecated_options.append_config(config, filename)
|
||||
|
||||
|
||||
def write_min_config(deprecated_options, config, filename):
|
||||
target_symbol = config.syms['IDF_TARGET']
|
||||
# 'esp32` is harcoded here because the default value of IDF_TARGET is set on the first run from the environment
|
||||
# variable. I.E. `esp32 is not defined as default value.
|
||||
write_target = target_symbol.str_value != 'esp32'
|
||||
|
||||
CONFIG_HEADING = textwrap.dedent('''\
|
||||
# This file was generated using idf.py save-defconfig. It can be edited manually.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
|
||||
#
|
||||
{}\
|
||||
'''.format(target_symbol.config_string if write_target else ''))
|
||||
config.write_min_config(filename, header=CONFIG_HEADING)
|
||||
|
||||
|
||||
def write_header(deprecated_options, config, filename):
|
||||
CONFIG_HEADING = """/*
|
||||
* Automatically generated file. DO NOT EDIT.
|
||||
* Espressif IoT Development Framework (ESP-IDF) Configuration Header
|
||||
*/
|
||||
#pragma once
|
||||
"""
|
||||
config.write_autoconf(filename, header=CONFIG_HEADING)
|
||||
deprecated_options.append_header(config, filename)
|
||||
|
||||
|
||||
def write_cmake(deprecated_options, config, filename):
|
||||
with open(filename, 'w') as f:
|
||||
tmp_dep_list = []
|
||||
write = f.write
|
||||
prefix = config.config_prefix
|
||||
|
||||
write("""#
|
||||
# Automatically generated file. DO NOT EDIT.
|
||||
# Espressif IoT Development Framework (ESP-IDF) Configuration cmake include file
|
||||
#
|
||||
""")
|
||||
|
||||
configs_list = list()
|
||||
|
||||
def write_node(node):
|
||||
sym = node.item
|
||||
if not isinstance(sym, kconfiglib.Symbol):
|
||||
return
|
||||
|
||||
if sym.config_string:
|
||||
val = sym.str_value
|
||||
if sym.orig_type in (kconfiglib.BOOL, kconfiglib.TRISTATE) and val == 'n':
|
||||
val = '' # write unset values as empty variables
|
||||
elif sym.orig_type == kconfiglib.STRING:
|
||||
val = kconfiglib.escape(val)
|
||||
elif sym.orig_type == kconfiglib.HEX:
|
||||
val = hex(int(val, 16)) # ensure 0x prefix
|
||||
write('set({}{} "{}")\n'.format(prefix, sym.name, val))
|
||||
|
||||
configs_list.append(prefix + sym.name)
|
||||
dep_opts = deprecated_options.get_deprecated_option(sym.name)
|
||||
for opt in dep_opts:
|
||||
tmp_dep_list.append('set({}{} "{}")\n'.format(prefix, opt, val))
|
||||
configs_list.append(prefix + opt)
|
||||
|
||||
for n in config.node_iter():
|
||||
write_node(n)
|
||||
write('set(CONFIGS_LIST {})'.format(';'.join(configs_list)))
|
||||
|
||||
if len(tmp_dep_list) > 0:
|
||||
write('\n# List of deprecated options for backward compatibility\n')
|
||||
f.writelines(tmp_dep_list)
|
||||
|
||||
|
||||
def get_json_values(config):
|
||||
config_dict = {}
|
||||
|
||||
def write_node(node):
|
||||
sym = node.item
|
||||
if not isinstance(sym, kconfiglib.Symbol):
|
||||
return
|
||||
|
||||
if sym.config_string:
|
||||
val = sym.str_value
|
||||
if sym.type in [kconfiglib.BOOL, kconfiglib.TRISTATE]:
|
||||
val = (val != 'n')
|
||||
elif sym.type == kconfiglib.HEX:
|
||||
val = int(val, 16)
|
||||
elif sym.type == kconfiglib.INT:
|
||||
val = int(val)
|
||||
config_dict[sym.name] = val
|
||||
for n in config.node_iter(False):
|
||||
write_node(n)
|
||||
return config_dict
|
||||
|
||||
|
||||
def write_json(deprecated_options, config, filename):
|
||||
config_dict = get_json_values(config)
|
||||
with open(filename, 'w') as f:
|
||||
json.dump(config_dict, f, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
def get_menu_node_id(node):
|
||||
""" Given a menu node, return a unique id
|
||||
which can be used to identify it in the menu structure
|
||||
|
||||
Will either be the config symbol name, or a menu identifier
|
||||
'slug'
|
||||
|
||||
"""
|
||||
try:
|
||||
if not isinstance(node.item, kconfiglib.Choice):
|
||||
return node.item.name
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
result = []
|
||||
while node.parent is not None:
|
||||
slug = re.sub(r'\W+', '-', node.prompt[0]).lower()
|
||||
result.append(slug)
|
||||
node = node.parent
|
||||
|
||||
result = '-'.join(reversed(result))
|
||||
return result
|
||||
|
||||
|
||||
def write_json_menus(deprecated_options, config, filename):
|
||||
existing_ids = set()
|
||||
result = [] # root level items
|
||||
node_lookup = {} # lookup from MenuNode to an item in result
|
||||
|
||||
def write_node(node):
|
||||
try:
|
||||
json_parent = node_lookup[node.parent]['children']
|
||||
except KeyError:
|
||||
assert node.parent not in node_lookup # if fails, we have a parent node with no "children" entity (ie a bug)
|
||||
json_parent = result # root level node
|
||||
|
||||
# node.kconfig.y means node has no dependency,
|
||||
if node.dep is node.kconfig.y:
|
||||
depends = None
|
||||
else:
|
||||
depends = kconfiglib.expr_str(node.dep)
|
||||
|
||||
try:
|
||||
# node.is_menuconfig is True in newer kconfiglibs for menus and choices as well
|
||||
is_menuconfig = node.is_menuconfig and isinstance(node.item, kconfiglib.Symbol)
|
||||
except AttributeError:
|
||||
is_menuconfig = False
|
||||
|
||||
new_json = None
|
||||
if node.item == kconfiglib.MENU or is_menuconfig:
|
||||
new_json = {'type': 'menu',
|
||||
'title': node.prompt[0],
|
||||
'depends_on': depends,
|
||||
'children': [],
|
||||
}
|
||||
if is_menuconfig:
|
||||
sym = node.item
|
||||
new_json['name'] = sym.name
|
||||
new_json['help'] = node.help
|
||||
new_json['is_menuconfig'] = is_menuconfig
|
||||
greatest_range = None
|
||||
if len(sym.ranges) > 0:
|
||||
# Note: Evaluating the condition using kconfiglib's expr_value
|
||||
# should have one condition which is true
|
||||
for min_range, max_range, cond_expr in sym.ranges:
|
||||
if kconfiglib.expr_value(cond_expr):
|
||||
greatest_range = [min_range, max_range]
|
||||
new_json['range'] = greatest_range
|
||||
|
||||
elif isinstance(node.item, kconfiglib.Symbol):
|
||||
sym = node.item
|
||||
greatest_range = None
|
||||
if len(sym.ranges) > 0:
|
||||
# Note: Evaluating the condition using kconfiglib's expr_value
|
||||
# should have one condition which is true
|
||||
for min_range, max_range, cond_expr in sym.ranges:
|
||||
if kconfiglib.expr_value(cond_expr):
|
||||
base = 16 if sym.type == kconfiglib.HEX else 10
|
||||
greatest_range = [int(min_range.str_value, base), int(max_range.str_value, base)]
|
||||
break
|
||||
|
||||
new_json = {
|
||||
'type': kconfiglib.TYPE_TO_STR[sym.type],
|
||||
'name': sym.name,
|
||||
'title': node.prompt[0] if node.prompt else None,
|
||||
'depends_on': depends,
|
||||
'help': node.help,
|
||||
'range': greatest_range,
|
||||
'children': [],
|
||||
}
|
||||
elif isinstance(node.item, kconfiglib.Choice):
|
||||
choice = node.item
|
||||
new_json = {
|
||||
'type': 'choice',
|
||||
'title': node.prompt[0],
|
||||
'name': choice.name,
|
||||
'depends_on': depends,
|
||||
'help': node.help,
|
||||
'children': []
|
||||
}
|
||||
|
||||
if new_json:
|
||||
node_id = get_menu_node_id(node)
|
||||
if node_id in existing_ids:
|
||||
raise RuntimeError('Config file contains two items with the same id: %s (%s). ' +
|
||||
'Please rename one of these items to avoid ambiguity.' % (node_id, node.prompt[0]))
|
||||
new_json['id'] = node_id
|
||||
|
||||
json_parent.append(new_json)
|
||||
node_lookup[node] = new_json
|
||||
|
||||
for n in config.node_iter():
|
||||
write_node(n)
|
||||
with open(filename, 'w') as f:
|
||||
f.write(json.dumps(result, sort_keys=True, indent=4))
|
||||
|
||||
|
||||
def write_docs(deprecated_options, config, filename):
|
||||
try:
|
||||
target = os.environ['IDF_TARGET']
|
||||
except KeyError:
|
||||
print('IDF_TARGET environment variable must be defined!')
|
||||
sys.exit(1)
|
||||
|
||||
visibility = gen_kconfig_doc.ConfigTargetVisibility(config, target)
|
||||
gen_kconfig_doc.write_docs(config, visibility, filename)
|
||||
deprecated_options.append_doc(config, visibility, filename)
|
||||
|
||||
|
||||
def update_if_changed(source, destination):
|
||||
with open(source, 'r') as f:
|
||||
source_contents = f.read()
|
||||
|
||||
if os.path.exists(destination):
|
||||
with open(destination, 'r') as f:
|
||||
dest_contents = f.read()
|
||||
if source_contents == dest_contents:
|
||||
return # nothing to update
|
||||
|
||||
with open(destination, 'w') as f:
|
||||
f.write(source_contents)
|
||||
|
||||
|
||||
OUTPUT_FORMATS = {'config': write_config,
|
||||
'header': write_header,
|
||||
'cmake': write_cmake,
|
||||
'docs': write_docs,
|
||||
'json': write_json,
|
||||
'json_menus': write_json_menus,
|
||||
'savedefconfig': write_min_config,
|
||||
}
|
||||
|
||||
|
||||
class FatalError(RuntimeError):
|
||||
"""
|
||||
Class for runtime errors (not caused by bugs but by user input).
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except FatalError as e:
|
||||
print('A fatal error occurred: %s' % e)
|
||||
sys.exit(2)
|
||||
sys.exit(subprocess.run([sys.executable, '-m', 'kconfgen'] + sys.argv[1:]).returncode)
|
||||
|
326
tools/kconfig_new/confserver.py
Executable file → Normal file
326
tools/kconfig_new/confserver.py
Executable file → Normal file
@ -1,325 +1,11 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Long-running server process uses stdin & stdout to communicate JSON
|
||||
# with a caller
|
||||
# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD
|
||||
#
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import confgen
|
||||
import kconfiglib
|
||||
from confgen import FatalError, __version__
|
||||
|
||||
# Min/Max supported protocol versions
|
||||
MIN_PROTOCOL_VERSION = 1
|
||||
MAX_PROTOCOL_VERSION = 2
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='confserver.py v%s - Config Generation Tool' % __version__, prog=os.path.basename(sys.argv[0]))
|
||||
|
||||
parser.add_argument('--config',
|
||||
help='Project configuration settings',
|
||||
required=True)
|
||||
|
||||
parser.add_argument('--kconfig',
|
||||
help='KConfig file with config item definitions',
|
||||
required=True)
|
||||
|
||||
parser.add_argument('--sdkconfig-rename',
|
||||
help='File with deprecated Kconfig options',
|
||||
required=False)
|
||||
|
||||
parser.add_argument('--env', action='append', default=[],
|
||||
help='Environment to set when evaluating the config file', metavar='NAME=VAL')
|
||||
|
||||
parser.add_argument('--env-file', type=argparse.FileType('r'),
|
||||
help='Optional file to load environment variables from. Contents '
|
||||
'should be a JSON object where each key/value pair is a variable.')
|
||||
|
||||
parser.add_argument('--version', help='Set protocol version to use on initial status',
|
||||
type=int, default=MAX_PROTOCOL_VERSION)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.version < MIN_PROTOCOL_VERSION:
|
||||
print('Version %d is older than minimum supported protocol version %d. Client is much older than ESP-IDF version?' %
|
||||
(args.version, MIN_PROTOCOL_VERSION))
|
||||
|
||||
if args.version > MAX_PROTOCOL_VERSION:
|
||||
print('Version %d is newer than maximum supported protocol version %d. Client is newer than ESP-IDF version?' %
|
||||
(args.version, MAX_PROTOCOL_VERSION))
|
||||
|
||||
try:
|
||||
args.env = [(name,value) for (name,value) in (e.split('=',1) for e in args.env)]
|
||||
except ValueError:
|
||||
print("--env arguments must each contain =. To unset an environment variable, use 'ENV='")
|
||||
sys.exit(1)
|
||||
|
||||
for name, value in args.env:
|
||||
os.environ[name] = value
|
||||
|
||||
if args.env_file is not None:
|
||||
env = json.load(args.env_file)
|
||||
os.environ.update(env)
|
||||
|
||||
run_server(args.kconfig, args.config, args.sdkconfig_rename)
|
||||
|
||||
|
||||
def run_server(kconfig, sdkconfig, sdkconfig_rename, default_version=MAX_PROTOCOL_VERSION):
|
||||
config = kconfiglib.Kconfig(kconfig)
|
||||
sdkconfig_renames = [sdkconfig_rename] if sdkconfig_rename else []
|
||||
sdkconfig_renames_from_env = os.environ.get('COMPONENT_SDKCONFIG_RENAMES')
|
||||
if sdkconfig_renames_from_env:
|
||||
sdkconfig_renames += sdkconfig_renames_from_env.split(';')
|
||||
deprecated_options = confgen.DeprecatedOptions(config.config_prefix, path_rename_files=sdkconfig_renames)
|
||||
f_o = tempfile.NamedTemporaryFile(mode='w+b', delete=False)
|
||||
try:
|
||||
with open(sdkconfig, mode='rb') as f_i:
|
||||
f_o.write(f_i.read())
|
||||
f_o.close() # need to close as DeprecatedOptions will reopen, and Windows only allows one open file
|
||||
deprecated_options.replace(sdkconfig_in=f_o.name, sdkconfig_out=sdkconfig)
|
||||
finally:
|
||||
os.unlink(f_o.name)
|
||||
config.load_config(sdkconfig)
|
||||
|
||||
print('Server running, waiting for requests on stdin...', file=sys.stderr)
|
||||
|
||||
config_dict = confgen.get_json_values(config)
|
||||
ranges_dict = get_ranges(config)
|
||||
visible_dict = get_visible(config)
|
||||
|
||||
if default_version == 1:
|
||||
# V1: no 'visibility' key, send value None for any invisible item
|
||||
values_dict = dict((k, v if visible_dict[k] else False) for (k,v) in config_dict.items())
|
||||
json.dump({'version': 1, 'values': values_dict, 'ranges': ranges_dict}, sys.stdout)
|
||||
else:
|
||||
# V2 onwards: separate visibility from version
|
||||
json.dump({'version': default_version, 'values': config_dict, 'ranges': ranges_dict, 'visible': visible_dict}, sys.stdout)
|
||||
print('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
while True:
|
||||
line = sys.stdin.readline()
|
||||
if not line:
|
||||
break
|
||||
try:
|
||||
req = json.loads(line)
|
||||
except ValueError as e: # json module throws JSONDecodeError (sublcass of ValueError) on Py3 but ValueError on Py2
|
||||
response = {'version': default_version, 'error': ['JSON formatting error: %s' % e]}
|
||||
json.dump(response, sys.stdout)
|
||||
print('\n')
|
||||
sys.stdout.flush()
|
||||
continue
|
||||
before = confgen.get_json_values(config)
|
||||
before_ranges = get_ranges(config)
|
||||
before_visible = get_visible(config)
|
||||
|
||||
if 'load' in req: # load a new sdkconfig
|
||||
|
||||
if req.get('version', default_version) == 1:
|
||||
# for V1 protocol, send all items when loading new sdkconfig.
|
||||
# (V2+ will only send changes, same as when setting an item)
|
||||
before = {}
|
||||
before_ranges = {}
|
||||
before_visible = {}
|
||||
|
||||
# if no new filename is supplied, use existing sdkconfig path, otherwise update the path
|
||||
if req['load'] is None:
|
||||
req['load'] = sdkconfig
|
||||
else:
|
||||
sdkconfig = req['load']
|
||||
|
||||
if 'save' in req:
|
||||
if req['save'] is None:
|
||||
req['save'] = sdkconfig
|
||||
else:
|
||||
sdkconfig = req['save']
|
||||
|
||||
error = handle_request(deprecated_options, config, req)
|
||||
|
||||
after = confgen.get_json_values(config)
|
||||
after_ranges = get_ranges(config)
|
||||
after_visible = get_visible(config)
|
||||
|
||||
values_diff = diff(before, after)
|
||||
ranges_diff = diff(before_ranges, after_ranges)
|
||||
visible_diff = diff(before_visible, after_visible)
|
||||
if req['version'] == 1:
|
||||
# V1 response, invisible items have value None
|
||||
for k in (k for (k,v) in visible_diff.items() if not v):
|
||||
values_diff[k] = None
|
||||
response = {'version': 1, 'values': values_diff, 'ranges': ranges_diff}
|
||||
else:
|
||||
# V2+ response, separate visibility values
|
||||
response = {'version': req['version'], 'values': values_diff, 'ranges': ranges_diff, 'visible': visible_diff}
|
||||
if error:
|
||||
for err in error:
|
||||
print('Error: %s' % err, file=sys.stderr)
|
||||
response['error'] = error
|
||||
json.dump(response, sys.stdout)
|
||||
print('\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def handle_request(deprecated_options, config, req):
|
||||
if 'version' not in req:
|
||||
return ["All requests must have a 'version'"]
|
||||
|
||||
if req['version'] < MIN_PROTOCOL_VERSION or req['version'] > MAX_PROTOCOL_VERSION:
|
||||
return ['Unsupported request version %d. Server supports versions %d-%d' % (
|
||||
req['version'],
|
||||
MIN_PROTOCOL_VERSION,
|
||||
MAX_PROTOCOL_VERSION)]
|
||||
|
||||
error = []
|
||||
|
||||
if 'load' in req:
|
||||
print('Loading config from %s...' % req['load'], file=sys.stderr)
|
||||
try:
|
||||
config.load_config(req['load'])
|
||||
except Exception as e:
|
||||
error += ['Failed to load from %s: %s' % (req['load'], e)]
|
||||
|
||||
if 'set' in req:
|
||||
handle_set(config, error, req['set'])
|
||||
|
||||
if 'save' in req:
|
||||
try:
|
||||
print('Saving config to %s...' % req['save'], file=sys.stderr)
|
||||
confgen.write_config(deprecated_options, config, req['save'])
|
||||
except Exception as e:
|
||||
error += ['Failed to save to %s: %s' % (req['save'], e)]
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def handle_set(config, error, to_set):
|
||||
missing = [k for k in to_set if k not in config.syms]
|
||||
if missing:
|
||||
error.append('The following config symbol(s) were not found: %s' % (', '.join(missing)))
|
||||
# replace name keys with the full config symbol for each key:
|
||||
to_set = dict((config.syms[k],v) for (k,v) in to_set.items() if k not in missing)
|
||||
|
||||
# Work through the list of values to set, noting that
|
||||
# some may not be immediately applicable (maybe they depend
|
||||
# on another value which is being set). Therefore, defer
|
||||
# knowing if any value is unsettable until then end
|
||||
|
||||
while len(to_set):
|
||||
set_pass = [(k,v) for (k,v) in to_set.items() if k.visibility]
|
||||
if not set_pass:
|
||||
break # no visible keys left
|
||||
for (sym,val) in set_pass:
|
||||
if sym.type in (kconfiglib.BOOL, kconfiglib.TRISTATE):
|
||||
if val is True:
|
||||
sym.set_value(2)
|
||||
elif val is False:
|
||||
sym.set_value(0)
|
||||
else:
|
||||
error.append('Boolean symbol %s only accepts true/false values' % sym.name)
|
||||
elif sym.type == kconfiglib.HEX:
|
||||
try:
|
||||
if not isinstance(val, int):
|
||||
val = int(val, 16) # input can be a decimal JSON value or a string of hex digits
|
||||
sym.set_value(hex(val))
|
||||
except ValueError:
|
||||
error.append('Hex symbol %s can accept a decimal integer or a string of hex digits, only')
|
||||
else:
|
||||
sym.set_value(str(val))
|
||||
print('Set %s' % sym.name)
|
||||
del to_set[sym]
|
||||
|
||||
if len(to_set):
|
||||
error.append('The following config symbol(s) were not visible so were not updated: %s' % (', '.join(s.name for s in to_set)))
|
||||
|
||||
|
||||
def diff(before, after):
|
||||
"""
|
||||
Return a dictionary with the difference between 'before' and 'after',
|
||||
for items which are present in 'after' dictionary
|
||||
"""
|
||||
diff = dict((k,v) for (k,v) in after.items() if before.get(k, None) != v)
|
||||
return diff
|
||||
|
||||
|
||||
def get_ranges(config):
|
||||
ranges_dict = {}
|
||||
|
||||
def is_base_n(i, n):
|
||||
try:
|
||||
int(i, n)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
def get_active_range(sym):
|
||||
"""
|
||||
Returns a tuple of (low, high) integer values if a range
|
||||
limit is active for this symbol, or (None, None) if no range
|
||||
limit exists.
|
||||
"""
|
||||
base = kconfiglib._TYPE_TO_BASE[sym.orig_type] if sym.orig_type in kconfiglib._TYPE_TO_BASE else 0
|
||||
|
||||
try:
|
||||
for low_expr, high_expr, cond in sym.ranges:
|
||||
if kconfiglib.expr_value(cond):
|
||||
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
|
||||
return (low, high)
|
||||
except ValueError:
|
||||
pass
|
||||
return (None, None)
|
||||
|
||||
def handle_node(node):
|
||||
sym = node.item
|
||||
if not isinstance(sym, kconfiglib.Symbol):
|
||||
return
|
||||
active_range = get_active_range(sym)
|
||||
if active_range[0] is not None:
|
||||
ranges_dict[sym.name] = active_range
|
||||
|
||||
for n in config.node_iter():
|
||||
handle_node(n)
|
||||
return ranges_dict
|
||||
|
||||
|
||||
def get_visible(config):
|
||||
"""
|
||||
Return a dict mapping node IDs (config names or menu node IDs) to True/False for their visibility
|
||||
"""
|
||||
result = {}
|
||||
menus = []
|
||||
|
||||
# when walking the menu the first time, only
|
||||
# record whether the config symbols are visible
|
||||
# and make a list of menu nodes (that are not symbols)
|
||||
def handle_node(node):
|
||||
sym = node.item
|
||||
try:
|
||||
visible = (sym.visibility != 0)
|
||||
result[node] = visible
|
||||
except AttributeError:
|
||||
menus.append(node)
|
||||
for n in config.node_iter():
|
||||
handle_node(n)
|
||||
|
||||
# now, figure out visibility for each menu. A menu is visible if any of its children are visible
|
||||
for m in reversed(menus): # reverse to start at leaf nodes
|
||||
result[m] = any(v for (n,v) in result.items() if n.parent == m)
|
||||
|
||||
# return a dict mapping the node ID to its visibility.
|
||||
result = dict((confgen.get_menu_node_id(n),v) for (n,v) in result.items())
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except FatalError as e:
|
||||
print('A fatal error occurred: %s' % e, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
sys.exit(subprocess.run([sys.executable, '-m', 'kconfserver'] + sys.argv[1:]).returncode)
|
||||
|
@ -1,391 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# gen_kconfig_doc - confgen.py support for generating ReST markup documentation
|
||||
#
|
||||
# For each option in the loaded Kconfig (e.g. 'FOO'), CONFIG_FOO link target is
|
||||
# generated, allowing options to be referenced in other documents
|
||||
# (using :ref:`CONFIG_FOO`)
|
||||
#
|
||||
# SPDX-FileCopyrightText: 2017-2022 Espressif Systems (Shanghai) CO LTD
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
from __future__ import print_function
|
||||
|
||||
import re
|
||||
|
||||
import kconfiglib
|
||||
|
||||
# Indentation to be used in the generated file
|
||||
INDENT = ' '
|
||||
|
||||
# Characters used when underlining section heading
|
||||
HEADING_SYMBOLS = '#*=-^"+'
|
||||
|
||||
# Keep the heading level in sync with api-reference/kconfig.rst
|
||||
INITIAL_HEADING_LEVEL = 3
|
||||
MAX_HEADING_LEVEL = len(HEADING_SYMBOLS) - 1
|
||||
|
||||
|
||||
class ConfigTargetVisibility(object):
|
||||
"""
|
||||
Determine the visibility of Kconfig options based on IDF targets. Note that other environment variables should not
|
||||
imply invisibility and neither dependencies on visible options with default disabled state. This difference makes
|
||||
it necessary to implement our own visibility and cannot use the visibility defined inside Kconfiglib.
|
||||
"""
|
||||
def __init__(self, config, target):
|
||||
# target actually is not necessary here because kconfiglib.expr_value() will evaluate it internally
|
||||
self.config = config
|
||||
self.visibility = dict() # node name to (x, y) mapping where x is the visibility (True/False) and y is the
|
||||
# name of the config which implies the visibility
|
||||
self.target_env_var = 'IDF_TARGET'
|
||||
self.direct_eval_set = frozenset([kconfiglib.EQUAL, kconfiglib.UNEQUAL, kconfiglib.LESS, kconfiglib.LESS_EQUAL,
|
||||
kconfiglib.GREATER, kconfiglib.GREATER_EQUAL])
|
||||
|
||||
def _implies_invisibility(self, item):
|
||||
if isinstance(item, tuple):
|
||||
if item[0] == kconfiglib.NOT:
|
||||
(invisibility, source) = self._implies_invisibility(item[1])
|
||||
if source is not None and source.startswith(self.target_env_var):
|
||||
return (not invisibility, source)
|
||||
else:
|
||||
# we want to be visible all configs which are not dependent on target variables,
|
||||
# e.g. "depends on XY" and "depends on !XY" as well
|
||||
return (False, None)
|
||||
elif item[0] == kconfiglib.AND:
|
||||
(invisibility, source) = self._implies_invisibility(item[1])
|
||||
if invisibility:
|
||||
return (True, source)
|
||||
(invisibility, source) = self._implies_invisibility(item[2])
|
||||
if invisibility:
|
||||
return (True, source)
|
||||
return (False, None)
|
||||
elif item[0] == kconfiglib.OR:
|
||||
implication_list = [self._implies_invisibility(item[1]), self._implies_invisibility(item[2])]
|
||||
if all([implies for (implies, _) in implication_list]):
|
||||
source_list = [s for (_, s) in implication_list if s.startswith(self.target_env_var)]
|
||||
# if source_list has more items then it should not matter which will imply the invisibility
|
||||
return (True, source_list[0])
|
||||
return (False, None)
|
||||
elif item[0] in self.direct_eval_set:
|
||||
def node_is_invisible(item):
|
||||
return all([node.prompt is None for node in item.nodes])
|
||||
if node_is_invisible(item[1]) or node_is_invisible(item[1]):
|
||||
# it makes no sense to call self._implies_invisibility() here because it won't generate any useful
|
||||
# "source"
|
||||
return (not kconfiglib.expr_value(item), None)
|
||||
else:
|
||||
# expressions with visible configs can be changed to make the item visible
|
||||
return (False, None)
|
||||
else:
|
||||
raise RuntimeError('Unimplemented operation in {}'.format(item))
|
||||
else: # Symbol or Choice
|
||||
vis_list = [self._visible(node) for node in item.nodes]
|
||||
if len(vis_list) > 0 and all([not visible for (visible, _) in vis_list]):
|
||||
source_list = [s for (_, s) in vis_list if s is not None and s.startswith(self.target_env_var)]
|
||||
# if source_list has more items then it should not matter which will imply the invisibility
|
||||
return (True, source_list[0])
|
||||
|
||||
if item.name.startswith(self.target_env_var):
|
||||
return (not kconfiglib.expr_value(item), item.name)
|
||||
|
||||
if len(vis_list) == 1:
|
||||
(visible, source) = vis_list[0]
|
||||
if visible:
|
||||
return (False, item.name) # item.name is important here in case the result will be inverted: if
|
||||
# the dependency is on another config then it can be still visible
|
||||
|
||||
return (False, None)
|
||||
|
||||
def _visible(self, node):
|
||||
if node.item == kconfiglib.COMMENT:
|
||||
return (False, None)
|
||||
if isinstance(node.item, kconfiglib.Symbol) or isinstance(node.item, kconfiglib.Choice):
|
||||
dependencies = node.item.direct_dep # "depends on" for configs
|
||||
name_id = node.item.name
|
||||
simple_def = len(node.item.nodes) <= 1 # defined only in one source file
|
||||
# Probably it is not necessary to check the default statements.
|
||||
else:
|
||||
dependencies = node.visibility # "visible if" for menu
|
||||
name_id = node.prompt[0]
|
||||
simple_def = False # menus can be defined with the same name at multiple locations and they don't know
|
||||
# about each other like configs through node.item.nodes. Therefore, they cannot be stored and have to be
|
||||
# re-evaluated always.
|
||||
|
||||
try:
|
||||
(visib, source) = self.visibility[name_id]
|
||||
except KeyError:
|
||||
def invert_first_arg(_tuple):
|
||||
return (not _tuple[0], _tuple[1])
|
||||
|
||||
(visib, source) = self._visible(node.parent) if node.parent else (True, None)
|
||||
|
||||
if visib:
|
||||
(visib, source) = invert_first_arg(self._implies_invisibility(dependencies))
|
||||
|
||||
if simple_def:
|
||||
# Configs defined at multiple places are not stored because they could have different visibility based
|
||||
# on different targets. kconfiglib.expr_value() will handle the visibility.
|
||||
self.visibility[name_id] = (visib, source)
|
||||
|
||||
return (visib, source) # not used in "finally" block because failure messages from _implies_invisibility are
|
||||
# this way more understandable
|
||||
|
||||
def visible(self, node):
|
||||
if not node.prompt:
|
||||
# don't store this in self.visibility because don't want to stop at invisible nodes when recursively
|
||||
# searching for invisible targets
|
||||
return False
|
||||
|
||||
return self._visible(node)[0]
|
||||
|
||||
|
||||
def write_docs(config, visibility, filename):
|
||||
""" Note: writing .rst documentation ignores the current value
|
||||
of any items. ie the --config option can be ignored.
|
||||
(However at time of writing it still needs to be set to something...) """
|
||||
with open(filename, 'w') as f:
|
||||
for node in config.node_iter():
|
||||
write_menu_item(f, node, visibility)
|
||||
|
||||
|
||||
def node_is_menu(node):
|
||||
try:
|
||||
return node.item in [kconfiglib.MENU, kconfiglib.COMMENT] or node.is_menuconfig
|
||||
except AttributeError:
|
||||
return False # not all MenuNodes have is_menuconfig for some reason
|
||||
|
||||
|
||||
def get_breadcrumbs(node):
|
||||
# this is a bit wasteful as it recalculates each time, but still...
|
||||
result = []
|
||||
node = node.parent
|
||||
while node.parent:
|
||||
if node.prompt:
|
||||
result = [':ref:`%s`' % get_link_anchor(node)] + result
|
||||
node = node.parent
|
||||
return ' > '.join(result)
|
||||
|
||||
|
||||
def get_link_anchor(node):
|
||||
try:
|
||||
return 'CONFIG_%s' % node.item.name
|
||||
except AttributeError:
|
||||
assert node_is_menu(node) # only menus should have no item.name
|
||||
|
||||
# for menus, build a link anchor out of the parents
|
||||
result = []
|
||||
while node.parent:
|
||||
if node.prompt:
|
||||
result = [re.sub(r'[^a-zA-z0-9]+', '-', node.prompt[0])] + result
|
||||
node = node.parent
|
||||
result = '-'.join(result).lower()
|
||||
return result
|
||||
|
||||
|
||||
def get_heading_level(node):
|
||||
result = INITIAL_HEADING_LEVEL
|
||||
node = node.parent
|
||||
while node.parent:
|
||||
result += 1
|
||||
if result == MAX_HEADING_LEVEL:
|
||||
return MAX_HEADING_LEVEL
|
||||
node = node.parent
|
||||
return result
|
||||
|
||||
|
||||
def format_rest_text(text, indent):
|
||||
# Format an indented text block for use with ReST
|
||||
text = indent + text.replace('\n', '\n' + indent)
|
||||
# Escape some characters which are inline formatting in ReST
|
||||
text = text.replace('*', '\\*')
|
||||
text = text.replace('_', '\\_')
|
||||
# replace absolute links to documentation by relative ones
|
||||
text = re.sub(r'https://docs.espressif.com/projects/esp-idf/\w+/\w+/(.+)\.html', r':doc:`../\1`', text)
|
||||
text += '\n'
|
||||
return text
|
||||
|
||||
|
||||
def _minimize_expr(expr, visibility):
|
||||
def expr_nodes_invisible(e):
|
||||
return hasattr(e, 'nodes') and len(e.nodes) > 0 and all(not visibility.visible(i) for i in e.nodes)
|
||||
|
||||
if isinstance(expr, tuple):
|
||||
if expr[0] == kconfiglib.NOT:
|
||||
new_expr = _minimize_expr(expr[1], visibility)
|
||||
return kconfiglib.Kconfig.y if new_expr == kconfiglib.Kconfig.n else new_expr
|
||||
else:
|
||||
new_expr1 = _minimize_expr(expr[1], visibility)
|
||||
new_expr2 = _minimize_expr(expr[2], visibility)
|
||||
if expr[0] == kconfiglib.AND:
|
||||
if new_expr1 == kconfiglib.Kconfig.n or new_expr2 == kconfiglib.Kconfig.n:
|
||||
return kconfiglib.Kconfig.n
|
||||
if new_expr1 == kconfiglib.Kconfig.y:
|
||||
return new_expr2
|
||||
if new_expr2 == kconfiglib.Kconfig.y:
|
||||
return new_expr1
|
||||
elif expr[0] == kconfiglib.OR:
|
||||
if new_expr1 == kconfiglib.Kconfig.y or new_expr2 == kconfiglib.Kconfig.y:
|
||||
return kconfiglib.Kconfig.y
|
||||
if new_expr1 == kconfiglib.Kconfig.n:
|
||||
return new_expr2
|
||||
if new_expr2 == kconfiglib.Kconfig.n:
|
||||
return new_expr1
|
||||
elif expr[0] == kconfiglib.EQUAL:
|
||||
if not isinstance(new_expr1, type(new_expr2)):
|
||||
return kconfiglib.Kconfig.n
|
||||
if new_expr1 == new_expr2:
|
||||
return kconfiglib.Kconfig.y
|
||||
elif expr[0] == kconfiglib.UNEQUAL:
|
||||
if not isinstance(new_expr1, type(new_expr2)):
|
||||
return kconfiglib.Kconfig.y
|
||||
if new_expr1 != new_expr2:
|
||||
return kconfiglib.Kconfig.n
|
||||
else: # <, <=, >, >=
|
||||
if not isinstance(new_expr1, type(new_expr2)):
|
||||
return kconfiglib.Kconfig.n # e.g "True < 2"
|
||||
|
||||
if expr_nodes_invisible(new_expr1) or expr_nodes_invisible(new_expr2):
|
||||
return kconfiglib.Kconfig.y if kconfiglib.expr_value(expr) else kconfiglib.Kconfig.n
|
||||
|
||||
return (expr[0], new_expr1, new_expr2)
|
||||
|
||||
if (not kconfiglib.expr_value(expr) and len(expr.config_string) == 0 and expr_nodes_invisible(expr)):
|
||||
# nodes which are invisible
|
||||
# len(expr.nodes) > 0 avoids constant symbols without actual node definitions, e.g. integer constants
|
||||
# len(expr.config_string) == 0 avoids hidden configs which reflects the values of choices
|
||||
return kconfiglib.Kconfig.n
|
||||
|
||||
if (kconfiglib.expr_value(expr) and len(expr.config_string) > 0 and expr_nodes_invisible(expr)):
|
||||
# hidden config dependencies which will be written to sdkconfig as enabled ones.
|
||||
return kconfiglib.Kconfig.y
|
||||
|
||||
if any(node.item.name.startswith(visibility.target_env_var) for node in expr.nodes):
|
||||
# We know the actual values for IDF_TARGETs
|
||||
return kconfiglib.Kconfig.y if kconfiglib.expr_value(expr) else kconfiglib.Kconfig.n
|
||||
|
||||
return expr
|
||||
|
||||
|
||||
def write_menu_item(f, node, visibility):
|
||||
def is_choice(node):
|
||||
""" Skip choice nodes, they are handled as part of the parent (see below) """
|
||||
return isinstance(node.parent.item, kconfiglib.Choice)
|
||||
|
||||
if is_choice(node) or not visibility.visible(node):
|
||||
return
|
||||
|
||||
try:
|
||||
name = node.item.name
|
||||
except AttributeError:
|
||||
name = None
|
||||
|
||||
is_menu = node_is_menu(node)
|
||||
|
||||
# Heading
|
||||
if name:
|
||||
title = 'CONFIG_%s' % name
|
||||
else:
|
||||
# if no symbol name, use the prompt as the heading
|
||||
title = node.prompt[0]
|
||||
|
||||
f.write('.. _%s:\n\n' % get_link_anchor(node))
|
||||
f.write('%s\n' % title)
|
||||
f.write(HEADING_SYMBOLS[get_heading_level(node)] * len(title))
|
||||
f.write('\n\n')
|
||||
|
||||
if name:
|
||||
f.write('%s%s\n\n' % (INDENT, node.prompt[0]))
|
||||
f.write('%s:emphasis:`Found in:` %s\n\n' % (INDENT, get_breadcrumbs(node)))
|
||||
|
||||
try:
|
||||
if node.help:
|
||||
# Help text normally contains newlines, but spaces at the beginning of
|
||||
# each line are stripped by kconfiglib. We need to re-indent the text
|
||||
# to produce valid ReST.
|
||||
f.write(format_rest_text(node.help, INDENT))
|
||||
f.write('\n')
|
||||
except AttributeError:
|
||||
pass # No help
|
||||
|
||||
if isinstance(node.item, kconfiglib.Choice):
|
||||
f.write('%sAvailable options:\n' % INDENT)
|
||||
choice_node = node.list
|
||||
while choice_node:
|
||||
# Format available options as a list
|
||||
f.write('%s- %-20s (%s)\n' % (INDENT * 2, choice_node.prompt[0], choice_node.item.name))
|
||||
if choice_node.help:
|
||||
HELP_INDENT = INDENT * 2
|
||||
fmt_help = format_rest_text(choice_node.help, ' ' + HELP_INDENT)
|
||||
f.write('%s \n%s\n' % (HELP_INDENT, fmt_help))
|
||||
choice_node = choice_node.next
|
||||
|
||||
f.write('\n\n')
|
||||
|
||||
if isinstance(node.item, kconfiglib.Symbol):
|
||||
def _expr_str(sc):
|
||||
if sc.is_constant or not sc.nodes or sc.choice:
|
||||
return '{}'.format(sc.name)
|
||||
return ':ref:`%s%s`' % (sc.kconfig.config_prefix, sc.name)
|
||||
|
||||
range_strs = []
|
||||
for low, high, cond in node.item.ranges:
|
||||
cond = _minimize_expr(cond, visibility)
|
||||
if cond == kconfiglib.Kconfig.n:
|
||||
continue
|
||||
if not isinstance(cond, tuple) and cond != kconfiglib.Kconfig.y:
|
||||
if len(cond.nodes) > 0 and all(not visibility.visible(i) for i in cond.nodes):
|
||||
if not kconfiglib.expr_value(cond):
|
||||
continue
|
||||
range_str = '%s- from %s to %s' % (INDENT * 2, low.str_value, high.str_value)
|
||||
if cond != kconfiglib.Kconfig.y and not kconfiglib.expr_value(cond):
|
||||
range_str += ' if %s' % kconfiglib.expr_str(cond, _expr_str)
|
||||
range_strs.append(range_str)
|
||||
if len(range_strs) > 0:
|
||||
f.write('%sRange:\n' % INDENT)
|
||||
f.write('\n'.join(range_strs))
|
||||
f.write('\n\n')
|
||||
|
||||
default_strs = []
|
||||
for default, cond in node.item.defaults:
|
||||
cond = _minimize_expr(cond, visibility)
|
||||
if cond == kconfiglib.Kconfig.n:
|
||||
continue
|
||||
if not isinstance(cond, tuple) and cond != kconfiglib.Kconfig.y:
|
||||
if len(cond.nodes) > 0 and all(not visibility.visible(i) for i in cond.nodes):
|
||||
if not kconfiglib.expr_value(cond):
|
||||
continue
|
||||
# default.type is mostly UNKNOWN so it cannot be used reliably for detecting the type
|
||||
d = default.str_value
|
||||
if d in ['y', 'Y']:
|
||||
d = 'Yes (enabled)'
|
||||
elif d in ['n', 'N']:
|
||||
d = 'No (disabled)'
|
||||
elif re.search(r'[^0-9a-fA-F]', d): # simple string detection: if it not a valid number
|
||||
d = '"%s"' % d
|
||||
default_str = '%s- %s' % (INDENT * 2, d)
|
||||
if cond != kconfiglib.Kconfig.y and not kconfiglib.expr_value(cond):
|
||||
default_str += ' if %s' % kconfiglib.expr_str(cond, _expr_str)
|
||||
default_strs.append(default_str)
|
||||
if len(default_strs) > 0:
|
||||
f.write('%sDefault value:\n' % INDENT)
|
||||
f.write('\n'.join(default_strs))
|
||||
f.write('\n\n')
|
||||
|
||||
if is_menu:
|
||||
# enumerate links to child items
|
||||
child_list = []
|
||||
child = node.list
|
||||
while child:
|
||||
if not is_choice(child) and child.prompt and visibility.visible(child):
|
||||
child_list.append((child.prompt[0], get_link_anchor(child)))
|
||||
child = child.next
|
||||
if len(child_list) > 0:
|
||||
f.write('Contains:\n\n')
|
||||
sorted_child_list = sorted(child_list, key=lambda pair: pair[0].lower())
|
||||
ref_list = ['- :ref:`{}`'.format(anchor) for _, anchor in sorted_child_list]
|
||||
f.write('\n'.join(ref_list))
|
||||
f.write('\n\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Run this via 'confgen.py --output doc FILENAME'")
|
@ -1,297 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
|
||||
|
||||
class ConfgenBaseTestCase(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
self.args = dict()
|
||||
self.functions = {'in': self.assertIn,
|
||||
'not in': self.assertNotIn,
|
||||
'equal': self.assertEqual,
|
||||
'not equal': self.assertNotEqual}
|
||||
|
||||
try:
|
||||
regex_func = self.assertRegex
|
||||
except AttributeError:
|
||||
# Python 2 fallback
|
||||
regex_func = self.assertRegexpMatches
|
||||
finally:
|
||||
self.functions['regex'] = lambda instance, s, expr: regex_func(instance, expr, s) # reverse args order
|
||||
|
||||
def setUp(self):
|
||||
with tempfile.NamedTemporaryFile(prefix='test_confgen_', delete=False) as f:
|
||||
self.output_file = f.name
|
||||
self.addCleanup(os.remove, self.output_file)
|
||||
|
||||
def invoke_confgen(self, args):
|
||||
call_args = [sys.executable, '../../confgen.py']
|
||||
|
||||
for (k, v) in args.items():
|
||||
if k != 'output':
|
||||
if isinstance(v, type('')): # easy Python 2/3 compatible str/unicode
|
||||
call_args += ['--{}'.format(k), v]
|
||||
else:
|
||||
for i in v:
|
||||
call_args += ['--{}'.format(k), i]
|
||||
call_args += ['--output', args['output'], self.output_file] # these arguments belong together
|
||||
|
||||
subprocess.check_call(call_args)
|
||||
|
||||
def invoke_and_test(self, in_text, out_text, test='in'):
|
||||
"""
|
||||
Main utility function for testing confgen:
|
||||
|
||||
- Runs confgen via invoke_confgen(), using output method pre-set in test class setup
|
||||
- in_text is the Kconfig file input content
|
||||
- out_text is some expected output from confgen
|
||||
- 'test' can be any function key from self.functions dict (see above). Default is 'in' to test if
|
||||
out_text is a substring of the full confgen output.
|
||||
"""
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w+', prefix='test_confgen_', delete=False) as f:
|
||||
self.addCleanup(os.remove, f.name)
|
||||
f.write(textwrap.dedent(in_text))
|
||||
|
||||
self.args['kconfig'] = f.name
|
||||
|
||||
self.invoke_confgen(self.args)
|
||||
|
||||
with open(self.output_file) as f_result:
|
||||
result = f_result.read()
|
||||
|
||||
try:
|
||||
out_text = textwrap.dedent(out_text)
|
||||
except TypeError:
|
||||
pass # probably a regex
|
||||
|
||||
self.functions[test](self, out_text, result)
|
||||
|
||||
|
||||
class CmakeTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(CmakeTestCase, self).setUpClass()
|
||||
self.args.update({'output': 'cmake'})
|
||||
|
||||
def testStringEscape(self):
|
||||
self.invoke_and_test("""
|
||||
config PASSWORD
|
||||
string "password"
|
||||
default "\\\\~!@#$%^&*()\\\""
|
||||
""", 'set(CONFIG_PASSWORD "\\\\~!@#$%^&*()\\\"")')
|
||||
|
||||
def testHexPrefix(self):
|
||||
self.invoke_and_test(HEXPREFIX_KCONFIG, 'set(CONFIG_HEX_NOPREFIX "0x33")')
|
||||
self.invoke_and_test(HEXPREFIX_KCONFIG, 'set(CONFIG_HEX_PREFIX "0x77")')
|
||||
|
||||
|
||||
class JsonTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(JsonTestCase, self).setUpClass()
|
||||
self.args.update({'output': 'json'})
|
||||
|
||||
def testStringEscape(self):
|
||||
self.invoke_and_test("""
|
||||
config PASSWORD
|
||||
string "password"
|
||||
default "\\\\~!@#$%^&*()\\\""
|
||||
""", '"PASSWORD": "\\\\~!@#$%^&*()\\\""')
|
||||
|
||||
def testHexPrefix(self):
|
||||
# hex values come out as integers in JSON, due to no hex type
|
||||
self.invoke_and_test(HEXPREFIX_KCONFIG, '"HEX_NOPREFIX": %d' % 0x33)
|
||||
self.invoke_and_test(HEXPREFIX_KCONFIG, '"HEX_PREFIX": %d' % 0x77)
|
||||
|
||||
|
||||
class JsonMenuTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(JsonMenuTestCase, self).setUpClass()
|
||||
self.args.update({'output': 'json_menus'})
|
||||
|
||||
def testMultipleRanges(self):
|
||||
self.invoke_and_test("""
|
||||
config IDF_TARGET
|
||||
string "IDF target"
|
||||
default "esp32"
|
||||
|
||||
config SOME_SETTING
|
||||
int "setting for the chip"
|
||||
range 0 100 if IDF_TARGET="esp32s0"
|
||||
range 0 10 if IDF_TARGET="esp32"
|
||||
range -10 1 if IDF_TARGET="esp32s2"
|
||||
""", re.compile(r'"range":\s+\[\s+0,\s+10\s+\]'), 'regex')
|
||||
|
||||
def testHexRanges(self):
|
||||
self.invoke_and_test("""
|
||||
config SOME_SETTING
|
||||
hex "setting for the chip"
|
||||
range 0x0 0xaf if UNDEFINED
|
||||
range 0x10 0xaf
|
||||
""", r'"range":\s+\[\s+16,\s+175\s+\]', 'regex')
|
||||
|
||||
|
||||
class ConfigTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(ConfigTestCase, self).setUpClass()
|
||||
self.args.update({'output': 'config'})
|
||||
self.input = """
|
||||
config TEST
|
||||
bool "test"
|
||||
default "n"
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(ConfigTestCase, self).setUp()
|
||||
with tempfile.NamedTemporaryFile(mode='w+', prefix='test_confgen_', delete=False) as f:
|
||||
self.addCleanup(os.remove, f.name)
|
||||
self.args.update({'config': f.name}) # this is input in contrast with {'output': 'config'}
|
||||
f.write(textwrap.dedent("""
|
||||
CONFIG_TEST=y
|
||||
CONFIG_UNKNOWN=y
|
||||
"""))
|
||||
|
||||
def testKeepSavedOption(self):
|
||||
self.invoke_and_test(self.input, 'CONFIG_TEST=y')
|
||||
|
||||
def testDiscardUnknownOption(self):
|
||||
self.invoke_and_test(self.input, 'CONFIG_UNKNOWN', 'not in')
|
||||
|
||||
|
||||
class RenameConfigTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(RenameConfigTestCase, self).setUpClass()
|
||||
# `args` attribute is a dictionary containing the parameters to pass to `confgen.py`.
|
||||
# Specify the name of the output file, this will generate the argument `--output config`.
|
||||
self.args.update({'output': 'config'})
|
||||
# Setup the KConfig file content in the `input` attribute.
|
||||
# Let's define an option that is enabled by default, this is very important.
|
||||
# Indeed, as we explicitly disables it by its former name below, rename will be considered as functional
|
||||
# if the new name, `(CONFIG_)RENAMED_OPTION` is also disabled in the final configuration file.
|
||||
self.input = """
|
||||
config RENAMED_OPTION
|
||||
bool "Renamed option"
|
||||
default y
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(RenameConfigTestCase, self).setUp()
|
||||
# Setup the actual test. What we want to do is to have a configuration file containing which
|
||||
# option should be enabled or not, this is the equivalent of the `sdkconfig` that we can find
|
||||
# in the examples.
|
||||
with tempfile.NamedTemporaryFile(mode='w+', prefix='test_confgen_', delete=False) as f:
|
||||
self.addCleanup(os.remove, f.name)
|
||||
# The current file name will be given to `confgen.py` after `--config` argument.
|
||||
self.args.update({'config': f.name})
|
||||
# Specify the content of that configuration file, in our case, we want to explicitely
|
||||
# have an option, which needs to be renamed, disabled/not set.
|
||||
f.write(textwrap.dedent("""
|
||||
# CONFIG_NAMED_OPTION is not set
|
||||
"""))
|
||||
# The configuration file is ready, we need to prepare a `rename` configuration file which will
|
||||
# provide the new name for `CONFIG_NAMED_OPTION` we defined above
|
||||
with tempfile.NamedTemporaryFile(mode='w+', prefix='test_confgen_', delete=False) as f:
|
||||
self.addCleanup(os.remove, f.name)
|
||||
# Same as above, the following entry will result in the generation of `--sdkconfig-rename`
|
||||
# parameter followed by the current temporary file name.
|
||||
self.args.update({'sdkconfig-rename': f.name})
|
||||
# The content of our `rename` file is simple: replace `CONFIG_NAMED_OPTION` by `CONFIG_RENAMED_OPTION`
|
||||
f.write(textwrap.dedent("""
|
||||
CONFIG_NAMED_OPTION CONFIG_RENAMED_OPTION
|
||||
"""))
|
||||
|
||||
def testRenamedOptionDisabled(self):
|
||||
# Invoke the unit test, specify that the final `sdkconfig` generated must contain the string:
|
||||
# "# CONFIG_RENAMED_OPTION is not set"
|
||||
self.invoke_and_test(self.input, '# CONFIG_RENAMED_OPTION is not set')
|
||||
|
||||
|
||||
class HeaderTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(HeaderTestCase, self).setUpClass()
|
||||
self.args.update({'output': 'header'})
|
||||
|
||||
def testStringEscape(self):
|
||||
self.invoke_and_test("""
|
||||
config PASSWORD
|
||||
string "password"
|
||||
default "\\\\~!@#$%^&*()\\\""
|
||||
""", '#define CONFIG_PASSWORD "\\\\~!@#$%^&*()\\\""')
|
||||
|
||||
def testHexPrefix(self):
|
||||
self.invoke_and_test(HEXPREFIX_KCONFIG, '#define CONFIG_HEX_NOPREFIX 0x33')
|
||||
self.invoke_and_test(HEXPREFIX_KCONFIG, '#define CONFIG_HEX_PREFIX 0x77')
|
||||
|
||||
|
||||
class DocsTestCase(ConfgenBaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
super(DocsTestCase, self).setUpClass()
|
||||
self.args.update({'output': 'docs',
|
||||
'env': 'IDF_TARGET=esp32'})
|
||||
|
||||
def testChoice(self):
|
||||
self.invoke_and_test("""
|
||||
menu "TEST"
|
||||
choice TYPES
|
||||
prompt "types"
|
||||
default TYPES_OP2
|
||||
help
|
||||
Description of TYPES
|
||||
|
||||
config TYPES_OP1
|
||||
bool "option 1"
|
||||
config TYPES_OP2
|
||||
bool "option 2"
|
||||
endchoice
|
||||
endmenu
|
||||
""", """
|
||||
TEST
|
||||
----
|
||||
|
||||
Contains:
|
||||
|
||||
- :ref:`CONFIG_TYPES`
|
||||
|
||||
.. _CONFIG_TYPES:
|
||||
|
||||
CONFIG_TYPES
|
||||
^^^^^^^^^^^^
|
||||
|
||||
types
|
||||
|
||||
:emphasis:`Found in:` :ref:`test`
|
||||
|
||||
Description of TYPES
|
||||
|
||||
Available options:
|
||||
- option 1 (TYPES_OP1)
|
||||
- option 2 (TYPES_OP2)
|
||||
""") # this is more readable than regex
|
||||
|
||||
|
||||
# Used by multiple testHexPrefix() test cases to verify correct hex output for each format
|
||||
HEXPREFIX_KCONFIG = """
|
||||
config HEX_NOPREFIX
|
||||
hex "Hex Item default no prefix"
|
||||
default 33
|
||||
|
||||
config HEX_PREFIX
|
||||
hex "Hex Item default prefix"
|
||||
default 0x77
|
||||
"""
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,78 +0,0 @@
|
||||
menu "Test config"
|
||||
|
||||
config TEST_BOOL
|
||||
bool "Test boolean"
|
||||
default n
|
||||
|
||||
config TEST_CHILD_BOOL
|
||||
bool "Test boolean"
|
||||
depends on TEST_BOOL
|
||||
default y
|
||||
|
||||
config TEST_CHILD_STR
|
||||
string "Test str"
|
||||
depends on TEST_BOOL
|
||||
default "OHAI!"
|
||||
|
||||
choice TEST_CHOICE
|
||||
prompt "Some choice"
|
||||
default CHOICE_A
|
||||
|
||||
config CHOICE_A
|
||||
bool "A"
|
||||
|
||||
config CHOICE_B
|
||||
bool "B"
|
||||
|
||||
endchoice
|
||||
|
||||
config DEPENDS_ON_CHOICE
|
||||
string "Depends on choice"
|
||||
default "Depends on A" if CHOICE_A
|
||||
default "Depends on B" if CHOICE_B
|
||||
default "WAT"
|
||||
|
||||
config SOME_UNRELATED_THING
|
||||
bool "Some unrelated thing"
|
||||
|
||||
config TEST_CONDITIONAL_RANGES
|
||||
int "Something with a range"
|
||||
range 0 100 if TEST_BOOL
|
||||
range 0 10
|
||||
default 1
|
||||
|
||||
config TEST_CONDITIONAL_HEX_RANGES
|
||||
hex "Something with a hex range"
|
||||
range 0x00 0xaf if TEST_BOOL
|
||||
range 0x10 0xaf
|
||||
default 0xa0
|
||||
|
||||
config SUBMENU_TRIGGER
|
||||
bool "I enable/disable some submenu items"
|
||||
default y
|
||||
|
||||
menu "Submenu"
|
||||
|
||||
config SUBMENU_ITEM_A
|
||||
int "I am a submenu item"
|
||||
depends on SUBMENU_TRIGGER
|
||||
default 77
|
||||
|
||||
config SUBMENU_ITEM_B
|
||||
bool "I am also submenu item"
|
||||
depends on SUBMENU_TRIGGER
|
||||
|
||||
endmenu # Submenu
|
||||
|
||||
menuconfig SUBMENU_CONFIG
|
||||
bool "Submenuconfig"
|
||||
default y
|
||||
help
|
||||
I am a submenu which is also a config item.
|
||||
|
||||
config SUBMENU_CONFIG_ITEM
|
||||
bool "Depends on submenuconfig"
|
||||
depends on SUBMENU_CONFIG
|
||||
default y
|
||||
|
||||
endmenu # Test config
|
@ -1,32 +0,0 @@
|
||||
# KConfig Tests
|
||||
|
||||
## confserver.py tests
|
||||
|
||||
Install pexpect (`pip install pexpect`).
|
||||
|
||||
Then run the tests manually like this:
|
||||
|
||||
```
|
||||
./test_confserver.py --logfile tests.log
|
||||
```
|
||||
|
||||
If a weird error message comes up from the test, check the log file (`tests.log`) which has the full interaction session (input and output) from confserver.py - sometimes the test suite misinterprets some JSON-like content in a Python error message as JSON content.
|
||||
|
||||
Note: confserver.py prints its error messages on stderr, to avoid overlap with JSON content on stdout. However pexpect uses a pty (virtual terminal) which can't distinguish stderr and stdout.
|
||||
|
||||
Test cases apply to `KConfig` config schema. Cases are listed in `testcases.txt` and are each of this form:
|
||||
|
||||
```
|
||||
* Set TEST_BOOL, showing child items
|
||||
> { "TEST_BOOL" : true }
|
||||
< { "values" : { "TEST_BOOL" : true, "TEST_CHILD_STR" : "OHAI!", "TEST_CHILD_BOOL" : true }, "ranges": {"TEST_CONDITIONAL_RANGES": [0, 100]}, "visible": {"TEST_CHILD_BOOL" : true, "TEST_CHILD_STR" : true} }
|
||||
|
||||
```
|
||||
|
||||
* First line (`*`) is description
|
||||
* Second line (`>`) is changes to send
|
||||
* Third line (`<`) is response to expect back
|
||||
* (Blank line between cases)
|
||||
|
||||
Test cases are run in sequence, so any test case depends on the state changes caused by all items above it.
|
||||
|
@ -1 +0,0 @@
|
||||
CONFIG_SOME_UNRELATED_THING=y
|
@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
import pexpect
|
||||
|
||||
# Each protocol version to be tested needs a 'testcases_vX.txt' file
|
||||
PROTOCOL_VERSIONS = [1, 2]
|
||||
|
||||
|
||||
def parse_testcases(version):
|
||||
with open('testcases_v%d.txt' % version, 'r') as f:
|
||||
cases = [line for line in f.readlines() if len(line.strip()) > 0]
|
||||
# Each 3 lines in the file should be formatted as:
|
||||
# * Description of the test change
|
||||
# * JSON "changes" to send to the server
|
||||
# * Result JSON to expect back from the server
|
||||
if len(cases) % 3 != 0:
|
||||
print('Warning: testcases.txt has wrong number of non-empty lines (%d). Should be 3 lines per test case, always.' % len(cases))
|
||||
|
||||
for i in range(0, len(cases), 3):
|
||||
desc = cases[i]
|
||||
send = cases[i + 1]
|
||||
expect = cases[i + 2]
|
||||
if not desc.startswith('* '):
|
||||
raise RuntimeError("Unexpected description at line %d: '%s'" % (i + 1, desc))
|
||||
if not send.startswith('> '):
|
||||
raise RuntimeError("Unexpected send at line %d: '%s'" % (i + 2, send))
|
||||
if not expect.startswith('< '):
|
||||
raise RuntimeError("Unexpected expect at line %d: '%s'" % (i + 3, expect))
|
||||
desc = desc[2:]
|
||||
send = json.loads(send[2:])
|
||||
expect = json.loads(expect[2:])
|
||||
yield (desc, send, expect)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--logfile', type=argparse.FileType('w'), help='Optional session log of the interactions with confserver.py')
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
# set up temporary file to use as sdkconfig copy
|
||||
with tempfile.NamedTemporaryFile(mode='w', delete=False) as temp_sdkconfig:
|
||||
temp_sdkconfig_path = os.path.join(tempfile.gettempdir(), temp_sdkconfig.name)
|
||||
with open('sdkconfig') as orig:
|
||||
temp_sdkconfig.write(orig.read())
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
temp_kconfigs_source_file = os.path.join(tempfile.gettempdir(), f.name)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
temp_kconfig_projbuilds_source_file = os.path.join(tempfile.gettempdir(), f.name)
|
||||
|
||||
cmdline = '''../../confserver.py --env "COMPONENT_KCONFIGS_SOURCE_FILE=%s" \
|
||||
--env "COMPONENT_KCONFIGS_PROJBUILD_SOURCE_FILE=%s" \
|
||||
--env "COMPONENT_KCONFIGS=" \
|
||||
--env "COMPONENT_KCONFIGS_PROJBUILD=" \
|
||||
--kconfig Kconfig \
|
||||
--config %s \
|
||||
''' % (temp_kconfigs_source_file, temp_kconfig_projbuilds_source_file, temp_sdkconfig_path)
|
||||
|
||||
cmdline = re.sub(r' +', ' ', cmdline)
|
||||
|
||||
# prepare_kconfig_files.py doesn't have to be called because COMPONENT_KCONFIGS and
|
||||
# COMPONENT_KCONFIGS_PROJBUILD are empty
|
||||
|
||||
print('Running: %s' % cmdline)
|
||||
p = pexpect.spawn(cmdline, timeout=30, logfile=args.logfile, echo=False, use_poll=True, maxread=1)
|
||||
|
||||
p.expect('Server running.+\r\n')
|
||||
initial = expect_json(p)
|
||||
print('Initial: %s' % initial)
|
||||
|
||||
for version in PROTOCOL_VERSIONS:
|
||||
test_protocol_version(p, version)
|
||||
|
||||
test_load_save(p, temp_sdkconfig_path)
|
||||
|
||||
test_invalid_json(p)
|
||||
|
||||
print('Done. All passed.')
|
||||
|
||||
finally:
|
||||
try:
|
||||
os.remove(temp_sdkconfig_path)
|
||||
os.remove(temp_kconfigs_source_file)
|
||||
os.remove(temp_kconfig_projbuilds_source_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def expect_json(p):
|
||||
# run p.expect() to expect a json object back, and return it as parsed JSON
|
||||
p.expect('{.+}\r\n')
|
||||
result = p.match.group(0).strip().decode()
|
||||
print('Read raw data from server: %s' % result)
|
||||
return json.loads(result)
|
||||
|
||||
|
||||
def send_request(p, req):
|
||||
req = json.dumps(req)
|
||||
print('Sending: %s' % (req))
|
||||
p.send('%s\n' % req)
|
||||
readback = expect_json(p)
|
||||
print('Read back: %s' % (json.dumps(readback)))
|
||||
return readback
|
||||
|
||||
|
||||
def test_protocol_version(p, version):
|
||||
print('*****')
|
||||
print('Testing version %d...' % version)
|
||||
|
||||
# reload the config from the sdkconfig file
|
||||
req = {'version': version, 'load': None}
|
||||
readback = send_request(p, req)
|
||||
print('Reset response: %s' % (json.dumps(readback)))
|
||||
|
||||
# run through each test case
|
||||
cases = parse_testcases(version)
|
||||
for (desc, send, expected) in cases:
|
||||
print(desc)
|
||||
req = {'version': version, 'set': send}
|
||||
readback = send_request(p, req)
|
||||
if readback.get('version', None) != version:
|
||||
raise RuntimeError('Expected {"version" : %d} in response' % version)
|
||||
for expect_key in expected.keys():
|
||||
read_vals = readback[expect_key]
|
||||
exp_vals = expected[expect_key]
|
||||
if read_vals != exp_vals:
|
||||
expect_diff = dict((k,v) for (k,v) in exp_vals.items() if k not in read_vals or v != read_vals[k])
|
||||
raise RuntimeError('Test failed! Was expecting %s: %s' % (expect_key, json.dumps(expect_diff)))
|
||||
print('OK')
|
||||
print('Version %d OK' % version)
|
||||
|
||||
|
||||
def test_load_save(p, temp_sdkconfig_path):
|
||||
print('Testing load/save...')
|
||||
before = os.stat(temp_sdkconfig_path).st_mtime
|
||||
save_result = send_request(p, {'version': 2, 'save': temp_sdkconfig_path})
|
||||
print('Save result: %s' % (json.dumps(save_result)))
|
||||
assert 'error' not in save_result
|
||||
assert len(save_result['values']) == 0 # nothing changes when we save
|
||||
assert len(save_result['ranges']) == 0
|
||||
after = os.stat(temp_sdkconfig_path).st_mtime
|
||||
assert after > before # something got written to disk
|
||||
|
||||
# Do a V2 load
|
||||
load_result = send_request(p, {'version': 2, 'load': temp_sdkconfig_path})
|
||||
print('V2 Load result: %s' % (json.dumps(load_result)))
|
||||
assert 'error' not in load_result
|
||||
assert len(load_result['values']) == 0 # in V2, loading same file should return no config items
|
||||
assert len(load_result['ranges']) == 0
|
||||
|
||||
# Do a V1 load
|
||||
load_result = send_request(p, {'version': 1, 'load': temp_sdkconfig_path})
|
||||
print('V1 Load result: %s' % (json.dumps(load_result)))
|
||||
assert 'error' not in load_result
|
||||
assert len(load_result['values']) > 0 # in V1, loading same file should return all config items
|
||||
assert len(load_result['ranges']) > 0
|
||||
|
||||
|
||||
def test_invalid_json(p):
|
||||
print('Testing invalid JSON formatting...')
|
||||
|
||||
bad_escaping = r'{ "version" : 2, "load" : "c:\some\path\not\escaped\as\json" }'
|
||||
p.send('%s\n' % bad_escaping)
|
||||
readback = expect_json(p)
|
||||
print(readback)
|
||||
assert 'json' in readback['error'][0].lower()
|
||||
|
||||
not_really_json = 'Hello world!!'
|
||||
p.send('%s\n' % not_really_json)
|
||||
readback = expect_json(p)
|
||||
print(readback)
|
||||
assert 'json' in readback['error'][0].lower()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,31 +0,0 @@
|
||||
* Set TEST_BOOL, showing child items
|
||||
> { "TEST_BOOL" : true }
|
||||
< { "values" : { "TEST_BOOL" : true, "TEST_CHILD_STR" : "OHAI!", "TEST_CHILD_BOOL" : true }, "ranges": {"TEST_CONDITIONAL_RANGES": [0, 100], "TEST_CONDITIONAL_HEX_RANGES": [0, 175]} }
|
||||
|
||||
* Set TEST_CHILD_STR
|
||||
> { "TEST_CHILD_STR" : "Other value" }
|
||||
< { "values" : { "TEST_CHILD_STR" : "Other value" } }
|
||||
|
||||
* Clear TEST_BOOL, hiding child items
|
||||
> { "TEST_BOOL" : false }
|
||||
< { "values" : { "TEST_BOOL" : false, "TEST_CHILD_STR" : null, "TEST_CHILD_BOOL" : null }, "ranges": {"TEST_CONDITIONAL_RANGES": [0, 10], "TEST_CONDITIONAL_HEX_RANGES": [16, 175]} }
|
||||
|
||||
* Set TEST_CHILD_BOOL, invalid as parent is disabled
|
||||
> { "TEST_CHILD_BOOL" : false }
|
||||
< { "values" : { } }
|
||||
|
||||
* Set TEST_BOOL & TEST_CHILD_STR together
|
||||
> { "TEST_BOOL" : true, "TEST_CHILD_STR" : "New value" }
|
||||
< { "values" : { "TEST_BOOL" : true, "TEST_CHILD_STR" : "New value", "TEST_CHILD_BOOL" : true } }
|
||||
|
||||
* Set choice
|
||||
> { "CHOICE_B" : true }
|
||||
< { "values" : { "CHOICE_B" : true, "CHOICE_A" : false, "DEPENDS_ON_CHOICE" : "Depends on B" } }
|
||||
|
||||
* Set string which depends on choice B
|
||||
> { "DEPENDS_ON_CHOICE" : "oh, really?" }
|
||||
< { "values" : { "DEPENDS_ON_CHOICE" : "oh, really?" } }
|
||||
|
||||
* Try setting boolean values to invalid types
|
||||
> { "CHOICE_A" : 11, "TEST_BOOL" : "false" }
|
||||
< { "values" : { } }
|
@ -1,55 +0,0 @@
|
||||
* Set TEST_BOOL, showing child items
|
||||
> { "TEST_BOOL" : true }
|
||||
< { "values" : { "TEST_BOOL" : true, "TEST_CHILD_STR" : "OHAI!", "TEST_CHILD_BOOL" : true }, "ranges": {"TEST_CONDITIONAL_RANGES": [0, 100], "TEST_CONDITIONAL_HEX_RANGES": [0, 175]}, "visible": {"TEST_CHILD_BOOL" : true, "TEST_CHILD_STR" : true} }
|
||||
|
||||
* Set TEST_CHILD_STR
|
||||
> { "TEST_CHILD_STR" : "Other value" }
|
||||
< { "values" : { "TEST_CHILD_STR" : "Other value" } }
|
||||
|
||||
* Clear TEST_BOOL, hiding child items
|
||||
> { "TEST_BOOL" : false }
|
||||
< { "values" : { "TEST_BOOL" : false }, "ranges": {"TEST_CONDITIONAL_RANGES": [0, 10], "TEST_CONDITIONAL_HEX_RANGES": [16, 175]}, "visible": { "TEST_CHILD_BOOL" : false, "TEST_CHILD_STR" : false } }
|
||||
|
||||
* Set TEST_CHILD_BOOL, invalid as parent is disabled
|
||||
> { "TEST_CHILD_BOOL" : false }
|
||||
< { "values" : { } }
|
||||
|
||||
* Set TEST_BOOL & TEST_CHILD_STR together
|
||||
> { "TEST_BOOL" : true, "TEST_CHILD_STR" : "New value" }
|
||||
< { "values" : { "TEST_BOOL" : true, "TEST_CHILD_STR" : "New value", "TEST_CHILD_BOOL" : true } }
|
||||
|
||||
* Set choice
|
||||
> { "CHOICE_B" : true }
|
||||
< { "values" : { "CHOICE_B" : true, "CHOICE_A" : false, "DEPENDS_ON_CHOICE" : "Depends on B" } }
|
||||
|
||||
* Set string which depends on choice B
|
||||
> { "DEPENDS_ON_CHOICE" : "oh, really?" }
|
||||
< { "values" : { "DEPENDS_ON_CHOICE" : "oh, really?" } }
|
||||
|
||||
* Try setting boolean values to invalid types
|
||||
> { "CHOICE_A" : 11, "TEST_BOOL" : "false" }
|
||||
< { "values" : { } }
|
||||
|
||||
* Disabling all items in a submenu causes all sub-items to have visible:False
|
||||
> { "SUBMENU_TRIGGER": false }
|
||||
< { "values" : { "SUBMENU_TRIGGER": false}, "visible": { "test-config-submenu" : false, "SUBMENU_ITEM_A": false, "SUBMENU_ITEM_B": false} }
|
||||
|
||||
* Re-enabling submenu causes that menu to be visible again, and refreshes sub-items
|
||||
> { "SUBMENU_TRIGGER": true }
|
||||
< { "values" : { "SUBMENU_TRIGGER": true}, "visible": {"test-config-submenu": true, "SUBMENU_ITEM_A": true, "SUBMENU_ITEM_B": true}, "values": {"SUBMENU_TRIGGER": true, "SUBMENU_ITEM_A": 77, "SUBMENU_ITEM_B": false } }
|
||||
|
||||
* Disabling submenuconfig item hides its children
|
||||
> { "SUBMENU_CONFIG": false }
|
||||
< { "values" : { "SUBMENU_CONFIG": false }, "visible": { "SUBMENU_CONFIG_ITEM": false } }
|
||||
|
||||
* Enabling submenuconfig item re-shows its children
|
||||
> { "SUBMENU_CONFIG": true }
|
||||
< { "values" : { "SUBMENU_CONFIG_ITEM": true, "SUBMENU_CONFIG" : true }, "visible": { "SUBMENU_CONFIG_ITEM": true } }
|
||||
|
||||
* Read/write hex values as decimal integers
|
||||
> { "TEST_CONDITIONAL_HEX_RANGES": 140 }
|
||||
< { "values" : { "TEST_CONDITIONAL_HEX_RANGES" : 140 } }
|
||||
|
||||
* Can write hex values as hex strings, but the result is a decimal integer
|
||||
> { "TEST_CONDITIONAL_HEX_RANGES": "90" }
|
||||
< { "values" : { "TEST_CONDITIONAL_HEX_RANGES" : 144 } }
|
@ -1,168 +0,0 @@
|
||||
config IDF_TARGET
|
||||
string
|
||||
default "$IDF_TARGET"
|
||||
|
||||
config IDF_TARGET_CHIPA
|
||||
bool
|
||||
default "y" if IDF_TARGET="chipa"
|
||||
|
||||
config IDF_TARGET_CHIPB
|
||||
bool
|
||||
default "y" if IDF_TARGET="chipb"
|
||||
|
||||
config ALWAYS_VISIBLE
|
||||
bool "Always visible option"
|
||||
|
||||
choice ALWAYS_VISIBLE_CHOICE
|
||||
prompt "Always visible choice"
|
||||
default ALWAYS_VISIBLE_CHOICE_OP1
|
||||
|
||||
config ALWAYS_VISIBLE_CHOICE_OP1
|
||||
bool "op1"
|
||||
|
||||
config ALWAYS_VISIBLE_CHOICE_OP2
|
||||
bool "op2"
|
||||
endchoice
|
||||
|
||||
config CONFIG_FOR_CHIPA
|
||||
bool "Config for chip A"
|
||||
depends on IDF_TARGET_CHIPA
|
||||
default n
|
||||
|
||||
config CONFIG_FOR_CHIPB
|
||||
bool "Config for chip B"
|
||||
depends on IDF_TARGET_CHIPB
|
||||
|
||||
choice CHOICE_FOR_CHIPA
|
||||
prompt "Always visible choice"
|
||||
default CHOICE_FOR_CHIPA_OP1
|
||||
depends on IDF_TARGET_CHIPA
|
||||
|
||||
config CHOICE_FOR_CHIPA_OP1
|
||||
bool "op1"
|
||||
|
||||
config CHOICE_FOR_CHIPA_OP2
|
||||
bool "op2"
|
||||
endchoice
|
||||
|
||||
choice CHOICE_FOR_CHIPB
|
||||
prompt "Always visible choice"
|
||||
default CHOICE_FOR_CHIPB_OP1
|
||||
depends on IDF_TARGET_CHIPB
|
||||
|
||||
config CHOICE_FOR_CHIPB_OP1
|
||||
bool "op1"
|
||||
|
||||
config CHOICE_FOR_CHIPB_OP2
|
||||
bool "op2"
|
||||
endchoice
|
||||
|
||||
source "Kconfig.chipa"
|
||||
source "Kconfig.chipb"
|
||||
|
||||
config DEEP_DEPENDENT_CONFIG
|
||||
bool "Config depends on another config with default no value"
|
||||
depends on CONFIG_FOR_CHIPA
|
||||
|
||||
config DEEP_DEPENDENT_CONFIG_INV
|
||||
bool "Config depends on the inverted value of another config"
|
||||
depends on !CONFIG_FOR_CHIPA
|
||||
|
||||
choice DEEP_DEPENDENT_CHOICE
|
||||
prompt "depends on target-specific config"
|
||||
default DEEP_DEPENDENT_CHOICE_OP1
|
||||
depends on DEEP_DEPENDENT_CONFIG
|
||||
|
||||
config DEEP_DEPENDENT_CHOICE_OP1
|
||||
bool "op1"
|
||||
|
||||
config DEEP_DEPENDENT_CHOICE_OP2
|
||||
bool "op2"
|
||||
endchoice
|
||||
|
||||
config INVISIBLE1
|
||||
bool "depends on cannot be satified at the same time"
|
||||
depends on CONFIG_FOR_CHIPA && IDF_TARGET_CHIPB
|
||||
|
||||
config VISIBLE1
|
||||
bool "makes no sense, just for testing OR dependencies"
|
||||
depends on CONFIG_FOR_CHIPA || IDF_TARGET_CHIPB
|
||||
|
||||
config CONFIG_FOR_CHIPA_DEPENDS_VAR1
|
||||
bool "redundant AND in depends on"
|
||||
depends on CONFIG_FOR_CHIPA && IDF_TARGET_CHIPA
|
||||
|
||||
config CONFIG_FOR_CHIPA_DEPENDS_VAR2
|
||||
bool "test AND + NOT"
|
||||
depends on CONFIG_FOR_CHIPA && !IDF_TARGET_CHIPB
|
||||
|
||||
config CONFIG_FOR_CHIPA_DEPENDS_VAR3
|
||||
bool "test NOT"
|
||||
depends on !IDF_TARGET_CHIPB
|
||||
|
||||
config CONFIG_DEPENDS_ENV_VAR1
|
||||
bool "test other environment variable (should be visible because only IDF_TARGET should make something invisible)"
|
||||
depends on IDF_XYZ
|
||||
|
||||
config CONFIG_DEPENDS_ENV_VAR2
|
||||
bool "test other environment variable (should be visible because only IDF_TARGET should make something invisible)"
|
||||
depends on !IDF_XYZ
|
||||
|
||||
choice CHIPA_VERSION
|
||||
prompt "CHIPA version"
|
||||
default CHIPA_VERSION2
|
||||
depends on CONFIG_FOR_CHIPA
|
||||
|
||||
config CHIPA_VERSION1
|
||||
bool "Version 1"
|
||||
config CHIPA_VERSION2
|
||||
bool "Version 2"
|
||||
config CHIPA_VERSION3
|
||||
bool "Version 3"
|
||||
endchoice
|
||||
|
||||
config CHIPA_REV_MIN
|
||||
int
|
||||
default 1 if CHIPA_VERSION1
|
||||
default 2 if CHIPA_VERSION2
|
||||
default 3 if CHIPA_VERSION3
|
||||
|
||||
config CHIPA_FEATURE_FROM_V1
|
||||
depends on CONFIG_FOR_CHIPA && (CHIPA_REV_MIN <= 1)
|
||||
bool "Feature available from version 1"
|
||||
|
||||
config CHIPA_FEATURE_FROM_V3
|
||||
depends on CONFIG_FOR_CHIPA && (CHIPA_REV_MIN <= 3)
|
||||
bool "Feature available from version 3"
|
||||
|
||||
config CHIPA_OPTION
|
||||
int "option with default value depending on the chip version"
|
||||
depends on IDF_TARGET_CHIPA
|
||||
default 5 if CHIPA_REV_MIN < 2
|
||||
default 4 if CHIPA_VERSION = 2
|
||||
default 9 if CHIPA_REV_MIN = 3
|
||||
|
||||
config COMPILER
|
||||
string "compiler prefix"
|
||||
default "ca" if IDF_TARGET_CHIPA
|
||||
default "cb" if IDF_TARGET_CHIPB
|
||||
|
||||
config BOOL_OPTION
|
||||
bool "bool option"
|
||||
default y
|
||||
|
||||
config BOOL_OPTION2
|
||||
bool "bool option 2"
|
||||
default BOOL_OPTION
|
||||
|
||||
config HEX_OPTION
|
||||
hex "bool option"
|
||||
default 0xce if IDF_TARGET_CHIPA
|
||||
default 0xff if IDF_TARGET_CHIPB
|
||||
range 0xf 0xce if IDF_TARGET_CHIPA
|
||||
range 0xfe 0xff if IDF_TARGET_CHIPB
|
||||
|
||||
config INT_OPTION
|
||||
int "int option"
|
||||
range 1 10 if IDF_TARGET_CHIPA
|
||||
range 100 200 if IDF_TARGET_CHIPB
|
@ -1,22 +0,0 @@
|
||||
menu "Menu for CHIPA"
|
||||
visible if IDF_TARGET_CHIPA
|
||||
|
||||
config EXT_CONFIG1_FOR_CHIPA_MENU
|
||||
bool "Config for chip A"
|
||||
depends on IDF_TARGET_CHIPA
|
||||
|
||||
config EXT_CONFIG2_FOR_CHIPA_MENU
|
||||
bool "Config for chip A (depend on the visibility of the menu)"
|
||||
|
||||
config EXT_CONFIG3_FOR_CHIPA_MENU
|
||||
int "integer"
|
||||
default 5
|
||||
endmenu
|
||||
|
||||
config EXT_CONFIG3_FOR_CHIPA
|
||||
bool "Config for chip A"
|
||||
depends on IDF_TARGET_CHIPA
|
||||
|
||||
config EXT_CONFIG4
|
||||
bool "Config for every chip (note that the config is defined at multiple places)"
|
||||
depends on IDF_TARGET_CHIPA
|
@ -1,19 +0,0 @@
|
||||
menu "Menu for CHIPB"
|
||||
visible if IDF_TARGET_CHIPB
|
||||
|
||||
config EXT_CONFIG1_FOR_CHIPB_MENU
|
||||
bool "Config for chip B"
|
||||
depends on IDF_TARGET_CHIPB
|
||||
|
||||
config EXT_CONFIG2_FOR_CHIPB_MENU
|
||||
bool "Config for chip B (depend on the visibility of the menu)"
|
||||
|
||||
endmenu
|
||||
|
||||
config EXT_CONFIG3_FOR_CHIPB
|
||||
bool "Config for chip B"
|
||||
depends on IDF_TARGET_CHIPB
|
||||
|
||||
config EXT_CONFIG4
|
||||
bool "Config for every chip (note that the config is defined at multiple places)"
|
||||
depends on IDF_TARGET_CHIPB
|
@ -1,81 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import kconfiglib
|
||||
|
||||
try:
|
||||
import gen_kconfig_doc
|
||||
except ImportError:
|
||||
sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..')))
|
||||
import gen_kconfig_doc
|
||||
|
||||
|
||||
class TestDocOutput(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
os.environ['IDF_TARGET'] = 'chipa'
|
||||
cls.target = os.environ['IDF_TARGET']
|
||||
cls.config = kconfiglib.Kconfig('Kconfig')
|
||||
cls.visibility = gen_kconfig_doc.ConfigTargetVisibility(cls.config, cls.target)
|
||||
|
||||
def get_config(self, name):
|
||||
sym = self.config.syms.get(name)
|
||||
if sym:
|
||||
return sym.nodes[0]
|
||||
choice = self.config.named_choices.get(name)
|
||||
if choice:
|
||||
return choice.nodes[0]
|
||||
raise RuntimeError('Unimplemented {}'.format(name))
|
||||
|
||||
def get_doc_out(self, config_name):
|
||||
with io.StringIO() if sys.version_info.major == 3 else io.BytesIO() as output:
|
||||
gen_kconfig_doc.write_menu_item(output, self.get_config(config_name), self.visibility)
|
||||
output.seek(0)
|
||||
return output.read()
|
||||
|
||||
def test_simple_default(self):
|
||||
s = self.get_doc_out('EXT_CONFIG3_FOR_CHIPA_MENU')
|
||||
self.assertIn('- 5', s)
|
||||
|
||||
def test_multiple_defaults(self):
|
||||
s = self.get_doc_out('CHIPA_OPTION')
|
||||
self.assertNotIn('- 5', s)
|
||||
self.assertIn('- 4 if CHIPA_VERSION = 2', s)
|
||||
self.assertNotIn('- 9', s)
|
||||
|
||||
def test_string_default(self):
|
||||
s = self.get_doc_out('COMPILER')
|
||||
self.assertIn('- ca', s)
|
||||
self.assertNotIn('- cb', s)
|
||||
|
||||
def test_bool_default(self):
|
||||
s = self.get_doc_out('BOOL_OPTION')
|
||||
self.assertIn('- Yes', s)
|
||||
|
||||
def test_bool_default_dependency(self):
|
||||
s = self.get_doc_out('BOOL_OPTION2')
|
||||
self.assertIn('- Yes', s)
|
||||
|
||||
def test_hex_default(self):
|
||||
s = self.get_doc_out('HEX_OPTION')
|
||||
self.assertIn('- "0xce"', s)
|
||||
self.assertNotIn('- "0xff"', s)
|
||||
|
||||
def test_hex_range(self):
|
||||
s = self.get_doc_out('HEX_OPTION')
|
||||
self.assertIn('- from 0xf to 0xce', s)
|
||||
self.assertNotIn('- from 0xfe', s)
|
||||
|
||||
def test_int_range(self):
|
||||
s = self.get_doc_out('INT_OPTION')
|
||||
self.assertIn('- from 1 to 10', s)
|
||||
self.assertNotIn('- from 100', s)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,112 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import kconfiglib
|
||||
|
||||
try:
|
||||
import gen_kconfig_doc
|
||||
except ImportError:
|
||||
sys.path.insert(0, os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..')))
|
||||
import gen_kconfig_doc
|
||||
|
||||
|
||||
class ConfigTargetVisibilityTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.target = os.environ['IDF_TARGET']
|
||||
self.config = kconfiglib.Kconfig('Kconfig')
|
||||
self.v = gen_kconfig_doc.ConfigTargetVisibility(self.config, self.target)
|
||||
|
||||
def _get_config(self, name):
|
||||
sym = self.config.syms.get(name)
|
||||
if sym and len(sym.nodes) > 0:
|
||||
return sym.nodes[0]
|
||||
choice = self.config.named_choices.get(name)
|
||||
if choice:
|
||||
return choice.nodes[0]
|
||||
raise RuntimeError('Unimplemented {}'.format(name))
|
||||
|
||||
def visible(self, config_name):
|
||||
self.assertTrue(self.v.visible(self._get_config(config_name)))
|
||||
|
||||
def invisible(self, config_name):
|
||||
self.assertFalse(self.v.visible(self._get_config(config_name)))
|
||||
|
||||
|
||||
class ConfigTargetVisibilityChipA(ConfigTargetVisibilityTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
os.environ['IDF_TARGET'] = 'chipa'
|
||||
|
||||
def test_config_visibility(self):
|
||||
self.invisible('IDF_TARGET')
|
||||
self.invisible('IDF_TARGET_CHIPA')
|
||||
self.visible('ALWAYS_VISIBLE')
|
||||
self.visible('ALWAYS_VISIBLE_CHOICE')
|
||||
self.visible('CONFIG_FOR_CHIPA')
|
||||
self.invisible('CONFIG_FOR_CHIPB')
|
||||
self.visible('CHOICE_FOR_CHIPA')
|
||||
self.invisible('CHOICE_FOR_CHIPB')
|
||||
self.visible('EXT_CONFIG1_FOR_CHIPA_MENU')
|
||||
self.visible('EXT_CONFIG2_FOR_CHIPA_MENU')
|
||||
self.visible('EXT_CONFIG3_FOR_CHIPA')
|
||||
self.invisible('EXT_CONFIG1_FOR_CHIPB_MENU')
|
||||
self.invisible('EXT_CONFIG2_FOR_CHIPB_MENU')
|
||||
self.invisible('EXT_CONFIG3_FOR_CHIPB')
|
||||
self.visible('EXT_CONFIG4')
|
||||
self.visible('DEEP_DEPENDENT_CONFIG')
|
||||
self.visible('DEEP_DEPENDENT_CONFIG_INV')
|
||||
self.visible('DEEP_DEPENDENT_CHOICE')
|
||||
self.invisible('INVISIBLE1')
|
||||
self.visible('VISIBLE1')
|
||||
self.visible('CONFIG_FOR_CHIPA_DEPENDS_VAR1')
|
||||
self.visible('CONFIG_FOR_CHIPA_DEPENDS_VAR2')
|
||||
self.visible('CONFIG_FOR_CHIPA_DEPENDS_VAR3')
|
||||
self.visible('CONFIG_DEPENDS_ENV_VAR1')
|
||||
self.visible('CONFIG_DEPENDS_ENV_VAR2')
|
||||
self.visible('CHIPA_VERSION')
|
||||
self.invisible('CHIPA_REV_MIN')
|
||||
self.visible('CHIPA_FEATURE_FROM_V1')
|
||||
self.visible('CHIPA_FEATURE_FROM_V3')
|
||||
|
||||
|
||||
class ConfigTargetVisibilityChipB(ConfigTargetVisibilityTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
os.environ['IDF_TARGET'] = 'chipb'
|
||||
|
||||
def test_config_visibility(self):
|
||||
self.invisible('IDF_TARGET')
|
||||
self.invisible('IDF_TARGET_CHIPA')
|
||||
self.visible('ALWAYS_VISIBLE')
|
||||
self.visible('ALWAYS_VISIBLE_CHOICE')
|
||||
self.invisible('CONFIG_FOR_CHIPA')
|
||||
self.visible('CONFIG_FOR_CHIPB')
|
||||
self.invisible('CHOICE_FOR_CHIPA')
|
||||
self.visible('CHOICE_FOR_CHIPB')
|
||||
self.invisible('EXT_CONFIG1_FOR_CHIPA_MENU')
|
||||
self.invisible('EXT_CONFIG2_FOR_CHIPA_MENU')
|
||||
self.invisible('EXT_CONFIG3_FOR_CHIPA')
|
||||
self.visible('EXT_CONFIG1_FOR_CHIPB_MENU')
|
||||
self.visible('EXT_CONFIG2_FOR_CHIPB_MENU')
|
||||
self.visible('EXT_CONFIG3_FOR_CHIPB')
|
||||
self.visible('EXT_CONFIG4')
|
||||
self.invisible('DEEP_DEPENDENT_CONFIG')
|
||||
self.visible('DEEP_DEPENDENT_CONFIG_INV')
|
||||
self.invisible('DEEP_DEPENDENT_CHOICE')
|
||||
self.invisible('INVISIBLE1')
|
||||
self.visible('VISIBLE1')
|
||||
self.invisible('CONFIG_FOR_CHIPA_DEPENDS_VAR1')
|
||||
self.invisible('CONFIG_FOR_CHIPA_DEPENDS_VAR2')
|
||||
self.invisible('CONFIG_FOR_CHIPA_DEPENDS_VAR3')
|
||||
self.visible('CONFIG_DEPENDS_ENV_VAR1')
|
||||
self.visible('CONFIG_DEPENDS_ENV_VAR2')
|
||||
self.invisible('CHIPA_VERSION')
|
||||
self.invisible('CHIPA_REV_MIN')
|
||||
self.invisible('CHIPA_FEATURE_FROM_V1')
|
||||
self.invisible('CHIPA_FEATURE_FROM_V3')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -9,10 +9,7 @@ pyelftools
|
||||
idf-component-manager
|
||||
esp-coredump
|
||||
esptool
|
||||
|
||||
# kconfig and menuconfig dependencies
|
||||
kconfiglib
|
||||
windows-curses; sys_platform == 'win32'
|
||||
esp-idf-kconfig
|
||||
|
||||
# gdb extensions dependencies
|
||||
freertos_gdb
|
||||
|
Loading…
Reference in New Issue
Block a user