blob: c0d7b94409e283edacd50101e3a648dd8e0e04cf [file] [log] [blame]
#!/usr/bin/env python
#
# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
#
# SPDX-License-Identifier: GPL-2.0+
#
"""
Converter from Kconfig and MAINTAINERS to boards.cfg
Run 'tools/genboardscfg.py' to create boards.cfg file.
Run 'tools/genboardscfg.py -h' for available options.
"""
import errno
import fnmatch
import glob
import optparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
BOARD_FILE = 'boards.cfg'
CONFIG_DIR = 'configs'
REFORMAT_CMD = [os.path.join('tools', 'reformat.py'),
'-i', '-d', '-', '-s', '8']
SHOW_GNU_MAKE = 'scripts/show-gnu-make'
SLEEP_TIME=0.03
COMMENT_BLOCK = '''#
# List of boards
# Automatically generated by %s: don't edit
#
# Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
''' % __file__
### helper functions ###
def get_terminal_columns():
"""Get the width of the terminal.
Returns:
The width of the terminal, or zero if the stdout is not
associated with tty.
"""
try:
return shutil.get_terminal_size().columns # Python 3.3~
except AttributeError:
import fcntl
import termios
import struct
arg = struct.pack('hhhh', 0, 0, 0, 0)
try:
ret = fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, arg)
except IOError as exception:
# If 'Inappropriate ioctl for device' error occurs,
# stdout is probably redirected. Return 0.
return 0
return struct.unpack('hhhh', ret)[1]
def get_devnull():
"""Get the file object of '/dev/null' device."""
try:
devnull = subprocess.DEVNULL # py3k
except AttributeError:
devnull = open(os.devnull, 'wb')
return devnull
def check_top_directory():
"""Exit if we are not at the top of source directory."""
for f in ('README', 'Licenses'):
if not os.path.exists(f):
sys.exit('Please run at the top of source directory.')
def get_make_cmd():
"""Get the command name of GNU Make."""
process = subprocess.Popen([SHOW_GNU_MAKE], stdout=subprocess.PIPE)
ret = process.communicate()
if process.returncode:
sys.exit('GNU Make not found')
return ret[0].rstrip()
def output_is_new():
"""Check if the boards.cfg file is up to date.
Returns:
True if the boards.cfg file exists and is newer than any of
*_defconfig, MAINTAINERS and Kconfig*. False otherwise.
"""
try:
ctime = os.path.getctime(BOARD_FILE)
except OSError as exception:
if exception.errno == errno.ENOENT:
# return False on 'No such file or directory' error
return False
else:
raise
for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
for filename in fnmatch.filter(filenames, '*_defconfig'):
if fnmatch.fnmatch(filename, '.*'):
continue
filepath = os.path.join(dirpath, filename)
if ctime < os.path.getctime(filepath):
return False
for (dirpath, dirnames, filenames) in os.walk('.'):
for filename in filenames:
if (fnmatch.fnmatch(filename, '*~') or
not fnmatch.fnmatch(filename, 'Kconfig*') and
not filename == 'MAINTAINERS'):
continue
filepath = os.path.join(dirpath, filename)
if ctime < os.path.getctime(filepath):
return False
# Detect a board that has been removed since the current boards.cfg
# was generated
with open(BOARD_FILE) as f:
for line in f:
if line[0] == '#' or line == '\n':
continue
defconfig = line.split()[6] + '_defconfig'
if not os.path.exists(os.path.join(CONFIG_DIR, defconfig)):
return False
return True
### classes ###
class MaintainersDatabase:
"""The database of board status and maintainers."""
def __init__(self):
"""Create an empty database."""
self.database = {}
def get_status(self, target):
"""Return the status of the given board.
Returns:
Either 'Active' or 'Orphan'
"""
if not target in self.database:
print >> sys.stderr, "WARNING: no status info for '%s'" % target
return '-'
tmp = self.database[target][0]
if tmp.startswith('Maintained'):
return 'Active'
elif tmp.startswith('Orphan'):
return 'Orphan'
else:
print >> sys.stderr, ("WARNING: %s: unknown status for '%s'" %
(tmp, target))
return '-'
def get_maintainers(self, target):
"""Return the maintainers of the given board.
If the board has two or more maintainers, they are separated
with colons.
"""
if not target in self.database:
print >> sys.stderr, "WARNING: no maintainers for '%s'" % target
return ''
return ':'.join(self.database[target][1])
def parse_file(self, file):
"""Parse the given MAINTAINERS file.
This method parses MAINTAINERS and add board status and
maintainers information to the database.
Arguments:
file: MAINTAINERS file to be parsed
"""
targets = []
maintainers = []
status = '-'
for line in open(file):
tag, rest = line[:2], line[2:].strip()
if tag == 'M:':
maintainers.append(rest)
elif tag == 'F:':
# expand wildcard and filter by 'configs/*_defconfig'
for f in glob.glob(rest):
front, match, rear = f.partition('configs/')
if not front and match:
front, match, rear = rear.rpartition('_defconfig')
if match and not rear:
targets.append(front)
elif tag == 'S:':
status = rest
elif line == '\n':
for target in targets:
self.database[target] = (status, maintainers)
targets = []
maintainers = []
status = '-'
if targets:
for target in targets:
self.database[target] = (status, maintainers)
class DotConfigParser:
"""A parser of .config file.
Each line of the output should have the form of:
Status, Arch, CPU, SoC, Vendor, Board, Target, Options, Maintainers
Most of them are extracted from .config file.
MAINTAINERS files are also consulted for Status and Maintainers fields.
"""
re_arch = re.compile(r'CONFIG_SYS_ARCH="(.*)"')
re_cpu = re.compile(r'CONFIG_SYS_CPU="(.*)"')
re_soc = re.compile(r'CONFIG_SYS_SOC="(.*)"')
re_vendor = re.compile(r'CONFIG_SYS_VENDOR="(.*)"')
re_board = re.compile(r'CONFIG_SYS_BOARD="(.*)"')
re_config = re.compile(r'CONFIG_SYS_CONFIG_NAME="(.*)"')
re_options = re.compile(r'CONFIG_SYS_EXTRA_OPTIONS="(.*)"')
re_list = (('arch', re_arch), ('cpu', re_cpu), ('soc', re_soc),
('vendor', re_vendor), ('board', re_board),
('config', re_config), ('options', re_options))
must_fields = ('arch', 'config')
def __init__(self, build_dir, output, maintainers_database):
"""Create a new .config perser.
Arguments:
build_dir: Build directory where .config is located
output: File object which the result is written to
maintainers_database: An instance of class MaintainersDatabase
"""
self.dotconfig = os.path.join(build_dir, '.config')
self.output = output
self.database = maintainers_database
def parse(self, defconfig):
"""Parse .config file and output one-line database for the given board.
Arguments:
defconfig: Board (defconfig) name
"""
fields = {}
for line in open(self.dotconfig):
if not line.startswith('CONFIG_SYS_'):
continue
for (key, pattern) in self.re_list:
m = pattern.match(line)
if m and m.group(1):
fields[key] = m.group(1)
break
# sanity check of '.config' file
for field in self.must_fields:
if not field in fields:
print >> sys.stderr, (
"WARNING: '%s' is not defined in '%s'. Skip." %
(field, defconfig))
return
# fix-up for aarch64
if fields['arch'] == 'arm' and 'cpu' in fields:
if fields['cpu'] == 'armv8':
fields['arch'] = 'aarch64'
target, match, rear = defconfig.partition('_defconfig')
assert match and not rear, \
'%s : invalid defconfig file name' % defconfig
fields['status'] = self.database.get_status(target)
fields['maintainers'] = self.database.get_maintainers(target)
if 'options' in fields:
options = fields['config'] + ':' + \
fields['options'].replace(r'\"', '"')
elif fields['config'] != target:
options = fields['config']
else:
options = '-'
self.output.write((' '.join(['%s'] * 9) + '\n') %
(fields['status'],
fields['arch'],
fields.get('cpu', '-'),
fields.get('soc', '-'),
fields.get('vendor', '-'),
fields.get('board', '-'),
target,
options,
fields['maintainers']))
class Slot:
"""A slot to store a subprocess.
Each instance of this class handles one subprocess.
This class is useful to control multiple processes
for faster processing.
"""
def __init__(self, output, maintainers_database, devnull, make_cmd):
"""Create a new slot.
Arguments:
output: File object which the result is written to
maintainers_database: An instance of class MaintainersDatabase
"""
self.occupied = False
self.build_dir = tempfile.mkdtemp()
self.devnull = devnull
self.make_cmd = make_cmd
self.parser = DotConfigParser(self.build_dir, output,
maintainers_database)
def __del__(self):
"""Delete the working directory"""
if not self.occupied:
while self.ps.poll() == None:
pass
shutil.rmtree(self.build_dir)
def add(self, defconfig):
"""Add a new subprocess to the slot.
Fails if the slot is occupied, that is, the current subprocess
is still running.
Arguments:
defconfig: Board (defconfig) name
Returns:
Return True on success or False on fail
"""
if self.occupied:
return False
o = 'O=' + self.build_dir
self.ps = subprocess.Popen([self.make_cmd, o, defconfig],
stdout=self.devnull)
self.defconfig = defconfig
self.occupied = True
return True
def poll(self):
"""Check if the subprocess is running and invoke the .config
parser if the subprocess is terminated.
Returns:
Return True if the subprocess is terminated, False otherwise
"""
if not self.occupied:
return True
if self.ps.poll() == None:
return False
if self.ps.poll() == 0:
self.parser.parse(self.defconfig)
else:
print >> sys.stderr, ("WARNING: failed to process '%s'. skip." %
self.defconfig)
self.occupied = False
return True
class Slots:
"""Controller of the array of subprocess slots."""
def __init__(self, jobs, output, maintainers_database):
"""Create a new slots controller.
Arguments:
jobs: A number of slots to instantiate
output: File object which the result is written to
maintainers_database: An instance of class MaintainersDatabase
"""
self.slots = []
devnull = get_devnull()
make_cmd = get_make_cmd()
for i in range(jobs):
self.slots.append(Slot(output, maintainers_database,
devnull, make_cmd))
def add(self, defconfig):
"""Add a new subprocess if a vacant slot is available.
Arguments:
defconfig: Board (defconfig) name
Returns:
Return True on success or False on fail
"""
for slot in self.slots:
if slot.add(defconfig):
return True
return False
def available(self):
"""Check if there is a vacant slot.
Returns:
Return True if a vacant slot is found, False if all slots are full
"""
for slot in self.slots:
if slot.poll():
return True
return False
def empty(self):
"""Check if all slots are vacant.
Returns:
Return True if all slots are vacant, False if at least one slot
is running
"""
ret = True
for slot in self.slots:
if not slot.poll():
ret = False
return ret
class Indicator:
"""A class to control the progress indicator."""
MIN_WIDTH = 15
MAX_WIDTH = 70
def __init__(self, total):
"""Create an instance.
Arguments:
total: A number of boards
"""
self.total = total
self.cur = 0
width = get_terminal_columns()
width = min(width, self.MAX_WIDTH)
width -= self.MIN_WIDTH
if width > 0:
self.enabled = True
else:
self.enabled = False
self.width = width
def inc(self):
"""Increment the counter and show the progress bar."""
if not self.enabled:
return
self.cur += 1
arrow_len = self.width * self.cur // self.total
msg = '%4d/%d [' % (self.cur, self.total)
msg += '=' * arrow_len + '>' + ' ' * (self.width - arrow_len) + ']'
sys.stdout.write('\r' + msg)
sys.stdout.flush()
class BoardsFileGenerator:
"""Generator of boards.cfg."""
def __init__(self):
"""Prepare basic things for generating boards.cfg."""
# All the defconfig files to be processed
defconfigs = []
for (dirpath, dirnames, filenames) in os.walk(CONFIG_DIR):
dirpath = dirpath[len(CONFIG_DIR) + 1:]
for filename in fnmatch.filter(filenames, '*_defconfig'):
if fnmatch.fnmatch(filename, '.*'):
continue
defconfigs.append(os.path.join(dirpath, filename))
self.defconfigs = defconfigs
self.indicator = Indicator(len(defconfigs))
# Parse all the MAINTAINERS files
maintainers_database = MaintainersDatabase()
for (dirpath, dirnames, filenames) in os.walk('.'):
if 'MAINTAINERS' in filenames:
maintainers_database.parse_file(os.path.join(dirpath,
'MAINTAINERS'))
self.maintainers_database = maintainers_database
def __del__(self):
"""Delete the incomplete boards.cfg
This destructor deletes boards.cfg if the private member 'in_progress'
is defined as True. The 'in_progress' member is set to True at the
beginning of the generate() method and set to False at its end.
So, in_progress==True means generating boards.cfg was terminated
on the way.
"""
if hasattr(self, 'in_progress') and self.in_progress:
try:
os.remove(BOARD_FILE)
except OSError as exception:
# Ignore 'No such file or directory' error
if exception.errno != errno.ENOENT:
raise
print 'Removed incomplete %s' % BOARD_FILE
def generate(self, jobs):
"""Generate boards.cfg
This method sets the 'in_progress' member to True at the beginning
and sets it to False on success. The boards.cfg should not be
touched before/after this method because 'in_progress' is used
to detect the incomplete boards.cfg.
Arguments:
jobs: The number of jobs to run simultaneously
"""
self.in_progress = True
print 'Generating %s ... (jobs: %d)' % (BOARD_FILE, jobs)
# Output lines should be piped into the reformat tool
reformat_process = subprocess.Popen(REFORMAT_CMD,
stdin=subprocess.PIPE,
stdout=open(BOARD_FILE, 'w'))
pipe = reformat_process.stdin
pipe.write(COMMENT_BLOCK)
slots = Slots(jobs, pipe, self.maintainers_database)
# Main loop to process defconfig files:
# Add a new subprocess into a vacant slot.
# Sleep if there is no available slot.
for defconfig in self.defconfigs:
while not slots.add(defconfig):
while not slots.available():
# No available slot: sleep for a while
time.sleep(SLEEP_TIME)
self.indicator.inc()
# wait until all the subprocesses finish
while not slots.empty():
time.sleep(SLEEP_TIME)
print ''
# wait until the reformat tool finishes
reformat_process.communicate()
if reformat_process.returncode != 0:
sys.exit('"%s" failed' % REFORMAT_CMD[0])
self.in_progress = False
def gen_boards_cfg(jobs=1, force=False):
"""Generate boards.cfg file.
The incomplete boards.cfg is deleted if an error (including
the termination by the keyboard interrupt) occurs on the halfway.
Arguments:
jobs: The number of jobs to run simultaneously
"""
check_top_directory()
if not force and output_is_new():
print "%s is up to date. Nothing to do." % BOARD_FILE
sys.exit(0)
generator = BoardsFileGenerator()
generator.generate(jobs)
def main():
parser = optparse.OptionParser()
# Add options here
parser.add_option('-j', '--jobs',
help='the number of jobs to run simultaneously')
parser.add_option('-f', '--force', action="store_true", default=False,
help='regenerate the output even if it is new')
(options, args) = parser.parse_args()
if options.jobs:
try:
jobs = int(options.jobs)
except ValueError:
sys.exit('Option -j (--jobs) takes a number')
else:
try:
jobs = int(subprocess.Popen(['getconf', '_NPROCESSORS_ONLN'],
stdout=subprocess.PIPE).communicate()[0])
except (OSError, ValueError):
print 'info: failed to get the number of CPUs. Set jobs to 1'
jobs = 1
gen_boards_cfg(jobs, force=options.force)
if __name__ == '__main__':
main()