1#!/usr/local/bin/python 2# 3# This script analyzes sys/conf/files*, sys/conf/options*, 4# sys/conf/NOTES, and sys/*/conf/NOTES and checks for inconsistencies 5# such as options or devices that are not specified in any NOTES files 6# or MI devices specified in MD NOTES files. 7# 8 9from __future__ import print_function 10 11import glob 12import os.path 13import sys 14 15def usage(): 16 print("notescheck <path>", file=sys.stderr) 17 print(file=sys.stderr) 18 print("Where 'path' is a path to a kernel source tree.", file=sys.stderr) 19 20# These files are used to determine if a path is a valid kernel source tree. 21requiredfiles = ['conf/files', 'conf/options', 'conf/NOTES'] 22 23# This special platform string is used for managing MI options. 24global_platform = 'global' 25 26# This is a global string that represents the current file and line 27# being parsed. 28location = "" 29 30# Format the contents of a set into a sorted, comma-separated string 31def format_set(set): 32 l = [] 33 for item in set: 34 l.append(item) 35 if len(l) == 0: 36 return "(empty)" 37 l.sort() 38 if len(l) == 2: 39 return "%s and %s" % (l[0], l[1]) 40 s = "%s" % (l[0]) 41 if len(l) == 1: 42 return s 43 for item in l[1:-1]: 44 s = "%s, %s" % (s, item) 45 s = "%s, and %s" % (s, l[-1]) 46 return s 47 48# This class actually covers both options and devices. For each named 49# option we maintain two different lists. One is the list of 50# platforms that the option was defined in via an options or files 51# file. The other is the list of platforms that the option was tested 52# in via a NOTES file. All options are stored as lowercase since 53# config(8) treats the names as case-insensitive. 54class Option: 55 def __init__(self, name): 56 self.name = name 57 self.type = None 58 self.defines = set() 59 self.tests = set() 60 61 def set_type(self, type): 62 if self.type is None: 63 self.type = type 64 self.type_location = location 65 elif self.type != type: 66 print("WARN: Attempt to change type of %s from %s to %s%s" % \ 67 (self.name, self.type, type, location)) 68 print(" Previous type set%s" % (self.type_location)) 69 70 def add_define(self, platform): 71 self.defines.add(platform) 72 73 def add_test(self, platform): 74 self.tests.add(platform) 75 76 def title(self): 77 if self.type == 'option': 78 return 'option %s' % (self.name.upper()) 79 if self.type == None: 80 return self.name 81 return '%s %s' % (self.type, self.name) 82 83 def warn(self): 84 # If the defined and tested sets are equal, then this option 85 # is ok. 86 if self.defines == self.tests: 87 return 88 89 # If the tested set contains the global platform, then this 90 # option is ok. 91 if global_platform in self.tests: 92 return 93 94 if global_platform in self.defines: 95 # If the device is defined globally and is never tested, whine. 96 if len(self.tests) == 0: 97 print('WARN: %s is defined globally but never tested' % \ 98 (self.title())) 99 return 100 101 # If the device is defined globally and is tested on 102 # multiple MD platforms, then it is ok. This often occurs 103 # for drivers that are shared across multiple, but not 104 # all, platforms (e.g. acpi, agp). 105 if len(self.tests) > 1: 106 return 107 108 # If a device is defined globally but is only tested on a 109 # single MD platform, then whine about this. 110 print('WARN: %s is defined globally but only tested in %s NOTES' % \ 111 (self.title(), format_set(self.tests))) 112 return 113 114 # If an option or device is never tested, whine. 115 if len(self.tests) == 0: 116 print('WARN: %s is defined in %s but never tested' % \ 117 (self.title(), format_set(self.defines))) 118 return 119 120 # The set of MD platforms where this option is defined, but not tested. 121 notest = self.defines - self.tests 122 if len(notest) != 0: 123 print('WARN: %s is not tested in %s NOTES' % \ 124 (self.title(), format_set(notest))) 125 return 126 127 print('ERROR: bad state for %s: defined in %s, tested in %s' % \ 128 (self.title(), format_set(self.defines), format_set(self.tests))) 129 130# This class maintains a dictionary of options keyed by name. 131class Options: 132 def __init__(self): 133 self.options = {} 134 135 # Look up the object for a given option by name. If the option 136 # doesn't already exist, then add a new option. 137 def find(self, name): 138 name = name.lower() 139 if name in self.options: 140 return self.options[name] 141 option = Option(name) 142 self.options[name] = option 143 return option 144 145 # Warn about inconsistencies 146 def warn(self): 147 keys = list(self.options.keys()) 148 keys.sort() 149 for key in keys: 150 option = self.options[key] 151 option.warn() 152 153# Global map of options 154options = Options() 155 156# Look for MD NOTES files to build our list of platforms. We ignore 157# platforms that do not have a NOTES file. 158def find_platforms(tree): 159 platforms = [] 160 for file in glob.glob(tree + '*/conf/NOTES'): 161 if not file.startswith(tree): 162 print("Bad MD NOTES file %s" %(file), file=sys.stderr) 163 sys.exit(1) 164 platforms.append(file[len(tree):].split('/')[0]) 165 if global_platform in platforms: 166 print("Found MD NOTES file for global platform", file=sys.stderr) 167 sys.exit(1) 168 return platforms 169 170# Parse a file that has escaped newlines. Any escaped newlines are 171# coalesced and each logical line is passed to the callback function. 172# This also skips blank lines and comments. 173def parse_file(file, callback, *args): 174 global location 175 176 f = open(file) 177 current = None 178 i = 0 179 for line in f: 180 # Update parsing location 181 i = i + 1 182 location = ' at %s:%d' % (file, i) 183 184 # Trim the newline 185 line = line[:-1] 186 187 # If the previous line had an escaped newline, append this 188 # line to that. 189 if current is not None: 190 line = current + line 191 current = None 192 193 # If the line ends in a '\', set current to the line (minus 194 # the escape) and continue. 195 if len(line) > 0 and line[-1] == '\\': 196 current = line[:-1] 197 continue 198 199 # Skip blank lines or lines with only whitespace 200 if len(line) == 0 or len(line.split()) == 0: 201 continue 202 203 # Skip comment lines. Any line whose first non-space 204 # character is a '#' is considered a comment. 205 if line.split()[0][0] == '#': 206 continue 207 208 # Invoke the callback on this line 209 callback(line, *args) 210 if current is not None: 211 callback(current, *args) 212 213 location = "" 214 215# Split a line into words on whitespace with the exception that quoted 216# strings are always treated as a single word. 217def tokenize(line): 218 if len(line) == 0: 219 return [] 220 221 # First, split the line on quote characters. 222 groups = line.split('"') 223 224 # Ensure we have an even number of quotes. The 'groups' array 225 # will contain 'number of quotes' + 1 entries, so it should have 226 # an odd number of entries. 227 if len(groups) % 2 == 0: 228 print("Failed to tokenize: %s%s" (line, location), file=sys.stderr) 229 return [] 230 231 # String split all the "odd" groups since they are not quoted strings. 232 quoted = False 233 words = [] 234 for group in groups: 235 if quoted: 236 words.append(group) 237 quoted = False 238 else: 239 for word in group.split(): 240 words.append(word) 241 quoted = True 242 return words 243 244# Parse a sys/conf/files* file adding defines for any options 245# encountered. Note files does not differentiate between options and 246# devices. 247def parse_files_line(line, platform): 248 words = tokenize(line) 249 250 # Skip include lines. 251 if words[0] == 'include': 252 return 253 254 # Skip standard lines as they have no devices or options. 255 if words[1] == 'standard': 256 return 257 258 # Remaining lines better be optional or mandatory lines. 259 if words[1] != 'optional' and words[1] != 'mandatory': 260 print("Invalid files line: %s%s" % (line, location), file=sys.stderr) 261 262 # Drop the first two words and begin parsing keywords and devices. 263 skip = False 264 for word in words[2:]: 265 if skip: 266 skip = False 267 continue 268 269 # Skip keywords 270 if word == 'no-obj' or word == 'no-implicit-rule' or \ 271 word == 'before-depend' or word == 'local' or \ 272 word == 'no-depend' or word == 'profiling-routine' or \ 273 word == 'nowerror': 274 continue 275 276 # Skip keywords and their following argument 277 if word == 'dependency' or word == 'clean' or \ 278 word == 'compile-with' or word == 'warning': 279 skip = True 280 continue 281 282 # Ignore pipes 283 if word == '|': 284 continue 285 286 option = options.find(word) 287 option.add_define(platform) 288 289# Parse a sys/conf/options* file adding defines for any options 290# encountered. Unlike a files file, options files only add options. 291def parse_options_line(line, platform): 292 # The first word is the option name. 293 name = line.split()[0] 294 295 # Ignore DEV_xxx options. These are magic options that are 296 # aliases for 'device xxx'. 297 if name.startswith('DEV_'): 298 return 299 300 option = options.find(name) 301 option.add_define(platform) 302 option.set_type('option') 303 304# Parse a sys/conf/NOTES file adding tests for any options or devices 305# encountered. 306def parse_notes_line(line, platform): 307 words = line.split() 308 309 # Skip lines with just whitespace 310 if len(words) == 0: 311 return 312 313 if words[0] == 'device' or words[0] == 'devices': 314 option = options.find(words[1]) 315 option.add_test(platform) 316 option.set_type('device') 317 return 318 319 if words[0] == 'option' or words[0] == 'options': 320 option = options.find(words[1].split('=')[0]) 321 option.add_test(platform) 322 option.set_type('option') 323 return 324 325def main(argv=None): 326 if argv is None: 327 argv = sys.argv 328 if len(sys.argv) != 2: 329 usage() 330 return 2 331 332 # Ensure the path has a trailing '/'. 333 tree = sys.argv[1] 334 if tree[-1] != '/': 335 tree = tree + '/' 336 for file in requiredfiles: 337 if not os.path.exists(tree + file): 338 print("Kernel source tree missing %s" % (file), file=sys.stderr) 339 return 1 340 341 platforms = find_platforms(tree) 342 343 # First, parse global files. 344 parse_file(tree + 'conf/files', parse_files_line, global_platform) 345 parse_file(tree + 'conf/options', parse_options_line, global_platform) 346 parse_file(tree + 'conf/NOTES', parse_notes_line, global_platform) 347 348 # Next, parse MD files. 349 for platform in platforms: 350 files_file = tree + 'conf/files.' + platform 351 if os.path.exists(files_file): 352 parse_file(files_file, parse_files_line, platform) 353 options_file = tree + 'conf/options.' + platform 354 if os.path.exists(options_file): 355 parse_file(options_file, parse_options_line, platform) 356 parse_file(tree + platform + '/conf/NOTES', parse_notes_line, platform) 357 358 options.warn() 359 return 0 360 361if __name__ == "__main__": 362 sys.exit(main()) 363