Skip to content

Commit 5e52641

Browse files
57300anangl
authored andcommitted
[nrf fromlist] twister: Account for board & SoC extensions
Problem ------- Board & SoC extensions are used to define out-of-tree board variants or SoC qualifiers. When a board is extended, it has multiple directories associated with it (each with its own `board.yml`), where twister should be able to find additional platform files to support these qualifiers. Currently, this doesn't work, because twister only traverses the primary BOARD_DIR and ignores the rest. The fix would've been trivial in the case of "legacy" platform files, i.e. those of the form `<normalized_board_target>.yaml`, but it's less straightforward for the newly introduced `twister.yaml` format. A `twister.yaml` file contains platform configuration that can be shared by multiple board targets and tweaked for specific targets by using the top-level `variants` key. Normally, there is at most one `twister.yaml` per board, but the file isn't necessarily unique to one board. Instead, it's unique to one directory, which may define multiple boards (as is the case with e.g. `boards/qemu/x86/`). With extensions in the picture, the goal is to initialize platforms when given multiple `twister.yaml` per board. The OOT files are expected to only provide information about OOT board targets, without being able to override in-tree targets (same principle as in the Zephyr build system). Solution -------- Scanning for `twister.yaml` is broken up into multiple passes - first loading all the files, then splitting the `variants` keys apart from the shared configuration, before constructing the Platform instances. The purpose of the split is to treat the variant information as global, instead of making unnecessary or faulty assumptions about locality. Remember that the build system can derive board target names not only from `board.yml`, but from `soc.yml` too. Considering that any board may end up using an OOT-extended SoC (and hence multiple `soc.yml` files), not every board target can be said to belong to some board dir. Unlike the variant data, the remaining top-level config is still rooted to the primary BOARD_DIR and inherited by the extension dirs from there. This is quite intuitive in most imagined cases, but there is a caveat: if a `twister.yaml` resides in an extension dir, then it is allowed to have a top-level config of its own, but it will be silently ignored. This is to support corner cases where, much like how a single board dir can define multiple boards, a single board dir can also extend multiple boards, or even do both. In those cases, the primary BOARD_DIR rule should make it unambiguous which config belongs to which board, even if it may seem counter-intuitive at first. Upstream PR #: 84975 Signed-off-by: Grzegorz Swiderski <[email protected]>
1 parent 874aef7 commit 5e52641

File tree

2 files changed

+89
-73
lines changed

2 files changed

+89
-73
lines changed

scripts/pylib/twister/twisterlib/platform.py

Lines changed: 12 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ def __init__(self):
8383
self.filter_data = dict()
8484
self.uart = ""
8585
self.resc = ""
86-
self.qualifier = None
8786

