#!/usr/bin/env python3 
 | 
# 
 | 
# SPDX-License-Identifier: GPL-2.0-only 
 | 
# 
 | 
  
 | 
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', 
 | 
    '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 
 | 
  
 | 
SCRIPT_LINENO_RE = re.compile(r' line (\d+) ') 
 | 
BASHISM_WARNING = re.compile(r'^(possible bashism in.*)$', re.MULTILINE) 
 | 
  
 | 
def process(filename, function, lineno, 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) 
 | 
        if not output or not output.startswith('possible bashism'): 
 | 
            # Probably starts with or contains only warnings. Dump verbatim 
 | 
            # with one space indention. Can't do the splitting and whitelist 
 | 
            # checking below. 
 | 
            return '\n'.join([filename, 
 | 
                              ' Unexpected output from checkbashisms.pl'] + 
 | 
                             [' ' + x for x in output.splitlines()]) 
 | 
  
 | 
        # We know that the first line matches and that therefore the first 
 | 
        # list entry will be empty - skip it. 
 | 
        output = BASHISM_WARNING.split(output)[1:] 
 | 
        # Turn the output into a single string like this: 
 | 
        # /.../foobar.bb 
 | 
        #  possible bashism in updatercd_postrm line 2 (type): 
 | 
        #   if ${@use_updatercd(d)} && type update-rc.d >/dev/null 2>/dev/null; then 
 | 
        #  ... 
 | 
        #   ... 
 | 
        result = [] 
 | 
        # Check the results against the whitelist 
 | 
        for message, source in zip(output[0::2], output[1::2]): 
 | 
            if not is_whitelisted(source): 
 | 
                if lineno is not None: 
 | 
                    message = SCRIPT_LINENO_RE.sub(lambda m: ' line %d ' % (int(m.group(1)) + int(lineno) - 1), 
 | 
                                                   message) 
 | 
                result.append(' ' + message.strip()) 
 | 
                result.extend(['  %s' % x for x in source.splitlines()]) 
 | 
        if result: 
 | 
            result.insert(0, filename) 
 | 
            return '\n'.join(result) 
 | 
        else: 
 | 
            return None 
 | 
  
 | 
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 argparse, shutil 
 | 
  
 | 
    parser = argparse.ArgumentParser(description='Bashim detector for shell fragments in recipes.') 
 | 
    parser.add_argument("recipes", metavar="RECIPE", nargs="*", help="recipes to check (if not specified, all will be checked)") 
 | 
    parser.add_argument("--verbose", default=False, action="store_true") 
 | 
    args = parser.parse_args() 
 | 
  
 | 
    if shutil.which("checkbashisms.pl") is None: 
 | 
        print("Cannot find checkbashisms.pl on $PATH, get it from https://salsa.debian.org/debian/devscripts/raw/master/scripts/checkbashisms.pl") 
 | 
        sys.exit(1) 
 | 
  
 | 
    # The order of defining the worker function, 
 | 
    # initializing the pool and connecting to the 
 | 
    # bitbake server is crucial, don't change it. 
 | 
    def func(item): 
 | 
        (filename, key, lineno), script = item 
 | 
        if args.verbose: 
 | 
            print("Scanning %s:%s" % (filename, key)) 
 | 
        return process(filename, key, lineno, script) 
 | 
  
 | 
    import multiprocessing 
 | 
    pool = multiprocessing.Pool() 
 | 
  
 | 
    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 
 | 
  
 | 
    if args.recipes: 
 | 
        initial_pns = args.recipes 
 | 
    else: 
 | 
        initial_pns = sorted(pkg_pn) 
 | 
  
 | 
    pns = set() 
 | 
    scripts = {} 
 | 
    print("Generating scripts...") 
 | 
    for pn in initial_pns: 
 | 
        for fn in pkg_pn[pn]: 
 | 
            # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe 
 | 
            # (at least in general - there is some risk that the variants contain different scripts) 
 | 
            realfn, _, _ = bb.cache.virtualfn2realfn(fn) 
 | 
            if realfn not in pns: 
 | 
                pns.add(realfn) 
 | 
                data = tinfoil.parse_recipe_file(realfn) 
 | 
                for key in data.keys(): 
 | 
                    if data.getVarFlag(key, "func") and not data.getVarFlag(key, "python"): 
 | 
                        script = data.getVar(key, False) 
 | 
                        if script: 
 | 
                            filename = data.getVarFlag(key, "filename") 
 | 
                            lineno = data.getVarFlag(key, "lineno") 
 | 
                            # There's no point in checking a function multiple 
 | 
                            # times just because different recipes include it. 
 | 
                            # We identify unique scripts by file, name, and (just in case) 
 | 
                            # line number. 
 | 
                            attributes = (filename or realfn, key, lineno) 
 | 
                            scripts.setdefault(attributes, script) 
 | 
  
 | 
  
 | 
    print("Scanning scripts...\n") 
 | 
    for result in pool.imap(func, scripts.items()): 
 | 
        if result: 
 | 
            print(result) 
 | 
    tinfoil.shutdown() 
 |