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 sys 9from collections import Counter 10from dataclasses import dataclass, field 11from pathlib import Path 12 13import jsonschema 14import list_hardware 15import yaml 16from jsonschema.exceptions import best_match 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.yaml') 25with open(BOARD_SCHEMA_PATH) as f: 26 board_schema = yaml.load(f.read(), Loader=SafeLoader) 27 28validator_class = jsonschema.validators.validator_for(board_schema) 29validator_class.check_schema(board_schema) 30board_validator = validator_class(board_schema) 31 32BOARD_YML = 'board.yml' 33 34# 35# This is shared code between the build system's 'boards' target 36# and the 'west boards' extension command. If you change it, make 37# sure to test both ways it can be used. 38# 39# (It's done this way to keep west optional, making it possible to run 40# 'ninja boards' in a build directory without west installed.) 41# 42 43 44@dataclass 45class Revision: 46 name: str 47 variants: list[str] = field(default_factory=list) 48 49 @staticmethod 50 def from_dict(revision): 51 revisions = [] 52 for r in revision.get('revisions', []): 53 revisions.append(Revision.from_dict(r)) 54 return Revision(revision['name'], revisions) 55 56 57@dataclass 58class Variant: 59 name: str 60 variants: list[str] = field(default_factory=list) 61 62 @staticmethod 63 def from_dict(variant): 64 variants = [] 65 for v in variant.get('variants', []): 66 variants.append(Variant.from_dict(v)) 67 return Variant(variant['name'], variants) 68 69 70@dataclass 71class Cpucluster: 72 name: str 73 variants: list[str] = field(default_factory=list) 74 75 76@dataclass 77class Soc: 78 name: str 79 cpuclusters: list[str] = field(default_factory=list) 80 variants: list[str] = field(default_factory=list) 81 82 @staticmethod 83 def from_soc(soc, variants): 84 if soc is None: 85 return None 86 if soc.cpuclusters: 87 cpus = [] 88 for c in soc.cpuclusters: 89 cpus.append(Cpucluster(c, 90 [Variant.from_dict(v) for v in variants if c == v['cpucluster']] 91 )) 92 return Soc(soc.name, cpuclusters=cpus) 93 return Soc(soc.name, variants=[Variant.from_dict(v) for v in variants]) 94 95 96@dataclass(frozen=True) 97class Board: 98 name: str 99 # HWMv1 only supports a single Path, and requires Board dataclass to be hashable. 100 directories: Path | list[Path] 101 hwm: str 102 full_name: str = None 103 arch: str = None 104 vendor: str = None 105 revision_format: str = None 106 revision_default: str = None 107 revision_exact: bool = False 108 revisions: list[str] = field(default_factory=list, compare=False) 109 socs: list[Soc] = field(default_factory=list, compare=False) 110 variants: list[str] = field(default_factory=list, compare=False) 111 112 @property 113 def dir(self): 114 # Get the main board directory. 115 if isinstance(self.directories, Path): 116 return self.directories 117 return self.directories[0] 118 119 def from_qualifier(self, qualifiers): 120 qualifiers_list = qualifiers.split('/') 121 122 node = Soc(None) 123 n = len(qualifiers_list) 124 if n > 0: 125 soc_qualifier = qualifiers_list.pop(0) 126 for s in self.socs: 127 if s.name == soc_qualifier: 128 node = s 129 break 130 131 if n > 1 and node.cpuclusters: 132 cpu_qualifier = qualifiers_list.pop(0) 133 for c in node.cpuclusters: 134 if c.name == cpu_qualifier: 135 node = c 136 break 137 else: 138 node = Variant(None) 139 140 for q in qualifiers_list: 141 for v in node.variants: 142 if v.name == q: 143 node = v 144 break 145 else: 146 node = Variant(None) 147 148 if node in (Soc(None), Variant(None)): 149 sys.exit(f'ERROR: qualifiers {qualifiers} not found when extending board {self.name}') 150 151 return node 152 153 154def load_v2_boards(board_name, board_yml, systems): 155 boards = {} 156 board_extensions = [] 157 if board_yml.is_file(): 158 with board_yml.open('r', encoding='utf-8') as f: 159 b = yaml.load(f.read(), Loader=SafeLoader) 160 161 errors = list(board_validator.iter_errors(b)) 162 if errors: 163 sys.exit('ERROR: Malformed board YAML file: ' 164 f'{board_yml.as_posix()}\n' 165 f'{best_match(errors).message} in {best_match(errors).json_path}') 166 167 board_array = b.get('boards', [b.get('board', None)]) 168 for board in board_array: 169 170 # This is a extending an existing board, place in array to allow later processing. 171 if 'extend' in board: 172 board.update({'dir': board_yml.parent}) 173 board_extensions.append(board) 174 continue 175 176 # Create board 177 if board_name is not None and board['name'] != board_name: 178 # Not the board we're looking for, ignore. 179 continue 180 181 socs = [Soc.from_soc(systems.get_soc(s['name']), s.get('variants', [])) 182 for s in board.get('socs', {})] 183 184 boards[board['name']] = Board( 185 name=board['name'], 186 directories=[board_yml.parent], 187 vendor=board.get('vendor'), 188 full_name=board.get('full_name'), 189 revision_format=board.get('revision', {}).get('format'), 190 revision_default=board.get('revision', {}).get('default'), 191 revision_exact=board.get('revision', {}).get('exact', False), 192 revisions=[Revision.from_dict(v) for v in 193 board.get('revision', {}).get('revisions', [])], 194 socs=socs, 195 variants=[Variant.from_dict(v) for v in board.get('variants', [])], 196 hwm='v2', 197 ) 198 board_qualifiers = board_v2_qualifiers(boards[board['name']]) 199 duplicates = [q for q, n in Counter(board_qualifiers).items() if n > 1] 200 if duplicates: 201 sys.exit(f'ERROR: Duplicated board qualifiers detected {duplicates} for board: ' 202 f'{board["name"]}.\nPlease check content of: {board_yml.as_posix()}\n') 203 return boards, board_extensions 204 205 206def extend_v2_boards(boards, board_extensions): 207 for e in board_extensions: 208 board = boards.get(e['extend']) 209 if board is None: 210 continue 211 board.directories.append(e['dir']) 212 213 for v in e.get('variants', []): 214 node = board.from_qualifier(v['qualifier']) 215 if str(v['qualifier'] + '/' + v['name']) in board_v2_qualifiers(board): 216 board_yml = e['dir'] / BOARD_YML 217 sys.exit(f'ERROR: Variant: {v["name"]}, defined multiple times for board: ' 218 f'{board.name}.\nLast defined in {board_yml}') 219 node.variants.append(Variant.from_dict(v)) 220 221 222# Note that this does not share the args.board functionality of find_v2_boards 223def find_v2_board_dirs(args): 224 dirs = [] 225 board_files = [] 226 for root in unique_paths(args.board_roots): 227 board_files.extend((root / 'boards').rglob(BOARD_YML)) 228 229 dirs = [board_yml.parent for board_yml in board_files if board_yml.is_file()] 230 return dirs 231 232 233def find_v2_boards(args): 234 root_args = argparse.Namespace(**{'soc_roots': args.soc_roots}) 235 systems = list_hardware.find_v2_systems(root_args) 236 237 boards = {} 238 board_extensions = [] 239 board_files = [] 240 if args.board_dir: 241 board_files = [d / BOARD_YML for d in args.board_dir] 242 else: 243 for root in unique_paths(args.board_roots): 244 board_files.extend((root / 'boards').rglob(BOARD_YML)) 245 246 for board_yml in board_files: 247 b, e = load_v2_boards(args.board, board_yml, systems) 248 conflict_boards = set(boards.keys()).intersection(b.keys()) 249 if conflict_boards: 250 sys.exit(f'ERROR: Board(s): {conflict_boards}, defined multiple times.\n' 251 f'Last defined in {board_yml}') 252 boards.update(b) 253 board_extensions.extend(e) 254 255 extend_v2_boards(boards, board_extensions) 256 return boards 257 258 259def parse_args(): 260 parser = argparse.ArgumentParser(allow_abbrev=False) 261 add_args(parser) 262 add_args_formatting(parser) 263 return parser.parse_args() 264 265 266def add_args(parser): 267 # Remember to update west-completion.bash if you add or remove 268 # flags 269 parser.add_argument("--arch-root", dest='arch_roots', default=[], 270 type=Path, action='append', 271 help='add an architecture root, may be given more than once') 272 parser.add_argument("--board-root", dest='board_roots', default=[], 273 type=Path, action='append', 274 help='add a board root, may be given more than once') 275 parser.add_argument("--soc-root", dest='soc_roots', default=[], 276 type=Path, action='append', 277 help='add a soc root, may be given more than once') 278 parser.add_argument("--board", dest='board', default=None, 279 help='lookup the specific board, fail if not found') 280 parser.add_argument("--board-dir", default=[], type=Path, action='append', 281 help='only look for boards at the specific location') 282 parser.add_argument("--fuzzy-match", default=None, 283 help='lookup boards similar to the given board name') 284 285 286def add_args_formatting(parser): 287 parser.add_argument("--cmakeformat", default=None, 288 help='''CMake Format string to use to list each board''') 289 290 291def variant_v2_qualifiers(variant, qualifiers = None): 292 qualifiers_list = [variant.name] if qualifiers is None else [qualifiers + '/' + variant.name] 293 for v in variant.variants: 294 qualifiers_list.extend(variant_v2_qualifiers(v, qualifiers_list[0])) 295 return qualifiers_list 296 297 298def board_v2_qualifiers(board): 299 qualifiers_list = [] 300 301 for s in board.socs: 302 if s.cpuclusters: 303 for c in s.cpuclusters: 304 id_str = s.name + '/' + c.name 305 qualifiers_list.append(id_str) 306 for v in c.variants: 307 qualifiers_list.extend(variant_v2_qualifiers(v, id_str)) 308 else: 309 qualifiers_list.append(s.name) 310 for v in s.variants: 311 qualifiers_list.extend(variant_v2_qualifiers(v, s.name)) 312 313 for v in board.variants: 314 qualifiers_list.extend(variant_v2_qualifiers(v)) 315 return qualifiers_list 316 317 318def board_v2_qualifiers_csv(board): 319 # Return in csv (comma separated value) format 320 return ",".join(board_v2_qualifiers(board)) 321 322 323def dump_v2_boards(args): 324 boards = find_v2_boards(args) 325 if args.fuzzy_match is not None: 326 close_boards = difflib.get_close_matches(args.fuzzy_match, boards.keys()) 327 boards = {b: boards[b] for b in close_boards} 328 329 for b in boards.values(): 330 qualifiers_list = board_v2_qualifiers(b) 331 if args.cmakeformat is not None: 332 def notfound(x): 333 return x or 'NOTFOUND' 334 info = args.cmakeformat.format( 335 NAME='NAME;' + b.name, 336 DIR='DIR;' + ';'.join( 337 [str(x.as_posix()) for x in b.directories]), 338 VENDOR='VENDOR;' + notfound(b.vendor), 339 HWM='HWM;' + b.hwm, 340 REVISION_DEFAULT='REVISION_DEFAULT;' + notfound(b.revision_default), 341 REVISION_FORMAT='REVISION_FORMAT;' + notfound(b.revision_format), 342 REVISION_EXACT='REVISION_EXACT;' + str(b.revision_exact), 343 REVISIONS='REVISIONS;' + ';'.join( 344 [x.name for x in b.revisions]), 345 SOCS='SOCS;' + ';'.join([s.name for s in b.socs]), 346 QUALIFIERS='QUALIFIERS;' + ';'.join(qualifiers_list) 347 ) 348 print(info) 349 else: 350 print(f'{b.name}') 351 352 353if __name__ == '__main__': 354 args = parse_args() 355 dump_v2_boards(args) 356