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