summaryrefslogtreecommitdiffstats
path: root/scripts/verify-bashisms
diff options
context:
space:
mode:
authorRoss Burton <ross.burton@intel.com>2016-02-12 14:55:50 +0000
committerRichard Purdie <richard.purdie@linuxfoundation.org>2016-09-16 15:24:03 +0100
commit359feedfd5b364d4e0695f1b080fabed939bf910 (patch)
tree50a087301845fb90a9aecd17f30dd4fe04ae9939 /scripts/verify-bashisms
parenta24b2fa8f88a02f295f79e8d3b4215c8c4df265d (diff)
downloadpoky-359feedfd5b364d4e0695f1b080fabed939bf910.tar.gz
scripts: add tool to scan for bashisms recipe shell scripts
Shell functions in bitbake are executed with /bin/sh so should be POSIX compliant and not use Bash extensions, or at least only use extensions that are implemented in both dash and ash (busybox). This tool will extract all of the shell scripts from all recipes and run them through checkbashisms (it assumes that checkbashisms is on $PATH). There is a whitelist to filter out false-positives such as the use of $HOSTNAME (a bashism) in functions where we have defined it, or using the 'type' builtin which is supported by ash/dash. [ YOCTO #8851 ] (From OE-Core rev: d77fe838ab7631a19e90ff4226f0712e54aa4e22) Signed-off-by: Ross Burton <ross.burton@intel.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'scripts/verify-bashisms')
-rwxr-xr-xscripts/verify-bashisms116
1 files changed, 116 insertions, 0 deletions
diff --git a/scripts/verify-bashisms b/scripts/verify-bashisms
new file mode 100755
index 0000000000..0741e18447
--- /dev/null
+++ b/scripts/verify-bashisms
@@ -0,0 +1,116 @@
1#!/usr/bin/env python3
2
3import sys, os, subprocess, re, shutil
4
5whitelist = (
6 # type is supported by dash
7 'if type systemctl >/dev/null 2>/dev/null; then',
8 'if type systemd-tmpfiles >/dev/null 2>/dev/null; then',
9 'if type update-rc.d >/dev/null 2>/dev/null; then',
10 'command -v',
11 # HOSTNAME is set locally
12 'buildhistory_single_commit "$CMDLINE" "$HOSTNAME"',
13 # False-positive, match is a grep not shell expression
14 'grep "^$groupname:[^:]*:[^:]*:\\([^,]*,\\)*$username\\(,[^,]*\\)*"',
15 # TODO verify dash's '. script args' behaviour
16 '. $target_sdk_dir/${oe_init_build_env_path} $target_sdk_dir >> $LOGFILE'
17 )
18
19def is_whitelisted(s):
20 for w in whitelist:
21 if w in s:
22 return True
23 return False
24
25def process(recipe, function, script):
26 import tempfile
27
28 if not script.startswith("#!"):
29 script = "#! /bin/sh\n" + script
30
31 fn = tempfile.NamedTemporaryFile(mode="w+t")
32 fn.write(script)
33 fn.flush()
34
35 try:
36 subprocess.check_output(("checkbashisms.pl", fn.name), universal_newlines=True, stderr=subprocess.STDOUT)
37 # No bashisms, so just return
38 return
39 except subprocess.CalledProcessError as e:
40 # TODO check exit code is 1
41
42 # Replace the temporary filename with the function and split it
43 output = e.output.replace(fn.name, function).splitlines()
44 if len(results) % 2 != 0:
45 print("Unexpected output from checkbashism: %s" % str(output))
46 return
47
48 # Turn the output into a list of (message, source) values
49 result = []
50 # Check the results against the whitelist
51 for message, source in zip(output[0::2], output[1::2]):
52 if not is_whitelisted(source):
53 result.append((message, source))
54 return result
55
56def get_tinfoil():
57 scripts_path = os.path.dirname(os.path.realpath(__file__))
58 lib_path = scripts_path + '/lib'
59 sys.path = sys.path + [lib_path]
60 import scriptpath
61 scriptpath.add_bitbake_lib_path()
62 import bb.tinfoil
63 tinfoil = bb.tinfoil.Tinfoil()
64 tinfoil.prepare()
65 # tinfoil.logger.setLevel(logging.WARNING)
66 return tinfoil
67
68if __name__=='__main__':
69 import shutil
70 if shutil.which("checkbashisms.pl") is None:
71 print("Cannot find checkbashisms.pl on $PATH")
72 sys.exit(1)
73
74 tinfoil = get_tinfoil()
75
76 # This is only the default configuration and should iterate over
77 # recipecaches to handle multiconfig environments
78 pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn
79
80 # TODO: use argparse and have --help
81 if len(sys.argv) > 1:
82 initial_pns = sys.argv[1:]
83 else:
84 initial_pns = sorted(pkg_pn)
85
86 pns = []
87 print("Generating file list...")
88 for pn in initial_pns:
89 for fn in pkg_pn[pn]:
90 # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe
91 realfn, _, _ = bb.cache.virtualfn2realfn(fn)
92 if realfn not in pns:
93 pns.append(realfn)
94
95
96 def func(fn):
97 result = []
98 data = tinfoil.parse_recipe_file(fn)
99 for key in data.keys():
100 if data.getVarFlag(key, "func", True) and not data.getVarFlag(key, "python", True):
101 script = data.getVar(key, False)
102 if not script: continue
103 #print ("%s:%s" % (fn, key))
104 r = process(fn, key, script)
105 if r: result.extend(r)
106 return fn, result
107
108 print("Scanning scripts...\n")
109 import multiprocessing
110 pool = multiprocessing.Pool()
111 for pn,results in pool.imap(func, pns):
112 if results:
113 print(pn)
114 for message,source in results:
115 print(" %s\n %s" % (message, source))
116 print()