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