mirror of
https://github.com/armbian/build.git
synced 2026-09-08 23:59:54 +08:00
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.
This commit is contained in:
@@ -42,6 +42,16 @@ _ASSIGN_RE = re.compile(
|
|||||||
re.MULTILINE,
|
re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Top-level `source ${SRC}/config/boards/<foo>.csc` (or .conf/.tvb/.wip).
|
||||||
|
# Anchored to start-of-line so a `source` call inside a function body
|
||||||
|
# (which is always indented) doesn't get followed. Captures the relative
|
||||||
|
# path under config/boards/ so the validator can resolve it next to the
|
||||||
|
# child file being validated.
|
||||||
|
_SOURCE_RE = re.compile(
|
||||||
|
r'^source\s+["\']?\$\{?SRC\}?/config/boards/([^"\'\s]+\.(?:conf|csc|tvb|wip))["\']?',
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -83,6 +93,56 @@ def parse_assignments(text: str) -> dict[str, str]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def collect_inherited_assignments(path: Path, _visited: set[Path] | None = None) -> dict[str, str]:
|
||||||
|
"""Resolve fields a board inherits via `source ${SRC}/config/boards/<foo>`.
|
||||||
|
|
||||||
|
Some boards (e.g. ayn-odin2{mini,portal}.csc, ayn-thor.csc) consist
|
||||||
|
of one `source` line pointing at a base .csc plus a handful of
|
||||||
|
overrides for the fields they actually change. Fields the child
|
||||||
|
doesn't redeclare — BOARDFAMILY, KERNEL_TARGET, KERNEL_TEST_TARGET
|
||||||
|
in the typical case — live in the sourced parent.
|
||||||
|
|
||||||
|
Returns a flat dict of effective fields the parent chain provides,
|
||||||
|
in the same shape parse_assignments() returns. Caller merges this
|
||||||
|
behind the child's own dict so the child's explicit values still
|
||||||
|
win.
|
||||||
|
|
||||||
|
Cycles (a sources b sources a) are guarded by the _visited set;
|
||||||
|
missing or unresolvable source targets are silently skipped — the
|
||||||
|
main validator will still flag any field that ends up unset.
|
||||||
|
"""
|
||||||
|
if _visited is None:
|
||||||
|
_visited = set()
|
||||||
|
try:
|
||||||
|
resolved = path.resolve()
|
||||||
|
except OSError:
|
||||||
|
return {}
|
||||||
|
if resolved in _visited:
|
||||||
|
return {}
|
||||||
|
_visited.add(resolved)
|
||||||
|
|
||||||
|
text = path.read_text(errors="replace")
|
||||||
|
inherited: dict[str, str] = {}
|
||||||
|
for m in _SOURCE_RE.finditer(text):
|
||||||
|
sourced = path.parent / m.group(1)
|
||||||
|
if not sourced.is_file():
|
||||||
|
continue
|
||||||
|
# Recurse first so transitive parents are merged behind the
|
||||||
|
# immediate parent's fields. Within a single chain, nearest
|
||||||
|
# parent's value should win over a more distant ancestor's,
|
||||||
|
# which falls out naturally from setdefault's "skip if set".
|
||||||
|
ancestors = collect_inherited_assignments(sourced, _visited)
|
||||||
|
parent_fields = parse_assignments(sourced.read_text(errors="replace"))
|
||||||
|
# Parent's own assignments win over its ancestors; merge in
|
||||||
|
# that order, then those become the inheritance pool the
|
||||||
|
# caller will lay behind the child.
|
||||||
|
for k, v in parent_fields.items():
|
||||||
|
inherited.setdefault(k, v)
|
||||||
|
for k, v in ancestors.items():
|
||||||
|
inherited.setdefault(k, v)
|
||||||
|
return inherited
|
||||||
|
|
||||||
|
|
||||||
def validate(path: Path) -> list[Finding]:
|
def validate(path: Path) -> list[Finding]:
|
||||||
ext = path.suffix.lstrip(".")
|
ext = path.suffix.lstrip(".")
|
||||||
if ext == "eos":
|
if ext == "eos":
|
||||||
@@ -90,6 +150,10 @@ def validate(path: Path) -> list[Finding]:
|
|||||||
|
|
||||||
text = path.read_text(errors="replace")
|
text = path.read_text(errors="replace")
|
||||||
fields = parse_assignments(text)
|
fields = parse_assignments(text)
|
||||||
|
# Layer fields the child inherits via `source ${SRC}/config/boards/<...>`
|
||||||
|
# behind its own — child's explicit value wins, parent fills the gaps.
|
||||||
|
for k, v in collect_inherited_assignments(path).items():
|
||||||
|
fields.setdefault(k, v)
|
||||||
|
|
||||||
findings: list[Finding] = []
|
findings: list[Finding] = []
|
||||||
fname = str(path)
|
fname = str(path)
|
||||||
|
|||||||
Reference in New Issue
Block a user