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