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