blob: 14d8c298e9b887c11d7caeb92c340994c6d05a56 [file] [log] [blame]
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001#!/usr/bin/env python3
Brad Bishopc342db32019-05-15 21:57:59 -04002#
3# SPDX-License-Identifier: GPL-2.0-only
4#
Patrick Williamsc0f7c042017-02-23 20:41:17 -06005
6import sys, os, subprocess, re, shutil
7
8whitelist = (
9 # type is supported by dash
10 'if type systemctl >/dev/null 2>/dev/null; then',
11 'if type systemd-tmpfiles >/dev/null 2>/dev/null; then',
Brad Bishop6e60e8b2018-02-01 10:27:11 -050012 'type update-rc.d >/dev/null 2>/dev/null; then',
Patrick Williamsc0f7c042017-02-23 20:41:17 -060013 'command -v',
14 # HOSTNAME is set locally
15 'buildhistory_single_commit "$CMDLINE" "$HOSTNAME"',
16 # False-positive, match is a grep not shell expression
17 'grep "^$groupname:[^:]*:[^:]*:\\([^,]*,\\)*$username\\(,[^,]*\\)*"',
18 # TODO verify dash's '. script args' behaviour
19 '. $target_sdk_dir/${oe_init_build_env_path} $target_sdk_dir >> $LOGFILE'
20 )
21
22def is_whitelisted(s):
23 for w in whitelist:
24 if w in s:
25 return True
26 return False
27
Brad Bishop6e60e8b2018-02-01 10:27:11 -050028SCRIPT_LINENO_RE = re.compile(r' line (\d+) ')
29BASHISM_WARNING = re.compile(r'^(possible bashism in.*)$', re.MULTILINE)
30
31def process(filename, function, lineno, script):
Patrick Williamsc0f7c042017-02-23 20:41:17 -060032 import tempfile
33
34 if not script.startswith("#!"):
35 script = "#! /bin/sh\n" + script
36
37 fn = tempfile.NamedTemporaryFile(mode="w+t")
38 fn.write(script)
39 fn.flush()
40
41 try:
42 subprocess.check_output(("checkbashisms.pl", fn.name), universal_newlines=True, stderr=subprocess.STDOUT)
43 # No bashisms, so just return
44 return
45 except subprocess.CalledProcessError as e:
46 # TODO check exit code is 1
47
48 # Replace the temporary filename with the function and split it
Brad Bishop6e60e8b2018-02-01 10:27:11 -050049 output = e.output.replace(fn.name, function)
50 if not output or not output.startswith('possible bashism'):
51 # Probably starts with or contains only warnings. Dump verbatim
52 # with one space indention. Can't do the splitting and whitelist
53 # checking below.
54 return '\n'.join([filename,
55 ' Unexpected output from checkbashisms.pl'] +
56 [' ' + x for x in output.splitlines()])
Patrick Williamsc0f7c042017-02-23 20:41:17 -060057
Brad Bishop6e60e8b2018-02-01 10:27:11 -050058 # We know that the first line matches and that therefore the first
59 # list entry will be empty - skip it.
60 output = BASHISM_WARNING.split(output)[1:]
61 # Turn the output into a single string like this:
62 # /.../foobar.bb
63 # possible bashism in updatercd_postrm line 2 (type):
64 # if ${@use_updatercd(d)} && type update-rc.d >/dev/null 2>/dev/null; then
65 # ...
66 # ...
Patrick Williamsc0f7c042017-02-23 20:41:17 -060067 result = []
68 # Check the results against the whitelist
69 for message, source in zip(output[0::2], output[1::2]):
70 if not is_whitelisted(source):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050071 if lineno is not None:
72 message = SCRIPT_LINENO_RE.sub(lambda m: ' line %d ' % (int(m.group(1)) + int(lineno) - 1),
73 message)
74 result.append(' ' + message.strip())
75 result.extend([' %s' % x for x in source.splitlines()])
76 if result:
77 result.insert(0, filename)
78 return '\n'.join(result)
79 else:
80 return None
Patrick Williamsc0f7c042017-02-23 20:41:17 -060081
82def get_tinfoil():
83 scripts_path = os.path.dirname(os.path.realpath(__file__))
84 lib_path = scripts_path + '/lib'
85 sys.path = sys.path + [lib_path]
86 import scriptpath
87 scriptpath.add_bitbake_lib_path()
88 import bb.tinfoil
89 tinfoil = bb.tinfoil.Tinfoil()
90 tinfoil.prepare()
91 # tinfoil.logger.setLevel(logging.WARNING)
92 return tinfoil
93
94if __name__=='__main__':
Brad Bishop316dfdd2018-06-25 12:45:53 -040095 import argparse, shutil
96
97 parser = argparse.ArgumentParser(description='Bashim detector for shell fragments in recipes.')
98 parser.add_argument("recipes", metavar="RECIPE", nargs="*", help="recipes to check (if not specified, all will be checked)")
99 parser.add_argument("--verbose", default=False, action="store_true")
100 args = parser.parse_args()
101
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600102 if shutil.which("checkbashisms.pl") is None:
Andrew Geissler95ac1b82021-03-31 14:34:31 -0500103 print("Cannot find checkbashisms.pl on $PATH, get it from https://salsa.debian.org/debian/devscripts/raw/master/scripts/checkbashisms.pl")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600104 sys.exit(1)
105
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500106 # The order of defining the worker function,
107 # initializing the pool and connecting to the
108 # bitbake server is crucial, don't change it.
109 def func(item):
110 (filename, key, lineno), script = item
Brad Bishop316dfdd2018-06-25 12:45:53 -0400111 if args.verbose:
112 print("Scanning %s:%s" % (filename, key))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500113 return process(filename, key, lineno, script)
114
115 import multiprocessing
116 pool = multiprocessing.Pool()
117
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600118 tinfoil = get_tinfoil()
119
120 # This is only the default configuration and should iterate over
121 # recipecaches to handle multiconfig environments
122 pkg_pn = tinfoil.cooker.recipecaches[""].pkg_pn
123
Brad Bishop316dfdd2018-06-25 12:45:53 -0400124 if args.recipes:
125 initial_pns = args.recipes
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600126 else:
127 initial_pns = sorted(pkg_pn)
128
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500129 pns = set()
130 scripts = {}
131 print("Generating scripts...")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600132 for pn in initial_pns:
133 for fn in pkg_pn[pn]:
134 # There's no point checking multiple BBCLASSEXTENDed variants of the same recipe
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500135 # (at least in general - there is some risk that the variants contain different scripts)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600136 realfn, _, _ = bb.cache.virtualfn2realfn(fn)
137 if realfn not in pns:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500138 pns.add(realfn)
139 data = tinfoil.parse_recipe_file(realfn)
140 for key in data.keys():
141 if data.getVarFlag(key, "func") and not data.getVarFlag(key, "python"):
142 script = data.getVar(key, False)
143 if script:
144 filename = data.getVarFlag(key, "filename")
145 lineno = data.getVarFlag(key, "lineno")
146 # There's no point in checking a function multiple
147 # times just because different recipes include it.
148 # We identify unique scripts by file, name, and (just in case)
149 # line number.
150 attributes = (filename or realfn, key, lineno)
151 scripts.setdefault(attributes, script)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600152
153
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600154 print("Scanning scripts...\n")
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500155 for result in pool.imap(func, scripts.items()):
156 if result:
157 print(result)
158 tinfoil.shutdown()