initial version
authorAndrea Righi <andrea.righi@canonical.com>
Mon, 14 Nov 2022 10:25:14 +0000 (11:25 +0100)
committerAndrea Righi <andrea.righi@canonical.com>
Mon, 14 Nov 2022 12:32:26 +0000 (13:32 +0100)
Signed-off-by: Andrea Righi <andrea.righi@canonical.com>
annotations [new file with mode: 0755]
kconfig/__init__.py [new file with mode: 0644]
kconfig/annotations.py [new file with mode: 0644]

diff --git a/annotations b/annotations
new file mode 100755 (executable)
index 0000000..4cff368
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/kconfig/annotations.py b/kconfig/annotations.py
new file mode 100644 (file)
index 0000000..221585e
--- /dev/null
@@ -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[<CONFIG_OPTION>]
+    """
+    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[<CONFIG_OPTION>]
+    """
+    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()