| #!/usr/bin/env python2 |
| # |
| # 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. |
| |
| This script only works on python 2.6 or later, but not python 3.x. |
| """ |
| |
| 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.003 |
| |
| 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 |
| devnull: file object of 'dev/null' |
| make_cmd: the command name of Make |
| """ |
| self.build_dir = tempfile.mkdtemp() |
| self.devnull = devnull |
| self.ps = subprocess.Popen([make_cmd, 'O=' + self.build_dir, |
| 'allnoconfig'], stdout=devnull) |
| self.occupied = True |
| self.parser = DotConfigParser(self.build_dir, output, |
| maintainers_database) |
| self.env = os.environ.copy() |
| self.env['srctree'] = os.getcwd() |
| self.env['UBOOTVERSION'] = 'dummy' |
| self.env['KCONFIG_OBJDIR'] = '' |
| |
| 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 |
| |
| with open(os.path.join(self.build_dir, '.tmp_defconfig'), 'w') as f: |
| for line in open(os.path.join(CONFIG_DIR, defconfig)): |
| colon = line.find(':CONFIG_') |
| if colon == -1: |
| f.write(line) |
| else: |
| f.write(line[colon + 1:]) |
| |
| self.ps = subprocess.Popen([os.path.join('scripts', 'kconfig', 'conf'), |
| '--defconfig=.tmp_defconfig', 'Kconfig'], |
| stdout=self.devnull, |
| cwd=self.build_dir, |
| env=self.env) |
| |
| self.defconfig = defconfig |
| self.occupied = True |
| return True |
| |
| def wait(self): |
| """Wait until the current subprocess finishes.""" |
| while self.occupied and self.ps.poll() == None: |
| time.sleep(SLEEP_TIME) |
| self.occupied = False |
| |
| 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)) |
| for slot in self.slots: |
| slot.wait() |
| |
| 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() |