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 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))
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'
219 target, match, rear = defconfig.partition('_defconfig')
220 assert match and not rear, \
221 '%s : invalid defconfig file name' % defconfig
223 fields['status'] = self.database.get_status(target)
224 fields['maintainers'] = self.database.get_maintainers(target)
226 if 'options' in fields:
227 options = fields['config'] + ':' + \
228 fields['options'].replace(r'\"', '"')
229 elif fields['config'] != target:
230 options = fields['config']
234 self.output.write((' '.join(['%s'] * 9) + '\n') %
237 fields.get('cpu', '-'),
238 fields.get('soc', '-'),
239 fields.get('vendor', '-'),
240 fields.get('board', '-'),
243 fields['maintainers']))
247 """A slot to store a subprocess.
249 Each instance of this class handles one subprocess.
250 This class is useful to control multiple processes
251 for faster processing.
254 def __init__(self, output, maintainers_database, devnull, make_cmd):
255 """Create a new slot.
258 output: File object which the result is written to
259 maintainers_database: An instance of class MaintainersDatabase
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)
269 """Delete the working directory"""
270 shutil.rmtree(self.build_dir)
272 def add(self, defconfig):
273 """Add a new subprocess to the slot.
275 Fails if the slot is occupied, that is, the current subprocess
279 defconfig: Board (defconfig) name
282 Return True on success or False on fail
286 o = 'O=' + self.build_dir
287 self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
289 self.defconfig = defconfig
294 """Check if the subprocess is running and invoke the .config
295 parser if the subprocess is terminated.
298 Return True if the subprocess is terminated, False otherwise
300 if not self.occupied:
302 if self.ps.poll() == None:
304 self.parser.parse(self.defconfig)
305 self.occupied = False
310 """Controller of the array of subprocess slots."""
312 def __init__(self, jobs, output, maintainers_database):
313 """Create a new slots controller.
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
321 devnull = get_devnull()
322 make_cmd = get_make_cmd()
323 for i in range(jobs):
324 self.slots.append(Slot(output, maintainers_database,
327 def add(self, defconfig):
328 """Add a new subprocess if a vacant slot is available.
331 defconfig: Board (defconfig) name
334 Return True on success or False on fail
336 for slot in self.slots:
337 if slot.add(defconfig):
342 """Check if there is a vacant slot.
345 Return True if a vacant slot is found, False if all slots are full
347 for slot in self.slots:
353 """Check if all slots are vacant.
356 Return True if all slots are vacant, False if at least one slot
360 for slot in self.slots:
367 """A class to control the progress indicator."""
372 def __init__(self, total):
373 """Create an instance.
376 total: A number of boards
380 width = get_terminal_columns()
381 width = min(width, self.MAX_WIDTH)
382 width -= self.MIN_WIDTH
390 """Increment the counter and show the progress bar."""
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)
400 def __gen_boards_cfg(jobs):
401 """Generate boards.cfg file.
404 jobs: The number of jobs to run simultaneously
407 The incomplete boards.cfg is left over when an error (including
408 the termination by the keyboard interrupt) occurs on the halfway.
410 check_top_directory()
411 print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
413 # All the defconfig files to be processed
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))
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,
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)
433 indicator = Indicator(len(defconfigs))
434 slots = Slots(jobs, pipe, maintainers_database)
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)
446 # wait until all the subprocesses finish
447 while not slots.empty():
448 time.sleep(SLEEP_TIME)
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])
456 def gen_boards_cfg(jobs):
457 """Generate boards.cfg file.
459 The incomplete boards.cfg is deleted if an error (including
460 the termination by the keyboard interrupt) occurs on the halfway.
463 jobs: The number of jobs to run simultaneously
466 __gen_boards_cfg(jobs)
468 # We should remove incomplete boards.cfg
470 os.remove(BOARD_FILE)
471 except OSError as exception:
472 # Ignore 'No such file or directory' error
473 if exception.errno != errno.ENOENT:
478 parser = optparse.OptionParser()
480 parser.add_option('-j', '--jobs',
481 help='the number of jobs to run simultaneously')
482 (options, args) = parser.parse_args()
485 jobs = int(options.jobs)
487 sys.exit('Option -j (--jobs) takes a number')
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'
497 if __name__ == '__main__':