1200941Sjhb#!/usr/local/bin/python 2200941Sjhb# 3200941Sjhb# This script analyzes sys/conf/files*, sys/conf/options*, 4200941Sjhb# sys/conf/NOTES, and sys/*/conf/NOTES and checks for inconsistencies 5200941Sjhb# such as options or devices that are not specified in any NOTES files 6200941Sjhb# or MI devices specified in MD NOTES files. 7200941Sjhb# 8200941Sjhb# $FreeBSD$ 9200941Sjhb 10245804Semastefrom __future__ import print_function 11245804Semaste 12200941Sjhbimport glob 13200941Sjhbimport os.path 14200941Sjhbimport sys 15200941Sjhb 16200941Sjhbdef usage(): 17245536Seadler print("notescheck <path>", file=sys.stderr) 18245536Seadler print(file=sys.stderr) 19245536Seadler print("Where 'path' is a path to a kernel source tree.", file=sys.stderr) 20200941Sjhb 21200941Sjhb# These files are used to determine if a path is a valid kernel source tree. 22200941Sjhbrequiredfiles = ['conf/files', 'conf/options', 'conf/NOTES'] 23200941Sjhb 24200941Sjhb# This special platform string is used for managing MI options. 25200941Sjhbglobal_platform = 'global' 26200941Sjhb 27200941Sjhb# This is a global string that represents the current file and line 28200941Sjhb# being parsed. 29200941Sjhblocation = "" 30200941Sjhb 31200941Sjhb# Format the contents of a set into a sorted, comma-separated string 32200941Sjhbdef format_set(set): 33200941Sjhb l = [] 34200941Sjhb for item in set: 35200941Sjhb l.append(item) 36200941Sjhb if len(l) == 0: 37200941Sjhb return "(empty)" 38200941Sjhb l.sort() 39200941Sjhb if len(l) == 2: 40200941Sjhb return "%s and %s" % (l[0], l[1]) 41200941Sjhb s = "%s" % (l[0]) 42200941Sjhb if len(l) == 1: 43200941Sjhb return s 44200941Sjhb for item in l[1:-1]: 45200941Sjhb s = "%s, %s" % (s, item) 46200941Sjhb s = "%s, and %s" % (s, l[-1]) 47200941Sjhb return s 48200941Sjhb 49200941Sjhb# This class actually covers both options and devices. For each named 50200941Sjhb# option we maintain two different lists. One is the list of 51200941Sjhb# platforms that the option was defined in via an options or files 52200941Sjhb# file. The other is the list of platforms that the option was tested 53200941Sjhb# in via a NOTES file. All options are stored as lowercase since 54200941Sjhb# config(8) treats the names as case-insensitive. 55200941Sjhbclass Option: 56200941Sjhb def __init__(self, name): 57200941Sjhb self.name = name 58200941Sjhb self.type = None 59200941Sjhb self.defines = set() 60200941Sjhb self.tests = set() 61200941Sjhb 62200941Sjhb def set_type(self, type): 63200941Sjhb if self.type is None: 64200941Sjhb self.type = type 65200941Sjhb self.type_location = location 66200941Sjhb elif self.type != type: 67245536Seadler print("WARN: Attempt to change type of %s from %s to %s%s" % \ 68245536Seadler (self.name, self.type, type, location)) 69245536Seadler print(" Previous type set%s" % (self.type_location)) 70200941Sjhb 71200941Sjhb def add_define(self, platform): 72200941Sjhb self.defines.add(platform) 73200941Sjhb 74200941Sjhb def add_test(self, platform): 75200941Sjhb self.tests.add(platform) 76200941Sjhb 77200941Sjhb def title(self): 78200941Sjhb if self.type == 'option': 79200941Sjhb return 'option %s' % (self.name.upper()) 80200941Sjhb if self.type == None: 81200941Sjhb return self.name 82200941Sjhb return '%s %s' % (self.type, self.name) 83200941Sjhb 84200941Sjhb def warn(self): 85200941Sjhb # If the defined and tested sets are equal, then this option 86200941Sjhb # is ok. 87200941Sjhb if self.defines == self.tests: 88200941Sjhb return 89200941Sjhb 90200941Sjhb # If the tested set contains the global platform, then this 91200941Sjhb # option is ok. 92200941Sjhb if global_platform in self.tests: 93200941Sjhb return 94200941Sjhb 95200941Sjhb if global_platform in self.defines: 96249587Sgabor # If the device is defined globally and is never tested, whine. 97200941Sjhb if len(self.tests) == 0: 98245536Seadler print('WARN: %s is defined globally but never tested' % \ 99245536Seadler (self.title())) 100200941Sjhb return 101200941Sjhb 102200941Sjhb # If the device is defined globally and is tested on 103200941Sjhb # multiple MD platforms, then it is ok. This often occurs 104200941Sjhb # for drivers that are shared across multiple, but not 105200941Sjhb # all, platforms (e.g. acpi, agp). 106200941Sjhb if len(self.tests) > 1: 107200941Sjhb return 108200941Sjhb 109200941Sjhb # If a device is defined globally but is only tested on a 110200941Sjhb # single MD platform, then whine about this. 111245536Seadler print('WARN: %s is defined globally but only tested in %s NOTES' % \ 112245536Seadler (self.title(), format_set(self.tests))) 113200941Sjhb return 114200941Sjhb 115200941Sjhb # If an option or device is never tested, whine. 116200941Sjhb if len(self.tests) == 0: 117245536Seadler print('WARN: %s is defined in %s but never tested' % \ 118245536Seadler (self.title(), format_set(self.defines))) 119200941Sjhb return 120200941Sjhb 121200941Sjhb # The set of MD platforms where this option is defined, but not tested. 122200941Sjhb notest = self.defines - self.tests 123200941Sjhb if len(notest) != 0: 124245536Seadler print('WARN: %s is not tested in %s NOTES' % \ 125245536Seadler (self.title(), format_set(notest))) 126200941Sjhb return 127200941Sjhb 128245536Seadler print('ERROR: bad state for %s: defined in %s, tested in %s' % \ 129245536Seadler (self.title(), format_set(self.defines), format_set(self.tests))) 130200941Sjhb 131200941Sjhb# This class maintains a dictionary of options keyed by name. 132200941Sjhbclass Options: 133200941Sjhb def __init__(self): 134200941Sjhb self.options = {} 135200941Sjhb 136200941Sjhb # Look up the object for a given option by name. If the option 137200941Sjhb # doesn't already exist, then add a new option. 138200941Sjhb def find(self, name): 139200941Sjhb name = name.lower() 140200941Sjhb if name in self.options: 141200941Sjhb return self.options[name] 142200941Sjhb option = Option(name) 143200941Sjhb self.options[name] = option 144200941Sjhb return option 145200941Sjhb 146200941Sjhb # Warn about inconsistencies 147200941Sjhb def warn(self): 148245536Seadler keys = list(self.options.keys()) 149200941Sjhb keys.sort() 150200941Sjhb for key in keys: 151200941Sjhb option = self.options[key] 152200941Sjhb option.warn() 153200941Sjhb 154200941Sjhb# Global map of options 155200941Sjhboptions = Options() 156200941Sjhb 157200941Sjhb# Look for MD NOTES files to build our list of platforms. We ignore 158200941Sjhb# platforms that do not have a NOTES file. 159200941Sjhbdef find_platforms(tree): 160200941Sjhb platforms = [] 161200941Sjhb for file in glob.glob(tree + '*/conf/NOTES'): 162200941Sjhb if not file.startswith(tree): 163245536Seadler print("Bad MD NOTES file %s" %(file), file=sys.stderr) 164200941Sjhb sys.exit(1) 165200941Sjhb platforms.append(file[len(tree):].split('/')[0]) 166200941Sjhb if global_platform in platforms: 167245536Seadler print("Found MD NOTES file for global platform", file=sys.stderr) 168200941Sjhb sys.exit(1) 169200941Sjhb return platforms 170200941Sjhb 171200941Sjhb# Parse a file that has escaped newlines. Any escaped newlines are 172200941Sjhb# coalesced and each logical line is passed to the callback function. 173200941Sjhb# This also skips blank lines and comments. 174200941Sjhbdef parse_file(file, callback, *args): 175200941Sjhb global location 176200941Sjhb 177200941Sjhb f = open(file) 178200941Sjhb current = None 179200941Sjhb i = 0 180200941Sjhb for line in f: 181200941Sjhb # Update parsing location 182200941Sjhb i = i + 1 183200941Sjhb location = ' at %s:%d' % (file, i) 184200941Sjhb 185200941Sjhb # Trim the newline 186200941Sjhb line = line[:-1] 187200941Sjhb 188200941Sjhb # If the previous line had an escaped newline, append this 189200941Sjhb # line to that. 190200941Sjhb if current is not None: 191200941Sjhb line = current + line 192200941Sjhb current = None 193200941Sjhb 194200941Sjhb # If the line ends in a '\', set current to the line (minus 195200941Sjhb # the escape) and continue. 196200941Sjhb if len(line) > 0 and line[-1] == '\\': 197200941Sjhb current = line[:-1] 198200941Sjhb continue 199200941Sjhb 200200941Sjhb # Skip blank lines or lines with only whitespace 201200941Sjhb if len(line) == 0 or len(line.split()) == 0: 202200941Sjhb continue 203200941Sjhb 204200941Sjhb # Skip comment lines. Any line whose first non-space 205200941Sjhb # character is a '#' is considered a comment. 206200941Sjhb if line.split()[0][0] == '#': 207200941Sjhb continue 208200941Sjhb 209200941Sjhb # Invoke the callback on this line 210200941Sjhb callback(line, *args) 211200941Sjhb if current is not None: 212200941Sjhb callback(current, *args) 213200941Sjhb 214200941Sjhb location = "" 215200941Sjhb 216200941Sjhb# Split a line into words on whitespace with the exception that quoted 217200941Sjhb# strings are always treated as a single word. 218200941Sjhbdef tokenize(line): 219200941Sjhb if len(line) == 0: 220200941Sjhb return [] 221200941Sjhb 222200941Sjhb # First, split the line on quote characters. 223200941Sjhb groups = line.split('"') 224200941Sjhb 225200941Sjhb # Ensure we have an even number of quotes. The 'groups' array 226200941Sjhb # will contain 'number of quotes' + 1 entries, so it should have 227200941Sjhb # an odd number of entries. 228200941Sjhb if len(groups) % 2 == 0: 229245536Seadler print("Failed to tokenize: %s%s" (line, location), file=sys.stderr) 230200941Sjhb return [] 231200941Sjhb 232200941Sjhb # String split all the "odd" groups since they are not quoted strings. 233200941Sjhb quoted = False 234200941Sjhb words = [] 235200941Sjhb for group in groups: 236200941Sjhb if quoted: 237200941Sjhb words.append(group) 238200941Sjhb quoted = False 239200941Sjhb else: 240200941Sjhb for word in group.split(): 241200941Sjhb words.append(word) 242200941Sjhb quoted = True 243200941Sjhb return words 244200941Sjhb 245200941Sjhb# Parse a sys/conf/files* file adding defines for any options 246200941Sjhb# encountered. Note files does not differentiate between options and 247200941Sjhb# devices. 248200941Sjhbdef parse_files_line(line, platform): 249200941Sjhb words = tokenize(line) 250200941Sjhb 251200941Sjhb # Skip include lines. 252200941Sjhb if words[0] == 'include': 253200941Sjhb return 254200941Sjhb 255200941Sjhb # Skip standard lines as they have no devices or options. 256200941Sjhb if words[1] == 'standard': 257200941Sjhb return 258200941Sjhb 259200941Sjhb # Remaining lines better be optional or mandatory lines. 260200941Sjhb if words[1] != 'optional' and words[1] != 'mandatory': 261245536Seadler print("Invalid files line: %s%s" % (line, location), file=sys.stderr) 262200941Sjhb 263200941Sjhb # Drop the first two words and begin parsing keywords and devices. 264200941Sjhb skip = False 265200941Sjhb for word in words[2:]: 266200941Sjhb if skip: 267200941Sjhb skip = False 268200941Sjhb continue 269200941Sjhb 270200941Sjhb # Skip keywords 271200941Sjhb if word == 'no-obj' or word == 'no-implicit-rule' or \ 272200941Sjhb word == 'before-depend' or word == 'local' or \ 273200941Sjhb word == 'no-depend' or word == 'profiling-routine' or \ 274200941Sjhb word == 'nowerror': 275200941Sjhb continue 276200941Sjhb 277200941Sjhb # Skip keywords and their following argument 278200941Sjhb if word == 'dependency' or word == 'clean' or \ 279200941Sjhb word == 'compile-with' or word == 'warning': 280200941Sjhb skip = True 281200941Sjhb continue 282200941Sjhb 283200941Sjhb # Ignore pipes 284200941Sjhb if word == '|': 285200941Sjhb continue 286200941Sjhb 287200941Sjhb option = options.find(word) 288200941Sjhb option.add_define(platform) 289200941Sjhb 290200941Sjhb# Parse a sys/conf/options* file adding defines for any options 291200941Sjhb# encountered. Unlike a files file, options files only add options. 292200941Sjhbdef parse_options_line(line, platform): 293200941Sjhb # The first word is the option name. 294200941Sjhb name = line.split()[0] 295200941Sjhb 296200941Sjhb # Ignore DEV_xxx options. These are magic options that are 297200941Sjhb # aliases for 'device xxx'. 298200941Sjhb if name.startswith('DEV_'): 299200941Sjhb return 300200941Sjhb 301200941Sjhb option = options.find(name) 302200941Sjhb option.add_define(platform) 303200941Sjhb option.set_type('option') 304200941Sjhb 305200941Sjhb# Parse a sys/conf/NOTES file adding tests for any options or devices 306200941Sjhb# encountered. 307200941Sjhbdef parse_notes_line(line, platform): 308200941Sjhb words = line.split() 309200941Sjhb 310200941Sjhb # Skip lines with just whitespace 311200941Sjhb if len(words) == 0: 312200941Sjhb return 313200941Sjhb 314200941Sjhb if words[0] == 'device' or words[0] == 'devices': 315200941Sjhb option = options.find(words[1]) 316200941Sjhb option.add_test(platform) 317200941Sjhb option.set_type('device') 318200941Sjhb return 319200941Sjhb 320200941Sjhb if words[0] == 'option' or words[0] == 'options': 321200941Sjhb option = options.find(words[1].split('=')[0]) 322200941Sjhb option.add_test(platform) 323200941Sjhb option.set_type('option') 324200941Sjhb return 325200941Sjhb 326200941Sjhbdef main(argv=None): 327200941Sjhb if argv is None: 328200941Sjhb argv = sys.argv 329200941Sjhb if len(sys.argv) != 2: 330200941Sjhb usage() 331200941Sjhb return 2 332200941Sjhb 333200941Sjhb # Ensure the path has a trailing '/'. 334200941Sjhb tree = sys.argv[1] 335200941Sjhb if tree[-1] != '/': 336200941Sjhb tree = tree + '/' 337200941Sjhb for file in requiredfiles: 338200941Sjhb if not os.path.exists(tree + file): 339245536Seadler print("Kernel source tree missing %s" % (file), file=sys.stderr) 340200941Sjhb return 1 341200941Sjhb 342200941Sjhb platforms = find_platforms(tree) 343200941Sjhb 344200941Sjhb # First, parse global files. 345200941Sjhb parse_file(tree + 'conf/files', parse_files_line, global_platform) 346200941Sjhb parse_file(tree + 'conf/options', parse_options_line, global_platform) 347200941Sjhb parse_file(tree + 'conf/NOTES', parse_notes_line, global_platform) 348200941Sjhb 349200941Sjhb # Next, parse MD files. 350200941Sjhb for platform in platforms: 351200941Sjhb files_file = tree + 'conf/files.' + platform 352200941Sjhb if os.path.exists(files_file): 353200941Sjhb parse_file(files_file, parse_files_line, platform) 354200941Sjhb options_file = tree + 'conf/options.' + platform 355200941Sjhb if os.path.exists(options_file): 356200941Sjhb parse_file(options_file, parse_options_line, platform) 357200941Sjhb parse_file(tree + platform + '/conf/NOTES', parse_notes_line, platform) 358200941Sjhb 359200941Sjhb options.warn() 360200941Sjhb return 0 361200941Sjhb 362200941Sjhbif __name__ == "__main__": 363200941Sjhb sys.exit(main()) 364