1#!/usr/bin/env python3 2 3# Copyright (c) 2023 Nordic Semiconductor ASA 4# SPDX-License-Identifier: Apache-2.0 5 6import argparse 7import re 8import sys 9from dataclasses import dataclass 10from pathlib import Path, PurePath 11 12import jsonschema 13import yaml 14from jsonschema.exceptions import best_match 15 16try: 17 from yaml import CSafeLoader as SafeLoader 18except ImportError: 19 from yaml import SafeLoader 20 21 22SOC_SCHEMA_PATH = str(Path(__file__).parent / 'schemas' / 'soc-schema.yaml') 23with open(SOC_SCHEMA_PATH) as f: 24 soc_schema = yaml.load(f.read(), Loader=SafeLoader) 25 26ARCH_SCHEMA_PATH = str(Path(__file__).parent / 'schemas' / 'arch-schema.yaml') 27with open(ARCH_SCHEMA_PATH) as f: 28 arch_schema = yaml.load(f.read(), Loader=SafeLoader) 29 30validator_class = jsonschema.validators.validator_for(soc_schema) 31validator_class.check_schema(soc_schema) 32soc_validator = validator_class(soc_schema) 33 34validator_class = jsonschema.validators.validator_for(arch_schema) 35validator_class.check_schema(arch_schema) 36arch_validator = validator_class(arch_schema) 37 38SOC_YML = 'soc.yml' 39ARCHS_YML_PATH = PurePath('arch/archs.yml') 40 41class Systems: 42 43 def __init__(self, folder='', soc_yaml=None): 44 self._socs = [] 45 self._series = [] 46 self._families = [] 47 self._extended_socs = [] 48 49 if soc_yaml is None: 50 return 51 52 data = yaml.load(soc_yaml, Loader=SafeLoader) 53 errors = list(soc_validator.iter_errors(data)) 54 if errors: 55 sys.exit('ERROR: Malformed soc YAML file: \n' 56 f'{soc_yaml}\n' 57 f'{best_match(errors).message} in {best_match(errors).json_path}') 58 59 for f in data.get('family', []): 60 family = Family(f['name'], [folder], [], []) 61 for s in f.get('series', []): 62 series = Series(s['name'], [folder], f['name'], []) 63 socs = [(Soc(soc['name'], 64 [c['name'] for c in soc.get('cpuclusters', [])], 65 [folder], s['name'], f['name'])) 66 for soc in s.get('socs', [])] 67 series.socs.extend(socs) 68 self._series.append(series) 69 self._socs.extend(socs) 70 family.series.append(series) 71 family.socs.extend(socs) 72 socs = [(Soc(soc['name'], 73 [c['name'] for c in soc.get('cpuclusters', [])], 74 [folder], None, f['name'])) 75 for soc in f.get('socs', [])] 76 self._socs.extend(socs) 77 self._families.append(family) 78 79 for s in data.get('series', []): 80 series = Series(s['name'], [folder], '', []) 81 socs = [(Soc(soc['name'], 82 [c['name'] for c in soc.get('cpuclusters', [])], 83 [folder], s['name'], '')) 84 for soc in s.get('socs', [])] 85 series.socs.extend(socs) 86 self._series.append(series) 87 self._socs.extend(socs) 88 89 for soc in data.get('socs', []): 90 if soc.get('name') is not None: 91 self._socs.append(Soc(soc['name'], [c['name'] for c in soc.get('cpuclusters', [])], 92 [folder], '', '')) 93 elif soc.get('extend') is not None: 94 self._extended_socs.append(Soc(soc['extend'], 95 [c['name'] for c in soc.get('cpuclusters', [])], 96 [folder], '', '')) 97 else: 98 # This should not happen if schema validation passed 99 sys.exit(f'ERROR: Malformed "socs" section in SoC file: {soc_yaml}\n' 100 f'SoC entry must have either "name" or "extend" property.') 101 102 # Ensure that any runner configuration matches socs and cpuclusters declared in the same 103 # soc.yml file 104 if 'runners' in data and 'run_once' in data['runners']: 105 for grp in data['runners']['run_once']: 106 for item_data in data['runners']['run_once'][grp]: 107 for group in item_data['groups']: 108 for qualifiers in group['qualifiers']: 109 soc_name = qualifiers.split('/')[0] 110 found_match = False 111 112 for soc in self._socs + self._extended_socs: 113 if re.match(fr'^{soc_name}$', soc.name) is not None: 114 found_match = True 115 break 116 117 if found_match is False: 118 sys.exit(f'ERROR: SoC qualifier match unresolved: {qualifiers}') 119 120 @staticmethod 121 def from_file(socs_file): 122 '''Load SoCs from a soc.yml file. 123 ''' 124 try: 125 with open(socs_file) as f: 126 socs_yaml = f.read() 127 except FileNotFoundError as e: 128 sys.exit(f'ERROR: socs.yml file not found: {socs_file.as_posix()}', e) 129 130 return Systems(str(socs_file.parent), socs_yaml) 131 132 @staticmethod 133 def from_yaml(socs_yaml): 134 '''Load socs from a string with YAML contents. 135 ''' 136 return Systems('', socs_yaml) 137 138 def extend(self, systems): 139 self._families.extend(systems.get_families()) 140 self._series.extend(systems.get_series()) 141 142 for es in self._extended_socs[:]: 143 for s in systems.get_socs(): 144 if s.name == es.name: 145 s.extend(es) 146 self._extended_socs.remove(es) 147 break 148 self._socs.extend(systems.get_socs()) 149 150 for es in systems.get_extended_socs(): 151 for s in self._socs: 152 if s.name == es.name: 153 s.extend(es) 154 break 155 else: 156 self._extended_socs.append(es) 157 158 def get_families(self): 159 return self._families 160 161 def get_series(self): 162 return self._series 163 164 def get_socs(self): 165 return self._socs 166 167 def get_extended_socs(self): 168 return self._extended_socs 169 170 def get_soc(self, name): 171 try: 172 return next(s for s in self._socs if s.name == name) 173 except StopIteration: 174 sys.exit(f"ERROR: SoC '{name}' is not found, please ensure that the SoC exists " 175 f"and that soc-root containing '{name}' has been correctly defined.") 176 177 178@dataclass 179class Soc: 180 name: str 181 cpuclusters: list[str] 182 folder: list[str] 183 series: str = '' 184 family: str = '' 185 186 def extend(self, soc): 187 if self.name == soc.name: 188 self.cpuclusters.extend(soc.cpuclusters) 189 self.folder.extend(soc.folder) 190 191 192@dataclass 193class Series: 194 name: str 195 folder: list[str] 196 family: str 197 socs: list[Soc] 198 199 200@dataclass 201class Family: 202 name: str 203 folder: list[str] 204 series: list[Series] 205 socs: list[Soc] 206 207 208def unique_paths(paths): 209 # Using dict keys ensures both uniqueness and a deterministic order. 210 yield from dict.fromkeys(map(Path.resolve, paths)).keys() 211 212 213def find_v2_archs(args): 214 ret = {'archs': []} 215 for root in unique_paths(args.arch_roots): 216 archs_yml = root / ARCHS_YML_PATH 217 218 if Path(archs_yml).is_file(): 219 with Path(archs_yml).open('r', encoding='utf-8') as f: 220 archs = yaml.load(f.read(), Loader=SafeLoader) 221 222 errors = list(arch_validator.iter_errors(archs)) 223 if errors: 224 sys.exit('ERROR: Malformed arch YAML file: ' 225 f'{archs_yml.as_posix()}\n' 226 f'{best_match(errors).message} in {best_match(errors).json_path}') 227 228 if args.arch is not None: 229 archs = {'archs': list(filter( 230 lambda arch: arch.get('name') == args.arch, archs['archs']))} 231 for arch in archs['archs']: 232 arch.update({'path': root / 'arch' / arch['path']}) 233 arch.update({'hwm': 'v2'}) 234 arch.update({'type': 'arch'}) 235 236 ret['archs'].extend(archs['archs']) 237 238 return ret 239 240 241def find_v2_systems(args): 242 yml_files = [] 243 systems = Systems() 244 for root in unique_paths(args.soc_roots): 245 yml_files.extend(sorted((root / 'soc').rglob(SOC_YML))) 246 247 for soc_yml in yml_files: 248 if soc_yml.is_file(): 249 systems.extend(Systems.from_file(soc_yml)) 250 251 return systems 252 253 254def parse_args(): 255 parser = argparse.ArgumentParser(allow_abbrev=False) 256 add_args(parser) 257 return parser.parse_args() 258 259 260def add_args(parser): 261 default_fmt = '{name}' 262 263 parser.add_argument("--soc-root", dest='soc_roots', default=[], 264 type=Path, action='append', 265 help='add a SoC root, may be given more than once') 266 parser.add_argument("--soc", default=None, help='lookup the specific soc') 267 parser.add_argument("--soc-series", default=None, help='lookup the specific soc series') 268 parser.add_argument("--soc-family", default=None, help='lookup the specific family') 269 parser.add_argument("--socs", action='store_true', help='lookup all socs') 270 parser.add_argument("--arch-root", dest='arch_roots', default=[], 271 type=Path, action='append', 272 help='add a arch root, may be given more than once') 273 parser.add_argument("--arch", default=None, help='lookup the specific arch') 274 parser.add_argument("--archs", action='store_true', help='lookup all archs') 275 parser.add_argument("--format", default=default_fmt, 276 help='''Format string to use to list each soc.''') 277 parser.add_argument("--cmakeformat", default=None, 278 help='''CMake format string to use to list each arch/soc.''') 279 280 281def dump_v2_archs(args): 282 archs = find_v2_archs(args) 283 284 for arch in archs['archs']: 285 if args.cmakeformat is not None: 286 info = args.cmakeformat.format( 287 TYPE='TYPE;' + arch['type'], 288 NAME='NAME;' + arch['name'], 289 DIR='DIR;' + str(arch['path'].as_posix()), 290 HWM='HWM;' + arch['hwm'], 291 # Below is non exising for arch but is defined here to support 292 # common formatting string. 293 SERIES='', 294 FAMILY='', 295 ARCH='', 296 VENDOR='' 297 ) 298 else: 299 info = args.format.format( 300 type=arch.get('type'), 301 name=arch.get('name'), 302 dir=arch.get('path'), 303 hwm=arch.get('hwm'), 304 # Below is non exising for arch but is defined here to support 305 # common formatting string. 306 series='', 307 family='', 308 arch='', 309 vendor='' 310 ) 311 312 print(info) 313 314 315def dump_v2_system(args, type, system): 316 if args.cmakeformat is not None: 317 info = args.cmakeformat.format( 318 TYPE='TYPE;' + type, 319 NAME='NAME;' + system.name, 320 DIR='DIR;' + ';'.join([Path(x).as_posix() for x in system.folder]), 321 HWM='HWM;' + 'v2' 322 ) 323 else: 324 info = args.format.format( 325 type=type, 326 name=system.name, 327 dir=system.folder, 328 hwm='v2' 329 ) 330 331 print(info) 332 333 334def dump_v2_systems(args): 335 systems = find_v2_systems(args) 336 337 for f in systems.get_families(): 338 dump_v2_system(args, 'family', f) 339 340 for s in systems.get_series(): 341 dump_v2_system(args, 'series', s) 342 343 for s in systems.get_socs(): 344 dump_v2_system(args, 'soc', s) 345 346 347if __name__ == '__main__': 348 args = parse_args() 349 if any([args.socs, args.soc, args.soc_series, args.soc_family]): 350 dump_v2_systems(args) 351 if args.archs or args.arch is not None: 352 dump_v2_archs(args) 353