Repository navigation
LankeOS v0.21 Release
Codename: Irrawaddy
“And believe me I am still alive.”
“I'm doing maintenance and I'm still alive...”
核心变更 (Major Changes)
0.21 的主题是 “把判据收成一份,把界面收成一层”。这个版本没有新包、也没有上游版本跟进——861 个包里唯一一个版本号变了的包就是 lpkg 自己(另外 linux 与 filesystem 各 bump 了一次 release)。全部工程量都压在包管理器上:lpkg 从 7.1.1 一路推到 8.0.0,中间还顺手换了一整套终端界面。
全新终端界面:pacman 风格
安装的输出以前是一串各说各话的日志行——正在解压到临时目录...、正在解压文件: 300 个已处理、解压完成,共处理 300 个文件、正在复制文件到系统...——同一个包刷七八行,一次 300 包的升级能刷出约 900 行,而且每行都不说在装谁。现在拆成两层,一眼可分:
:: <阶段>分隔条(pacman 风格)::: 下载→:: 解压→:: 安装/:: 升级→:: 运行安装后钩子→:: 系统触发器→:: 已安装 N 个包: …(卸载侧是:: 卸载→:: 系统触发器→:: 已卸载 N 个包: …)。==> <动作> ... [OK]单行动作行(openrc风格..?我以前用 gentoo 的时候有一点印象,大概是吧。[OK]/[FAILED]/[SKIPPED]贴终端最右):每个阶段只有一行,长操作期间先打出"在干什么",右侧原地刷新。- 进度条:
[####------] 45%,下载条带字节数 + 速率 + ETA(mm:ss,pacman 的写法),解压条按字节、安装条按文件数、git 源按对象数。 - 批次末一条 summary:逐包的"X 已成功安装!"全部删除,300 包的升级从 ~900 行降到 ~300 行 + 1 行。
- 非 TTY 一律降级为纯文本:字面意思,我懒得解释了,重定向输出可见。
与之配套的一批"人机界面"修正(都不只是好看,脚本/farm 靠它们分辨成败):
- 取消 ≠ 完成:交互提示答"不"、验证码输错、
Ctrl+C统一抛UserAbort——打消息(不套Error:前缀,用户取消不是错误)+ 退出码 1,且不再打"安装/卸载完成"、不再打 summary。(此前 install/upgrade/remove 三条取消路径会继续走到完成行、退出码 0;remove -r输错验证码就是这个症状。) - 交互输入期间 Ctrl+C 真的生效:所有交互输入改走可中断的
read_line_interruptible()——裸std::cin >> x在 glibc 的SA_RESTART下会被自动重启,Ctrl+C 只置标志、进程仍卡着(用户看到的是"Ctrl+C 无效,只能 kill -9")。 --help走 stdout(lpkg --help | less此前拿到的是空——全写了 stderr);参数不合法打用法仍走 stderr。- 命令拼错不再报"需要 root":未知命令现在在要 root、加锁、跑恢复之前就被拒(打用法 + 退 1)。此前打错一个字母,非 root 用户看到的是 "Root permission is required"——完全指错方向。
query <未安装>/query <无主文件>退出码改 1(此前退 0,且复用 remove 语境的文案);remove只在真删了东西时才打"卸载完成"。install的本地包参数解析不出来时中止,不再"打个错误、丢掉这个参数、然后报成功退 0";同理,请求的目标没能进计划也是硬错(error.target_missing_from_plan)。upgrade在索引解析出 0 个包时不再假装"已是最新":本机装着包而索引是空的,现在抛错而不是打"所有包都已更新"再退 0(远程索引/空索引以前看起来跟成功一模一样)。- 同一原则贯彻在所有路径上:任何地方都不许把异常吞成"打条警告就正常返回"——那等于告诉脚本"做成了"。
安装流水线重构(功能性,不只是版式)
:: 下载阶段就做逐字段的元数据核对:归档的metadata.json与索引逐字段比对(name / version / deps / provides / provides_soname / needed_so),任何一项不符即抛错——此刻什么都还没落盘(连BEGIN_PKGS都还没写)。以前这一步在事务内部做,不一致时是"重解析依赖、重置游标、继续";现在整批回滚并硬报错,且明确不再加回"动态重解析"。- 文件冲突预检提到整批之前:
check_batch_file_conflicts拿全批成员的清单 + 当前归属 + 批内接管顺序一次性判完,跑在事务开始前。以前是逐包判,第一个包的 prerm 钩子可能已经跑过、然后整批回滚。 - 每个批次只写 2 次 DB(
:batch-start/:batch-end),不再逐包 flush。 - 安装后钩子成为独立阶段(仍在提交之后、不在 WAL 里——这是有意的),钩子目录清理改用不抛异常的增量循环,免得把已经提交的批次搞失败。
- 移除侧的两条行为修正:
remove -r/autoremove曾硬编force=true、完全不处理/etc,会直接删掉配置文件——现在每条移除路径都把配置改名成<path>.lpkgsave(只有--purge-config才真删);remove a b(b 依赖 a)以前会被整体拒绝("a 还被 b 需要"),现在批内成员不再互相阻挡。 --overwrite的模式侧补剥尾斜杠(usr/lib/foo/此前静默不匹配),但剥完只剩通配符的不剥(*/保持不匹配 = fail-closed,免得把"只豁免目录"悄悄放宽成"豁免一切")。lpkg scan重写:扫描根从{usr,etc,opt,var}改成整个根,忽略集补上/var、/mnt、/home、/root且不再下钻(以前只是"报告时过滤")。旧实现在扫/时会走进/proc然后静默截断——一边报着"已扫描 389551 个文件"、一边/usr下一个文件都没扫到。现在截断会明说(warning.scan_walk_aborted)。depend remove现在也跟deps边(此前只看 SONAME 边,会少报受影响集合);lpkg depend/scan的树状输出此前是硬编英文,现已接入本地化。
lpkg 8.0.0:两处破坏性改动
两处都不兼容:
-
provides拆成两个字段。旧的一列同时装着"虚拟能力"和"本包导出的 SONAME",分类只能靠字符串形状猜(looks_like_soname),由此生出三条实测过的毛病:求解器里两个命名空间互相串、farm 全量覆写provides会把手写的虚拟提供顶掉(实测真实仓库 2748 条provides全部含.so,虚拟提供 0 条)、于是"虚拟包"这条语义只活在测试里。现在池里只剩两个命名空间,全程零形状判断:deps: X / 能力型 target → 裸名 X → 包名(自提供)或虚拟 provides needed_so: X → so:X → 只有 provides_soname 里的 Xso:前缀含:,而包名不允许含:,所以这两者结构上不可能再出现离奇冲突。索引版本块随之收紧为恰好 6 段(ver:hash:deps:provides:provides_soname:needed_so),新增provides_soname.db(DB 一族 6→7 个)。 -
版本语义直接采用 rpm 的 EVR。旧设计有两套语义(lpkg 自己的
-= 预发布 /+N= 发行修订号,加一层to_libsolv_evr()编解码桥),两套判据在 4032 组合的(候选 × 约束)矩阵上差过 78 处,两个方向都有——假满足会装出坏系统,假不满足则事务无解。期间先用"换更冷门的编码(^当 release 分隔符)"救过一轮,但那只治标:只要还存在第二套语义,就得靠手动保持一致活着。8.0.0 把桥接整段删掉了。 -
成品版本串随之由
1.2.3+1变成1.2.3-1(807 个带非零 release 的包受影响,纯文本迁移)。 -
预发布改用
~:1.0~rc1 < 1.0;而-rc1现在是 release,1.0-rc1 > 1.0。 -
顺带修掉一处既有误判:
ncurses=6.5-20250809是上游的补丁快照(更新),旧语义把它判成旧于6.5,新语义下它是6.5的 release。
ABI 审计进入「符号版本」级
以前只按裸 SONAME 匹配。glibc 升版新增 GLIBC_2.40、libstdc++ 的 GLIBCXX_3.4.x、Qt 的私有 API——这些都是同一个 SONAME 内部的 ABI 细节,"二进制要求某个符号版本、提供者拿不出来"过去完全看不见,只能等运行期崩。
- 语法:
libc.so.6(裸)、libc.so.6@GLIBC_2.40(单个)、libc.so.6@{GLIBC_2.40,GLIBC_2.39}(多个,只能在末尾、只出现一次)。 - 匹配是保守的:带符号版本的需求只能被"也声明了符号版本、且覆盖它"的提供者满足——只写裸
libc.so.6的提供者不算数(否则这个特性形同虚设)。裸需求不受影响:声明得更细不会打破裸需求。 - farm 自动产出:每个 ELF 的
.gnu.version_d(verdef)与.gnu.version_r(verneed)被扫出来写进这两个字段,不需要手编。实测产物:glibc 的libc.so.6声明了 46 个版本节点,gcc 的libgcc_s.so.1@{GCC_3.0,…,GCC_7.0.0},qt6-base 的libQt6Core.so.6@{NonQt,Qt_6,…,Qt_6_PRIVATE_API}。 - 全仓 861 个包一次性回填:逐包解包 → 扫 ELF → 写回配方 → 按同一份扫描结果重打归档,并做了全量独立复算与三方一致性验收。
- C++ (
so_spec.cpp) 与 Python (check_deps.py) / Rust (scan.rs) 三方切分规则同一份,由同一组真值表双向钉住。
引入 fuzzing,然后它抓到了三个真缺陷
第一次给 lpkg 上 libFuzzer(make fuzz,ASan + UBSan,11 个 harness:ELF strip、strip 整归档、归档成员名、版本比较差分、SONAME 目录、WAL 行、DB 行、依赖串、索引行、求解器差分、get_elf_soname)。它上线当天就在 strip.cpp 的 ET_REL 路径上抓到三处畸形输入漏洞:
e_shoff没有下界 ⇒e_shoff == 0时写节区头正好覆盖 ELF 头,函数返回 true,损坏内容被写回原文件、还报了成功。sh_offset/sh_size没有量级封顶 ⇒ 一个 1224 字节的畸形 ET_REL 能声明约 480 GB 的节区,elf_update照着去分配。sh_addralign没有量级封顶 ⇒ 一个sh_addralign = 2^56的节区让输出偏移跳到 64 PiB,memfd 被ftruncate到 64 PiB 并真实占掉几十 GB。实测:宿主可用内存 7 秒掉 48 GB,而进程自身 RSS 只有 0.4 GB。这正是之前几次"莫名其妙打崩桌面"的元凶——最小复现是"取一个正常.o,只把这一个字段改成2^32",读代码几乎不可能发现。
多轮并行审计的收口(安全与正确性)
0.20 之后到 0.21 之间跑了多轮并行只读审计(五路 / 四路,按子系统切:装卸入口、安装趟与文件操作、ELF 与归档、WAL 与恢复、求解器与配置,外加定位与测试可信度专项):
- 暂存目录改为
mkdtemp(原子、0700):此前是exists()探测 +create_directories(),TOCTOU——本地无权用户预建同名目录或符号链接就能劫持 root 的暂存目录。 --root下的祖先链穿透:落位目标只解析父目录、且新增整条路径级判据,包内合法的绝对符号链接照旧放行,但从中间段逃出 root 不可能了。写入层 8 个原语各自入口加了同一道闸(DB 键派生的路径从此与归档内容那条腿同源)。DIR_META字段改严格十进制解析:stoul("-1")不抛、回绕成07777(setuid + setgid + sticky + 世界可写)——一条写坏的 WAL 行就能把任意目录改成完全开放。- 恢复期的 post-commit 清理加前缀守卫:此前按 WAL 里的字面路径
remove_all,一条被篡改的CLEANUP /etc能删掉整棵/etc。 - WAL 回滚的 DB 备份判定改 lstat 语义:悬空链接会让回滚静默跳过还原,备份随后被清理。
- 归档成员名分帧字符:
deps/provides/needed_so拒控制字符(\n会读回成额外依赖、\t会截断provides.db的键),包名/版本额外拒,/|/;(coreutils,evil会让files.db的属主集合读回成两个幽灵属主);归档里的特殊文件(FIFO / 字符 / 块设备)整包拒绝。 - 新增
archive/tar_guard.{hpp,cpp}:重名成员 / 控制字符 / 超长名字 /..分量 / 符号链接目标逃出根,五条判据一律整包拒绝。 - 静态库(
.a)符号索引重算:strip让.o变短后,照抄的ar索引偏移就失效——本机实测有 4 个真实.a被改坏(ld直接报no more archived files)。现在按 GNU ar 自己的做法重算索引(占位 + 原地回填),认不出的索引(BSD__.SYMDEF/ 畸形 GNU 索引)则整份归档不剥 + 告警——宁可少剥一个库,也不写一份偏移可能错的索引。 - 反向依赖的键提取收成一份(
dependency_name_of()):此前某处按空白切分,于是provb>=2.0变成一个查不到的键——autoremove有可能删掉一个仍被需要的包。移除侧的状态读取同时改成 fail-closed(FIFO / 不可读 / 目录都会报错并回滚,以前会被读成"没有反向依赖",留下永久的陈旧边)。 content/根必须是真目录、落位暂存路径拒符号链接(error.tmp_path_is_symlink)——构造过的归档不能把包内容指向解压根之外,也不能让暂存写入穿过链接改到目标上。- WAL 未配对批次守卫:WAL 里还有未完成批次时拒绝开新批次;且此时
cleanup_db_backups()提前返回(lstat 语义),不把唯一的还原点删掉。 - SIGINT 检查点铺到进程内的长步骤(curl 进度回调、strip 循环、打包前):此前只在阶段边界检查,下载 / strip / 打包窗口里的 Ctrl+C 被静默吞掉;
build/pack此前更是直接SIG_DFL,中断后会留下build/work|content|hooks残骸。
结构重构
base/utils.hpp拆成 11 个子头(process/locking/tmpdir/path_predicates/path_safety/stash/fs_atomic/io_set_file/repo_index/strings/xattr),utils.hpp变成只做转发的聚合头——约 90 个 include 方零改动。- 新增
repo/revdep.{hpp,cpp}:反向依赖图收成一份实现,按边种类参数化(SonameOnly/DepsAndSoname),替换原先两份各自演化、会漂移的实现。 db/*收口:DB 里程碑坍缩成每批次两个(:batch-start/:batch-end),批次配对与扫描式恢复、备份 GC 守卫、op-sink 与 WAL 语义统一。- 工具链:删除
build_deps_summary.sh(636 行);check_deps.py扩展成 SONAME /provides_soname感知;新增check_index_conformance.py(C++ 与 Python 的索引格式契约,make check-index-format)。 - Makefile:新增
check-index-format/compile-commands/tidy/test-sanitize/fuzz/fuzz-collect目标;构建依赖去掉fmt-dev(改用自带的string_format)、加入clang/compiler-rt/clang-extra-tools(sanitizer 运行时与 clang-tidy);format-check覆盖 165 → 197 个文件。 - CI 接上了
make format-check:这个检查在 CI 里一直没接线(容器是 alpine、没装 clang,必须跑在宿主)。现固定在ubuntu-24.04+ apt.llvm.org 的官方clang-format-23——版本必须钉在 23.x,clang-format 换大版本会因换行决策不同而误报。
构建农场(lankefarm)
- 适配lpkg的新动态库符号版本解析代码 生成lpkg可识别的metadata。
- 版本比较器换成 libsolv rpm 算法的忠实移植(
track/vercmp.rs),以 3944 对期望值(由容器里的真 libsolv 生成)逐值对齐兜底——那是"移植是否忠实"的唯一证据链。
打包修复
- filesystem:打包时
chown -R root:root(此前 staging 树里的属主是1000)、清掉源里的.gitkeep占位文件,并补上/var/lock → ../run/lock、/var/run → ../run两个符号链接。 - linux:打开
CONFIG_MEMCG。
迁移指南
- 0.20 → 0.21 不是
lpkg upgrade一条命令能完成的。索引版本块从 5 段变 6 段、provides拆成两个字段,7.x 的 lpkg 读不了 8.0.0 的仓库,8.0.0 也读不了 7.x 装出来的 DB。两条路二选一:- 推荐:下载新 ISO 重装或就地替换(干净、无残留)。
- 就地迁移:解压
lankeos_final_0.21_pkgs.tar.xz与lankeos_final_0.21_lpkg_merge.tar.xz,然后它会用你现在的 lpkg 把 lpkg 本体重装成 8.0.0,之后每个包都用新 lpkg 重装一遍——这是让新语义重建数据库最直接的办法。注意sudo ./lpkg-merge --dry-run --pkgs-dir /path/to/pkgs # 先只看计划 sudo ./lpkg-merge --pkgs-dir /path/to/pkgs # 确认后执行
reinstall必须带--no-deps(迁移工具已经替你带了;手工重装时忘了会撞 unresolved_soname)。
- 预发布写法变了:以前写
1.0-rc1表示预发布,现在-rc1是 release(比1.0新)。要表达预发布请写1.0~rc1。如果你自己写的依赖约束里用了-rc1/-beta这类后缀,升级前先改过来。 lpkg install <SONAME>不再解析 SONAME:install 不做任何 soname 特判,找不到就按"未找到包"报错。要按能力安装请用虚拟provides。- 虚拟
provides不能用so:前缀:那是求解器内部的 SONAME 命名空间,写了会被元数据校验直接拒绝(报错会点名是哪一段)。 - 包的成品版本号形态变了(
1.2.3+1→1.2.3-1),如果你有脚本在解析版本串,注意这个分隔符。 - 给脚本作者的提醒:本版把"取消"和"失败"都收敛成非零退出码(以前取消/部分失败可能退 0)。如果你的脚本只看退出码,这版之后它的判断才是对的;反之,若你此前依赖"退 0"来判断某种状态,请重新核对。
镜像体积变化
- 完整 LiveISO 1.40 GB(1.30 GiB)—— 与 0.20 的 1.40 GB 几乎逐字节相同(+0.02%,主要是内嵌的 lpkg 换成了 8.0.0)。这个版本不跟进上游版本,所以体积没有变化。
- 完整包 archive(
lankeos_final_0.21_pkgs.tar.xz)2.44 GB(2.27 GiB),861 个包。 - 构建工作区快照(
lankeos_final_0.21_workdir_snapshot.tar.xz)13.7 MB。 - 新增迁移工具(
lankeos_final_0.21_lpkg_merge.tar.xz)417 KB。
稳定性与兼容性
本版本经过验证:
- 全部 861 个包在 farm docker 容器中构建通过(
farm/out里 861 个.lpkg) - 全量破坏性改动后重新打包并回填符号版本,861 个
.build_ok全部重算,且逐一复核与当前配方内容一致(861/861 匹配,0 失配) - lpkg
make test:1216 用例 / 144 套件 / 1215 通过 / 0 失败 / 1 跳过 - lpkg
make fuzz:11 个 harness 全绿(约 986 万次迭代,无崩溃样本) - farm
cargo test:323 通过 / 0 失败,cargo fmt --check与cargo clippy --all-targets0 告警 - 仓库里 861 个真实
.lpkg全量扫描,新归档判据零误报 - LiveISO 内嵌的 lpkg 已更新到 8.0.0 (顺便所有包的安装也都修改了)
下一步计划 (Roadmap)
- farm 本机并行调度器(阶段 1):把
run_build的顺序队列换成 DAG 前沿调度器 + N 个本地 docker 槽位,验收标准是-j1与-jN的产物逐字节相同 - farm 分布式构建(阶段 2/3):client/server + SSH,复用阶段 1 的 worker 池与前沿抽象
- COSMIC 桌面(0.20 起在列,仍未开工)
开发者的话
0.21 的主题是 “把判据收成一份,把界面收成一层”。
(啊,天呐,是重构,我们没救了)
这个版本没有新包、没有上游版本跟进——861 个包里唯一版本号变了的包就是 lpkg 自己。所有工程量都在包管理器上。
我基本上...算了不说了你们自己看吧。
消费金额:¥452.17CNY API 请求次数:43,442 Tokens:10,773,665,193
AI真是太好用了你们知道吗 百亿Token这块。
我这几天简直算是疯魔了,在终于有时间自己审计一下那些之前完全用 AI 写的代码之后我发现这什么鬼啊,不该用启发式的地方给用上了,provides 语义混淆,非得来个 libsolv vercmp 语义桥接而不是直接用其默认语义比较...
完全就是根本不知道这写的什么代码。顺便 1000 多个测试都抓不完 bug,这 lpkg 质量真不行了,需要大修。那么大修改语义正好 breaking,我索性彻底用AI重写了一遍。
顺便弄了 fuzzing,然后它当天就抓到了三个 strip 缺陷。虽然都没有藏得很深,但是效果相当诡异——一个 sh_addralign = 2^56 的节区能让 memfd 被撑到 64 PiB,7 秒吃掉宿主 48 GB 内存。我只是跑个 fuzz 就会把桌面打崩。看得出威力了。
LankeOS可能几天内无commit但是不是断更了,是我这几天维护压力已经到了就算用AI写也得每天写12小时的状态。甚至我release note里变更部分都得用AI写了。我感觉自己不太行了,国庆假期最后两天给自己放个假。
—— Wtada233
10月3日 lpkg 7.2.0:全新终端界面 + 引入 fuzzing + 内核开 memcg
- 新增
ui/term输出层:::阶段分隔条、==> ... [OK]单行原地刷新、进度条(下载带字节/速率/ETA)、批次末 summary;非 TTY 降级纯文本 - 安装拆成「下载 / 解压 / 安装 / 运行钩子」等阶段,逐包日志压缩成一行
- 首次引入
make fuzz(libFuzzer + ASan/UBSan)与语料;当天抓到strip.cpp的三处畸形输入漏洞(ELF 头被覆盖 / 尺寸无上限 / 对齐值撑爆 memfd) - 内核开
CONFIG_MEMCG(linux release 3 → 4)——此前docker --memory/systemd MemoryMax都是空壳 - 静态库
ar符号索引按新位置重算并保留(不再丢弃);认不出的索引整份归档不剥 + 告警 - 修两条中危:
--root下经符号链接祖先穿透、归档成员名守卫漏了.lpkg_bak_命名空间 - 紧接着的多轮并行审计收口:暂存目录改
mkdtemp、DIR_META严格十进制、WAL 回滚 lstat 语义、恢复期清理前缀守卫、归档特殊文件拒绝、metadata 分帧字符等
10月4日 lpkg 8.0.0:provides 拆分 + rpm 版本语义
provides拆成"虚拟 provider / SONAME"两个字段,池里只剩两个命名空间,零形状判断;索引版本块收紧为恰好 6 段,新增provides_soname.db- 版本语义直接采用 rpm 的 EVR,删掉
+N编码桥;farm 的track/vercmp.rs换成 libsolv rpm 算法的移植(3944 对 fixture 逐值对齐) - 861 个配方的
provides键改名provides_soname(纯文本迁移,不走 JSON 重序列化)
10月5日 符号版本 + farm 修复 + 全量回填
needed_so/provides_soname支持 ELF 符号版本(保守匹配),farm 扫 verdef/verneed 自动产出;新增base/so_spec唯一实现,C++/Python/Rust 三方同一组真值表- 全套件回填符号版本 + 版本号迁移到 rpm 语义 + 重算全部 861 个
.build_ok - 修 abifix 的 SONAME 判据口径——此前会误报 734 个包并给它们白 bump release
- 补一条"部分覆盖必须拒绝"的端到端用例(provider ⊂ need)
10月6日 畸形归档守卫 + 打包
- lpkg 修 6 条审计缺陷,新增
archive/tar_guard(5 条判据,整包拒绝);"反斜杠一律拒"在真实仓库全量扫描上撞出 systemd 的误报,当天删除 - filesystem 包修属主与
.gitkeep残留,补/var/lock、/var/run符号链接 - 重新打包 0.21 的 ISO / pkgs / workdir_snapshot,并发布 lpkg-merge 迁移工具
10月18日 进行 world rebuild + 大量修复
- 全部 871 包 rebuild 并发布
- 修改 Python 生态自举方式为 lanke-py-bootstrap 代替 Arch 的实现
- 在修改自举方式后顺利在一天内迁移到 Python 3.15 并构建完成所有受影响包
- KF 6.31.0 和 KDE Plasma 6.8.0
- 修改众多坏 tracker,现在全部可运行
- 所有包升级到最新版本
- 更换默认 dbus-daemon 实现为 dbus-broker
- 修复 lpkg 在进行 strip 时无法处理部分 ELF 的问题
- 修复 farm 的 repack,换掉无法处理稀疏文件的 tar crate