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