When generating JSON metadata for ranges where there are conditional ranges (ie different allowed range depending on another config setting), the JSON metadata would always have the last named range as the expression was not evaluated properly. Thanks to ulfalizer on GitHub for pointing this out. Closes https://github.com/espressif/esp-idf/issues/2195
304 lines
10 KiB
Python
Executable file
304 lines
10 KiB
Python
Executable file
#!/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).
|
|
#
|
|
# Used internally by the ESP-IDF build system. But designed to be
|
|
# non-IDF-specific.
|
|
#
|
|
# Copyright 2018 Espressif Systems (Shanghai) PTE LTD
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http:#www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from __future__ import print_function
|
|
import argparse
|
|
import sys
|
|
import os
|
|
import os.path
|
|
import tempfile
|
|
import json
|
|
|
|
import gen_kconfig_doc
|
|
import kconfiglib
|
|
|
|
__version__ = "0.1"
|
|
|
|
if "IDF_CMAKE" not in os.environ:
|
|
os.environ["IDF_CMAKE"] = ""
|
|
|
|
|
|
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('--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')
|
|
|
|
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
|
|
|
|
config = kconfiglib.Kconfig(args.kconfig)
|
|
config.disable_redun_warnings()
|
|
config.disable_override_warnings()
|
|
|
|
if len(args.defaults) > 0:
|
|
# 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)
|
|
config.load_config(name, replace=False)
|
|
|
|
# If config file previously exists, load it
|
|
if args.config and os.path.exists(args.config):
|
|
config.load_config(args.config, replace=False)
|
|
|
|
# Output the files specified in the arguments
|
|
for output_type, filename in args.output:
|
|
temp_file = tempfile.mktemp(prefix="confgen_tmp")
|
|
try:
|
|
output_function = OUTPUT_FORMATS[output_type]
|
|
output_function(config, temp_file)
|
|
update_if_changed(temp_file, filename)
|
|
finally:
|
|
try:
|
|
os.remove(temp_file)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def write_config(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)
|
|
|
|
|
|
def write_header(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)
|
|
|
|
|
|
def write_cmake(config, filename):
|
|
with open(filename, "w") as f:
|
|
write = f.write
|
|
prefix = config.config_prefix
|
|
|
|
write("""#
|
|
# Automatically generated file. DO NOT EDIT.
|
|
# Espressif IoT Development Framework (ESP-IDF) Configuration cmake include file
|
|
#
|
|
""")
|
|
|
|
def write_node(node):
|
|
sym = node.item
|
|
if not isinstance(sym, kconfiglib.Symbol):
|
|
return
|
|
|
|
# Note: str_value calculates _write_to_conf, due to
|
|
# internal magic in kconfiglib...
|
|
val = sym.str_value
|
|
if sym._write_to_conf:
|
|
if sym.orig_type in (kconfiglib.BOOL, kconfiglib.TRISTATE) and val == "n":
|
|
val = "" # write unset values as empty variables
|
|
write("set({}{} \"{}\")\n".format(
|
|
prefix, sym.name, val))
|
|
config.walk_menu(write_node)
|
|
|
|
|
|
def get_json_values(config):
|
|
config_dict = {}
|
|
|
|
def write_node(node):
|
|
sym = node.item
|
|
if not isinstance(sym, kconfiglib.Symbol):
|
|
return
|
|
|
|
val = sym.str_value # this calculates _write_to_conf, due to kconfiglib magic
|
|
if sym._write_to_conf:
|
|
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
|
|
config.walk_menu(write_node)
|
|
return config_dict
|
|
|
|
|
|
def write_json(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 write_json_menus(config, filename):
|
|
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:
|
|
is_menuconfig = node.is_menuconfig
|
|
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):
|
|
greatest_range = [int(min_range.str_value), int(max_range.str_value)]
|
|
|
|
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:
|
|
json_parent.append(new_json)
|
|
node_lookup[node] = new_json
|
|
|
|
config.walk_menu(write_node)
|
|
with open(filename, "w") as f:
|
|
f.write(json.dumps(result, sort_keys=True, indent=4))
|
|
|
|
|
|
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": gen_kconfig_doc.write_docs,
|
|
"json": write_json,
|
|
"json_menus": write_json_menus,
|
|
}
|
|
|
|
|
|
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)
|