|  | #!/usr/bin/env python3 | 
|  |  | 
|  | # Copyright (c) 2020 Nordic Semiconductor ASA | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  |  | 
|  | import argparse | 
|  | import difflib | 
|  | import sys | 
|  | from collections import Counter | 
|  | from dataclasses import dataclass, field | 
|  | from pathlib import Path | 
|  |  | 
|  | import jsonschema | 
|  | import list_hardware | 
|  | import yaml | 
|  | from jsonschema.exceptions import best_match | 
|  | 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.yaml') | 
|  | with open(BOARD_SCHEMA_PATH) as f: | 
|  | board_schema = yaml.load(f.read(), Loader=SafeLoader) | 
|  |  | 
|  | validator_class = jsonschema.validators.validator_for(board_schema) | 
|  | validator_class.check_schema(board_schema) | 
|  | board_validator = validator_class(board_schema) | 
|  |  | 
|  | 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: 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 and 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 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) | 
|  |  | 
|  | errors = list(board_validator.iter_errors(b)) | 
|  | if errors: | 
|  | sys.exit('ERROR: Malformed board YAML file: ' | 
|  | f'{board_yml.as_posix()}\n' | 
|  | f'{best_match(errors).message} in {best_match(errors).json_path}') | 
|  |  | 
|  | board_array = b.get('boards', [b.get('board', None)]) | 
|  | for board in board_array: | 
|  |  | 
|  | # 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 and board['name'] != board_name: | 
|  | # Not the board we're looking for, ignore. | 
|  | continue | 
|  |  | 
|  | 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') | 
|  | parser.add_argument("--fuzzy-match", default=None, | 
|  | help='lookup boards similar to the given board name') | 
|  |  | 
|  |  | 
|  | 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) | 
|  | if args.fuzzy_match is not None: | 
|  | close_boards = difflib.get_close_matches(args.fuzzy_match, boards.keys()) | 
|  | boards = {b: boards[b] for b in close_boards} | 
|  |  | 
|  | for b in boards.values(): | 
|  | qualifiers_list = board_v2_qualifiers(b) | 
|  | if args.cmakeformat is not None: | 
|  | def notfound(x): | 
|  | return 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}') | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | args = parse_args() | 
|  | dump_v2_boards(args) |