371 lines
11 KiB
Python
371 lines
11 KiB
Python
# -*- 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()
|