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:
Djordje Nedic 2022-09-28 17:59:32 +02:00
parent f7f3c514df
commit ebb6c2e77b
27 changed files with 35 additions and 2599 deletions

View File

@ -5,7 +5,6 @@
- "components/**/Kconfig*"
- "components/**/CMakeList.txt"
- "components/**/sdkconfig*"
- "tools/kconfig_new/**/*"
- "tools/tools.json"
- "tools/idf_tools.py"
- "CONTRIBUTING.rst"

View File

@ -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:

View File

@ -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
=======================

View File

@ -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

View File

@ -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>`_.
构建系统内部
=======================

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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
View 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
View 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)

View File

@ -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'")

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -1 +0,0 @@
CONFIG_SOME_UNRELATED_THING=y

View File

@ -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()

View File

@ -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" : { } }

View File

@ -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 } }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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