夜雨聆风学习资料网

ARTICLE · 1232034

红米 Note 7Pro 从源码编译LineageOS并自签名记录

红米 Note 7Pro 从源码编译LineageOS并自签名记录

晚上九点五十,签名 OTA 出来了:1.3G,签的是我自己的私钥。

往前推二十四小时零三分钟,是一条完整的 Android 16 编译链:从 993 个 git 工程、183G 源码,到 2.9G 的 target-files,最后是 169 个 APK 和 35 个 APEX 全部换签名。全程一个字节的代理流量都没走。

目标设备是小米 Redmi Note 7 Pro(violet),LineageOS 23.2。我签自己的 key 不是为了刷机,是想让 OTA 升级链和签名信任锚完全归自己——设备上装的东西,签名得是我的。

三个约束,决定了后面每一个选择

先说清楚条件,因为下面所有的"怪招"都是被它们逼出来的:

  1. 1. 代理每月只有 40G 流量。 AOSP 光工程就有 874 个,源码树 100G 起步,走代理必爆。
  2. 2. 源码盘是 NTFS-3G 的机械盘。 这块 1T 盘挂给了 Docker 容器用。
  3. 3. 宿主 46.7G 内存,上面还跑着别的服务,编译不能把它们挤死。

一、零代理:把 AOSP 全部换到国内镜像

第一步先量网络。实测结果很干脆:

  • • android.googlesource.com 直连超时——AOSP 的 874 个工程全在这上面
  • • 清华 TUNA 的 /git/lineageOS/ 和 /git/AOSP 都是 200

所以 manifest 用 TUNA 镜像的 LineageOS 仓库,AOSP 的部分不加进 manifest,而是用 git 自己的 URL 改写,把 googlesource 整体重定向到 TUNA:

repo init --repo-url=https://mirrors.tuna.tsinghua.edu.cn/git/git-repo \
  -u https://mirrors.tuna.tsinghua.edu.cn/git/lineageOS/LineageOS/android.git \
  -b lineage-23.2

git config --global url."https://mirrors.tuna.tsinghua.edu.cn/git/AOSP".insteadOf \
  "https://android.googlesource.com"

这里有个小细节:repo 自己的工具仓库在 gerrit.googlesource.com 上,它是 init 阶段就要拉的,所以得单独用 --repo-url 指向镜像的 git-repo。漏了这一步,第一步就卡死。

993 个工程里 874 个来自 AOSP。这一改,它们全部不过代理。

然后是镜像的排队机制。 第一次 repo sync -j8,结果是 990/993,四个工程失败:

remote: Waiting in queue... Position: 51/98/94/173

失败的是 hardware/qcom-caf/sm8350/audio、packages/apps/Dialer、packages/apps/LineageCustomizer、packages/services/Mms。原因不是网络,是镜像站也在排队限流,我这边并发开太高,刷得太快就被挤出去了。降到 -j4 重跑,993/993,工作树完整。

教训:镜像的并发是大家共享的,自己的 -j 要留余量,不要在公共镜像上打满。

二、密钥必须放 ext4,不能放 NTFS

这套签名脚本对密钥目录有硬要求:0700。而 NTFS-3G 表达不了权限位——挂在 Linux 上全是 777,chmod 改不动。

于是目录直接分成两半:源码和固件放 /data(大、慢,但便宜),私钥放项目目录里的 keys/(ext4,权限能落下去)。

密钥一共 131 个文件:11 个基础密钥(platform / releasekey / media / shared / networkstack / sdk_sandbox / bluetooth / verity / nfc / testcert / cyngn-app),加上第一次签名时按需生成的 35 组 APEX 密钥。其中 testkey 是一条指向 releasekey 的软链——这条软链是后面"整棵树都用同一个 key 签"的关键。

三、内存:soong 分析的 27G 峰值

第一次编译直接被杀:

Memory cgroup out of memory: Killed process soong_build ...
anon-rss:27119220kB

当时容器上限是 26G。这不是普通的爆内存——soong 分析阶段要在内存里把整个源码树的构建描述展开成 199539 条 ninja 边,单进程峰值 27G 起步,而且 kati 和 soong 同时跑还会叠加。

