2 # SPDX-License-Identifier: GPL-2.0+
4 # Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
8 Converter from Kconfig and MAINTAINERS to a board database.
10 Run 'tools/genboardscfg.py' to create a board database.
12 Run 'tools/genboardscfg.py -h' for available options.
14 Python 2.6 or later, but not Python 3.x is necessary to run this script.
20 import multiprocessing
27 sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'buildman'))
30 ### constant variables ###
31 OUTPUT_FILE = 'boards.cfg'
32 CONFIG_DIR = 'configs'
36 # Automatically generated by %s: don't edit
38 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
42 ### helper functions ###
44 """Remove a file ignoring 'No such file or directory' error."""
47 except OSError as exception:
48 # Ignore 'No such file or directory' error
49 if exception.errno != errno.ENOENT:
52 def check_top_directory():
53 """Exit if we are not at the top of source directory."""
54 for f in ('README', 'Licenses'):
55 if not os.path.exists(f):
56 sys.exit('Please run at the top of source directory.')
58 def output_is_new(output):
59 """Check if the output file is up to date.
62 True if the given output file exists and is newer than any of
63 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
66 ctime = os.path.getctime(output)
67 except OSError as exception:
68 if exception.errno == errno.ENOENT:
69 # return False on 'No such file or directory' error
74 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
75 for filename in fnmatch.filter(filenames, '*_defconfig'):
76 if fnmatch.fnmatch(filename, '.*'):
78 filepath = os.path.join(dirpath, filename)
79 if ctime < os.path.getctime(filepath):
82 for (dirpath, dirnames, filenames) in os.walk('.'):
83 for filename in filenames:
84 if (fnmatch.fnmatch(filename, '*~') or
85 not fnmatch.fnmatch(filename, 'Kconfig*') and
86 not filename == 'MAINTAINERS'):
88 filepath = os.path.join(dirpath, filename)
89 if ctime < os.path.getctime(filepath):
92 # Detect a board that has been removed since the current board database
94 with open(output, encoding="utf-8") as f:
96 if line[0] == '#' or line == '\n':
98 defconfig = line.split()[6] + '_defconfig'
99 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
105 class KconfigScanner:
107 """Kconfig scanner."""
109 ### constant variable only used in this class ###
114 'vendor' : 'SYS_VENDOR',
115 'board' : 'SYS_BOARD',
116 'config' : 'SYS_CONFIG_NAME',
117 'options' : 'SYS_EXTRA_OPTIONS'
121 """Scan all the Kconfig files and create a Kconfig object."""
122 # Define environment variables referenced from Kconfig
123 os.environ['srctree'] = os.getcwd()
124 os.environ['UBOOTVERSION'] = 'dummy'
125 os.environ['KCONFIG_OBJDIR'] = ''
126 self._conf = kconfiglib.Kconfig(warn=False)
129 """Delete a leftover temporary file before exit.
131 The scan() method of this class creates a temporay file and deletes
132 it on success. If scan() method throws an exception on the way,
133 the temporary file might be left over. In that case, it should be
134 deleted in this destructor.
136 if hasattr(self, '_tmpfile') and self._tmpfile:
137 try_remove(self._tmpfile)
139 def scan(self, defconfig):
140 """Load a defconfig file to obtain board parameters.
143 defconfig: path to the defconfig file to be processed
146 A dictionary of board parameters. It has a form of:
151 'vendor': <vendor_name>,
152 'board': <board_name>,
153 'target': <target_name>,
154 'config': <config_header_name>,
155 'options': <extra_options>
158 # strip special prefixes and save it in a temporary file
159 fd, self._tmpfile = tempfile.mkstemp()
160 with os.fdopen(fd, 'w') as f:
161 for line in open(defconfig):
162 colon = line.find(':CONFIG_')
166 f.write(line[colon + 1:])
168 self._conf.load_config(self._tmpfile)
169 try_remove(self._tmpfile)
174 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
175 # Set '-' if the value is empty.
176 for key, symbol in list(self._SYMBOL_TABLE.items()):
177 value = self._conf.syms.get(symbol).str_value
183 defconfig = os.path.basename(defconfig)
184 params['target'], match, rear = defconfig.partition('_defconfig')
185 assert match and not rear, '%s : invalid defconfig' % defconfig
188 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
189 params['arch'] = 'aarch64'
191 # fix-up options field. It should have the form:
192 # <config name>[:comma separated config options]
193 if params['options'] != '-':
194 params['options'] = params['config'] + ':' + \
195 params['options'].replace(r'\"', '"')
196 elif params['config'] != params['target']:
197 params['options'] = params['config']
201 def scan_defconfigs_for_multiprocess(queue, defconfigs):
202 """Scan defconfig files and queue their board parameters
204 This function is intended to be passed to
205 multiprocessing.Process() constructor.
208 queue: An instance of multiprocessing.Queue().
209 The resulting board parameters are written into it.
210 defconfigs: A sequence of defconfig files to be scanned.
212 kconf_scanner = KconfigScanner()
213 for defconfig in defconfigs:
214 queue.put(kconf_scanner.scan(defconfig))
216 def read_queues(queues, params_list):
217 """Read the queues and append the data to the paramers list"""
220 params_list.append(q.get())
222 def scan_defconfigs(jobs=1):
223 """Collect board parameters for all defconfig files.
225 This function invokes multiple processes for faster processing.
228 jobs: The number of jobs to run simultaneously
231 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
232 for filename in fnmatch.filter(filenames, '*_defconfig'):
233 if fnmatch.fnmatch(filename, '.*'):
235 all_defconfigs.append(os.path.join(dirpath, filename))
237 total_boards = len(all_defconfigs)
240 for i in range(jobs):
241 defconfigs = all_defconfigs[total_boards * i // jobs :
242 total_boards * (i + 1) // jobs]
243 q = multiprocessing.Queue(maxsize=-1)
244 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
245 args=(q, defconfigs))
250 # The resulting data should be accumulated to this list
253 # Data in the queues should be retrieved preriodically.
254 # Otherwise, the queues would become full and subprocesses would get stuck.
255 while any([p.is_alive() for p in processes]):
256 read_queues(queues, params_list)
257 # sleep for a while until the queues are filled
258 time.sleep(SLEEP_TIME)
260 # Joining subprocesses just in case
261 # (All subprocesses should already have been finished)
265 # retrieve leftover data
266 read_queues(queues, params_list)
270 class MaintainersDatabase:
272 """The database of board status and maintainers."""
275 """Create an empty database."""
278 def get_status(self, target):
279 """Return the status of the given board.
281 The board status is generally either 'Active' or 'Orphan'.
282 Display a warning message and return '-' if status information
286 'Active', 'Orphan' or '-'.
288 if not target in self.database:
289 print("WARNING: no status info for '%s'" % target, file=sys.stderr)
292 tmp = self.database[target][0]
293 if tmp.startswith('Maintained'):
295 elif tmp.startswith('Supported'):
297 elif tmp.startswith('Orphan'):
300 print(("WARNING: %s: unknown status for '%s'" %
301 (tmp, target)), file=sys.stderr)
304 def get_maintainers(self, target):
305 """Return the maintainers of the given board.
308 Maintainers of the board. If the board has two or more maintainers,
309 they are separated with colons.
311 if not target in self.database:
312 print("WARNING: no maintainers for '%s'" % target, file=sys.stderr)
315 return ':'.join(self.database[target][1])
317 def parse_file(self, file):
318 """Parse a MAINTAINERS file.
320 Parse a MAINTAINERS file and accumulates board status and
321 maintainers information.
324 file: MAINTAINERS file to be parsed
329 for line in open(file, encoding="utf-8"):
330 # Check also commented maintainers
331 if line[:3] == '#M:':
333 tag, rest = line[:2], line[2:].strip()
335 maintainers.append(rest)
337 # expand wildcard and filter by 'configs/*_defconfig'
338 for f in glob.glob(rest):
339 front, match, rear = f.partition('configs/')
340 if not front and match:
341 front, match, rear = rear.rpartition('_defconfig')
342 if match and not rear:
343 targets.append(front)
347 for target in targets:
348 self.database[target] = (status, maintainers)
353 for target in targets:
354 self.database[target] = (status, maintainers)
356 def insert_maintainers_info(params_list):
357 """Add Status and Maintainers information to the board parameters list.
360 params_list: A list of the board parameters
362 database = MaintainersDatabase()
363 for (dirpath, dirnames, filenames) in os.walk('.'):
364 if 'MAINTAINERS' in filenames:
365 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
367 for i, params in enumerate(params_list):
368 target = params['target']
369 params['status'] = database.get_status(target)
370 params['maintainers'] = database.get_maintainers(target)
371 params_list[i] = params
373 def format_and_output(params_list, output):
374 """Write board parameters into a file.
376 Columnate the board parameters, sort lines alphabetically,
377 and then write them to a file.
380 params_list: The list of board parameters
381 output: The path to the output file
383 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
384 'options', 'maintainers')
386 # First, decide the width of each column
387 max_length = dict([ (f, 0) for f in FIELDS])
388 for params in params_list:
390 max_length[f] = max(max_length[f], len(params[f]))
393 for params in params_list:
396 # insert two spaces between fields like column -t would
397 line += ' ' + params[f].ljust(max_length[f])
398 output_lines.append(line.strip())
400 # ignore case when sorting
401 output_lines.sort(key=str.lower)
403 with open(output, 'w', encoding="utf-8") as f:
404 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
406 def gen_boards_cfg(output, jobs=1, force=False, quiet=False):
407 """Generate a board database file.
410 output: The name of the output file
411 jobs: The number of jobs to run simultaneously
412 force: Force to generate the output even if it is new
413 quiet: True to avoid printing a message if nothing needs doing
415 check_top_directory()
417 if not force and output_is_new(output):
419 print("%s is up to date. Nothing to do." % output)
422 params_list = scan_defconfigs(jobs)
423 insert_maintainers_info(params_list)
424 format_and_output(params_list, output)
428 cpu_count = multiprocessing.cpu_count()
429 except NotImplementedError:
432 parser = optparse.OptionParser()
434 parser.add_option('-f', '--force', action="store_true", default=False,
435 help='regenerate the output even if it is new')
436 parser.add_option('-j', '--jobs', type='int', default=cpu_count,
437 help='the number of jobs to run simultaneously')
438 parser.add_option('-o', '--output', default=OUTPUT_FILE,
439 help='output file [default=%s]' % OUTPUT_FILE)
440 parser.add_option('-q', '--quiet', action="store_true", help='run silently')
441 (options, args) = parser.parse_args()
443 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force,
446 if __name__ == '__main__':