aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/verify-bashisms
blob: 0741e184477af40e61e4ac680fa2e945c53ad04f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/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", True) and not data.getVarFlag(key, "python", True):
                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()