Skip to content

Commit 3640a9d

Browse files
committed
validate-board-config: follow source ${SRC}/config/boards/<foo> inheritance
`Validate changed board configs` (PR #9747 CI run 25258999953) failed on the three Ayn-Odin2 derivative boards with: ERROR: config/boards/ayn-odin2mini.csc: BOARDFAMILY: required, ... ERROR: config/boards/ayn-odin2mini.csc: KERNEL_TARGET: required, ... (same pair on ayn-odin2portal.csc and ayn-thor.csc) The validator's header explicitly says it does NOT source the file ("boards have side-effecty function bodies"), and the regex `parse_assignments` only sees top-level `KEY=value` lines. Boards that consist of one `source "${SRC}/config/boards/<parent>.csc"` line plus a handful of overrides (BOARD_NAME, BOARD_VENDOR, BOARD_MAINTAINER, ARCH) inherit BOARDFAMILY / KERNEL_TARGET / KERNEL_TEST_TARGET from the parent, so the validator saw them as missing and erred out. The errors are pre-existing — they would fire on any future change to those .csc files. PR #9747 just made them visible by being the first PR to touch an inheriting board since the validator landed. Add `collect_inherited_assignments`: when a top-level `source` line points at another file under config/boards/ (matched by a regex anchored to start-of-line so `source` calls inside function bodies — which are always indented — aren't followed), parse that file's top-level assignments and lay them behind the child's own. Child's explicit values still win via dict.setdefault. Recursion is guarded by a visited set keyed on resolved paths, so a self-source or a parent-source-child cycle terminates instead of looping. Missing source targets are silently skipped — the main required-field checks will still flag any field that ends up unset after the merge, so a typo'd source path doesn't mask a real gap. Verified against the five PR-changed files: 6 errors → 0 errors, 4 warnings → 1 warning (the unmaintained-board warning on aml-t95z-plus.tvb, which is unrelated to inheritance and was already there). Self-contained boards (musepipro.conf etc.) produce identical output to before this change.
1 parent e825695 commit 3640a9d

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

tools/validate-board-config.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@
4242
re.MULTILINE,
4343
)
4444

45+
# Top-level `source ${SRC}/config/boards/<foo>.csc` (or .conf/.tvb/.wip).
46+
# Anchored to start-of-line so a `source` call inside a function body
47+
# (which is always indented) doesn't get followed. Captures the relative
48+
# path under config/boards/ so the validator can resolve it next to the
49+
# child file being validated.
50+
_SOURCE_RE = re.compile(
51+
r'^source\s+["\']?\$\{?SRC\}?/config/boards/([^"\'\s]+\.(?:conf|csc|tvb|wip))["\']?',
52+
re.MULTILINE,
53+
)
54+
4555

4656

4757
@dataclass
@@ -83,13 +93,67 @@ def parse_assignments(text: str) -> dict[str, str]:
8393
return out
8494

8595

96+
def collect_inherited_assignments(path: Path, _visited: set[Path] | None = None) -> dict[str, str]:
97+
"""Resolve fields a board inherits via `source ${SRC}/config/boards/<foo>`.
98+
99+
Some boards (e.g. ayn-odin2{mini,portal}.csc, ayn-thor.csc) consist
100+
of one `source` line pointing at a base .csc plus a handful of
101+
overrides for the fields they actually change. Fields the child
102+
doesn't redeclare — BOARDFAMILY, KERNEL_TARGET, KERNEL_TEST_TARGET
103+
in the typical case — live in the sourced parent.
104+
105+
Returns a flat dict of effective fields the parent chain provides,
106+
in the same shape parse_assignments() returns. Caller merges this
107+
behind the child's own dict so the child's explicit values still
108+
win.
109+
110+
Cycles (a sources b sources a) are guarded by the _visited set;
111+
missing or unresolvable source targets are silently skipped — the
112+
main validator will still flag any field that ends up unset.
113+
"""
114+
if _visited is None:
115+
_visited = set()
116+
try:
117+
resolved = path.resolve()
118+
except OSError:
119+
return {}
120+
if resolved in _visited:
121+
return {}
122+
_visited.add(resolved)
123+
124+
text = path.read_text(errors="replace")
125+
inherited: dict[str, str] = {}
126+
for m in _SOURCE_RE.finditer(text):
127+
sourced = path.parent / m.group(1)
128+
if not sourced.is_file():
129+
continue
130+
# Recurse first so transitive parents are merged behind the
131+
# immediate parent's fields. Within a single chain, nearest
132+
# parent's value should win over a more distant ancestor's,
133+
# which falls out naturally from setdefault's "skip if set".
134+
ancestors = collect_inherited_assignments(sourced, _visited)
135+
parent_fields = parse_assignments(sourced.read_text(errors="replace"))
136+
# Parent's own assignments win over its ancestors; merge in
137+
# that order, then those become the inheritance pool the
138+
# caller will lay behind the child.
139+
for k, v in parent_fields.items():
140+
inherited.setdefault(k, v)
141+
for k, v in ancestors.items():
142+
inherited.setdefault(k, v)
143+
return inherited
144+
145+
86146
def validate(path: Path) -> list[Finding]:
87147
ext = path.suffix.lstrip(".")
88148
if ext == "eos":
89149
return [] # dead boards aren't validated
90150

91151
text = path.read_text(errors="replace")
92152
fields = parse_assignments(text)
153+
# Layer fields the child inherits via `source ${SRC}/config/boards/<...>`
154+
# behind its own — child's explicit value wins, parent fills the gaps.
155+
for k, v in collect_inherited_assignments(path).items():
156+
fields.setdefault(k, v)
93157

94158
findings: list[Finding] = []
95159
fname = str(path)

0 commit comments

Comments
 (0)