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