结论:这个原生 Git 安装器把 git clone/fetch、git checkout/reset 和复制、删除、移动交给 shell,只用断言判断返回码,且在关键窗口先删缓存与旧安装目录、再把新目录移入,三步不是一次原子替换——断电或命令失败可能留下旧目录已删、新目录未就位的状态。它也没有内建的下载总超时、安装树签名/摘要验证或固定 commit,远端仓库与分支即安装内容的信任根。已有的继续启动脚本会让程序直接进入完成画面而不核对安装目录,因此进度 100% 或 60 秒完成屏幕都不是安装成功回执。本文为固定快照静态分析,未运行下载、Git 命令、文件替换或设备启动。
构建、入口与板型分支
构建脚本仅在 extras 开启且架构为 comma_arm64 时产出四个变体:openpilot、openpilot_test、openpilot_nightly、openpilot_internal,分支由 BRANCH 编译宏写入,internal 变体另定义 INTERNAL。继续脚本和字体通过 ld -r -b binary 嵌入 ELF。安装器全局的 tici_device 调 C++ 硬件类型判断,选择 2160×1080 或 536×240 窗口;新板型进入该路径前必须先满足跨语言硬件身份契约。
| 函数/组件 | 实现链与限制 |
|---|---|
get_str()、GIT_URL/BRANCH_STR |
从带分隔与填充空格的编译字符串截取有效值;没有运行时 URL/分支白名单 |
branchMigration() |
按 TICI/TIZI/MICI 对 release3、staging、master 等分支作迁移;未知板型保留原分支 |
doInstall() |
无限等待系统时间有效,删除临时安装目录;只有旧安装目录与缓存目录同时存在才走缓存复制 |
freshClone()/cachedFetch() |
新安装用浅克隆与递归子模块;缓存分支先整目录复制、改 remote/branch,再 fetch |
executeGitCommand() |
popen() 读 Git 输出,以英文进度前缀与百分号更新界面;命令没有整体期限 |
cloneFinished() |
断言 Git 返回 0,checkout/reset/submodule update,删旧标记与安装目录,移动新目录 |
main() |
初始化界面与字体,迁移分支;已有继续启动脚本则直接显示完成画面,否则安装后收尾 |
拉取路径上,doInstall() 没有内建超时或离线失败界面:代码不验证旧目录或缓存标记内容的真实性。executeGitCommand() 用 popen() 读 Git 输出并以英文进度前缀与百分号更新界面,std::stoi() 解析进度没有局部异常处理,命令也没有整体 deadline。
落盘次序与非事务替换
安装器把 git clone/fetch 的 shell 命令、git checkout/reset 和复制、删除、移动、改权限都交给系统调用;run() 只用 assert(err == 0) 报失败。在启用断言的构建中会中止;如果断言被编译掉,命令非零返回仍可能继续后续步骤。cloneFinished() 对 pclose 和 chdir 也采用断言,没有结构化错误恢复。不能把最终 100% 进度或 60 秒完成屏幕当作安装成功回执。
关键窗口在 cloneFinished():先删缓存目录,再删旧安装目录,然后移动新目录;这些操作不是单个原子替换,断电或命令失败可能留下旧目录已删除而新目录未就位的状态。继续脚本先写新文件、再改权限与重命名;脚本内容仅进入安装目录并执行启动脚本。源码中未见安装树签名或摘要验证、Git commit 固定 pin,也没有对继续脚本执行成功的确认,因此远端仓库与分支就是安装内容的信任根。INTERNAL 分支还会写入 SSH 开关、前向摄像头锁和公钥参数,不能把该构建变体当作普通公开设备的等价安装包。
| 步骤 | 动作 | 风险 |
|---|---|---|
| 1 | 删除缓存目录 | 后续失败时缓存已丢失 |
| 2 | 删除旧安装目录 | 旧版本在此刻起不可用 |
| 3 | 移动新目录就位 | 断电可致新目录未就位 |
| 4 | 写并重命名继续脚本 | 仅在收尾阶段执行 |
继续启动链与两层信任
已有继续启动脚本时,本程序不再核对安装目录是否存在、是否匹配标记,也不执行更新,只进入完成画面;旧安装树与标记的组合可能来自外部安装或恢复流程,本归档无法证明其完整性。若外部启动器执行该脚本,它会先进入安装目录,再执行启动脚本,而启动脚本立即转交另一个脚本。后者先加载环境脚本中的平台版本与 staging 根,再检查已定稿的 overlay 更新,并可能替换整个安装目录、重新执行启动脚本;之后建立子模块 import 链接,执行平台初始化与版本升级门,最后按 prebuilt 标志决定是否调用构建,再运行进程管理器。这进一步说明继续脚本不是「安装完成」的最终回执,后面仍可能发生 overlay 切换、系统镜像更新和构建失败。
还需要区分两层:Python 下载界面只检查 ELF 魔数,这个原生 Git 安装器再拉取源码并准备启动脚本;中间的分发、执行与重启交接仍需在目标镜像与服务端验证,不能从这两份源码推导端到端可信链。平台初始化在版本比对和进程管理器启动之前把当前槽位标记为成功,它只表示该标记的时点,不证明服务已通过健康检查。
自制硬件与插件化验收
安装器应作为受信任维护服务,而不是任意插件执行 shell 的入口。新硬件方案需定义板型与目标分支的签名清单、固定 commit 或内容摘要、下载总超时和空间预算;先在新目录完整验证,再以可回滚的目录或槽位协议切换。宿主应记录安装事务 ID、来源、验证结果、旧版本与新版本,并在启动后回执确认;界面插件只显示状态。台架至少覆盖时钟长期无效、缓存损坏、Git 进度格式变化、fetch 与子模块失败、每一步断电、继续脚本已存在但安装目录缺失,以及不同板型窗口与字体。
常见问题
安装进度到 100% 就说明安装完成了吗?
不能。进度来自解析 Git 的英文输出,cloneFinished() 之后还有 checkout、reset、子模块更新和目录替换,且这些操作不是原子的。已有继续脚本时程序还会跳过安装直接显示完成画面,因此 100% 与 60 秒完成屏幕都不是安装成功回执。
为什么断电可能让设备处于中间状态?
因为关键窗口先删缓存目录、再删旧安装目录、然后移动新目录,这三步不是一次原子替换。cloneFinished() 只用断言判断返回码,如果断电发生在移动之前,就可能出现旧目录已删除而新目录尚未就位的状态,后续还依赖外部启动器恢复。
安装内容的信任根是什么?
是远端仓库与分支。源码中未见安装树签名或摘要验证,也没有固定 commit pin;get_str() 只从编译进去的字符串截取 URL 与分支,没有运行时白名单。因此产物或分发端若改写这些字节,不能只靠界面文案判断目标值。
缓存复制路径更安全吗?
不必然。doInstall() 只在旧安装目录与缓存目录同时存在时才走缓存复制,cachedFetch() 会先整目录复制,再改 remote/branch 并 fetch;代码不验证旧目录或缓存标记内容的真实性,因此缓存路径的提升主要是速度,不是可信度。
延伸阅读
- Ed25519 签名加 96 字节事务记录:让 MCU 升级不怕中途断电 —— 对照签名包与事务记录如何避免本文指出的非原子替换窗口。
- EdgeDeploy:BLE 与 USB 两条通道怎么把签名包送到 ESP32-S3 —— 说明带校验与断点续传的分发与本文 shell 安装的差异。
- 更新器的暂存与激活:先验证再切换的落地方式 —— 承接本文继续启动链里的 overlay 切换与重执行。
- 进程生命周期、通信与设备服务 —— 解释启动脚本末尾交给进程管理器之后的分工。
参考资料
- 项目 02 ——
openpilot-research/NATIVE_GIT_INSTALLER_BRANCH_CACHE_ACTIVATION_DEEP_DIVE.md(取用了构建变体、分支迁移、缓存复制、落盘次序、非事务替换窗口与继续启动链)。 - 项目 02 ——
openpilot-research/INITIAL_INSTALLER_DOWNLOAD_TRUST_BOUNDARY_DEEP_DIVE.md(仅用于确认下载侧只检查 ELF 魔数的信任边界)。
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复