From 8cd9be1fe5dc5d33c07b8a2818b12e9a266c6119 Mon Sep 17 00:00:00 2001 From: Andrea Righi Date: Mon, 4 Dec 2023 12:53:42 +0100 Subject: [PATCH] annotations: support JSON format Allow to read and dump all annotations data in pure JSON format. With this change applied the "annotations" script is able to read either the old custom format (format version 4) or a new pure-JSON format (format version 5). It is possible to convert an old annotations file to the newer format simply by running "annotations" (no argument): the script will parse the old annotations (format version 4) and it will dump in output the new content in the new pure-JSON format (format version 5). Signed-off-by: Andrea Righi --- kconfig/annotations.py | 56 +++++++++++++++++++++++++++++++++++++++++- kconfig/run.py | 49 +++++++++++++++++++++++++++++------- kconfig/version.py | 2 ++ 3 files changed, 97 insertions(+), 10 deletions(-) diff --git a/kconfig/annotations.py b/kconfig/annotations.py index 5cd51ae..e3edfb5 100644 --- a/kconfig/annotations.py +++ b/kconfig/annotations.py @@ -12,6 +12,8 @@ from abc import abstractmethod from ast import literal_eval from os.path import dirname, abspath +from kconfig.version import ANNOTATIONS_FORMAT_VERSION + class Config: def __init__(self, fname): @@ -131,7 +133,7 @@ class Annotation(Config): # Invalid line raise SyntaxError(f"invalid line: {line}") - def _parse(self, data: str): + def _legacy_parse(self, data: str): """ Parse main annotations file, individual config options can be accessed via self.config[] @@ -161,6 +163,13 @@ class Annotation(Config): else: break + # Return an error if architectures are not defined + if not self.arch: + raise SyntaxError(f"ARCH not defined in annotations") + # Return an error if flavours are not defined + if not self.flavour: + raise SyntaxError(f"FLAVOUR not defined in annotations") + # Parse body (handle includes recursively) self._parse_body(data) @@ -171,6 +180,51 @@ class Annotation(Config): if tgt not in self.include_flavour: raise SyntaxError(f"Invalid target flavour in FLAVOUR_DEP: {tgt}") + def _json_parse(self, data, is_included=False): + data = json.loads(data) + + # Check if version is supported + version = data['attributes']['_version'] + if version > ANNOTATIONS_FORMAT_VERSION: + raise SyntaxError(f"annotations format version {version} not supported") + + # Check for top-level annotations vs imported annotations + if not is_included: + self.config = data['config'] + self.arch = data['attributes']['arch'] + self.flavour = data['attributes']['flavour'] + self.flavour_dep = data['attributes']['flavour_dep'] + self.include = data['attributes']['include'] + self.include_flavour = [] + else: + # We are procesing an imported annotations, so merge all the + # configs and attributes. + try: + self.config |= data['config'] + except TypeError: + self.config = { + **self.config, + **data['config'] + } + self.arch = list(set(self.arch) | set(data['attributes']['arch'])) + self.flavour = list(set(self.flavour) | set(data['attributes']['flavour'])) + self.include_flavour = list(set(self.include_flavour) | set(data['attributes']['flavour'])) + self.flavour_dep = list(set(self.flavour_dep) | set(data['attributes']['flavour_dep'])) + + # Handle recursive inclusions + for f in data['attributes']['include']: + include_fname = dirname(abspath(self.fname)) + "/" + f + data = self._load(include_fname) + self._json_parse(data, is_included=True) + + def _parse(self, data: str): + # Try to parse the legacy format first, otherwise use the new JSON + # format. + try: + self._legacy_parse(data) + except SyntaxError: + self._json_parse(data, is_included=False) + def _remove_entry(self, config: str): if self.config[config]: del self.config[config] diff --git a/kconfig/run.py b/kconfig/run.py index 743ae79..992dc56 100644 --- a/kconfig/run.py +++ b/kconfig/run.py @@ -22,7 +22,7 @@ except ModuleNotFoundError: from kconfig.annotations import Annotation, KConfig from kconfig.utils import autodetect_annotations, arg_fail -from kconfig.version import VERSION +from kconfig.version import VERSION, ANNOTATIONS_FORMAT_VERSION SKIP_CONFIGS = ( @@ -127,11 +127,36 @@ def make_parser(): _ARGPARSER = make_parser() +def export_result(data): + # Dump metadata / attributes first + out = '{\n "attributes": {\n' + for key, value in sorted(data['attributes'].items()): + out += f' "{key}": {json.dumps(value)},\n' + out = out.rstrip(',\n') + out += '\n },' + print(out) + + configs_with_note = {key: value for key, value in data['config'].items() if 'note' in value} + configs_without_note = {key: value for key, value in data['config'].items() if 'note' not in value} + + # Dump configs, sorted alphabetically, showing items with a note first + out = ' "config": {\n' + for key in sorted(configs_with_note) + sorted(configs_without_note): + policy = data['config'][key]['policy'] + if 'note' in data['config'][key]: + note = data['config'][key]['note'] + out += f' "{key}": {{"policy": {json.dumps(policy)}, "note": {json.dumps(note)}}},\n' + else: + out += f' "{key}": {{"policy": {json.dumps(policy)}}},\n' + out = out.rstrip(',\n') + out += '\n }\n}' + print(out) + -def print_result(config, res): - if res is not None and config is not None and config not in res: - res = {config: res} - print(json.dumps(res, indent=4)) +def print_result(config, data): + if data is not None and config is not None and config not in data: + data = {config: data} + print(json.dumps(data, sort_keys=True, indent=2)) def do_query(args): @@ -142,12 +167,18 @@ def do_query(args): # If no arguments are specified dump the whole annotations structure if args.config is None and args.arch is None and args.flavour is None: res = { - "arch": a.arch, - "flavour": a.flavour, - "flavour_dep": a.flavour_dep, + "attributes": { + "arch": a.arch, + "flavour": a.flavour, + "flavour_dep": a.flavour_dep, + "include": a.include, + "_version": ANNOTATIONS_FORMAT_VERSION, + }, "config": res, } - print_result(args.config, res) + export_result(res) + else: + print_result(args.config, res) def do_autocomplete(args): diff --git a/kconfig/version.py b/kconfig/version.py index ac5ad54..b0d479a 100644 --- a/kconfig/version.py +++ b/kconfig/version.py @@ -5,5 +5,7 @@ VERSION = "0.1" +ANNOTATIONS_FORMAT_VERSION = 5 + if __name__ == '__main__': print(VERSION) -- 2.31.1