verify-bashisms 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  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. 'if 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. def process(recipe, function, script):
  22. import tempfile
  23. if not script.startswith("#!"):
  24. script = "#! /bin/sh\n" + script
  25. fn = tempfile.NamedTemporaryFile(mode="w+t")
  26. fn.write(script)
  27. fn.flush()
  28. try:
  29. subprocess.check_output(("checkbashisms.pl", fn.name), universal_newlines=True, stderr=subprocess.STDOUT)
  30. # No bashisms, so just return
  31. return
  32. except subprocess.CalledProcessError as e:
  33. # TODO check exit code is 1
  34. # Replace the temporary filename with the function and split it
  35. output = e.output.replace(fn.name, function).splitlines()
  36. if len(results) % 2 != 0:
  37. print("Unexpected output from checkbashism: %s" % str(output))
  38. return
  39. # Turn the output into a list of (message, source) values
  40. result = []
  41. # Check the results against the whitelist
  42. for message, source in zip(output[0::2], output[1::2]):
  43. if not is_whitelisted(source):
  44. result.append((message, source))
  45. return result
  46. def get_tinfoil():
  47. scripts_path = os.path.dirname(os.path.realpath(__file__))
  48. lib_path = scripts_path + '/lib'
  49. sys.path = sys.path + [lib_path]
  50. import scriptpath
  51. scriptpath.add_bitbake_lib_path()
  52. import bb.tinfoil
  53. tinfoil = bb.tinfoil.Tinfoil()
  54. tinfoil.prepare()
  55. # tinfoil.logger.setLevel(logging.WARNING)
  56. return tinfoil
  57. if __name__=='__main__':
  58. import shutil
  59. if shutil.which("checkbashisms.pl") is None:
  60. print("Cannot find checkbashisms.pl on $PATH")
  61. sys.exit(1)
  62. tinfoil = get_tinfoil()
  63. # This is only the default configuration and should iterate over
  64. # recipecaches to handle multiconfig environments
  65. pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn
  66. # TODO: use argparse and have --help
  67. if len(sys.argv) > 1:
  68. initial_pns = sys.argv[1:]
  69. else:
  70. initial_pns = sorted(pkg_pn)
  71. pns = []
  72. print("Generating file list...")
  73. for pn in initial_pns:
  74. for fn in pkg_pn[pn]:
  75. # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe
  76. realfn, _, _ = bb.cache.virtualfn2realfn(fn)
  77. if realfn not in pns:
  78. pns.append(realfn)
  79. def func(fn):
  80. result = []
  81. data = tinfoil.parse_recipe_file(fn)
  82. for key in data.keys():
  83. if data.getVarFlag(key, "func", True) and not data.getVarFlag(key, "python", True):
  84. script = data.getVar(key, False)
  85. if not script: continue
  86. #print ("%s:%s" % (fn, key))
  87. r = process(fn, key, script)
  88. if r: result.extend(r)
  89. return fn, result
  90. print("Scanning scripts...\n")
  91. import multiprocessing
  92. pool = multiprocessing.Pool()
  93. for pn,results in pool.imap(func, pns):
  94. if results:
  95. print(pn)
  96. for message,source in results:
  97. print(" %s\n %s" % (message, source))
  98. print()