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, 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 sys.exit('Please run at the top of source directory.')
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()
91 class MaintainersDatabase:
93 """The database of board status and maintainers."""
96 """Create an empty database."""
99 def get_status(self, target):
100 """Return the status of the given board.
103 Either 'Active' or 'Orphan'
105 tmp = self.database[target][0]
106 if tmp.startswith('Maintained'):
108 elif tmp.startswith('Orphan'):
111 print >> sys.stderr, 'Error: %s: unknown status' % tmp
113 def get_maintainers(self, target):
114 """Return the maintainers of the given board.
116 If the board has two or more maintainers, they are separated
119 return ':'.join(self.database[target][1])
121 def parse_file(self, file):
122 """Parse the given MAINTAINERS file.
124 This method parses MAINTAINERS and add board status and
125 maintainers information to the database.
128 file: MAINTAINERS file to be parsed
133 for line in open(file):
134 tag, rest = line[:2], line[2:].strip()
136 maintainers.append(rest)
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)
147 elif line == '\n' and targets:
148 for target in targets:
149 self.database[target] = (status, maintainers)
154 for target in targets:
155 self.database[target] = (status, maintainers)
157 class DotConfigParser:
159 """A parser of .config file.
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.
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')
179 def __init__(self, build_dir, output, maintainers_database):
180 """Create a new .config perser.
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
187 self.dotconfig = os.path.join(build_dir, '.config')
189 self.database = maintainers_database
191 def parse(self, defconfig):
192 """Parse .config file and output one-line database for the given board.
195 defconfig: Board (defconfig) name
198 for line in open(self.dotconfig):
199 if not line.startswith('CONFIG_SYS_'):
201 for (key, pattern) in self.re_list:
202 m = pattern.match(line)
204 fields[key] = m.group(1)
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))
213 if fields['arch'] == 'arm' and 'cpu' in fields:
214 if fields['cpu'] == 'armv8':
215 fields['arch'] = 'aarch64'
217 target, match, rear = defconfig.partition('_defconfig')
218 assert match and not rear, \
219 '%s : invalid defconfig file name' % defconfig
221 fields['status'] = self.database.get_status(target)
222 fields['maintainers'] = self.database.get_maintainers(target)
224 if 'options' in fields:
225 options = fields['config'] + ':' + \
226 fields['options'].replace(r'\"', '"')
227 elif fields['config'] != target:
228 options = fields['config']
232 self.output.write((' '.join(['%s'] * 9) + '\n') %
235 fields.get('cpu', '-'),
236 fields.get('soc', '-'),
237 fields.get('vendor', '-'),
238 fields.get('board', '-'),
241 fields['maintainers']))
245 """A slot to store a subprocess.
247 Each instance of this class handles one subprocess.
248 This class is useful to control multiple processes
249 for faster processing.
252 def __init__(self, output, maintainers_database, devnull, make_cmd):
253 """Create a new slot.
256 output: File object which the result is written to
257 maintainers_database: An instance of class MaintainersDatabase
259 self.occupied = False
260 self.build_dir = tempfile.mkdtemp()
261 self.devnull = devnull
262 self.make_cmd = make_cmd
263 self.parser = DotConfigParser(self.build_dir, output,
264 maintainers_database)
267 """Delete the working directory"""
268 shutil.rmtree(self.build_dir)
270 def add(self, defconfig):
271 """Add a new subprocess to the slot.
273 Fails if the slot is occupied, that is, the current subprocess
277 defconfig: Board (defconfig) name
280 Return True on success or False on fail
284 o = 'O=' + self.build_dir
285 self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
287 self.defconfig = defconfig
292 """Check if the subprocess is running and invoke the .config
293 parser if the subprocess is terminated.
296 Return True if the subprocess is terminated, False otherwise
298 if not self.occupied:
300 if self.ps.poll() == None:
302 self.parser.parse(self.defconfig)
303 self.occupied = False
308 """Controller of the array of subprocess slots."""
310 def __init__(self, jobs, output, maintainers_database):
311 """Create a new slots controller.
314 jobs: A number of slots to instantiate
315 output: File object which the result is written to
316 maintainers_database: An instance of class MaintainersDatabase
319 devnull = get_devnull()
320 make_cmd = get_make_cmd()
321 for i in range(jobs):
322 self.slots.append(Slot(output, maintainers_database,
325 def add(self, defconfig):
326 """Add a new subprocess if a vacant slot is available.
329 defconfig: Board (defconfig) name
332 Return True on success or False on fail
334 for slot in self.slots:
335 if slot.add(defconfig):
340 """Check if there is a vacant slot.
343 Return True if a vacant slot is found, False if all slots are full
345 for slot in self.slots:
351 """Check if all slots are vacant.
354 Return True if all slots are vacant, False if at least one slot
358 for slot in self.slots:
365 """A class to control the progress indicator."""
370 def __init__(self, total):
371 """Create an instance.
374 total: A number of boards
378 width = get_terminal_columns()
379 width = min(width, self.MAX_WIDTH)
380 width -= self.MIN_WIDTH
388 """Increment the counter and show the progress bar."""
392 arrow_len = self.width * self.cur // self.total
393 msg = '%4d/%d [' % (self.cur, self.total)
394 msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
395 sys.stdout.write('\r' + msg)
398 def __gen_boards_cfg(jobs):
399 """Generate boards.cfg file.
402 jobs: The number of jobs to run simultaneously
405 The incomplete boards.cfg is left over when an error (including
406 the termination by the keyboard interrupt) occurs on the halfway.
408 check_top_directory()
409 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
411 # All the defconfig files to be processed
413 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
414 dirpath = dirpath[len(CONFIG_DIR) + 1:]
415 for filename in fnmatch.filter(filenames, '*_defconfig'):
416 defconfigs.append(os.path.join(dirpath, filename))
418 # Parse all the MAINTAINERS files
419 maintainers_database = MaintainersDatabase()
420 for (dirpath, dirnames, filenames) in os.walk('.'):
421 if 'MAINTAINERS' in filenames:
422 maintainers_database.parse_file(os.path.join(dirpath,
425 # Output lines should be piped into the reformat tool
426 reformat_process = subprocess.Popen(REFORMAT_CMD, stdin=subprocess.PIPE,
427 stdout=open(BOARD_FILE, 'w'))
428 pipe = reformat_process.stdin
429 pipe.write(COMMENT_BLOCK)
431 indicator = Indicator(len(defconfigs))
432 slots = Slots(jobs, pipe, maintainers_database)
434 # Main loop to process defconfig files:
435 # Add a new subprocess into a vacant slot.
436 # Sleep if there is no available slot.
437 for defconfig in defconfigs:
438 while not slots.add(defconfig):
439 while not slots.available():
440 # No available slot: sleep for a while
441 time.sleep(SLEEP_TIME)
444 # wait until all the subprocesses finish
445 while not slots.empty():
446 time.sleep(SLEEP_TIME)
449 # wait until the reformat tool finishes
450 reformat_process.communicate()
451 if reformat_process.returncode != 0:
452 sys.exit('"%s" failed' % REFORMAT_CMD[0])
454 def gen_boards_cfg(jobs):
455 """Generate boards.cfg file.
457 The incomplete boards.cfg is deleted if an error (including
458 the termination by the keyboard interrupt) occurs on the halfway.
461 jobs: The number of jobs to run simultaneously
464 __gen_boards_cfg(jobs)
466 # We should remove incomplete boards.cfg
468 os.remove(BOARD_FILE)
469 except OSError as exception:
470 # Ignore 'No such file or directory' error
471 if exception.errno != errno.ENOENT:
476 parser = optparse.OptionParser()
478 parser.add_option('-j', '--jobs',
479 help='the number of jobs to run simultaneously')
480 (options, args) = parser.parse_args()
483 jobs = int(options.jobs)
485 sys.exit('Option -j (--jobs) takes a number')
488 jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
489 stdout=subprocess.PIPE).communicate()[0])
490 except (OSError, ValueError):
491 print 'info: failed to get the number of CPUs. Set jobs to 1'
495 if __name__ == '__main__':