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