ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_c6_patch_upstream.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"""Apply-check the C6 patch series against its exact fetched upstream pin.
5
6The per-push patch gate is deliberately offline: it proves the pin, numbered
7series, and build entry point remain connected. This networked companion runs
8in the controlled weekly SOUP refresh job. It fetches only the exact immutable
9commit into a disposable repository, verifies the checkout identity, then
10applies the committed series without building, flashing, or contacting a rig.
11"""
12
13from __future__ import annotations
14
15import argparse
16import os
17import re
18import subprocess
19import sys
20import tempfile
21from pathlib import Path
22
23REPO_ROOT = Path(__file__).resolve().parents[2]
24sys.path.insert(0, str(REPO_ROOT / "scripts" / "dev"))
25
26from git_environment import sanitized_git_environment # noqa: E402 -- repository path added above
27
28C6_DIR = REPO_ROOT / "coprocessor" / "esp32c6"
29PINS_FILE = C6_DIR / "pins.env"
30SERIES_FILE = C6_DIR / "patches" / "series"
31EXPECTED_UPSTREAM = "https://github.com/espressif/esp-hosted-mcu"
32PATCH_NAME_RE = re.compile(r"^[0-9]{4}-[a-z0-9][a-z0-9-]*\.patch$")
33COMMIT_RE = re.compile(r"^[0-9a-f]{40}$")
34
35
36def _git_environment() -> dict[str, str]:
37 """Return a noninteractive Git environment with no host-level filters."""
38 environment = sanitized_git_environment()
39 environment.update(
40 {
41 "GIT_CONFIG_GLOBAL": os.devnull,
42 "GIT_CONFIG_NOSYSTEM": "1",
43 "GIT_LFS_SKIP_SMUDGE": "1",
44 "GIT_TERMINAL_PROMPT": "0",
45 }
46 )
47 return environment
48
49
50def _git(cwd: Path, *args: str, timeout: int = 300) -> subprocess.CompletedProcess[str]:
51 """Run one bounded Git command in the disposable repository."""
52 return subprocess.run( # noqa: S603 -- fixed Git and controlled argv
53 [ # noqa: S607 -- Git is a required repository tool
54 "git",
55 "-c",
56 "core.hooksPath=/dev/null",
57 "-c",
58 "http.followRedirects=false",
59 "-c",
60 "http.sslVerify=true",
61 "-c",
62 "protocol.file.allow=never",
63 "-c",
64 "protocol.ext.allow=never",
65 "-C",
66 str(cwd),
67 *args,
68 ],
69 env=_git_environment(),
70 text=True,
71 capture_output=True,
72 check=False,
73 timeout=timeout,
74 )
75
76
77def _pin(text: str, name: str) -> str:
78 """Return one unquoted strict KEY=value row."""
79 prefix = name + "="
80 values = [line[len(prefix) :] for line in text.splitlines() if line.startswith(prefix)]
81 if len(values) != 1 or not values[0] or any(char.isspace() for char in values[0]):
82 msg = f"{PINS_FILE.relative_to(REPO_ROOT)}: expected one strict {name}=value row"
83 raise ValueError(msg)
84 return values[0]
85
86
87def load_inputs() -> tuple[str, str, tuple[Path, ...]]:
88 """Load and validate the allowlisted upstream, full commit, and series."""
89 pins = PINS_FILE.read_text(encoding="utf-8")
90 upstream = _pin(pins, "ESP_HOSTED_MCU_URL")
91 commit = _pin(pins, "ESP_HOSTED_MCU_COMMIT")
92 if upstream != EXPECTED_UPSTREAM:
93 msg = f"refusing unallowlisted C6 upstream: {upstream!r}"
94 raise ValueError(msg)
95 if not COMMIT_RE.fullmatch(commit):
96 msg = "ESP_HOSTED_MCU_COMMIT must be one full lowercase 40-hex commit"
97 raise ValueError(msg)
98 names = tuple(
99 line.strip()
100 for line in SERIES_FILE.read_text(encoding="utf-8").splitlines()
101 if line.strip() and not line.lstrip().startswith("#")
102 )
103 if not names or len(names) != len(set(names)):
104 msg = "C6 patch series must be nonempty and contain no duplicates"
105 raise ValueError(msg)
106 if any(PATCH_NAME_RE.fullmatch(name) is None for name in names):
107 msg = "C6 patch series contains a non-numbered patch name"
108 raise ValueError(msg)
109 patches = tuple(SERIES_FILE.parent / name for name in names)
110 missing = [path.name for path in patches if not path.is_file()]
111 if missing:
112 msg = f"C6 patch series is missing: {', '.join(missing)}"
113 raise ValueError(msg)
114 return upstream, commit, patches
115
116
117def check_checkout(checkout: Path, commit: str, patches: tuple[Path, ...]) -> list[str]:
118 """Verify identity, non-vacuous apply, and exact reverse replay."""
119 findings: list[str] = []
120 identity = _git(checkout, "rev-parse", "HEAD")
121 actual = identity.stdout.strip() if identity.returncode == 0 else ""
122 if actual != commit:
123 findings.append(f"checkout is {actual or 'unreadable'}, expected exact pin {commit}")
124 return findings
125 if not patches:
126 return ["patch series is empty; applicability proof would be vacuous"]
127 for patch in patches:
128 check = _git(checkout, "apply", "--unidiff-zero", "--check", str(patch.resolve()))
129 if check.returncode != 0:
130 findings.append(f"{patch.name}: apply-check failed: {check.stderr.strip()}")
131 break
132 apply = _git(checkout, "apply", "--unidiff-zero", str(patch.resolve()))
133 if apply.returncode != 0:
134 findings.append(f"{patch.name}: checked but could not apply: {apply.stderr.strip()}")
135 break
136 if findings:
137 return findings
138 changed = _git(checkout, "diff", "--quiet", "--exit-code")
139 if changed.returncode == 0:
140 return ["applied series leaves no changed bytes; applicability proof is vacuous"]
141 if changed.returncode != 1:
142 return [f"could not inspect applied patch bytes: {changed.stderr.strip()}"]
143 for patch in reversed(patches):
144 reverse_check = _git(
145 checkout,
146 "apply",
147 "--unidiff-zero",
148 "--reverse",
149 "--check",
150 str(patch.resolve()),
151 )
152 if reverse_check.returncode != 0:
153 findings.append(
154 f"{patch.name}: reverse apply-check failed: {reverse_check.stderr.strip()}"
155 )
156 break
157 reverse = _git(
158 checkout,
159 "apply",
160 "--unidiff-zero",
161 "--reverse",
162 str(patch.resolve()),
163 )
164 if reverse.returncode != 0:
165 findings.append(f"{patch.name}: reverse apply failed: {reverse.stderr.strip()}")
166 break
167 restored = _git(checkout, "status", "--porcelain=v1", "--untracked-files=all")
168 if not findings and (restored.returncode != 0 or restored.stdout):
169 findings.append("reverse replay did not restore the exact clean pinned checkout")
170 return findings
171
172
173def verify_upstream() -> list[str]:
174 """Fetch only the exact pin and apply-check the series in a temp tree."""
175 upstream, commit, patches = load_inputs()
176 with tempfile.TemporaryDirectory(prefix="ra8-c6-patch-upstream-") as raw_tmp:
177 checkout = Path(raw_tmp) / "esp-hosted-mcu"
178 checkout.mkdir()
179 init = _git(checkout, "init", "--quiet")
180 if init.returncode != 0:
181 return [f"git init failed: {init.stderr.strip()}"]
182 fetch = _git(
183 checkout,
184 "fetch",
185 "--quiet",
186 "--no-tags",
187 "--depth=1",
188 upstream,
189 commit,
190 )
191 if fetch.returncode != 0:
192 return [f"exact-pin fetch failed: {fetch.stderr.strip()}"]
193 checkout_pin = _git(checkout, "checkout", "--quiet", "--detach", "FETCH_HEAD")
194 if checkout_pin.returncode != 0:
195 return [f"exact-pin checkout failed: {checkout_pin.stderr.strip()}"]
196 return check_checkout(checkout, commit, patches)
197
198
199def _fixture(root: Path, content: str) -> str:
200 """Create one committed source tree and return its exact commit."""
201 root.mkdir()
202 if _git(root, "init", "--quiet").returncode != 0:
203 msg = "selftest git init failed"
204 raise RuntimeError(msg)
205 (root / "source.txt").write_text(content, encoding="ascii")
206 for args in (
207 ("config", "user.email", "selftest@invalid"),
208 ("config", "user.name", "selftest"),
209 ("add", "source.txt"),
210 ("commit", "--quiet", "-m", "fixture"),
211 ):
212 result = _git(root, *args)
213 if result.returncode != 0:
214 msg = f"selftest git {' '.join(args)} failed: {result.stderr}"
215 raise RuntimeError(msg)
216 return _git(root, "rev-parse", "HEAD").stdout.strip()
217
218
219def selftest() -> int:
220 """Prove apply/reverse succeeds and context, identity, or scope drift fires."""
221 failures: list[str] = []
222 patch_text = """diff --git a/source.txt b/source.txt
223--- a/source.txt
224+++ b/source.txt
225@@ -1 +1 @@
226-before
227+after
228"""
229 with tempfile.TemporaryDirectory(prefix="ra8-c6-patch-selftest-") as raw_tmp:
230 root = Path(raw_tmp)
231 patch = root / "0001-fixture.patch"
232 patch.write_text(patch_text, encoding="ascii")
233 good = root / "good"
234 good_pin = _fixture(good, "before\n")
235 if check_checkout(good, good_pin, (patch,)):
236 failures.append("exact-pin applicable patch did not stay quiet")
237 drift = root / "drift"
238 drift_pin = _fixture(drift, "different\n")
239 if not check_checkout(drift, drift_pin, (patch,)):
240 failures.append("patch context drift did not fire")
241 wrong_pin = root / "wrong-pin"
242 actual_pin = _fixture(wrong_pin, "before\n")
243 if not check_checkout(wrong_pin, "0" * len(actual_pin), (patch,)):
244 failures.append("checkout identity drift did not fire")
245 empty = root / "empty"
246 empty_pin = _fixture(empty, "before\n")
247 if not check_checkout(empty, empty_pin, ()):
248 failures.append("empty patch series did not fire")
249 if failures:
250 print("check_c6_patch_upstream.py --selftest: FAIL", file=sys.stderr)
251 for failure in failures:
252 print(f" {failure}", file=sys.stderr)
253 return 1
254 print("check_c6_patch_upstream.py --selftest: PASS (4 cases, both directions)")
255 return 0
256
257
258def main() -> int:
259 """Run isolated selftests or the controlled exact-pin network check."""
260 parser = argparse.ArgumentParser(description=__doc__)
261 group = parser.add_mutually_exclusive_group(required=True)
262 group.add_argument("--selftest", action="store_true")
263 group.add_argument("--verify-upstream", action="store_true")
264 args = parser.parse_args()
265 if args.selftest:
266 return selftest()
267 try:
268 findings = verify_upstream()
269 except (OSError, ValueError, subprocess.SubprocessError) as exc:
270 findings = [str(exc)]
271 if findings:
272 print("check_c6_patch_upstream.py: FAIL", file=sys.stderr)
273 for finding in findings:
274 print(f" {finding}", file=sys.stderr)
275 return 1
276 _upstream, commit, patches = load_inputs()
277 print(
278 f"check_c6_patch_upstream.py: {len(patches)} non-vacuous patch(es) "
279 f"apply and reverse cleanly at exact pin {commit}"
280 )
281 return 0
282
283
284if __name__ == "__main__":
285 raise SystemExit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298