Merge branch 'bmk' into 'master'

buildman: Fix pylint warnings in board.py and boards.py

See merge request u-boot/u-boot!358
This commit is contained in:
Simon Glass
2026-01-03 13:32:55 -07:00
2 changed files with 389 additions and 206 deletions

View File

@@ -4,21 +4,23 @@
"""A single board which can be selected and built"""
# pylint: disable=too-many-instance-attributes,too-few-public-methods
class Board:
"""A particular board that we can build"""
# pylint: disable=too-many-arguments
def __init__(self, status, arch, cpu, soc, vendor, board_name, target,
cfg_name, extended=None, orig_target=None):
"""Create a new board type.
Args:
status: define whether the board is 'Active' or 'Orphaned'
arch: Architecture name (e.g. arm)
cpu: Cpu name (e.g. arm1136)
soc: Name of SOC, or '' if none (e.g. mx31)
vendor: Name of vendor (e.g. armltd)
board_name: Name of board (e.g. integrator)
target: Target name (use make <target>_defconfig to configure)
cfg_name: Config-file name (in includes/configs/)
status (str): Either 'Active' or 'Orphaned'
arch (str): Architecture name (e.g. arm)
cpu (str): Cpu name (e.g. arm1136)
soc (str): Name of SOC, or '' if none (e.g. mx31)
vendor (str): Name of vendor (e.g. armltd)
board_name (str): Name of board (e.g. integrator)
target (str): Target name (use make <target>_defconfig to configure)
cfg_name (str): Config-file name (in includes/configs/)
extended (boards.Extended): Extended board, if this board is one
orig_target (str): Name of target this extended board is based on
"""

View File

