blob: 99e4e872fc483d3f4ffcad994b4eb2a97cf5b23c [file] [log] [blame]
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +09001#!/usr/bin/env python
2#
3# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
4#
5# SPDX-License-Identifier: GPL-2.0+
6#
7
8"""
9Converter from Kconfig and MAINTAINERS to boards.cfg
10
11Run 'tools/genboardscfg.py' to create boards.cfg file.
12
13Run 'tools/genboardscfg.py -h' for available options.
14"""
15
16import errno
17import fnmatch
18import glob
19import optparse
20import os
21import re
22import shutil
23import subprocess
24import sys
25import tempfile
26import time
27
28BOARD_FILE = 'boards.cfg'
29CONFIG_DIR = 'configs'
30REFORMAT_CMD = [os.path.join('tools', 'reformat.py'),
31 '-i', '-d', '-', '-s', '8']
32SHOW_GNU_MAKE = 'scripts/show-gnu-make'
33SLEEP_TIME=0.03
34
35COMMENT_BLOCK = '''#
36# List of boards
37# Automatically generated by %s: don't edit
38#
39# Status, Arch, CPU(:SPLCPU), SoC, Vendor, Board, Target, Options, Maintainers
40
41''' % __file__
42
43### helper functions ###
44def get_terminal_columns():
45 """Get the width of the terminal.
46
47 Returns:
48 The width of the terminal, or zero if the stdout is not
49 associated with tty.
50 """
51 try:
52 return shutil.get_terminal_size().columns # Python 3.3~
53 except AttributeError:
54 import fcntl
55 import termios
56 import struct
57 arg = struct.pack('hhhh', 0, 0, 0, 0)
58 try:
59 ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg)
60 except IOError as exception:
61 if exception.errno != errno.ENOTTY:
62 raise
63 # If 'Inappropriate ioctl for device' error occurs,
64 # stdout is probably redirected. Return 0.
65 return 0
66 return struct.unpack('hhhh', ret)[1]
67
68def get_devnull():
69 """Get the file object of '/dev/null' device."""
70 try:
71 devnull = subprocess.DEVNULL # py3k
72 except AttributeError:
73 devnull = open(os.devnull, 'wb')
74 return devnull
75
76def check_top_directory():
77 """Exit if we are not at the top of source directory."""
78 for f in ('README', 'Licenses'):
79 if not os.path.exists(f):
Masahiro Yamada31e21412014-08-16 00:59:26 +090080 sys.exit('Please run at the top of source directory.')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090081
82def get_make_cmd():
83 """Get the command name of GNU Make."""
84 process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE)
85 ret = process.communicate()
86 if process.returncode:
Masahiro Yamada31e21412014-08-16 00:59:26 +090087 sys.exit('GNU Make not found')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090088 return ret[0].rstrip()
89
90### classes ###
91class MaintainersDatabase:
92
93 """The database of board status and maintainers."""
94
95 def __init__(self):
96 """Create an empty database."""
97 self.database = {}
98
99 def get_status(self, target):
100 """Return the status of the given board.
101
102 Returns:
103 Either 'Active' or 'Orphan'
104 """
105 tmp = self.database[target][0]
106 if tmp.startswith('Maintained'):
107 return 'Active'
108 elif tmp.startswith('Orphan'):
109 return 'Orphan'
110 else:
111 print >> sys.stderr, 'Error: %s: unknown status' % tmp
112
113 def get_maintainers(self, target):
114 """Return the maintainers of the given board.
115
116 If the board has two or more maintainers, they are separated
117 with colons.
118 """
119 return ':'.join(self.database[target][1])
120
121 def parse_file(self, file):
122 """Parse the given MAINTAINERS file.
123
124 This method parses MAINTAINERS and add board status and
125 maintainers information to the database.
126
127 Arguments:
128 file: MAINTAINERS file to be parsed
129 """
130 targets = []
131 maintainers = []
132 status = '-'
133 for line in open(file):
134 tag, rest = line[:2], line[2:].strip()
135 if tag == 'M:':
136 maintainers.append(rest)
137 elif tag == 'F:':
138 # expand wildcard and filter by 'configs/*_defconfig'
139 for f in glob.glob(rest):
140 front, match, rear = f.partition('configs/')
141 if not front and match:
142 front, match, rear = rear.rpartition('_defconfig')
143 if match and not rear:
144 targets.append(front)
145 elif tag == 'S:':
146 status = rest
147 elif line == '\n' and targets:
148 for target in targets:
149 self.database[target] = (status, maintainers)
150 targets = []
151 maintainers = []
152 status = '-'
153 if targets:
154 for target in targets:
155 self.database[target] = (status, maintainers)
156
157class DotConfigParser:
158
159 """A parser of .config file.
160
161 Each line of the output should have the form of:
162 Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
163 Most of them are extracted from .config file.
164 MAINTAINERS files are also consulted for Status and Maintainers fields.
165 """
166
167 re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
168 re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
169 re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
170 re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
171 re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
172 re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
173 re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
174 re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
175 ('vendor', re_vendor), ('board', re_board),
176 ('config', re_config), ('options', re_options))
177 must_fields = ('arch', 'config')
178
179 def __init__(self, build_dir, output, maintainers_database):
180 """Create a new .config perser.
181
182 Arguments:
183 build_dir: Build directory where .config is located
184 output: File object which the result is written to
185 maintainers_database: An instance of class MaintainersDatabase
186 """
187 self.dotconfig = os.path.join(build_dir, '.config')
188 self.output = output
189 self.database = maintainers_database
190
191 def parse(self, defconfig):
192 """Parse .config file and output one-line database for the given board.
193
194 Arguments:
195 defconfig: Board (defconfig) name
196 """
197 fields = {}
198 for line in open(self.dotconfig):
199 if not line.startswith('CONFIG_SYS_'):
200 continue
201 for (key, pattern) in self.re_list:
202 m = pattern.match(line)
203 if m and m.group(1):
204 fields[key] = m.group(1)
205 break
206
207 # sanity check of '.config' file
208 for field in self.must_fields:
209 if not field in fields:
Masahiro Yamada31e21412014-08-16 00:59:26 +0900210 sys.exit('Error: %s is not defined in %s' % (field, defconfig))
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900211
212 # fix-up for aarch64 and tegra
213 if fields['arch'] == 'arm' and 'cpu' in fields:
214 if fields['cpu'] == 'armv8':
215 fields['arch'] = 'aarch64'
216 if 'soc' in fields and re.match('tegra[0-9]*$', fields['soc']):
217 fields['cpu'] += ':arm720t'
218
219 target, match, rear = defconfig.partition('_defconfig')
220 assert match and not rear, \
221 '%s : invalid defconfig file name' % defconfig
222
223 fields['status'] = self.database.get_status(target)
224 fields['maintainers'] = self.database.get_maintainers(target)
225
226 if 'options' in fields:
227 options = fields['config'] + ':' + \
228 fields['options'].replace(r'\"', '"')
229 elif fields['config'] != target:
230 options = fields['config']
231 else:
232 options = '-'
233
234 self.output.write((' '.join(['%s'] * 9) + '\n') %
235 (fields['status'],
236 fields['arch'],
237 fields.get('cpu', '-'),
238 fields.get('soc', '-'),
239 fields.get('vendor', '-'),
240 fields.get('board', '-'),
241 target,
242 options,
243 fields['maintainers']))
244
245class Slot:
246
247 """A slot to store a subprocess.
248
249 Each instance of this class handles one subprocess.
250 This class is useful to control multiple processes
251 for faster processing.
252 """
253
254 def __init__(self, output, maintainers_database, devnull, make_cmd):
255 """Create a new slot.
256
257 Arguments:
258 output: File object which the result is written to
259 maintainers_database: An instance of class MaintainersDatabase
260 """
261 self.occupied = False
262 self.build_dir = tempfile.mkdtemp()
263 self.devnull = devnull
264 self.make_cmd = make_cmd
265 self.parser = DotConfigParser(self.build_dir, output,
266 maintainers_database)
267
268 def __del__(self):
269 """Delete the working directory"""
270 shutil.rmtree(self.build_dir)
271
272 def add(self, defconfig):
273 """Add a new subprocess to the slot.
274
275 Fails if the slot is occupied, that is, the current subprocess
276 is still running.
277
278 Arguments:
279 defconfig: Board (defconfig) name
280
281 Returns:
282 Return True on success or False on fail
283 """
284 if self.occupied:
285 return False
286 o = 'O=' + self.build_dir
287 self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
288 stdout=self.devnull)
289 self.defconfig = defconfig
290 self.occupied = True
291 return True
292
293 def poll(self):
294 """Check if the subprocess is running and invoke the .config
295 parser if the subprocess is terminated.
296
297 Returns:
298 Return True if the subprocess is terminated, False otherwise
299 """
300 if not self.occupied:
301 return True
302 if self.ps.poll() == None:
303 return False
304 self.parser.parse(self.defconfig)
305 self.occupied = False
306 return True
307
308class Slots:
309
310 """Controller of the array of subprocess slots."""
311
312 def __init__(self, jobs, output, maintainers_database):
313 """Create a new slots controller.
314
315 Arguments:
316 jobs: A number of slots to instantiate
317 output: File object which the result is written to
318 maintainers_database: An instance of class MaintainersDatabase
319 """
320 self.slots = []
321 devnull = get_devnull()
322 make_cmd = get_make_cmd()
323 for i in range(jobs):
324 self.slots.append(Slot(output, maintainers_database,
325 devnull, make_cmd))
326
327 def add(self, defconfig):
328 """Add a new subprocess if a vacant slot is available.
329
330 Arguments:
331 defconfig: Board (defconfig) name
332
333 Returns:
334 Return True on success or False on fail
335 """
336 for slot in self.slots:
337 if slot.add(defconfig):
338 return True
339 return False
340
341 def available(self):
342 """Check if there is a vacant slot.
343
344 Returns:
345 Return True if a vacant slot is found, False if all slots are full
346 """
347 for slot in self.slots:
348 if slot.poll():
349 return True
350 return False
351
352 def empty(self):
353 """Check if all slots are vacant.
354
355 Returns:
356 Return True if all slots are vacant, False if at least one slot
357 is running
358 """
359 ret = True
360 for slot in self.slots:
361 if not slot.poll():
362 ret = False
363 return ret
364
365class Indicator:
366
367 """A class to control the progress indicator."""
368
369 MIN_WIDTH = 15
370 MAX_WIDTH = 70
371
372 def __init__(self, total):
373 """Create an instance.
374
375 Arguments:
376 total: A number of boards
377 """
378 self.total = total
379 self.cur = 0
380 width = get_terminal_columns()
381 width = min(width, self.MAX_WIDTH)
382 width -= self.MIN_WIDTH
383 if width > 0:
384 self.enabled = True
385 else:
386 self.enabled = False
387 self.width = width
388
389 def inc(self):
390 """Increment the counter and show the progress bar."""
391 if not self.enabled:
392 return
393 self.cur += 1
394 arrow_len = self.width * self.cur // self.total
395 msg = '%4d/%d [' % (self.cur, self.total)
396 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
397 sys.stdout.write('\r' + msg)
398 sys.stdout.flush()
399
400def __gen_boards_cfg(jobs):
401 """Generate boards.cfg file.
402
403 Arguments:
404 jobs: The number of jobs to run simultaneously
405
406 Note:
407 The incomplete boards.cfg is left over when an error (including
408 the termination by the keyboard interrupt) occurs on the halfway.
409 """
410 check_top_directory()
411 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
412
413 # All the defconfig files to be processed
414 defconfigs = []
415 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
416 dirpath = dirpath[len(CONFIG_DIR) + 1:]
417 for filename in fnmatch.filter(filenames, '*_defconfig'):
418 defconfigs.append(os.path.join(dirpath, filename))
419
420 # Parse all the MAINTAINERS files
421 maintainers_database = MaintainersDatabase()
422 for (dirpath, dirnames, filenames) in os.walk('.'):
423 if 'MAINTAINERS' in filenames:
424 maintainers_database.parse_file(os.path.join(dirpath,
425 'MAINTAINERS'))
426
427 # Output lines should be piped into the reformat tool
428 reformat_process = subprocess.Popen(REFORMAT_CMD, stdin=subprocess.PIPE,
429 stdout=open(BOARD_FILE, 'w'))
430 pipe = reformat_process.stdin
431 pipe.write(COMMENT_BLOCK)
432
433 indicator = Indicator(len(defconfigs))
434 slots = Slots(jobs, pipe, maintainers_database)
435
436 # Main loop to process defconfig files:
437 # Add a new subprocess into a vacant slot.
438 # Sleep if there is no available slot.
439 for defconfig in defconfigs:
440 while not slots.add(defconfig):
441 while not slots.available():
442 # No available slot: sleep for a while
443 time.sleep(SLEEP_TIME)
444 indicator.inc()
445
446 # wait until all the subprocesses finish
447 while not slots.empty():
448 time.sleep(SLEEP_TIME)
449 print ''
450
451 # wait until the reformat tool finishes
452 reformat_process.communicate()
453 if reformat_process.returncode != 0:
Masahiro Yamada31e21412014-08-16 00:59:26 +0900454 sys.exit('"%s" failed' % REFORMAT_CMD[0])
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900455
456def gen_boards_cfg(jobs):
457 """Generate boards.cfg file.
458
459 The incomplete boards.cfg is deleted if an error (including
460 the termination by the keyboard interrupt) occurs on the halfway.
461
462 Arguments:
463 jobs: The number of jobs to run simultaneously
464 """
465 try:
466 __gen_boards_cfg(jobs)
467 except:
468 # We should remove incomplete boards.cfg
469 try:
470 os.remove(BOARD_FILE)
471 except OSError as exception:
472 # Ignore 'No such file or directory' error
473 if exception.errno != errno.ENOENT:
474 raise
475 raise
476
477def main():
478 parser = optparse.OptionParser()
479 # Add options here
480 parser.add_option('-j', '--jobs',
481 help='the number of jobs to run simultaneously')
482 (options, args) = parser.parse_args()
483 if options.jobs:
484 try:
485 jobs = int(options.jobs)
486 except ValueError:
Masahiro Yamada31e21412014-08-16 00:59:26 +0900487 sys.exit('Option -j (--jobs) takes a number')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900488 else:
489 try:
490 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
491 stdout=subprocess.PIPE).communicate()[0])
492 except (OSError, ValueError):
493 print 'info: failed to get the number of CPUs. Set jobs to 1'
494 jobs = 1
495 gen_boards_cfg(jobs)
496
497if __name__ == '__main__':
498 main()