1#!/usr/bin/env python2 2 3"""Find Kconfig symbols that are referenced but not defined.""" 4 5# (c) 2014-2015 Valentin Rothberg <[email protected]> 6# (c) 2014 Stefan Hengelein <[email protected]> 7# 8# Licensed under the terms of the GNU GPL License version 2 9 10 11import os 12import re 13import signal 14import sys 15from multiprocessing import Pool, cpu_count 16from optparse import OptionParser 17from subprocess import Popen, PIPE, STDOUT 18 19 20# regex expressions 21OPERATORS = r"&|\(|\)|\||\!" 22FEATURE = r"(?:\w*[A-Z0-9]\w*){2,}" 23DEF = r"^\s*(?:menu){,1}config\s+(" + FEATURE + r")\s*" 24EXPR = r"(?:" + OPERATORS + r"|\s|" + FEATURE + r")+" 25DEFAULT = r"default\s+.*?(?:if\s.+){,1}" 26STMT = r"^\s*(?:if|select|depends\s+on|(?:" + DEFAULT + r"))\s+" + EXPR 27SOURCE_FEATURE = r"(?:\W|\b)+[D]{,1}CONFIG_(" + FEATURE + r")" 28 29# regex objects 30REGEX_FILE_KCONFIG = re.compile(r".*Kconfig[\.\w+\-]*$") 31REGEX_FEATURE = re.compile(r'(?!\B)' + FEATURE + r'(?!\B)') 32REGEX_SOURCE_FEATURE = re.compile(SOURCE_FEATURE) 33REGEX_KCONFIG_DEF = re.compile(DEF) 34REGEX_KCONFIG_EXPR = re.compile(EXPR) 35REGEX_KCONFIG_STMT = re.compile(STMT) 36REGEX_KCONFIG_HELP = re.compile(r"^\s+(help|---help---)\s*$") 37REGEX_FILTER_FEATURES = re.compile(r"[A-Za-z0-9]$") 38REGEX_NUMERIC = re.compile(r"0[xX][0-9a-fA-F]+|[0-9]+") 39REGEX_QUOTES = re.compile("(\"(.*?)\")") 40 41 42def parse_options(): 43 """The user interface of this module.""" 44 usage = "%prog [options]\n\n" \ 45 "Run this tool to detect Kconfig symbols that are referenced but " \ 46 "not defined in\nKconfig. The output of this tool has the " \ 47 "format \'Undefined symbol\\tFile list\'\n\n" \ 48 "If no option is specified, %prog will default to check your\n" \ 49 "current tree. Please note that specifying commits will " \ 50 "\'git reset --hard\'\nyour current tree! You may save " \ 51 "uncommitted changes to avoid losing data." 52 53 parser = OptionParser(usage=usage) 54 55 parser.add_option('-c', '--commit', dest='commit', action='store', 56 default="", 57 help="Check if the specified commit (hash) introduces " 58 "undefined Kconfig symbols.") 59 60 parser.add_option('-d', '--diff', dest='diff', action='store', 61 default="", 62 help="Diff undefined symbols between two commits. The " 63 "input format bases on Git log's " 64 "\'commmit1..commit2\'.") 65 66 parser.add_option('-f', '--find', dest='find', action='store_true', 67 default=False, 68 help="Find and show commits that may cause symbols to be " 69 "missing. Required to run with --diff.") 70 71 parser.add_option('-i', '--ignore', dest='ignore', action='store', 72 default="", 73 help="Ignore files matching this pattern. Note that " 74 "the pattern needs to be a Python regex. To " 75 "ignore defconfigs, specify -i '.*defconfig'.") 76 77 parser.add_option('', '--force', dest='force', action='store_true', 78 default=False, 79 help="Reset current Git tree even when it's dirty.") 80 81 (opts, _) = parser.parse_args() 82 83 if opts.commit and opts.diff: 84 sys.exit("Please specify only one option at once.") 85 86 if opts.diff and not re.match(r"^[\w\-\.]+\.\.[\w\-\.]+$", opts.diff): 87 sys.exit("Please specify valid input in the following format: " 88 "\'commmit1..commit2\'") 89 90 if opts.commit or opts.diff: 91 if not opts.force and tree_is_dirty(): 92 sys.exit("The current Git tree is dirty (see 'git status'). " 93 "Running this script may\ndelete important data since it " 94 "calls 'git reset --hard' for some performance\nreasons. " 95 " Please run this script in a clean Git tree or pass " 96 "'--force' if you\nwant to ignore this warning and " 97 "continue.") 98 99 if opts.commit: 100 opts.find = False 101 102 if opts.ignore: 103 try: 104 re.match(opts.ignore, "this/is/just/a/test.c") 105 except: 106 sys.exit("Please specify a valid Python regex.") 107 108 return opts 109 110 111def main(): 112 """Main function of this module.""" 113 opts = parse_options() 114 115 if opts.commit or opts.diff: 116 head = get_head() 117 118 # get commit range 119 commit_a = None 120 commit_b = None 121 if opts.commit: 122 commit_a = opts.commit + "~" 123 commit_b = opts.commit 124 elif opts.diff: 125 split = opts.diff.split("..") 126 commit_a = split[0] 127 commit_b = split[1] 128 undefined_a = {} 129 undefined_b = {} 130 131 # get undefined items before the commit 132 execute("git reset --hard %s" % commit_a) 133 undefined_a = check_symbols(opts.ignore) 134 135 # get undefined items for the commit 136 execute("git reset --hard %s" % commit_b) 137 undefined_b = check_symbols(opts.ignore) 138 139 # report cases that are present for the commit but not before 140 for feature in sorted(undefined_b): 141 # feature has not been undefined before 142 if not feature in undefined_a: 143 files = sorted(undefined_b.get(feature)) 144 print "%s\t%s" % (yel(feature), ", ".join(files)) 145 if opts.find: 146 commits = find_commits(feature, opts.diff) 147 print red(commits) 148 # check if there are new files that reference the undefined feature 149 else: 150 files = sorted(undefined_b.get(feature) - 151 undefined_a.get(feature)) 152 if files: 153 print "%s\t%s" % (yel(feature), ", ".join(files)) 154 if opts.find: 155 commits = find_commits(feature, opts.diff) 156 print red(commits) 157 158 # reset to head 159 execute("git reset --hard %s" % head) 160 161 # default to check the entire tree 162 else: 163 undefined = check_symbols(opts.ignore) 164 for feature in sorted(undefined): 165 files = sorted(undefined.get(feature)) 166 print "%s\t%s" % (yel(feature), ", ".join(files)) 167 168 169def yel(string): 170 """ 171 Color %string yellow. 172 """ 173 return "\033[33m%s\033[0m" % string 174 175 176def red(string): 177 """ 178 Color %string red. 179 """ 180 return "\033[31m%s\033[0m" % string 181 182 183def execute(cmd): 184 """Execute %cmd and return stdout. Exit in case of error.""" 185 pop = Popen(cmd, stdout=PIPE, stderr=STDOUT, shell=True) 186 (stdout, _) = pop.communicate() # wait until finished 187 if pop.returncode != 0: 188 sys.exit(stdout) 189 return stdout 190 191 192def find_commits(symbol, diff): 193 """Find commits changing %symbol in the given range of %diff.""" 194 commits = execute("git log --pretty=oneline --abbrev-commit -G %s %s" 195 % (symbol, diff)) 196 return commits 197 198 199def tree_is_dirty(): 200 """Return true if the current working tree is dirty (i.e., if any file has 201 been added, deleted, modified, renamed or copied but not committed).""" 202 stdout = execute("git status --porcelain") 203 for line in stdout: 204 if re.findall(r"[URMADC]{1}", line[:2]): 205 return True 206 return False 207 208 209def get_head(): 210 """Return commit hash of current HEAD.""" 211 stdout = execute("git rev-parse HEAD") 212 return stdout.strip('\n') 213 214 215def partition(lst, size): 216 """Partition list @lst into eveni-sized lists of size @size.""" 217 return [lst[i::size] for i in xrange(size)] 218 219 220def init_worker(): 221 """Set signal handler to ignore SIGINT.""" 222 signal.signal(signal.SIGINT, signal.SIG_IGN) 223 224 225def check_symbols(ignore): 226 """Find undefined Kconfig symbols and return a dict with the symbol as key 227 and a list of referencing files as value. Files matching %ignore are not 228 checked for undefined symbols.""" 229 pool = Pool(cpu_count(), init_worker) 230 try: 231 return check_symbols_helper(pool, ignore) 232 except KeyboardInterrupt: 233 pool.terminate() 234 pool.join() 235 sys.exit(1) 236 237 238def check_symbols_helper(pool, ignore): 239 """Helper method for check_symbols(). Used to catch keyboard interrupts in 240 check_symbols() in order to properly terminate running worker processes.""" 241 source_files = [] 242 kconfig_files = [] 243 defined_features = [] 244 referenced_features = dict() # {file: [features]} 245 246 # use 'git ls-files' to get the worklist 247 stdout = execute("git ls-files") 248 if len(stdout) > 0 and stdout[-1] == "\n": 249 stdout = stdout[:-1] 250 251 for gitfile in stdout.rsplit("\n"): 252 if ".git" in gitfile or "ChangeLog" in gitfile or \ 253 ".log" in gitfile or os.path.isdir(gitfile) or \ 254 gitfile.startswith("tools/"): 255 continue 256 if REGEX_FILE_KCONFIG.match(gitfile): 257 kconfig_files.append(gitfile) 258 else: 259 if ignore and not re.match(ignore, gitfile): 260 continue 261 # add source files that do not match the ignore pattern 262 source_files.append(gitfile) 263 264 # parse source files 265 arglist = partition(source_files, cpu_count()) 266 for res in pool.map(parse_source_files, arglist): 267 referenced_features.update(res) 268 269 270 # parse kconfig files 271 arglist = [] 272 for part in partition(kconfig_files, cpu_count()): 273 arglist.append((part, ignore)) 274 for res in pool.map(parse_kconfig_files, arglist): 275 defined_features.extend(res[0]) 276 referenced_features.update(res[1]) 277 defined_features = set(defined_features) 278 279 # inverse mapping of referenced_features to dict(feature: [files]) 280 inv_map = dict() 281 for _file, features in referenced_features.iteritems(): 282 for feature in features: 283 inv_map[feature] = inv_map.get(feature, set()) 284 inv_map[feature].add(_file) 285 referenced_features = inv_map 286 287 undefined = {} # {feature: [files]} 288 for feature in sorted(referenced_features): 289 # filter some false positives 290 if feature == "FOO" or feature == "BAR" or \ 291 feature == "FOO_BAR" or feature == "XXX": 292 continue 293 if feature not in defined_features: 294 if feature.endswith("_MODULE"): 295 # avoid false positives for kernel modules 296 if feature[:-len("_MODULE")] in defined_features: 297 continue 298 undefined[feature] = referenced_features.get(feature) 299 return undefined 300 301 302def parse_source_files(source_files): 303 """Parse each source file in @source_files and return dictionary with source 304 files as keys and lists of references Kconfig symbols as values.""" 305 referenced_features = dict() 306 for sfile in source_files: 307 referenced_features[sfile] = parse_source_file(sfile) 308 return referenced_features 309 310 311def parse_source_file(sfile): 312 """Parse @sfile and return a list of referenced Kconfig features.""" 313 lines = [] 314 references = [] 315 316 if not os.path.exists(sfile): 317 return references 318 319 with open(sfile, "r") as stream: 320 lines = stream.readlines() 321 322 for line in lines: 323 if not "CONFIG_" in line: 324 continue 325 features = REGEX_SOURCE_FEATURE.findall(line) 326 for feature in features: 327 if not REGEX_FILTER_FEATURES.search(feature): 328 continue 329 references.append(feature) 330 331 return references 332 333 334def get_features_in_line(line): 335 """Return mentioned Kconfig features in @line.""" 336 return REGEX_FEATURE.findall(line) 337 338 339def parse_kconfig_files(args): 340 """Parse kconfig files and return tuple of defined and references Kconfig 341 symbols. Note, @args is a tuple of a list of files and the @ignore 342 pattern.""" 343 kconfig_files = args[0] 344 ignore = args[1] 345 defined_features = [] 346 referenced_features = dict() 347 348 for kfile in kconfig_files: 349 defined, references = parse_kconfig_file(kfile) 350 defined_features.extend(defined) 351 if ignore and re.match(ignore, kfile): 352 # do not collect references for files that match the ignore pattern 353 continue 354 referenced_features[kfile] = references 355 return (defined_features, referenced_features) 356 357 358def parse_kconfig_file(kfile): 359 """Parse @kfile and update feature definitions and references.""" 360 lines = [] 361 defined = [] 362 references = [] 363 skip = False 364 365 if not os.path.exists(kfile): 366 return defined, references 367 368 with open(kfile, "r") as stream: 369 lines = stream.readlines() 370 371 for i in range(len(lines)): 372 line = lines[i] 373 line = line.strip('\n') 374 line = line.split("#")[0] # ignore comments 375 376 if REGEX_KCONFIG_DEF.match(line): 377 feature_def = REGEX_KCONFIG_DEF.findall(line) 378 defined.append(feature_def[0]) 379 skip = False 380 elif REGEX_KCONFIG_HELP.match(line): 381 skip = True 382 elif skip: 383 # ignore content of help messages 384 pass 385 elif REGEX_KCONFIG_STMT.match(line): 386 line = REGEX_QUOTES.sub("", line) 387 features = get_features_in_line(line) 388 # multi-line statements 389 while line.endswith("\\"): 390 i += 1 391 line = lines[i] 392 line = line.strip('\n') 393 features.extend(get_features_in_line(line)) 394 for feature in set(features): 395 if REGEX_NUMERIC.match(feature): 396 # ignore numeric values 397 continue 398 references.append(feature) 399 400 return defined, references 401 402 403if __name__ == "__main__": 404 main() 405