From 1abe7b72370205fb183edb6633cad51c18284ce4 Mon Sep 17 00:00:00 2001 From: AI Dev Date: Tue, 25 Aug 2026 19:46:23 +0000 Subject: [PATCH 1/2] ci: watch for drift from firmware instead of finding it by accident builder.sh copies devices//* over a fresh firmware clone, so a device directory that ships its own copy of a firmware file replaces it outright. A fleet-wide change in firmware reaches every device that only references a path and none that ships its own copy, silently, because nothing in either repository is looking. One comment on OpenIPC/firmware#2308 turned up three instances of that in an afternoon: 13 board configs that missed the CONFIG_VT sweep (#126), 97 defconfigs that kept BR2_PACKAGE_JSONFILTER after firmware retired it and put three devices over their rootfs cap (#128), and excludes lists naming files that no longer exist (firmware#2313 reports those at build time). The trigger is the design. Every one of those was caused by a commit in firmware while this repository sat untouched, so a pull_request check would never have fired for any of them -- nobody opens a builder PR when firmware changes. firmware-drift.yml runs on a schedule and files an issue rather than failing: drift someone else introduced is not a reason to redden a nightly that is otherwise fine. Only the self-test gates a merge here, out of lint.yml. Two checks, because the two kinds of drift are not the same shape. Shadowed files: comparing content is useless, a builder copy is *supposed* to differ. What matters is whether firmware's version moved since a human last reconciled them, so each of the 13 entries pins the blob it was reconciled against. Re-pinning is the act of looking again. The mapping is hand-authored -- nothing can infer that gk7205v200.generic-fpv derives from gk7205v200.generic. Defconfig symbols: nothing here copies a firmware defconfig, so pinning does not apply. Every BR2_PACKAGE_*=y is resolved against buildroot, firmware and this tree and sorted into "resolves nowhere" (a dead line kconfig ignores) and "resolves, but no firmware defconfig selects it" (needs an allowlist entry). An allowlist entry can also fence a symbol to the devices that need it; without that, writing JSONFILTER down as builder-only for devices/apfpv would equally bless it creeping back onto 95 unrelated defconfigs, which is #128 verbatim. Replayed against the three regressions, each reproduced by hand on a clean tree: firmware moves a shadowed config -> br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config moved in firmware since this copy was reconciled (2026-08-25). builder copy: devices/common/.../gk7205v200.generic-fpv.config pinned 7450c6803f6d, firmware now f97dce84c0f8 firmware commits since: 85233f0e kernel: drop CONFIG_VT and the busybox applets ... (#2308) jsonfilter creeps back onto a non-apfpv device -> BR2_PACKAGE_JSONFILTER is allowlisted only for apfpv/*/configs/*_defconfig, but 1 other defconfig(s) select it. firmware retires a symbol builder still selects -> BR2_PACKAGE_LIBUBOX is selected here but by no firmware defconfig. Resolving against buildroot is not optional and getting it wrong is how a checker lies. Buildroot is vendored by neither repository, so a first pass over OpenIPC packages alone called HOSTAPD, IW, PHP, UHTTPD, LIBZIP and BWM_NG dead; all six are upstream. Matching only "Config.in" then called PHP_EXT_ZIP dead, because buildroot declares php's extensions in package/php/Config.ext. Without --buildroot that half is skipped and says so rather than inventing findings. Two symbols really do resolve nowhere today -- BR2_PACKAGE_APFPV_GREG on the four greg-generic devices and BR2_PACKAGE_WIFIBROADCAST_EXT in devices/common. Both are recorded as known-dead and reported as a notice on every run rather than removed: whether those devices still want the packages is a maintainer's call, not a side effect of adding a check. The three new paths are classified in ci-matrix.py so this PR and every future re-pin cost no device builds. Without that a blob update is a 107-device run. --- .github/firmware-drift.json | 151 ++++++++++ .github/scripts/check-firmware-drift.py | 374 ++++++++++++++++++++++++ .github/scripts/ci-matrix.py | 13 +- .github/workflows/firmware-drift.yml | 125 ++++++++ .github/workflows/lint.yml | 8 + 5 files changed, 669 insertions(+), 2 deletions(-) create mode 100644 .github/firmware-drift.json create mode 100644 .github/scripts/check-firmware-drift.py create mode 100644 .github/workflows/firmware-drift.yml diff --git a/.github/firmware-drift.json b/.github/firmware-drift.json new file mode 100644 index 000000000..da7ddb514 --- /dev/null +++ b/.github/firmware-drift.json @@ -0,0 +1,151 @@ +{ + "_comment": [ + "Records what has been reconciled against OpenIPC/firmware, so that a fleet-wide", + "change there cannot move under this repository unnoticed. See", + ".github/scripts/check-firmware-drift.py for why this exists and what each key means.", + "A pinned blob means a human looked at firmware's version of that file and was", + "satisfied; re-pinning it is the act of looking again." + ], + "shadows": [ + { + "builder": "devices/common/br-ext-chip-goke/board/gk7205v200/gk7205v200.generic-fpv.config", + "firmware": "br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config", + "blob": "f97dce84c0f85d1a08a45bb7a173448d4a060e75", + "reconciled": "2026-08-25", + "note": "fpv/lte/venc variant of the generic config; backs 5 gk7205v200/v210 builds" + }, + { + "builder": "devices/common/br-ext-chip-goke/board/gk7205v200/gk7205v300.generic-fpv.config", + "firmware": "br-ext-chip-goke/board/gk7205v200/gk7205v300.generic.config", + "blob": "1f36f13f42ed1b1616608e330c63719ae00a1e0a", + "reconciled": "2026-08-25", + "note": "fpv/lte/venc variant; backs 3 gk7205v300 builds" + }, + { + "builder": "devices/common/br-ext-chip-hisilicon/board/hi3516ev200/hi3516ev200.generic-fpv.config", + "firmware": "br-ext-chip-hisilicon/board/hi3516ev200/hi3516ev200.generic.config", + "blob": "bccb6da3b61ea9ee15a08c676b41b820ddfc96f4", + "reconciled": "2026-08-25", + "note": "fpv/lte variant" + }, + { + "builder": "devices/common/br-ext-chip-hisilicon/board/hi3516ev200/hi3516ev300.generic-fpv.config", + "firmware": "br-ext-chip-hisilicon/board/hi3516ev200/hi3516ev300.generic.config", + "blob": "dbf2a1630e2a81f30568a3a798cc777a5b7191f1", + "reconciled": "2026-08-25", + "note": "fpv/lte variant" + }, + { + "builder": "devices/gk7202v300_lite_xg521/br-ext-chip-goke/board/gk7205v200/gk7202v300.generic.config", + "firmware": "br-ext-chip-goke/board/gk7205v200/gk7202v300.generic.config", + "blob": "a6b1534abde2aed025d61a1693bf9265176141df", + "reconciled": "2026-08-25", + "note": "device-local copy" + }, + { + "builder": "devices/gk7205v200_otg_generic/br-ext-chip-goke/board/gk7205v200/gk7205v200.generic-otg.config", + "firmware": "br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config", + "blob": "f97dce84c0f85d1a08a45bb7a173448d4a060e75", + "reconciled": "2026-08-25", + "note": "otg variant" + }, + { + "builder": "devices/gk7205v200_rubyfpv_generic/br-ext-chip-goke/board/gk7205v200/gk7205v200.generic-fpv.config", + "firmware": "br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config", + "blob": "f97dce84c0f85d1a08a45bb7a173448d4a060e75", + "reconciled": "2026-08-25", + "note": "rubyfpv variant" + }, + { + "builder": "devices/hi3516cv200_lite_trassir-tr-d4121ir1-v2/br-ext-chip-hisilicon/board/hi3516cv200/hi3516cv200.generic.config", + "firmware": "br-ext-chip-hisilicon/board/hi3516cv200/hi3516cv200.generic.config", + "blob": "68f8da5fb686dd93a405b0e2d8410db009b301f8", + "reconciled": "2026-08-25", + "note": "device-local copy" + }, + { + "builder": "devices/hi3518ev200_lite_lenovo-snowman-1080p/br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", + "firmware": "br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", + "blob": "92805f802503b38be9bb08843b5760e91bb3abd7", + "reconciled": "2026-08-25", + "note": "device-local copy" + }, + { + "builder": "devices/hi3518ev200_lite_vstarcam-c8892wip/br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", + "firmware": "br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", + "blob": "92805f802503b38be9bb08843b5760e91bb3abd7", + "reconciled": "2026-08-25", + "note": "device-local copy" + }, + { + "builder": "devices/hi3518ev200_ultimate_lenovo-snowman-1080p/br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", + "firmware": "br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", + "blob": "92805f802503b38be9bb08843b5760e91bb3abd7", + "reconciled": "2026-08-25", + "note": "device-local copy" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/br-ext-chip-goke/board/gk710x/gk7102c.generic.config", + "firmware": "br-ext-chip-goke/board/gk710x/gk710x.generic.config", + "blob": "ad93ad88301ebadeb5f9e3ea96a9d6439610272b", + "reconciled": "2026-08-25", + "note": "gk7102c variant of gk710x; keeps CONFIG_INPUT for its evdev consumer" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/br-ext-chip-goke/board/gk710x/gk7102c.generic.config", + "firmware": "br-ext-chip-goke/board/gk710x/gk710x.generic.config", + "blob": "ad93ad88301ebadeb5f9e3ea96a9d6439610272b", + "reconciled": "2026-08-25", + "note": "gk7102c variant of gk710x" + } + ], + "builder_only_symbols": { + "BR2_PACKAGE_ADAPTIVE_LINK": "FPV link adaptation; firmware ships the package but no firmware defconfig enables it", + "BR2_PACKAGE_ATBM_WIFI": "ATBM wifi, only retail models here carry the part", + "BR2_PACKAGE_ATBM_WIFI_INTERFACE_USB": "sub-option of ATBM_WIFI", + "BR2_PACKAGE_ATBM_WIFI_MODEL_6012B": "sub-option of ATBM_WIFI", + "BR2_PACKAGE_AUTONIGHT": "day/night switching for specific retail models", + "BR2_PACKAGE_BWM_NG": "upstream Buildroot bandwidth monitor, FPV debugging", + "BR2_PACKAGE_FFMPEG_OPENIPC": "only the waybeam/venc profiles need it", + "BR2_PACKAGE_HISILICON_OSDRV_HI3536DV100": "hi3536dv100 is a builder-only family", + "BR2_PACKAGE_HOSTAPD": "upstream Buildroot; AP mode for the apfpv profiles", + "BR2_PACKAGE_HOST_LINUX_HEADERS_CUSTOM_3_4": "upstream Buildroot; the 3.4 kernel families here", + "BR2_PACKAGE_IW": "upstream Buildroot; selected by wifibroadcast-ng, also set directly here", + "BR2_PACKAGE_JSONFILTER": { + "reason": "only devices/apfpv needs it: its own etc/init.d/S99msposd pipes majestic's config.json through jsonfilter, and those two devices do not enable wifibroadcast-ng, which carries the select for every other FPV device. See builder#128, which removed it from the 95 defconfigs that did not.", + "devices": [ + "apfpv/*/configs/*_defconfig" + ] + }, + "BR2_PACKAGE_KC110_BOARD_SUPPORT": "builder's own package, package/kc110-board-support", + "BR2_PACKAGE_LIBZIP": "upstream Buildroot; PHP webui profiles", + "BR2_PACKAGE_LINUX_FIRMWARE_OPENIPC_ATHEROS_9271": "ath9k_htc firmware for specific adapters", + "BR2_PACKAGE_LINUX_PATCHER_ATHEROS": "atheros patching for the FPV profiles", + "BR2_PACKAGE_MAVFWD": "FPV mavlink forwarding; selected by wifibroadcast-ng, also set directly", + "BR2_PACKAGE_MAVLINK_ROUTER": "FPV mavlink routing", + "BR2_PACKAGE_MINI": "the mini variant is builder-only", + "BR2_PACKAGE_MSPOSD": "FPV OSD; selected by wifibroadcast-ng, also set directly", + "BR2_PACKAGE_ONVIF_SIMPLE_SERVER": "ONVIF for retail models that advertise it", + "BR2_PACKAGE_PHP": "upstream Buildroot; the PHP webui profiles", + "BR2_PACKAGE_PHP_EXT_ZIP": "sub-option of PHP", + "BR2_PACKAGE_RCJOYSTICK": "FPV RC input", + "BR2_PACKAGE_RTL8192EU_OPENIPC": "wifi driver for specific retail models", + "BR2_PACKAGE_RTL8811CU_OPENIPC": "wifi driver for specific retail models", + "BR2_PACKAGE_RTL8812AU": "wifi driver for FPV adapters", + "BR2_PACKAGE_RTL8812AU_OPENIPC": "wifi driver for FPV adapters", + "BR2_PACKAGE_RTL88X2EU_OPENIPC": "wifi driver for FPV adapters", + "BR2_PACKAGE_RUBYFPV": "RubyFPV profiles are builder-only", + "BR2_PACKAGE_SSV635X_OPENIPC": "wifi driver for specific retail models", + "BR2_PACKAGE_UHTTPD": "upstream Buildroot; the PHP webui profiles", + "BR2_PACKAGE_VDEC_OPENIPC": "waybeam decode profile", + "BR2_PACKAGE_VENC_OPENIPC": "venc profiles are builder-only", + "BR2_PACKAGE_W1_DS18B20": "1-wire temperature sensor on specific models", + "BR2_PACKAGE_WEBUI": "the PHP webui, builder-only", + "BR2_PACKAGE_WIFIBROADCAST": "FPV, builder-only", + "BR2_PACKAGE_WIFIBROADCAST_NG": "FPV, builder-only" + }, + "known_dead_symbols": { + "BR2_PACKAGE_APFPV_GREG": "no Kconfig in buildroot, firmware or here, so kconfig ignores the line. Set by the four *_apfpv_greg-generic-* devices, which suggests a package that was never landed. Left in place deliberately: removing it is a maintainer's call about those devices, not a side effect of adding this check.", + "BR2_PACKAGE_WIFIBROADCAST_EXT": "no Kconfig anywhere; set in devices/common beside BR2_PACKAGE_WIFIBROADCAST_NG, which does exist. Probably a rename that left this behind. Same reasoning as APFPV_GREG." + } +} diff --git a/.github/scripts/check-firmware-drift.py b/.github/scripts/check-firmware-drift.py new file mode 100644 index 000000000..bd0eacb4e --- /dev/null +++ b/.github/scripts/check-firmware-drift.py @@ -0,0 +1,374 @@ +#!/usr/bin/env python3 +"""Report where this repository has drifted from OpenIPC/firmware. + +WHY THIS EXISTS. builder.sh copies devices//* over a fresh firmware clone +before building, so a device directory that ships its own copy of a firmware +file replaces it outright. A fleet-wide change in firmware therefore reaches +every device that only *references* a path, and none that ships its own copy -- +silently, because nothing in either repository is looking. + +That is not hypothetical. One forum comment on OpenIPC/firmware#2308 turned up +three separate instances of it in an afternoon: + + * 13 board kernel configs missed the CONFIG_VT/CONFIG_INPUT sweep that landed + as firmware #2308 and #2309, so 21 device builds kept building the + virtual-terminal layer (fixed in #126); + * 97 defconfigs kept BR2_PACKAGE_JSONFILTER after firmware #2304 dropped it + from every one of its own, which is what put three devices over their + rootfs cap (fixed in #128); + * the per-device excludes lists name files that no longer exist while missing + files that do (reported at build time by firmware#2313). + +THE TRIGGER IS THE WHOLE POINT. Every one of those was caused by a commit in +*firmware* while *builder* sat untouched. A pull_request check here would never +have fired -- nobody opens a builder PR when firmware changes. This has to run +on a schedule, and when it finds something the right response is an issue, not +a red nightly: drift someone else introduced is not a reason to break a build +that is otherwise fine. + +WHAT IT CANNOT DO is fix anything. Auto-syncing firmware's version over a +builder copy would overwrite deliberate board-specific settings, which is the +one thing these copies exist for. It detects, a human decides, and the decision +is recorded by updating .github/firmware-drift.json. That file is the artifact: +a pinned blob means "someone looked at firmware's version of this and was +satisfied", and re-pinning it is the act of looking again. + +CHECK 1, SHADOWED FILES. Content comparison is useless here -- a builder copy is +*supposed* to differ, that is why it exists. What matters is whether firmware's +version has moved since a human last reconciled the two, so each entry pins the +blob SHA it was reconciled against and this compares that to firmware HEAD. The +mapping is hand-authored because nothing can infer that gk7205v200.generic-fpv +derives from gk7205v200.generic. + +CHECK 2, DEFCONFIG SYMBOLS. Nothing here is a copy of a firmware defconfig, so +pinning does not apply; the drift is a symbol firmware retired that builder kept +selecting. Every BR2_PACKAGE_*=y in a builder defconfig is resolved against all +three Kconfig sources and sorted into: + + * resolves nowhere -> a dead line, silently ignored by kconfig; + * resolves, but no firmware defconfig selects it -> needs an allowlist entry. + +Both halves matter and the second is the one that catches a JSONFILTER. The +allowlist is not bureaucracy: RUBYFPV, MSPOSD and the rtl88xx drivers are +legitimately builder-only, and writing that down is what makes a *new* entry +mean something. An entry may be a bare reason string, or an object with a +`devices` list of globs fencing it to the defconfigs that need it -- without +that, allowlisting JSONFILTER for devices/apfpv would also bless it creeping +back onto 95 unrelated defconfigs, which is the #128 bug verbatim. + +RESOLVING AGAINST BUILDROOT IS NOT OPTIONAL, and getting this wrong is how the +check lies. Buildroot is not vendored in either repository, so a first pass that +consulted only OpenIPC packages reported HOSTAPD, IW, PHP, UHTTPD, LIBZIP and +BWM_NG as dead. All six are upstream Buildroot packages. Without --buildroot the +dead-symbol half is skipped and says so, rather than inventing findings. +""" + +import argparse +import fnmatch +import json +import os +import re +import subprocess +import sys + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +CONFIG = os.path.join(REPO_ROOT, ".github", "firmware-drift.json") + +SELECT = re.compile(r"^(BR2_PACKAGE_[A-Z0-9_]+)=y\s*$", re.M) + + +def kconfig_symbols(*trees): + """Every `config ` / `menuconfig ` declared under the given trees.""" + found = set() + pattern = re.compile(r"^\s*(?:menu)?config\s+([A-Za-z0-9_]+)\s*$", re.M) + for tree in trees: + if not tree or not os.path.isdir(tree): + continue + for root, _dirs, files in os.walk(tree): + for name in files: + # Config.in is not the only name kconfig sources. buildroot's + # php declares every extension in package/php/Config.ext, and + # matching only "Config.in" reported BR2_PACKAGE_PHP_EXT_ZIP as + # resolving nowhere -- a finding invented by the checker rather + # than found in the tree. Config.ext, Config.in.host and + # Config.in.options all exist upstream; take anything Config*. + if not name.startswith("Config"): + continue + path = os.path.join(root, name) + try: + with open(path, encoding="utf-8", errors="replace") as handle: + found.update(pattern.findall(handle.read())) + except OSError: + continue + return found + + +def selected_in(directory, pattern="*_defconfig"): + """symbol -> sorted list of defconfigs selecting it, under `directory`.""" + out = {} + for root, _dirs, files in os.walk(directory): + for name in files: + if not fnmatch.fnmatch(name, pattern): + continue + path = os.path.join(root, name) + try: + with open(path, encoding="utf-8", errors="replace") as handle: + body = handle.read() + except OSError: + continue + for sym in SELECT.findall(body): + out.setdefault(sym, []).append(os.path.relpath(path, directory)) + return {k: sorted(v) for k, v in out.items()} + + +def blob_sha(firmware, path): + """firmware HEAD's blob SHA for `path`, or None if it is not there.""" + try: + return subprocess.run( + ["git", "-C", firmware, "rev-parse", f"HEAD:{path}"], + capture_output=True, text=True, check=True).stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return None + + +def firmware_log(firmware, path, since): + try: + return subprocess.run( + ["git", "-C", firmware, "log", "--oneline", f"{since}..HEAD", "--", path], + capture_output=True, text=True, check=True).stdout.strip() + except (subprocess.CalledProcessError, FileNotFoundError): + return "" + + +def load_config(): + with open(CONFIG, encoding="utf-8") as handle: + return json.load(handle) + + +def check(firmware, buildroot, config, repo_root=REPO_ROOT): + """Return (findings, notices). A finding is drift; a notice is FYI.""" + findings, notices = [], [] + devices = os.path.join(repo_root, "devices") + + # --- Check 1: shadowed files --- + for entry in config.get("shadows", []): + builder_path = os.path.join(repo_root, entry["builder"]) + if not os.path.exists(builder_path): + findings.append( + f"shadow entry names {entry['builder']}, which is not in this tree; " + f"drop the entry or fix the path") + continue + if firmware is None: + continue + current = blob_sha(firmware, entry["firmware"]) + if current is None: + findings.append( + f"{entry['builder']} shadows {entry['firmware']}, which no longer " + f"exists in firmware; re-point or drop the entry") + continue + if current != entry["blob"]: + moved = firmware_log(firmware, entry["firmware"], entry["blob"]) + detail = f"\n firmware commits since:\n " + \ + "\n ".join(moved.splitlines()) if moved else "" + findings.append( + f"{entry['firmware']} moved in firmware since this copy was " + f"reconciled ({entry.get('reconciled', 'unknown date')}).\n" + f" builder copy: {entry['builder']}\n" + f" pinned {entry['blob'][:12]}, firmware now {current[:12]}" + f"{detail}") + + # --- Check 2: defconfig symbols --- + builder_selects = selected_in(devices) + firmware_selects = set() + if firmware is not None: + firmware_selects = set(selected_in(firmware).keys()) + + known = config.get("builder_only_symbols", {}) + dead = config.get("known_dead_symbols", {}) + + declared = None + if buildroot: + declared = kconfig_symbols( + os.path.join(buildroot, "package"), + os.path.join(firmware, "general", "package") if firmware else None, + os.path.join(repo_root, "package")) + else: + notices.append( + "no --buildroot given, so the dead-symbol half is skipped: upstream " + "Buildroot packages cannot be told apart from symbols that resolve " + "nowhere, and guessing produces false findings either way") + + for sym in sorted(builder_selects): + users = builder_selects[sym] + where = f"{users[0]}" + (f" (+{len(users) - 1} more)" if len(users) > 1 else "") + + if declared is not None and sym not in declared: + if sym in dead: + notices.append(f"{sym} still resolves to no Kconfig anywhere -- {dead[sym]}") + else: + findings.append( + f"{sym} resolves to no Kconfig in buildroot, firmware or here, " + f"so kconfig ignores the line.\n first seen in {where}") + continue + + # An allowlist entry may also fence the symbol to the devices that + # actually need it. Without that, allowlisting is all-or-nothing: once + # JSONFILTER was written down as builder-only for devices/apfpv, the + # same symbol creeping back onto 95 unrelated defconfigs -- exactly the + # #128 bug -- would have passed silently. + entry = known.get(sym) + if isinstance(entry, dict) and entry.get("devices"): + allowed = entry["devices"] + strays = [u for u in users + if not any(fnmatch.fnmatch(u, glob) for glob in allowed)] + if strays: + findings.append( + f"{sym} is allowlisted only for {', '.join(allowed)}, but " + f"{len(strays)} other defconfig(s) select it.\n" + f" first stray: {strays[0]}\n" + f" Either they need it too -- widen the devices list -- or " + f"this is the OpenIPC/firmware#2304 case again.") + + if firmware is None: + continue + if sym not in firmware_selects and sym not in known and sym not in dead: + findings.append( + f"{sym} is selected here but by no firmware defconfig.\n" + f" first seen in {where}\n" + f" Either firmware retired it (the OpenIPC/firmware#2304 case) or it " + f"is deliberately builder-only -- say which in builder_only_symbols.") + + # Rot in the config file itself. + for sym in sorted(set(known) | set(dead)): + if sym not in builder_selects: + findings.append( + f"{sym} is listed in firmware-drift.json but no defconfig selects it; " + f"drop the entry") + + return findings, notices + + +def self_test(): + """Exercise the classifier on synthetic trees; no network, no clones.""" + import tempfile + import textwrap + problems = [] + + with tempfile.TemporaryDirectory() as tmp: + repo = os.path.join(tmp, "builder") + d = os.path.join(repo, "devices", "thing", "br-ext-chip-x", "configs") + os.makedirs(d) + os.makedirs(os.path.join(repo, "package")) + with open(os.path.join(d, "thing_defconfig"), "w") as handle: + handle.write("BR2_PACKAGE_KNOWN=y\nBR2_PACKAGE_RETIRED=y\n" + "BR2_PACKAGE_NOWHERE=y\nBR2_PACKAGE_SHARED=y\n") + + br = os.path.join(tmp, "buildroot", "package", "known") + os.makedirs(br) + with open(os.path.join(br, "Config.in"), "w") as handle: + handle.write("config BR2_PACKAGE_KNOWN\n\tbool \"known\"\n") + for sym in ("RETIRED", "SHARED"): + sub = os.path.join(tmp, "buildroot", "package", sym.lower()) + os.makedirs(sub) + with open(os.path.join(sub, "Config.in"), "w") as handle: + handle.write(f"config BR2_PACKAGE_{sym}\n\tbool \"{sym}\"\n") + + fw = os.path.join(tmp, "firmware", "br-ext-chip-y", "configs") + os.makedirs(fw) + with open(os.path.join(fw, "board_defconfig"), "w") as handle: + handle.write("BR2_PACKAGE_SHARED=y\n") + + cfg = {"shadows": [], + "builder_only_symbols": {"BR2_PACKAGE_KNOWN": "builder-only on purpose"}, + "known_dead_symbols": {}} + findings, notices = check(os.path.join(tmp, "firmware"), + os.path.join(tmp, "buildroot"), cfg, repo_root=repo) + blob = "\n".join(findings) + + def want(cond, what): + if not cond: + problems.append(what) + + want(any("BR2_PACKAGE_NOWHERE" in f and "no Kconfig" in f for f in findings), + "a symbol declared nowhere must be reported as a dead line") + want(any("BR2_PACKAGE_RETIRED" in f and "no firmware defconfig" in f for f in findings), + "a symbol firmware no longer selects must be reported") + want("BR2_PACKAGE_KNOWN" not in blob, + "an allowlisted builder-only symbol must not be reported") + want("BR2_PACKAGE_SHARED" not in blob, + "a symbol firmware also selects must not be reported") + + # Without buildroot the dead-symbol half must go quiet rather than guess. + findings2, notices2 = check(os.path.join(tmp, "firmware"), None, cfg, repo_root=repo) + want(not any("no Kconfig" in f for f in findings2), + "without --buildroot the dead-symbol half must be skipped, not guessed") + want(any("--buildroot" in n for n in notices2), + "skipping the dead-symbol half must be said out loud") + + # Rot: an allowlist entry nothing selects any more. + cfg2 = dict(cfg, builder_only_symbols={"BR2_PACKAGE_KNOWN": "x", + "BR2_PACKAGE_VANISHED": "y"}) + findings3, _ = check(os.path.join(tmp, "firmware"), + os.path.join(tmp, "buildroot"), cfg2, repo_root=repo) + want(any("BR2_PACKAGE_VANISHED" in f and "drop the entry" in f for f in findings3), + "an allowlist entry no defconfig selects must be reported as rot") + + # The shipped config must describe this tree, the same way ci-matrix.py's + # NOT_BUILT must. An entry for a device that was renamed is a name + # describing nothing. + config = load_config() + for entry in config.get("shadows", []): + if not os.path.exists(os.path.join(REPO_ROOT, entry["builder"])): + problems.append(f"shadows: {entry['builder']} is not in this tree") + for key in ("firmware", "blob", "reconciled"): + if not entry.get(key): + problems.append(f"shadows: {entry['builder']} has no {key}") + + if problems: + for problem in problems: + print(f" - {problem}") + print(f"\ncheck-firmware-drift: self-test FAILED ({len(problems)} problem(s))") + return 1 + counts = (len(config.get("shadows", [])), + len(config.get("builder_only_symbols", {})), + len(config.get("known_dead_symbols", {}))) + print("check-firmware-drift: self-test ok " + f"({counts[0]} shadowed files, {counts[1]} builder-only symbols, " + f"{counts[2]} known-dead symbols)") + return 0 + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--firmware", help="path to an OpenIPC/firmware checkout") + parser.add_argument("--buildroot", help="path to an extracted buildroot tree") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + + if args.self_test: + return self_test() + + if not args.firmware: + parser.error("--firmware is required (or use --self-test)") + + findings, notices = check(args.firmware, args.buildroot, load_config()) + + for notice in notices: + print(f"note: {notice}") + if notices: + print() + + if not findings: + print("check-firmware-drift: no drift from firmware.") + return 0 + + print(f"check-firmware-drift: {len(findings)} finding(s)\n") + for finding in findings: + print(f" - {finding}") + print("\nEach of these is a decision, not a build failure. Reconcile the copy or\n" + "the defconfig, then record the decision in .github/firmware-drift.json.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/scripts/ci-matrix.py b/.github/scripts/ci-matrix.py index 201cdcac3..c5dfb7f31 100644 --- a/.github/scripts/ci-matrix.py +++ b/.github/scripts/ci-matrix.py @@ -78,10 +78,16 @@ # whole filename, never as a prefix: one this list has never heard of is unknown, # and unknown widens. package.sh and repack.sh are developer and end-user tools # that no workflow invokes -- only builder.sh is on the CI path. -NO_BUILD_WORKFLOWS = {"build-one.yml", "cleanup.yml", "lint.yml", "manifest.yml"} -NO_BUILD_SCRIPTS = {"enrich_manifest.py", "lint-workflow-shell.py"} +NO_BUILD_WORKFLOWS = {"build-one.yml", "cleanup.yml", "lint.yml", "manifest.yml", + "firmware-drift.yml"} +NO_BUILD_SCRIPTS = {"enrich_manifest.py", "lint-workflow-shell.py", + "check-firmware-drift.py"} NO_BUILD_FILES = { ".github/CODEOWNERS", ".gitignore", "LICENSE", "package.sh", "repack.sh", + # The drift checker's config. It records what has been reconciled against + # OpenIPC/firmware; nothing reads it during a build, so re-pinning a blob + # must not cost 107 device builds. + ".github/firmware-drift.json", } # CI plumbing: decides how the build runs, cannot change a byte of what it @@ -438,6 +444,9 @@ def self_test(): ([".github/workflows/lint.yml"], 0, "the workflow linter never builds"), ([".github/scripts/lint-workflow-shell.py"], 0, "its script"), (["repack.sh"], 0, "end-user tool, no workflow runs it"), + ([".github/workflows/firmware-drift.yml"], 0, "the drift watcher never builds"), + ([".github/scripts/check-firmware-drift.py"], 0, "its script"), + ([".github/firmware-drift.json"], 0, "and what it reconciles against"), (["package.sh"], 0, "developer tool, no workflow runs it"), (["archive/gk7205v200_fpv/202607231714/openipc.tgz"], 0, "build output"), # Anchoring: the same names elsewhere are not them. diff --git a/.github/workflows/firmware-drift.yml b/.github/workflows/firmware-drift.yml new file mode 100644 index 000000000..4cad42dd6 --- /dev/null +++ b/.github/workflows/firmware-drift.yml @@ -0,0 +1,125 @@ +name: firmware-drift +on: + # Scheduled, and that is the whole design. Every instance of this drift was + # caused by a commit in OpenIPC/firmware while this repository sat untouched, + # so a pull_request trigger would never have fired for any of them -- nobody + # opens a builder PR when firmware changes. An hour after firmware's own + # nightly, so the two do not race for a picture of the same HEAD. + schedule: + - cron: '0 5 * * *' + workflow_dispatch: + +# Reads both trees and files an issue. No write access to code. +permissions: + contents: read + issues: write + +jobs: + drift: + name: drift from OpenIPC/firmware + # Same guard the jobs in master.yml and lint.yml carry, for the same reason: + # a clone inherits this file, and a schedule fires on the fork's bill. + if: >- + github.repository == 'OpenIPC/builder' || + github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + # Full history, not --depth=1: the report names the firmware commits that + # touched a shadowed file since it was last reconciled, and a shallow + # clone cannot walk back to the pinned blob to find them. + - name: Clone firmware + run: git clone -q https://github.com/OpenIPC/firmware.git "$RUNNER_TEMP/firmware" + + # Buildroot is vendored by neither repository, so without it a symbol + # declared upstream is indistinguishable from one declared nowhere. The + # checker skips that half rather than guess, but skipping it here would + # give up the check that finds dead defconfig lines. Version comes from + # firmware's Makefile so this cannot drift from what builds actually use. + - name: Fetch the buildroot firmware pins + id: br + run: | + BR_VER=$(sed -n 's/^BR_VER *= *//p' "$RUNNER_TEMP/firmware/Makefile" | head -1) + if [ -z "$BR_VER" ]; then + echo "::error::could not read BR_VER from firmware's Makefile" + exit 1 + fi + echo "ver=$BR_VER" >> "$GITHUB_OUTPUT" + echo "Buildroot $BR_VER" + + - name: Cache buildroot + id: cache + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/buildroot + key: buildroot-kconfig-${{ steps.br.outputs.ver }} + + # Only the Kconfig files are wanted -- the checker reads declarations and + # nothing else -- so unpack those and leave the rest of a 6MB tarball on + # the floor. + - name: Unpack buildroot Kconfig + if: steps.cache.outputs.cache-hit != 'true' + run: | + set -eu + ver='${{ steps.br.outputs.ver }}' + mkdir -p "$RUNNER_TEMP/buildroot" + wget -q "https://github.com/buildroot/buildroot/archive/${ver}.tar.gz" \ + -O "$RUNNER_TEMP/br.tar.gz" + tar -xzf "$RUNNER_TEMP/br.tar.gz" -C "$RUNNER_TEMP/buildroot" \ + --strip-components=1 --wildcards '*/package/*/Config*' + find "$RUNNER_TEMP/buildroot" -name 'Config*' | wc -l + + - name: Check the checker still catches what it should + run: python3 .github/scripts/check-firmware-drift.py --self-test + + - name: Look for drift + id: check + run: | + set -o pipefail + if python3 .github/scripts/check-firmware-drift.py \ + --firmware "$RUNNER_TEMP/firmware" \ + --buildroot "$RUNNER_TEMP/buildroot" 2>&1 | tee "$RUNNER_TEMP/report.txt"; then + echo "drift=no" >> "$GITHUB_OUTPUT" + else + echo "drift=yes" >> "$GITHUB_OUTPUT" + fi + + # An issue, not a failed job. This drift is introduced by commits in + # another repository, so failing here would paint the nightly red for + # something no builder change caused and nobody here can see from a diff. + # One issue, reopened and re-bodied rather than duplicated, so a week of + # unattended drift is one thread and not seven. + - name: File or refresh the drift issue + if: steps.check.outputs.drift == 'yes' + env: + GH_TOKEN: ${{ github.token }} + run: | + set -eu + title='Drift from OpenIPC/firmware' + body="$(printf 'Found by `.github/workflows/firmware-drift.yml` on %s (run %s).\n\n```\n%s\n```\n\nEach finding is a decision, not a build failure. Reconcile the copy or the defconfig, then record the decision in `.github/firmware-drift.json` — re-pinning a blob is the act of saying someone looked.\n' \ + "$(date -u +%Y-%m-%d)" \ + "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \ + "$(cat "$RUNNER_TEMP/report.txt")")" + existing=$(gh issue list --repo "$GITHUB_REPOSITORY" --state all \ + --search "$title in:title" --json number,title,state \ + --jq ".[] | select(.title == \"$title\") | .number" | head -1) + if [ -n "$existing" ]; then + gh issue edit "$existing" --repo "$GITHUB_REPOSITORY" --body "$body" + gh issue reopen "$existing" --repo "$GITHUB_REPOSITORY" 2>/dev/null || true + echo "refreshed issue #$existing" + else + gh issue create --repo "$GITHUB_REPOSITORY" --title "$title" --body "$body" + fi + + - name: Summarise + if: always() + run: | + { + echo '## Drift from OpenIPC/firmware' + echo + echo '```' + cat "$RUNNER_TEMP/report.txt" 2>/dev/null || echo 'no report produced' + echo '```' + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 26caa14d7..cb57e7953 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -64,3 +64,11 @@ jobs: run: python3 .github/scripts/lint-workflow-shell.py --self-test - name: Parse every workflow run block run: python3 .github/scripts/lint-workflow-shell.py + # The drift checker's own config has to keep describing this tree, the + # same way ci-matrix.py's NOT_BUILT does: a shadow entry for a device that + # was renamed is a name describing nothing, and it would go quiet exactly + # when it mattered. Self-test only -- the real check needs a firmware + # clone and runs on a schedule, because the drift it looks for is caused + # by commits in that repository and not by anything in a PR here. + - name: Check the drift checker still describes the tree + run: python3 .github/scripts/check-firmware-drift.py --self-test From 6b5b6771d81b5f16b55f148e8408e946fd0ef139 Mon Sep 17 00:00:00 2001 From: AI Dev Date: Tue, 25 Aug 2026 21:36:06 +0000 Subject: [PATCH 2/2] ci: discover shadowed files instead of listing them by hand Qodo caught the shadow list omitting the device-local t21 and t31 kernel configs. It was right, and the hole was much bigger than the five it named: a hand-written list of 13 board configs against 93 same-path shadows covering 35 firmware paths. The missing ones are not incidental -- load_goke, load_hisilicon, load_sigmastar and four vendor .mk files are all shadowed by some device, and those are exactly the shared files a fleet-wide fix lands in. firmware regression-tests load_hisilicon's os_mem_size derivation; two devices here ship their own copy of that script and would not have seen the fix. Listing them by hand was the wrong shape. builder.sh copies devices//* over the firmware clone, so any file at a path firmware also has replaces it -- which makes same-path shadows discoverable rather than something to remember. discover_shadows() walks devices/ and reports anything sitting on a firmware path with no entry in firmware-drift.json, so the list cannot silently rot and a device added tomorrow cannot quietly introduce an unreconciled shadow. The manifest goes from 13 entries to 101: the 88 discovered ones, plus the 13 hand-authored entries that discovery cannot find because the name differs from the file they replace -- gk7205v200.generic-fpv shadows gk7205v200.generic, and nothing can infer that. Two self-test cases for the new path: a same-path shadow with no entry is reported, and an entry silences it. Verified against the tree by dropping the load_hisilicon entries, which surfaces both devices immediately. --- .github/firmware-drift.json | 632 +++++++++++++++++++++++- .github/scripts/check-firmware-drift.py | 62 +++ .gitignore | 1 + 3 files changed, 687 insertions(+), 8 deletions(-) diff --git a/.github/firmware-drift.json b/.github/firmware-drift.json index da7ddb514..45bb49b95 100644 --- a/.github/firmware-drift.json +++ b/.github/firmware-drift.json @@ -7,6 +7,20 @@ "satisfied; re-pinning it is the act of looking again." ], "shadows": [ + { + "builder": "devices/apfpv/general/overlay/etc/init.d/S40network", + "firmware": "general/overlay/etc/init.d/S40network", + "blob": "b8fd109321b556bbfdc9f1671b376992897fb9f1", + "reconciled": "2026-08-25", + "note": "shared network init" + }, + { + "builder": "devices/apfpv/general/overlay/etc/network/interfaces.d/wlan0", + "firmware": "general/overlay/etc/network/interfaces.d/wlan0", + "blob": "7bce98b97a24f2e45d09ad654328eab3b6ea2125", + "reconciled": "2026-08-25", + "note": "shared interface definition" + }, { "builder": "devices/common/br-ext-chip-goke/board/gk7205v200/gk7205v200.generic-fpv.config", "firmware": "br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config", @@ -35,6 +49,216 @@ "reconciled": "2026-08-25", "note": "fpv/lte variant" }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/br-ext-chip-goke/board/gk710x/gk7102c.generic.config", + "firmware": "br-ext-chip-goke/board/gk710x/gk710x.generic.config", + "blob": "ad93ad88301ebadeb5f9e3ea96a9d6439610272b", + "reconciled": "2026-08-25", + "note": "gk7102c variant of gk710x; keeps CONFIG_INPUT for its evdev consumer" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/kmod/audio.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/audio.ko", + "blob": "d7fd53da67d5bc3bcb12dd331a3b06b803bb94bc", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/kmod/hal.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/hal.ko", + "blob": "d8233486df56140502d018abc0fc233685a4edb1", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/kmod/hw_crypto.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/hw_crypto.ko", + "blob": "eb4c04f4ba4e503e4f5ab8ba4b7f08949f54cd31", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/kmod/i2s.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/i2s.ko", + "blob": "c2f1f8b313a4e93a26b9e1c235e60d082456d8d3", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/kmod/media.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/media.ko", + "blob": "3cec20d363cf0dde9761466c33b2ca1b6832b494", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/kmod/sensor.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/sensor.ko", + "blob": "77076209e044c7f08d569fae4d5a9cda026319bf", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/lib/libadi.so", + "firmware": "general/package/goke-osdrv-gk710x/files/lib/libadi.so", + "blob": "1455b648bd6b517b8615274eacdf0a2401d014b8", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/lib/libimage.so", + "firmware": "general/package/goke-osdrv-gk710x/files/lib/libimage.so", + "blob": "2d35911581d0bf18910e4117b9d468cc88b9b7ec", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/script/load_goke", + "firmware": "general/package/goke-osdrv-gk710x/files/script/load_goke", + "blob": "b4ae2a74154a7df0a224b4886b6fd2852e87dac9", + "reconciled": "2026-08-25", + "note": "vendor driver loader; fleet-wide fixes here land in firmware" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/sensor/config/mis2003.bin", + "firmware": "general/package/goke-osdrv-gk710x/files/sensor/config/mis2003.bin", + "blob": "1c8449d8b1fc2d961ce3b8aab17143375db53df6", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/sensor/config/mis2003_hw.bin", + "firmware": "general/package/goke-osdrv-gk710x/files/sensor/config/mis2003_hw.bin", + "blob": "ebcad21990816d4574b01dcd16eea5df5cfa8237", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710x.bin", + "firmware": "general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710x.bin", + "blob": "1f3d647655e1147d1d1a314d8160233bb2f31f39", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710xs.bin", + "firmware": "general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710xs.bin", + "blob": "002e71c69b9e562af3c0e98a1e8fec3ee373d91d", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_umea-qc01x/general/package/goke-osdrv-gk710x/goke-osdrv-gk710x.mk", + "firmware": "general/package/goke-osdrv-gk710x/goke-osdrv-gk710x.mk", + "blob": "d2d056f649b2fc9005d645a56691783375585729", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/br-ext-chip-goke/board/gk710x/gk7102c.generic.config", + "firmware": "br-ext-chip-goke/board/gk710x/gk710x.generic.config", + "blob": "ad93ad88301ebadeb5f9e3ea96a9d6439610272b", + "reconciled": "2026-08-25", + "note": "gk7102c variant of gk710x" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/kmod/audio.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/audio.ko", + "blob": "d7fd53da67d5bc3bcb12dd331a3b06b803bb94bc", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/kmod/hal.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/hal.ko", + "blob": "d8233486df56140502d018abc0fc233685a4edb1", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/kmod/hw_crypto.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/hw_crypto.ko", + "blob": "eb4c04f4ba4e503e4f5ab8ba4b7f08949f54cd31", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/kmod/i2s.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/i2s.ko", + "blob": "c2f1f8b313a4e93a26b9e1c235e60d082456d8d3", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/kmod/media.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/media.ko", + "blob": "3cec20d363cf0dde9761466c33b2ca1b6832b494", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/kmod/sensor.ko", + "firmware": "general/package/goke-osdrv-gk710x/files/kmod/sensor.ko", + "blob": "77076209e044c7f08d569fae4d5a9cda026319bf", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/lib/libadi.so", + "firmware": "general/package/goke-osdrv-gk710x/files/lib/libadi.so", + "blob": "1455b648bd6b517b8615274eacdf0a2401d014b8", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/lib/libimage.so", + "firmware": "general/package/goke-osdrv-gk710x/files/lib/libimage.so", + "blob": "2d35911581d0bf18910e4117b9d468cc88b9b7ec", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/script/load_goke", + "firmware": "general/package/goke-osdrv-gk710x/files/script/load_goke", + "blob": "b4ae2a74154a7df0a224b4886b6fd2852e87dac9", + "reconciled": "2026-08-25", + "note": "vendor driver loader; fleet-wide fixes here land in firmware" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710x.bin", + "firmware": "general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710x.bin", + "blob": "1f3d647655e1147d1d1a314d8160233bb2f31f39", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710xs.bin", + "firmware": "general/package/goke-osdrv-gk710x/files/sensor/fw/gk_fw_710xs.bin", + "blob": "002e71c69b9e562af3c0e98a1e8fec3ee373d91d", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/general/package/goke-osdrv-gk710x/goke-osdrv-gk710x.mk", + "firmware": "general/package/goke-osdrv-gk710x/goke-osdrv-gk710x.mk", + "blob": "d2d056f649b2fc9005d645a56691783375585729", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/gk7202v300_lite_cootli_camv0103/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/gk7202v300_lite_ipg-g3-wr/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, { "builder": "devices/gk7202v300_lite_xg521/br-ext-chip-goke/board/gk7205v200/gk7202v300.generic.config", "firmware": "br-ext-chip-goke/board/gk7205v200/gk7202v300.generic.config", @@ -42,6 +266,20 @@ "reconciled": "2026-08-25", "note": "device-local copy" }, + { + "builder": "devices/gk7202v300_lite_xg521/general/overlay/etc/wireless/modem", + "firmware": "general/overlay/etc/wireless/modem", + "blob": "075faaf5b960cd02ca8e336d92bf95f18bedb7be", + "reconciled": "2026-08-25", + "note": "shared modem table" + }, + { + "builder": "devices/gk7202v300_lite_xg521/general/package/goke-osdrv-gk7205v200/goke-osdrv-gk7205v200.mk", + "firmware": "general/package/goke-osdrv-gk7205v200/goke-osdrv-gk7205v200.mk", + "blob": "7238dd51068dadfc07c6156074dd39fe02cc842e", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, { "builder": "devices/gk7205v200_otg_generic/br-ext-chip-goke/board/gk7205v200/gk7205v200.generic-otg.config", "firmware": "br-ext-chip-goke/board/gk7205v200/gk7205v200.generic.config", @@ -63,6 +301,20 @@ "reconciled": "2026-08-25", "note": "device-local copy" }, + { + "builder": "devices/hi3516ev300_ultimate_rostelecom-ipc8232swc-we/general/package/hisilicon-osdrv-hi3516ev200/files/script/load_hisilicon", + "firmware": "general/package/hisilicon-osdrv-hi3516ev200/files/script/load_hisilicon", + "blob": "18f88b4f8fba3a3fdc317c0af293280b1ddf6754", + "reconciled": "2026-08-25", + "note": "vendor driver loader; carries the os_mem_size fix firmware regression-tests" + }, + { + "builder": "devices/hi3516ev300_ultimate_rvi-1ncmw2028/general/package/hisilicon-osdrv-hi3516ev200/files/script/load_hisilicon", + "firmware": "general/package/hisilicon-osdrv-hi3516ev200/files/script/load_hisilicon", + "blob": "18f88b4f8fba3a3fdc317c0af293280b1ddf6754", + "reconciled": "2026-08-25", + "note": "vendor driver loader; carries the os_mem_size fix firmware regression-tests" + }, { "builder": "devices/hi3518ev200_lite_lenovo-snowman-1080p/br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", "firmware": "br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", @@ -70,6 +322,13 @@ "reconciled": "2026-08-25", "note": "device-local copy" }, + { + "builder": "devices/hi3518ev200_lite_lenovo-snowman-1080p/general/overlay/etc/init.d/S70vendor", + "firmware": "general/overlay/etc/init.d/S70vendor", + "blob": "d0a1dc58dae0a2c46b104274438cea2cae1106f1", + "reconciled": "2026-08-25", + "note": "shared vendor init" + }, { "builder": "devices/hi3518ev200_lite_vstarcam-c8892wip/br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", "firmware": "br-ext-chip-hisilicon/board/hi3516cv200/hi3518ev200.generic.config", @@ -85,18 +344,375 @@ "note": "device-local copy" }, { - "builder": "devices/gk7102ca_lite_umea-qc01x/br-ext-chip-goke/board/gk710x/gk7102c.generic.config", - "firmware": "br-ext-chip-goke/board/gk710x/gk710x.generic.config", - "blob": "ad93ad88301ebadeb5f9e3ea96a9d6439610272b", + "builder": "devices/hi3518ev200_ultimate_lenovo-snowman-1080p/general/overlay/etc/init.d/S70vendor", + "firmware": "general/overlay/etc/init.d/S70vendor", + "blob": "d0a1dc58dae0a2c46b104274438cea2cae1106f1", "reconciled": "2026-08-25", - "note": "gk7102c variant of gk710x; keeps CONFIG_INPUT for its evdev consumer" + "note": "shared vendor init" }, { - "builder": "devices/gk7102ca_lite_vstarcam-g8896wip/br-ext-chip-goke/board/gk710x/gk7102c.generic.config", - "firmware": "br-ext-chip-goke/board/gk710x/gk710x.generic.config", - "blob": "ad93ad88301ebadeb5f9e3ea96a9d6439610272b", + "builder": "devices/hi3518ev300_lite_xiaomi-mjsxj02hl/general/overlay/etc/wireless/sdio", + "firmware": "general/overlay/etc/wireless/sdio", + "blob": "955032922fc2903353fb5c9c83f722e328d7d467", "reconciled": "2026-08-25", - "note": "gk7102c variant of gk710x" + "note": "shared wireless adapter table" + }, + { + "builder": "devices/hi3518ev300_lite_xiaomi-mjsxj02hl/general/package/hisilicon-osdrv-hi3516ev200/hisilicon-osdrv-hi3516ev200.mk", + "firmware": "general/package/hisilicon-osdrv-hi3516ev200/hisilicon-osdrv-hi3516ev200.mk", + "blob": "e4ee5c21d1771154facba266307709ddb0f913f3", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/ssc30kd_lite_cmcc-ds-ytj5301/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-bu-eu/general/overlay/etc/init.d/S40network", + "firmware": "general/overlay/etc/init.d/S40network", + "blob": "b8fd109321b556bbfdc9f1671b376992897fb9f1", + "reconciled": "2026-08-25", + "note": "shared network init" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-bu-eu/general/overlay/etc/init.d/S99rc.local", + "firmware": "general/overlay/etc/init.d/S99rc.local", + "blob": "688a126281f9110458564c168a95c2a1a5995274", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-bu-eu/general/overlay/etc/network/interfaces.d/wlan0", + "firmware": "general/overlay/etc/network/interfaces.d/wlan0", + "blob": "7bce98b97a24f2e45d09ad654328eab3b6ea2125", + "reconciled": "2026-08-25", + "note": "shared interface definition" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-bu-eu/general/overlay/usr/share/udhcpc/default.script", + "firmware": "general/overlay/usr/share/udhcpc/default.script", + "blob": "925137f983fb27cf22e56480a85978db668ca651", + "reconciled": "2026-08-25", + "note": "shared udhcpc handler" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-cu-eu/general/overlay/etc/init.d/S40network", + "firmware": "general/overlay/etc/init.d/S40network", + "blob": "b8fd109321b556bbfdc9f1671b376992897fb9f1", + "reconciled": "2026-08-25", + "note": "shared network init" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-cu-eu/general/overlay/etc/init.d/S99rc.local", + "firmware": "general/overlay/etc/init.d/S99rc.local", + "blob": "688a126281f9110458564c168a95c2a1a5995274", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-cu-eu/general/overlay/etc/network/interfaces.d/wlan0", + "firmware": "general/overlay/etc/network/interfaces.d/wlan0", + "blob": "7bce98b97a24f2e45d09ad654328eab3b6ea2125", + "reconciled": "2026-08-25", + "note": "shared interface definition" + }, + { + "builder": "devices/ssc30kq_apfpv_greg-generic-cu-eu/general/overlay/usr/share/udhcpc/default.script", + "firmware": "general/overlay/usr/share/udhcpc/default.script", + "blob": "925137f983fb27cf22e56480a85978db668ca651", + "reconciled": "2026-08-25", + "note": "shared udhcpc handler" + }, + { + "builder": "devices/ssc30kq_rubyfpv_generic/general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "firmware": "general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "blob": "d56e9aeb776345db4322d2ec378f6b0e701f885e", + "reconciled": "2026-08-25", + "note": "vendor driver loader; firmware#2307 updated its sensor list" + }, + { + "builder": "devices/ssc325_lite_imou-c22cp/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc325_lite_tp-link-tapo-c310-v1/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc325_lite_trassir-tr-w2c1-v1/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc333_lite_vstarcam-c43s_b/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc335_lite_tp-link-tapo-c110-v1/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc335_lite_tp-link-tapo-c310-v220/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc335_lite_tp-link-tapo-c310-v220/general/package/sigmastar-osdrv-infinity6b0/files/sensor/configs/sc3338.bin", + "firmware": "general/package/sigmastar-osdrv-infinity6b0/files/sensor/configs/sc3338.bin", + "blob": "4154f87d3027c8e9325587002336cfdeed4d677d", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/ssc335_lite_trassir-tr-w2c1-v2/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-bu-eu/general/overlay/etc/init.d/S40network", + "firmware": "general/overlay/etc/init.d/S40network", + "blob": "b8fd109321b556bbfdc9f1671b376992897fb9f1", + "reconciled": "2026-08-25", + "note": "shared network init" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-bu-eu/general/overlay/etc/init.d/S99rc.local", + "firmware": "general/overlay/etc/init.d/S99rc.local", + "blob": "688a126281f9110458564c168a95c2a1a5995274", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-bu-eu/general/overlay/etc/network/interfaces.d/wlan0", + "firmware": "general/overlay/etc/network/interfaces.d/wlan0", + "blob": "7bce98b97a24f2e45d09ad654328eab3b6ea2125", + "reconciled": "2026-08-25", + "note": "shared interface definition" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-bu-eu/general/overlay/usr/share/udhcpc/default.script", + "firmware": "general/overlay/usr/share/udhcpc/default.script", + "blob": "925137f983fb27cf22e56480a85978db668ca651", + "reconciled": "2026-08-25", + "note": "shared udhcpc handler" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-cu-eu/general/overlay/etc/init.d/S40network", + "firmware": "general/overlay/etc/init.d/S40network", + "blob": "b8fd109321b556bbfdc9f1671b376992897fb9f1", + "reconciled": "2026-08-25", + "note": "shared network init" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-cu-eu/general/overlay/etc/init.d/S99rc.local", + "firmware": "general/overlay/etc/init.d/S99rc.local", + "blob": "688a126281f9110458564c168a95c2a1a5995274", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-cu-eu/general/overlay/etc/network/interfaces.d/wlan0", + "firmware": "general/overlay/etc/network/interfaces.d/wlan0", + "blob": "7bce98b97a24f2e45d09ad654328eab3b6ea2125", + "reconciled": "2026-08-25", + "note": "shared interface definition" + }, + { + "builder": "devices/ssc338q_apfpv_greg-generic-cu-eu/general/overlay/usr/share/udhcpc/default.script", + "firmware": "general/overlay/usr/share/udhcpc/default.script", + "blob": "925137f983fb27cf22e56480a85978db668ca651", + "reconciled": "2026-08-25", + "note": "shared udhcpc handler" + }, + { + "builder": "devices/ssc338q_fpv_openipc-mario-aio/general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "firmware": "general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "blob": "d56e9aeb776345db4322d2ec378f6b0e701f885e", + "reconciled": "2026-08-25", + "note": "vendor driver loader; firmware#2307 updated its sensor list" + }, + { + "builder": "devices/ssc338q_fpv_openipc-thinker-aio/general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "firmware": "general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "blob": "d56e9aeb776345db4322d2ec378f6b0e701f885e", + "reconciled": "2026-08-25", + "note": "vendor driver loader; firmware#2307 updated its sensor list" + }, + { + "builder": "devices/ssc338q_fpv_openipc-urllc-aio/general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "firmware": "general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "blob": "d56e9aeb776345db4322d2ec378f6b0e701f885e", + "reconciled": "2026-08-25", + "note": "vendor driver loader; firmware#2307 updated its sensor list" + }, + { + "builder": "devices/ssc338q_rubyfpv_generic/general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "firmware": "general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "blob": "d56e9aeb776345db4322d2ec378f6b0e701f885e", + "reconciled": "2026-08-25", + "note": "vendor driver loader; firmware#2307 updated its sensor list" + }, + { + "builder": "devices/ssc338q_rubyfpv_thinker_internal_wifi/general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "firmware": "general/package/sigmastar-osdrv-infinity6e/files/script/load_sigmastar", + "blob": "d56e9aeb776345db4322d2ec378f6b0e701f885e", + "reconciled": "2026-08-25", + "note": "vendor driver loader; firmware#2307 updated its sensor list" + }, + { + "builder": "devices/ssc377qe_fpv_ccdcam-im50q01-tipoman/general/package/linux-patcher/linux-patcher.mk", + "firmware": "general/package/linux-patcher/linux-patcher.mk", + "blob": "fac2fcbf7b0b24092690892341d3890ea0738cc7", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/t21_lite_wansview-q5-1080p/br-ext-chip-ingenic/board/t21/t21.generic.config", + "firmware": "br-ext-chip-ingenic/board/t21/t21.generic.config", + "blob": "f0c54d0af58f8127dfd9be0d3b43df413514b8a2", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/t21_lite_xyx-06s/br-ext-chip-ingenic/board/t21/t21.generic.config", + "firmware": "br-ext-chip-ingenic/board/t21/t21.generic.config", + "blob": "f0c54d0af58f8127dfd9be0d3b43df413514b8a2", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/t23_lite_jooan-a6m-u/general/overlay/etc/rc.local", + "firmware": "general/overlay/etc/rc.local", + "blob": "1426ecbbba979c0005f2dd5e465fc75284d4c5b3", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/t23_lite_jooan-a6m-u/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t23_lite_jooan-q3r-u/general/overlay/etc/rc.local", + "firmware": "general/overlay/etc/rc.local", + "blob": "1426ecbbba979c0005f2dd5e465fc75284d4c5b3", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/t23_lite_jooan-q3r-u/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t23_lite_lsc-3215672/general/overlay/etc/rc.local", + "firmware": "general/overlay/etc/rc.local", + "blob": "1426ecbbba979c0005f2dd5e465fc75284d4c5b3", + "reconciled": "2026-08-25", + "note": "shared rc.local" + }, + { + "builder": "devices/t23_lite_lsc-3215672/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t31_lite_aceline-aip-o4/br-ext-chip-ingenic/board/t31/t31.generic.config", + "firmware": "br-ext-chip-ingenic/board/t31/t31.generic.config", + "blob": "0518469cce944263b2b288e5ba8322e1c886bcab", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/t31_lite_aceline-aip-o4/general/overlay/etc/wireless/sdio", + "firmware": "general/overlay/etc/wireless/sdio", + "blob": "955032922fc2903353fb5c9c83f722e328d7d467", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t31_lite_aoni-ep01j05/general/overlay/etc/init.d/S70vendor", + "firmware": "general/overlay/etc/init.d/S70vendor", + "blob": "d0a1dc58dae0a2c46b104274438cea2cae1106f1", + "reconciled": "2026-08-25", + "note": "shared vendor init" + }, + { + "builder": "devices/t31_lite_tp-link-tapo-tc70-v3/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t31_lite_tuya-gv7630-t31-ptz/br-ext-chip-ingenic/board/t31/t31.generic.config", + "firmware": "br-ext-chip-ingenic/board/t31/t31.generic.config", + "blob": "0518469cce944263b2b288e5ba8322e1c886bcab", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/t31_lite_tuya-gv7630-t31-ptz/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t31_lite_vstarcam-cs55/general/overlay/etc/wireless/usb", + "firmware": "general/overlay/etc/wireless/usb", + "blob": "f9df96bc190e8f8a11794f9410568eac10f50320", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t31_lite_wansview-q5-2k/br-ext-chip-ingenic/board/t31/t31.generic.config", + "firmware": "br-ext-chip-ingenic/board/t31/t31.generic.config", + "blob": "0518469cce944263b2b288e5ba8322e1c886bcab", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" + }, + { + "builder": "devices/t31_lite_wansview-q5-2k/general/overlay/etc/wireless/sdio", + "firmware": "general/overlay/etc/wireless/sdio", + "blob": "955032922fc2903353fb5c9c83f722e328d7d467", + "reconciled": "2026-08-25", + "note": "shared wireless adapter table" + }, + { + "builder": "devices/t40_lite_movols-mo-805p/br-ext-chip-ingenic/board/t40/t40.generic.config", + "firmware": "br-ext-chip-ingenic/board/t40/t40.generic.config", + "blob": "d027af78c683d0cb625e94fb8b525d4d1d39466a", + "reconciled": "2026-08-25", + "note": "device-local copy of a firmware file" } ], "builder_only_symbols": { diff --git a/.github/scripts/check-firmware-drift.py b/.github/scripts/check-firmware-drift.py index bd0eacb4e..3c0e73fdc 100644 --- a/.github/scripts/check-firmware-drift.py +++ b/.github/scripts/check-firmware-drift.py @@ -121,6 +121,35 @@ def selected_in(directory, pattern="*_defconfig"): return {k: sorted(v) for k, v in out.items()} +def discover_shadows(repo_root, firmware): + """Every devices// whose also exists in firmware. + + builder.sh copies devices//* over the firmware clone, so any file + sitting at a path firmware also has replaces it. That makes same-path + shadows discoverable rather than something to remember, which matters: the + hand-written list in the first version of this file covered 13 board configs + and missed 93 shadows across 35 firmware paths -- among them load_goke, + load_hisilicon, load_sigmastar and four vendor .mk files, exactly the shared + files a fleet-wide fix lands in. A shadow found here with no entry in + firmware-drift.json is itself a finding, so the list cannot silently rot. + + Shadows whose name differs from the file they replace -- gk7205v200.generic-fpv + for gk7205v200.generic -- cannot be discovered this way and stay hand-authored. + """ + found = {} + devices = os.path.join(repo_root, "devices") + for root, _dirs, files in os.walk(devices): + for name in files: + path = os.path.join(root, name) + rel = os.path.relpath(path, devices).split(os.sep) + if len(rel) < 2: + continue + in_firmware = os.path.join(*rel[1:]) + if os.path.isfile(os.path.join(firmware, in_firmware)): + found[os.path.relpath(path, repo_root)] = in_firmware + return found + + def blob_sha(firmware, path): """firmware HEAD's blob SHA for `path`, or None if it is not there.""" try: @@ -151,6 +180,16 @@ def check(firmware, buildroot, config, repo_root=REPO_ROOT): devices = os.path.join(repo_root, "devices") # --- Check 1: shadowed files --- + entries = {e["builder"]: e for e in config.get("shadows", [])} + if firmware is not None: + for builder_path, firmware_path in sorted(discover_shadows(repo_root, firmware).items()): + if builder_path not in entries: + findings.append( + f"{builder_path} sits at a path firmware also has " + f"({firmware_path}), so builder.sh replaces firmware's copy, but " + f"nothing has reconciled the two.\n" + f" Add it to firmware-drift.json with the blob it was checked against.") + for entry in config.get("shadows", []): builder_path = os.path.join(repo_root, entry["builder"]) if not os.path.exists(builder_path): @@ -305,6 +344,29 @@ def want(cond, what): want(any("--buildroot" in n for n in notices2), "skipping the dead-symbol half must be said out loud") + # Discovery: a file sitting at a path firmware also has, with no entry, + # is the omission that let 88 shadows go unlisted in the first version. + shadowed = os.path.join(repo, "devices", "thing", "general", "overlay", "etc", "rc.local") + os.makedirs(os.path.dirname(shadowed)) + with open(shadowed, "w") as handle: + handle.write("# device copy\n") + fwcopy = os.path.join(tmp, "firmware", "general", "overlay", "etc", "rc.local") + os.makedirs(os.path.dirname(fwcopy)) + with open(fwcopy, "w") as handle: + handle.write("# firmware copy\n") + findings4, _ = check(os.path.join(tmp, "firmware"), + os.path.join(tmp, "buildroot"), cfg, repo_root=repo) + want(any("rc.local" in f and "nothing has reconciled" in f for f in findings4), + "a same-path shadow with no entry must be reported") + + cfg5 = dict(cfg, shadows=[{"builder": "devices/thing/general/overlay/etc/rc.local", + "firmware": "general/overlay/etc/rc.local", + "blob": "deadbeef", "reconciled": "2026-01-01"}]) + findings5, _ = check(os.path.join(tmp, "firmware"), + os.path.join(tmp, "buildroot"), cfg5, repo_root=repo) + want(not any("nothing has reconciled" in f for f in findings5), + "an entry must silence the discovery finding for that path") + # Rot: an allowlist entry nothing selects any more. cfg2 = dict(cfg, builder_only_symbols={"BR2_PACKAGE_KNOWN": "x", "BR2_PACKAGE_VANISHED": "y"}) diff --git a/.gitignore b/.gitignore index 6480f4784..79ced9649 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /cache /openipc /output +__pycache__/