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