三件事一起做才压住:

  • • 宿主加了 32G 的 /swapfile
  • • 关掉桌面上吃内存的程序:Android Studio(5.4G RSS + 1.4G swap)、微信、chrome 的 26 个进程、rwork
  • • 容器限制调到 32G RAM + 20G swap,并且先单独串行跑一次 soong 分析,别让它和 kati 同时到峰值

结果是 memory.peak=32.0G,溢出到 swap 3.6G,oom_kill=0。

后来我单独重跑了一次 target-files-package otatools(-j16,不串行),峰值 33.1G 也没被杀,swap 一点没用上。所以那次 OOM 的锅很单纯:上限给小了,26G 就是不够。

四、二十四小时,瓶颈不在 CPU

-j16 跑完是 24 小时 03 分。这时间花得不冤枉,但也不是花在算力上——瓶颈是 NTFS-3G 加机械盘:源码树加中间产物上百万个小文件,每一次 stat、open、unlink 都要穿过 FUSE 层。看 ninja 的进度条(98560/199539)就知道,它在慢慢爬。

编译结束后 /data 用了 671G,其中同步下来的源码 183G,剩下全是中间产物。

五、那三条无害的失败边

构建最后是失败的,退出码 1,报的是:

OSError: [Errno 39] Directory not empty: '/src/out/soong/.temp/tmpvmj9p_qg'

顺着栈追下去:apex_sepolicy_tests 调 Python tempfile 清理临时目录,shutil.rmtree 走到 _rmtree_safe_fd,在 NTFS-3G/FUSE 上存在竞态——目录里的文件已经删了,但目录还没彻底消失,就报非空。

三条失败边全是 APEX 的 sepolicy 校验,不参与签名产物。而我要的东西在那之前就已经生成了:lineage_violet-target_files.zip,2.92G,10403 个条目。

这里有个我差点踩的坑:不要把"构建失败"直接等同于"产物不可用"。ninja 是按目标跑的,target-files 已经写出来了,后面只是几条并列的校验边挂了。确认产物完整(META/apkcerts.txt、apexkeys.txt、misc_info.txt 都在)之后,签名完全可以从它继续,不需要重新编 24 小时。

顺便说:在 NTFS 上跑 AOSP 编译是能跑通的,但别指望它干净。

六、签名:169 个 APK + 35 个 APEX

签名器是 sign_target_files_apks,参数看着简单,映射规则得先弄清楚,否则会白跑一次。

第一件事:-d 只映射八个标准证书名。

-d <keys> 并不是"所有证书都用我的 key",它只覆盖 build/make/target/product/security/ 下的标准名:testkey / devkey / media / shared / platform / networkstack / sdk_sandbox / bluetooth。其中 testkey 指向 releasekey,所以全树用 testkey 签的 APK 会统一变成 releasekey 签——这也是 LineageOS 一贯的做法。

第二件事:证书路径会去掉 .x509.pem 再查表。 所以 build/make/target/product/security/testkey.x509.pem 能命中 .../security/testkey 这条映射。手工核对时要注意这个后缀处理。

正式签之前我先静态跑了一遍映射,把 zip 里真实存在的 169 个 APK 全过了一遍:每一个的证书路径都能解析到一对存在的 .pk8 / .x509.pem。零遗漏,这步花五分钟,能省掉一小时的白跑。

第三件事:APEX 的 .capex 要归一化。apexkeys.txt 里 36 条,1 条是 PRESIGNED(com.android.apex.cts.shim),其余 35 个要现场生成 RSA-4096 密钥——容器一对 .pk8/.x509.pem,载荷一枚 .pem。而 zip 里的 35 个 APEX 是 26 个 .capex(压缩)+ 10 个 .apex,签名器会把 .capex 归一化成 .apex 再查表,所以传参时按 .apex 名给。

整个签名阶段跑了 21 分钟(21:29 开始,21:50 出 OTA)。

七、三重验证:怎么确认真的签上了

