verify-bashisms 6.0 KB

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