8887
def load(self, board, target, aliases, data):
8988
"""Load the platform data from the board data and target data
@@ -95,26 +94,16 @@ def load(self, board, target, aliases, data):
9594
self.name = target
9695
self.aliases = aliases
9796

98-
# Get data for various targets and use the main board data as a
99-
# defauly. Individual variant information will replace the default data
100-
# provded in the main twister configuration for this board.
101-
variants = data.get("variants", {})
102-
variant_data = {}
103-
for alias in aliases:
104-
variant_data = variants.get(alias, {})
105-
if variant_data:
106-
break
107-
10897
self.normalized_name = self.name.replace("/", "_")
109-
self.sysbuild = variant_data.get("sysbuild", data.get("sysbuild", self.sysbuild))
110-
self.twister = variant_data.get("twister", data.get("twister", self.twister))
98+
self.sysbuild = data.get("sysbuild", self.sysbuild)
99+
self.twister = data.get("twister", self.twister)
111100

112101
# if no RAM size is specified by the board, take a default of 128K
113-
self.ram = variant_data.get("ram", data.get("ram", self.ram))
102+
self.ram = data.get("ram", self.ram)
114103
# if no flash size is specified by the board, take a default of 512K
115-
self.flash = variant_data.get("flash", data.get("flash", self.flash))
104+
self.flash = data.get("flash", self.flash)
116105

117-
testing = variant_data.get("testing", data.get("testing", {}))
106+
testing = data.get("testing", {})
118107
self.timeout_multiplier = testing.get("timeout_multiplier", self.timeout_multiplier)
119108
self.ignore_tags = testing.get("ignore_tags", self.ignore_tags)
120109
self.only_tags = testing.get("only_tags", self.only_tags)
@@ -124,26 +113,23 @@ def load(self, board, target, aliases, data):
124113
self.uart = renode.get("uart", "")
125114
self.resc = renode.get("resc", "")
126115
self.supported = set()
127-
for supp_feature in variant_data.get("supported", data.get("supported", [])):
116+
for supp_feature in data.get("supported", []):
128117
for item in supp_feature.split(":"):
129118
self.supported.add(item)
130119

131-
self.arch = variant_data.get('arch', data.get('arch', self.arch))
120+
self.arch = data.get('arch', self.arch)
132121
self.vendor = board.vendor
133-
self.tier = variant_data.get("tier", data.get("tier", self.tier))
134-
self.type = variant_data.get('type', data.get('type', self.type))
122+
self.tier = data.get("tier", self.tier)
123+
self.type = data.get('type', self.type)
135124

136125
self.simulators = [
137-
Simulator(data) for data in variant_data.get(
138-
'simulation',
139-
data.get('simulation', self.simulators)
140-
)
126+
Simulator(data) for data in data.get('simulation', self.simulators)
141127
]
142128
default_sim = self.simulator_by_name(None)
143129
if default_sim:
144130
self.simulation = default_sim.name
145131

146-
self.supported_toolchains = variant_data.get("toolchain", data.get("toolchain", []))
132+
self.supported_toolchains = data.get("toolchain", [])
147133
if self.supported_toolchains is None:
148134
self.supported_toolchains = []
149135

@@ -170,7 +156,7 @@ def load(self, board, target, aliases, data):
170156
if toolchain not in self.supported_toolchains:
171157
self.supported_toolchains.append(toolchain)
172158

173-
self.env = variant_data.get("env", data.get("env", []))
159+
self.env = data.get("env", [])
174160
self.env_satisfied = True
175161
for env in self.env:
176162
if not os.environ.get(env, None):

scripts/pylib/twister/twisterlib/testplan.py

Lines changed: 77 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
# SPDX-License-Identifier: Apache-2.0
88
import collections
99
import copy
10-
import glob
1110
import itertools
1211
import json
1312
import logging
@@ -437,12 +436,6 @@ def info(what):
437436
sys.stdout.write(what + "\n")
438437
sys.stdout.flush()
439438

440-
def find_twister_data(self, board_data_list, board_aliases):
441-
"""Find the twister data for a board in the list of board data based on the aliases"""
442-
for board_data in board_data_list:
443-
if board_data.get('identifier') in board_aliases:
444-
return board_data
445-
446439
def add_configurations(self):
447440
# Create a list of board roots as defined by the build system in general
448441
# Note, internally in twister a board root includes the `boards` folder
@@ -452,52 +445,42 @@ def add_configurations(self):
452445
board_roots=board_roots, board=None, board_dir=None)
453446

454447
known_boards = list_boards.find_v2_boards(lb_args)
455-
bdirs = {}
456448
platform_config = self.test_config.get('platforms', {})
457449

450+
alias2target = {}
451+
target2board = {}
452+
target2data = {}
453+
dir2data = {}
454+
legacy_files = []
455+
458456
# helper function to initialize and add platforms
459-
def init_and_add_platforms(data, board, target, qualifier, aliases):
457+
def init_and_add_platforms(data, board, target, aliases, src_dir):
460458
platform = Platform()
461-
if not new_config_found:
462-
data = self.find_twister_data(bdirs[board.dir], aliases)
463-
if not data:
464-
return
465459
platform.load(board, target, aliases, data)
466-
platform.qualifier = qualifier
467460
if platform.name in [p.name for p in self.platforms]:
468-
logger.error(f"Duplicate platform {platform.name} in {board.dir}")
461+
logger.error(f"Duplicate platform {platform.name} in {src_dir}")
469462
raise Exception(f"Duplicate platform identifier {platform.name} found")
470463
if not platform.twister:
471464
return
472465
self.platforms.append(platform)
473466

474467
for board in known_boards.values():
475-
new_config_found = False
476-
# don't load the same board data twice
477-
if not bdirs.get(board.dir):
478-
datas = []
479-
for file in glob.glob(os.path.join(board.dir, "*.yaml")):
480-
if os.path.basename(file) == "twister.yaml":
481-
continue
468+
for board_dir in board.directories:
469+
if board_dir in dir2data:
470+
# don't load the same board data twice
471+
continue
472+
legacy_files.extend(
473+
file for file in board_dir.glob("*.yaml") if file.name != "twister.yaml"
474+
)
475+
data = None
476+
file = board_dir / "twister.yaml"
477+
if file.is_file():
482478
try:
483-
scp = TwisterConfigParser(file, Platform.platform_schema)
484-
sdata = scp.load()
485-
datas.append(sdata)
479+
data = scl.yaml_load_verify(file, Platform.platform_schema)
486480
except Exception as e:
487481
logger.error(f"Error loading {file}: {e!r}")
488482
self.load_errors += 1
489-
continue
490-
bdirs[board.dir] = datas
491-
data = {}
492-
if os.path.exists(board.dir / 'twister.yaml'):
493-
try:
494-
scp = TwisterConfigParser(board.dir / 'twister.yaml', Platform.platform_schema)
495-
data = scp.load()
496-
except Exception as e:
497-
logger.error(f"Error loading {board.dir / 'twister.yaml'}: {e!r}")
498-
self.load_errors += 1
499-
continue
500-
new_config_found = True
483+
dir2data[board_dir] = data
501484

502485

503486

@@ -507,27 +490,74 @@ def init_and_add_platforms(data, board, target, qualifier, aliases):
507490
for rev in board.revisions:
508491
if rev.name:
509492
target = f"{board.name}@{rev.name}/{qual}"
510-
aliases = [target]
493+
alias2target[target] = target
511494
if rev.name == board.revision_default:
512-
aliases.append(f"{board.name}/{qual}")
495+
alias2target[f"{board.name}/{qual}"] = target
513496
if '/' not in qual and len(board.socs) == 1:
514497
if rev.name == board.revision_default:
515-
aliases.append(f"{board.name}")
516-
aliases.append(f"{board.name}@{rev.name}")
498+
alias2target[f"{board.name}"] = target
499+
alias2target[f"{board.name}@{rev.name}"] = target
517500
else:
518501
target = f"{board.name}/{qual}"
519-
aliases = [target]
502+
alias2target[target] = target
520503
if '/' not in qual and len(board.socs) == 1 \
521504
and rev.name == board.revision_default:
522-
aliases.append(f"{board.name}")
505+
alias2target[f"{board.name}"] = target
523506

524-
init_and_add_platforms(data, board, target, qual, aliases)
507+
target2board[target] = board
525508
else:
526509
target = f"{board.name}/{qual}"
527-
aliases = [target]
510+
alias2target[target] = target
528511
if '/' not in qual and len(board.socs) == 1:
529-
aliases.append(board.name)
530-
init_and_add_platforms(data, board, target, qual, aliases)
512+
alias2target[board.name] = target
513+
target2board[target] = board
514+
515+
for board_dir, data in dir2data.items():
516+
if data is None:
517+
continue
518+
# Separate the default and variant information in the loaded board data.
519+
# The default (top-level) data can be shared by multiple board targets;
520+
# it will be overlaid by the variant data (if present) for each target.
521+
variant_data = data.pop("variants", {})
522+
for variant in variant_data:
523+
target = alias2target.get(variant)
524+
if target is None:
525+
continue
526+
if target in target2data:
527+
logger.error(f"Duplicate platform {target} in {board_dir}")
528+
raise Exception(f"Duplicate platform identifier {target} found")
529+
target2data[target] = variant_data[variant]
530+
531+
# note: this inverse mapping will only be used for loading legacy files
532+
target2aliases = {}
533+
534+
for target, aliases in itertools.groupby(alias2target, alias2target.get):
535+
aliases = list(aliases)
536+
board = target2board[target]
537+
538+
# Default board data always comes from the primary 'board.dir'.
539+
# Other 'board.directories' can only supply variant data.
540+
data = dir2data[board.dir]
541+
if data is not None:
542+
if target in target2data:
543+
data = copy.deepcopy(data)
544+
data.update(target2data[target])
545+
init_and_add_platforms(data, board, target, aliases, board.dir)
546+
547+
target2aliases[target] = aliases
548+
549+
for file in legacy_files:
550+
try:
551+
data = scl.yaml_load_verify(file, Platform.platform_schema)
552+
except Exception as e:
553+
logger.error(f"Error loading {file}: {e!r}")
554+
self.load_errors += 1
555+
continue
556+
target = alias2target.get(data.get("identifier"))
557+
if target is not None:
558+
init_and_add_platforms(
559+
data, target2board[target], target, target2aliases[target], file.parent
560+
)
531561

532562
for platform in self.platforms:
533563
if not platform_config.get('override_default_platforms', False):

0 commit comments

Comments
 (0)