2
1

check-package 12 KB

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