verify-bashisms 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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 shutil
  81. if shutil.which("checkbashisms.pl") is None:
  82. print("Cannot find checkbashisms.pl on $PATH, get it from https://anonscm.debian.org/cgit/collab-maint/devscripts.git/plain/scripts/checkbashisms.pl")
  83. sys.exit(1)
  84. # The order of defining the worker function,
  85. # initializing the pool and connecting to the
  86. # bitbake server is crucial, don't change it.
  87. def func(item):
  88. (filename, key, lineno), script = item
  89. return process(filename, key, lineno, script)
  90. import multiprocessing
  91. pool = multiprocessing.Pool()
  92. tinfoil = get_tinfoil()
  93. # This is only the default configuration and should iterate over
  94. # recipecaches to handle multiconfig environments
  95. pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn
  96. # TODO: use argparse and have --help
  97. if len(sys.argv) > 1:
  98. initial_pns = sys.argv[1:]
  99. else:
  100. initial_pns = sorted(pkg_pn)
  101. pns = set()
  102. scripts = {}
  103. print("Generating scripts...")
  104. for pn in initial_pns:
  105. for fn in pkg_pn[pn]:
  106. # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe
  107. # (at least in general - there is some risk that the variants contain different scripts)
  108. realfn, _, _ = bb.cache.virtualfn2realfn(fn)
  109. if realfn not in pns:
  110. pns.add(realfn)
  111. data = tinfoil.parse_recipe_file(realfn)
  112. for key in data.keys():
  113. if data.getVarFlag(key, "func") and not data.getVarFlag(key, "python"):
  114. script = data.getVar(key, False)
  115. if script:
  116. filename = data.getVarFlag(key, "filename")
  117. lineno = data.getVarFlag(key, "lineno")
  118. # There's no point in checking a function multiple
  119. # times just because different recipes include it.
  120. # We identify unique scripts by file, name, and (just in case)
  121. # line number.
  122. attributes = (filename or realfn, key, lineno)
  123. scripts.setdefault(attributes, script)
  124. print("Scanning scripts...\n")
  125. for result in pool.imap(func, scripts.items()):
  126. if result:
  127. print(result)
  128. tinfoil.shutdown()