check-package 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. #!/usr/bin/env python3
  2. # See utils/checkpackagelib/readme.txt before editing this file.
  3. import argparse
  4. import inspect
  5. import magic
  6. import os
  7. import re
  8. import sys
  9. import checkpackagelib.base
  10. import checkpackagelib.lib_config
  11. import checkpackagelib.lib_hash
  12. import checkpackagelib.lib_ignore
  13. import checkpackagelib.lib_mk
  14. import checkpackagelib.lib_patch
  15. import checkpackagelib.lib_python
  16. import checkpackagelib.lib_shellscript
  17. import checkpackagelib.lib_sysv
  18. VERBOSE_LEVEL_TO_SHOW_IGNORED_FILES = 3
  19. flags = None # Command line arguments.
  20. # There are two Python packages called 'magic':
  21. # https://pypi.org/project/file-magic/
  22. # https://pypi.org/project/python-magic/
  23. # Both allow to return a MIME file type, but with a slightly different
  24. # interface. Detect which one of the two we have based on one of the
  25. # attributes.
  26. if hasattr(magic, 'FileMagic'):
  27. # https://pypi.org/project/file-magic/
  28. def get_filetype(fname):
  29. return magic.detect_from_filename(fname).mime_type
  30. else:
  31. # https://pypi.org/project/python-magic/
  32. def get_filetype(fname):
  33. return magic.from_file(fname, mime=True)
  34. def get_ignored_parsers_per_file(intree_only, ignore_filename):
  35. ignored = dict()
  36. entry_base_dir = ''
  37. if not ignore_filename:
  38. return ignored
  39. filename = os.path.abspath(ignore_filename)
  40. entry_base_dir = os.path.join(os.path.dirname(filename))
  41. with open(filename, "r") as f:
  42. for line in f.readlines():
  43. filename, warnings_str = line.split(' ', 1)
  44. warnings = warnings_str.split()
  45. ignored[os.path.join(entry_base_dir, filename)] = warnings
  46. return ignored
  47. def parse_args():
  48. parser = argparse.ArgumentParser()
  49. # Do not use argparse.FileType("r") here because only files with known
  50. # format will be open based on the filename.
  51. parser.add_argument("files", metavar="F", type=str, nargs="*",
  52. help="list of files")
  53. parser.add_argument("--br2-external", "-b", dest='intree_only', action="store_false",
  54. help="do not apply the pathname filters used for intree files")
  55. parser.add_argument("--ignore-list", dest='ignore_filename', action="store",
  56. help='override the default list of ignored warnings')
  57. parser.add_argument("--manual-url", action="store",
  58. default="https://nightly.buildroot.org/",
  59. help="default: %(default)s")
  60. parser.add_argument("--verbose", "-v", action="count", default=0)
  61. parser.add_argument("--quiet", "-q", action="count", default=0)
  62. # Now the debug options in the order they are processed.
  63. parser.add_argument("--include-only", dest="include_list", action="append",
  64. help="run only the specified functions (debug)")
  65. parser.add_argument("--exclude", dest="exclude_list", action="append",
  66. help="do not run the specified functions (debug)")
  67. parser.add_argument("--dry-run", action="store_true", help="print the "
  68. "functions that would be called for each file (debug)")
  69. parser.add_argument("--failed-only", action="store_true", help="print only"
  70. " the name of the functions that failed (debug)")
  71. flags = parser.parse_args()
  72. flags.ignore_list = get_ignored_parsers_per_file(flags.intree_only, flags.ignore_filename)
  73. if flags.failed_only:
  74. flags.dry_run = False
  75. flags.verbose = -1
  76. return flags
  77. def get_lib_from_filetype(fname):
  78. if not os.path.isfile(fname):
  79. return None
  80. filetype = get_filetype(fname)
  81. if filetype == "text/x-shellscript":
  82. return checkpackagelib.lib_shellscript
  83. if filetype in ["text/x-python", "text/x-script.python"]:
  84. return checkpackagelib.lib_python
  85. return None
  86. CONFIG_IN_FILENAME = re.compile(r"Config\.\S*$")
  87. DO_CHECK_INTREE = re.compile(r"|".join([
  88. r".checkpackageignore",
  89. r"Config.in",
  90. r"arch/",
  91. r"board/",
  92. r"boot/",
  93. r"fs/",
  94. r"linux/",
  95. r"package/",
  96. r"support/",
  97. r"system/",
  98. r"toolchain/",
  99. r"utils/",
  100. ]))
  101. DO_NOT_CHECK_INTREE = re.compile(r"|".join([
  102. r"boot/barebox/barebox\.mk$",
  103. r"fs/common\.mk$",
  104. r"package/doc-asciidoc\.mk$",
  105. r"package/pkg-\S*\.mk$",
  106. r"support/dependencies/[^/]+\.mk$",
  107. r"support/gnuconfig/config\.",
  108. r"support/kconfig/",
  109. r"support/misc/[^/]+\.mk$",
  110. r"support/testing/tests/.*br2-external/",
  111. r"toolchain/helpers\.mk$",
  112. r"toolchain/toolchain-external/pkg-toolchain-external\.mk$",
  113. ]))
  114. SYSV_INIT_SCRIPT_FILENAME = re.compile(r"/S\d\d[^/]+$")
  115. def get_lib_from_filename(fname):
  116. if flags.intree_only:
  117. if DO_CHECK_INTREE.match(fname) is None:
  118. return None
  119. if DO_NOT_CHECK_INTREE.match(fname):
  120. return None
  121. else:
  122. if os.path.basename(fname) == "external.mk" and \
  123. os.path.exists(fname[:-2] + "desc"):
  124. return None
  125. if fname == ".checkpackageignore":
  126. return checkpackagelib.lib_ignore
  127. if CONFIG_IN_FILENAME.search(fname):
  128. return checkpackagelib.lib_config
  129. if fname.endswith(".hash"):
  130. return checkpackagelib.lib_hash
  131. if fname.endswith(".mk"):
  132. return checkpackagelib.lib_mk
  133. if fname.endswith(".patch"):
  134. return checkpackagelib.lib_patch
  135. if SYSV_INIT_SCRIPT_FILENAME.search(fname):
  136. return checkpackagelib.lib_sysv
  137. return get_lib_from_filetype(fname)
  138. def common_inspect_rules(m):
  139. # do not call the base class
  140. if m.__name__.startswith("_"):
  141. return False
  142. if flags.include_list and m.__name__ not in flags.include_list:
  143. return False
  144. if flags.exclude_list and m.__name__ in flags.exclude_list:
  145. return False
  146. return True
  147. def is_a_check_function(m):
  148. if not inspect.isclass(m):
  149. return False
  150. if not issubclass(m, checkpackagelib.base._CheckFunction):
  151. return False
  152. return common_inspect_rules(m)
  153. def is_external_tool(m):
  154. if not inspect.isclass(m):
  155. return False
  156. if not issubclass(m, checkpackagelib.base._Tool):
  157. return False
  158. return common_inspect_rules(m)
  159. def print_warnings(warnings, xfail):
  160. # Avoid the need to use 'return []' at the end of every check function.
  161. if warnings is None:
  162. return 0, 0 # No warning generated.
  163. if xfail:
  164. return 0, 1 # Warning not generated, fail expected for this file.
  165. for level, message in enumerate(warnings):
  166. if flags.verbose >= level:
  167. print(message.replace("\t", "< tab >").rstrip())
  168. return 1, 1 # One more warning to count.
  169. def check_file_using_lib(fname):
  170. # Count number of warnings generated and lines processed.
  171. nwarnings = 0
  172. nlines = 0
  173. xfail = flags.ignore_list.get(os.path.abspath(fname), [])
  174. failed = set()
  175. lib = get_lib_from_filename(fname)
  176. if not lib:
  177. if flags.verbose >= VERBOSE_LEVEL_TO_SHOW_IGNORED_FILES:
  178. print("{}: ignored".format(fname))
  179. return nwarnings, nlines
  180. internal_functions = inspect.getmembers(lib, is_a_check_function)
  181. external_tools = inspect.getmembers(lib, is_external_tool)
  182. all_checks = internal_functions + external_tools
  183. if flags.dry_run:
  184. functions_to_run = [c[0] for c in all_checks]
  185. print("{}: would run: {}".format(fname, functions_to_run))
  186. return nwarnings, nlines
  187. objects = [[c[0], c[1](fname, flags.manual_url)] for c in internal_functions]
  188. for name, cf in objects:
  189. warn, fail = print_warnings(cf.before(), name in xfail)
  190. if fail > 0:
  191. failed.add(name)
  192. nwarnings += warn
  193. lastline = ""
  194. with open(fname, "r", errors="surrogateescape") as f:
  195. for lineno, text in enumerate(f):
  196. nlines += 1
  197. for name, cf in objects:
  198. if cf.disable.search(lastline):
  199. continue
  200. line_sts = cf.check_line(lineno + 1, text)
  201. warn, fail = print_warnings(line_sts, name in xfail)
  202. if fail > 0:
  203. failed.add(name)
  204. nwarnings += warn
  205. lastline = text
  206. for name, cf in objects:
  207. warn, fail = print_warnings(cf.after(), name in xfail)
  208. if fail > 0:
  209. failed.add(name)
  210. nwarnings += warn
  211. tools = [[c[0], c[1](fname)] for c in external_tools]
  212. for name, tool in tools:
  213. warn, fail = print_warnings(tool.run(), name in xfail)
  214. if fail > 0:
  215. failed.add(name)
  216. nwarnings += warn
  217. for should_fail in xfail:
  218. if should_fail not in failed:
  219. print("{}:0: {} was expected to fail, did you fix the file and forget to update {}?"
  220. .format(fname, should_fail, flags.ignore_filename))
  221. nwarnings += 1
  222. if flags.failed_only:
  223. if len(failed) > 0:
  224. f = " ".join(sorted(failed))
  225. print("{} {}".format(fname, f))
  226. return nwarnings, nlines
  227. def __main__():
  228. global flags
  229. flags = parse_args()
  230. if flags.intree_only:
  231. # change all paths received to be relative to the base dir
  232. base_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
  233. files_to_check = [os.path.relpath(os.path.abspath(f), base_dir) for f in flags.files]
  234. # move current dir so the script find the files
  235. os.chdir(base_dir)
  236. else:
  237. files_to_check = flags.files
  238. if len(files_to_check) == 0:
  239. print("No files to check style")
  240. sys.exit(1)
  241. # Accumulate number of warnings generated and lines processed.
  242. total_warnings = 0
  243. total_lines = 0
  244. for fname in files_to_check:
  245. nwarnings, nlines = check_file_using_lib(fname)
  246. total_warnings += nwarnings
  247. total_lines += nlines
  248. # The warning messages are printed to stdout and can be post-processed
  249. # (e.g. counted by 'wc'), so for stats use stderr. Wait all warnings are
  250. # printed, for the case there are many of them, before printing stats.
  251. sys.stdout.flush()
  252. if not flags.quiet:
  253. print("{} lines processed".format(total_lines), file=sys.stderr)
  254. print("{} warnings generated".format(total_warnings), file=sys.stderr)
  255. if total_warnings > 0 and not flags.failed_only:
  256. sys.exit(1)
  257. __main__()