ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_example_board_pins.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Gate: examples shall not hand-encode board connector pins.
5
6The EK-RA8D2 pinout is a fixed board fact, and the board layer
7(``libs/ra8_board_ek_ra8d2``) is its single source of truth: it exposes the
8USB, I2C/I3C, SD, Ethernet, audio, console, LED and switch pins as
9``k_ra8_board_*`` symbols (and accessors like ``ra8_board_sw_pin``). When an
10application instead re-encodes a pin as ``(port << 8) | pin`` it duplicates
11that fact -- and #251 showed the cost: the four USB-FS pins were copy-pasted,
12byte-identical, across 29 apps under a dozen different local names, so a pin
13correction in one silently skipped the rest.
14
15This gate forbids the ``((uint16_t)k_ra8_port_N << 8) | (uint16_t)k_ra8_pin_M``
16board-pin encoding idiom anywhere under ``examples/``. The fix is always to
17reference the board symbol, or -- if the pin is a real board connector the
18board layer does not expose yet -- to add it there first, then reference it.
19
20Scope note: this targets the specific *encoding* idiom that was duplicated,
21not every pin literal. A genuinely app-specific pin (e.g. where an external
22motor driver is wired, which is not an EK-RA8D2 board fact and has no board
23home) is out of scope by construction -- it does not use this idiom.
24
25Run::
26
27 check_example_board_pins.py # scan examples/
28 check_example_board_pins.py path/to/main.c ... # scan listed files
29 check_example_board_pins.py --selftest # prove both directions
30
31Exit 0 if no example hand-encodes a board pin, exit 1 (with the offenders)
32otherwise, exit 2 when the whole-tree sweep collapses below FILE_FLOOR.
33"""
34
35from __future__ import annotations
36
37import re
38import sys
39from collections.abc import Iterable
40from pathlib import Path
41
42sys.path.insert(0, str(Path(__file__).resolve().parent))
43
44from lint_targets import is_build_output_path # needs the sys.path line above
45from selftest_assert import expect, report # needs the sys.path line above
46
47REPO_ROOT = Path(__file__).resolve().parents[2]
48
49SOURCE_SUFFIXES = (".c", ".h", ".cpp", ".hpp")
50SCAN_ROOT = "examples"
51
52# ((uint16_t)k_ra8_port_N << 8) | (uint16_t)k_ra8_pin_M, tolerant of spacing.
53ENCODING_RE = re.compile(r"k_ra8_port_\d+\s*<<\s*8\s*\‍)\s*\|\s*\‍(\s*uint16_t\s*\‍)\s*k_ra8_pin_\d+")
54
55# An examples/ tree this size cannot legitimately collapse to a handful of
56# files. If the whole-tree sweep returns less than this, something broke (an
57# unreachable repo root, a renamed SCAN_ROOT) and reporting "none hand-encode a
58# board pin" would be a lie: the idiom cannot be found in a file nobody read.
59# Measured 2026-07-28: 408 example sources. Same trip-wire as check_ruff.py.
60#
61# The count is not reproducible unless build output is excluded: a configured
62# in-source build under examples/<app>/build/ inflated the sweep 408 -> 409
63# (#549). `is_build_output_path` filters those, matching every peer checker, so
64# what the gate scans is the committed source and nothing generated on top.
65FILE_FLOOR = 320
66
67
68def _is_source(path: Path) -> bool:
69 return path.suffix in SOURCE_SUFFIXES
70
71
72def _rel(path: Path) -> str:
73 if path.is_relative_to(REPO_ROOT):
74 return str(path.relative_to(REPO_ROOT))
75 return str(path)
76
77
78def _enumerate_targets(arg_paths: Iterable[str]) -> list[Path]:
79 args = list(arg_paths)
80 if args:
81 out: list[Path] = []
82 for raw in args:
83 path = Path(raw)
84 if not path.is_absolute():
85 path = REPO_ROOT / path
86 if path.is_dir():
87 for suffix in SOURCE_SUFFIXES:
88 out.extend(path.rglob("*" + suffix))
89 elif _is_source(path):
90 out.append(path)
91 return [p for p in out if not is_build_output_path(p)]
92
93 out = []
94 for suffix in SOURCE_SUFFIXES:
95 out.extend((REPO_ROOT / SCAN_ROOT).rglob("*" + suffix))
96 return [p for p in out if not is_build_output_path(p)]
97
98
99def selftest() -> int:
100 """Prove the detector fires on the idiom and that build output is excluded.
101
102 Three things have to hold at once for a clean run to mean anything: the
103 encoding regex must FIRE on the ``(port << 8) | pin`` shape and stay QUIET
104 on a board-symbol reference, the enumeration must DROP an in-source build
105 file (the scope defect #549 fixed) while KEEPING a real example source, and
106 the live sweep must clear ``FILE_FLOOR`` so a collapsed scope cannot pass as
107 a clean tree.
108
109 Returns:
110 0 when every assertion held in both directions, 1 otherwise.
111 """
112 failures: list[str] = []
113
114 idiom = " cfg.pin = ((uint16_t)k_ra8_port_6 << 8) | (uint16_t)k_ra8_pin_11;"
115 board_ref = " cfg.pin = ra8_board_sw_pin(k_ra8_board_sw_user);"
116 expect(bool(ENCODING_RE.search(idiom)), "MUST FIRE: hand-encoded (port << 8) | pin", failures)
117 expect(
118 not ENCODING_RE.search(board_ref),
119 "MUST NOT FIRE: a board-symbol reference",
120 failures,
121 )
122
123 enumerated = {
124 str(p) for p in _enumerate_targets(["examples/x/build/gen.c", "examples/x/src/main.c"])
125 }
126 expect(
127 any(p.endswith("examples/x/src/main.c") for p in enumerated),
128 "MUST FIRE: a real example source is enumerated",
129 failures,
130 )
131 expect(
132 not any(p.endswith("examples/x/build/gen.c") for p in enumerated),
133 "MUST NOT FIRE: an in-source build file is excluded from the scope",
134 failures,
135 )
136
137 live = _enumerate_targets([])
138 expect(
139 len(live) >= FILE_FLOOR,
140 f"live sweep sees {len(live)} example file(s) (floor {FILE_FLOOR})",
141 failures,
142 )
143 expect(
144 all(not is_build_output_path(p) for p in live),
145 "no enumerated file lives in a build tree",
146 failures,
147 )
148 return report(failures)
149
150
151def main(argv: list[str]) -> int:
152 """Fail any example that spells a board pin as an inline ``(port << 8) | pin``.
153
154 The match is textual, on the encoding SHAPE rather than on known pin
155 values, because the defect #251 found was the same four USB-FS pins
156 copy-pasted across 29 apps under a dozen different local names -- a value
157 allowlist would have had to know all twelve names, while the shift-or
158 pattern catches the next one regardless of what it is called.
159
160 Undecodable bytes are replaced rather than raising, so one bad file cannot
161 abort the sweep; encoding is check-encoding's gate, not this one's.
162
163 FILE_FLOOR applies to the whole-tree sweep ONLY, and exits 2 below it. An
164 argv file list comes from the pre-commit hook and legitimately filters to
165 nothing when a commit touches no example source, so an empty list there is
166 a real answer; an empty SWEEP is a broken enumeration reporting a clean
167 tree because it read nothing.
168
169 Returns 1 listing each site, 0 when clean or when argv filtered to nothing,
170 2 when the whole-tree sweep enumerated too few files to trust.
171 """
172 if "--selftest" in argv[1:]:
173 return selftest()
174 paths = argv[1:]
175 targets = _enumerate_targets(paths)
176 if not paths and len(targets) < FILE_FLOOR:
177 print(
178 f"check_example_board_pins.py: FATAL -- only {len(targets)} example file(s) "
179 f"in scope, floor is {FILE_FLOOR}. A collapsed sweep reports a clean tree "
180 "because it scanned nothing.",
181 file=sys.stderr,
182 )
183 return 2
184 if not targets:
185 print("check_example_board_pins.py: no files to scan", file=sys.stderr)
186 return 0
187
188 hits = []
189 for path in targets:
190 try:
191 text = path.read_text(encoding="utf-8", errors="replace")
192 except OSError:
193 continue
194 for lineno, line in enumerate(text.splitlines(), 1):
195 if ENCODING_RE.search(line):
196 hits.append((_rel(path), lineno, line.strip()))
197
198 if not hits:
199 print(
200 f"check_example_board_pins.py: {len(targets)} example file(s) "
201 "scanned, none hand-encode a board pin."
202 )
203 return 0
204
205 print(
206 f"check_example_board_pins.py: {len(hits)} hand-encoded board pin(s) in examples:\n",
207 file=sys.stderr,
208 )
209 for path, lineno, snippet in hits:
210 print(f" {path}:{lineno} {snippet}", file=sys.stderr)
211 print(
212 "\nThe EK-RA8D2 pinout belongs to the board layer, not to each app.\n"
213 "Reference the board symbol (k_ra8_board_*_pin_*, or an accessor like\n"
214 "ra8_board_sw_pin) instead of re-encoding (port << 8 | pin). If the pin\n"
215 "is a real board connector the board layer does not expose yet, add it\n"
216 "to libs/ra8_board_ek_ra8d2 first, then reference it here.",
217 file=sys.stderr,
218 )
219 return 1
220
221
222if __name__ == "__main__":
223 sys.exit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298