470 lines
16 KiB
Python
Executable File
470 lines
16 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2020 Nordic Semiconductor ASA
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import argparse
|
|
from collections import defaultdict, Counter
|
|
from dataclasses import dataclass, field
|
|
import itertools
|
|
from pathlib import Path
|
|
import pykwalify.core
|
|
import sys
|
|
from typing import List, Union
|
|
import yaml
|
|
import list_hardware
|
|
from list_hardware import unique_paths
|
|
|
|
try:
|
|
from yaml import CSafeLoader as SafeLoader
|
|
except ImportError:
|
|
from yaml import SafeLoader
|
|
|
|
BOARD_SCHEMA_PATH = str(Path(__file__).parent / 'schemas' / 'board-schema.yml')
|
|
with open(BOARD_SCHEMA_PATH, 'r') as f:
|
|
board_schema = yaml.load(f.read(), Loader=SafeLoader)
|
|
|
|
BOARD_YML = 'board.yml'
|
|
|
|
#
|
|
# This is shared code between the build system's 'boards' target
|
|
# and the 'west boards' extension command. If you change it, make
|
|
# sure to test both ways it can be used.
|
|
#
|
|
# (It's done this way to keep west optional, making it possible to run
|
|
# 'ninja boards' in a build directory without west installed.)
|
|
#
|
|
|
|
|
|
@dataclass
|
|
class Revision:
|
|
name: str
|
|
variants: List[str] = field(default_factory=list)
|
|
|
|
@staticmethod
|
|
def from_dict(revision):
|
|
revisions = []
|
|
for r in revision.get('revisions', []):
|
|
revisions.append(Revision.from_dict(r))
|
|
return Revision(revision['name'], revisions)
|
|
|
|
|
|
@dataclass
|
|
class Variant:
|
|
name: str
|
|
variants: List[str] = field(default_factory=list)
|
|
|
|
@staticmethod
|
|
def from_dict(variant):
|
|
variants = []
|
|
for v in variant.get('variants', []):
|
|
variants.append(Variant.from_dict(v))
|
|
return Variant(variant['name'], variants)
|
|
|
|
|
|
@dataclass
|
|
class Cpucluster:
|
|
name: str
|
|
variants: List[str] = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class Soc:
|
|
name: str
|
|
cpuclusters: List[str] = field(default_factory=list)
|
|
variants: List[str] = field(default_factory=list)
|
|
|
|
@staticmethod
|
|
def from_soc(soc, variants):
|
|
if soc is None:
|
|
return None
|
|
if soc.cpuclusters:
|
|
cpus = []
|
|
for c in soc.cpuclusters:
|
|
cpus.append(Cpucluster(c,
|
|
[Variant.from_dict(v) for v in variants if c == v['cpucluster']]
|
|
))
|
|
return Soc(soc.name, cpuclusters=cpus)
|
|
return Soc(soc.name, variants=[Variant.from_dict(v) for v in variants])
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Board:
|
|
name: str
|
|
# HWMv1 only supports a single Path, and requires Board dataclass to be hashable.
|
|
directories: Union[Path, List[Path]]
|
|
hwm: str
|
|
full_name: str = None
|
|
arch: str = None
|
|
vendor: str = None
|
|
revision_format: str = None
|
|
revision_default: str = None
|
|
revision_exact: bool = False
|
|
revisions: List[str] = field(default_factory=list, compare=False)
|
|
socs: List[Soc] = field(default_factory=list, compare=False)
|
|
variants: List[str] = field(default_factory=list, compare=False)
|
|
|
|
@property
|
|
def dir(self):
|
|
# Get the main board directory.
|
|
if isinstance(self.directories, Path):
|
|
return self.directories
|
|
return self.directories[0]
|
|
|
|
def from_qualifier(self, qualifiers):
|
|
qualifiers_list = qualifiers.split('/')
|
|
|
|
node = Soc(None)
|
|
n = len(qualifiers_list)
|
|
if n > 0:
|
|
soc_qualifier = qualifiers_list.pop(0)
|
|
for s in self.socs:
|
|
if s.name == soc_qualifier:
|
|
node = s
|
|
break
|
|
|
|
if n > 1:
|
|
if node.cpuclusters:
|
|
cpu_qualifier = qualifiers_list.pop(0)
|
|
for c in node.cpuclusters:
|
|
if c.name == cpu_qualifier:
|
|
node = c
|
|
break
|
|
else:
|
|
node = Variant(None)
|
|
|
|
for q in qualifiers_list:
|
|
for v in node.variants:
|
|
if v.name == q:
|
|
node = v
|
|
break
|
|
else:
|
|
node = Variant(None)
|
|
|
|
if node in (Soc(None), Variant(None)):
|
|
sys.exit(f'ERROR: qualifiers {qualifiers} not found when extending board {self.name}')
|
|
|
|
return node
|
|
|
|
|
|
def board_key(board):
|
|
return board.name
|
|
|
|
|
|
def find_arch2boards(args):
|
|
arch2board_set = find_arch2board_set(args)
|
|
return {arch: sorted(arch2board_set[arch], key=board_key)
|
|
for arch in arch2board_set}
|
|
|
|
|
|
def find_boards(args):
|
|
return sorted(itertools.chain(*find_arch2board_set(args).values()),
|
|
key=board_key)
|
|
|
|
|
|
def find_arch2board_set(args):
|
|
arches = sorted(find_arches(args))
|
|
ret = defaultdict(set)
|
|
|
|
for root in unique_paths(args.board_roots):
|
|
for arch, boards in find_arch2board_set_in(root, arches, args.board_dir).items():
|
|
if args.board is not None:
|
|
ret[arch] |= {b for b in boards if b.name == args.board}
|
|
else:
|
|
ret[arch] |= boards
|
|
|
|
return ret
|
|
|
|
|
|
def find_arches(args):
|
|
arch_set = set()
|
|
|
|
for root in unique_paths(args.arch_roots):
|
|
arch_set |= find_arches_in(root)
|
|
|
|
return arch_set
|
|
|
|
|
|
def find_arches_in(root):
|
|
ret = set()
|
|
arch = root / 'arch'
|
|
common = arch / 'common'
|
|
|
|
if not arch.is_dir():
|
|
return ret
|
|
|
|
for maybe_arch in arch.iterdir():
|
|
if not maybe_arch.is_dir() or maybe_arch == common:
|
|
continue
|
|
ret.add(maybe_arch.name)
|
|
|
|
return ret
|
|
|
|
|
|
def find_arch2board_set_in(root, arches, board_dir):
|
|
ret = defaultdict(set)
|
|
boards = root / 'boards'
|
|
|
|
for arch in arches:
|
|
if not (boards / arch).is_dir():
|
|
continue
|
|
for maybe_board in (boards / arch).iterdir():
|
|
if not maybe_board.is_dir():
|
|
continue
|
|
if board_dir and maybe_board not in board_dir:
|
|
continue
|
|
for maybe_defconfig in maybe_board.iterdir():
|
|
file_name = maybe_defconfig.name
|
|
if file_name.endswith('_defconfig') and not (maybe_board / BOARD_YML).is_file():
|
|
board_name = file_name[:-len('_defconfig')]
|
|
ret[arch].add(Board(board_name, maybe_board, 'v1', arch=arch))
|
|
|
|
return ret
|
|
|
|
|
|
def load_v2_boards(board_name, board_yml, systems):
|
|
boards = {}
|
|
board_extensions = []
|
|
if board_yml.is_file():
|
|
with board_yml.open('r', encoding='utf-8') as f:
|
|
b = yaml.load(f.read(), Loader=SafeLoader)
|
|
|
|
try:
|
|
pykwalify.core.Core(source_data=b, schema_data=board_schema).validate()
|
|
except pykwalify.errors.SchemaError as e:
|
|
sys.exit('ERROR: Malformed "build" section in file: {}\n{}'
|
|
.format(board_yml.as_posix(), e))
|
|
|
|
mutual_exclusive = {'board', 'boards'}
|
|
if len(mutual_exclusive - b.keys()) < 1:
|
|
sys.exit(f'ERROR: Malformed content in file: {board_yml.as_posix()}\n'
|
|
f'{mutual_exclusive} are mutual exclusive at this level.')
|
|
|
|
board_array = b.get('boards', [b.get('board', None)])
|
|
for board in board_array:
|
|
mutual_exclusive = {'name', 'extend'}
|
|
if len(mutual_exclusive - board.keys()) < 1:
|
|
sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
|
|
f'{mutual_exclusive} are mutual exclusive at this level.')
|
|
|
|
# This is a extending an existing board, place in array to allow later processing.
|
|
if 'extend' in board:
|
|
board.update({'dir': board_yml.parent})
|
|
board_extensions.append(board)
|
|
continue
|
|
|
|
# Create board
|
|
if board_name is not None:
|
|
if board['name'] != board_name:
|
|
# Not the board we're looking for, ignore.
|
|
continue
|
|
|
|
board_revision = board.get('revision')
|
|
if board_revision is not None and board_revision.get('format') != 'custom':
|
|
if board_revision.get('default') is None:
|
|
sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
|
|
"Cannot find required key 'default'. Path: '/board/revision.'")
|
|
if board_revision.get('revisions') is None:
|
|
sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
|
|
"Cannot find required key 'revisions'. Path: '/board/revision.'")
|
|
|
|
mutual_exclusive = {'socs', 'variants'}
|
|
if len(mutual_exclusive - board.keys()) < 1:
|
|
sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
|
|
f'{mutual_exclusive} are mutual exclusive at this level.')
|
|
socs = [Soc.from_soc(systems.get_soc(s['name']), s.get('variants', []))
|
|
for s in board.get('socs', {})]
|
|
|
|
boards[board['name']] = Board(
|
|
name=board['name'],
|
|
directories=[board_yml.parent],
|
|
vendor=board.get('vendor'),
|
|
full_name=board.get('full_name'),
|
|
revision_format=board.get('revision', {}).get('format'),
|
|
revision_default=board.get('revision', {}).get('default'),
|
|
revision_exact=board.get('revision', {}).get('exact', False),
|
|
revisions=[Revision.from_dict(v) for v in
|
|
board.get('revision', {}).get('revisions', [])],
|
|
socs=socs,
|
|
variants=[Variant.from_dict(v) for v in board.get('variants', [])],
|
|
hwm='v2',
|
|
)
|
|
board_qualifiers = board_v2_qualifiers(boards[board['name']])
|
|
duplicates = [q for q, n in Counter(board_qualifiers).items() if n > 1]
|
|
if duplicates:
|
|
sys.exit(f'ERROR: Duplicated board qualifiers detected {duplicates} for board: '
|
|
f'{board["name"]}.\nPlease check content of: {board_yml.as_posix()}\n')
|
|
return boards, board_extensions
|
|
|
|
|
|
def extend_v2_boards(boards, board_extensions):
|
|
for e in board_extensions:
|
|
board = boards.get(e['extend'])
|
|
if board is None:
|
|
continue
|
|
board.directories.append(e['dir'])
|
|
|
|
for v in e.get('variants', []):
|
|
node = board.from_qualifier(v['qualifier'])
|
|
if str(v['qualifier'] + '/' + v['name']) in board_v2_qualifiers(board):
|
|
board_yml = e['dir'] / BOARD_YML
|
|
sys.exit(f'ERROR: Variant: {v["name"]}, defined multiple times for board: '
|
|
f'{board.name}.\nLast defined in {board_yml}')
|
|
node.variants.append(Variant.from_dict(v))
|
|
|
|
|
|
# Note that this does not share the args.board functionality of find_v2_boards
|
|
def find_v2_board_dirs(args):
|
|
dirs = []
|
|
board_files = []
|
|
for root in unique_paths(args.board_roots):
|
|
board_files.extend((root / 'boards').rglob(BOARD_YML))
|
|
|
|
dirs = [board_yml.parent for board_yml in board_files if board_yml.is_file()]
|
|
return dirs
|
|
|
|
|
|
def find_v2_boards(args):
|
|
root_args = argparse.Namespace(**{'soc_roots': args.soc_roots})
|
|
systems = list_hardware.find_v2_systems(root_args)
|
|
|
|
boards = {}
|
|
board_extensions = []
|
|
board_files = []
|
|
if args.board_dir:
|
|
board_files = [d / BOARD_YML for d in args.board_dir]
|
|
else:
|
|
for root in unique_paths(args.board_roots):
|
|
board_files.extend((root / 'boards').rglob(BOARD_YML))
|
|
|
|
for board_yml in board_files:
|
|
b, e = load_v2_boards(args.board, board_yml, systems)
|
|
conflict_boards = set(boards.keys()).intersection(b.keys())
|
|
if conflict_boards:
|
|
sys.exit(f'ERROR: Board(s): {conflict_boards}, defined multiple times.\n'
|
|
f'Last defined in {board_yml}')
|
|
boards.update(b)
|
|
board_extensions.extend(e)
|
|
|
|
extend_v2_boards(boards, board_extensions)
|
|
return boards
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
|
add_args(parser)
|
|
add_args_formatting(parser)
|
|
return parser.parse_args()
|
|
|
|
|
|
def add_args(parser):
|
|
# Remember to update west-completion.bash if you add or remove
|
|
# flags
|
|
parser.add_argument("--arch-root", dest='arch_roots', default=[],
|
|
type=Path, action='append',
|
|
help='add an architecture root, may be given more than once')
|
|
parser.add_argument("--board-root", dest='board_roots', default=[],
|
|
type=Path, action='append',
|
|
help='add a board root, may be given more than once')
|
|
parser.add_argument("--soc-root", dest='soc_roots', default=[],
|
|
type=Path, action='append',
|
|
help='add a soc root, may be given more than once')
|
|
parser.add_argument("--board", dest='board', default=None,
|
|
help='lookup the specific board, fail if not found')
|
|
parser.add_argument("--board-dir", default=[], type=Path, action='append',
|
|
help='Only look for boards at the specific location')
|
|
|
|
|
|
def add_args_formatting(parser):
|
|
parser.add_argument("--cmakeformat", default=None,
|
|
help='''CMake Format string to use to list each board''')
|
|
|
|
|
|
def variant_v2_qualifiers(variant, qualifiers = None):
|
|
qualifiers_list = [variant.name] if qualifiers is None else [qualifiers + '/' + variant.name]
|
|
for v in variant.variants:
|
|
qualifiers_list.extend(variant_v2_qualifiers(v, qualifiers_list[0]))
|
|
return qualifiers_list
|
|
|
|
|
|
def board_v2_qualifiers(board):
|
|
qualifiers_list = []
|
|
|
|
for s in board.socs:
|
|
if s.cpuclusters:
|
|
for c in s.cpuclusters:
|
|
id_str = s.name + '/' + c.name
|
|
qualifiers_list.append(id_str)
|
|
for v in c.variants:
|
|
qualifiers_list.extend(variant_v2_qualifiers(v, id_str))
|
|
else:
|
|
qualifiers_list.append(s.name)
|
|
for v in s.variants:
|
|
qualifiers_list.extend(variant_v2_qualifiers(v, s.name))
|
|
|
|
for v in board.variants:
|
|
qualifiers_list.extend(variant_v2_qualifiers(v))
|
|
return qualifiers_list
|
|
|
|
|
|
def board_v2_qualifiers_csv(board):
|
|
# Return in csv (comma separated value) format
|
|
return ",".join(board_v2_qualifiers(board))
|
|
|
|
|
|
def dump_v2_boards(args):
|
|
boards = find_v2_boards(args)
|
|
|
|
for b in boards.values():
|
|
qualifiers_list = board_v2_qualifiers(b)
|
|
if args.cmakeformat is not None:
|
|
notfound = lambda x: x or 'NOTFOUND'
|
|
info = args.cmakeformat.format(
|
|
NAME='NAME;' + b.name,
|
|
DIR='DIR;' + ';'.join(
|
|
[str(x.as_posix()) for x in b.directories]),
|
|
VENDOR='VENDOR;' + notfound(b.vendor),
|
|
HWM='HWM;' + b.hwm,
|
|
REVISION_DEFAULT='REVISION_DEFAULT;' + notfound(b.revision_default),
|
|
REVISION_FORMAT='REVISION_FORMAT;' + notfound(b.revision_format),
|
|
REVISION_EXACT='REVISION_EXACT;' + str(b.revision_exact),
|
|
REVISIONS='REVISIONS;' + ';'.join(
|
|
[x.name for x in b.revisions]),
|
|
SOCS='SOCS;' + ';'.join([s.name for s in b.socs]),
|
|
QUALIFIERS='QUALIFIERS;' + ';'.join(qualifiers_list)
|
|
)
|
|
print(info)
|
|
else:
|
|
print(f'{b.name}')
|
|
|
|
|
|
def dump_boards(args):
|
|
arch2boards = find_arch2boards(args)
|
|
for arch, boards in arch2boards.items():
|
|
if args.cmakeformat is None:
|
|
print(f'{arch}:')
|
|
for board in boards:
|
|
if args.cmakeformat is not None:
|
|
info = args.cmakeformat.format(
|
|
NAME='NAME;' + board.name,
|
|
DIR='DIR;' + str(board.dir.as_posix()),
|
|
HWM='HWM;' + board.hwm,
|
|
VENDOR='VENDOR;NOTFOUND',
|
|
REVISION_DEFAULT='REVISION_DEFAULT;NOTFOUND',
|
|
REVISION_FORMAT='REVISION_FORMAT;NOTFOUND',
|
|
REVISION_EXACT='REVISION_EXACT;NOTFOUND',
|
|
REVISIONS='REVISIONS;NOTFOUND',
|
|
VARIANT_DEFAULT='VARIANT_DEFAULT;NOTFOUND',
|
|
SOCS='SOCS;',
|
|
QUALIFIERS='QUALIFIERS;'
|
|
)
|
|
print(info)
|
|
else:
|
|
print(f' {board.name}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
args = parse_args()
|
|
dump_boards(args)
|
|
dump_v2_boards(args)
|