From: Simon Glass Date: Wed, 1 Feb 2023 20:19:12 +0000 (-0700) Subject: moveconfig: Add an option to compare Kconfig against source X-Git-Url: http://git.dujemihanovic.xyz/?a=commitdiff_plain;h=65e62037dd13840f9638632f5a6048c677a68a74;p=u-boot.git moveconfig: Add an option to compare Kconfig against source Sometimes the Makefile rules or source code refers to Kconfig options which don't exist. Update the moveconfig tool to check this and produce a series of reports about inconsistencies. This can then be used to generate patches to correct the problems. Signed-off-by: Simon Glass --- diff --git a/tools/moveconfig.py b/tools/moveconfig.py index 8f084a6070..c4d72ede36 100755 --- a/tools/moveconfig.py +++ b/tools/moveconfig.py @@ -93,6 +93,18 @@ SIZES = { RE_REMOVE_DEFCONFIG = re.compile(r'(.*)_defconfig') +# CONFIG symbols present in the build system (from Linux) but not actually used +# in U-Boot; KCONFIG symbols +IGNORE_SYMS = ['DEBUG_SECTION_MISMATCH', 'FTRACE_MCOUNT_RECORD', 'GCOV_KERNEL', + 'GCOV_PROFILE_ALL', 'KALLSYMS', 'KASAN', 'MODVERSIONS', 'SHELL', + 'TPL_BUILD', 'VPL_BUILD', 'IS_ENABLED', 'FOO', 'IF_ENABLED_INT', + 'IS_ENABLED_', 'IS_ENABLED_1', 'IS_ENABLED_2', 'IS_ENABLED_3', + 'SPL_', 'TPL_', 'SPL_FOO', 'TPL_FOO', 'TOOLS_FOO', + 'ACME', 'SPL_ACME', 'TPL_ACME', 'TRACE_BRANCH_PROFILING', + 'VAL', '_UNDEFINED', 'SPL_BUILD', ] + +SPL_PREFIXES = ['SPL_', 'TPL_', 'VPL_', 'TOOLS_'] + ### helper functions ### def check_top_directory(): """Exit if we are not at the top of source directory.""" @@ -536,6 +548,7 @@ class KconfigScanner: os.environ['srctree'] = os.getcwd() os.environ['UBOOTVERSION'] = 'dummy' os.environ['KCONFIG_OBJDIR'] = '' + os.environ['CC'] = 'gcc' self.conf = kconfiglib.Kconfig() @@ -1593,6 +1606,319 @@ def prefix_config(cfg): return op + cfg +RE_MK_CONFIGS = re.compile('CONFIG_(\$\(SPL_(?:TPL_)?\))?([A-Za-z0-9_]*)') +RE_IFDEF = re.compile('(ifdef|ifndef)') +RE_C_CONFIGS = re.compile('CONFIG_([A-Za-z0-9_]*)') +RE_CONFIG_IS = re.compile('CONFIG_IS_ENABLED\(([A-Za-z0-9_]*)\)') + +class ConfigUse: + def __init__(self, cfg, is_spl, fname, rest): + self.cfg = cfg + self.is_spl = is_spl + self.fname = fname + self.rest = rest + + def __hash__(self): + return hash((self.cfg, self.is_spl)) + +def scan_makefiles(fnames): + """Scan Makefiles looking for Kconfig options + + Looks for uses of CONFIG options in Makefiles + + Args: + fnames (list of tuple): + str: Makefile filename where the option was found + str: Line of the Makefile + + Returns: + tuple: + dict: all_uses + key (ConfigUse): object + value (list of str): matching lines + dict: Uses by filename + key (str): filename + value (set of ConfigUse): uses in that filename + + >>> RE_MK_CONFIGS.search('CONFIG_FRED').groups() + (None, 'FRED') + >>> RE_MK_CONFIGS.search('CONFIG_$(SPL_)MARY').groups() + ('$(SPL_)', 'MARY') + >>> RE_MK_CONFIGS.search('CONFIG_$(SPL_TPL_)MARY').groups() + ('$(SPL_TPL_)', 'MARY') + """ + all_uses = collections.defaultdict(list) + fname_uses = {} + for fname, rest in fnames: + m_iter = RE_MK_CONFIGS.finditer(rest) + found = False + for m in m_iter: + found = True + real_opt = m.group(2) + if real_opt == '': + continue + is_spl = False + if m.group(1): + is_spl = True + use = ConfigUse(real_opt, is_spl, fname, rest) + if fname not in fname_uses: + fname_uses[fname] = set() + fname_uses[fname].add(use) + all_uses[use].append(rest) + return all_uses, fname_uses + + +def scan_src_files(fnames): + """Scan source files (other than Makefiles) looking for Kconfig options + + Looks for uses of CONFIG options + + Args: + fnames (list of tuple): + str: Makefile filename where the option was found + str: Line of the Makefile + + Returns: + tuple: + dict: all_uses + key (ConfigUse): object + value (list of str): matching lines + dict: Uses by filename + key (str): filename + value (set of ConfigUse): uses in that filename + + >>> RE_C_CONFIGS.search('CONFIG_FRED').groups() + ('FRED',) + >>> RE_CONFIG_IS.search('CONFIG_IS_ENABLED(MARY)').groups() + ('MARY',) + >>> RE_CONFIG_IS.search('#if CONFIG_IS_ENABLED(OF_PLATDATA)').groups() + ('OF_PLATDATA',) + """ + def add_uses(m_iter, is_spl): + for m in m_iter: + found = True + real_opt = m.group(1) + if real_opt == '': + continue + use = ConfigUse(real_opt, is_spl, fname, rest) + if fname not in fname_uses: + fname_uses[fname] = set() + fname_uses[fname].add(use) + all_uses[use].append(rest) + + all_uses = collections.defaultdict(list) + fname_uses = {} + for fname, rest in fnames: + m_iter = RE_C_CONFIGS.finditer(rest) + add_uses(m_iter, False) + + m_iter2 = RE_CONFIG_IS.finditer(rest) + add_uses(m_iter2, True) + + return all_uses, fname_uses + + +MODE_NORMAL, MODE_SPL, MODE_PROPER = range(3) + +def do_scan_source(path, do_update): + """Scan the source tree for Kconfig inconsistencies + + Args: + path (str): Path to source tree + do_update (bool) : True to write to scripts/kconf_... files + """ + def is_not_proper(name): + for prefix in SPL_PREFIXES: + if name.startswith(prefix): + return name[len(prefix):] + return False + + def check_not_found(all_uses, spl_mode): + """Check for Kconfig options mentioned in the source but not in Kconfig + + Args: + all_uses (dict): + key (ConfigUse): object + value (list of str): matching lines + spl_mode (int): If MODE_SPL, look at source code which implies + an SPL_ option, but for which there is none; + for MOD_PROPER, look at source code which implies a Proper + option (i.e. use of CONFIG_IS_ENABLED() or $(SPL_) or + $(SPL_TPL_) but for which there none; + if MODE_NORMAL, ignore SPL + + Returns: + dict: + key (str): CONFIG name (without 'CONFIG_' prefix + value (list of ConfigUse): List of uses of this CONFIG + """ + # Make sure we know about all the options + not_found = collections.defaultdict(list) + for use, rest in all_uses.items(): + name = use.cfg + if name in IGNORE_SYMS: + continue + check = True + + if spl_mode == MODE_SPL: + check = use.is_spl + + # If it is an SPL symbol, try prepending all SPL_ prefixes to + # find at least one SPL symbol + if use.is_spl: + add_to_dict = False + for prefix in SPL_PREFIXES: + try_name = prefix + name + sym = kconf.syms.get(try_name) + if sym: + break + if not sym: + not_found[f'SPL_{name}'].append(use) + continue + elif spl_mode == MODE_PROPER: + # Try to find the Proper version of this symbol, i.e. without + # the SPL_ prefix + proper_name = is_not_proper(name) + if proper_name: + name = proper_name + elif not use.is_spl: + check = False + else: # MODE_NORMAL + debug = False + sym = kconf.syms.get(name) + if not sym: + proper_name = is_not_proper(name) + if proper_name: + name = proper_name + sym = kconf.syms.get(name) + if not sym: + for prefix in SPL_PREFIXES: + try_name = prefix + name + sym = kconf.syms.get(try_name) + if sym: + break + if not sym: + not_found[name].append(use) + continue + + sym = kconf.syms.get(name) + if not sym and check: + not_found[name].append(use) + return not_found + + def show_uses(uses): + """Show a list of uses along with their filename and code snippet + + Args: + uses (dict): + key (str): CONFIG name (without 'CONFIG_' prefix + value (list of ConfigUse): List of uses of this CONFIG + """ + for name in sorted(uses): + print(f'{name}: ', end='') + for i, use in enumerate(uses[name]): + print(f'{" " if i else ""}{use.fname}: {use.rest.strip()}') + + + print('Scanning Kconfig') + kconf = KconfigScanner().conf + print(f'Scanning source in {path}') + args = ['git', 'grep', '-E', r'IS_ENABLED|\bCONFIG'] + with subprocess.Popen(args, stdout=subprocess.PIPE) as proc: + out, err = proc.communicate() + lines = out.splitlines() + re_fname = re.compile('^([^:]*):(.*)') + src_list = [] + mk_list = [] + for line in lines: + linestr = line.decode('utf-8') + m_fname = re_fname.search(linestr) + if not m_fname: + continue + fname, rest = m_fname.groups() + dirname, leaf = os.path.split(fname) + root, ext = os.path.splitext(leaf) + if ext == '.autoconf': + pass + elif ext in ['.c', '.h', '.S', '.lds', '.dts', '.dtsi', '.asl', '.cfg', + '.env', '.tmpl']: + src_list.append([fname, rest]) + elif 'Makefile' in root or ext == '.mk': + mk_list.append([fname, rest]) + elif ext in ['.yml', '.sh', '.py', '.awk', '.pl', '.rst', '', '.sed']: + pass + elif 'Kconfig' in root or 'Kbuild' in root: + pass + elif 'README' in root: + pass + elif dirname in ['configs']: + pass + elif dirname.startswith('doc') or dirname.startswith('scripts/kconfig'): + pass + else: + print(f'Not sure how to handle file {fname}') + + # Scan the Makefiles + all_uses, fname_uses = scan_makefiles(mk_list) + + spl_not_found = set() + proper_not_found = set() + + # Make sure we know about all the options + print('\nCONFIG options present in Makefiles but not Kconfig:') + not_found = check_not_found(all_uses, MODE_NORMAL) + show_uses(not_found) + + print('\nCONFIG options present in Makefiles but not Kconfig (SPL):') + not_found = check_not_found(all_uses, MODE_SPL) + show_uses(not_found) + spl_not_found |= set([is_not_proper(key) or key for key in not_found.keys()]) + + print('\nCONFIG options used as Proper in Makefiles but without a non-SPL_ variant:') + not_found = check_not_found(all_uses, MODE_PROPER) + show_uses(not_found) + proper_not_found |= set([key for key in not_found.keys()]) + + # Scan the source code + all_uses, fname_uses = scan_src_files(src_list) + + # Make sure we know about all the options + print('\nCONFIG options present in source but not Kconfig:') + not_found = check_not_found(all_uses, MODE_NORMAL) + show_uses(not_found) + + print('\nCONFIG options present in source but not Kconfig (SPL):') + not_found = check_not_found(all_uses, MODE_SPL) + show_uses(not_found) + spl_not_found |= set([is_not_proper(key) or key for key in not_found.keys()]) + + print('\nCONFIG options used as Proper in source but without a non-SPL_ variant:') + not_found = check_not_found(all_uses, MODE_PROPER) + show_uses(not_found) + proper_not_found |= set([key for key in not_found.keys()]) + + print('\nCONFIG options used as SPL but without an SPL_ variant:') + for item in sorted(spl_not_found): + print(f' {item}') + + print('\nCONFIG options used as Proper but without a non-SPL_ variant:') + for item in sorted(proper_not_found): + print(f' {item}') + + # Write out the updated information + if do_update: + with open(os.path.join(path, 'scripts', 'conf_nospl'), 'w') as out: + print('# These options should not be enabled in SPL builds\n', + file=out) + for item in sorted(spl_not_found): + print(item, file=out) + with open(os.path.join(path, 'scripts', 'conf_noproper'), 'w') as out: + print('# These options should not be enabled in Proper builds\n', + file=out) + for item in sorted(proper_not_found): + print(item, file=out) + + def main(): try: cpu_count = multiprocessing.cpu_count() @@ -1642,10 +1968,14 @@ doc/develop/moveconfig.rst for documentation.''' help='force sync by savedefconfig') parser.add_argument('-S', '--spl', action='store_true', default=False, help='parse config options defined for SPL build') + parser.add_argument('--scan-source', action='store_true', default=False, + help='scan source for uses of CONFIG options') parser.add_argument('-t', '--test', action='store_true', default=False, help='run unit tests') parser.add_argument('-y', '--yes', action='store_true', default=False, help="respond 'yes' to any prompts") + parser.add_argument('-u', '--update', action='store_true', default=False, + help="update scripts/ files (use with --scan-source)") parser.add_argument('-v', '--verbose', action='store_true', default=False, help='show any build errors as boards are built') parser.add_argument('configs', nargs='*') @@ -1660,6 +1990,10 @@ doc/develop/moveconfig.rst for documentation.''' return 1 unittest.main() + if args.scan_source: + do_scan_source(os.getcwd(), args.update) + return + if not any((len(configs), args.force_sync, args.build_db, args.imply, args.find)): parser.print_usage()