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