LeonOS Documentation

LeonOS 4 构建迁移验证记录(P0 与 P1 局部)

当前全量迁移结果见 全量迁移记录,以下为历史分阶段证据。

接管后的新增验证及修正见 接管记录。本文原有通过记录只对应当时提交和范围,尤其不能代替当前锁实现或新 PAM/SDK 链的验收。

配套计划:原构建重建计划(已清理,历史版本可从 Git 查看) 分支:xiaobai/dev/buildsystem,基线提交 6be4c69 记录时间:2026-09-19(宿主 Linux 7.2.4-3-cachyos-bore-lto,x86_64)

本文只写实际执行过并看到输出的检查。第 5 节逐条列出未执行项。凡未执行的验收编号,一律写"未运行", 不以 skip 状态冒充通过(计划第 13 节要求)。

1. 环境与依赖探测

命令:make --version、gcc --version、clang --version、qemu-system-x86_64 --version、 逐工具 command -v、git submodule status、df -h .

结果:GNU Make 4.4.1;GCC 16.2.1;Clang 22.1.8;QEMU 11.1.1;ld.lld、llvm-ar、llvm-objcopy、 llvm-config、xorriso、mformat、mkfs.fat、qemu-img、curl、flock、gperf、autoconf、 automake、libtoolize、flex、bison、msgfmt、pkg-config、patch、tar、xz、install 均存在。

两点必须记进 make doctor 的设计:mke2fs 解析到 /opt/android-sdk/platform-tools/mke2fs(不是 e2fsprogs 的系统路径);squashfs-tools 不存在, 但当前镜像链未使用它。22 个 submodule 全部已检出,含 third_party/kconfig-frontends (kconfig-frontends-3.12.0.0)与 third_party/zlib(v1.3.2,含 contrib/puff/puff.c)。

磁盘:/home/xiaobai/Projects 已用 312G、可用 387G;现有 build/ 占 78G。

2. 旧系统行为基线(P0-b)

测量方法:tests/build/measure.sh,把被测命令放进独立进程组,每 0.2 秒采样该组 RSS 之和取峰值。 共享页按进程各计一次,因此数值跨运行可比,但不等于唯一驻留集。GNU time 未安装(首次尝试以 rc=127 全表失败),故改用此自研脚本,新旧系统将使用同一方法。

用例命令退出码墙钟峰值组 RSS日志
kernel-1python3 build.py -v run kernel01.52 s150 MBbuildsystem/logs/migration-baseline/kernel-1.log
kernel-noop-again同上,紧接第二次01.53 s149 MB.../kernel-noop-again.log
sdkpython3 build.py -v run sdk0146.7 s1.25 GB.../sdk.log
image-vmdkpython3 build.py -v run image-vmdk0193.4 s1.58 GB.../image-vmdk.log
image-isopython3 build.py -v run image-iso0198.4 s1.64 GB.../image-iso.log
installerpython3 build.py -v run installer0266.7 s1.60 GB.../installer-retry.log
allpython3 build.py -v run all重跑进行中——.../all-retry.log

image-iso 日志尾部为 Started 6 tasks / Entered 159 folders / Built 563 files / Generated 424 files / Ran 541 commands / Downloaded 0 files / 0 errors。 installer 日志尾部为 Started 12 tasks / Entered 162 folders / Built 565 files / Generated 427 files / Ran 545 commands / Downloaded 0 files / 0 errors。

首次 installer 与 all 采集的 rc=126 不是构建失败,是我自己的工具链缺陷:批量缩进规范化用 expand -t4 $f > $f.tmp && mv 覆盖了 tests/build/measure.sh,mv 带上了重定向产生的 0644, 可执行位丢失,于是 "$MEASURE" 无法执行。已 chmod +x 并以新文件名重跑,上表取自重跑结果。

产物哈希(本轮旧系统实际产出):


b93d66628f80796e0b837fae3c229f44176390f4f9672e64d49394cd80ac087c  build/images/leonos4.vmdk
8a672b5f5e52f29a35bee3028dd8513e12095ef8c8d9134b93d2a05c8804cf6a  build/images/leonos4.iso
87b7da68dee236932f8e0d4c787e327d6dab6d4c8b0d4043b3fd028d0d6e6f2c  build/system/kernel.sys
357275d0b76111905491f875768def6069cea5dbe5f7536e9f033d08a66f51ea  build/system/kernel.debug
2c7c89ced5b5d219fe2e13bcdd925a75ad8c5d4d121a217f9c010c2e46690479  build/images/leonos4-installer.iso

