]> git.ipfire.org Git - thirdparty/u-boot.git/blob - tools/genboardscfg.py
tools, scripts: refactor error-out statements of Python scripts
[thirdparty/u-boot.git] / tools / genboardscfg.py
1 #!/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 """
9 Converter from Kconfig and MAINTAINERS to boards.cfg
10
11 Run 'tools/genboardscfg.py' to create boards.cfg file.
12
13 Run 'tools/genboardscfg.py -h' for available options.
14 """
15
16 import errno
17 import fnmatch
18 import glob
19 import optparse
20 import os
21 import re
22 import shutil
23 import subprocess
24 import sys
25 import tempfile
26 import time
27
28 BOARD_FILE = 'boards.cfg'
29 CONFIG_DIR = 'configs'
30 REFORMAT_CMD = [os.path.join('tools', 'reformat.py'),
31 '-i', '-d', '-', '-s', '8']
32 SHOW_GNU_MAKE = 'scripts/show-gnu-make'
33 SLEEP_TIME=0.03
34
35 COMMENT_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 ###
44 def 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
68 def 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
76 def 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):
80 sys.exit('Please run at the top of source directory.')
81
82 def 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:
87 sys.exit('GNU Make not found')
88 return ret[0].rstrip()
89
90 ### classes ###
91 class 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
157 class 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:
210 sys.exit('Error: %s is not defined in %s' % (field, defconfig))
211
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
245 class 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
308 class 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
365 class 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
400 def __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:
454 sys.exit('"%s" failed' % REFORMAT_CMD[0])
455
456 def 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
477 def 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:
487 sys.exit('Option -j (--jobs) takes a number')
488 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
497 if __name__ == '__main__':
498 main()