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, Counter
8from dataclasses import dataclass, field
9import itertools
10from pathlib import Path
11import pykwalify.core
12import sys
13from typing import List, Union
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    # HWMv1 only supports a single Path, and requires Board dataclass to be hashable.
95    directories: Union[Path, List[Path]]
96    hwm: str
97    full_name: str = None
98    arch: str = None
99    vendor: str = None
100    revision_format: str = None
101    revision_default: str = None
102    revision_exact: bool = False
103    revisions: List[str] = field(default_factory=list, compare=False)
104    socs: List[Soc] = field(default_factory=list, compare=False)
105    variants: List[str] = field(default_factory=list, compare=False)
106
107    @property
108    def dir(self):
109        # Get the main board directory.
110        if isinstance(self.directories, Path):
111            return self.directories
112        return self.directories[0]
113
114    def from_qualifier(self, qualifiers):
115        qualifiers_list = qualifiers.split('/')
116
117        node = Soc(None)
118        n = len(qualifiers_list)
119        if n > 0:
120            soc_qualifier = qualifiers_list.pop(0)
121            for s in self.socs:
122                if s.name == soc_qualifier:
123                    node = s
124                    break
125
126        if n > 1:
127            if node.cpuclusters:
128                cpu_qualifier = qualifiers_list.pop(0)
129                for c in node.cpuclusters:
130                    if c.name == cpu_qualifier:
131                        node = c
132                        break
133                else:
134                    node = Variant(None)
135
136        for q in qualifiers_list:
137            for v in node.variants:
138                if v.name == q:
139                    node = v
140                    break
141            else:
142                node = Variant(None)
143
144        if node in (Soc(None), Variant(None)):
145            sys.exit(f'ERROR: qualifiers {qualifiers} not found when extending board {self.name}')
146
147        return node
148
149
150def board_key(board):
151    return board.name
152
153
154def find_arch2boards(args):
155    arch2board_set = find_arch2board_set(args)
156    return {arch: sorted(arch2board_set[arch], key=board_key)
157            for arch in arch2board_set}
158
159
160def find_boards(args):
161    return sorted(itertools.chain(*find_arch2board_set(args).values()),
162                  key=board_key)
163
164
165def find_arch2board_set(args):
166    arches = sorted(find_arches(args))
167    ret = defaultdict(set)
168
169    for root in unique_paths(args.board_roots):
170        for arch, boards in find_arch2board_set_in(root, arches, args.board_dir).items():
171            if args.board is not None:
172                ret[arch] |= {b for b in boards if b.name == args.board}
173            else:
174                ret[arch] |= boards
175
176    return ret
177
178
179def find_arches(args):
180    arch_set = set()
181
182    for root in unique_paths(args.arch_roots):
183        arch_set |= find_arches_in(root)
184
185    return arch_set
186
187
188def find_arches_in(root):
189    ret = set()
190    arch = root / 'arch'
191    common = arch / 'common'
192
193    if not arch.is_dir():
194        return ret
195
196    for maybe_arch in arch.iterdir():
197        if not maybe_arch.is_dir() or maybe_arch == common:
198            continue
199        ret.add(maybe_arch.name)
200
201    return ret
202
203
204def find_arch2board_set_in(root, arches, board_dir):
205    ret = defaultdict(set)
206    boards = root / 'boards'
207
208    for arch in arches:
209        if not (boards / arch).is_dir():
210            continue
211        for maybe_board in (boards / arch).iterdir():
212            if not maybe_board.is_dir():
213                continue
214            if board_dir and maybe_board not in board_dir:
215                continue
216            for maybe_defconfig in maybe_board.iterdir():
217                file_name = maybe_defconfig.name
218                if file_name.endswith('_defconfig') and not (maybe_board / BOARD_YML).is_file():
219                    board_name = file_name[:-len('_defconfig')]
220                    ret[arch].add(Board(board_name, maybe_board, 'v1', arch=arch))
221
222    return ret
223
224
225def load_v2_boards(board_name, board_yml, systems):
226    boards = {}
227    board_extensions = []
228    if board_yml.is_file():
229        with board_yml.open('r', encoding='utf-8') as f:
230            b = yaml.load(f.read(), Loader=SafeLoader)
231
232        try:
233            pykwalify.core.Core(source_data=b, schema_data=board_schema).validate()
234        except pykwalify.errors.SchemaError as e:
235            sys.exit('ERROR: Malformed "build" section in file: {}\n{}'
236                     .format(board_yml.as_posix(), e))
237
238        mutual_exclusive = {'board', 'boards'}
239        if len(mutual_exclusive - b.keys()) < 1:
240            sys.exit(f'ERROR: Malformed content in file: {board_yml.as_posix()}\n'
241                     f'{mutual_exclusive} are mutual exclusive at this level.')
242
243        board_array = b.get('boards', [b.get('board', None)])
244        for board in board_array:
245            mutual_exclusive = {'name', 'extend'}
246            if len(mutual_exclusive - board.keys()) < 1:
247                sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
248                         f'{mutual_exclusive} are mutual exclusive at this level.')
249
250            # This is a extending an existing board, place in array to allow later processing.
251            if 'extend' in board:
252                board.update({'dir': board_yml.parent})
253                board_extensions.append(board)
254                continue
255
256            # Create board
257            if board_name is not None:
258                if board['name'] != board_name:
259                    # Not the board we're looking for, ignore.
260                    continue
261
262            board_revision = board.get('revision')
263            if board_revision is not None and board_revision.get('format') != 'custom':
264                if board_revision.get('default') is None:
265                    sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
266                             "Cannot find required key 'default'. Path: '/board/revision.'")
267                if board_revision.get('revisions') is None:
268                    sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
269                             "Cannot find required key 'revisions'. Path: '/board/revision.'")
270
271            mutual_exclusive = {'socs', 'variants'}
272            if len(mutual_exclusive - board.keys()) < 1:
273                sys.exit(f'ERROR: Malformed "board" section in file: {board_yml.as_posix()}\n'
274                         f'{mutual_exclusive} are mutual exclusive at this level.')
275            socs = [Soc.from_soc(systems.get_soc(s['name']), s.get('variants', []))
276                    for s in board.get('socs', {})]
277
278            boards[board['name']] = Board(
279                name=board['name'],
280                directories=[board_yml.parent],
281                vendor=board.get('vendor'),
282                full_name=board.get('full_name'),
283                revision_format=board.get('revision', {}).get('format'),
284                revision_default=board.get('revision', {}).get('default'),
285                revision_exact=board.get('revision', {}).get('exact', False),
286                revisions=[Revision.from_dict(v) for v in
287                           board.get('revision', {}).get('revisions', [])],
288                socs=socs,
289                variants=[Variant.from_dict(v) for v in board.get('variants', [])],
290                hwm='v2',
291            )
292            board_qualifiers = board_v2_qualifiers(boards[board['name']])
293            duplicates = [q for q, n in Counter(board_qualifiers).items() if n > 1]
294            if duplicates:
295                sys.exit(f'ERROR: Duplicated board qualifiers detected {duplicates} for board: '
296                         f'{board["name"]}.\nPlease check content of: {board_yml.as_posix()}\n')
297    return boards, board_extensions
298
299
300def extend_v2_boards(boards, board_extensions):
301    for e in board_extensions:
302        board = boards.get(e['extend'])
303        if board is None:
304            continue
305        board.directories.append(e['dir'])
306
307        for v in e.get('variants', []):
308            node = board.from_qualifier(v['qualifier'])
309            if str(v['qualifier'] + '/' + v['name']) in board_v2_qualifiers(board):
310                board_yml = e['dir'] / BOARD_YML
311                sys.exit(f'ERROR: Variant: {v["name"]}, defined multiple times for board: '
312                         f'{board.name}.\nLast defined in {board_yml}')
313            node.variants.append(Variant.from_dict(v))
314
315
316# Note that this does not share the args.board functionality of find_v2_boards
317def find_v2_board_dirs(args):
318    dirs = []
319    board_files = []
320    for root in unique_paths(args.board_roots):
321        board_files.extend((root / 'boards').rglob(BOARD_YML))
322
323    dirs = [board_yml.parent for board_yml in board_files if board_yml.is_file()]
324    return dirs
325
326
327def find_v2_boards(args):
328    root_args = argparse.Namespace(**{'soc_roots': args.soc_roots})
329    systems = list_hardware.find_v2_systems(root_args)
330
331    boards = {}
332    board_extensions = []
333    board_files = []
334    if args.board_dir:
335        board_files = [d / BOARD_YML for d in args.board_dir]
336    else:
337        for root in unique_paths(args.board_roots):
338            board_files.extend((root / 'boards').rglob(BOARD_YML))
339
340    for board_yml in board_files:
341        b, e = load_v2_boards(args.board, board_yml, systems)
342        conflict_boards = set(boards.keys()).intersection(b.keys())
343        if conflict_boards:
344            sys.exit(f'ERROR: Board(s): {conflict_boards}, defined multiple times.\n'
345                     f'Last defined in {board_yml}')
346        boards.update(b)
347        board_extensions.extend(e)
348
349    extend_v2_boards(boards, board_extensions)
350    return boards
351
352
353def parse_args():
354    parser = argparse.ArgumentParser(allow_abbrev=False)
355    add_args(parser)
356    add_args_formatting(parser)
357    return parser.parse_args()
358
359
360def add_args(parser):
361    # Remember to update west-completion.bash if you add or remove
362    # flags
363    parser.add_argument("--arch-root", dest='arch_roots', default=[],
364                        type=Path, action='append',
365                        help='add an architecture root, may be given more than once')
366    parser.add_argument("--board-root", dest='board_roots', default=[],
367                        type=Path, action='append',
368                        help='add a board root, may be given more than once')
369    parser.add_argument("--soc-root", dest='soc_roots', default=[],
370                        type=Path, action='append',
371                        help='add a soc root, may be given more than once')
372    parser.add_argument("--board", dest='board', default=None,
373                        help='lookup the specific board, fail if not found')
374    parser.add_argument("--board-dir", default=[], type=Path, action='append',
375                        help='Only look for boards at the specific location')
376
377
378def add_args_formatting(parser):
379    parser.add_argument("--cmakeformat", default=None,
380                        help='''CMake Format string to use to list each board''')
381
382
383def variant_v2_qualifiers(variant, qualifiers = None):
384    qualifiers_list = [variant.name] if qualifiers is None else [qualifiers + '/' + variant.name]
385    for v in variant.variants:
386        qualifiers_list.extend(variant_v2_qualifiers(v, qualifiers_list[0]))
387    return qualifiers_list
388
389
390def board_v2_qualifiers(board):
391    qualifiers_list = []
392
393    for s in board.socs:
394        if s.cpuclusters:
395            for c in s.cpuclusters:
396                id_str = s.name + '/' + c.name
397                qualifiers_list.append(id_str)
398                for v in c.variants:
399                    qualifiers_list.extend(variant_v2_qualifiers(v, id_str))
400        else:
401            qualifiers_list.append(s.name)
402            for v in s.variants:
403                qualifiers_list.extend(variant_v2_qualifiers(v, s.name))
404
405    for v in board.variants:
406        qualifiers_list.extend(variant_v2_qualifiers(v))
407    return qualifiers_list
408
409
410def board_v2_qualifiers_csv(board):
411    # Return in csv (comma separated value) format
412    return ",".join(board_v2_qualifiers(board))
413
414
415def dump_v2_boards(args):
416    boards = find_v2_boards(args)
417
418    for b in boards.values():
419        qualifiers_list = board_v2_qualifiers(b)
420        if args.cmakeformat is not None:
421            notfound = lambda x: x or 'NOTFOUND'
422            info = args.cmakeformat.format(
423                NAME='NAME;' + b.name,
424                DIR='DIR;' + ';'.join(
425                    [str(x.as_posix()) for x in b.directories]),
426                VENDOR='VENDOR;' + notfound(b.vendor),
427                HWM='HWM;' + b.hwm,
428                REVISION_DEFAULT='REVISION_DEFAULT;' + notfound(b.revision_default),
429                REVISION_FORMAT='REVISION_FORMAT;' + notfound(b.revision_format),
430                REVISION_EXACT='REVISION_EXACT;' + str(b.revision_exact),
431                REVISIONS='REVISIONS;' + ';'.join(
432                          [x.name for x in b.revisions]),
433                SOCS='SOCS;' + ';'.join([s.name for s in b.socs]),
434                QUALIFIERS='QUALIFIERS;' + ';'.join(qualifiers_list)
435            )
436            print(info)
437        else:
438            print(f'{b.name}')
439
440
441def dump_boards(args):
442    arch2boards = find_arch2boards(args)
443    for arch, boards in arch2boards.items():
444        if args.cmakeformat is None:
445            print(f'{arch}:')
446        for board in boards:
447            if args.cmakeformat is not None:
448                info = args.cmakeformat.format(
449                    NAME='NAME;' + board.name,
450                    DIR='DIR;' + str(board.dir.as_posix()),
451                    HWM='HWM;' + board.hwm,
452                    VENDOR='VENDOR;NOTFOUND',
453                    REVISION_DEFAULT='REVISION_DEFAULT;NOTFOUND',
454                    REVISION_FORMAT='REVISION_FORMAT;NOTFOUND',
455                    REVISION_EXACT='REVISION_EXACT;NOTFOUND',
456                    REVISIONS='REVISIONS;NOTFOUND',
457                    VARIANT_DEFAULT='VARIANT_DEFAULT;NOTFOUND',
458                    SOCS='SOCS;',
459                    QUALIFIERS='QUALIFIERS;'
460                )
461                print(info)
462            else:
463                print(f'  {board.name}')
464
465
466if __name__ == '__main__':
467    args = parse_args()
468    dump_boards(args)
469    dump_v2_boards(args)
470