123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- #!/usr/bin/env python3
- # See utils/checkpackagelib/readme.txt before editing this file.
- # /// script
- # requires-python = ">=3.9"
- # dependencies = [
- # "flake8",
- # "python-magic",
- # ]
- # ///
- import argparse
- import inspect
- import fileinput
- 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)")
- parser.add_argument("--patch", "-p", action="store_true",
- help="The 'files' are patch files to be sent to the"
- " Buildroot mailing list")
- parser.add_argument("--test-suite", action="store_true", help="Run the"
- " test-suite")
- 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/alchemy/atom.mk.in$",
- 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") or fname.endswith(".mk.in"):
- 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 = ""
- try:
- 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
- except FileNotFoundError:
- print(f"{fname}: missing; unstaged file removal?")
- nwarnings += 1
- return nwarnings, nlines
- 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 patch_modified_files(patches):
- """
- Find files modified in a patch file
- :param patches: Patch files to read, as a list of paths or '-' for stdin
- :returns: List of modified filenames
- """
- files = []
- with fileinput.input(files=patches) as fp:
- # Search for unified-diff to-file lines
- for line in fp:
- if line.startswith('+++'):
- line = line.removeprefix('+++').strip()
- # Remove the prefix git adds to filenames
- if line.startswith('b/'):
- line = line.removeprefix('b/')
- files.append(line)
- files.sort()
- return files
- def __main__():
- global flags
- flags = parse_args()
- if flags.test_suite:
- return checkpackagelib.base.run_test_suite()
- if flags.patch:
- files_to_check = patch_modified_files(flags.files)
- else:
- files_to_check = flags.files
- 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 files_to_check]
- # move current dir so the script find the files
- os.chdir(base_dir)
- 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__()
|