ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_c6_integration.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 the offline-checkable ESP32-C6 staging and link contract.
5
6The pinned ESP-hosted patch can apply cleanly while the eventual image is still
7wrong. A renamed first-party header may make ``build.sh`` stage a nonexistent
8path, the patched explicit component set may omit the directory we stage, or a
9component ABI marker may become private while the post-link assertion retains
10its old name. Those failures otherwise appear only in a full ESP-IDF build.
11
12This gate checks the committed recipe without ESP-IDF, a network, or hardware:
13every staged source exists, copies retain their basename, component identities
14agree, and the patch/header/source/post-link symbol contracts form one chain.
15A full pinned ESP-IDF build remains the end-to-end proof.
16
17Run::
18
19 check_c6_integration.py
20 check_c6_integration.py --selftest
21"""
22
23from __future__ import annotations
24
25import re
26import sys
27from pathlib import Path
28
29REPO_ROOT = Path(__file__).resolve().parents[2]
30C6_DIR = REPO_ROOT / "coprocessor" / "esp32c6"
31BUILD_SCRIPT = C6_DIR / "build.sh"
32PATCH_FILE = C6_DIR / "patches" / "0001-custom-rpc-sync-response-hook.patch"
33SERVICE_HEADER = REPO_ROOT / "port" / "esp32_c6" / "inc" / "ra8_mdl_service.h"
34SERVICE_SOURCE = REPO_ROOT / "port" / "esp32_c6" / "src" / "mdl_service.c"
35
36EXIT_OK = 0
37EXIT_FAIL = 1
38EXIT_CONFIG = 2
39
40COMPONENT_ABI = "ra8_mdl_service_component_abi"
41CUSTOM_RPC_HOOK = "esp_hosted_custom_rpc_sync_handler"
42
43_STAGED_COPY_RE = re.compile(
44 r'cp\s+"\$\{SCRIPT_DIR\}/\.\./\.\./(?P<src>[^"]+)"\s*'
45 r'(?:\\\s*)?"\$\{COMPONENT_DIR\}/(?P<dest>[^"]+)"',
46 re.MULTILINE,
47)
48_COMPONENT_DIR_RE = re.compile(
49 r'^[ \t]*COMPONENT_DIR="\$\{PERIPHERAL_DIR\}/components/(?P<name>[^"/]+)"$',
50 re.MULTILINE,
51)
52_PATCH_COMPONENTS_RE = re.compile(r"^\+set\‍(COMPONENTS (?P<names>[^)]+)\‍)$", re.MULTILINE)
53
54# These critical copies must not silently disappear from the recipe. Existence
55# checks alone only prove the copies that remain, so they cannot catch removal.
56REQUIRED_STAGED_SOURCES: frozenset[str] = frozenset(
57 {
58 "port/esp32_c6/CMakeLists.txt",
59 "port/esp32_c6/src/mdl_service.c",
60 "port/esp32_c6/inc/ra8_mdl_service.h",
61 "libs/ra8_c6link/inc/ra8_mdl_protocol.h",
62 "libs/ra8_c6link/inc/ra8_mdl_http.h",
63 }
64)
65
66
67def parse_staged_copies(build_text: str) -> list[tuple[str, str]]:
68 """Return repository source and component destination for each staged copy."""
69 return [match.group("src", "dest") for match in _STAGED_COPY_RE.finditer(build_text)]
70
71
72def _check_staged_copies(build_text: str, available_sources: frozenset[str] | None) -> list[str]:
73 """Return findings for missing, renamed, or nonexistent staged files."""
74 findings: list[str] = []
75 copies = parse_staged_copies(build_text)
76 if not copies:
77 findings.append("staging: build.sh contains no first-party component copies")
78
79 staged_sources = {source for source, _destination in copies}
80 findings.extend(
81 f"staging: build.sh no longer stages required source {required}"
82 for required in sorted(REQUIRED_STAGED_SOURCES - staged_sources)
83 )
84 for source, destination in copies:
85 exists = (
86 source in available_sources
87 if available_sources is not None
88 else (REPO_ROOT / source).is_file()
89 )
90 if not exists:
91 findings.append(f"staging: source does not exist: {source}")
92 if Path(source).name != Path(destination).name:
93 findings.append(
94 f"staging: {source} is renamed to {destination}; copied component "
95 "files must retain their source basename"
96 )
97 return findings
98
99
100def _check_component(build_text: str, patch_text: str) -> list[str]:
101 """Return findings when staged and explicitly enabled component names differ."""
102 findings: list[str] = []
103 component_match = _COMPONENT_DIR_RE.search(build_text)
104 patch_matches = list(_PATCH_COMPONENTS_RE.finditer(patch_text))
105 if component_match is None:
106 findings.append("component: build.sh does not declare a literal staged component name")
107 if len(patch_matches) != 1:
108 findings.append(
109 "component: patch must add exactly one explicit set(COMPONENTS ...) declaration"
110 )
111 if component_match is not None and len(patch_matches) == 1:
112 component = component_match.group("name")
113 if component not in patch_matches[0].group("names").split():
114 findings.append(
115 f"component: build.sh stages {component!r}, but the patch's explicit "
116 "COMPONENTS set does not include it"
117 )
118 return findings
119
120
121def _check_symbols(
122 build_text: str, patch_text: str, header_text: str, source_text: str
123) -> list[str]:
124 """Return findings for public, weak, strong, and post-link symbols."""
125 findings: list[str] = []
126 abi_decl = re.compile(rf"^\s*uint32_t\s+{COMPONENT_ABI}\s*\‍(\s*void\s*\‍)\s*;", re.MULTILINE)
127 abi_definition = re.compile(
128 rf"^\s*(?:\‍[\‍[[^\n]+\‍]\‍]\s*)?uint32_t\s+{COMPONENT_ABI}\s*\‍(\s*void\s*\‍)",
129 re.MULTILINE,
130 )
131 if abi_decl.search(header_text) is None:
132 findings.append(f"ABI: public header does not declare {COMPONENT_ABI}(void)")
133 if abi_definition.search(source_text) is None:
134 findings.append(f"ABI: source does not define externally visible {COMPONENT_ABI}(void)")
135 if f"T[[:space:]]+{COMPONENT_ABI}$" not in build_text:
136 findings.append(f"ABI: build.sh does not require strong text symbol {COMPONENT_ABI}")
137
138 weak_hook = re.compile(
139 rf"^\+__attribute__\‍(\‍(weak\‍)\‍)\s+esp_err_t\s+{CUSTOM_RPC_HOOK}\s*\‍(",
140 re.MULTILINE,
141 )
142 strong_hook = re.compile(rf"^\s*esp_err_t\s+{CUSTOM_RPC_HOOK}\s*\‍(", re.MULTILINE)
143 if weak_hook.search(patch_text) is None:
144 findings.append(f"hook: patch does not provide weak extension point {CUSTOM_RPC_HOOK}")
145 if strong_hook.search(source_text) is None:
146 findings.append(f"hook: component source does not define strong {CUSTOM_RPC_HOOK}")
147 if f"T[[:space:]]+{CUSTOM_RPC_HOOK}$" not in build_text:
148 findings.append(f"hook: build.sh does not require strong text symbol {CUSTOM_RPC_HOOK}")
149 return findings
150
151
152def check_contract(
153 build_text: str,
154 patch_text: str,
155 header_text: str,
156 source_text: str,
157 available_sources: frozenset[str] | None = None,
158) -> list[str]:
159 """Return all offline-checkable C6 integration-contract findings."""
160 findings = _check_staged_copies(build_text, available_sources)
161 findings.extend(_check_component(build_text, patch_text))
162 findings.extend(_check_symbols(build_text, patch_text, header_text, source_text))
163 return findings
164
165
166_GOOD_BUILD = r"""
167 COMPONENT_DIR="${PERIPHERAL_DIR}/components/mdl_service"
168cp "${SCRIPT_DIR}/../../port/esp32_c6/CMakeLists.txt" "${COMPONENT_DIR}/CMakeLists.txt"
169cp "${SCRIPT_DIR}/../../port/esp32_c6/src/mdl_service.c" \
170 "${COMPONENT_DIR}/src/mdl_service.c"
171cp "${SCRIPT_DIR}/../../port/esp32_c6/inc/ra8_mdl_service.h" \
172 "${COMPONENT_DIR}/include/ra8_mdl_service.h"
173cp "${SCRIPT_DIR}/../../libs/ra8_c6link/inc/ra8_mdl_protocol.h" \
174 "${COMPONENT_DIR}/include/ra8_mdl_protocol.h"
175cp "${SCRIPT_DIR}/../../libs/ra8_c6link/inc/ra8_mdl_http.h" \
176 "${COMPONENT_DIR}/include/ra8_mdl_http.h"
177grep -Eq 'T[[:space:]]+ra8_mdl_service_component_abi$'
178grep -Eq 'T[[:space:]]+esp_hosted_custom_rpc_sync_handler$'
179"""
180_GOOD_PATCH = r"""
181+set(COMPONENTS esp_timer main mdl_service)
182+__attribute__((weak)) esp_err_t esp_hosted_custom_rpc_sync_handler(
183"""
184_GOOD_HEADER = "uint32_t ra8_mdl_service_component_abi(void);\n"
185_GOOD_SOURCE = """
186[[gnu::noinline]] uint32_t ra8_mdl_service_component_abi(void) { return 1U; }
187esp_err_t esp_hosted_custom_rpc_sync_handler(uint32_t id) { return ESP_OK; }
188"""
189
190SelftestCase = tuple[str, str, str, str, str, bool]
191
192
193def _selftest_staging_cases() -> list[SelftestCase]:
194 """Return the quiet control and staged-file/component drift cases."""
195 return [
196 ("contract agrees", _GOOD_BUILD, _GOOD_PATCH, _GOOD_HEADER, _GOOD_SOURCE, False),
197 (
198 "staged source renamed away",
199 _GOOD_BUILD.replace("inc/ra8_mdl_http.h", "inc/mdl_http.h", 1),
200 _GOOD_PATCH,
201 _GOOD_HEADER,
202 _GOOD_SOURCE,
203 True,
204 ),
205 (
206 "destination kept stale basename",
207 _GOOD_BUILD.replace("include/ra8_mdl_protocol.h", "include/mdl_protocol.h"),
208 _GOOD_PATCH,
209 _GOOD_HEADER,
210 _GOOD_SOURCE,
211 True,
212 ),
213 (
214 "required copy removed",
215 _GOOD_BUILD.replace(
216 'cp "${SCRIPT_DIR}/../../port/esp32_c6/CMakeLists.txt" '
217 '"${COMPONENT_DIR}/CMakeLists.txt"\n',
218 "",
219 ),
220 _GOOD_PATCH,
221 _GOOD_HEADER,
222 _GOOD_SOURCE,
223 True,
224 ),
225 (
226 "component identity drifted",
227 _GOOD_BUILD,
228 _GOOD_PATCH.replace("main mdl_service", "main ra8_mdl_service"),
229 _GOOD_HEADER,
230 _GOOD_SOURCE,
231 True,
232 ),
233 (
234 "component assignment became nonliteral",
235 _GOOD_BUILD.replace(
236 ' COMPONENT_DIR="${PERIPHERAL_DIR}/components/mdl_service"',
237 ' COMPONENT_DIR="${PERIPHERAL_DIR}/components/${COMPONENT_NAME}"',
238 ),
239 _GOOD_PATCH,
240 _GOOD_HEADER,
241 _GOOD_SOURCE,
242 True,
243 ),
244 ]
245
246
247def _selftest_symbol_cases() -> list[SelftestCase]:
248 """Return public/private/post-link ABI and hook drift cases."""
249 return [
250 (
251 "public ABI name drifted",
252 _GOOD_BUILD,
253 _GOOD_PATCH,
254 _GOOD_HEADER.replace(COMPONENT_ABI, "mdl_service_component_abi"),
255 _GOOD_SOURCE,
256 True,
257 ),
258 (
259 "ABI became private",
260 _GOOD_BUILD,
261 _GOOD_PATCH,
262 _GOOD_HEADER,
263 _GOOD_SOURCE.replace("[[gnu::noinline]] uint32_t", "static uint32_t"),
264 True,
265 ),
266 (
267 "post-link ABI name drifted",
268 _GOOD_BUILD.replace(COMPONENT_ABI, "mdl_service_component_abi"),
269 _GOOD_PATCH,
270 _GOOD_HEADER,
271 _GOOD_SOURCE,
272 True,
273 ),
274 (
275 "weak hook disappeared",
276 _GOOD_BUILD,
277 _GOOD_PATCH.replace(CUSTOM_RPC_HOOK, "custom_rpc_sync_handler"),
278 _GOOD_HEADER,
279 _GOOD_SOURCE,
280 True,
281 ),
282 ]
283
284
285def _selftest_cases() -> list[SelftestCase]:
286 """Return one quiet control and one case for each protected seam."""
287 return _selftest_staging_cases() + _selftest_symbol_cases()
288
289
290def selftest() -> int:
291 """Prove the detector fires on drift and stays quiet on agreement."""
292 inventory = frozenset(source for source, _dest in parse_staged_copies(_GOOD_BUILD))
293 failures: list[str] = []
294 for label, build, patch, header, source, expect in _selftest_cases():
295 findings = check_contract(build, patch, header, source, inventory)
296 if bool(findings) != expect:
297 verb = "reported nothing" if expect else f"reported {findings}"
298 failures.append(f" {label}: {verb}")
299 if failures:
300 sys.stderr.write("check_c6_integration.py --selftest: FAILED\n")
301 sys.stderr.write("\n".join(failures) + "\n")
302 return EXIT_FAIL
303 cases = _selftest_cases()
304 fires = sum(1 for case in cases if case[-1])
305 print(
306 f"check_c6_integration.py --selftest: OK "
307 f"({len(cases)} cases: {fires} fire, {len(cases) - fires} stays quiet)."
308 )
309 return EXIT_OK
310
311
312def main(argv: list[str]) -> int:
313 """Run the selftest or validate the real committed integration recipe."""
314 if "--selftest" in argv[1:]:
315 return selftest()
316 required = (BUILD_SCRIPT, PATCH_FILE, SERVICE_HEADER, SERVICE_SOURCE)
317 for path in required:
318 if not path.is_file():
319 sys.stderr.write(f"check_c6_integration.py: FATAL -- missing {path}\n")
320 return EXIT_CONFIG
321 findings = check_contract(*(path.read_text(encoding="utf-8") for path in required))
322 if findings:
323 sys.stderr.write(f"check_c6_integration.py: {len(findings)} C6 integration drift(s):\n")
324 for finding in findings:
325 sys.stderr.write(f" {finding}\n")
326 return EXIT_FAIL
327 print("check_c6_integration.py: C6 staging/component/ABI contract agrees.")
328 return EXIT_OK
329
330
331if __name__ == "__main__":
332 raise SystemExit(main(sys.argv))
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298