02802a5113
V2 adds: * Independent result for visibility (showing/hiding menus) * Includes adding IDs for all items (menus & symbols) in kconfig_menus.json Still backwards compatible with V1, with some small changes (menu items now listed in results). Also added some protocol docs, changed the "listening on stdin" message to come after any kconfiglib warnings
336 lines
11 KiB
Python
Executable file
336 lines
11 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 re
|
|
|
|
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 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(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:
|
|
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:
|
|
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
|
|
|
|
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)
|