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