#!/usr/bin/env python3 # See utils/checkpackagelib/readme.txt before editing this file. import argparse import inspect import magic import os import re import sys import checkpackagelib.base import checkpackagelib.lib_config import checkpackagelib.lib_defconfig import checkpackagelib.lib_hash import checkpackagelib.lib_ignore import checkpackagelib.lib_mk import checkpackagelib.lib_patch import checkpackagelib.lib_python import checkpackagelib.lib_shellscript import checkpackagelib.lib_sysv VERBOSE_LEVEL_TO_SHOW_IGNORED_FILES = 3 flags = None # Command line arguments. # There are two Python packages called 'magic': # https://pypi.org/project/file-magic/ # https://pypi.org/project/python-magic/ # Both allow to return a MIME file type, but with a slightly different # interface. Detect which one of the two we have based on one of the # attributes. if hasattr(magic, 'FileMagic'): # https://pypi.org/project/file-magic/ def get_filetype(fname): return magic.detect_from_filename(fname).mime_type else: # https://pypi.org/project/python-magic/ def get_filetype(fname): return magic.from_file(fname, mime=True) def get_ignored_parsers_per_file(intree_only, ignore_filename): ignored = dict() entry_base_dir = '' if not ignore_filename: return ignored filename = os.path.abspath(ignore_filename) entry_base_dir = os.path.join(os.path.dirname(filename)) with open(filename, "r") as f: for line in f.readlines(): filename, warnings_str = line.split(' ', 1) warnings = warnings_str.split() ignored[os.path.join(entry_base_dir, filename)] = warnings return ignored def parse_args(): parser = argparse.ArgumentParser() # Do not use argparse.FileType("r") here because only files with known # format will be open based on the filename. parser.add_argument("files", metavar="F", type=str, nargs="*", help="list of files") parser.add_argument("--br2-external", "-b", dest='intree_only', action="store_false", help="do not apply the pathname filters used for intree files") parser.add_argument("--ignore-list", dest='ignore_filename', action="store", help='override the default list of ignored warnings') parser.add_argument("--manual-url", action="store", default="https://nightly.buildroot.org/", help="default: %(default)s") parser.add_argument("--verbose", "-v", action="count", default=0) parser.add_argument("--quiet", "-q", action="count", default=0) # Now the debug options in the order they are processed. parser.add_argument("--include-only", dest="include_list", action="append", help="run only the specified functions (debug)") parser.add_argument("--exclude", dest="exclude_list", action="append", help="do not run the specified functions (debug)") parser.add_argument("--dry-run", action="store_true", help="print the " "functions that would be called for each file (debug)") parser.add_argument("--failed-only", action="store_true", help="print only" " the name of the functions that failed (debug)") flags = parser.parse_args() flags.ignore_list = get_ignored_parsers_per_file(flags.intree_only, flags.ignore_filename) if flags.failed_only: flags.dry_run = False flags.verbose = -1 return flags def get_lib_from_filetype(fname): if not os.path.isfile(fname): return None filetype = get_filetype(fname) if filetype == "text/x-shellscript": return checkpackagelib.lib_shellscript if filetype in ["text/x-python", "text/x-script.python"]: return checkpackagelib.lib_python return None CONFIG_IN_FILENAME = re.compile(r"Config\.\S*$") DO_CHECK_INTREE = re.compile(r"|".join([ r".checkpackageignore", r"Config.in", r"arch/", r"board/", r"boot/", r"configs/", r"fs/", r"linux/", r"package/", r"support/", r"system/", r"toolchain/", r"utils/", ])) DO_NOT_CHECK_INTREE = re.compile(r"|".join([ r"boot/barebox/barebox\.mk$", r"fs/common\.mk$", r"package/doc-asciidoc\.mk$", r"package/pkg-\S*\.mk$", r"support/dependencies/[^/]+\.mk$", r"support/gnuconfig/config\.", r"support/kconfig/", r"support/misc/[^/]+\.mk$", r"support/testing/tests/.*br2-external/", r"toolchain/helpers\.mk$", r"toolchain/toolchain-external/pkg-toolchain-external\.mk$", ])) SYSV_INIT_SCRIPT_FILENAME = re.compile(r"/S\d\d[^/]+$") # For defconfigs: avoid matching kernel, uboot... defconfig files, so # limit to defconfig files in a configs/ directory, either in-tree or # in a br2-external tree. BR_DEFCONFIG_FILENAME = re.compile(r"^(.+/)?configs/[^/]+_defconfig$") def get_lib_from_filename(fname): if flags.intree_only: if DO_CHECK_INTREE.match(fname) is None: return None if DO_NOT_CHECK_INTREE.match(fname): return None else: if os.path.basename(fname) == "external.mk" and \ os.path.exists(fname[:-2] + "desc"): return None if fname == ".checkpackageignore": return checkpackagelib.lib_ignore if CONFIG_IN_FILENAME.search(fname): return checkpackagelib.lib_config if BR_DEFCONFIG_FILENAME.search(fname): return checkpackagelib.lib_defconfig if fname.endswith(".hash"): return checkpackagelib.lib_hash if fname.endswith(".mk"): return checkpackagelib.lib_mk if fname.endswith(".patch"): return checkpackagelib.lib_patch if SYSV_INIT_SCRIPT_FILENAME.search(fname): return checkpackagelib.lib_sysv return get_lib_from_filetype(fname) def common_inspect_rules(m): # do not call the base class if m.__name__.startswith("_"): return False if flags.include_list and m.__name__ not in flags.include_list: return False if flags.exclude_list and m.__name__ in flags.exclude_list: return False return True def is_a_check_function(m): if not inspect.isclass(m): return False if not issubclass(m, checkpackagelib.base._CheckFunction): return False return common_inspect_rules(m) def is_external_tool(m): if not inspect.isclass(m): return False if not issubclass(m, checkpackagelib.base._Tool): return False return common_inspect_rules(m) def print_warnings(warnings, xfail): # Avoid the need to use 'return []' at the end of every check function. if warnings is None: return 0, 0 # No warning generated. if xfail: return 0, 1 # Warning not generated, fail expected for this file. for level, message in enumerate(warnings): if flags.verbose >= level: print(message.replace("\t", "< tab >").rstrip()) return 1, 1 # One more warning to count. def check_file_using_lib(fname): # Count number of warnings generated and lines processed. nwarnings = 0 nlines = 0 xfail = flags.ignore_list.get(os.path.abspath(fname), []) failed = set() lib = get_lib_from_filename(fname) if not lib: if flags.verbose >= VERBOSE_LEVEL_TO_SHOW_IGNORED_FILES: print("{}: ignored".format(fname)) return nwarnings, nlines internal_functions = inspect.getmembers(lib, is_a_check_function) external_tools = inspect.getmembers(lib, is_external_tool) all_checks = internal_functions + external_tools if flags.dry_run: functions_to_run = [c[0] for c in all_checks] print("{}: would run: {}".format(fname, functions_to_run)) return nwarnings, nlines objects = [[f"{lib.__name__[16:]}.{c[0]}", c[1](fname, flags.manual_url)] for c in internal_functions] for name, cf in objects: warn, fail = print_warnings(cf.before(), name in xfail) if fail > 0: failed.add(name) nwarnings += warn lastline = "" with open(fname, "r", errors="surrogateescape") as f: for lineno, text in enumerate(f): nlines += 1 for name, cf in objects: if cf.disable.search(lastline): continue line_sts = cf.check_line(lineno + 1, text) warn, fail = print_warnings(line_sts, name in xfail) if fail > 0: failed.add(name) nwarnings += warn lastline = text for name, cf in objects: warn, fail = print_warnings(cf.after(), name in xfail) if fail > 0: failed.add(name) nwarnings += warn tools = [[c[0], c[1](fname)] for c in external_tools] for name, tool in tools: warn, fail = print_warnings(tool.run(), name in xfail) if fail > 0: failed.add(name) nwarnings += warn for should_fail in xfail: if should_fail not in failed: print("{}:0: {} was expected to fail, did you fix the file and forget to update {}?" .format(fname, should_fail, flags.ignore_filename)) nwarnings += 1 if flags.failed_only: if len(failed) > 0: f = " ".join(sorted(failed)) print("{} {}".format(fname, f)) return nwarnings, nlines def __main__(): global flags flags = parse_args() if flags.intree_only: # change all paths received to be relative to the base dir base_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) files_to_check = [os.path.relpath(os.path.abspath(f), base_dir) for f in flags.files] # move current dir so the script find the files os.chdir(base_dir) else: files_to_check = flags.files if len(files_to_check) == 0: print("No files to check style") sys.exit(1) # Accumulate number of warnings generated and lines processed. total_warnings = 0 total_lines = 0 for fname in files_to_check: nwarnings, nlines = check_file_using_lib(fname) total_warnings += nwarnings total_lines += nlines # The warning messages are printed to stdout and can be post-processed # (e.g. counted by 'wc'), so for stats use stderr. Wait all warnings are # printed, for the case there are many of them, before printing stats. sys.stdout.flush() if not flags.quiet: print("{} lines processed".format(total_lines), file=sys.stderr) print("{} warnings generated".format(total_warnings), file=sys.stderr) if total_warnings > 0 and not flags.failed_only: sys.exit(1) __main__()