check-package 9.7 KB

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