本轮未在 QEMU 中启动这些镜像,因此它们只是文件产出证据,不构成第 11 节要求的运行验证。

2.1 关键发现:旧系统的"重复构建"不是空操作

对 kernel-noop-again.log 逐行核对,第二次 run kernel 实际执行了 7 条命令:


python3 tools/generate_component_kconfig.py
python3 tools/build_info.py --header include/generated/build_info.h --state buildsystem/state/build_number.txt
python3 tools/kconfig_sync.py --config buildsystem/config/leonos.conf ...
clang -target x86_64-unknown-none -O3 ... -c kernel/ntclks/version.c -o build/obj/kernel/kernel/ntclks/version.c.o
ld.lld -nostdlib -z max-page-size=0x1000 -T kernel/ntclks/arch/x86_64/linker.ld -o build/system/kernel.unstripped <87 objects>
llvm-objcopy --strip-debug ... ; llvm-objcopy --only-keep-debug ...

机制在 build.py:4213-4217:build_roots() 给除 BUILD_NUMBER_EXEMPT_TARGETS 外的每个目标前置 build-info,而该目标是 always=True。实测两次构建把受跟踪的 buildsystem/state/build_number.txt 从 3833 推到 3836,并重写受跟踪的 include/generated/build_info.h。

→ 结论:A02(空操作不重编译)与 A17(受跟踪文件零变化)在旧系统上本来就不成立。新系统必须做到, 而不是"沿用旧行为"。

3. 新 C 宿主工具(P1-a,TDD)

3.1 红灯

命令:gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -Wformat=2 -Wshadow -Wstrict-prototypes -Wmissing-prototypes tests/host/test_common.c

首次输出(正确的失败原因——生产实现不存在):


tests/host/common/... fatal error: ../../tools/host/common/buffer.h:没有那个文件或目录

红灯过程中还暴露了测试脚手架自身的一个真实缺陷:test-support.h 检查 rewind() 返回值, 而 rewind() 返回 void,-Werror 下编译失败。已改为 fseek(stream, 0, SEEK_SET)。

3.2 绿灯

tools/host/common/{buffer,io,process}.{c,h} 实现后:

检查命令结果
GCC 严格警告集gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -Wformat=2 -Wshadow -Wstrict-prototypes -Wmissing-prototypes编译无输出,ok - host/common: 74 checks passed,rc=0
Clang 同一警告集clang + 同上ok - host/common: 74 checks passed,rc=0
ASan + UBSan + 泄漏检测gcc -g -O1 -fsanitize=address,undefined 后 ASAN_OPTIONS=detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1 运行rc=0,无 sanitizer 报告

覆盖的契约(计划第 8 节):扩容保留旧内容、SIZE_MAX 请求被拒且旧缓冲不丢、 buffer_destroy 接受零值结构、按请求 mode 建文件、零字节输入产出空文件、 内容相同则 mtime 与 inode 均不变、内容变化则原子替换且目录内不留临时文件、 只读目录中发布失败且原文件完好、子进程正常退出、非零退出码进 child_status、 信号死亡用 WIFSIGNALED/WTERMSIG 可辨、程序不存在与非existent 工作目录返回 -1 且置 errno、 working_dir 真实生效。

3.3 leonos-emit

单独编译通过(同一严格警告集),手工契约验证:

场景观察
首次写入 --mode 0600文件建立,权限 -rw-------
相同内容再写一次mtime 纳秒级完全不变(...08.468586876)
不同内容再写内容更新,mtime 改变
--help打印用法,rc=0
缺 --outputleonos-emit: --output is required,rc=1
目标目录只读cannot publish ...: Permission denied,rc=1,原文件保留

注意:leonos-emit 尚未有 Make 目标,以上是我用 gcc 直接编译二进制后手工执行的。它被计入 "已编译并验证行为",不计入"已接入构建系统"。

4. 新 Make 系统的 P1 结果

4.1 契约测试

