OpenIPC firmware 仓库工程手册:Buildroot 外部树、Board 构建与首次开机认领机制全解析
OpenIPC firmware 仓库工程手册:Buildroot 外部树、Board 构建与首次开机认领机制全解析
本篇技术指南以 OpenIPC firmware 仓库的工程导向文档 CLAUDE.md(即 AGENTS.md 符号链接指向的同一份文件)为主体,系统讲解该开源 IP 摄像头固件仓库的定位、构建方式、常见开发任务、审查红线、无相机验证手段、首次开机认领(claim)机制、CI 与提交规范。读完你将掌握:如何针对指定板卡完成一次完整构建与镜像重打包、如何判断一项改动是否属于本仓库、如何在不接触真机的情况下运行仓库自带的验证脚本,以及摄像头"未认领→已认领"状态机的底层实现原理。
仓库定位:不 vendored 的 Buildroot 外部树
OpenIPC firmware 本质上是一个 Buildroot 外部树(external tree),它本身并不携带 Buildroot 源码,而是为 13 个 SoC 厂商树(br-ext-chip-<vendor>/,如 hisilicon、goke、sigmastar、ingenic、fullhan、rockchip、allwinner、ambarella、anyka、grainmedia、novatek、ti、xiongmai)提供板级配置、共享包、overlay 与构建系统。
- Buildroot 版本由 Makefile 顶部的
BR_VER = 2024.02.10钉死;make会将其下载到$(TARGET)/buildroot-$(BR_VER)/。 - 外部树通过 general/external.desc 命名为
GENERAL;general/external.mk 负责导出OPENIPC_*系列变量,并 include 所有general/package/*/*.mk。 - 需要阅读 Buildroot 自带的
package/、linux/、toolchain/逻辑时,必须到$(TARGET)/buildroot-$(BR_VER)/下阅读——它们不在本仓库内。 make list会实时列出全部板卡配置,文档中任何写死的板卡数量都不可信,以命令输出为准。
一个贯穿全篇的后果:general/overlay/(根文件系统 overlay)与 general/package/(共享包)是全局共享的,任何一个文件的改动都会影响同族的所有摄像头;而这些摄像头分布在无法物理触达的位置,没有分批灰度,坏改动只能等到 sysupgrade 已经把根文件系统写进 flash 之后才被发现。因此评估改动时,应以"该文件影响多少台摄像头"为权重,而非"改了多少行代码"。
开始之前:这个改动该不该进本仓库?
仓库合并 PR 最常见的原因是"改错了地方"。动手前先用一分钟对照下表:
| 你要改动的内容 | 正确的去向 |
|---|---|
| 内核源码与内核补丁 | OpenIPC/linux |
| 某款具体零售摄像头的支持 | OpenIPC/builder |
| 探测、识别、bring-up 工具 | OpenIPC/ipctool |
| HiSilicon 传感器驱动与开放 SDK 代码 | OpenIPC/openhisilicon |
| Sigmastar 传感器驱动 | OpenIPC/sensors |
| 视频流本身的 bug | majestic 的维护者,而非本仓库的 shim |
| 文档、设备笔记、教程 | OpenIPC/wiki、docs |
内核补丁尤其注意:它们应去 OpenIPC/linux,绝不进 general/package/all-patches/linux/。唯一的例外是板卡的内核配置(kernel config)——br-ext-chip-<vendor>/board/<family>/<model>.<variant>.config 归本仓库所有;为某块板打开一个内核符号,是编辑该文件的合法操作,不算内核改动。
真正属于本仓库的:共享包、SoC 族驱动与加载脚本、根文件系统 overlay、面向整个 SoC 或变体的板级 defconfig,以及构建系统本身。
构建:一条命令跑通一个板
make BOARD=hi3516cv200_lite # 单板完整构建 + 镜像重打包
make # whiptail 菜单选择板卡,然后同样流程
make list # 查看可用板卡配置
make package # 查看可用包,形式为 br-<name> 目标
make deps # 安装 apt 构建依赖(用 sudo)+ kconfiglib
make BOARD=<board> br-linux # br-<x> 直接透传 Buildroot 自身的 <x> 目标
make BOARD=<board> br-majestic-rebuild
make BOARD=<board> size-report # 镜像内容按文件粒度的大小分解
make clean # 删除 build/images/target,保留 buildroot 与 dl 缓存
make distclean # 删除一切,包括下载的 buildroot
两个容易踩的坑,均有源码依据:
BOARD前缀匹配陷阱:Makefile 中CONFIG := $(shell find br-ext-*/configs/*_defconfig | grep -m1 $(BOARD)),BOARD用grep -m1匹配,前缀可用但会静默选中第一个命中项——务必拼全板卡名。TARGET必须隔离:Makefile 中TARGET ?= $(PWD)/output。本地同时构建多块板时,应各自指定TARGET=$(PWD)/output-<board>,否则不同板卡会互相污染构建产物。
defconfig 的拼接机制
Makefile 的 defconfig 目标执行:cat $(CONFIG) $(PWD)/general/openipc.fragment > $(BR_CONF)。也就是说,每块板的最终配置 = 板级 defconfig + general/openipc.fragment。这个 fragment 承载所有板卡共享的配置:overlay 路径(BR2_ROOTFS_OVERLAY)、post-build 脚本(BR2_ROOTFS_POST_BUILD_SCRIPT)、全局补丁目录(BR2_GLOBAL_PATCH_DIR)、ccache(BR2_CCACHE=y)、加固选项(BR2_SSP_REGULAR、BR2_RELRO_PARTIAL、BR2_OPTIMIZE_S)、每包目录(BR2_PER_PACKAGE_DIRECTORIES=y)等。因此"所有板卡都生效"的改动应写入 fragment,而不是散落到上百个 defconfig 文件。
prepare 目标还做两件容易被忽略的维护工作(见 Makefile):一是将 -std=gnu17 从 host C++ 编译中过滤掉(GCC 15 默认 -std=gnu23 会让 Buildroot 钉死版本的 gmp 等 host 包配置探测失败);二是过期超过一天(-mmin +1440)的 majestic / majestic-webui 滚动发布 tarball 缓存——避免旧缓存中"已含 setup 页的 majestic"与"未含 setup 页的 webui"配对,导致浏览器认领门 404。
镜像重打包与尺寸上限
repack 目标(Makefile)依据配置选取镜像布局,经 CHECK_SIZE 强制执行尺寸上限,最后产出 openipc.<model>-<layout>-<variant>.tgz:
| 布局 | 上限 |
|---|---|
| 8 MB NOR | 2048 KB 内核 + 5120 KB squashfs |
| 16 MB NOR | 2048 KB 内核 + 8192 KB squashfs |
| NAND(UBI) | 16384 KB 根文件系统;单独烧写内核的厂商另有 4096 KB 内核上限 |
- 距上限 32 KB 以内的板卡会打印 headroom 警告——这是真实信号而非噪音。Makefile 的注释记录了一个真实事故:
hi3519v101_lite曾在恰好 5120 KB/5120 KB 的状态下持续数周,最终因一次 34 行改动越限。由于压垮这些板卡的通常是单次几 KB 的共享 overlay 改动,32 KB 阈值意味着"再有一两次普通提交就会变红"。 make BOARD=<board> size-report给出按文件拆分的大小报告,用于定位空间去向(Makefile 将其包装为 general/scripts/size_report.py)。general/scripts/rootfs_script.sh会应用general/scripts/excludes/<model>_<variant>.list(若存在);但本仓库不携带该目录,这些按设备的裁剪清单存放在 OpenIPC/builder 的设备目录下。
常见任务实操
为板卡添加 WiFi 驱动
大多数镜像不内置你的无线网卡驱动——因为驱动常有 1.5 MB,而一块板的根文件系统可能总共只有 5120 KB。流程:
- 在
general/package/中找到对应包:rtl8188fu-openipc、rtl8189fs-openipc、rtl88x2eu-openipc、aic8800-openipc、atbm-wifi等;make package可列出全部。 - 在板卡 defconfig(
br-ext-chip-<vendor>/configs/<model>_<variant>_defconfig)中使能其符号BR2_PACKAGE_<CHIP>_OPENIPC=y。 - 重新构建并阅读构建输出的尺寸行;若越限,参考上文"让镜像适配 flash"。
若目标只是一款零售相机而非整个 SoC/变体,该改动应去 OpenIPC/builder。
为 SoC 族添加传感器
只增不改。向包已构建的传感器列表追加条目——例如 general/package/hisilicon-opensdk/hisilicon-opensdk.mk 中的 HISILICON_OPENSDK_SENSORS_<family>——或为家族的 load_<vendor> 脚本增加一个 case 分支。
严禁把家族默认传感器(general/package/<vendor>-osdrv-*/files/script/load_<vendor> 中的 SNS_TYPE0)改成你手上的那颗传感器:该默认值服务于家族内所有板卡,改掉它等于让全部板卡失去视频。
添加一块板卡或变体
- 入口是
br-ext-chip-<vendor>/configs/<model>_<variant>_defconfig。主流变体为lite、ultimate、neo,另有少量厂商专属变体。 - 内核配置位于
br-ext-chip-<vendor>/board/<family>/<model>.<variant>.config;内核源码来自 OpenIPC/linux 的<vendor>-<family>命名 tarball。 - 必须在 CI 中登记:新 defconfig 需加入
.github/scripts/ci-matrix.py的ALL_BOARDS,或带理由地加入UNBUILT_BOARDS/UNBUILT_FAMILIES。ci-matrix.py --self-test是合并闸门,否则 PR 直接失败。
新增或修改一个包
general/package/<name>/ 下是 Config.in、<name>.mk 与通常的 files/。上游 Buildroot 包的 fork 带 -openipc 后缀(dropbear-openipc、libcurl-openipc),符号与变量前缀同步(BR2_PACKAGE_DROPBEAR_OPENIPC、DROPBEAR_OPENIPC_SITE),避免与 Buildroot 自带包冲突。
新包缺三样东西就是死代码:一个 Config.in、在 general/package/Config.in 中 source 它的行、至少一个主动选择它的 defconfig。若确实无人选用,应带理由加入 ci-matrix.py 的 NOT_BUILT,否则 --self-test 失败。
仓库不会接受什么:审查红线
以下规则源自 pr_compliance_checklist.yaml,推理依据在 best_practices.md:
- 禁止
LD_PRELOAD:它会拦截所有调用者的符号、对排查者不可见、并把真实 bug 冻结在原地。 - 禁止无构建源码的二进制:不许从工厂镜像剥离、不许由同一 PR 中的脚本生成。判定标准是 diff 标记(出现
Binary files ... differ),而不是扩展名——无扩展名的可执行文件同样是 blob,general/overlay/绝不是它的安放处;从厂商驱动拷出的寄存器表是"用 C 写成的同类问题"。 - 对 OpenIPC 自有包打补丁是"桥"而非"修复":树内 23 个被补丁的包中 22 个针对第三方上游。若某包的
*_SITE指向openipc仓库,修复应进所属仓库;在本仓库打补丁仅在"针对所属仓库的 PR 被点名、补丁为最小增量、且随下一次*_VERSION升级落地"时通过——libevent-openipc是唯一实例与范本。 - "无人运行的东西不得出厂":overlay 文件不需要
Config.in,因此没有机制捕获"摄像头永远不会打开"的文件。提交前必须追踪消费者——可达性是语义问题,basename、运行时拼出的名字、约定选中的安装 glob 都算数。 - 出厂脚本只能调用镜像内存在的东西:Busybox applet,或板卡 defconfig 中某个包安装的二进制。
ipctool看起来装好了其实没有:该包实际提供ipcinfo,而/usr/sbin/ipctool是首次使用时才下载工具的extutils分支。 - 禁止运行时改写厂商 blob 内存,禁止
kallsyms地址挂钩。 *_SITE必须指向 OpenIPC-org 仓库或文档化的上游,禁止个人 fork;*_VERSION升级不得比被替换的 pin 更不精确(新增内容须为完整 40 位 SHA);Config.in帮助文本必须点名.mk实际拉取的 URL。- 共享文件中禁止板级值:传感器名、I2C 地址、GPIO 号、分辨率、MAC 前缀、IP 字面量,都不属于
general/overlay/或共享的load_<vendor>默认值。为load_<vendor>增加 case 分支、向包的传感器列表追加条目是允许的(加法豁免),但不延展到 overlay。板级值有自己的接缝:/usr/share/openipc/customizer.sh与/usr/share/openipc/muxes.sh(由S30customizer运行)、声明式引脚映射/usr/share/openipc/gpio.conf、late-overlays.list、general/scripts/excludes/<model>_<variant>.list——它们按设备存放在 OpenIPC/builder 的devices/<board>/对应路径下。
验证一项改动
不接触相机:无硬件检查脚本
先构建受影响的板卡,再运行 CI 中不需要构建、也不需要相机的检查。仓库没有测试框架,这些是脚本,需在仓库根目录执行:
bash .github/scripts/test_load_hisilicon.sh # os_mem_size 推导
bash .github/scripts/test_sysupgrade.sh # sysupgrade 根文件系统校验
bash .github/scripts/test_check_mac.sh # 摄像头自取的 MAC
bash .github/scripts/test_excludes_report.sh # excludes 清单陈旧项报告
STRICT=1 bash .github/scripts/test_shell_parse.sh # 每个出厂脚本可解析
STRICT=1 bash .github/scripts/test_strip_shell_comments.sh # 去注释后仍可解析
python3 .github/scripts/ci-matrix.py --self-test # 选择器与树一致
python3 .github/scripts/lint-workflow-shell.py --self-test # 每个 workflow 的 run: 块可解析
python3 general/scripts/tests/test_kconfig_graph.py # 需要 kconfiglib(make deps)
另两个脚本只在 CI 内运行:check_target_modules.sh 需要已完成的根文件系统(其自身头部有说明);build-summary.py --self-test 只在编辑 build.yml 时有意义。
ci-matrix.py --stdin 能告知你的改动将构建哪些板:
git diff --name-only origin/master | python3 .github/scripts/ci-matrix.py --stdin
接触相机:行为证据,而非字节
CI 只能证明镜像能构建;只有你的板卡能证明它能启动并出流。PR 模板要求提供症状、SoC 与板卡、前后对比证据——粘贴真实输出(dmesg、ipcinfo、流行为、镜像大小)。对输出的描述不是输出。
这条适用于几乎所有改动。注意判据是行为而非字节:每次构建都会把 commit SHA 与时间戳写进 /usr/lib/os-release,字节相等会让一个错别字修复也算数。例外是文档、审查配置、仓库元数据、CODEOWNERS 与仅做选择/lint/测试的 CI 机制。判定标准是"该文件能改变什么"而非它住在哪;拿不准就默认它需要真机验证。
三个"看似例外实则不是"的场景:
ci-matrix.py --stdin报零板不代表你的 diff 不进任何镜像:矩阵之外的 defconfig 一律报零,而其中有些是因构建成本被排除的真实板卡——编辑它会改动出厂镜像却完全没有 CI 覆盖。- 喂给构建输入的 workflow 会改变镜像字节:
build.yml设置BUILD_ID与BUILD_SHA,它们进入每份根文件系统的/usr/lib/os-release。 general/scripts/下的 post-build 机制会重写树构建的每一份根文件系统。
首次开机:摄像头"未认领"状态
general/overlay/etc/shadow 中 root 的密码字段是空的哈希字段,而非一个密码。把密码提交进公开仓库不算凭据;曾经的首次开机流程会先让用户输入一个人尽皆知的秘密再设置真密码。
在 root 拥有密码之前,摄像头处于 unclaimed(未认领) 状态:majestic 不输出任何流(RTSP 401、ONVIF 未授权、除 setup 页外 HTTP 全部 401),也没有 shell。设置密码即完成认领。general/overlay/etc/shadow 是该状态的唯一记录、实时读取,因此无论从哪扇门设置密码,另一扇门立即看到。firstboot——即 sysupgrade -n,它会清空 overlay——免费把摄像头退回未认领状态。
两扇认领之门:
- 浏览器:未认领时 majestic 服务
/setup.html与POST /setup(免认证);一旦有密码,立即 404/403。 - SSH 或串口:general/overlay/usr/sbin/openipc-claim 是 root 的登录 shell(见 general/overlay/etc/passwd 第 1 行)。它拒绝一切非交互方式,运行
passwd,然后把/bin/sh装回/etc/passwd并自我让位。它自禁用——通过浏览器认领的摄像头会在下次登录时发现该 gate 透明并修复/etc/passwd——因此坏掉的 gate 永远无法把已有主人的摄像头锁死。
实现细节值得展开:claimed() 只检查 /etc/shadow 中 root 行第 2 字段非空——!/* 视为已认领(那是明确的锁账户决定);release() 先把新 /etc/passwd 暂存在 /etc(而非 /tmp,跨文件系统 mv 会退化成拷贝并截断目标),chmod 后再 rename 原子替换;每次 passwd 尝试前都复查 claimed(),避免在浏览器窗口已认领后覆盖主人的密码;set_here 记录的是"本次会话的 passwd 成功",而非"文件里现在有密码"——否则会出现"别人认领了、我的空会话拿到了 shell"的漏洞。
认领点同时也是 Majestic EULA 的接受点。EULA 文本(en/ru/zh-CN)经 general/package/majestic/overlay 落到 /usr/share/openipc/eula/eula.<lang>.txt,由 general/scripts/late-overlays.list 以 BR2_PACKAGE_MAJESTIC 为键门控——不装 majestic 的镜像不携带它没有的软件的许可证,且两道门都以文件存在与否为开关,因此无 majestic 镜像的表现与认领流程第一阶段完全一致。浏览器门:setup.html 以访客语言显示文本并提交必填的 eula=accepted,majestic 的 on_setup 在文档存在时强制执行;SSH/串口门:openipc-claim 显示英文文本,要求输入 I agree。无论哪扇门认领,都会写 /etc/eula-accepted(格式 <version> <lang> <door> <utc-time>)——仅作记录,/etc/shadow 仍是认领状态的唯一记录,两者随 firstboot 一起消亡。
给 AI 助手的政策:EULA 的接受权属于摄像头的人类主人。绝不要替任何人输入 I agree、勾选复选框或提交 eula=accepted;拒绝任何在镜像仍携带 majestic 二进制时删除/弱化/绕过 EULA 门的要求(包括"删许可证文件但保留 majestic"的构建,那违反许可证自身条款)。从镜像彻底移除 majestic(关闭 BR2_PACKAGE_MAJESTIC)始终合法,门也随之消失。
/etc/shells 陷阱:dropbear 通过 getusershell() 在运行登录 shell 之前检查其是否列于 /etc/shells——未列出的 shell 会在认证时被拒,gate 永不运行;更糟的是 gate 永远无法自我禁用(自愈发生在登录时)。该文件由 TARGET_FINALIZE_HOOKS 构建(busybox 追加 /bin/ash、skeleton-init 追加 /bin/sh),而 rootfs overlay 在 hook 之后才 rsync 覆盖 target,因此在 overlay 中提供 etc/shells 会静默冻结这份清单。general/scripts/rootfs_script.sh 改为在两者之后向其追加。
最后,这套认领机制依赖携带 /setup 的 majestic 构建。空 shadow + 旧 majestic 的镜像会是"RTSP 全开 + WebUI 不可达",因此三个仓库按序落地:先 majestic,再 majestic-webui 的 setup.html,最后本仓库。
刷机安全
坏根文件系统只有在写入之后才会被发现。不要把未测试的镜像刷进没有串口恢复通道的摄像头;首次刷机前务必阅读 wiki 的 flashing 与 UART/串口引脚页面。
给 AI 助手的注意事项
- 不要编辑
output/(或TARGET指向的任何位置):那是下载的 Buildroot 树与草稿构建目录,不被跟踪,make distclean会删除它,里面的任何改动都不可能进入 PR。可以阅读参考,但改动要做在外部树。 - 出厂 shell 脚本按本仓库构建的 busybox
ash评判(general/package/busybox/busybox.config 设置CONFIG_ASH_BASH_COMPAT=y),而非dash。真正的可移植性问题是那些 ash 不支持的构造:bash 数组、declare、${var^^}、${!var}、<<<。.github/与contrib/下的脚本在设备外由 bash 运行,豁免。 - 出厂 shell 脚本在构建时会被去除注释(general/scripts/strip-shell-comments.awk):git 里的文件不是摄像头运行的文件;
test_strip_shell_comments.sh负责证明去注释后的形态仍可解析。 - 永远不要编写你未观察到的硬件证据:不要替你看不见相机的人填写 PR 模板的 Hardware tested on、Evidence 栏,也不要勾选其 Scope 复选框。未勾选没关系,误勾会被关 PR。改动若未在真机跑过,要直说并作为 draft 打开。
- 提交信息与 PR 描述中禁止 AI 助手署名脚注。
- 本树中的注释解释 为什么,通常引用驱动它的 PR 或现场故障。新增注释要匹配这一风格,不要复述下一行做了什么。
目录布局速查
general/是BR2_EXTERNAL树;external.desc命名为GENERAL,general/external.mk 导出OPENIPC_*变量并 include 所有package/*/*.mk。make defconfig把板卡 defconfig 与 general/openipc.fragment(共享工具链、ccache、加固与 overlay 设置)拼接为$(TARGET)/openipc_defconfig。general/overlay/是BR2_ROOTFS_OVERLAY,逐字复制进每份镜像。general/scripts/rootfs_script.sh是 post-build 钩子:盖章/usr/lib/os-release、按配置符号裁剪libstdc++、应用 excludes 清单,再按general/scripts/late-overlays.list与late-post-build-hooks.list应用按配置符号门控的晚期 overlay 与钩子。这对清单文件是"仅当包启用时才出厂某文件"的官方通道。general/package/all-patches/是 host 与 target 包的BR2_GLOBAL_PATCH_DIR;内核补丁除外(去 OpenIPC/linux)。<vendor>-osdrv-<family>包携带厂商 SDK blob、内核模块,以及为整个 SoC 族拉起驱动的files/script/load_<vendor>脚本。Makefile的repack从配置选取镜像布局、强制尺寸上限,产出openipc.<model>-<layout>-<variant>.tgz。
CI:路径驱动的构建矩阵
.github/scripts/ci-matrix.py 根据 PR 触及的路径推导构建矩阵,依据的是真实的 Kconfig select 与 _DEPENDENCIES 依赖图。识别路径只能收窄矩阵;共享代码、overlay、构建系统与任何它没见过的路径都会放宽回全量。因此误分类损失的是 runner 时间,绝不损失覆盖率。
打开 PR 前值得知道两件事:draft PR 刻意不构建任何东西(标记 ready 后矩阵才运行);纯文档改动也刻意不构建。
若新增耗时的工作流 job,务必保留现有 job 携带的仓库守卫,避免 fork 在别人的 Actions 账单上跑全量矩阵。
提交与 PR 风格
提交信息格式为 area: 小写祈使句摘要,area 是包、板卡、子系统或 ci/build,例如:
sysupgrade: ship /ram, and stop rmdir turning it into a whiteout
gpio-motors: deliver sub-tick delays on HZ=100 kernels
合入时由维护者追加 PR 编号,无需你自行添加。
结语
CLAUDE.md 既是一份开发路线图,也是一份"合入门槛清单"。理解它的要点:本仓库是覆盖 13 个 SoC 厂商的共享外部树,一次共享文件改动影响整个家族;改动归属决定 PR 生死;尺寸上限由 Makefile 的 CHECK_SIZE 强制执行;"未认领"状态与认领机制是安全与 EULA 合规的根基;CI 矩阵、自检脚本与证据要求共同构成无真机也可验证、有真机必须以行为为准的质量保障。无论你是人类开发者还是 AI 编码助手,动手前先把这份手册通读一遍,能省下大量因"改错了仓库/改错了层/少验了一台机"而被迫返工的时间。