Loading...
#!/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)