套件命令结果
C 单元测试(普通 + ASan/UBSan)make test-toolsok - host/common: 74 checks passed 与 test_json 各两次,rc=0
入口契约sh tests/build/test-bootstrap.sh22 checks, 0 failures
并发互斥(A09)sh tests/build/test-concurrency.sh17 checks, 0 failures
依赖锁 CLIsh tests/build/test-deps.sh44 checks, 0 failures
增量契约(A02–A07)sh tests/build/test-incremental.sh21 checks, 0 failures
可重现性(A13)sh tests/build/test-reproducible.sh11 checks, 0 failures
第三方/sysroot(A11 部分)sh tests/build/test-third-party.sh29 checks, 0 failures
长测(A08/A10/A16)make test-longtest-execchain 20 checks、test-jobs 19 checks,均 0 failures

make test 的 rc=0 记录:P1+P2-a/P2-b 批次跑为 2:05(make -j12 test,宿主同第 1 节); make test-long 两个套件合计约 6 分钟,独立于 make test。

4.2 目标面

make help(PATH 中无编译器时仍可用)、make doctor、make tools、make defconfig、 make kernel、make fetch、make test-tools、make test-build、make test-long、 make clean 已实现并实测;userland、runtime、 sdk、rootfs、apk-repo、image-vmdk、iso、installer、run*、test-smoke、test-legacy 由 scripts/not-migrated.sh 显式 exit 2,不伪装成功。

4.3 验收行实测证据

编号观察
A02第二次 make kernel 输出的 CC/AS/LD/IMAGE/GEN/HOSTCC/CONFIG 动作数为 0;$(O)/obj/kernel 下全部 85 个对象与 kernel.sys、kernel.debug、kernel.unstripped、autoconf.h、boot_logo.h 的 mtime(含纳秒)逐项不变
A03touch kernel/ntclks/futex.c → 恰好 1 次 CC(且就是该文件)、1 次 LD、2 次 IMAGE
A04touch kernel/ntclks/include/ntclks/types.h → 重编译数 恰好等于 depfile 记录了该头的对象数 82,既无漏编也无过编;随后 1 次 LD
A04btouch kernel/ntclks/arch/x86_64/linker.ld → 0 次 CC、1 次 LD
A05KERNEL_CFLAGS=-DLEONOS_SIG_PROBE → $(O)/obj/kernel 下 83 个 .c.o 的 mtime 全部改变、2 个 .S.o 一个都没改变(逐个对象比较两次快照,不看 Make 的日志流);kernel.sys 等产品 mtime 改变
A09见第 7 节:同 O 双进程已被显式拒绝,不同 O 并行互不串用
A06移走 kernel/ntclks/signalfd.c → 重新链接,且 make -n 的输出不再包含该对象;恢复后再链接一次
A07删除 kernel.sys 或删除 boot_logo.h → 各自被重新生成(多输出阶段无 stamp 误判)
A12O= 空、O=/、O=.(含解析为源码根的相对形式)、含空格、含换行、含非法字符全部被拒;clean 对无所有权标记的 O 拒绝执行;未知 CONFIG_* 命令行覆写报错
A14tools/host 与 tests/host 在 GCC 16 与 Clang 22 的 -std=c11 -Wall -Wextra -Wpedantic -Werror -Wformat=2 -Wshadow -Wstrict-prototypes -Wmissing-prototypes 下零告警;ASan+UBSan(detect_leaks=1、halt_on_error=1)无报告
A17新构建不改动任何受跟踪文件:buildsystem/state/build_number.txt 与 include/generated/build_info.h 哈希保持 ebdbfc0b / b135f754;跨文件系统 O=/tmp/... 构建后源树 git status 干净,且 kconfig-frontends.sh 会回收它留下的 .tmpconfig.* 与空 include/config

4.4 产物等价性(内核)

新系统从空 O 产出的 kernel.sys 为 1196352 字节,与旧基线 build/system/kernel.sys 同尺寸。 两者不可能逐字节相同:旧基线嵌入 LEONOS_KERNEL_VERSION "4.7.1-3838" 与调用时刻的 LEONOS_BUILD_TIME,新系统按 §7 不递增计数、不读挂钟。逐字节比较需在两侧固定 SOURCE_DATE_EPOCH 与 BUILD_ID 后进行,属 A13,本轮未做。

leonos-boot-logo 与退役的 tools/generate_boot_logo.py 输出逐字节一致(仅第一行署名不同), diff 只报告第 1 行;leonos-version 重放出同名同形状的宏集合。

5. 验收矩阵状态

