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