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.
18 import multiprocessing
25 from buildman import kconfiglib
27 ### constant variables ###
28 OUTPUT_FILE = 'boards.cfg'
29 CONFIG_DIR = 'configs'
33 # Automatically generated by %s: don't edit
35 # Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
39 ### helper functions ###
41 """Remove a file ignoring 'No such file or directory' error."""
44 except OSError as exception:
45 # Ignore 'No such file or directory' error
46 if exception.errno != errno.ENOENT:
49 def check_top_directory():
50 """Exit if we are not at the top of source directory."""
51 for f in ('README', 'Licenses'):
52 if not os.path.exists(f):
53 sys.exit('Please run at the top of source directory.')
55 def output_is_new(output):
56 """Check if the output file is up to date.
59 True if the given output file exists and is newer than any of
60 *_defconfig, MAINTAINERS and Kconfig*. False otherwise.
63 ctime = os.path.getctime(output)
64 except OSError as exception:
65 if exception.errno == errno.ENOENT:
66 # return False on 'No such file or directory' error
71 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
72 for filename in fnmatch.filter(filenames, '*_defconfig'):
73 if fnmatch.fnmatch(filename, '.*'):
75 filepath = os.path.join(dirpath, filename)
76 if ctime < os.path.getctime(filepath):
79 for (dirpath, dirnames, filenames) in os.walk('.'):
80 for filename in filenames:
81 if (fnmatch.fnmatch(filename, '*~') or
82 not fnmatch.fnmatch(filename, 'Kconfig*') and
83 not filename == 'MAINTAINERS'):
85 filepath = os.path.join(dirpath, filename)
86 if ctime < os.path.getctime(filepath):
89 # Detect a board that has been removed since the current board database
91 with open(output, encoding="utf-8") as f:
93 if line[0] == '#' or line == '\n':
95 defconfig = line.split()[6] + '_defconfig'
96 if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
102 class KconfigScanner:
104 """Kconfig scanner."""
106 ### constant variable only used in this class ###
111 'vendor' : 'SYS_VENDOR',
112 'board' : 'SYS_BOARD',
113 'config' : 'SYS_CONFIG_NAME',
117 """Scan all the Kconfig files and create a Kconfig object."""
118 # Define environment variables referenced from Kconfig
119 os.environ['srctree'] = os.getcwd()
120 os.environ['UBOOTVERSION'] = 'dummy'
121 os.environ['KCONFIG_OBJDIR'] = ''
122 self._conf = kconfiglib.Kconfig(warn=False)
125 """Delete a leftover temporary file before exit.
127 The scan() method of this class creates a temporay file and deletes
128 it on success. If scan() method throws an exception on the way,
129 the temporary file might be left over. In that case, it should be
130 deleted in this destructor.
132 if hasattr(self, '_tmpfile') and self._tmpfile:
133 try_remove(self._tmpfile)
135 def scan(self, defconfig):
136 """Load a defconfig file to obtain board parameters.
139 defconfig: path to the defconfig file to be processed
142 A dictionary of board parameters. It has a form of:
147 'vendor': <vendor_name>,
148 'board': <board_name>,
149 'target': <target_name>,
150 'config': <config_header_name>,
153 # strip special prefixes and save it in a temporary file
154 fd, self._tmpfile = tempfile.mkstemp()
155 with os.fdopen(fd, 'w') as f:
156 for line in open(defconfig):
157 colon = line.find(':CONFIG_')
161 f.write(line[colon + 1:])
163 self._conf.load_config(self._tmpfile)
164 try_remove(self._tmpfile)
169 # Get the value of CONFIG_SYS_ARCH, CONFIG_SYS_CPU, ... etc.
170 # Set '-' if the value is empty.
171 for key, symbol in list(self._SYMBOL_TABLE.items()):
172 value = self._conf.syms.get(symbol).str_value
178 defconfig = os.path.basename(defconfig)
179 params['target'], match, rear = defconfig.partition('_defconfig')
180 assert match and not rear, '%s : invalid defconfig' % defconfig
183 if params['arch'] == 'arm' and params['cpu'] == 'armv8':
184 params['arch'] = 'aarch64'
188 def scan_defconfigs_for_multiprocess(queue, defconfigs):
189 """Scan defconfig files and queue their board parameters
191 This function is intended to be passed to
192 multiprocessing.Process() constructor.
195 queue: An instance of multiprocessing.Queue().
196 The resulting board parameters are written into it.
197 defconfigs: A sequence of defconfig files to be scanned.
199 kconf_scanner = KconfigScanner()
200 for defconfig in defconfigs:
201 queue.put(kconf_scanner.scan(defconfig))
203 def read_queues(queues, params_list):
204 """Read the queues and append the data to the paramers list"""
207 params_list.append(q.get())
209 def scan_defconfigs(jobs=1):
210 """Collect board parameters for all defconfig files.
212 This function invokes multiple processes for faster processing.
215 jobs: The number of jobs to run simultaneously
218 for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
219 for filename in fnmatch.filter(filenames, '*_defconfig'):
220 if fnmatch.fnmatch(filename, '.*'):
222 all_defconfigs.append(os.path.join(dirpath, filename))
224 total_boards = len(all_defconfigs)
227 for i in range(jobs):
228 defconfigs = all_defconfigs[total_boards * i // jobs :
229 total_boards * (i + 1) // jobs]
230 q = multiprocessing.Queue(maxsize=-1)
231 p = multiprocessing.Process(target=scan_defconfigs_for_multiprocess,
232 args=(q, defconfigs))
237 # The resulting data should be accumulated to this list
240 # Data in the queues should be retrieved preriodically.
241 # Otherwise, the queues would become full and subprocesses would get stuck.
242 while any([p.is_alive() for p in processes]):
243 read_queues(queues, params_list)
244 # sleep for a while until the queues are filled
245 time.sleep(SLEEP_TIME)
247 # Joining subprocesses just in case
248 # (All subprocesses should already have been finished)
252 # retrieve leftover data
253 read_queues(queues, params_list)
257 class MaintainersDatabase:
259 """The database of board status and maintainers."""
262 """Create an empty database."""
265 def get_status(self, target):
266 """Return the status of the given board.
268 The board status is generally either 'Active' or 'Orphan'.
269 Display a warning message and return '-' if status information
273 'Active', 'Orphan' or '-'.
275 if not target in self.database:
276 print("WARNING: no status info for '%s'" % target, file=sys.stderr)
279 tmp = self.database[target][0]
280 if tmp.startswith('Maintained'):
282 elif tmp.startswith('Supported'):
284 elif tmp.startswith('Orphan'):
287 print(("WARNING: %s: unknown status for '%s'" %
288 (tmp, target)), file=sys.stderr)
291 def get_maintainers(self, target):
292 """Return the maintainers of the given board.
295 Maintainers of the board. If the board has two or more maintainers,
296 they are separated with colons.
298 if not target in self.database:
299 print("WARNING: no maintainers for '%s'" % target, file=sys.stderr)
302 return ':'.join(self.database[target][1])
304 def parse_file(self, file):
305 """Parse a MAINTAINERS file.
307 Parse a MAINTAINERS file and accumulates board status and
308 maintainers information.
311 file: MAINTAINERS file to be parsed
316 for line in open(file, encoding="utf-8"):
317 # Check also commented maintainers
318 if line[:3] == '#M:':
320 tag, rest = line[:2], line[2:].strip()
322 maintainers.append(rest)
324 # expand wildcard and filter by 'configs/*_defconfig'
325 for f in glob.glob(rest):
326 front, match, rear = f.partition('configs/')
327 if not front and match:
328 front, match, rear = rear.rpartition('_defconfig')
329 if match and not rear:
330 targets.append(front)
334 for target in targets:
335 self.database[target] = (status, maintainers)
340 for target in targets:
341 self.database[target] = (status, maintainers)
343 def insert_maintainers_info(params_list):
344 """Add Status and Maintainers information to the board parameters list.
347 params_list: A list of the board parameters
349 database = MaintainersDatabase()
350 for (dirpath, dirnames, filenames) in os.walk('.'):
351 if 'MAINTAINERS' in filenames:
352 database.parse_file(os.path.join(dirpath, 'MAINTAINERS'))
354 for i, params in enumerate(params_list):
355 target = params['target']
356 params['status'] = database.get_status(target)
357 params['maintainers'] = database.get_maintainers(target)
358 params_list[i] = params
360 def format_and_output(params_list, output):
361 """Write board parameters into a file.
363 Columnate the board parameters, sort lines alphabetically,
364 and then write them to a file.
367 params_list: The list of board parameters
368 output: The path to the output file
370 FIELDS = ('status', 'arch', 'cpu', 'soc', 'vendor', 'board', 'target',
373 # First, decide the width of each column
374 max_length = dict([ (f, 0) for f in FIELDS])
375 for params in params_list:
377 max_length[f] = max(max_length[f], len(params[f]))
380 for params in params_list:
383 # insert two spaces between fields like column -t would
384 line += ' ' + params[f].ljust(max_length[f])
385 output_lines.append(line.strip())
387 # ignore case when sorting
388 output_lines.sort(key=str.lower)
390 with open(output, 'w', encoding="utf-8") as f:
391 f.write(COMMENT_BLOCK + '\n'.join(output_lines) + '\n')
393 def gen_boards_cfg(output, jobs=1, force=False, quiet=False):
394 """Generate a board database file.
397 output: The name of the output file
398 jobs: The number of jobs to run simultaneously
399 force: Force to generate the output even if it is new
400 quiet: True to avoid printing a message if nothing needs doing
402 check_top_directory()
404 if not force and output_is_new(output):
406 print("%s is up to date. Nothing to do." % output)
409 params_list = scan_defconfigs(jobs)
410 insert_maintainers_info(params_list)
411 format_and_output(params_list, output)
415 cpu_count = multiprocessing.cpu_count()
416 except NotImplementedError:
419 parser = optparse.OptionParser()
421 parser.add_option('-f', '--force', action="store_true", default=False,
422 help='regenerate the output even if it is new')
423 parser.add_option('-j', '--jobs', type='int', default=min(cpu_count, 240),
424 help='the number of jobs to run simultaneously')
425 parser.add_option('-o', '--output', default=OUTPUT_FILE,
426 help='output file [default=%s]' % OUTPUT_FILE)
427 parser.add_option('-q', '--quiet', action="store_true", help='run silently')
428 (options, args) = parser.parse_args()
430 gen_boards_cfg(options.output, jobs=options.jobs, force=options.force,
433 if __name__ == '__main__':