blob: e6870f5bba9ce4012603b7d4fa3726836c9715c5 [file] [log] [blame]
Masahiro Yamada78d3fde2014-08-27 14:05:51 +09001#!/usr/bin/env python2
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +09002#
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.
Masahiro Yamada78d3fde2014-08-27 14:05:51 +090014
15This script only works on python 2.6 or later, but not python 3.x.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090016"""
17
18import errno
19import fnmatch
20import glob
21import optparse
22import os
23import re
24import shutil
25import subprocess
26import sys
27import tempfile
28import time
29
30BOARD_FILE = 'boards.cfg'
31CONFIG_DIR = 'configs'
32REFORMAT_CMD = [os.path.join('tools', 'reformat.py'),
33 '-i', '-d', '-', '-s', '8']
34SHOW_GNU_MAKE = 'scripts/show-gnu-make'
Masahiro Yamada11748a62014-08-25 12:39:48 +090035SLEEP_TIME=0.003
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090036
37COMMENT_BLOCK = '''#
38# List of boards
39# Automatically generated by %s: don't edit
40#
Masahiro Yamada8d141bd2014-08-06 13:42:34 +090041# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090042
43''' % __file__
44
45### helper functions ###
46def get_terminal_columns():
47 """Get the width of the terminal.
48
49 Returns:
50 The width of the terminal, or zero if the stdout is not
51 associated with tty.
52 """
53 try:
54 return shutil.get_terminal_size().columns # Python 3.3~
55 except AttributeError:
56 import fcntl
57 import termios
58 import struct
59 arg = struct.pack('hhhh', 0, 0, 0, 0)
60 try:
61 ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg)
62 except IOError as exception:
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090063 # 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 Yamada880828d2014-08-16 00:59:26 +090080 sys.exit('Please run at the top of source directory.')
Masahiro Yamadac1ee48e2014-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 Yamada880828d2014-08-16 00:59:26 +090087 sys.exit('GNU Make not found')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +090088 return ret[0].rstrip()
89
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +090090def output_is_new():
91 """Check if the boards.cfg file is up to date.
92
93 Returns:
94 True if the boards.cfg file exists and is newer than any of
95 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
96 """
97 try:
98 ctime = os.path.getctime(BOARD_FILE)
99 except OSError as exception:
100 if exception.errno == errno.ENOENT:
101 # return False on 'No such file or directory' error
102 return False
103 else:
104 raise
105
106 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
107 for filename in fnmatch.filter(filenames, '*_defconfig'):
108 if fnmatch.fnmatch(filename, '.*'):
109 continue
110 filepath = os.path.join(dirpath, filename)
111 if ctime < os.path.getctime(filepath):
112 return False
113
114 for (dirpath, dirnames, filenames) in os.walk('.'):
115 for filename in filenames:
116 if (fnmatch.fnmatch(filename, '*~') or
117 not fnmatch.fnmatch(filename, 'Kconfig*') and
118 not filename == 'MAINTAINERS'):
119 continue
120 filepath = os.path.join(dirpath, filename)
121 if ctime < os.path.getctime(filepath):
122 return False
123
124 # Detect a board that has been removed since the current boards.cfg
125 # was generated
126 with open(BOARD_FILE) as f:
127 for line in f:
128 if line[0] == '#' or line == '\n':
129 continue
130 defconfig = line.split()[6] + '_defconfig'
131 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
132 return False
133
134 return True
135
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900136### classes ###
137class MaintainersDatabase:
138
139 """The database of board status and maintainers."""
140
141 def __init__(self):
142 """Create an empty database."""
143 self.database = {}
144
145 def get_status(self, target):
146 """Return the status of the given board.
147
148 Returns:
149 Either 'Active' or 'Orphan'
150 """
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900151 if not target in self.database:
152 print >> sys.stderr, "WARNING: no status info for '%s'" % target
153 return '-'
154
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900155 tmp = self.database[target][0]
156 if tmp.startswith('Maintained'):
157 return 'Active'
158 elif tmp.startswith('Orphan'):
159 return 'Orphan'
160 else:
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900161 print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
162 (tmp, target))
163 return '-'
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900164
165 def get_maintainers(self, target):
166 """Return the maintainers of the given board.
167
168 If the board has two or more maintainers, they are separated
169 with colons.
170 """
Masahiro Yamadab3c529b2014-08-25 12:39:43 +0900171 if not target in self.database:
172 print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
173 return ''
174
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900175 return ':'.join(self.database[target][1])
176
177 def parse_file(self, file):
178 """Parse the given MAINTAINERS file.
179
180 This method parses MAINTAINERS and add board status and
181 maintainers information to the database.
182
183 Arguments:
184 file: MAINTAINERS file to be parsed
185 """
186 targets = []
187 maintainers = []
188 status = '-'
189 for line in open(file):
190 tag, rest = line[:2], line[2:].strip()
191 if tag == 'M:':
192 maintainers.append(rest)
193 elif tag == 'F:':
194 # expand wildcard and filter by 'configs/*_defconfig'
195 for f in glob.glob(rest):
196 front, match, rear = f.partition('configs/')
197 if not front and match:
198 front, match, rear = rear.rpartition('_defconfig')
199 if match and not rear:
200 targets.append(front)
201 elif tag == 'S:':
202 status = rest
Masahiro Yamadaba133b22014-08-22 14:10:43 +0900203 elif line == '\n':
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900204 for target in targets:
205 self.database[target] = (status, maintainers)
206 targets = []
207 maintainers = []
208 status = '-'
209 if targets:
210 for target in targets:
211 self.database[target] = (status, maintainers)
212
213class DotConfigParser:
214
215 """A parser of .config file.
216
217 Each line of the output should have the form of:
218 Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
219 Most of them are extracted from .config file.
220 MAINTAINERS files are also consulted for Status and Maintainers fields.
221 """
222
223 re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
224 re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
225 re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
226 re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
227 re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
228 re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
229 re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
230 re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
231 ('vendor', re_vendor), ('board', re_board),
232 ('config', re_config), ('options', re_options))
233 must_fields = ('arch', 'config')
234
235 def __init__(self, build_dir, output, maintainers_database):
236 """Create a new .config perser.
237
238 Arguments:
239 build_dir: Build directory where .config is located
240 output: File object which the result is written to
241 maintainers_database: An instance of class MaintainersDatabase
242 """
243 self.dotconfig = os.path.join(build_dir, '.config')
244 self.output = output
245 self.database = maintainers_database
246
247 def parse(self, defconfig):
248 """Parse .config file and output one-line database for the given board.
249
250 Arguments:
251 defconfig: Board (defconfig) name
252 """
253 fields = {}
254 for line in open(self.dotconfig):
255 if not line.startswith('CONFIG_SYS_'):
256 continue
257 for (key, pattern) in self.re_list:
258 m = pattern.match(line)
259 if m and m.group(1):
260 fields[key] = m.group(1)
261 break
262
263 # sanity check of '.config' file
264 for field in self.must_fields:
265 if not field in fields:
Masahiro Yamada482cb3d2014-08-25 12:39:44 +0900266 print >> sys.stderr, (
267 "WARNING: '%s' is not defined in '%s'. Skip." %
268 (field, defconfig))
269 return
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900270
Masahiro Yamada8d141bd2014-08-06 13:42:34 +0900271 # fix-up for aarch64
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900272 if fields['arch'] == 'arm' and 'cpu' in fields:
273 if fields['cpu'] == 'armv8':
274 fields['arch'] = 'aarch64'
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900275
276 target, match, rear = defconfig.partition('_defconfig')
277 assert match and not rear, \
278 '%s : invalid defconfig file name' % defconfig
279
280 fields['status'] = self.database.get_status(target)
281 fields['maintainers'] = self.database.get_maintainers(target)
282
283 if 'options' in fields:
284 options = fields['config'] + ':' + \
285 fields['options'].replace(r'\"', '"')
286 elif fields['config'] != target:
287 options = fields['config']
288 else:
289 options = '-'
290
291 self.output.write((' '.join(['%s'] * 9) + '\n') %
292 (fields['status'],
293 fields['arch'],
294 fields.get('cpu', '-'),
295 fields.get('soc', '-'),
296 fields.get('vendor', '-'),
297 fields.get('board', '-'),
298 target,
299 options,
300 fields['maintainers']))
301
302class Slot:
303
304 """A slot to store a subprocess.
305
306 Each instance of this class handles one subprocess.
307 This class is useful to control multiple processes
308 for faster processing.
309 """
310
311 def __init__(self, output, maintainers_database, devnull, make_cmd):
312 """Create a new slot.
313
314 Arguments:
315 output: File object which the result is written to
316 maintainers_database: An instance of class MaintainersDatabase
Masahiro Yamada11748a62014-08-25 12:39:48 +0900317 devnull: file object of 'dev/null'
318 make_cmd: the command name of Make
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900319 """
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900320 self.build_dir = tempfile.mkdtemp()
321 self.devnull = devnull
Masahiro Yamada11748a62014-08-25 12:39:48 +0900322 self.ps = subprocess.Popen([make_cmd, 'O=' + self.build_dir,
323 'allnoconfig'], stdout=devnull)
324 self.occupied = True
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900325 self.parser = DotConfigParser(self.build_dir, output,
326 maintainers_database)
Masahiro Yamada11748a62014-08-25 12:39:48 +0900327 self.env = os.environ.copy()
328 self.env['srctree'] = os.getcwd()
329 self.env['UBOOTVERSION'] = 'dummy'
330 self.env['KCONFIG_OBJDIR'] = ''
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900331
332 def __del__(self):
333 """Delete the working directory"""
Masahiro Yamada30e25992014-08-25 12:39:45 +0900334 if not self.occupied:
335 while self.ps.poll() == None:
336 pass
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900337 shutil.rmtree(self.build_dir)
338
339 def add(self, defconfig):
340 """Add a new subprocess to the slot.
341
342 Fails if the slot is occupied, that is, the current subprocess
343 is still running.
344
345 Arguments:
346 defconfig: Board (defconfig) name
347
348 Returns:
349 Return True on success or False on fail
350 """
351 if self.occupied:
352 return False
Masahiro Yamada11748a62014-08-25 12:39:48 +0900353
354 with open(os.path.join(self.build_dir, '.tmp_defconfig'), 'w') as f:
355 for line in open(os.path.join(CONFIG_DIR, defconfig)):
356 colon = line.find(':CONFIG_')
357 if colon == -1:
358 f.write(line)
359 else:
360 f.write(line[colon + 1:])
361
362 self.ps = subprocess.Popen([os.path.join('scripts', 'kconfig', 'conf'),
363 '--defconfig=.tmp_defconfig', 'Kconfig'],
364 stdout=self.devnull,
365 cwd=self.build_dir,
366 env=self.env)
367
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900368 self.defconfig = defconfig
369 self.occupied = True
370 return True
371
Masahiro Yamada11748a62014-08-25 12:39:48 +0900372 def wait(self):
373 """Wait until the current subprocess finishes."""
374 while self.occupied and self.ps.poll() == None:
375 time.sleep(SLEEP_TIME)
376 self.occupied = False
377
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900378 def poll(self):
379 """Check if the subprocess is running and invoke the .config
380 parser if the subprocess is terminated.
381
382 Returns:
383 Return True if the subprocess is terminated, False otherwise
384 """
385 if not self.occupied:
386 return True
387 if self.ps.poll() == None:
388 return False
Masahiro Yamada482cb3d2014-08-25 12:39:44 +0900389 if self.ps.poll() == 0:
390 self.parser.parse(self.defconfig)
391 else:
392 print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
393 self.defconfig)
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900394 self.occupied = False
395 return True
396
397class Slots:
398
399 """Controller of the array of subprocess slots."""
400
401 def __init__(self, jobs, output, maintainers_database):
402 """Create a new slots controller.
403
404 Arguments:
405 jobs: A number of slots to instantiate
406 output: File object which the result is written to
407 maintainers_database: An instance of class MaintainersDatabase
408 """
409 self.slots = []
410 devnull = get_devnull()
411 make_cmd = get_make_cmd()
412 for i in range(jobs):
413 self.slots.append(Slot(output, maintainers_database,
414 devnull, make_cmd))
Masahiro Yamada11748a62014-08-25 12:39:48 +0900415 for slot in self.slots:
416 slot.wait()
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900417
418 def add(self, defconfig):
419 """Add a new subprocess if a vacant slot is available.
420
421 Arguments:
422 defconfig: Board (defconfig) name
423
424 Returns:
425 Return True on success or False on fail
426 """
427 for slot in self.slots:
428 if slot.add(defconfig):
429 return True
430 return False
431
432 def available(self):
433 """Check if there is a vacant slot.
434
435 Returns:
436 Return True if a vacant slot is found, False if all slots are full
437 """
438 for slot in self.slots:
439 if slot.poll():
440 return True
441 return False
442
443 def empty(self):
444 """Check if all slots are vacant.
445
446 Returns:
447 Return True if all slots are vacant, False if at least one slot
448 is running
449 """
450 ret = True
451 for slot in self.slots:
452 if not slot.poll():
453 ret = False
454 return ret
455
456class Indicator:
457
458 """A class to control the progress indicator."""
459
460 MIN_WIDTH = 15
461 MAX_WIDTH = 70
462
463 def __init__(self, total):
464 """Create an instance.
465
466 Arguments:
467 total: A number of boards
468 """
469 self.total = total
470 self.cur = 0
471 width = get_terminal_columns()
472 width = min(width, self.MAX_WIDTH)
473 width -= self.MIN_WIDTH
474 if width > 0:
475 self.enabled = True
476 else:
477 self.enabled = False
478 self.width = width
479
480 def inc(self):
481 """Increment the counter and show the progress bar."""
482 if not self.enabled:
483 return
484 self.cur += 1
485 arrow_len = self.width * self.cur // self.total
486 msg = '%4d/%d [' % (self.cur, self.total)
487 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
488 sys.stdout.write('\r' + msg)
489 sys.stdout.flush()
490
Masahiro Yamada2b093192014-08-25 12:39:46 +0900491class BoardsFileGenerator:
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900492
Masahiro Yamada2b093192014-08-25 12:39:46 +0900493 """Generator of boards.cfg."""
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900494
Masahiro Yamada2b093192014-08-25 12:39:46 +0900495 def __init__(self):
496 """Prepare basic things for generating boards.cfg."""
497 # All the defconfig files to be processed
498 defconfigs = []
499 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
500 dirpath = dirpath[len(CONFIG_DIR) + 1:]
501 for filename in fnmatch.filter(filenames, '*_defconfig'):
502 if fnmatch.fnmatch(filename, '.*'):
503 continue
504 defconfigs.append(os.path.join(dirpath, filename))
505 self.defconfigs = defconfigs
506 self.indicator = Indicator(len(defconfigs))
507
508 # Parse all the MAINTAINERS files
509 maintainers_database = MaintainersDatabase()
510 for (dirpath, dirnames, filenames) in os.walk('.'):
511 if 'MAINTAINERS' in filenames:
512 maintainers_database.parse_file(os.path.join(dirpath,
513 'MAINTAINERS'))
514 self.maintainers_database = maintainers_database
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900515
Masahiro Yamada2b093192014-08-25 12:39:46 +0900516 def __del__(self):
517 """Delete the incomplete boards.cfg
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900518
Masahiro Yamada2b093192014-08-25 12:39:46 +0900519 This destructor deletes boards.cfg if the private member 'in_progress'
520 is defined as True. The 'in_progress' member is set to True at the
521 beginning of the generate() method and set to False at its end.
522 So, in_progress==True means generating boards.cfg was terminated
523 on the way.
524 """
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900525
Masahiro Yamada2b093192014-08-25 12:39:46 +0900526 if hasattr(self, 'in_progress') and self.in_progress:
527 try:
528 os.remove(BOARD_FILE)
529 except OSError as exception:
530 # Ignore 'No such file or directory' error
531 if exception.errno != errno.ENOENT:
532 raise
533 print 'Removed incomplete %s' % BOARD_FILE
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900534
Masahiro Yamada2b093192014-08-25 12:39:46 +0900535 def generate(self, jobs):
536 """Generate boards.cfg
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900537
Masahiro Yamada2b093192014-08-25 12:39:46 +0900538 This method sets the 'in_progress' member to True at the beginning
539 and sets it to False on success. The boards.cfg should not be
540 touched before/after this method because 'in_progress' is used
541 to detect the incomplete boards.cfg.
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900542
Masahiro Yamada2b093192014-08-25 12:39:46 +0900543 Arguments:
544 jobs: The number of jobs to run simultaneously
545 """
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900546
Masahiro Yamada2b093192014-08-25 12:39:46 +0900547 self.in_progress = True
548 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
549
550 # Output lines should be piped into the reformat tool
551 reformat_process = subprocess.Popen(REFORMAT_CMD,
552 stdin=subprocess.PIPE,
553 stdout=open(BOARD_FILE, 'w'))
554 pipe = reformat_process.stdin
555 pipe.write(COMMENT_BLOCK)
556
557 slots = Slots(jobs, pipe, self.maintainers_database)
558
559 # Main loop to process defconfig files:
560 # Add a new subprocess into a vacant slot.
561 # Sleep if there is no available slot.
562 for defconfig in self.defconfigs:
563 while not slots.add(defconfig):
564 while not slots.available():
565 # No available slot: sleep for a while
566 time.sleep(SLEEP_TIME)
567 self.indicator.inc()
568
569 # wait until all the subprocesses finish
570 while not slots.empty():
571 time.sleep(SLEEP_TIME)
572 print ''
573
574 # wait until the reformat tool finishes
575 reformat_process.communicate()
576 if reformat_process.returncode != 0:
577 sys.exit('"%s" failed' % REFORMAT_CMD[0])
578
579 self.in_progress = False
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900580
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900581def gen_boards_cfg(jobs=1, force=False):
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900582 """Generate boards.cfg file.
583
584 The incomplete boards.cfg is deleted if an error (including
585 the termination by the keyboard interrupt) occurs on the halfway.
586
587 Arguments:
588 jobs: The number of jobs to run simultaneously
589 """
Masahiro Yamada2b093192014-08-25 12:39:46 +0900590 check_top_directory()
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900591 if not force and output_is_new():
592 print "%s is up to date. Nothing to do." % BOARD_FILE
593 sys.exit(0)
594
Masahiro Yamada2b093192014-08-25 12:39:46 +0900595 generator = BoardsFileGenerator()
596 generator.generate(jobs)
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900597
598def main():
599 parser = optparse.OptionParser()
600 # Add options here
601 parser.add_option('-j', '--jobs',
602 help='the number of jobs to run simultaneously')
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900603 parser.add_option('-f', '--force', action="store_true", default=False,
604 help='regenerate the output even if it is new')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900605 (options, args) = parser.parse_args()
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900606
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900607 if options.jobs:
608 try:
609 jobs = int(options.jobs)
610 except ValueError:
Masahiro Yamada880828d2014-08-16 00:59:26 +0900611 sys.exit('Option -j (--jobs) takes a number')
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900612 else:
613 try:
614 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
615 stdout=subprocess.PIPE).communicate()[0])
616 except (OSError, ValueError):
617 print 'info: failed to get the number of CPUs. Set jobs to 1'
618 jobs = 1
Masahiro Yamadac2d99dd2014-08-25 12:39:47 +0900619
620 gen_boards_cfg(jobs, force=options.force)
Masahiro Yamadac1ee48e2014-07-30 14:08:19 +0900621
622if __name__ == '__main__':
623 main()