From 98f2a74db54060fa27e09d1e574a1e4be8038bf0 Mon Sep 17 00:00:00 2001 From: Andrea Righi Date: Mon, 14 Nov 2022 11:25:14 +0100 Subject: [PATCH] initial version Signed-off-by: Andrea Righi --- annotations | 117 ++++++++++++++++++++++ kconfig/__init__.py | 0 kconfig/annotations.py | 213 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 330 insertions(+) create mode 100755 annotations create mode 100644 kconfig/__init__.py create mode 100644 kconfig/annotations.py diff --git a/annotations b/annotations new file mode 100755 index 0000000..4cff368 --- /dev/null +++ b/annotations @@ -0,0 +1,117 @@ +#!/usr/bin/env python +# -*- mode: python -*- +# Manage Ubuntu kernel .config and annotations +# Copyright © 2022 Canonical Ltd. + +import argparse +import sys +import json +from kconfig.annotations import Annotation, KConfig + +VERSION = '0.1' + +def make_parser(): + parser = argparse.ArgumentParser( + description='Manage Ubuntu kernel .config and annotations', + ) + parser.add_argument('--version', '-v', action='version', version=f'%(prog)s {VERSION}') + + parser.add_argument('--file', '-f', action='store', required=True, + help='Pass annotations or .config file to be parsed') + parser.add_argument('--arch', '-a', action='store', + help='Select architecture') + parser.add_argument('--flavour', '-l', action='store', + help='Select flavour (default is "generic")') + parser.add_argument('--config', '-c', action='store', + help='Select a specific config option') + + ga = parser.add_argument_group(title='Action').add_mutually_exclusive_group() + ga.add_argument('--query', '-q', action='store_true', + help='Query annotations') + ga.add_argument('--export', '-e', action='store_true', + help='Convert annotations to .config format') + ga.add_argument('--import', '-i', action='store', + metavar="FILE", dest='import_file', + help='Convet .config to annotations format') + ga.add_argument('--check', '-k', action='store', + metavar="FILE", dest='check_file', + help='Validate kernel .config with annotations') + return parser + +_ARGPARSER = make_parser() + +def arg_fail(message): + print(message) + _ARGPARSER.print_usage() + exit(1) + +def do_query(args): + a = Annotation(args.file) + res = a.search_config(config=args.config, arch=args.arch, flavour=args.flavour) + print(json.dumps(res, indent=4)) + +def do_export(args): + if args.arch is None: + arg_fail('error: --export requires --arch') + a = Annotation(args.file) + conf = a.search_config(config=args.config, arch=args.arch, flavour=args.flavour) + print(a.to_config(conf)) + +def do_import(args): + # Determine arch and flavour + if args.arch is None: + arg_fail('error: --arch is required with --import') + + # Merge with the current annotations + c = KConfig(args.import_file) + a = Annotation(args.file) + a.update(c, args.arch, args.flavour) + + # Save back to annotations + a.save(args.file) + +def do_check(args): + # Determine arch and flavour + if args.arch is None: + arg_fail('error: --arch is required with --check') + + # Parse target .config + c = KConfig(args.check_file) + + # Load annotations settings + a = Annotation(args.file) + + # Validate .config against annotations + print(f"check-config: loading annotations from {args.file}") + total = good = ret = 0 + for conf in c.config: + # CONFIG_VERSION_SIGNATURE is dynamically set during the build + if conf == 'CONFIG_VERSION_SIGNATURE': + continue + # Allow to use a different version of gcc + if conf == 'CONFIG_CC_VERSION_TEXT': + continue + policy = a.config[conf] if conf in a.config else None + expected = a.search_config(config=conf, arch=args.arch, flavour=args.flavour)[conf] + if expected != c.config[conf]: + print(f"check-config: FAIL: ({c.config[conf]} != {expected}): {conf} {policy})") + ret = 1 + else: + good += 1 + total += 1 + print(f"check-config: {good}/{total} checks passed -- exit {ret}") + exit(ret) + +def main(): + args = _ARGPARSER.parse_args() + if args.query: + do_query(args) + elif args.export: + do_export(args) + elif args.import_file: + do_import(args) + elif args.check_file: + do_check(args) + +if __name__ == '__main__': + main() diff --git a/kconfig/__init__.py b/kconfig/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kconfig/annotations.py b/kconfig/annotations.py new file mode 100644 index 0000000..221585e --- /dev/null +++ b/kconfig/annotations.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +# -*- mode: python -*- +# python module to manage Ubuntu kernel .config and annotations +# Copyright © 2022 Canonical Ltd. + +import json +import re +import shutil +import tempfile +from ast import literal_eval +from os.path import dirname, abspath + +class Config(object): + def __init__(self, fname: str, arch: str = None, flavour: str = None): + """ + Basic configuration file object + """ + self.fname = fname + self.raw_data = self._load(fname) + self.config = self._parse(self.raw_data) + + def _load(self, fname: str) -> str: + with open(fname, 'rt') as fd: + data = fd.read() + return data.rstrip() + + def __str__(self): + """ Return a JSON representation of the config """ + return json.dumps(self.config, indent=4) + +class KConfig(Config): + """ + Parse a .config file, individual config options can be accessed via + .config[] + """ + def _parse(self, data: str) -> dict: + config = {} + for line in data.splitlines(): + m = re.match(r'^# (CONFIG_.*) is not set$', line) + if m: + config[m.group(1)] = literal_eval("'n'") + continue + m = re.match(r'^(CONFIG_[A-Za-z0-9_]+)=(.*)$', line) + if m: + config[m.group(1)] = literal_eval("'" + m.group(2) + "'") + continue + return config + +class Annotation(Config): + """ + Parse annotations file, individual config options can be accessed via + .config[] + """ + def _parse(self, data: str) -> dict: + # Parse header + self.header = '' + for line in data.splitlines(): + if re.match(r'^#.*', line): + self.header += line + "\n" + else: + break + + # Skip comments + data = re.sub(r'(?m)^\s*#.*\n?', '', data) + + # Handle includes (recursively) + self.include = [] + expand_data = '' + for line in data.splitlines(): + m = re.match(r'^include\s+"?([^"]*)"?', line) + if m: + self.include.append(m.group(1)) + include_fname = dirname(abspath(self.fname)) + '/' + m.group(1) + include_data = self._load(include_fname) + expand_data += include_data + '\n' + else: + expand_data += line + '\n' + + # Skip empty, non-policy and non-note lines + data = "\n".join([l.rstrip() for l in expand_data.splitlines() + if l.strip() and (re.match('.* policy<', l) or re.match('.* note<', l))]) + + # Convert multiple spaces to single space to simplifly parsing + data = re.sub(r' *', ' ', data) + + # Parse config/note statements + config = {} + for line in data.splitlines(): + try: + conf = line.split(' ')[0] + if conf in config: + entry = config[conf] + else: + entry = {} + m = re.match(r'.*policy<(.*)>', line) + if m: + entry['policy'] = literal_eval(m.group(1)) + m = re.match(r'.*note<(.*?)>', line) + if m: + entry['note'] = "'" + m.group(1).replace("'", '') + "'" + if entry: + config[conf] = entry + except Exception as e: + raise Exception(str(e) + f', line = {line}') + return config + + def update(self, c: KConfig, arch: str, flavour: str = None): + """ Merge configs from a Kconfig object into Annotation object """ + if flavour is not None: + flavour = arch + f'-{flavour}' + else: + flavour = arch + # Apply configs from the Kconfig object into Annotations + for conf in c.config: + if conf in self.config: + if 'policy' in self.config[conf]: + self.config[conf]['policy'][flavour] = c.config[conf] + else: + self.config[conf]['policy'] = {flavour: c.config[conf]} + else: + self.config[conf] = {'policy': {flavour: c.config[conf]}} + if flavour != arch: + if arch in self.config[conf]['policy']: + if self.config[conf]['policy'][arch] == self.config[conf]['policy'][flavour]: + del self.config[conf]['policy'][flavour] + # If flavour is specified override default arch configs with flavour + # configs (especially if a flavour disables a config that was enabled + # for the arch) + if flavour != arch: + for conf in self.config: + if 'policy' in self.config[conf]: + if arch in self.config[conf]['policy'] and conf not in c.config: + self.config[conf]['policy'][flavour] = '-' + + def save(self, fname: str): + """ Save annotations data to the annotation file """ + with tempfile.NamedTemporaryFile(mode='w+t', delete=False) as tmp: + # Write header + tmp.write(self.header + '\n') + + # Write includes + for i in self.include: + tmp.write(f'include "{i}"\n') + if self.include: + tmp.write("\n") + + # Write config annotations and notes + tmp.flush() + shutil.copy(tmp.name, fname) + tmp_a = Annotation(fname) + + # Only save local differences (preserve includes) + for conf in self.config: + old_val = tmp_a.config[conf] if conf in tmp_a.config else None + new_val = self.config[conf] + if old_val != new_val: + if 'policy' in self.config[conf]: + val = self.config[conf]['policy'] + line = f"{conf : <47} policy<{val}>" + tmp.write(line + "\n") + if 'note' in self.config[conf]: + val = self.config[conf]['note'] + line = f"{conf : <47} note<{val}>" + tmp.write(line + "\n\n") + + # Replace annotations with the updated version + tmp.flush() + shutil.move(tmp.name, fname) + + def search_config(self, config: str = None, arch: str = None, flavour: str = None) -> dict: + """ Return config value of a specific config option or architecture """ + if flavour is None: + flavour = 'generic' + flavour = f'{arch}-{flavour}' + if config is None and arch is None: + # Get all config options for all architectures + return self.config + elif config is None and arch is not None: + # Get config options of a specific architecture + ret = {} + for c in self.config: + if not 'policy' in self.config[c]: + continue + if flavour in self.config[c]['policy']: + ret[c] = self.config[c]['policy'][flavour] + elif arch in self.config[c]['policy']: + ret[c] = self.config[c]['policy'][arch] + return ret + elif config is not None and arch is None: + # Get a specific config option for all architectures + return self.config[config] + elif config is not None and arch is not None: + # Get a specific config option for a specific architecture + if 'policy' in self.config[config]: + if flavour in self.config[config]['policy']: + return {config: self.config[config]['policy'][flavour]} + elif arch in self.config[config]['policy']: + return {config: self.config[config]['policy'][arch]} + return None + + @staticmethod + def to_config(data: dict) -> str: + """ Convert annotations data to .config format """ + s = '' + for c in data: + v = data[c] + if v == 'n': + s += f"# {c} is not set\n" + elif v == '-': + pass + else: + s += f"{c}={v}\n" + return s.rstrip() -- 2.31.1