ccb4e5db5c
Currently, when we generate .checkpackageignore, we store, for each error, only the name of the function that generated that error. Although we currently do not have two check libs that have same-name check functions, there is nothing that would prevent that, and there is no reason why two unrelated libs could not implement checks with the same name. If such a situation were to arise, we'd have no way, when parsing the ignore list (in-tree: .checkpackageignore), to know which of the libs the exclusion would apply to. Fix that by storing both the library and function names together. The leading "checkpackagelib." (with the trailing dot, 16 chars) is removed for brevity, because it's present in all libs' names. As a consequence, regenerate .checkpackageignore. Note: people using that script to validate their br2-external trees will also have to regenerate their own exclusion list if they have one. Signed-off-by: Yann E. MORIN <yann.morin.1998@free.fr> Cc: Ricardo Martincoski <ricardo.martincoski@datacom.com.br> Reviewed-by: Arnout Vandecappelle <arnout@mind.be>
317 lines
10 KiB
Python
Executable File
317 lines
10 KiB
Python
Executable File
#!/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_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"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[^/]+$")
|
|
|
|
|
|
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 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__()
|