123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594 |
- #!/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()
|