3 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
5 # SPDX-License-Identifier: GPL-2.0+
9 Converter from Kconfig and MAINTAINERS to boards.cfg
11 Run 'tools/genboardscfg.py' to create boards.cfg file.
13 Run 'tools/genboardscfg.py -h' for available options.
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'
37 # Automatically generated by %s: don't edit
39 # Status, Arch, CPU(:SPLCPU), SoC, Vendor, Board, Target, Options, Maintainers
43 ### helper functions ###
44 def get_terminal_columns():
45 """Get the width of the terminal.
48 The width of the terminal, or zero if the stdout is not
52 return shutil.get_terminal_size().columns # Python 3.3~
53 except AttributeError:
57 arg = struct.pack('hhhh', 0, 0, 0, 0)
59 ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg)
60 except IOError as exception:
61 if exception.errno != errno.ENOTTY:
63 # If 'Inappropriate ioctl for device' error occurs,
64 # stdout is probably redirected. Return 0.
66 return struct.unpack('hhhh', ret)[1]
69 """Get the file object of '/dev/null' device."""
71 devnull = subprocess.DEVNULL # py3k
72 except AttributeError:
73 devnull = open(os.devnull, 'wb')
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 print >> sys.stderr, 'Please run at the top of source directory.'
84 """Get the command name of GNU Make."""
85 process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE)
86 ret = process.communicate()
87 if process.returncode:
88 print >> sys.stderr, 'GNU Make not found'
90 return ret[0].rstrip()
93 class MaintainersDatabase:
95 """The database of board status and maintainers."""
98 """Create an empty database."""
101 def get_status(self, target):
102 """Return the status of the given board.
105 Either 'Active' or 'Orphan'
107 tmp = self.database[target][0]
108 if tmp.startswith('Maintained'):
110 elif tmp.startswith('Orphan'):
113 print >> sys.stderr, 'Error: %s: unknown status' % tmp
115 def get_maintainers(self, target):
116 """Return the maintainers of the given board.
118 If the board has two or more maintainers, they are separated
121 return ':'.join(self.database[target][1])
123 def parse_file(self, file):
124 """Parse the given MAINTAINERS file.
126 This method parses MAINTAINERS and add board status and
127 maintainers information to the database.
130 file: MAINTAINERS file to be parsed
135 for line in open(file):
136 tag, rest = line[:2], line[2:].strip()
138 maintainers.append(rest)
140 # expand wildcard and filter by 'configs/*_defconfig'
141 for f in glob.glob(rest):
142 front, match, rear = f.partition('configs/')
143 if not front and match:
144 front, match, rear = rear.rpartition('_defconfig')
145 if match and not rear:
146 targets.append(front)
149 elif line == '\n' and targets:
150 for target in targets:
151 self.database[target] = (status, maintainers)
156 for target in targets:
157 self.database[target] = (status, maintainers)
159 class DotConfigParser:
161 """A parser of .config file.
163 Each line of the output should have the form of:
164 Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
165 Most of them are extracted from .config file.
166 MAINTAINERS files are also consulted for Status and Maintainers fields.
169 re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
170 re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
171 re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
172 re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
173 re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
174 re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
175 re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
176 re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
177 ('vendor', re_vendor), ('board', re_board),
178 ('config', re_config), ('options', re_options))
179 must_fields = ('arch', 'config')
181 def __init__(self, build_dir, output, maintainers_database):
182 """Create a new .config perser.
185 build_dir: Build directory where .config is located
186 output: File object which the result is written to
187 maintainers_database: An instance of class MaintainersDatabase
189 self.dotconfig = os.path.join(build_dir, '.config')
191 self.database = maintainers_database
193 def parse(self, defconfig):
194 """Parse .config file and output one-line database for the given board.
197 defconfig: Board (defconfig) name
200 for line in open(self.dotconfig):
201 if not line.startswith('CONFIG_SYS_'):
203 for (key, pattern) in self.re_list:
204 m = pattern.match(line)
206 fields[key] = m.group(1)
209 # sanity check of '.config' file
210 for field in self.must_fields:
211 if not field in fields:
212 print >> sys.stderr, 'Error: %s is not defined in %s' % \
216 # fix-up for aarch64 and tegra
217 if fields['arch'] == 'arm' and 'cpu' in fields:
218 if fields['cpu'] == 'armv8':
219 fields['arch'] = 'aarch64'
220 if 'soc' in fields and re.match('tegra[0-9]*$', fields['soc']):
221 fields['cpu'] += ':arm720t'
223 target, match, rear = defconfig.partition('_defconfig')
224 assert match and not rear, \
225 '%s : invalid defconfig file name' % defconfig
227 fields['status'] = self.database.get_status(target)
228 fields['maintainers'] = self.database.get_maintainers(target)
230 if 'options' in fields:
231 options = fields['config'] + ':' + \
232 fields['options'].replace(r'\"', '"')
233 elif fields['config'] != target:
234 options = fields['config']
238 self.output.write((' '.join(['%s'] * 9) + '\n') %
241 fields.get('cpu', '-'),
242 fields.get('soc', '-'),
243 fields.get('vendor', '-'),
244 fields.get('board', '-'),
247 fields['maintainers']))
251 """A slot to store a subprocess.
253 Each instance of this class handles one subprocess.
254 This class is useful to control multiple processes
255 for faster processing.
258 def __init__(self, output, maintainers_database, devnull, make_cmd):
259 """Create a new slot.
262 output: File object which the result is written to
263 maintainers_database: An instance of class MaintainersDatabase
265 self.occupied = False
266 self.build_dir = tempfile.mkdtemp()
267 self.devnull = devnull
268 self.make_cmd = make_cmd
269 self.parser = DotConfigParser(self.build_dir, output,
270 maintainers_database)
273 """Delete the working directory"""
274 shutil.rmtree(self.build_dir)
276 def add(self, defconfig):
277 """Add a new subprocess to the slot.
279 Fails if the slot is occupied, that is, the current subprocess
283 defconfig: Board (defconfig) name
286 Return True on success or False on fail
290 o = 'O=' + self.build_dir
291 self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
293 self.defconfig = defconfig
298 """Check if the subprocess is running and invoke the .config
299 parser if the subprocess is terminated.
302 Return True if the subprocess is terminated, False otherwise
304 if not self.occupied:
306 if self.ps.poll() == None:
308 self.parser.parse(self.defconfig)
309 self.occupied = False
314 """Controller of the array of subprocess slots."""
316 def __init__(self, jobs, output, maintainers_database):
317 """Create a new slots controller.
320 jobs: A number of slots to instantiate
321 output: File object which the result is written to
322 maintainers_database: An instance of class MaintainersDatabase
325 devnull = get_devnull()
326 make_cmd = get_make_cmd()
327 for i in range(jobs):
328 self.slots.append(Slot(output, maintainers_database,
331 def add(self, defconfig):
332 """Add a new subprocess if a vacant slot is available.
335 defconfig: Board (defconfig) name
338 Return True on success or False on fail
340 for slot in self.slots:
341 if slot.add(defconfig):
346 """Check if there is a vacant slot.
349 Return True if a vacant slot is found, False if all slots are full
351 for slot in self.slots:
357 """Check if all slots are vacant.
360 Return True if all slots are vacant, False if at least one slot
364 for slot in self.slots:
371 """A class to control the progress indicator."""
376 def __init__(self, total):
377 """Create an instance.
380 total: A number of boards
384 width = get_terminal_columns()
385 width = min(width, self.MAX_WIDTH)
386 width -= self.MIN_WIDTH
394 """Increment the counter and show the progress bar."""
398 arrow_len = self.width * self.cur // self.total
399 msg = '%4d/%d [' % (self.cur, self.total)
400 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
401 sys.stdout.write('\r' + msg)
404 def __gen_boards_cfg(jobs):
405 """Generate boards.cfg file.
408 jobs: The number of jobs to run simultaneously
411 The incomplete boards.cfg is left over when an error (including
412 the termination by the keyboard interrupt) occurs on the halfway.
414 check_top_directory()
415 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
417 # All the defconfig files to be processed
419 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
420 dirpath = dirpath[len(CONFIG_DIR) + 1:]
421 for filename in fnmatch.filter(filenames, '*_defconfig'):
422 defconfigs.append(os.path.join(dirpath, filename))
424 # Parse all the MAINTAINERS files
425 maintainers_database = MaintainersDatabase()
426 for (dirpath, dirnames, filenames) in os.walk('.'):
427 if 'MAINTAINERS' in filenames:
428 maintainers_database.parse_file(os.path.join(dirpath,
431 # Output lines should be piped into the reformat tool
432 reformat_process = subprocess.Popen(REFORMAT_CMD, stdin=subprocess.PIPE,
433 stdout=open(BOARD_FILE, 'w'))
434 pipe = reformat_process.stdin
435 pipe.write(COMMENT_BLOCK)
437 indicator = Indicator(len(defconfigs))
438 slots = Slots(jobs, pipe, maintainers_database)
440 # Main loop to process defconfig files:
441 # Add a new subprocess into a vacant slot.
442 # Sleep if there is no available slot.
443 for defconfig in defconfigs:
444 while not slots.add(defconfig):
445 while not slots.available():
446 # No available slot: sleep for a while
447 time.sleep(SLEEP_TIME)
450 # wait until all the subprocesses finish
451 while not slots.empty():
452 time.sleep(SLEEP_TIME)
455 # wait until the reformat tool finishes
456 reformat_process.communicate()
457 if reformat_process.returncode != 0:
458 print >> sys.stderr, '"%s" failed' % REFORMAT_CMD[0]
461 def gen_boards_cfg(jobs):
462 """Generate boards.cfg file.
464 The incomplete boards.cfg is deleted if an error (including
465 the termination by the keyboard interrupt) occurs on the halfway.
468 jobs: The number of jobs to run simultaneously
471 __gen_boards_cfg(jobs)
473 # We should remove incomplete boards.cfg
475 os.remove(BOARD_FILE)
476 except OSError as exception:
477 # Ignore 'No such file or directory' error
478 if exception.errno != errno.ENOENT:
483 parser = optparse.OptionParser()
485 parser.add_option('-j', '--jobs',
486 help='the number of jobs to run simultaneously')
487 (options, args) = parser.parse_args()
490 jobs = int(options.jobs)
492 print >> sys.stderr, 'Option -j (--jobs) takes a number'
496 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
497 stdout=subprocess.PIPE).communicate()[0])
498 except (OSError, ValueError):
499 print 'info: failed to get the number of CPUs. Set jobs to 1'
503 if __name__ == '__main__':