# -*- mode: python -*- # Manage Ubuntu kernel .config and annotations # Copyright © 2022 Canonical Ltd. import sys import os import argparse import json from signal import signal, SIGPIPE, SIG_DFL try: from argcomplete import autocomplete except ModuleNotFoundError: # Allow to run this program also when argcomplete is not available def autocomplete(_unused): pass from kconfig.annotations import Annotation, KConfig # noqa: E402 Import not at top of file from kconfig.utils import autodetect_annotations, arg_fail # noqa: E402 Import not at top of file from kconfig.version import VERSION, ANNOTATIONS_FORMAT_VERSION # noqa: E402 Import not at top of file SKIP_CONFIGS = ( # CONFIG_VERSION_SIGNATURE is dynamically set during the build "CONFIG_VERSION_SIGNATURE", # Allow to use a different versions of toolchain tools "CONFIG_GCC_VERSION", "CONFIG_CC_VERSION_TEXT", "CONFIG_AS_VERSION", "CONFIG_LD_VERSION", "CONFIG_LLD_VERSION", "CONFIG_CLANG_VERSION", "CONFIG_PAHOLE_VERSION", "CONFIG_RUSTC_VERSION_TEXT", "CONFIG_BINDGEN_VERSION_TEXT", ) 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", 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") parser.add_argument("--query", "-q", action="store_true", help="Query annotations") parser.add_argument( "--note", "-n", action="store", help="Write a specific note to a config option in annotations", ) parser.add_argument( "--autocomplete", action="store_true", help="Enable config bash autocomplete: `source <(annotations --autocomplete)`", ) parser.add_argument( "--source", "-t", action="store_true", help="Jump to a config definition in the kernel source code", ) parser.add_argument( "--no-include", action="store_true", help="Do not process included annotations (stop at the main file)", ) parser.add_argument( "--json", action="store_true", help="Try to parse annotations file in pure JSON format", ) ga = parser.add_argument_group(title="Action").add_mutually_exclusive_group(required=False) ga.add_argument( "--write", "-w", action="store", metavar="VALUE", dest="value", help="Set a specific config value in annotations (use 'null' to remove)", ) 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="Import a full .config for a specific arch and flavour into annotations", ) ga.add_argument( "--update", "-u", action="store", metavar="FILE", dest="update_file", help="Import a partial .config into annotations (only resync configs specified in FILE)", ) ga.add_argument( "--check", "-k", action="store", metavar="FILE", dest="check_file", help="Validate kernel .config with annotations", ) return 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, 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): if args.arch is None and args.flavour is not None: arg_fail(_ARGPARSER, "error: --flavour requires --arch") a = Annotation(args.file, do_include=(not args.no_include), do_json=args.json) res = a.search_config(config=args.config, arch=args.arch, flavour=args.flavour) # 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 = { "attributes": { "arch": a.arch, "flavour": a.flavour, "flavour_dep": a.flavour_dep, "include": a.include, "_version": ANNOTATIONS_FORMAT_VERSION, }, "config": res, } export_result(res) else: print_result(args.config, res) def do_autocomplete(args): a = Annotation(args.file) res = (c.removeprefix("CONFIG_") for c in a.search_config()) res_str = " ".join(res) print(f'complete -W "{res_str}" annotations') def do_source(args): if args.config is None: arg_fail(_ARGPARSER, "error: --source requires --config") if not os.path.exists("tags"): print("tags not found in the current directory, try: `make tags`") sys.exit(1) os.system(f"vim -t {args.config}") def do_note(args): if args.config is None: arg_fail(_ARGPARSER, "error: --note requires --config") # Set the note in annotations a = Annotation(args.file) a.set(args.config, note=args.note) # Save back to annotations a.save(args.file) # Query and print back the value a = Annotation(args.file) res = a.search_config(config=args.config) print_result(args.config, res) def do_write(args): if args.config is None: arg_fail(_ARGPARSER, "error: --write requires --config") # Set the value in annotations ('null' means remove) a = Annotation(args.file) if args.value == "null": a.remove(args.config, arch=args.arch, flavour=args.flavour) else: a.set( args.config, arch=args.arch, flavour=args.flavour, value=args.value, note=args.note, ) # Save back to annotations a.save(args.file) # Query and print back the value a = Annotation(args.file) res = a.search_config(config=args.config) print_result(args.config, res) def do_export(args): if args.arch is None: arg_fail(_ARGPARSER, "error: --export requires --arch") a = Annotation(args.file) conf = a.search_config(config=args.config, arch=args.arch, flavour=args.flavour) if conf: print(a.to_config(conf)) def do_import(args): if args.arch is None: arg_fail(_ARGPARSER, "error: --arch is required with --import") if args.flavour is None: arg_fail(_ARGPARSER, "error: --flavour is required with --import") if args.config is not None: arg_fail(_ARGPARSER, "error: --config cannot be used with --import (try --update)") # Merge with the current annotations a = Annotation(args.file) c = KConfig(args.import_file) a.update(c, arch=args.arch, flavour=args.flavour) # Save back to annotations a.save(args.file) def do_update(args): if args.arch is None: arg_fail(_ARGPARSER, "error: --arch is required with --update") # Merge with the current annotations a = Annotation(args.file) c = KConfig(args.update_file) if args.config is None: configs = list(set(c.config.keys()) - set(SKIP_CONFIGS)) if configs: a.update(c, arch=args.arch, flavour=args.flavour, configs=configs) # Save back to annotations a.save(args.file) def do_check(args): # Determine arch and flavour if args.arch is None: arg_fail(_ARGPARSER, "error: --arch is required with --check") print(f"check-config: loading annotations from {args.file}") total = good = ret = 0 # Load annotations settings a = Annotation(args.file) a_configs = a.search_config(arch=args.arch, flavour=args.flavour).keys() # Parse target .config c = KConfig(args.check_file) c_configs = c.config.keys() # Validate .config against annotations for conf in sorted(a_configs | c_configs): if conf in SKIP_CONFIGS: continue entry = a.search_config(config=conf, arch=args.arch, flavour=args.flavour) expected = entry[conf] if entry else "-" value = c.config[conf] if conf in c.config else "-" if value != expected: policy = a.config[conf] if conf in a.config else "undefined" if "policy" in policy: policy = f"policy<{policy['policy']}>" print(f"check-config: {conf} changed from {expected} to {value}: {policy})") ret = 1 else: good += 1 total += 1 num = total - good if ret: if os.path.exists(".git"): print(f"check-config: {num} config options have been changed, review them with `git diff`") else: print(f"check-config: {num} config options have changed") else: print("check-config: all good") sys.exit(ret) def main(): # Prevent broken pipe errors when showing output in pipe to other tools # (less for example) signal(SIGPIPE, SIG_DFL) # Main annotations program autocomplete(_ARGPARSER) args = _ARGPARSER.parse_args() if args.file is None: args.file = autodetect_annotations() if args.file is None: arg_fail( _ARGPARSER, "error: could not determine DEBDIR, try using: --file/-f", show_usage=False, ) if args.config and not args.config.startswith("CONFIG_"): args.config = "CONFIG_" + args.config if args.value: do_write(args) elif args.note: do_note(args) elif args.export: do_export(args) elif args.import_file: do_import(args) elif args.update_file: do_update(args) elif args.check_file: do_check(args) elif args.autocomplete: do_autocomplete(args) elif args.source: do_source(args) else: do_query(args) if __name__ == "__main__": main()