编号状态
A02 A03 A04 A05 A06 A07 A12 A14 A17内核范围内通过(证据见 4.3;A12/A14/A17 只覆盖已实现部分)
A01部分:干净 O 可构建内核与 musl sysroot,make fetch 已实现并验证(见 6.2),但 userland/runtime/sdk/镜像 目标未迁移,故"全目标"未达成
A08通过:-j1 与 -j8 各三轮(含一轮 82 对象的重轮),选中的对象集合两侧逐行相同,六个产物 SHA-256 两两相同(见 7.4,19 项)
A09通过:同 O 双进程按"明确拒绝"契约实现并有变异检验;两个不同 O 并行互不串用;嵌套 make 继承同一 O 的锁不死锁(机制与证据见第 7 节,17 项)
A10部分通过:中断恢复已证(SIGTERM 落在工作中、无伪产物、重跑逐字节恢复,见 7.5)。未做:磁盘写失败(需容量受限文件系统)、交互 Ctrl-C(需 pty)、子进程失败的独立 fixture
A11部分通过:make fetch 下载 19 项并逐一校验 SHA-256(实测 alpine-findutils 摘要与锁文件一致);--verify-only 在缺项时列出 id 并非零退出、不联网。未覆盖:断网下完成全套生产目标(属 P4)
A13通过:固定 SOURCE_DATE_EPOCH=1700000000 + BUILD_ID 下,两个不同深度的干净 O 全量重建,autoconf.h、boot_logo.h、build_info.h、obj/kernel/sources.list、kernel.unstripped、kernel.sys、kernel.debug 七个产物 SHA-256 两两相同;原地重建后镜像不变(tests/build/test-reproducible.sh,11 项)
A15未运行:SDK/APK/三类镜像与升级尚未迁移
A16已迁移范围内通过(见 7.6):defconfig(4259)、tools(204)、kernel(348)、musl sysroot(3040)、暖缓存 fetch(100)、缓存校验(97) 六条 execve 跟踪零 python/meson/ninja,且每条都断言跟踪非空。未覆盖:SDK 与镜像链(未迁移)
A18 起不适用

5.1 P1-d 的来宾运行验证:受阻,未通过

make kernel 的产物已在 QEMU 中尝试启动,但判定不成立,因此本项记为受阻而非通过:

  1. 首轮用 -bios 指向不存在的路径,QEMU 直接退出;对照组同样失败,属夹具缺陷。
  2. 次轮不带固件(SeaBIOS):control(旧 build/system/kernel.sys)与 candidate (新 out/.../kernel.sys)的串口日志完全同形,都停在 Booting 'LeonOS 4 Live Desktop (musl + Vim + GCC)' 后重新回到 GRUB 横幅。
  3. 三轮用 /usr/share/edk2/x64/OVMF.4m.fd:对照组的串口变成 EDK2 自身的异常寄存器转储 (Find image based on IP(...)、CR0/CR3/IDTR 等),说明该固件镜像与 q35 + 这张 --bios 生成的 ISO 组合不可用。

新旧内核在所有配置下行为一致,因此没有回归证据,但也没有成功启动证据。 按 AGENT.md 第 7 节"编译通过不等于镜像已含文件;镜像已生成不等于虚拟机能启动", 以及计划第 11 节"测试必须识别来宾成功标记并核验退出状态,启动 QEMU 进程本身不算通过", 本项必须等 mk/run.mk + QMP/截图判定(P3)落地后重做。用于对照的两张 ISO 与临时产物已删除。

6. P2-a:依赖锁文件、make fetch 与 musl sysroot

6.1 锁文件成为唯一权威

configs/dependencies.lock.json(schema_version=1,41 条)合并了原先分散在三处的同类职责: configs/auth-upstream.json(7 条)、configs/storage-upstream.json(3 条)、 configs/openrc-packages.json(5 条 APK,其 URL 原先是 repository + 名字 + 版本 拼出来的, 现在逐条存为字面 URL),另加 23 个 third_party/ 子模块的固定提交、LTP、Linux 6.12 头、 fastfetch 与 apk-tools-static。补丁不再"构建时临时算摘要",而是 {"path","sha256"} 钉死: patches/musl/0001-...patch = e336ed20…,与旧系统留下的 .leonos-musl.json 记录一致。

解析与校验只有一处:tools/host/manifest/json.c(严格 JSON 读取器,66 项 C 契约测试)+ leonos-deps CLI(44 项 shell 契约测试)。计划第 5 节禁止"用正则冒充 JSON 解析器", 因此 Make 侧只在解析期对锁文件整体取摘要放进签名,从不切片读字段。