退出码只能说明工具没报错,不能说明签的是我的 key。我要的是证据,所以去包里挖:

1. APK: 从签名后的 target-files 里掏出 SYSTEM/framework/framework-res.apk,读它的 META-INF/CERT.RSA:

sha256 指纹 = 84:19:54:DF:8D:6C:E7:8F:3C:70:E6:CD:E6:3F:CC:1C:51:C4:73:EE:BC:83:33:FA:A7:97:87:E5:79:21:FC:00
subject    = C=CN, O=SuQishuo, OU=PersonalROM, CN=violet

和我的 keys/platform.x509.pem 指纹逐字符相同。

2. APEX: 把 com.android.art 容器里的 apex_pubkey 挖出来,再用 avbtool 从我自己生成的 com.android.art.pem 派生公钥:

avbtool extract_public_key --key com.android.art.pem --output expected.bin

两者 1032 字节逐字节相同——APEX 的载荷确实是我签的。

3. OTA 信任锚: 包内 META-INF/com/android/otacert 的指纹是 D0:71:94:B7:…:C9:B7(我的 releasekey),官方 nightly 是 51:83:25:EF:…:BB。不同,说明 OTA 的信任锚已经换成我自己的。

顺手发现一件事,值得记一笔:Android 16 的块 OTA 里没有 CERT.RSA / MANIFEST.MF。我一开始以为是签名漏了,专门去抓了一个官方 23.2 nightly 对比——官方包也没有,两边 META-INF 里的东西一模一样(metadata、otacert、update-binary、updater-script)。所以这是格式本身的设计,跟签没签没关系。

八、我自己的两个失误

第一个:同一个输出目录跑了两个 soong。 我先起了一次重跑,没确认它是否真的在跑,又起了第二次。第二个在 10 秒内拿不到 out/.lock 直接退出(这点它做得对)。更阴的是第一个:它的 ninja 随后僵死了——wchan 停在 futex_do_wait,30 秒内 I/O 增量 0,只剩一个 /src/out/.ninja_fifo 句柄。第二个 soong 进程启动时重建了这个 FIFO,第一个的 ninja 就在一个没有读者的管道上永远等下去。

确认僵死之后杀干净,锁才释放(用 flock 实测过,不是猜的)。一个僵死的 soong 会一直占着 out/.lock,把后面所有构建都堵住,这是必须清掉的东西。

第二个失误是不该把"构建失败"当成"白干了"——见第五节。

九、刷之前必须知道的三件事

  1. 1. 不要重新生成这套密钥。 密钥一换,OTA 的信任锚就变了,已经刷机的设备再也收不到后续升级。照 0600 权限备份三份,其中至少一份放离线介质。
  2. 2. AVB / vbmeta 仍然是 AOSP test key(avb_*_key_path=external/avb/test/data/testkey_rsa4096.pem),这和官方 LineageOS 的行为一致。所以 bootloader 保持解锁,不要重锁。
  3. 3. 从官方(或任何其它密钥的)ROM 直接覆盖会因签名不一致失败,要按官方 wiki 的迁移流程走,或者清数据安装。

数字汇总

项目
数字
git 工程
993 个(874 个来自 AOSP)
源码树
183G(/data 最终占用 671G)
编译耗时
24 小时 03 分
ninja 边
199539 条
soong 分析峰值
27.1G(26G 上限下被杀)→ 32G 上限通过
签名耗时
21 分钟
重签对象
169 个 APK + 35 个 APEX
产物
1.3G OTA + 2.7G 签名 target-files
代理流量
0(最初一次 apt 约 700MB 后改国内源,之后全程零)

一套流程封装在本地 Docker 项目里(Dockerfile + lineage.sh + signing.py),README 里记了这次实测的校验状态;密钥的指纹和备份位置单独记在 KEYS.md。

一句话总结:在 40G 月流量、NTFS 机械盘、46.7G 内存的夹缝里,安卓大版本从源码编译到私有签名是可做的——前提是别把上游当唯一真相,该换镜像换镜像,该量数据量数据,签完一定要去包里挖证据。

相关学习资料