
字节笔记本
2026年10月6日 · 约 7 分钟读完
landlock-run 发布流程:npm 原生包怎么发
landlock-run 是 DeepSeek 开源的一个 Landlock 启动器:先在自己的进程上装好内核级文件系统允许清单,再执行被包裹的命令。整段实现只有约 300 行 C11,与 musl 静态链接,限制规则跨 execve 继承,而且失败闭合:内核不支持就干脆不跑。它以 npm 包的形式发布,一个 JS 入口包,加上 linux-x64 与 linux-arm64 两个平台包。对这种包来说,发布不是走过场:二进制错一个字节、错一个权限位,用户装到的就是一个跑不起来的启动器,而它又刻意不做安装时编译回退,没有补救机会。项目仓库里的 docs/release.md 就是一份按这个标准写成的发布清单,本文把它拆开讲清楚。

一个版本号管全部包
启动器工作区的根包和三个公开包共享同一个版本号。改版本不靠手动挨个改 manifest,仓库提供了统一的命令:
pnpm --dir native/landlock-run release:bump patch这条命令同时更新根 package.json 和每个子包的 manifest,用 --ignore-scripts --lockfile-only 刷新仓库根的 lockfile(只刷锁不装依赖),最后跑一遍 release:verify 自检。它也接受完整 semver,包括预发布版本;发布工作流会把预发布版本放进 npm 的 next 标签,保证 latest 永远不指向测试构建。
还有一个细节:源码里依赖始终写成 workspace:*,打包时才由 pnpm 换成具体版本号。仓库内部永远跟着工作区走,不会出现改了子包忘了改引用的版本漂移。
tag 与版本号互锁
版本变更被当作一次普通的源码变更:开一个发布 PR(或直接提交),把所有 manifest 和根 lockfile 的改动合进去,再从合并后的那个提交打 landlock-run-vX.Y.Z 标签。前缀是刻意的:仓库里还有其他包家族,各自的发布标签互不打架。发布工作流会校验 tag 与每个包的版本完全一致,对不上就拒绝发布。想省事可以用 release:commit,一条命令完成改版本、暂存、提交三步。
发布前把能跑的都跑一遍
预检分两层。任何主机都要过:pnpm install --frozen-lockfile、build:ts、typecheck、test:entry。在 Linux 宿主上还要多走一层真实演练:build:native 编出当前架构的二进制,test:launcher 跑启动器测试,再用 pack-release.mjs 与 verify-packed-install.mjs 做 --current-platform-only 的打包和安装彩排。这层的意思是:能在本地暴露的问题,绝不留到 CI 上猜。
先演练,再真发
正式发布走主仓库的 Landlock Run Release 工作流,原因是每个平台的二进制都必须在对应的原生 runner 上构建,本地永远只能覆盖自己一台机器。流程分三步:先从发布提交跑一次 publish=false,这一趟构建全部平台二进制、组装并校验载荷、按发布顺序打好 tarball、彩排一遍安装,并把 npm-tarballs 工件传上去供人工检查;然后打并推送 landlock-run-vX.Y.Z 标签;最后从这个标签重跑同一个工作流,这次 publish=true,把打好的 tarball 按顺序推上 npm。演练和正式跑的是同一条流水线,不可逆动作被压缩成最后的一个开关。
平台包先行,入口包殿后
工作流只从最终打包产物发布,顺序由 publish-order.txt 决定:平台包在前,入口包在后。这个顺序不是随意的。入口包用 optionalDependencies 引用两个平台包,如果入口包先上线而新版平台包还没就位,就会出现公开的入口版本指向一个尚不存在的依赖版本的窗口。平台包永远先走一步,这个窗口就不存在。文档还交代了一个边界情况:本机平台的安装彩排可以去 npm 查询不兼容平台包的元数据,因为那个包本来就给不了当前宿主启动器,启动器来自本地对应的 tarball。

首次冷启动与可信发布
发布优先走 npm 的可信发布,用 GitHub OIDC 做身份验证,不需要长期 token;没配好的环境可以退回 npm-publish 环境里的 NPM_TOKEN。这里有个先后矛盾:npm 要求包先存在,才能为它配置可信发布者,而这三个 scoped 包一开始并不存在。所以首发必须先用 @deepseek-ai 组织的 token 走 NPM_TOKEN 兜底,把三个包发出去;之后再逐个把包配置为信任仓库里的 landlock-run-release.yml 工作流,等组织政策允许时移除兜底 token。所有包都以 --access public 发布;另有一条红线:绝不提交带 token 或 registry 覆盖的 .npmrc 文件。
一个可执行位引发的分工
清单里最有意思的一条:手动兜底发布也必须经过 pack-release.mjs,绝不许直接 pnpm publish。pnpm 的 pack 路径(11.7.0 实测)会规范化文件权限,把启动器二进制的可执行位抹掉,发出去的包没人能直接执行;但入口包又离不开 pnpm,因为只有它能把 workspace:* 换成具体版本。于是打包被刻意拆成两条路:平台包没有依赖,不需要 workspace 转换,用 npm pack;入口包是纯 JS,不含二进制,用 pnpm pack。两条路都要过 prepack 门禁:平台包检查声明的每个二进制都存在、可执行、ELF 的 e_machine 与声明的 cpu 一致,bin/ 里没有多余文件;入口包检查构建产物 lib/ 已经在包里。
最后一道闸是 verify-packed-install.mjs:从打好的 tarball 做一次真正的临时安装,把装出来的二进制和工作区构建逐字节比对,确认安装副本可执行,再用装好的启动器跑一次真实的文件系统隔离验证。二进制缺失或不可执行会在这里响亮报错,而不是伪装成内核不支持。
值得抄走的四件事
这份清单写给一个很小的项目,方法论却是通用的:单一版本号加机器改版,消灭手改 manifest 的漂移;tag 与包版本互锁,发布物和源码状态永远对得上;平台件先于门面件发布,依赖图上不出现悬空引用;把发布拆成演练和正式两段,不可逆动作只剩一个开关。原生二进制包没有重发一次就当没发生的余地,这些前置的笨功夫,就是失败闭合在工程流程上的样子。



