blob: 13dbc6326ee43d9f50d7ce1925152cce5a2a3cc5 [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'
Masahiro Yamada9a65cb72014-08-25 12:39:48 +090033SLEEP_TIME=0.003
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +090034
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
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +090088def output_is_new():
89 """Check if the boards.cfg file is up to date.
90
91 Returns:
92 True if the boards.cfg file exists and is newer than any of
93 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
94 """
95 try:
96 ctime = os.path.getctime(BOARD_FILE)
97 except OSError as exception:
98 if exception.errno == errno.ENOENT:
99 # return False on 'No such file or directory' error
100 return False
101 else:
102 raise
103
104 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
105 for filename in fnmatch.filter(filenames, '*_defconfig'):
106 if fnmatch.fnmatch(filename, '.*'):
107 continue
108 filepath = os.path.join(dirpath, filename)
109 if ctime < os.path.getctime(filepath):
110 return False
111
112 for (dirpath, dirnames, filenames) in os.walk('.'):
113 for filename in filenames:
114 if (fnmatch.fnmatch(filename, '*~') or
115 not fnmatch.fnmatch(filename, 'Kconfig*') and
116 not filename == 'MAINTAINERS'):
117 continue
118 filepath = os.path.join(dirpath, filename)
119 if ctime < os.path.getctime(filepath):
120 return False
121
122 # Detect a board that has been removed since the current boards.cfg
123 # was generated
124 with open(BOARD_FILE) as f:
125 for line in f:
126 if line[0] == '#' or line == '\n':
127 continue
128 defconfig = line.split()[6] + '_defconfig'
129 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
130 return False
131
132 return True
133
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900134### classes ###
135class MaintainersDatabase:
136
137 """The database of board status and maintainers."""
138
139 def __init__(self):
140 """Create an empty database."""
141 self.database = {}
142
143 def get_status(self, target):
144 """Return the status of the given board.
145
146 Returns:
147 Either 'Active' or 'Orphan'
148 """
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900149 if not target in self.database:
150 print >> sys.stderr, "WARNING: no status info for '%s'" % target
151 return '-'
152
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900153 tmp = self.database[target][0]
154 if tmp.startswith('Maintained'):
155 return 'Active'
156 elif tmp.startswith('Orphan'):
157 return 'Orphan'
158 else:
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900159 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
160 (tmp, target))
161 return '-'
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900162
163 def get_maintainers(self, target):
164 """Return the maintainers of the given board.
165
166 If the board has two or more maintainers, they are separated
167 with colons.
168 """
Masahiro Yamadab8828e82014-08-25 12:39:43 +0900169 if not target in self.database:
170 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
171 return ''
172
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900173 return ':'.join(self.database[target][1])
174
175 def parse_file(self, file):
176 """Parse the given MAINTAINERS file.
177
178 This method parses MAINTAINERS and add board status and
179 maintainers information to the database.
180
181 Arguments:
182 file: MAINTAINERS file to be parsed
183 """
184 targets = []
185 maintainers = []
186 status = '-'
187 for line in open(file):
188 tag, rest = line[:2], line[2:].strip()
189 if tag == 'M:':
190 maintainers.append(rest)
191 elif tag == 'F:':
192 # expand wildcard and filter by 'configs/*_defconfig'
193 for f in glob.glob(rest):
194 front, match, rear = f.partition('configs/')
195 if not front and match:
196 front, match, rear = rear.rpartition('_defconfig')
197 if match and not rear:
198 targets.append(front)
199 elif tag == 'S:':
200 status = rest
Masahiro Yamada9c2d60c2014-08-22 14:10:43 +0900201 elif line == '\n':
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900202 for target in targets:
203 self.database[target] = (status, maintainers)
204 targets = []
205 maintainers = []
206 status = '-'
207 if targets:
208 for target in targets:
209 self.database[target] = (status, maintainers)
210
211class DotConfigParser:
212
213 """A parser of .config file.
214
215 Each line of the output should have the form of:
216 Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
217 Most of them are extracted from .config file.
218 MAINTAINERS files are also consulted for Status and Maintainers fields.
219 """
220
221 re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
222 re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
223 re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
224 re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
225 re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
226 re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
227 re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
228 re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
229 ('vendor', re_vendor), ('board', re_board),
230 ('config', re_config), ('options', re_options))
231 must_fields = ('arch', 'config')
232
233 def __init__(self, build_dir, output, maintainers_database):
234 """Create a new .config perser.
235
236 Arguments:
237 build_dir: Build directory where .config is located
238 output: File object which the result is written to
239 maintainers_database: An instance of class MaintainersDatabase
240 """
241 self.dotconfig = os.path.join(build_dir, '.config')
242 self.output = output
243 self.database = maintainers_database
244
245 def parse(self, defconfig):
246 """Parse .config file and output one-line database for the given board.
247
248 Arguments:
249 defconfig: Board (defconfig) name
250 """
251 fields = {}
252 for line in open(self.dotconfig):
253 if not line.startswith('CONFIG_SYS_'):
254 continue
255 for (key, pattern) in self.re_list:
256 m = pattern.match(line)
257 if m and m.group(1):
258 fields[key] = m.group(1)
259 break
260
261 # sanity check of '.config' file
262 for field in self.must_fields:
263 if not field in fields:
Masahiro Yamada13246f42014-08-25 12:39:44 +0900264 print >> sys.stderr, (
265 "WARNING: '%s' is not defined in '%s'. Skip." %
266 (field, defconfig))
267 return
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900268
Masahiro Yamadaca418dd2014-08-06 13:42:34 +0900269 # fix-up for aarch64
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900270 if fields['arch'] == 'arm' and 'cpu' in fields:
271 if fields['cpu'] == 'armv8':
272 fields['arch'] = 'aarch64'
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900273
274 target, match, rear = defconfig.partition('_defconfig')
275 assert match and not rear, \
276 '%s : invalid defconfig file name' % defconfig
277
278 fields['status'] = self.database.get_status(target)
279 fields['maintainers'] = self.database.get_maintainers(target)
280
281 if 'options' in fields:
282 options = fields['config'] + ':' + \
283 fields['options'].replace(r'\"', '"')
284 elif fields['config'] != target:
285 options = fields['config']
286 else:
287 options = '-'
288
289 self.output.write((' '.join(['%s'] * 9) + '\n') %
290 (fields['status'],
291 fields['arch'],
292 fields.get('cpu', '-'),
293 fields.get('soc', '-'),
294 fields.get('vendor', '-'),
295 fields.get('board', '-'),
296 target,
297 options,
298 fields['maintainers']))
299
300class Slot:
301
302 """A slot to store a subprocess.
303
304 Each instance of this class handles one subprocess.
305 This class is useful to control multiple processes
306 for faster processing.
307 """
308
309 def __init__(self, output, maintainers_database, devnull, make_cmd):
310 """Create a new slot.
311
312 Arguments:
313 output: File object which the result is written to
314 maintainers_database: An instance of class MaintainersDatabase
Masahiro Yamada9a65cb72014-08-25 12:39:48 +0900315 devnull: file object of 'dev/null'
316 make_cmd: the command name of Make
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900317 """
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900318 self.build_dir = tempfile.mkdtemp()
319 self.devnull = devnull
Masahiro Yamada9a65cb72014-08-25 12:39:48 +0900320 self.ps = subprocess.Popen([make_cmd, 'O=' + self.build_dir,
321 'allnoconfig'], stdout=devnull)
322 self.occupied = True
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900323 self.parser = DotConfigParser(self.build_dir, output,
324 maintainers_database)
Masahiro Yamada9a65cb72014-08-25 12:39:48 +0900325 self.env = os.environ.copy()
326 self.env['srctree'] = os.getcwd()
327 self.env['UBOOTVERSION'] = 'dummy'
328 self.env['KCONFIG_OBJDIR'] = ''
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900329
330 def __del__(self):
331 """Delete the working directory"""
Masahiro Yamadad6538d22014-08-25 12:39:45 +0900332 if not self.occupied:
333 while self.ps.poll() == None:
334 pass
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900335 shutil.rmtree(self.build_dir)
336
337 def add(self, defconfig):
338 """Add a new subprocess to the slot.
339
340 Fails if the slot is occupied, that is, the current subprocess
341 is still running.
342
343 Arguments:
344 defconfig: Board (defconfig) name
345
346 Returns:
347 Return True on success or False on fail
348 """
349 if self.occupied:
350 return False
Masahiro Yamada9a65cb72014-08-25 12:39:48 +0900351
352 with open(os.path.join(self.build_dir, '.tmp_defconfig'), 'w') as f:
353 for line in open(os.path.join(CONFIG_DIR, defconfig)):
354 colon = line.find(':CONFIG_')
355 if colon == -1:
356 f.write(line)
357 else:
358 f.write(line[colon + 1:])
359
360 self.ps = subprocess.Popen([os.path.join('scripts', 'kconfig', 'conf'),
361 '--defconfig=.tmp_defconfig', 'Kconfig'],
362 stdout=self.devnull,
363 cwd=self.build_dir,
364 env=self.env)
365
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900366 self.defconfig = defconfig
367 self.occupied = True
368 return True
369
Masahiro Yamada9a65cb72014-08-25 12:39:48 +0900370 def wait(self):
371 """Wait until the current subprocess finishes."""
372 while self.occupied and self.ps.poll() == None:
373 time.sleep(SLEEP_TIME)
374 self.occupied = False
375
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900376 def poll(self):
377 """Check if the subprocess is running and invoke the .config
378 parser if the subprocess is terminated.
379
380 Returns:
381 Return True if the subprocess is terminated, False otherwise
382 """
383 if not self.occupied:
384 return True
385 if self.ps.poll() == None:
386 return False
Masahiro Yamada13246f42014-08-25 12:39:44 +0900387 if self.ps.poll() == 0:
388 self.parser.parse(self.defconfig)
389 else:
390 print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
391 self.defconfig)
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900392 self.occupied = False
393 return True
394
395class Slots:
396
397 """Controller of the array of subprocess slots."""
398
399 def __init__(self, jobs, output, maintainers_database):
400 """Create a new slots controller.
401
402 Arguments:
403 jobs: A number of slots to instantiate
404 output: File object which the result is written to
405 maintainers_database: An instance of class MaintainersDatabase
406 """
407 self.slots = []
408 devnull = get_devnull()
409 make_cmd = get_make_cmd()
410 for i in range(jobs):
411 self.slots.append(Slot(output, maintainers_database,
412 devnull, make_cmd))
Masahiro Yamada9a65cb72014-08-25 12:39:48 +0900413 for slot in self.slots:
414 slot.wait()
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900415
416 def add(self, defconfig):
417 """Add a new subprocess if a vacant slot is available.
418
419 Arguments:
420 defconfig: Board (defconfig) name
421
422 Returns:
423 Return True on success or False on fail
424 """
425 for slot in self.slots:
426 if slot.add(defconfig):
427 return True
428 return False
429
430 def available(self):
431 """Check if there is a vacant slot.
432
433 Returns:
434 Return True if a vacant slot is found, False if all slots are full
435 """
436 for slot in self.slots:
437 if slot.poll():
438 return True
439 return False
440
441 def empty(self):
442 """Check if all slots are vacant.
443
444 Returns:
445 Return True if all slots are vacant, False if at least one slot
446 is running
447 """
448 ret = True
449 for slot in self.slots:
450 if not slot.poll():
451 ret = False
452 return ret
453
454class Indicator:
455
456 """A class to control the progress indicator."""
457
458 MIN_WIDTH = 15
459 MAX_WIDTH = 70
460
461 def __init__(self, total):
462 """Create an instance.
463
464 Arguments:
465 total: A number of boards
466 """
467 self.total = total
468 self.cur = 0
469 width = get_terminal_columns()
470 width = min(width, self.MAX_WIDTH)
471 width -= self.MIN_WIDTH
472 if width > 0:
473 self.enabled = True
474 else:
475 self.enabled = False
476 self.width = width
477
478 def inc(self):
479 """Increment the counter and show the progress bar."""
480 if not self.enabled:
481 return
482 self.cur += 1
483 arrow_len = self.width * self.cur // self.total
484 msg = '%4d/%d [' % (self.cur, self.total)
485 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
486 sys.stdout.write('\r' + msg)
487 sys.stdout.flush()
488
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900489class BoardsFileGenerator:
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900490
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900491 """Generator of boards.cfg."""
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900492
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900493 def __init__(self):
494 """Prepare basic things for generating boards.cfg."""
495 # All the defconfig files to be processed
496 defconfigs = []
497 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
498 dirpath = dirpath[len(CONFIG_DIR) + 1:]
499 for filename in fnmatch.filter(filenames, '*_defconfig'):
500 if fnmatch.fnmatch(filename, '.*'):
501 continue
502 defconfigs.append(os.path.join(dirpath, filename))
503 self.defconfigs = defconfigs
504 self.indicator = Indicator(len(defconfigs))
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900505
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900506 # Parse all the MAINTAINERS files
507 maintainers_database = MaintainersDatabase()
508 for (dirpath, dirnames, filenames) in os.walk('.'):
509 if 'MAINTAINERS' in filenames:
510 maintainers_database.parse_file(os.path.join(dirpath,
511 'MAINTAINERS'))
512 self.maintainers_database = maintainers_database
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900513
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900514 def __del__(self):
515 """Delete the incomplete boards.cfg
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900516
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900517 This destructor deletes boards.cfg if the private member 'in_progress'
518 is defined as True. The 'in_progress' member is set to True at the
519 beginning of the generate() method and set to False at its end.
520 So, in_progress==True means generating boards.cfg was terminated
521 on the way.
522 """
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900523
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900524 if hasattr(self, 'in_progress') and self.in_progress:
525 try:
526 os.remove(BOARD_FILE)
527 except OSError as exception:
528 # Ignore 'No such file or directory' error
529 if exception.errno != errno.ENOENT:
530 raise
531 print 'Removed incomplete %s' % BOARD_FILE
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900532
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900533 def generate(self, jobs):
534 """Generate boards.cfg
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900535
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900536 This method sets the 'in_progress' member to True at the beginning
537 and sets it to False on success. The boards.cfg should not be
538 touched before/after this method because 'in_progress' is used
539 to detect the incomplete boards.cfg.
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900540
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900541 Arguments:
542 jobs: The number of jobs to run simultaneously
543 """
544
545 self.in_progress = True
546 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
547
548 # Output lines should be piped into the reformat tool
549 reformat_process = subprocess.Popen(REFORMAT_CMD,
550 stdin=subprocess.PIPE,
551 stdout=open(BOARD_FILE, 'w'))
552 pipe = reformat_process.stdin
553 pipe.write(COMMENT_BLOCK)
554
555 slots = Slots(jobs, pipe, self.maintainers_database)
556
557 # Main loop to process defconfig files:
558 # Add a new subprocess into a vacant slot.
559 # Sleep if there is no available slot.
560 for defconfig in self.defconfigs:
561 while not slots.add(defconfig):
562 while not slots.available():
563 # No available slot: sleep for a while
564 time.sleep(SLEEP_TIME)
565 self.indicator.inc()
566
567 # wait until all the subprocesses finish
568 while not slots.empty():
569 time.sleep(SLEEP_TIME)
570 print ''
571
572 # wait until the reformat tool finishes
573 reformat_process.communicate()
574 if reformat_process.returncode != 0:
575 sys.exit('"%s" failed' % REFORMAT_CMD[0])
576
577 self.in_progress = False
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900578
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900579def gen_boards_cfg(jobs=1, force=False):
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900580 """Generate boards.cfg file.
581
582 The incomplete boards.cfg is deleted if an error (including
583 the termination by the keyboard interrupt) occurs on the halfway.
584
585 Arguments:
586 jobs: The number of jobs to run simultaneously
587 """
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900588 check_top_directory()
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900589 if not force and output_is_new():
590 print "%s is up to date. Nothing to do." % BOARD_FILE
591 sys.exit(0)
592
Masahiro Yamada79d45d32014-08-25 12:39:46 +0900593 generator = BoardsFileGenerator()
594 generator.generate(jobs)
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900595
596def main():
597 parser = optparse.OptionParser()
598 # Add options here
599 parser.add_option('-j', '--jobs',
600 help='the number of jobs to run simultaneously')
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900601 parser.add_option('-f', '--force', action="store_true", default=False,
602 help='regenerate the output even if it is new')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900603 (options, args) = parser.parse_args()
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900604
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900605 if options.jobs:
606 try:
607 jobs = int(options.jobs)
608 except ValueError:
Masahiro Yamada31e21412014-08-16 00:59:26 +0900609 sys.exit('Option -j (--jobs) takes a number')
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900610 else:
611 try:
612 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
613 stdout=subprocess.PIPE).communicate()[0])
614 except (OSError, ValueError):
615 print 'info: failed to get the number of CPUs. Set jobs to 1'
616 jobs = 1
Masahiro Yamadad1bf4af2014-08-25 12:39:47 +0900617
618 gen_boards_cfg(jobs, force=options.force)
Masahiro Yamada3c08e8b2014-07-30 14:08:19 +0900619
620if __name__ == '__main__':
621 main()