9b5f25ae2c
This is not necessary for correct behaviour or to have valid sdkconfig files (previous commit adds tests for this), but it's useful for consistency with sdkconfig files generated by menuconfig. As reported in https://github.com/espressif/vscode-esp-idf-extension/issues/83
328 lines
12 KiB
Python
Executable file
328 lines
12 KiB
Python
Executable file
#!/usr/bin/env python
|
|
#
|
|
# Long-running server process uses stdin & stdout to communicate JSON
|
|
# with a caller
|
|
#
|
|
from __future__ import print_function
|
|
import argparse
|
|
import confgen
|
|
import json
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
from confgen import FatalError, __version__
|
|
|
|
try:
|
|
from . import kconfiglib
|
|
except Exception:
|
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
|
import kconfiglib
|
|
|
|
# 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(confgen.dict_enc_for_env(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 += os.environ.get("COMPONENT_SDKCONFIG_RENAMES", "").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 e in error:
|
|
print("Error: %s" % e, 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)
|