diff --git a/.github/firmware-drift.json b/.github/firmware-drift.json new file mode 100644 index 000000000..45bb49b95 --- /dev/null +++ b/.github/firmware-drift.json @@ -0,0 +1,767 @@ +{ + "_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/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", + "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/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", + "blob": "a6b1534abde2aed025d61a1693bf9265176141df", + "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", + "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/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", + "blob": "92805f802503b38be9bb08843b5760e91bb3abd7", + "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", + "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/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": "shared vendor init" + }, + { + "builder": "devices/hi3518ev300_lite_xiaomi-mjsxj02hl/general/overlay/etc/wireless/sdio", + "firmware": "general/overlay/etc/wireless/sdio", + "blob": "955032922fc2903353fb5c9c83f722e328d7d467", + "reconciled": "2026-08-25", + "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": { + "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..3c0e73fdc --- /dev/null +++ b/.github/scripts/check-firmware-drift.py @@ -0,0 +1,436 @@ +#!/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 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: + 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 --- + 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): + 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") + + # 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"}) + 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 diff --git a/.gitignore b/.gitignore index 6480f4784..79ced9649 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /cache /openipc /output +__pycache__/