三个旧清单文件仍然存在且仍被旧 build.py 使用;它们的消费者(auth-upstream、 storage-upstream、apk-root)迁移到新目标时改读锁文件,随后删除旧文件。本轮不提前删, 以免旧构建系统在本次交付里失去依据。

6.2 make fetch 与离线契约

tools/build/fetch.sh 逐行消费 leonos-deps --fetch-list(id⇥sha256⇥url⇥缓存名): 下载到 .<name>.partial.$$/、校验、再用 link() 原子发布;摘要不符直接失败且不回写锁文件。 实测:19 项全部下载并校验通过(cache/downloads 176 MB),第二次 make fetch 全部命中缓存、 零联网;--verify-only 在空缓存下逐一列出缺失 id 并非零退出。并发不需要锁文件: 两个进程各自下载自己的 partial,link() 只有一个赢,输的一方复用已存在文件并重新校验。

6.3 musl sysroot 的产物等价性

mk/third-party.mk + tools/build/musl-sysroot.sh(POSIX sh + musl 自带 configure/Makefile, 无 Meson/Ninja/Python)产出 $(O)/sysroot/musl。与旧基线 build/musl/sysroot 逐文件比较:

项结果
文件数239 = 239
全部文件 SHA-256238 个相同;唯一差异是 .leonos-musl.json(indent=2 排版不同,json.load 后内容完全相同)
lib/libc.so / lib/libc.aSHA-256 逐一相同
config.mak归一化前缀路径后逐行相同
重复 makeno-op(stamp mtime 不变,无 SYSROOT/RESET/PATCH 动作)
耗时首次约 75 s(-j8,含 configure/make/install/mimalloc)

tests/build/test-third-party.sh 29 项把上述内容固化为契约:14 个链接产物既声明又存在、 stamp 记录的提交/补丁摘要与锁文件一致、锁文件内容变化会移动 sysroot 签名(保持 mtime 也移动, 证明走的是摘要而非时间戳)、校验失败的缓存文件不会被 --verify-only 删除。

6.4 本轮由测试暴露并修掉的两个自身缺陷

  1. 测试计数缺陷(严重):test-bootstrap.sh 的 expect_output_contains 与 expect_failure 打印 FAIL 但没有累加 failures,因此第 4.1 节记录的 "20/20" 中,关于 ARCH/PROFILE/O 拒绝与 HOSTCC 分组的断言永远不会使套件失败。已补计数,并把判定改为 双保险:mk/tests.mk 现在既看退出码也扫描输出中的 FAIL - 行。修好后 bootstrap 的真实计数是 22/22,且 make doctor 被改为显式打印 HOSTCC、TARGET_CC、TARGET_LD 三行,使"宿主与目标编译器相互独立"成为可断言的事实而不是空话。
  2. A04 断言自身的子串匹配缺陷:test-incremental.sh 原来用 grep -lF "$header" 统计"哪些 depfile 引用了这个头",而 depfile 里的路径是子串关系 (ntclks/signal.h 被 posix/signal.h 之类前后缀包含时同样命中),于是期望值比真实 消费者多一个,表现为"改了 types.h 只重编 81 个"。现改为按整词统计;A05 也从 "对象数 vs 磁盘计数"改成"干净构建编译过的源文件集合 vs 本次重编集合", 失败时直接点出是哪个对象没重编。修好后的三轮串行/并行复跑结果见本节末尾。
  3. 同 O 并发的签名争用:$(O_META)/<class>.candidate 原先是固定文件名,两个共享同一 输出目录的 make 进程在解析期会互相覆盖对方的 candidate,于是某个进程可能提升一份 自己从未计算过的签名。上一轮把它改成 $(O_META)/<class>.$(MAKEPID).candidate 并记录为 "已修",这个结论是错的:GNU Make 在 POSIX 平台上不把 MAKEPID 定义出来,实测 make -f x.mk 里 MAKEPID=[],于是文件名退化成 <class>..candidate,仍然是共享的。 本轮改用 LEONOS_PARSE_ID := $(or $(MAKEPID),$(shell echo $$$$))(后者取一个子 shell 的 pid, := 保证每个 make 进程只展开一次),并确认 $(O)/meta 下的 candidate 名字真的带上了 pid。
  4. A05 读的是从未写入的文件:把 A05 的度量从"数 Make 日志行"改成"比较两次快照的 对象 mtime"时,快照被存进了 shell 变量 before_flags/after_flags,而下面的 changed_objects 却按文件名读 $work/flags-before/$work/flags-after。awk 打不开第一个 文件就什么都比不上,于是稳定报告 moved=0 of 83。改成落盘快照后 83/83 才是真的量出来 的。同一次改动还暴露出 comm 在混合 locale 下对 C 排序输入告警,已在套件顶部 export LC_ALL=C。
  5. 同 O 双 make 并未被拒绝(上一条遗留的问题):见第 7 节。

