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