1200941Sjhb#!/usr/local/bin/python
2200941Sjhb#
3200941Sjhb# This script analyzes sys/conf/files*, sys/conf/options*,
4200941Sjhb# sys/conf/NOTES, and sys/*/conf/NOTES and checks for inconsistencies
5200941Sjhb# such as options or devices that are not specified in any NOTES files
6200941Sjhb# or MI devices specified in MD NOTES files.
7200941Sjhb#
8200941Sjhb# $FreeBSD$
9200941Sjhb
10245804Semastefrom __future__ import print_function
11245804Semaste
12200941Sjhbimport glob
13200941Sjhbimport os.path
14200941Sjhbimport sys
15200941Sjhb
16200941Sjhbdef usage():
17245536Seadler    print("notescheck <path>", file=sys.stderr)
18245536Seadler    print(file=sys.stderr)
19245536Seadler    print("Where 'path' is a path to a kernel source tree.", file=sys.stderr)
20200941Sjhb
21200941Sjhb# These files are used to determine if a path is a valid kernel source tree.
22200941Sjhbrequiredfiles = ['conf/files', 'conf/options', 'conf/NOTES']
23200941Sjhb
24200941Sjhb# This special platform string is used for managing MI options.
25200941Sjhbglobal_platform = 'global'
26200941Sjhb
27200941Sjhb# This is a global string that represents the current file and line
28200941Sjhb# being parsed.
29200941Sjhblocation = ""
30200941Sjhb
31200941Sjhb# Format the contents of a set into a sorted, comma-separated string
32200941Sjhbdef format_set(set):
33200941Sjhb    l = []
34200941Sjhb    for item in set:
35200941Sjhb        l.append(item)
36200941Sjhb    if len(l) == 0:
37200941Sjhb        return "(empty)"
38200941Sjhb    l.sort()
39200941Sjhb    if len(l) == 2:
40200941Sjhb        return "%s and %s" % (l[0], l[1])
41200941Sjhb    s = "%s" % (l[0])
42200941Sjhb    if len(l) == 1:
43200941Sjhb        return s
44200941Sjhb    for item in l[1:-1]:
45200941Sjhb        s = "%s, %s" % (s, item)
46200941Sjhb    s = "%s, and %s" % (s, l[-1])
47200941Sjhb    return s
48200941Sjhb
49200941Sjhb# This class actually covers both options and devices.  For each named
50200941Sjhb# option we maintain two different lists.  One is the list of
51200941Sjhb# platforms that the option was defined in via an options or files
52200941Sjhb# file.  The other is the list of platforms that the option was tested
53200941Sjhb# in via a NOTES file.  All options are stored as lowercase since
54200941Sjhb# config(8) treats the names as case-insensitive.
55200941Sjhbclass Option:
56200941Sjhb    def __init__(self, name):
57200941Sjhb        self.name = name
58200941Sjhb        self.type = None
59200941Sjhb        self.defines = set()
60200941Sjhb        self.tests = set()
61200941Sjhb
62200941Sjhb    def set_type(self, type):
63200941Sjhb        if self.type is None:
64200941Sjhb            self.type = type
65200941Sjhb            self.type_location = location
66200941Sjhb        elif self.type != type:
67245536Seadler            print("WARN: Attempt to change type of %s from %s to %s%s" % \
68245536Seadler                (self.name, self.type, type, location))
69245536Seadler            print("      Previous type set%s" % (self.type_location))
70200941Sjhb
71200941Sjhb    def add_define(self, platform):
72200941Sjhb        self.defines.add(platform)
73200941Sjhb
74200941Sjhb    def add_test(self, platform):
75200941Sjhb        self.tests.add(platform)
76200941Sjhb
77200941Sjhb    def title(self):
78200941Sjhb        if self.type == 'option':
79200941Sjhb            return 'option %s' % (self.name.upper())
80200941Sjhb        if self.type == None:
81200941Sjhb            return self.name
82200941Sjhb        return '%s %s' % (self.type, self.name)
83200941Sjhb
84200941Sjhb    def warn(self):
85200941Sjhb        # If the defined and tested sets are equal, then this option
86200941Sjhb        # is ok.
87200941Sjhb        if self.defines == self.tests:
88200941Sjhb            return
89200941Sjhb
90200941Sjhb        # If the tested set contains the global platform, then this
91200941Sjhb        # option is ok.
92200941Sjhb        if global_platform in self.tests:
93200941Sjhb            return
94200941Sjhb
95200941Sjhb        if global_platform in self.defines:
96249587Sgabor            # If the device is defined globally and is never tested, whine.
97200941Sjhb            if len(self.tests) == 0:
98245536Seadler                print('WARN: %s is defined globally but never tested' % \
99245536Seadler                    (self.title()))
100200941Sjhb                return
101200941Sjhb
102200941Sjhb            # If the device is defined globally and is tested on
103200941Sjhb            # multiple MD platforms, then it is ok.  This often occurs
104200941Sjhb            # for drivers that are shared across multiple, but not
105200941Sjhb            # all, platforms (e.g. acpi, agp).
106200941Sjhb            if len(self.tests) > 1:
107200941Sjhb                return
108200941Sjhb
109200941Sjhb            # If a device is defined globally but is only tested on a
110200941Sjhb            # single MD platform, then whine about this.
111245536Seadler            print('WARN: %s is defined globally but only tested in %s NOTES' % \
112245536Seadler                (self.title(), format_set(self.tests)))
113200941Sjhb            return
114200941Sjhb
115200941Sjhb        # If an option or device is never tested, whine.
116200941Sjhb        if len(self.tests) == 0:
117245536Seadler            print('WARN: %s is defined in %s but never tested' % \
118245536Seadler                (self.title(), format_set(self.defines)))
119200941Sjhb            return
120200941Sjhb
121200941Sjhb        # The set of MD platforms where this option is defined, but not tested.
122200941Sjhb        notest = self.defines - self.tests
123200941Sjhb        if len(notest) != 0:
124245536Seadler            print('WARN: %s is not tested in %s NOTES' % \
125245536Seadler                (self.title(), format_set(notest)))
126200941Sjhb            return
127200941Sjhb
128245536Seadler        print('ERROR: bad state for %s: defined in %s, tested in %s' % \
129245536Seadler            (self.title(), format_set(self.defines), format_set(self.tests)))
130200941Sjhb
131200941Sjhb# This class maintains a dictionary of options keyed by name.
132200941Sjhbclass Options:
133200941Sjhb    def __init__(self):
134200941Sjhb        self.options = {}
135200941Sjhb
136200941Sjhb    # Look up the object for a given option by name.  If the option
137200941Sjhb    # doesn't already exist, then add a new option.
138200941Sjhb    def find(self, name):
139200941Sjhb        name = name.lower()
140200941Sjhb        if name in self.options:
141200941Sjhb            return self.options[name]
142200941Sjhb        option = Option(name)
143200941Sjhb        self.options[name] = option
144200941Sjhb        return option
145200941Sjhb
146200941Sjhb    # Warn about inconsistencies
147200941Sjhb    def warn(self):
148245536Seadler        keys = list(self.options.keys())
149200941Sjhb        keys.sort()
150200941Sjhb        for key in keys:
151200941Sjhb            option = self.options[key]
152200941Sjhb            option.warn()
153200941Sjhb
154200941Sjhb# Global map of options
155200941Sjhboptions = Options()
156200941Sjhb
157200941Sjhb# Look for MD NOTES files to build our list of platforms.  We ignore
158200941Sjhb# platforms that do not have a NOTES file.
159200941Sjhbdef find_platforms(tree):
160200941Sjhb    platforms = []
161200941Sjhb    for file in glob.glob(tree + '*/conf/NOTES'):
162200941Sjhb        if not file.startswith(tree):
163245536Seadler            print("Bad MD NOTES file %s" %(file), file=sys.stderr)
164200941Sjhb            sys.exit(1)
165200941Sjhb        platforms.append(file[len(tree):].split('/')[0])
166200941Sjhb    if global_platform in platforms:
167245536Seadler        print("Found MD NOTES file for global platform", file=sys.stderr)
168200941Sjhb        sys.exit(1)
169200941Sjhb    return platforms
170200941Sjhb
171200941Sjhb# Parse a file that has escaped newlines.  Any escaped newlines are
172200941Sjhb# coalesced and each logical line is passed to the callback function.
173200941Sjhb# This also skips blank lines and comments.
174200941Sjhbdef parse_file(file, callback, *args):
175200941Sjhb    global location
176200941Sjhb
177200941Sjhb    f = open(file)
178200941Sjhb    current = None
179200941Sjhb    i = 0
180200941Sjhb    for line in f:
181200941Sjhb        # Update parsing location
182200941Sjhb        i = i + 1
183200941Sjhb        location = ' at %s:%d' % (file, i)
184200941Sjhb
185200941Sjhb        # Trim the newline
186200941Sjhb        line = line[:-1]
187200941Sjhb
188200941Sjhb        # If the previous line had an escaped newline, append this
189200941Sjhb        # line to that.
190200941Sjhb        if current is not None:
191200941Sjhb            line = current + line
192200941Sjhb            current = None
193200941Sjhb
194200941Sjhb        # If the line ends in a '\', set current to the line (minus
195200941Sjhb        # the escape) and continue.
196200941Sjhb        if len(line) > 0 and line[-1] == '\\':
197200941Sjhb            current = line[:-1]
198200941Sjhb            continue
199200941Sjhb
200200941Sjhb        # Skip blank lines or lines with only whitespace
201200941Sjhb        if len(line) == 0 or len(line.split()) == 0:
202200941Sjhb            continue
203200941Sjhb
204200941Sjhb        # Skip comment lines.  Any line whose first non-space
205200941Sjhb        # character is a '#' is considered a comment.
206200941Sjhb        if line.split()[0][0] == '#':
207200941Sjhb            continue
208200941Sjhb
209200941Sjhb        # Invoke the callback on this line
210200941Sjhb        callback(line, *args)
211200941Sjhb    if current is not None:
212200941Sjhb        callback(current, *args)
213200941Sjhb
214200941Sjhb    location = ""
215200941Sjhb
216200941Sjhb# Split a line into words on whitespace with the exception that quoted
217200941Sjhb# strings are always treated as a single word.
218200941Sjhbdef tokenize(line):
219200941Sjhb    if len(line) == 0:
220200941Sjhb        return []
221200941Sjhb
222200941Sjhb    # First, split the line on quote characters.
223200941Sjhb    groups = line.split('"')
224200941Sjhb
225200941Sjhb    # Ensure we have an even number of quotes.  The 'groups' array
226200941Sjhb    # will contain 'number of quotes' + 1 entries, so it should have
227200941Sjhb    # an odd number of entries.
228200941Sjhb    if len(groups) % 2 == 0:
229245536Seadler        print("Failed to tokenize: %s%s" (line, location), file=sys.stderr)
230200941Sjhb        return []
231200941Sjhb
232200941Sjhb    # String split all the "odd" groups since they are not quoted strings.
233200941Sjhb    quoted = False
234200941Sjhb    words = []
235200941Sjhb    for group in groups:
236200941Sjhb        if quoted:
237200941Sjhb            words.append(group)
238200941Sjhb            quoted = False
239200941Sjhb        else:
240200941Sjhb            for word in group.split():
241200941Sjhb                words.append(word)
242200941Sjhb            quoted = True
243200941Sjhb    return words
244200941Sjhb
245200941Sjhb# Parse a sys/conf/files* file adding defines for any options
246200941Sjhb# encountered.  Note files does not differentiate between options and
247200941Sjhb# devices.
248200941Sjhbdef parse_files_line(line, platform):
249200941Sjhb    words = tokenize(line)
250200941Sjhb
251200941Sjhb    # Skip include lines.
252200941Sjhb    if words[0] == 'include':
253200941Sjhb        return
254200941Sjhb
255200941Sjhb    # Skip standard lines as they have no devices or options.
256200941Sjhb    if words[1] == 'standard':
257200941Sjhb        return
258200941Sjhb
259200941Sjhb    # Remaining lines better be optional or mandatory lines.
260200941Sjhb    if words[1] != 'optional' and words[1] != 'mandatory':
261245536Seadler        print("Invalid files line: %s%s" % (line, location), file=sys.stderr)
262200941Sjhb
263200941Sjhb    # Drop the first two words and begin parsing keywords and devices.
264200941Sjhb    skip = False
265200941Sjhb    for word in words[2:]:
266200941Sjhb        if skip:
267200941Sjhb            skip = False
268200941Sjhb            continue
269200941Sjhb
270200941Sjhb        # Skip keywords
271200941Sjhb        if word == 'no-obj' or word == 'no-implicit-rule' or \
272200941Sjhb                word == 'before-depend' or word == 'local' or \
273200941Sjhb                word == 'no-depend' or word == 'profiling-routine' or \
274200941Sjhb                word == 'nowerror':
275200941Sjhb            continue
276200941Sjhb
277200941Sjhb        # Skip keywords and their following argument
278200941Sjhb        if word == 'dependency' or word == 'clean' or \
279200941Sjhb                word == 'compile-with' or word == 'warning':
280200941Sjhb            skip = True
281200941Sjhb            continue
282200941Sjhb
283200941Sjhb        # Ignore pipes
284200941Sjhb        if word == '|':
285200941Sjhb            continue
286200941Sjhb
287200941Sjhb        option = options.find(word)
288200941Sjhb        option.add_define(platform)
289200941Sjhb
290200941Sjhb# Parse a sys/conf/options* file adding defines for any options
291200941Sjhb# encountered.  Unlike a files file, options files only add options.
292200941Sjhbdef parse_options_line(line, platform):
293200941Sjhb    # The first word is the option name.
294200941Sjhb    name = line.split()[0]
295200941Sjhb
296200941Sjhb    # Ignore DEV_xxx options.  These are magic options that are
297200941Sjhb    # aliases for 'device xxx'.
298200941Sjhb    if name.startswith('DEV_'):
299200941Sjhb        return
300200941Sjhb
301200941Sjhb    option = options.find(name)
302200941Sjhb    option.add_define(platform)
303200941Sjhb    option.set_type('option')
304200941Sjhb
305200941Sjhb# Parse a sys/conf/NOTES file adding tests for any options or devices
306200941Sjhb# encountered.
307200941Sjhbdef parse_notes_line(line, platform):
308200941Sjhb    words = line.split()
309200941Sjhb
310200941Sjhb    # Skip lines with just whitespace
311200941Sjhb    if len(words) == 0:
312200941Sjhb        return
313200941Sjhb
314200941Sjhb    if words[0] == 'device' or words[0] == 'devices':
315200941Sjhb        option = options.find(words[1])
316200941Sjhb        option.add_test(platform)
317200941Sjhb        option.set_type('device')
318200941Sjhb        return
319200941Sjhb
320200941Sjhb    if words[0] == 'option' or words[0] == 'options':
321200941Sjhb        option = options.find(words[1].split('=')[0])
322200941Sjhb        option.add_test(platform)
323200941Sjhb        option.set_type('option')
324200941Sjhb        return
325200941Sjhb
326200941Sjhbdef main(argv=None):
327200941Sjhb    if argv is None:
328200941Sjhb        argv = sys.argv
329200941Sjhb    if len(sys.argv) != 2:
330200941Sjhb        usage()
331200941Sjhb        return 2
332200941Sjhb
333200941Sjhb    # Ensure the path has a trailing '/'.
334200941Sjhb    tree = sys.argv[1]
335200941Sjhb    if tree[-1] != '/':
336200941Sjhb        tree = tree + '/'
337200941Sjhb    for file in requiredfiles:
338200941Sjhb        if not os.path.exists(tree + file):
339245536Seadler            print("Kernel source tree missing %s" % (file), file=sys.stderr)
340200941Sjhb            return 1
341200941Sjhb
342200941Sjhb    platforms = find_platforms(tree)
343200941Sjhb
344200941Sjhb    # First, parse global files.
345200941Sjhb    parse_file(tree + 'conf/files', parse_files_line, global_platform)
346200941Sjhb    parse_file(tree + 'conf/options', parse_options_line, global_platform)
347200941Sjhb    parse_file(tree + 'conf/NOTES', parse_notes_line, global_platform)
348200941Sjhb
349200941Sjhb    # Next, parse MD files.
350200941Sjhb    for platform in platforms:
351200941Sjhb        files_file = tree + 'conf/files.' + platform
352200941Sjhb        if os.path.exists(files_file):
353200941Sjhb            parse_file(files_file, parse_files_line, platform)
354200941Sjhb        options_file = tree + 'conf/options.' + platform
355200941Sjhb        if os.path.exists(options_file):
356200941Sjhb            parse_file(options_file, parse_options_line, platform)
357200941Sjhb        parse_file(tree + platform + '/conf/NOTES', parse_notes_line, platform)
358200941Sjhb
359200941Sjhb    options.warn()
360200941Sjhb    return 0
361200941Sjhb
362200941Sjhbif __name__ == "__main__":
363200941Sjhb    sys.exit(main())
364