@@ -3,6 +3,8 @@
# Author: Simon Glass <sjg@chromium.org>
# Author: Masahiro Yamada <yamada.m@jp.panasonic.com>
# pylint: disable=too-many-lines
"""Maintains a list of boards and allows them to be selected"""
from collections import OrderedDict, namedtuple
@@ -56,6 +58,50 @@ def try_remove(fname):
raise
def _check_srcdir_is_current(ctime, srcdir):
"""Check if any Kconfig or MAINTAINERS files are newer than ctime
Args:
ctime (float): Reference time to compare against
srcdir (str): Directory containing Kconfig and MAINTAINERS files
Returns:
bool: True if all files are older than ctime
"""
for (dirpath, _, filenames) in os.walk(srcdir):
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
return True
def _check_output_is_current(output, config_dir):
"""Check if output references any removed boards
Args:
output (str): Path to the output file
config_dir (str): Directory containing defconfig files
Returns:
bool: True if all referenced boards still exist
"""
with open(output, encoding="utf-8") as inf:
for line in inf:
if 'Options,' in line:
return False
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
def output_is_new(output, config_dir, srcdir):
"""Check if the output file is up to date.
@@ -69,13 +115,12 @@ def output_is_new(output, config_dir, srcdir):
srcdir (str): Directory containing Kconfig and MAINTAINERS files
Returns:
True if the given output file exists and is newer than any of
*_defconfig, MAINTAINERS and Kconfig*. False otherwise.
bool: True if the given output file exists and is newer than any of
*_defconfig, MAINTAINERS and Kconfig*. False otherwise.
Raises:
OSError: output file exists but could not be opened
"""
# pylint: disable=too-many-branches
try:
ctime = os.path.getctime(output)
except OSError as exception:
@@ -92,27 +137,11 @@ def output_is_new(output, config_dir, srcdir):
if ctime < os.path.getctime(filepath):
return False
for (dirpath, _, filenames) in os.walk(srcdir):
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
if not _check_srcdir_is_current(ctime, srcdir):
return False
# Detect a board that has been removed since the current board database
# was generated
with open(output, encoding="utf-8") as inf:
for line in inf:
if 'Options,' in line:
return False
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
if not _check_output_is_current(output, config_dir):
return False
return True
@@ -134,8 +163,9 @@ class Expr:
Args:
props (list of str): List of properties to check
Returns:
True if any of the properties match the regular expression
bool: True if any of the properties match the regular expression
"""
for prop in props:
if self._re.match(prop):
@@ -175,8 +205,9 @@ class Term:
Args:
props (list of str): List of properties to check
Returns:
True if all of the expressions in the Term match, else False
bool: True if all of the expressions in the Term match, else False
"""
for expr in self._expr_list:
if not expr.matches(props):
@@ -220,6 +251,38 @@ class KconfigScanner:
if self._tmpfile:
try_remove(self._tmpfile)
def _load_defconfig(self, defconfig):
"""Load a defconfig file, preprocessing if needed
If the defconfig contains #include directives, run the C
preprocessor to expand them before loading.
Args:
defconfig (str): Path to the defconfig file
"""
temp = None
if b'#include' in tools.read_file(defconfig):
cpp = os.getenv('CPP', 'cpp').split()
cmd = cpp + [
'-nostdinc', '-P',
'-I', self._srctree,
'-undef',
'-x', 'assembler-with-cpp',
defconfig]
stdout = command.output(*cmd, capture_stderr=True)
# pylint: disable=consider-using-with
temp = tempfile.NamedTemporaryFile(prefix='buildman-')
tools.write_file(temp.name, stdout, False)
fname = temp.name
tout.info(f'Processing #include to produce {defconfig}')
else:
fname = defconfig
self._conf.load_config(fname)
if temp:
del temp
self._tmpfile = None
def scan(self, defconfig, warn_targets):
"""Load a defconfig file to obtain board parameters.
@@ -245,27 +308,7 @@ class KconfigScanner:
expect_target, match, rear = leaf.partition('_defconfig')
assert match and not rear, f'{leaf} : invalid defconfig'
temp = None
if b'#include' in tools.read_file(defconfig):
cpp = os.getenv('CPP', 'cpp').split()
cmd = cpp + [
'-nostdinc', '-P',
'-I', self._srctree,
'-undef',
'-x', 'assembler-with-cpp',
defconfig]
stdout = command.output(*cmd, capture_stderr=True)
temp = tempfile.NamedTemporaryFile(prefix='buildman-')
tools.write_file(temp.name, stdout, False)
fname = temp.name
tout.info(f'Processing #include to produce {defconfig}')
else:
fname = defconfig
self._conf.load_config(fname)
if temp:
del temp
self._tmpfile = None
self._load_defconfig(defconfig)
params = {}
warnings = []
@@ -281,22 +324,23 @@ class KconfigScanner:
# Check there is exactly one TARGET_xxx set
if warn_targets:
target = None
for name, sym in self._conf.syms.items():
if name.startswith('TARGET_') and sym.str_value == 'y':
tname = name[7:].lower()
if target:
warnings.append(
f'WARNING: {leaf}: Duplicate TARGET_xxx: {target} and {tname}')
else:
target = tname
if not target:
cfg_name = expect_target.replace('-', '_').upper()
warnings.append(f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
warnings += self._check_targets(leaf, expect_target)
params['target'] = expect_target
self._fixup_arch(params)
return params, warnings
def _fixup_arch(self, params):
"""Fix up architecture names
Handle cases where the architecture name needs adjustment based on
CPU type or other configuration.
Args:
params (dict): Board parameters to update in place
"""
# fix-up for aarch64
if params['arch'] == 'arm' and params['cpu'] == 'armv8':
params['arch'] = 'aarch64'
@@ -305,14 +349,41 @@ class KconfigScanner:
if params['arch'] == 'riscv':
try:
value = self._conf.syms.get('ARCH_RV32I').str_value
except:
except AttributeError:
value = ''
if value == 'y':
params['arch'] = 'riscv32'
else:
params['arch'] = 'riscv64'
return params, warnings
def _check_targets(self, leaf, expect_target):
"""Check that exactly one TARGET_xxx option is set
Args:
leaf (str): Leaf name of defconfig file (for warnings)
expect_target (str): Expected target name
Returns:
list of str: List of warnings found
"""
warnings = []
target = None
for name, sym in self._conf.syms.items():
if name.startswith('TARGET_') and sym.str_value == 'y':
tname = name[7:].lower()
if target:
warnings.append(
f'WARNING: {leaf}: Duplicate TARGET_xxx: '
f'{target} and {tname}')
else:
target = tname
if not target:
cfg_name = expect_target.replace('-', '_').upper()
warnings.append(
f'WARNING: {leaf}: No TARGET_{cfg_name} enabled')
return warnings
class MaintainersDatabase:
@@ -380,6 +451,60 @@ class MaintainersDatabase:
self.warnings.append(f"WARNING: no maintainers for '{target}'")
return ''
def _add_targets(self, targets, status, maintainers):
"""Add targets to the database
Args:
targets (list of str): List of target names
status (str): Board status
maintainers (list of str): List of maintainers
"""
for target in targets:
self.database[target] = (status, maintainers)
@staticmethod
def _handle_f_tag(srcdir, rest, targets):
"""Handle F: tag - expand wildcard and filter by defconfig
Args:
srcdir (str): Source directory
rest (str): Remainder of line after 'F:'
targets (list of str): List to append targets to
"""
glob_path = os.path.join(srcdir, rest)
for item in glob.glob(glob_path):
front, match, rear = item.partition('configs/')
if front.endswith('/'):
front = front[:-1]
if front == srcdir and match:
front, match, rear = rear.rpartition('_defconfig')
if match and not rear:
targets.append(front)
@staticmethod
def _handle_n_tag(srcdir, rest, targets):
"""Handle N: tag - scan configs dir and match with regex
Args:
srcdir (str): Source directory
rest (str): Remainder of line after 'N:'
targets (list of str): List to append targets to
"""
walk_path = os.walk(os.path.join(srcdir, 'configs'))
for dirpath, _, fnames in walk_path:
for cfg in fnames:
path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
front, match, rear = path.partition('configs/')
if front or not match:
continue
front, match, rear = rear.rpartition('_defconfig')
# Use this entry if it matches the defconfig file
# without the _defconfig suffix. For example
# 'am335x.*' matches am335x_guardian_defconfig
if match and not rear and re.search(rest, front):
targets.append(front)
def parse_file(self, srcdir, fname):
"""Parse a MAINTAINERS file.
@@ -397,21 +522,11 @@ class MaintainersDatabase:
srcdir (str): Directory containing source code (Kconfig files)
fname (str): MAINTAINERS file to be parsed
"""
def add_targets(linenum):
"""Add any new targets
Args:
linenum (int): Current line number
"""
if targets:
for target in targets:
self.database[target] = (status, maintainers)
targets = []
maintainers = []
status = '-'
with open(fname, encoding="utf-8") as inf:
for linenum, line in enumerate(inf):
for _, line in enumerate(inf):
# Check also commented maintainers
if line[:3] == '#M:':
line = line[1:]
@@ -419,41 +534,17 @@ class MaintainersDatabase:
if tag == 'M:':
maintainers.append(rest)
elif tag == 'F:':
# expand wildcard and filter by 'configs/*_defconfig'
glob_path = os.path.join(srcdir, rest)
for item in glob.glob(glob_path):
front, match, rear = item.partition('configs/')
if front.endswith('/'):
front = front[:-1]
if front == srcdir and match:
front, match, rear = rear.rpartition('_defconfig')
if match and not rear:
targets.append(front)
self._handle_f_tag(srcdir, rest, targets)
elif tag == 'S:':
status = rest
elif tag == 'N:':
# Just scan the configs directory since that's all we care
# about
walk_path = os.walk(os.path.join(srcdir, 'configs'))
for dirpath, _, fnames in walk_path:
for cfg in fnames:
path = os.path.join(dirpath, cfg)[len(srcdir) + 1:]
front, match, rear = path.partition('configs/')
if front or not match:
continue
front, match, rear = rear.rpartition('_defconfig')
# Use this entry if it matches the defconfig file
# without the _defconfig suffix. For example
# 'am335x.*' matches am335x_guardian_defconfig
if match and not rear and re.search(rest, front):
targets.append(front)
self._handle_n_tag(srcdir, rest, targets)
elif line == '\n':
add_targets(linenum)
self._add_targets(targets, status, maintainers)
targets = []
maintainers = []
status = '-'
add_targets(linenum)
self._add_targets(targets, status, maintainers)
class Boards:
@@ -502,7 +593,7 @@ class Boards:
"""Return a list of available boards.
Returns:
List of Board objects
list of Board: List of Board objects
"""
return self._boards
@@ -523,7 +614,8 @@ class Boards:
"""Return a dictionary containing the selected boards
Returns:
List of Board objects that are marked selected
OrderedDict: Boards that are marked selected (key=target,
value=Board)
"""
board_dict = OrderedDict()
for brd in self._boards:
@@ -535,7 +627,7 @@ class Boards:
"""Return a list of selected boards
Returns:
List of Board objects that are marked selected
list of Board: Board objects that are marked selected
"""
return [brd for brd in self._boards if brd.build_it]
@@ -543,7 +635,7 @@ class Boards:
"""Return a list of selected boards
Returns:
List of board names that are marked selected
list of str: Board names that are marked selected
"""
return [brd.target for brd in self._boards if brd.build_it]
@@ -598,6 +690,54 @@ class Boards:
terms.append(term)
return terms
@staticmethod
# pylint: disable=too-many-arguments
def _check_board(brd, terms, brds, found, exclude_list, result):
"""Check whether to include or exclude a board
Checks the various terms and decides whether to build it or not.
If it is built, add the board to the result[term] list so we know
which term caused it to be built. Add it to result['all'] also.
Keep a list of boards we found in 'found', so we can report boards
which appear in self._boards but not in brds.
Args:
brd (Board): Board to check
terms (list of Term): Terms to match against
brds (list of str): List of board names to build, or None
found (list of str): List to append found board names to
exclude_list (list of Expr): Expressions for boards to exclude
result (OrderedDict): Dict to store results in
"""
matching_term = None
build_it = False
if terms:
for term in terms:
if term.matches(brd.props):
matching_term = str(term)
build_it = True
break
elif brds:
if brd.target in brds:
build_it = True
found.append(brd.target)
else:
build_it = True
# Check that it is not specifically excluded
for expr in exclude_list:
if expr.matches(brd.props):
build_it = False
break
if build_it:
brd.build_it = True
if matching_term:
result[matching_term].append(brd.target)
result['all'].append(brd.target)
def select_boards(self, args, exclude=None, brds=None):
"""Mark boards selected based on args
@@ -616,53 +756,11 @@ class Boards:
brds (list of Board): List of boards to build, or None/[] for all
Returns:
Tuple
Dictionary which holds the list of boards which were selected
due to each argument, arranged by argument.
List of errors found
tuple:
OrderedDict: Boards selected due to each argument, keyed by
argument
list of str: Errors/warnings found
"""
def _check_board(brd):
"""Check whether to include or exclude a board
Checks the various terms and decide whether to build it or not (the
'build_it' variable).
If it is built, add the board to the result[term] list so we know
which term caused it to be built. Add it to result['all'] also.
Keep a list of boards we found in 'found', so we can report boards
which appear in self._boards but not in brds.
Args:
brd (Board): Board to check
"""
matching_term = None
build_it = False
if terms:
for term in terms:
if term.matches(brd.props):
matching_term = str(term)
build_it = True
break
elif brds:
if brd.target in brds:
build_it = True
found.append(brd.target)
else:
build_it = True
# Check that it is not specifically excluded
for expr in exclude_list:
if expr.matches(brd.props):
build_it = False
break
if build_it:
brd.build_it = True
if matching_term:
result[matching_term].append(brd.target)
result['all'].append(brd.target)
result = OrderedDict()
warnings = []
terms = self._build_terms(args)
@@ -678,7 +776,7 @@ class Boards:
found = []
for brd in self._boards:
_check_board(brd)
self._check_board(brd, terms, brds, found, exclude_list, result)
if brds:
remaining = set(brds) - set(found)
@@ -723,6 +821,55 @@ class Boards:
params_list.append(params)
warnings.update(warn)
@staticmethod
def _collect_defconfigs(config_dir):
"""Collect all defconfig files from a directory
Args:
config_dir (str): Directory containing the defconfig files
Returns:
list of str: Paths to all defconfig files found
"""
all_defconfigs = []
for (dirpath, _, filenames) in os.walk(config_dir):
for filename in fnmatch.filter(filenames, '*_defconfig'):
if fnmatch.fnmatch(filename, '.*'):
continue
all_defconfigs.append(os.path.join(dirpath, filename))
return all_defconfigs
def _start_scanners(self, all_defconfigs, srcdir, jobs, warn_targets):
"""Start parallel defconfig scanning processes
Args:
all_defconfigs (list of str): Paths to defconfig files to scan
srcdir (str): Directory containing source code (Kconfig files)
jobs (int): The number of jobs to run simultaneously
warn_targets (bool): True to warn about missing or duplicate
CONFIG_TARGET options
Returns:
tuple:
list of Process: Running scanner processes
list of Queue: Queues for receiving results
"""
total_boards = len(all_defconfigs)
processes = []
queues = []
for i in range(jobs):
defconfigs = all_defconfigs[total_boards * i // jobs :
total_boards * (i + 1) // jobs]
que = multiprocessing.Queue(maxsize=-1)
proc = multiprocessing.Process(
target=self.scan_defconfigs_for_multiprocess,
args=(srcdir, que, defconfigs, warn_targets))
proc.start()
processes.append(proc)
queues.append(que)
return processes, queues
def scan_defconfigs(self, config_dir, srcdir, jobs=1, warn_targets=False):
"""Collect board parameters for all defconfig files.
@@ -743,33 +890,16 @@ class Boards:
value: string value of the key
list of str: List of warnings recorded
"""
all_defconfigs = []
for (dirpath, _, filenames) in os.walk(config_dir):
for filename in fnmatch.filter(filenames, '*_defconfig'):
if fnmatch.fnmatch(filename, '.*'):
continue
all_defconfigs.append(os.path.join(dirpath, filename))
total_boards = len(all_defconfigs)
processes = []
queues = []
for i in range(jobs):
defconfigs = all_defconfigs[total_boards * i // jobs :
total_boards * (i + 1) // jobs]
que = multiprocessing.Queue(maxsize=-1)
proc = multiprocessing.Process(
target=self.scan_defconfigs_for_multiprocess,
args=(srcdir, que, defconfigs, warn_targets))
proc.start()
processes.append(proc)
queues.append(que)
all_defconfigs = self._collect_defconfigs(config_dir)
processes, queues = self._start_scanners(all_defconfigs, srcdir, jobs,
warn_targets)
# The resulting data should be accumulated to these lists
params_list = []
warnings = set()
# Data in the queues should be retrieved preriodically.
# Otherwise, the queues would become full and subprocesses would get stuck.
# Data in the queues should be retrieved preriodically. Otherwise,
# the queues would become full and subprocesses would get stuck.
while any(p.is_alive() for p in processes):
self.read_queues(queues, params_list, warnings)
# sleep for a while until the queues are filled
@@ -790,6 +920,7 @@ class Boards:
"""Add Status and Maintainers information to the board parameters list.
Args:
srcdir (str): Directory containing source code (MAINTAINERS files)
params_list (list of dict): A list of the board parameters
Returns:
@@ -884,7 +1015,8 @@ class Boards:
output (str): The name of the output file
jobs (int): The number of jobs to run simultaneously
force (bool): Force to generate the output even if it is new
quiet (bool): True to avoid printing a message if nothing needs doing
quiet (bool): True to avoid printing a message if nothing needs
doing
Returns:
bool: True if all is well, False if there were warnings
@@ -948,7 +1080,12 @@ class Boards:
raise ValueError(f"Board '{target}' not found")
def parse_extended(self, dbase, fname):
"""Parse a single 'extended' file"""
"""Parse a single 'extended' file
Args:
dbase (tuple): Database of defconfigs from qconfig
fname (str): Path to the extended-board file to parse
"""
result = ExtendedParser.parse_file(fname)
for ext in result:
ext_boards = self.scan_extended(dbase, ext)
@@ -963,7 +1100,15 @@ class Boards:
self.add_board(newb)
def scan_extended(self, dbase, ext):
"""Scan for extended boards"""
"""Scan for extended boards
Args:
dbase (tuple): Database of defconfigs
ext (Extended): Extended-board definition
Returns:
set of str: Set of board names matching the extended definition
"""
# First check the fragments
frags = []
for frag in ext.fragments:
@@ -1025,13 +1170,28 @@ class ExtendedParser:
@staticmethod
def parse_file(fname):
"""Parse a file and return the result"""
"""Parse a file and return the result
Args:
fname (str): Filename to parse
Returns:
list of Extended: List of extended-board definitions
"""
return ExtendedParser.parse_data(fname,
tools.read_file(fname, binary=False))
@staticmethod
def parse_data(fname, data):
"""Parse a file and return the result"""
"""Parse a file and return the result
Args:
fname (str): Filename (for error messages)
data (str): Contents of the file
Returns:
list of Extended: List of extended-board definitions
"""
parser = ExtendedParser()
parser.parse(fname, data)
return parser.extended
@@ -1042,6 +1202,12 @@ class ExtendedParser:
Args:
fname (str): Filename to parse (used for error messages)
data (str): Contents of the file
Returns:
list of Extended: List of extended-board definitions
Raises:
ValueError: Invalid syntax in file
"""
self.start()
for seq, line in enumerate(data.splitlines()):
@@ -1054,7 +1220,8 @@ class ExtendedParser:
if '=' in line:
pair = line.split('=')
if len(pair) != 2:
raise ValueError(f'{fname}:{linenum}: Invalid CONFIG syntax')
raise ValueError(
f'{fname}:{linenum}: Invalid CONFIG syntax')
first, rest = pair
cfg = first.strip()
value = rest.strip()
@@ -1062,27 +1229,41 @@ class ExtendedParser:
else:
target = line.strip()
if ' ' in target:
raise ValueError(f'{fname}:{linenum}: Invalid target regex')
raise ValueError(
f'{fname}:{linenum}: Invalid target regex')
self.targets.append(['regex', line.strip()])
else:
pair = line.split(':')
if len(pair) != 2:
raise ValueError(f'{fname}:{linenum}: Invalid tag')
tag, rest = pair
value = rest.strip()
if tag == 'name':
self.finish()
if ' ' in value:
raise ValueError(f'{fname}:{linenum}: Invalid name')
self.name = value
elif tag == 'desc':
self.desc = value
elif tag == 'fragment':
self.fragments.append(value)
elif tag == 'targets':
self.in_targets = True
else:
raise ValueError(f"{fname}:{linenum}: Unknown tag '{tag}'")
self._parse_tag(fname, linenum, line)
self.finish()
return self.extended
def _parse_tag(self, fname, linenum, line):
"""Parse a tag line (one not starting with a space)
Args:
fname (str): Filename (for error messages)
linenum (int): Line number (for error messages)
line (str): Line to parse
Raises:
ValueError: Invalid syntax
"""
pair = line.split(':')
if len(pair) != 2:
raise ValueError(f'{fname}:{linenum}: Invalid tag')
tag, rest = pair
value = rest.strip()
if tag == 'name':
self.finish()
if ' ' in value:
raise ValueError(f'{fname}:{linenum}: Invalid name')
self.name = value
elif tag == 'desc':
self.desc = value
elif tag == 'fragment':
self.fragments.append(value)
elif tag == 'targets':
self.in_targets = True
else:
raise ValueError(f"{fname}:{linenum}: Unknown tag '{tag}'")