1 #!/usr/local/bin/python
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.
10 from __future__ import print_function
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)
21 # These files are used to determine if a path is a valid kernel source tree.
22 requiredfiles = ['conf/files', 'conf/options', 'conf/NOTES']
24 # This special platform string is used for managing MI options.
25 global_platform = 'global'
27 # This is a global string that represents the current file and line
31 # Format the contents of a set into a sorted, comma-separated string
40 return "%s and %s" % (l[0], l[1])
45 s = "%s, %s" % (s, item)
46 s = "%s, and %s" % (s, l[-1])
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.
56 def __init__(self, name):
62 def set_type(self, 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))
71 def add_define(self, platform):
72 self.defines.add(platform)
74 def add_test(self, platform):
75 self.tests.add(platform)
78 if self.type == 'option':
79 return 'option %s' % (self.name.upper())
82 return '%s %s' % (self.type, self.name)
85 # If the defined and tested sets are equal, then this option
87 if self.defines == self.tests:
90 # If the tested set contains the global platform, then this
92 if global_platform in self.tests:
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' % \
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:
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)))
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)))
121 # The set of MD platforms where this option is defined, but not tested.
122 notest = self.defines - self.tests
124 print('WARN: %s is not tested in %s NOTES' % \
125 (self.title(), format_set(notest)))
128 print('ERROR: bad state for %s: defined in %s, tested in %s' % \
129 (self.title(), format_set(self.defines), format_set(self.tests)))
131 # This class maintains a dictionary of options keyed by name.
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):
140 if name in self.options:
141 return self.options[name]
142 option = Option(name)
143 self.options[name] = option
146 # Warn about inconsistencies
148 keys = list(self.options.keys())
151 option = self.options[key]
154 # Global map of options
157 # Look for MD NOTES files to build our list of platforms. We ignore
158 # platforms that do not have a NOTES file.
159 def find_platforms(tree):
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)
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)
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.
174 def parse_file(file, callback, *args):
181 # Update parsing location
183 location = ' at %s:%d' % (file, i)
188 # If the previous line had an escaped newline, append this
190 if current is not None:
191 line = current + line
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] == '\\':
200 # Skip blank lines or lines with only whitespace
201 if len(line) == 0 or len(line.split()) == 0:
204 # Skip comment lines. Any line whose first non-space
205 # character is a '#' is considered a comment.
206 if line.split()[0][0] == '#':
209 # Invoke the callback on this line
210 callback(line, *args)
211 if current is not None:
212 callback(current, *args)
216 # Split a line into words on whitespace with the exception that quoted
217 # strings are always treated as a single word.
222 # First, split the line on quote characters.
223 groups = line.split('"')
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)
232 # String split all the "odd" groups since they are not quoted strings.
240 for word in group.split():
245 # Parse a sys/conf/files* file adding defines for any options
246 # encountered. Note files does not differentiate between options and
248 def parse_files_line(line, platform):
249 words = tokenize(line)
251 # Skip include lines.
252 if words[0] == 'include':
255 # Skip standard lines as they have no devices or options.
256 if words[1] == 'standard':
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)
263 # Drop the first two words and begin parsing keywords and devices.
265 for word in words[2:]:
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 \
277 # Skip keywords and their following argument
278 if word == 'dependency' or word == 'clean' or \
279 word == 'compile-with' or word == 'warning':
287 option = options.find(word)
288 option.add_define(platform)
290 # Parse a sys/conf/options* file adding defines for any options
291 # encountered. Unlike a files file, options files only add options.
292 def parse_options_line(line, platform):
293 # The first word is the option name.
294 name = line.split()[0]
296 # Ignore DEV_xxx options. These are magic options that are
297 # aliases for 'device xxx'.
298 if name.startswith('DEV_'):
301 option = options.find(name)
302 option.add_define(platform)
303 option.set_type('option')
305 # Parse a sys/conf/NOTES file adding tests for any options or devices
307 def parse_notes_line(line, platform):
310 # Skip lines with just whitespace
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')
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')
329 if len(sys.argv) != 2:
333 # Ensure the path has a trailing '/'.
337 for file in requiredfiles:
338 if not os.path.exists(tree + file):
339 print("Kernel source tree missing %s" % (file), file=sys.stderr)
342 platforms = find_platforms(tree)
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)
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)
362 if __name__ == "__main__":