7. P2-b/P2-c:并发互斥、并行调度与执行链(A09、A08、A10、A16)

7.1 要求与为什么不是 flock

计划第 6.3 节要求:同一 O 的两个顶层构建必须明确拒绝其中一个或经测试安全串行;不同 O 可以并行;递归 make 不得重复获取外层锁而死锁。

先试的是 flock:Make 无法为整个构建持有一个描述符(每条 recipe 各自 fork shell),也没有本 构建可依赖的退出钩子——实测 GNU Make 4.4.1 既不执行 .EXIT 也不执行 .STATUS(正常退出、 SIGINT、SIGTERM 三种情况下都没有运行它们的 recipe)。所以锁改成"owner 记录 + 进程存活性": 记录文件名是 <pid>.<进程启动时钟滴答>,只要那个进程还在且启动时间一致才算持有;构建被 SIGKILL 也不会永久卡住输出目录。

7.2 机制(scripts/build-lock.sh,由顶层 Makefile 在解析期调用)

7.3 实测(tests/build/test-concurrency.sh,17 项)

同一 O:先起的 make 完成构建(85 个对象与 sources.list 逐项对得上、kernel.sys 非空), 后起的 make 非零退出并打印持有者 pid、启动滴答与记录位置;-n/help/doctor 在锁被 持有时仍然可用;构建结束后下一条 make 不再看到陈旧记录。两个不同 O 并行:各自产出镜像, 各自生成的 sources.mk 只指向自己的目录(不串用)。嵌套 make 不自拒。

变异检验:把 Makefile 里的加锁分支改成永不成立(等价于"没实现互斥")后复跑,同一 O 的 两个 make 都跑起来,套件报 3 项失败,其中第一项是真实伤害——后起的进程在 host/kconfig-frontends 的构建目录上把先起的进程撞失败(kconfig-frontends: front end build failed)。这正是该锁要防的事,也证明这些断言测的是生产规则而不是测试自己的空文件。

长测套件独立成 tests/long/,由 make test-long 运行(make test 只跑 tests/build/): 它们要重复整棵树的构建,按第 13 节不能混进每次改动都要跑的套件里,但也必须是可运行的目标 而不是"忘了跑的脚本"。

7.4 A08:-j1 与 -j8 各三轮(tests/long/test-jobs.sh,19 项)

两侧都从干净 O 开始,固定 SOURCE_DATE_EPOCH=1700000000 与 BUILD_ID=7,因此两棵树按构造 可比;对象快照记 %T@(纳秒)并按各自树的相对路径记录,否则比的是测试自己的目录名而不是 Make 选中的工作集。

观察结果
第 1 轮(types.h,82 个消费者)-j1 与 -j8 移动的对象集合完全相同(82 = 82,逐行 cmp)
第 2、3 轮(单个 .c + 链接脚本)两侧同为 1 个对象,集合相同
产物内容kernel.sys、kernel.debug、kernel.unstripped、sources.list、autoconf.h、build_info.h 六个产物 SHA-256 两两相同(如 kernel.sys 4afa332cc79e…)
签名归一化 -I<tree>/include 后,两个 job level 的 kernel-cc.sig 相同:工具、身份、flags 一致
声明四个内核动作类(cc/as/link/objcopy)都发布了 .sig;未被本目标用到的类没有签名是预期的

7.5 A10(中断部分):并行构建被打断不留伪产物

同一套件里用 -j2 重编 82 个对象,等到确实有对象被重写再发信号(kill -INT 一度得到 status=0,原因是 POSIX shell 在关闭 job control 时会给后台命令把 SIGINT 设为 ignore,信号被 吞掉、构建跑完了;改用 SIGTERM 后 status=143,这才是真的打断)。断言与结果:

未覆盖:计划第 13 节要求的"写失败"(容量受限文件系统)与交互 Ctrl-C(需要 pty)。A10 因此 记为部分通过,不是全通过。

7.6 A16:execve 跟踪(tests/long/test-execchain.sh,20 项)

