verify-bashisms 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright OpenEmbedded Contributors
  4. #
  5. # SPDX-License-Identifier: GPL-2.0-only
  6. #
  7. import sys, os, subprocess, re, shutil
  8. allowed = (
  9. # type is supported by dash
  10. 'if type systemctl >/dev/null 2>/dev/null; then',
  11. 'if type systemd-tmpfiles >/dev/null 2>/dev/null; then',
  12. 'type update-rc.d >/dev/null 2>/dev/null; then',
  13. 'command -v',
  14. # HOSTNAME is set locally
  15. 'buildhistory_single_commit "$CMDLINE" "$HOSTNAME"',
  16. # False-positive, match is a grep not shell expression
  17. 'grep "^$groupname:[^:]*:[^:]*:\\([^,]*,\\)*$username\\(,[^,]*\\)*"',
  18. # TODO verify dash's '. script args' behaviour
  19. '. $target_sdk_dir/${oe_init_build_env_path} $target_sdk_dir >> $LOGFILE'
  20. )
  21. def is_allowed(s):
  22. for w in allowed:
  23. if w in s:
  24. return True
  25. return False
  26. SCRIPT_LINENO_RE = re.compile(r' line (\d+) ')
  27. BASHISM_WARNING = re.compile(r'^(possible bashism in.*)$', re.MULTILINE)
  28. def process(filename, function, lineno, script):
  29. import tempfile
  30. if not script.startswith("#!"):
  31. script = "#! /bin/sh\n" + script
  32. fn = tempfile.NamedTemporaryFile(mode="w+t")
  33. fn.write(script)
  34. fn.flush()
  35. try:
  36. subprocess.check_output(("checkbashisms.pl", fn.name), universal_newlines=True, stderr=subprocess.STDOUT)
  37. # No bashisms, so just return
  38. return
  39. except subprocess.CalledProcessError as e:
  40. # TODO check exit code is 1
  41. # Replace the temporary filename with the function and split it
  42. output = e.output.replace(fn.name, function)
  43. if not output or not output.startswith('possible bashism'):
  44. # Probably starts with or contains only warnings. Dump verbatim
  45. # with one space indention. Can't do the splitting and allowed
  46. # checking below.
  47. return '\n'.join([filename,
  48. ' Unexpected output from checkbashisms.pl'] +
  49. [' ' + x for x in output.splitlines()])
  50. # We know that the first line matches and that therefore the first
  51. # list entry will be empty - skip it.
  52. output = BASHISM_WARNING.split(output)[1:]
  53. # Turn the output into a single string like this:
  54. # /.../foobar.bb
  55. # possible bashism in updatercd_postrm line 2 (type):
  56. # if ${@use_updatercd(d)} && type update-rc.d >/dev/null 2>/dev/null; then
  57. # ...
  58. # ...
  59. result = []
  60. # Check the results against the allowed list
  61. for message, source in zip(output[0::2], output[1::2]):
  62. if not is_whitelisted(source):
  63. if lineno is not None:
  64. message = SCRIPT_LINENO_RE.sub(lambda m: ' line %d ' % (int(m.group(1)) + int(lineno) - 1),
  65. message)
  66. result.append(' ' + message.strip())
  67. result.extend([' %s' % x for x in source.splitlines()])
  68. if result:
  69. result.insert(0, filename)
  70. return '\n'.join(result)
  71. else:
  72. return None
  73. def get_tinfoil():
  74. scripts_path = os.path.dirname(os.path.realpath(__file__))
  75. lib_path = scripts_path + '/lib'
  76. sys.path = sys.path + [lib_path]
  77. import scriptpath
  78. scriptpath.add_bitbake_lib_path()
  79. import bb.tinfoil
  80. tinfoil = bb.tinfoil.Tinfoil()
  81. tinfoil.prepare()
  82. # tinfoil.logger.setLevel(logging.WARNING)
  83. return tinfoil
  84. if __name__=='__main__':
  85. import argparse, shutil
  86. parser = argparse.ArgumentParser(description='Bashim detector for shell fragments in recipes.')
  87. parser.add_argument("recipes", metavar="RECIPE", nargs="*", help="recipes to check (if not specified, all will be checked)")
  88. parser.add_argument("--verbose", default=False, action="store_true")
  89. args = parser.parse_args()
  90. if shutil.which("checkbashisms.pl") is None:
  91. print("Cannot find checkbashisms.pl on $PATH, get it from https://salsa.debian.org/debian/devscripts/raw/master/scripts/checkbashisms.pl")
  92. sys.exit(1)
  93. # The order of defining the worker function,
  94. # initializing the pool and connecting to the
  95. # bitbake server is crucial, don't change it.
  96. def func(item):
  97. (filename, key, lineno), script = item
  98. if args.verbose:
  99. print("Scanning %s:%s" % (filename, key))
  100. return process(filename, key, lineno, script)
  101. import multiprocessing
  102. pool = multiprocessing.Pool()
  103. tinfoil = get_tinfoil()
  104. # This is only the default configuration and should iterate over
  105. # recipecaches to handle multiconfig environments
  106. pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn
  107. if args.recipes:
  108. initial_pns = args.recipes
  109. else:
  110. initial_pns = sorted(pkg_pn)
  111. pns = set()
  112. scripts = {}
  113. print("Generating scripts...")
  114. for pn in initial_pns:
  115. for fn in pkg_pn[pn]:
  116. # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe
  117. # (at least in general - there is some risk that the variants contain different scripts)
  118. realfn, _, _ = bb.cache.virtualfn2realfn(fn)
  119. if realfn not in pns:
  120. pns.add(realfn)
  121. data = tinfoil.parse_recipe_file(realfn)
  122. for key in data.keys():
  123. if data.getVarFlag(key, "func") and not data.getVarFlag(key, "python"):
  124. script = data.getVar(key, False)
  125. if script:
  126. filename = data.getVarFlag(key, "filename")
  127. lineno = data.getVarFlag(key, "lineno")
  128. # There's no point in checking a function multiple
  129. # times just because different recipes include it.
  130. # We identify unique scripts by file, name, and (just in case)
  131. # line number.
  132. attributes = (filename or realfn, key, lineno)
  133. scripts.setdefault(attributes, script)
  134. print("Scanning scripts...\n")
  135. for result in pool.imap(func, scripts.items()):
  136. if result:
  137. print(result)
  138. tinfoil.shutdown()