1#!/usr/bin/env python3
2# vim: set syntax=python ts=4 :
3#
4# Copyright (c) 2018-2022 Intel Corporation
5# Copyright (c) 2024 Arm Limited (or its affiliates). All rights reserved.
6#
7# SPDX-License-Identifier: Apache-2.0
8
9import logging
10import os
11import shutil
12from argparse import Namespace
13from itertools import groupby
14
15import list_boards
16import scl
17from twisterlib.constants import SUPPORTED_SIMS
18from twisterlib.environment import ZEPHYR_BASE
19
20logger = logging.getLogger('twister')
21
22
23class Simulator:
24    """Class representing a simulator"""
25
26    def __init__(self, data: dict[str, str]):
27        assert "name" in data
28        assert data["name"] in SUPPORTED_SIMS
29        self.name = data["name"]
30        self.exec = data.get("exec")
31
32    def is_runnable(self) -> bool:
33        if self.name == "simics":
34            return shutil.which(self.exec, path=os.environ.get("SIMICS_PROJECT")) is not None
35
36        return not bool(self.exec) or bool(shutil.which(self.exec))
37
38    def __str__(self):
39        return f"Simulator(name: {self.name}, exec: {self.exec})"
40
41    def __eq__(self, other):
42        if isinstance(other, Simulator):
43            return self.name == other.name and self.exec == other.exec
44        else:
45            return False
46
47
48class Platform:
49    """Class representing metadata for a particular platform
50
51    Maps directly to BOARD when building"""
52
53    platform_schema = scl.yaml_load(
54        os.path.join(ZEPHYR_BASE, "scripts", "schemas", "twister", "platform-schema.yaml")
55    )
56
57    def __init__(self):
58        """Constructor.
59
60        """
61
62        self.name = ""
63        self.aliases = []
64        self.normalized_name = ""
65        # if sysbuild to be used by default on a given platform
66        self.sysbuild = False
67        self.twister = True
68        # if no RAM size is specified by the board, take a default of 128K
69        self.ram = 128
70
71        self.timeout_multiplier = 1.0
72        self.ignore_tags = []
73        self.only_tags = []
74        self.default = False
75        # if no flash size is specified by the board, take a default of 512K
76        self.flash = 512
77        self.supported = set()
78        self.binaries = []
79
80        self.arch = None
81        self.vendor = ""
82        self.tier = -1
83        self.type = "na"
84        self.simulators: list[Simulator] = []
85        self.simulation: str = "na"
86        self.supported_toolchains = []
87        self.env = []
88        self.env_satisfied = True
89        self.filter_data = dict()
90        self.uart = ""
91        self.resc = ""
92
93    def load(self, board, target, aliases, data, variant_data):
94        """Load the platform data from the board data and target data
95        board: the board object as per the zephyr build system
96        target: the target name of the board as per the zephyr build system
97        aliases: list of aliases for the target
98        data: the default data from the twister.yaml file for the board
99        variant_data: the target-specific data to replace the default data
100        """
101        self.name = target
102        self.aliases = aliases
103
104        self.normalized_name = self.name.replace("/", "_")
105        self.sysbuild = variant_data.get("sysbuild", data.get("sysbuild", self.sysbuild))
106        self.twister = variant_data.get("twister", data.get("twister", self.twister))
107
108        # if no RAM size is specified by the board, take a default of 128K
109        self.ram = variant_data.get("ram", data.get("ram", self.ram))
110        # if no flash size is specified by the board, take a default of 512K
111        self.flash = variant_data.get("flash", data.get("flash", self.flash))
112
113        testing = data.get("testing", {})
114        self.ignore_tags = testing.get("ignore_tags", [])
115        self.only_tags = testing.get("only_tags", [])
116        self.default = testing.get("default", self.default)
117        self.binaries = testing.get("binaries", [])
118        self.timeout_multiplier = testing.get("timeout_multiplier", self.timeout_multiplier)
119
120        # testing data for variant
121        testing_var = variant_data.get("testing", data.get("testing", {}))
122        self.timeout_multiplier = testing_var.get("timeout_multiplier", self.timeout_multiplier)
123        self.ignore_tags = testing_var.get("ignore_tags", self.ignore_tags)
124        self.only_tags = testing_var.get("only_tags", self.only_tags)
125        self.default = testing_var.get("default", self.default)
126        self.binaries = testing_var.get("binaries", self.binaries)
127        renode = testing.get("renode", {})
128        self.uart = renode.get("uart", "")
129        self.resc = renode.get("resc", "")
130
131        self.supported = set()
132        for supp_feature in variant_data.get("supported", data.get("supported", [])):
133            for item in supp_feature.split(":"):
134                self.supported.add(item)
135
136        self.arch = variant_data.get('arch', data.get('arch', self.arch))
137        self.vendor = board.vendor
138        self.tier = variant_data.get("tier", data.get("tier", self.tier))
139        self.type = variant_data.get('type', data.get('type', self.type))
140
141        self.simulators = [
142            Simulator(data) for data in variant_data.get(
143                'simulation',
144                data.get('simulation', self.simulators)
145            )
146        ]
147        default_sim = self.simulator_by_name(None)
148        if default_sim:
149            self.simulation = default_sim.name
150
151        self.supported_toolchains = variant_data.get("toolchain", data.get("toolchain", []))
152        if self.supported_toolchains is None:
153            self.supported_toolchains = []
154
155        support_toolchain_variants = {
156          # we don't provide defaults for 'arc' intentionally: some targets can't be built with GNU
157          # toolchain ("zephyr", "cross-compile" options) and for some targets we haven't provided
158          # MWDT compiler / linker options in corresponding SoC file in Zephyr, so these targets
159          # can't be built with ARC MWDT toolchain ("arcmwdt" option) by Zephyr build system Instead
160          # for 'arc' we rely on 'toolchain' option in board yaml configuration.
161          "arm": ["zephyr", "gnuarmemb", "armclang", "llvm"],
162          "arm64": ["zephyr", "cross-compile"],
163          "mips": ["zephyr"],
164          "riscv": ["zephyr", "cross-compile"],
165          "posix": ["host", "llvm"],
166          "sparc": ["zephyr"],
167          "x86": ["zephyr", "llvm"],
168          # Xtensa is not listed on purpose, since there is no single toolchain
169          # that is supported on all board targets for xtensa.
170        }
171
172        if self.arch in support_toolchain_variants:
173            for toolchain in support_toolchain_variants[self.arch]:
174                if toolchain not in self.supported_toolchains:
175                    self.supported_toolchains.append(toolchain)
176
177        self.env = variant_data.get("env", data.get("env", []))
178        self.env_satisfied = True
179        for env in self.env:
180            if not os.environ.get(env, None):
181                self.env_satisfied = False
182
183    def simulator_by_name(self, sim_name: str | None) -> Simulator | None:
184        if sim_name:
185            return next(filter(lambda s: s.name == sim_name, iter(self.simulators)), None)
186        else:
187            return next(iter(self.simulators), None)
188
189    def __repr__(self):
190        return f"<{self.name} on {self.arch}>"
191
192
193def generate_platforms(board_roots, soc_roots, arch_roots):
194    """Initialize and yield all Platform instances.
195
196    Using the provided board/soc/arch roots, determine the available
197    platform names and load the test platform description files.
198
199    An exception is raised if not all platform files are valid YAML,
200    or if not all platform names are unique.
201    """
202    alias2target = {}
203    target2board = {}
204    target2data = {}
205    dir2data = {}
206    legacy_files = []
207
208    lb_args = Namespace(board_roots=board_roots, soc_roots=soc_roots, arch_roots=arch_roots,
209                        board=None, board_dir=None)
210
211    for board in list_boards.find_v2_boards(lb_args).values():
212        for board_dir in board.directories:
213            if board_dir in dir2data:
214                # don't load the same board data twice
215                continue
216            file = board_dir / "twister.yaml"
217            if file.is_file():
218                data = scl.yaml_load_verify(file, Platform.platform_schema)
219            else:
220                data = None
221            dir2data[board_dir] = data
222
223            legacy_files.extend(
224                file for file in board_dir.glob("*.yaml") if file.name != "twister.yaml"
225            )
226
227        for qual in list_boards.board_v2_qualifiers(board):
228            if board.revisions:
229                for rev in board.revisions:
230                    if rev.name:
231                        target = f"{board.name}@{rev.name}/{qual}"
232                        alias2target[target] = target
233                        if rev.name == board.revision_default:
234                            alias2target[f"{board.name}/{qual}"] = target
235                        if '/' not in qual and len(board.socs) == 1:
236                            if rev.name == board.revision_default:
237                                alias2target[f"{board.name}"] = target
238                            alias2target[f"{board.name}@{rev.name}"] = target
239                    else:
240                        target = f"{board.name}/{qual}"
241                        alias2target[target] = target
242                        if '/' not in qual and len(board.socs) == 1 \
243                                and rev.name == board.revision_default:
244                            alias2target[f"{board.name}"] = target
245
246                    target2board[target] = board
247            else:
248                target = f"{board.name}/{qual}"
249                alias2target[target] = target
250                if '/' not in qual and len(board.socs) == 1:
251                    alias2target[board.name] = target
252                target2board[target] = board
253
254    for board_dir, data in dir2data.items():
255        if data is None:
256            continue
257        # Separate the default and variant information in the loaded board data.
258        # The default (top-level) data can be shared by multiple board targets;
259        # it will be overlaid by the variant data (if present) for each target.
260        variant_data = data.pop("variants", {})
261        for variant in variant_data:
262            target = alias2target.get(variant)
263            if target is None:
264                continue
265            if target in target2data:
266                logger.error(f"Duplicate platform {target} in {board_dir}")
267                raise Exception(f"Duplicate platform identifier {target} found")
268            target2data[target] = variant_data[variant]
269
270    # note: this inverse mapping will only be used for loading legacy files
271    target2aliases = {}
272
273    for target, aliases in groupby(alias2target, alias2target.get):
274        aliases = list(aliases)
275        board = target2board[target]
276
277        # Default board data always comes from the primary 'board.dir'.
278        # Other 'board.directories' can only supply variant data.
279        data = dir2data[board.dir]
280        if data is not None:
281            variant_data = target2data.get(target, {})
282
283            platform = Platform()
284            platform.load(board, target, aliases, data, variant_data)
285            yield platform
286
287        target2aliases[target] = aliases
288
289    for file in legacy_files:
290        data = scl.yaml_load_verify(file, Platform.platform_schema)
291        target = alias2target.get(data.get("identifier"))
292        if target is None:
293            continue
294
295        board = target2board[target]
296        if dir2data[board.dir] is not None:
297            # all targets are already loaded for this board
298            logger.error(f"Duplicate platform {target} in {file.parent}")
299            raise Exception(f"Duplicate platform identifier {target} found")
300
301        platform = Platform()
302        platform.load(board, target, target2aliases[target], data, variant_data={})
303        yield platform
304