对每个已迁移的生产入口跑 strace -f -e trace=execve,然后检查被执行程序的路径与 argv 里的 可执行词。"没有 Python"必须是看到的,不是推理出来的。

入口execve 记录数python/meson/ninja
make defconfig42590
make tools2040
make -j4 kernel3480
musl sysroot(上游 configure + make)30400
make fetch(硬链接暖缓存,不联网)1000
缓存校验(--verify-only 路径)970

每条目还断言"跟踪确实看到了真实工作"(execve 记录 >20 且包含编译器/make),否则"零命中"是空 跟踪造成的假象。另外 8 项断言 userland/runtime/sdk/rootfs/apk-repo/image-vmdk/iso/installer 仍然以退出码 2 明确拒绝——这是"旧 Python 链没有被偷偷留着"的另一半证据。

未覆盖:SDK 与镜像链本身(尚未迁移),所以 A16 记为已迁移范围内通过。

8. 发现并已修的计划/环境问题

.gitignore 第 1 行的 build/ 会匹配任意层级名为 build 的目录,导致计划第 5、12、15 节要求的 docs/build/migration-inventory.md、docs/build/legacy-removal.md、docs/build/verification.md 和 tests/build/ 契约测试目录全部无法提交;本轮新建的 tools/build/ 是第三次踩坑。 已把首行改为锚定的 /build/ 并移除两条临时反选,验证 docs/build、tests/build、tools/build 不再被忽略,而 build/linux-6.12/tools/build 仍随 /build/ 一起被忽略(git status 无新增噪声)。

9. 已知限制

10. 性能记录与产物事实(计划第 15 节要求)

测量方法与第 2 节一致(同一宿主、同一配置)。命令为 make -s O=<全新临时目录> -j8 kernel, 用 GNU time 的 -v 取墙钟与最大驻留;incremental 是 touch kernel/ntclks/futex.c 之后同命令, noop 是紧接着再跑一次同命令。

用例墙钟峰值 RSS说明
clean kernel(85 个对象 + 链接 + 两个 objcopy,含 host 工具与 kconfig 前端)16.1 s128 MB全新 O,一次性
incremental(1 个对象 + 链接 + 镜像)0.24 s104 MB
no-op0.11 s46 MB与 A02 的"零动作"一致
make test(C 单测双份 + 6 个 shell 套件,含内核全量构建)2:05—make -j12 test
make test-long(A16 六条 execve 跟踪 + A08 两轮树 + A10 中断)约 6 分—make test-long

不做倍数对比:第 2 节的旧系统 kernel-1 = 1.52 s 是热目录上重复同一条命令的采集,不是干净 构建,旧系统没有干净内核构建的同法基线;按计划第 15 节"无可比基线就只报告观察"。 installer 也没有可比数据(旧值 266.7 s,新链未迁移)。

产物事实(截至本提交,新链实际产出并被验证的东西):

产物路径SHA-256(前 12)是否启动过
内核镜像(固定 epoch 1700000000 / BUILD_ID 7)<O>/generated/system/kernel.sys4afa332cc79e否(见 5.1:受阻,且无回归证据也无成功启动证据)
未剥离内核<O>/generated/system/kernel.unstripped45ce7e9b2c02—
调试内核<O>/generated/system/kernel.debugc61de02c4d2f—
musl sysroot<O>/sysroot/musl/(14 个声明产物)见 6.3 的逐文件比较表—

新链没有产出过任何 SDK、APK 仓库或镜像,因此本审查包不附带任何镜像哈希,也没有"旧镜像 冒充新结果"的风险:交付物只有源码、Make 片段、C 工具、脚本与测试。

11. P2-d:认证链第一段(Linux UAPI 头 + libxcrypt)

tools/build/auth-upstream.sh + mk/third-party.mk 的 auth 段,make leonos-auth, 契约套件 tests/build/test-auth-stage.sh(23 项,0 失败)。

11.1 为什么只做这两个包

Linux-PAM 1.7.2 上游只有 Meson(autotools 在 1.6.0 被移除),而裁定是不降级、不开 Meson/Ninja 例外、不删认证,所以 libpam 需要手写 Makefile 移植,本段不包含它, 并且用一条断言把它"不在"这件事钉住:Linux-PAM is honestly absent(stage 里既没有 lib/libpam.so.0 也没有 usr/include/security/pam_appl.h)。仍带 POSIX configure 的包 按计划第 9 节允许由上游自己的 configure/Makefile 构建,因此这里做的是 linux-headers (make headers_install)与 libxcrypt。

