1#!/usr/bin/env python3
2
3# Copyright (c) 2020 Nordic Semiconductor ASA
4# SPDX-License-Identifier: Apache-2.0
5
6import argparse
7from collections import defaultdict
8from dataclasses import dataclass, field
9import itertools
10from pathlib import Path
11import pykwalify.core
12import sys
13from typing import List
14import yaml
15import list_hardware
16from list_hardware import unique_paths
17
18try:
19    from yaml import CSafeLoader as SafeLoader
20except ImportError:
21    from yaml import SafeLoader
22
23BOARD_SCHEMA_PATH = str(Path(__file__).parent / 'schemas' / 'board-schema.yml')
24with open(BOARD_SCHEMA_PATH, 'r') as f:
25    board_schema = yaml.load(f.read(), Loader=SafeLoader)
26
27BOARD_YML = 'board.yml'
28
29#
30# This is shared code between the build system's 'boards' target
31# and the 'west boards' extension command. If you change it, make
32# sure to test both ways it can be used.
33#
34# (It's done this way to keep west optional, making it possible to run
35# 'ninja boards' in a build directory without west installed.)
36#
37
38
39@dataclass
40class Revision:
41    name: str
42    variants: List[str] = field(default_factory=list)
43
44    @staticmethod
45    def from_dict(revision):
46        revisions = []
47        for r in revision.get('revisions', []):
48            revisions.append(Revision.from_dict(r))
49        return Revision(revision['name'], revisions)
50
51
52@dataclass
53class Variant:
54    name: str
55    variants: List[str] = field(default_factory=list)
56
57    @staticmethod
58    def from_dict(variant):
59        variants = []
60        for v in variant.get('variants', []):
61            variants.append(Variant.from_dict(v))
62        return Variant(variant['name'], variants)
63
64
65@dataclass
66class Cpucluster:
67    name: str
68    variants: List[str] = field(default_factory=list)
69
70
71@dataclass
72class Soc:
73    name: str
74    cpuclusters: List[str] = field(default_factory=list)
75    variants: List[str] = field(default_factory=list)
76
77    @staticmethod
78    def from_soc(soc, variants):
79        if soc is None:
80            return None
81        if soc.cpuclusters:
82            cpus = []
83            for c in soc.cpuclusters:
84                cpus.append(Cpucluster(c,
85                            [Variant.from_dict(v) for v in variants if c == v['cpucluster']]
86                ))
87            return Soc(soc.name, cpuclusters=cpus)
88        return Soc(soc.name, variants=[Variant.from_dict(v) for v in variants])
89
90
91@dataclass(frozen=True)
92class Board:
93    name: str
94    dir: Path
95    hwm: str
96    arch: str = None
97    vendor: str = None
98    revision_format: str = None
99    revision_default: str = None
100    revision_exact: bool = False
101    revisions: List[str] = field(default_factory=list, compare=False)
102    socs: List[Soc] = field(default_factory=list, compare=False)
103    variants: List[str] = field(default_factory=list, compare=False)
104
105
106def board_key(board):
107    return board.name
108
109
110def find_arch2boards(args):
111    arch2board_set = find_arch2board_set(args)
112    return {arch: sorted(arch2board_set[arch], key=board_key)
113            for arch in arch2board_set}
114
115
116def find_boards(args):
117    return sorted(itertools.chain(*find_arch2board_set(args).values()),
118                  key=board_key)
119
120
121def find_arch2board_set(args):
122    arches = sorted(find_arches(args))
123    ret = defaultdict(set)
124
125    for root in unique_paths(args.board_roots):
126        for arch, boards in find_arch2board_set_in(root, arches, args.board_dir).items():
127            if args.board is not None:
128                ret[arch] |= {b for b in boards if b.name == args.board}
129            else:
130                ret[arch] |= boards
131
132    return ret
133
134
135def find_arches(args):
136    arch_set = set()
137
138    for root in unique_paths(args.arch_roots):
139        arch_set |= find_arches_in(root)
140
141    return arch_set
142
143
144def find_arches_in(root):
145    ret = set()
146    arch = root / 'arch'
147    common = arch / 'common'
148
149    if not arch.is_dir():
150        return ret
151
152    for maybe_arch in arch.iterdir():
153        if not maybe_arch.is_dir() or maybe_arch == common:
154            continue
155        ret.add(maybe_arch.name)
156
157    return ret
158
159
160def find_arch2board_set_in(root, arches, board_dir):
161    ret = defaultdict(set)
162    boards = root / 'boards'
163
164    for arch in arches:
165        if not (boards / arch).is_dir():
166            continue
167
168        for maybe_board in (boards / arch).iterdir():
169            if not maybe_board.is_dir():
170                continue
171            if board_dir is not None and board_dir != maybe_board:
172                continue
173            for maybe_defconfig in maybe_board.iterdir():
174                file_name = maybe_defconfig.name
175                if file_name.endswith('_defconfig') and not (maybe_board / BOARD_YML).is_file():
176                    board_name = file_name[:-len('_defconfig')]
177                    ret[arch].add(Board(board_name, maybe_board, 'v1', arch=arch))
178
179    return ret
180
181
182def load_v2_boards(board_name, board_yml, systems):
183    boards = []
184    if board_yml.is_file():
185        with board_yml.open('r') as f:
186            b = yaml.load(f.read(), Loader=SafeLoader)
187
188        try:
189            pykwalify.core.Core(source_data=b, schema_data=board_schema).validate()
190        except pykwalify.errors.SchemaError as e:
191            sys.exit('ERROR: Malformed "build" section in file: {}\n{}'
192                     .format(board_yml.as_posix(), e))
193
194        mutual_exclusive = {'board', 'boards'}
195        if len(mutual_exclusive - b.keys()) < 1:
196            sys.exit(f'ERROR: Malformed content in file: {board_yml.as_posix()}\n'
197                     f'{mutual_exclusive} are mutual exclusive at this level.')
198
199        board_array = b.get('boards', [b.get('board', None)])
200        for board in board_array:
201            if board_name is not None:
202                if board['name'] != board_name:
203                    # Not the board we're looking for, ignore.
204                    continue
205
206            board_revision = board.get('revision')
207            if board_revision is not None and board_revision.get('format') != 'custom':
208                if board_revision.get('default') is None:
209                    sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
210                             "Cannot find required key 'default'. Path: '/board/revision.'")
211                if board_revision.get('revisions') is None:
212                    sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
213                             "Cannot find required key 'revisions'. Path: '/board/revision.'")
214
215            mutual_exclusive = {'socs', 'variants'}
216            if len(mutual_exclusive - board.keys()) < 1:
217                sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
218                         f'{mutual_exclusive} are mutual exclusive at this level.')
219            socs = [Soc.from_soc(systems.get_soc(s['name']), s.get('variants', []))
220                    for s in board.get('socs', {})]
221
222            board = Board(
223                name=board['name'],
224                dir=board_yml.parent,
225                vendor=board.get('vendor'),
226                revision_format=board.get('revision', {}).get('format'),
227                revision_default=board.get('revision', {}).get('default'),
228                revision_exact=board.get('revision', {}).get('exact', False),
229                revisions=[Revision.from_dict(v) for v in
230                           board.get('revision', {}).get('revisions', [])],
231                socs=socs,
232                variants=[Variant.from_dict(v) for v in board.get('variants', [])],
233                hwm='v2',
234            )
235            boards.append(board)
236    return boards
237
238
239# Note that this does not share the args.board functionality of find_v2_boards
240def find_v2_board_dirs(args):
241    dirs = []
242    board_files = []
243    for root in unique_paths(args.board_roots):
244        board_files.extend((root / 'boards').rglob(BOARD_YML))
245
246    dirs = [board_yml.parent for board_yml in board_files if board_yml.is_file()]
247    return dirs
248
249
250def find_v2_boards(args):
251    root_args = argparse.Namespace(**{'soc_roots': args.soc_roots})
252    systems = list_hardware.find_v2_systems(root_args)
253
254    boards = []
255    board_files = []
256    for root in unique_paths(args.board_roots):
257        board_files.extend((root / 'boards').rglob(BOARD_YML))
258
259    for board_yml in board_files:
260        b = load_v2_boards(args.board, board_yml, systems)
261        boards.extend(b)
262    return boards
263
264
265def parse_args():
266    parser = argparse.ArgumentParser(allow_abbrev=False)
267    add_args(parser)
268    add_args_formatting(parser)
269    return parser.parse_args()
270
271
272def add_args(parser):
273    # Remember to update west-completion.bash if you add or remove
274    # flags
275    parser.add_argument("--arch-root", dest='arch_roots', default=[],
276                        type=Path, action='append',
277                        help='add an architecture root, may be given more than once')
278    parser.add_argument("--board-root", dest='board_roots', default=[],
279                        type=Path, action='append',
280                        help='add a board root, may be given more than once')
281    parser.add_argument("--soc-root", dest='soc_roots', default=[],
282                        type=Path, action='append',
283                        help='add a soc root, may be given more than once')
284    parser.add_argument("--board", dest='board', default=None,
285                        help='lookup the specific board, fail if not found')
286    parser.add_argument("--board-dir", default=None, type=Path,
287                        help='Only look for boards at the specific location')
288
289
290def add_args_formatting(parser):
291    parser.add_argument("--cmakeformat", default=None,
292                        help='''CMake Format string to use to list each board''')
293
294
295def variant_v2_qualifiers(variant, qualifiers = None):
296    qualifiers_list = [variant.name] if qualifiers is None else [qualifiers + '/' + variant.name]
297    for v in variant.variants:
298        qualifiers_list.extend(variant_v2_qualifiers(v, qualifiers_list[0]))
299    return qualifiers_list
300
301
302def board_v2_qualifiers(board):
303    qualifiers_list = []
304
305    for s in board.socs:
306        if s.cpuclusters:
307            for c in s.cpuclusters:
308                id_str = s.name + '/' + c.name
309                qualifiers_list.append(id_str)
310                for v in c.variants:
311                    qualifiers_list.extend(variant_v2_qualifiers(v, id_str))
312        else:
313            qualifiers_list.append(s.name)
314            for v in s.variants:
315                qualifiers_list.extend(variant_v2_qualifiers(v, s.name))
316
317    for v in board.variants:
318        qualifiers_list.extend(variant_v2_qualifiers(v))
319    return qualifiers_list
320
321
322def board_v2_qualifiers_csv(board):
323    # Return in csv (comma separated value) format
324    return ",".join(board_v2_qualifiers(board))
325
326
327def dump_v2_boards(args):
328    if args.board_dir:
329        root_args = argparse.Namespace(**{'soc_roots': args.soc_roots})
330        systems = list_hardware.find_v2_systems(root_args)
331        boards = load_v2_boards(args.board, args.board_dir / BOARD_YML, systems)
332    else:
333        boards = find_v2_boards(args)
334
335    for b in boards:
336        qualifiers_list = board_v2_qualifiers(b)
337        if args.cmakeformat is not None:
338            notfound = lambda x: x or 'NOTFOUND'
339            info = args.cmakeformat.format(
340                NAME='NAME;' + b.name,
341                DIR='DIR;' + str(b.dir.as_posix()),
342                VENDOR='VENDOR;' + notfound(b.vendor),
343                HWM='HWM;' + b.hwm,
344                REVISION_DEFAULT='REVISION_DEFAULT;' + notfound(b.revision_default),
345                REVISION_FORMAT='REVISION_FORMAT;' + notfound(b.revision_format),
346                REVISION_EXACT='REVISION_EXACT;' + str(b.revision_exact),
347                REVISIONS='REVISIONS;' + ';'.join(
348                          [x.name for x in b.revisions]),
349                SOCS='SOCS;' + ';'.join([s.name for s in b.socs]),
350                QUALIFIERS='QUALIFIERS;' + ';'.join(qualifiers_list)
351            )
352            print(info)
353        else:
354            print(f'{b.name}')
355
356
357def dump_boards(args):
358    arch2boards = find_arch2boards(args)
359    for arch, boards in arch2boards.items():
360        if args.cmakeformat is None:
361            print(f'{arch}:')
362        for board in boards:
363            if args.cmakeformat is not None:
364                info = args.cmakeformat.format(
365                    NAME='NAME;' + board.name,
366                    DIR='DIR;' + str(board.dir.as_posix()),
367                    HWM='HWM;' + board.hwm,
368                    VENDOR='VENDOR;NOTFOUND',
369                    REVISION_DEFAULT='REVISION_DEFAULT;NOTFOUND',
370                    REVISION_FORMAT='REVISION_FORMAT;NOTFOUND',
371                    REVISION_EXACT='REVISION_EXACT;NOTFOUND',
372                    REVISIONS='REVISIONS;NOTFOUND',
373                    VARIANT_DEFAULT='VARIANT_DEFAULT;NOTFOUND',
374                    SOCS='SOCS;',
375                    QUALIFIERS='QUALIFIERS;'
376                )
377                print(info)
378            else:
379                print(f'  {board.name}')
380
381
382if __name__ == '__main__':
383    args = parse_args()
384    dump_boards(args)
385    dump_v2_boards(args)
386