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