]> CyberLeo.Net >> Repos - FreeBSD/releng/10.0.git/blob - tools/tools/notescheck/notescheck.py
- Copy stable/10 (r259064) to releng/10.0 as part of the
[FreeBSD/releng/10.0.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 from __future__ import print_function
11
12 import glob
13 import os.path
14 import sys
15
16 def 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.
22 requiredfiles = ['conf/files', 'conf/options', 'conf/NOTES']
23
24 # This special platform string is used for managing MI options.
25 global_platform = 'global'
26
27 # This is a global string that represents the current file and line
28 # being parsed.
29 location = ""
30
31 # Format the contents of a set into a sorted, comma-separated string
32 def 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.
55 class 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.
132 class 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
155 options = 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.
159 def 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.
174 def 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.
218 def 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.
248 def 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.
292 def 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.
307 def 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
326 def 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
362 if __name__ == "__main__":
363     sys.exit(main())