Merge https://gitlab.denx.de/u-boot/custodians/u-boot-marvell
[oweals/u-boot.git] / tools / genboardscfg.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: GPL-2.0+
3 #
4 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5 #
6
7 """
8 Converter from Kconfig and MAINTAINERS to a board database.
9
10 Run 'tools/genboardscfg.py' to create a board database.
11
12 Run 'tools/genboardscfg.py -h' for available options.
13 """
14
15 import errno
16 import fnmatch
17 import glob
18 import multiprocessing
19 import optparse
20 import os
21 import sys
22 import tempfile
23 import time
24
25 sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'buildman'))
26 import kconfiglib
27
28 ### constant variables ###
29 OUTPUT_FILE = 'boards.cfg'
30 CONFIG_DIR = 'configs'
31 SLEEP_TIME = 0.03
32 COMMENT_BLOCK = '''#
33 # List of boards
34 #   Automatically generated by %s: don't edit
35 #
36 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
37
38 ''' % __file__
39
40 ### helper functions ###
41 def try_remove(f):
42     """Remove a file ignoring 'No such file or directory' error."""
43     try:
44         os.remove(f)
45     except OSError as exception:
46         # Ignore 'No such file or directory' error
47         if exception.errno != errno.ENOENT:
48             raise
49
50 def check_top_directory():
51     """Exit if we are not at the top of source directory."""
52     for f in ('README', 'Licenses'):
53         if not os.path.exists(f):
54             sys.exit('Please run at the top of source directory.')
55
56 def output_is_new(output):
57     """Check if the output file is up to date.
58
59     Returns:
60       True if the given output file exists and is newer than any of
61       *_defconfig, MAINTAINERS and Kconfig*.  False otherwise.
62     """
63     try:
64         ctime = os.path.getctime(output)
65     except OSError as exception:
66         if exception.errno == errno.ENOENT:
67             # return False on 'No such file or directory' error
68             return False
69         else:
70             raise
71
72     for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
73         for filename in fnmatch.filter(filenames, '*_defconfig'):
74             if fnmatch.fnmatch(filename, '.*'):
75                 continue
76             filepath = os.path.join(dirpath, filename)
77             if ctime < os.path.getctime(filepath):
78                 return False
79
80     for (dirpath, dirnames, filenames) in os.walk('.'):
81         for filename in filenames:
82             if (fnmatch.fnmatch(filename, '*~') or
83                 not fnmatch.fnmatch(filename, 'Kconfig*') and
84                 not filename == 'MAINTAINERS'):
85                 continue
86             filepath = os.path.join(dirpath, filename)
87             if ctime < os.path.getctime(filepath):
88                 return False
89
90     # Detect a board that has been removed since the current board database
91     # was generated
92     with open(output, encoding="utf-8") as f:
93         for line in f:
94             if line[0] == '#' or line == '\n':
95                 continue
96             defconfig = line.split()[6] + '_defconfig'
97             if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
98                 return False
99
100     return True
101
102 ### classes ###
103 class KconfigScanner:
104
105     """Kconfig scanner."""
106
107     ### constant variable only used in this class ###
108     _SYMBOL_TABLE = {
109         'arch' : 'SYS_ARCH',
110         'cpu' : 'SYS_CPU',
111         'soc' : 'SYS_SOC',
112         'vendor' : 'SYS_VENDOR',
113         'board' : 'SYS_BOARD',
114         'config' : 'SYS_CONFIG_NAME',
115         'options' : 'SYS_EXTRA_OPTIONS'
116     }
117
118     def __init__(self):
119         """Scan all the Kconfig files and create a Kconfig object."""
120         # Define environment variables referenced from Kconfig
121         os.environ['srctree'] = os.getcwd()
122         os.environ['UBOOTVERSION'] = 'dummy'
123         os.environ['KCONFIG_OBJDIR'] = ''
124         self._conf = kconfiglib.Kconfig(warn=False)
125
126     def __del__(self):
127         """Delete a leftover temporary file before exit.
128
129         The scan() method of this class creates a temporay file and deletes
130         it on success.  If scan() method throws an exception on the way,
131         the temporary file might be left over.  In that case, it should be
132         deleted in this destructor.
133         """
134         if hasattr(self, '_tmpfile') and self._tmpfile:
135             try_remove(self._tmpfile)
136
137     def scan(self, defconfig):
138         """Load a defconfig file to obtain board parameters.
139
140         Arguments:
141           defconfig: path to the defconfig file to be processed
142
143         Returns:
144           A dictionary of board parameters.  It has a form of:
145           {
146               'arch': <arch_name>,
147               'cpu': <cpu_name>,
148               'soc': <soc_name>,
149               'vendor': <vendor_name>,
150               'board': <board_name>,
151               'target': <target_name>,
152               'config': <config_header_name>,
153               'options': <extra_options>
154           }
155         """
156         # strip special prefixes and save it in a temporary file
157         fd, self._tmpfile = tempfile.mkstemp()
158         with os.fdopen(fd, 'w') as f:
159             for line in open(defconfig):
160                 colon = line.find(':CONFIG_')
161                 if colon == -1:
162                     f.write(line)
163                 else:
164                     f.write(line[colon + 1:])
165
166         self._conf.load_config(self._tmpfile)
167         try_remove(self._tmpfile)
168         self._tmpfile = None
169
170         params = {}
171
172         # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
173         # Set '-' if the value is empty.
174         for key, symbol in list(self._SYMBOL_TABLE.items()):
175             value = self._conf.syms.get(symbol).str_value
176             if value:
177                 params[key] = value
178             else:
179                 params[key] = '-'
180
181         defconfig = os.path.basename(defconfig)
182         params['target'], match, rear = defconfig.partition('_defconfig')
183         assert match and not rear, '%s : invalid defconfig' % defconfig
184
185         # fix-up for aarch64
186         if params['arch'] == 'arm' and params['cpu'] == 'armv8':
187             params['arch'] = 'aarch64'
188
189         # fix-up options field. It should have the form:
190         # <config name>[:comma separated config options]
191         if params['options'] != '-':
192             params['options'] = params['config'] + ':' + \
193                                 params['options'].replace(r'\"', '"')
194         elif params['config'] != params['target']:
195             params['options'] = params['config']
196
197         return params
198
199 def scan_defconfigs_for_multiprocess(queue, defconfigs):
200     """Scan defconfig files and queue their board parameters
201
202     This function is intended to be passed to
203     multiprocessing.Process() constructor.
204
205     Arguments:
206       queue: An instance of multiprocessing.Queue().
207              The resulting board parameters are written into it.
208       defconfigs: A sequence of defconfig files to be scanned.
209     """
210     kconf_scanner = KconfigScanner()
211     for defconfig in defconfigs:
212         queue.put(kconf_scanner.scan(defconfig))
213
214 def read_queues(queues, params_list):
215     """Read the queues and append the data to the paramers list"""
216     for q in queues:
217         while not q.empty():
218             params_list.append(q.get())
219
220 def scan_defconfigs(jobs=1):
221     """Collect board parameters for all defconfig files.
222
223     This function invokes multiple processes for faster processing.
224
225     Arguments:
226       jobs: The number of jobs to run simultaneously
227     """
228     all_defconfigs = []
229     for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
230         for filename in fnmatch.filter(filenames, '*_defconfig'):
231             if fnmatch.fnmatch(filename, '.*'):
232                 continue
233             all_defconfigs.append(os.path.join(dirpath, filename))
234
235     total_boards = len(all_defconfigs)
236     processes = []
237     queues = []
238     for i in range(jobs):
239         defconfigs = all_defconfigs[total_boards * i // jobs :
240                                     total_boards * (i + 1) // jobs]
241         q = multiprocessing.Queue(maxsize=-1)
242         p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
243                                     args=(q, defconfigs))
244         p.start()
245         processes.append(p)
246         queues.append(q)
247
248     # The resulting data should be accumulated to this list
249     params_list = []
250
251     # Data in the queues should be retrieved preriodically.
252     # Otherwise, the queues would become full and subprocesses would get stuck.
253     while any([p.is_alive() for p in processes]):
254         read_queues(queues, params_list)
255         # sleep for a while until the queues are filled
256         time.sleep(SLEEP_TIME)
257
258     # Joining subprocesses just in case
259     # (All subprocesses should already have been finished)
260     for p in processes:
261         p.join()
262
263     # retrieve leftover data
264     read_queues(queues, params_list)
265
266     return params_list
267
268 class MaintainersDatabase:
269
270     """The database of board status and maintainers."""
271
272     def __init__(self):
273         """Create an empty database."""
274         self.database = {}
275
276     def get_status(self, target):
277         """Return the status of the given board.
278
279         The board status is generally either 'Active' or 'Orphan'.
280         Display a warning message and return '-' if status information
281         is not found.
282
283         Returns:
284           'Active', 'Orphan' or '-'.
285         """
286         if not target in self.database:
287             print("WARNING: no status info for '%s'" % target, file=sys.stderr)
288             return '-'
289
290         tmp = self.database[target][0]
291         if tmp.startswith('Maintained'):
292             return 'Active'
293         elif tmp.startswith('Supported'):
294             return 'Active'
295         elif tmp.startswith('Orphan'):
296             return 'Orphan'
297         else:
298             print(("WARNING: %s: unknown status for '%s'" %
299                                   (tmp, target)), file=sys.stderr)
300             return '-'
301
302     def get_maintainers(self, target):
303         """Return the maintainers of the given board.
304
305         Returns:
306           Maintainers of the board.  If the board has two or more maintainers,
307           they are separated with colons.
308         """
309         if not target in self.database:
310             print("WARNING: no maintainers for '%s'" % target, file=sys.stderr)
311             return ''
312
313         return ':'.join(self.database[target][1])
314
315     def parse_file(self, file):
316         """Parse a MAINTAINERS file.
317
318         Parse a MAINTAINERS file and accumulates board status and
319         maintainers information.
320
321         Arguments:
322           file: MAINTAINERS file to be parsed
323         """
324         targets = []
325         maintainers = []
326         status = '-'
327         for line in open(file, encoding="utf-8"):
328             # Check also commented maintainers
329             if line[:3] == '#M:':
330                 line = line[1:]
331             tag, rest = line[:2], line[2:].strip()
332             if tag == 'M:':
333                 maintainers.append(rest)
334             elif tag == 'F:':
335                 # expand wildcard and filter by 'configs/*_defconfig'
336                 for f in glob.glob(rest):
337                     front, match, rear = f.partition('configs/')
338                     if not front and match:
339                         front, match, rear = rear.rpartition('_defconfig')
340                         if match and not rear:
341                             targets.append(front)
342             elif tag == 'S:':
343                 status = rest
344             elif line == '\n':
345                 for target in targets:
346                     self.database[target] = (status, maintainers)
347                 targets = []
348                 maintainers = []
349                 status = '-'
350         if targets:
351             for target in targets:
352                 self.database[target] = (status, maintainers)
353
354 def insert_maintainers_info(params_list):
355     """Add Status and Maintainers information to the board parameters list.
356
357     Arguments:
358       params_list: A list of the board parameters
359     """
360     database = MaintainersDatabase()
361     for (dirpath, dirnames, filenames) in os.walk('.'):
362         if 'MAINTAINERS' in filenames:
363             database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
364
365     for i, params in enumerate(params_list):
366         target = params['target']
367         params['status'] = database.get_status(target)
368         params['maintainers'] = database.get_maintainers(target)
369         params_list[i] = params
370
371 def format_and_output(params_list, output):
372     """Write board parameters into a file.
373
374     Columnate the board parameters, sort lines alphabetically,
375     and then write them to a file.
376
377     Arguments:
378       params_list: The list of board parameters
379       output: The path to the output file
380     """
381     FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
382               'options', 'maintainers')
383
384     # First, decide the width of each column
385     max_length = dict([ (f, 0) for f in FIELDS])
386     for params in params_list:
387         for f in FIELDS:
388             max_length[f] = max(max_length[f], len(params[f]))
389
390     output_lines = []
391     for params in params_list:
392         line = ''
393         for f in FIELDS:
394             # insert two spaces between fields like column -t would
395             line += '  ' + params[f].ljust(max_length[f])
396         output_lines.append(line.strip())
397
398     # ignore case when sorting
399     output_lines.sort(key=str.lower)
400
401     with open(output, 'w', encoding="utf-8") as f:
402         f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
403
404 def gen_boards_cfg(output, jobs=1, force=False, quiet=False):
405     """Generate a board database file.
406
407     Arguments:
408       output: The name of the output file
409       jobs: The number of jobs to run simultaneously
410       force: Force to generate the output even if it is new
411       quiet: True to avoid printing a message if nothing needs doing
412     """
413     check_top_directory()
414
415     if not force and output_is_new(output):
416         if not quiet:
417             print("%s is up to date. Nothing to do." % output)
418         sys.exit(0)
419
420     params_list = scan_defconfigs(jobs)
421     insert_maintainers_info(params_list)
422     format_and_output(params_list, output)
423
424 def main():
425     try:
426         cpu_count = multiprocessing.cpu_count()
427     except NotImplementedError:
428         cpu_count = 1
429
430     parser = optparse.OptionParser()
431     # Add options here
432     parser.add_option('-f', '--force', action="store_true", default=False,
433                       help='regenerate the output even if it is new')
434     parser.add_option('-j', '--jobs', type='int', default=cpu_count,
435                       help='the number of jobs to run simultaneously')
436     parser.add_option('-o', '--output', default=OUTPUT_FILE,
437                       help='output file [default=%s]' % OUTPUT_FILE)
438     parser.add_option('-q', '--quiet', action="store_true", help='run silently')
439     (options, args) = parser.parse_args()
440
441     gen_boards_cfg(options.output, jobs=options.jobs, force=options.force,
442                    quiet=options.quiet)
443
444 if __name__ == '__main__':
445     main()