configure argv 与交叉编译环境逐条抄自退役的 tools/build_auth_upstream.py:89-98,166-195 (--host/--prefix/--sysconfdir/--localstatedir/--libdir=/lib/--enable-hashes=all/ --enable-obsolete-api=no,LDFLAGS=-Wl,-z,relro,-z,now ...,PKG_CONFIG_*),差异只有一处, 见 11.3。

11.2 与旧系统的一致性

比较结果
usr/include/crypt.h与 build/auth-upstream/root/usr/include/crypt.h 逐字节相同(11195 字节,cmp 通过)
config.h 的 #define 集合两侧各 304 条,diff 无差异:所有探测结果一致
动态符号表 / SONAMESONAME=libcrypt.so.2,导出 crypt、crypt_r、crypt_gensalt、crypt_gensalt_rn、crypt_checksalt,NEEDED 只有 libc.so(musl 的 SONAME;出现 libgcc_s.so.1 会被判为链到了宿主)
库二进制见 11.3:差异的唯一来源是构建目录路径字符串

11.3 一处有意的偏离:-fmacro-prefix-map

比较新旧 libcrypt.so.2.0.0 时先得到".text 大小相同但 206 字节不同、文件差 240 字节"。 根因是 libxcrypt 的断言把 FILE 编进 .rodata:旧树里嵌的是 /home/xiaobai/.../build/auth-upstream/src/libxcrypt-4.5.2/lib/.c,新树是 /tmp/auth1/third-party/auth/src/libxcrypt-4.5.2/lib/.c。.rodata 短了 240 字节, .eh_frame_hdr 及其后所有段地址整体平移 0xF0,于是 .text 里所有与地址相关的立即数 都变了——代码本身相同,差异全部来自编译路径。

这与旧系统行为一致,但对新链是不可接受的:产物字节会随 O= 路径变化,直接违反计划第 6.4 节对可重现的要求。因此 auth 适配器额外传 -fmacro-prefix-map=<srcdir>=libxcrypt,并用断言 钉住结果:两个不同深度的输出树构建出的 libcrypt.so.2.0.0 与 libcrypt.a 逐字节相同。

11.4 顺带修掉的两个真实缺陷

  1. O= 泄漏进内核构建。 内核 Makefile 自己把 O= 解释为 out-of-tree 构建目录,而顶层 make O=/path 通过 MAKEFLAGS 把它传给了子 make,于是 headers_install 把整套 arch/、scripts/ 内核构建树写进了本项目的输出目录。现在显式传 O=$work/build/linux-headers,并新增断言"$(O) 顶层只允许布局里声明过的目录"—— 这条断言属于计划第 6.3/12 节的输出目录安全范围(A12)。
  2. make clean 认不出只做构建目标产生的树。 所有权标记原先只有 defconfig 一类目标才写, 因此 make kernel/make leonos-auth 造出来的树会被 clean 以"不是我创建的"拒绝, 而 $(O)/auth 也不在 clean 的显式清单里。现在标记随每次非被动调用写入,auth 已加入 清理清单,并由 make clean removes the staged tree and keeps the directory 断言覆盖。

11.5 本轮的一次自身失误与恢复(如实记录)

做 11.3 的比较时,我用了 llvm-objcopy -j .text --dump-section .text=FILE <输入> 且没有给 输出文件参数:llvm-objcopy 就地重写输入,于是同时毁掉了新树和旧基线的 build/auth-upstream/root/lib/libcrypt.so.2.0.0(readelf -d 报 no .dynamic section)。 因此那一次比较得到的"段大小相同 / dynsym 相同"结论是在已损坏文件上算的,作废。 恢复方式:用旧系统自己的 action 重新生成(python3 build.py run auth-upstream,rc=0, 0 errors,文件回到受损前的 227216 字节且 readelf -d 正常)。该次重建把两个受跟踪文件 按旧系统的习惯向前跳了一个构建号(buildsystem/state/build_number.txt 3833→3834、 include/generated/build_info.h 的 LEONOS_BUILD_NUMBER/LEONOS_BUILD_TIME), 我已把这两个文件恢复到来时状态,git status 现在只剩本阶段的改动。这也从反面说明新链为什么 坚持不写受跟踪文件(A17)。正确且只读的比较方式是 objcopy -O binary --only-section=.text <输入> <输出>(显式输出文件),本文的结论以重做后的 数据为准。