]> CyberLeo.Net >> Repos - FreeBSD/releng/9.2.git/blob - tools/tools/notescheck/notescheck.py
- Copy stable/9 to releng/9.2 as part of the 9.2-RELEASE cycle.
[FreeBSD/releng/9.2.git] / tools / tools / notescheck / notescheck.py
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
10 import glob
11 import os.path
12 import sys
13
14 def 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.
20 requiredfiles = ['conf/files', 'conf/options', 'conf/NOTES']
21
22 # This special platform string is used for managing MI options.
23 global_platform = 'global'
24
25 # This is a global string that represents the current file and line
26 # being parsed.
27 location = ""
28
29 # Format the contents of a set into a sorted, comma-separated string
30 def 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.
53 class 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.
130 class 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
153 options = 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.
157 def 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.
172 def 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.
216 def 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.
246 def 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.
290 def 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.
305 def 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
324 def 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
360 if __name__ == "__main__":
361     sys.exit(main())