#!/usr/bin/env python3 import sys, os, subprocess, re, shutil whitelist = ( # type is supported by dash 'if type systemctl >/dev/null 2>/dev/null; then', 'if type systemd-tmpfiles >/dev/null 2>/dev/null; then', 'if type update-rc.d >/dev/null 2>/dev/null; then', 'command -v', # HOSTNAME is set locally 'buildhistory_single_commit "$CMDLINE" "$HOSTNAME"', # False-positive, match is a grep not shell expression 'grep "^$groupname:[^:]*:[^:]*:\\([^,]*,\\)*$username\\(,[^,]*\\)*"', # TODO verify dash's '. script args' behaviour '. $target_sdk_dir/${oe_init_build_env_path} $target_sdk_dir >> $LOGFILE' ) def is_whitelisted(s): for w in whitelist: if w in s: return True return False def process(recipe, function, script): import tempfile if not script.startswith("#!"): script = "#! /bin/sh\n" + script fn = tempfile.NamedTemporaryFile(mode="w+t") fn.write(script) fn.flush() try: subprocess.check_output(("checkbashisms.pl", fn.name), universal_newlines=True, stderr=subprocess.STDOUT) # No bashisms, so just return return except subprocess.CalledProcessError as e: # TODO check exit code is 1 # Replace the temporary filename with the function and split it output = e.output.replace(fn.name, function).splitlines() if len(results) % 2 != 0: print("Unexpected output from checkbashism: %s" % str(output)) return # Turn the output into a list of (message, source) values result = [] # Check the results against the whitelist for message, source in zip(output[0::2], output[1::2]): if not is_whitelisted(source): result.append((message, source)) return result def get_tinfoil(): scripts_path = os.path.dirname(os.path.realpath(__file__)) lib_path = scripts_path + '/lib' sys.path = sys.path + [lib_path] import scriptpath scriptpath.add_bitbake_lib_path() import bb.tinfoil tinfoil = bb.tinfoil.Tinfoil() tinfoil.prepare() # tinfoil.logger.setLevel(logging.WARNING) return tinfoil if __name__=='__main__': import shutil if shutil.which("checkbashisms.pl") is None: print("Cannot find checkbashisms.pl on $PATH") sys.exit(1) tinfoil = get_tinfoil() # This is only the default configuration and should iterate over # recipecaches to handle multiconfig environments pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn # TODO: use argparse and have --help if len(sys.argv) > 1: initial_pns = sys.argv[1:] else: initial_pns = sorted(pkg_pn) pns = [] print("Generating file list...") for pn in initial_pns: for fn in pkg_pn[pn]: # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe realfn, _, _ = bb.cache.virtualfn2realfn(fn) if realfn not in pns: pns.append(realfn) def func(fn): result = [] data = tinfoil.parse_recipe_file(fn) for key in data.keys(): if data.getVarFlag(key, "func") and not data.getVarFlag(key, "python"): script = data.getVar(key, False) if not script: continue #print ("%s:%s" % (fn, key)) r = process(fn, key, script) if r: result.extend(r) return fn, result print("Scanning scripts...\n") import multiprocessing pool = multiprocessing.Pool() for pn,results in pool.imap(func, pns): if results: print(pn) for message,source in results: print(" %s\n %s" % (message, source)) print()