Nix 打包与可复现安装
本文说明 Kuruwa 的 Nix flake、包内布局、安装配置和真实打包烟测。范围只有一个
x86_64 Linux 输出包:产品 kuruwa、补丁版 RMUX 0.8.0、示例定义和安装配置。
不含 overlay、NixOS module、开发 shell 或 apps 输出;nix run 依赖包的
meta.mainProgram。
1. 固定输入
flake.nix 的两个输入与 devenv.lock 完全一致:
| flake input | devenv.lock 节点 | revision |
|---|---|---|
devenv-nixpkgs |
nixpkgs |
cachix/devenv-nixpkgs 760b85220489f69855e6df78a6d1dad5873f8bf6 |
nixpkgs-src |
nixpkgs-src |
NixOS/nixpkgs 8edc0c72e3a38faf5434e40d1f19431125fe4b30 |
两者都是 flake = false,pkgs 通过
import devenv-nixpkgs { inherit system; } 得到,也就是 devenv 正在使用的滚动包集
及其 overlay。flake.lock 是在本地生成的,没有独立更新任何依赖;它的 narHash
与 devenv.lock 中两个节点的值相同。
Rust 依赖来自仓库 Cargo.lock;项目没有新增 Nix 专属依赖或环境 pin。
2. 构建与运行
在仓库根目录:
nix build .#kuruwa
nix run .#kuruwa -- --help
如果新增的 flake 文件还没有被 Git 跟踪,使用路径 flake,避免为了构建而 git add:
nix build path:$PWD#kuruwa
nix run path:$PWD -- --help
packages.x86_64-linux.kuruwa 与 default 是同一个派生包。构建完成后:
export PATH="$(readlink -f result)/bin:$PATH" # 仅当前 shell
kuruwa --help
flake 检查:
nix flake show path:$PWD
nix flake check path:$PWD
3. 包内布局
bin/kuruwa # makeWrapper 包装的产品 CLI
share/kuruwa/kuruwa.toml # 生成的安装配置(绝对 store 路径)
share/kuruwa/examples/kuruwa.toml # 原仓库示例,逐字节安装
share/kuruwa/examples/init-host.sh # host 初始化示例
share/kuruwa/examples/init-bwrap.sh # bubblewrap 初始化示例
share/kuruwa/examples/kuruwa-sandbox.toml # 声明式 sandbox 示例配置
share/kuruwa/examples/sandbox-hooks/instance-work-dir.sh # 上面配置引用的 hook
share/kuruwa/logo.svg # 纯图标 logo
bin/kuruwa 由 makeWrapper 包装产品二进制,做四件事:
--set-default KURUWA_RMUX_BIN=<patched-rmux>/bin/rmux。用户显式设置的KURUWA_RMUX_BIN优先;无效路径会在启动时明确报错,不会回退到包内后端。--set-default KURUWA_BWRAP_BIN=${bubblewrap}/bin/bwrap与--set-default KURUWA_BASH_BIN=${bash}/bin/bash:声明式 sandbox 定义用同一个 解析规则固定 bwrap 与 Bash,显式环境变量仍然优先,无效路径直接失败。--prefix PATH加入包内bash、coreutils、bubblewrap和补丁版rmux, 因此已有配置里的裸bash、bwrap等 init 程序名可以直接解析。- 不设置其他命令环境、网络或 namespace 变量;实例里运行的命令环境仍由定义和 宿主决定。
rmux 与 rmux-daemon 安装在同一 bin 目录,init-bwrap.sh 会把两者一起暂存
进实例私有目录。
4. 补丁版 RMUX
Kuruwa 的实例启动契约依赖 listener FD 导入,而上游 0.8.0 没有这个入口。nixpkgs
的 rmux 是 0.10.0,且其 long-socket 改动与本项目补丁不兼容,所以包内不引用
pkgs.rmux,而是固定构建:
- 来源:
Helvesec/rmux,commitc4ced0ae6c49781bc7095933e593e8084a62c918(0.8.0),fetchFromGitHub。 - 源归档固定输出 hash:
sha256-73pSH4wowEWYyKQf1htbB0RnCw3qHe0rENr66eyFnM4=。这是 Nix 对固定源 归档的完整性校验值,不是项目自定义摘要。 - 补丁:
patches/rmux-listener-fd.patch(8 个文件)与patches/rmux-long-socket-path.patch(长sun_path通过/proc/self/fd间接寻址,真实 socket 路径不变)。两者不重叠,双向顺序都能应用。 - 构建:
rustPlatform.buildRustPackage,cargoLock.lockFile指向上游Cargo.lock,不执行scripts/build-rmux.sh,也不在构建中联网运行测试脚本。 - 产品依赖的 crates.io
rmux-ipc/rmux-sdk也必须带同一 long-socket 补丁, 否则产品自己连不上深路径实例。nix/package.nix用importCargoLock得到baseCargoDeps,在cargo-vendor-dir里复制可写 vendor tree、从rmuxSrc取crates/rmux-server与根src/(让整份补丁可应用),应用补丁后 写回两个 crate;crate 的.cargo-checksum.json原样保留,不手改Cargo.lock。产品构建因此使用cargoDeps(离线可用),不再用cargoLock。 - 测试:构建阶段显式选择
cargo test --locked -p rmux-ipc --lib --test long_socket_path,覆盖补丁新增的 inherited-listener 单元测试与长路径集成测试。这里选择的是 IPC 库范围,不是关闭检查;上游完整 workspace 套件属于开发验收范围,本次没有把它 当作 Nix 构建检查,也没有据此声称整套测试与 sandbox 不兼容。包外真实行为由 第 6 节烟测和scripts/test-long-socket.py验证。
5. 安装配置与示例
share/kuruwa/kuruwa.toml 是包生成的配置,语义与 examples/kuruwa.toml 一致:
host:host 初始化;bwrap:bubblewrap 初始化;- 租户
alice获准bwrap; - 租户
bob没有获准定义。
区别只在于所有 init 程序和脚本都是绝对 store 路径。配置可以复制到任意目录、 任意项目根目录后直接使用,不依赖当前工作目录或仓库 checkout:
pkg="$(nix build .#kuruwa --no-link --print-out-paths)"
mkdir -p ./kuruwa-run
cp "$pkg/share/kuruwa/kuruwa.toml" ./kuruwa.toml
"$pkg/bin/kuruwa" serve --config ./kuruwa.toml --runtime ./kuruwa-run
包内二进制同时嵌入原 examples/init-host.sh 与 examples/init-bwrap.sh 的
字节内容。首次在项目目录执行不带 --config 的 "$pkg/bin/kuruwa" serve 时,
它会在 $PWD/.kuruwa 生成默认 config.toml、init/ 脚本和后台日志;
wrapper 提供 bash、bwrap、coreutils 与补丁版 rmux,因此这些相对 init
路径可直接工作。已有配置/脚本不会被覆盖。该默认生成行为与 share/kuruwa
下两个安装配置是两条兼容路径:默认生成适合新项目,显式安装配置适合固定部署。
share/kuruwa/examples/kuruwa.toml 保留原示例的相对路径说明;它不是自动使用的
安装配置。原 examples/init-*.sh 没有被修改,包只是逐字节复制它们;安装阶段
关闭了 shebang 重写,避免 Nix fixup 改写原 #!/bin/bash。
bwrap 内的命令路径
init-bwrap.sh 进入 namespace 后会 --clearenv 并固定
PATH=/opt:/run/current-system/sw/bin:/usr/bin:/bin。包 wrapper 在外层加的
PATH 不会自动带进这个沙盒。因此:
- 宿主编排程序仍可使用包 wrapper 提供的裸
bash、bwrap、coreutils; - bwrap 实例内要运行的命令必须由定义或宿主明确提供,或使用 store 绝对 argv;
- 纯 Nix 场景下不要假设 bwrap 内能按名字找到
bash、printf、sh。
例如用 store 绝对路径启动一个 bwrap 终端命令:
BASH=/nix/store/...-bash-interactive-5.3p15/bin/bash
COREUTILS=/nix/store/...-coreutils-9.11/bin
kuruwa --socket "$RUNTIME/tenants/alice.sock" run \
--instance alice-lab --name probe --interval 10 -- \
"$BASH" -c "$COREUTILS/printf 'INSIDE=%s\\n' \"\${KURUWA_BWRAP:-none}\"; exit 17"
这里没有把 store 路径写进原 examples/init-bwrap.sh,也没有扩大 sandbox 策略;
它只是调用者对自己要执行的命令负责。
6. 包外真实烟测
脚本 scripts/accept-package.py 只消费安装包,不读取 target/debug/kuruwa、
target/rmux 或仓库构建产物:
pkg="$(nix build path:$PWD#kuruwa --no-link --print-out-paths)"
python3 scripts/accept-package.py "$pkg"
脚本在 /tmp/opencode 下创建独立临时目录(可用 KURUWA_SMOKE_BASE 覆盖),从
安装包的 share/kuruwa/kuruwa.toml 复制配置,以显式 --foreground 启动测试
daemon(后台默认行为由 scripts/test-root.py 覆盖);serve 和所有客户端构造
等价于 env -i 的环境:只有 HOME、TMPDIR、PATH=/usr/bin:/bin、
LANG=C.UTF-8,不带 KURUWA_RMUX_BIN。它断言:
- wrapper 在子进程中实际导出了补丁版
rmux0.8.0 的绝对路径; - host 实例创建、
run字面输出和真实退出码保留; - bwrap 实例创建,使用 store bash 与 store coreutils 绝对 argv,得到
KURUWA_BWRAP=1、PWD=/mnt等真实输出,并确认管理 socket 被 namespace 隐藏; - 显式设置无效
KURUWA_RMUX_BIN时 serve 明确失败,日志包含该路径,且不创建 management socket,证明 wrapper 没有吞掉 override 或回退; shutdown后 serve exit 0,admin/tenant socket 和实例目录被清理。
失败时保留临时目录并打印位置;成功默认删除自己的临时 runtime 和目录。不会 停止或复用任何其他 Kuruwa/RMUX 服务。
深路径(runtime 内任意 socket 路径超过 108 字节 sun_path)由
scripts/test-long-socket.py 覆盖;它对安装包运行 serve、租户入口、实例创建、
start/status/tail 和 shutdown,并断言 socket 仍位于真实深路径:
pkg="$(nix build .#kuruwa --no-link --print-out-paths)"
KURUWA_BIN="$pkg/bin/kuruwa" python3 scripts/test-long-socket.py
该脚本默认使用 checkout 的 target/debug/kuruwa 时也能验证管理与租户入口,但
实例后端需要自带 long-socket 补丁的 rmux-sdk(发布包满足),普通
cargo build 链接的是 crates.io 未补丁版本,脚本会明确报告这一点。
7. 限制
- 仅提供
x86_64-linux,与其他已验证平台范围一致;没有 macOS/Windows 输出。 - 构建需要 Nix flakes 和网络(固定输入和 crates.io 依赖);首次构建会编译 Rust 产品与 RMUX。
- 构建阶段只运行不需要 namespace 的单元测试。真实 host/bwrap 行为、bwrap namespace 权限和监听 FD 交接由包外烟测覆盖。
path:$PWD在本地包含活动 Unix socket 时会被 Nix 路径拷贝拒绝。本次仓库中的.harness/opencode/runtime/rmux.sock就触发了error: file ... has an unsupported type;这是 Nix 源路径机制的限制,不是包 构建失败。验收因此从排除.harness/、target/、.devenv/等工作树副本的path:源构建;不要把正在运行的服务 socket 移走。- 包没有新增 LICENSE;许可证仍待项目所有者决定。包内只复制仓库现有品牌资产和 示例,不改变产品源码、环境 pin、RMUX 补丁或构建脚本。
- bwrap 示例是 mount namespace 弱隔离,不是安全边界;同 UID 进程仍可能通过
/proc、ptrace 或开放 FD 观察。详见 部署与 M5 演练。
8. 验收记录
本节记录本次真实执行结果。构建源是从工作树排除 .git/、.harness/、target/、
.devenv/、.direnv/ 后得到的路径副本;flake 文件尚未被 Git 跟踪,没有执行
git add 或任何提交。
8.1 flake 与构建
nix flake show path:/tmp/opencode/kuruwa-nix-src # exit 0
nix flake check path:/tmp/opencode/kuruwa-nix-src # exit 0
nix build path:/tmp/opencode/kuruwa-nix-src#kuruwa \
--no-link --print-out-paths # exit 0
关键输出:
packages.x86_64-linux.default: package 'kuruwa-0.1.0'
packages.x86_64-linux.kuruwa: package 'kuruwa-0.1.0'
all checks passed!
/nix/store/4rik66n7syyjhrsd3f4j9bsqcv0hk67x-kuruwa-0.1.0
NIX_BUILD_EXIT=0
default、kuruwa 和显式 #default 三次构建得到同一输出路径。安装包内的
Rust 产品二进制为:
/nix/store/ak94r4w5y4hqif7q2vyzy1svrxiikgkw-kuruwa-0.1.0/bin/kuruwa
补丁版 RMUX 为:
/nix/store/ybrqvq724hs9iaxxcwlhckhpdil0smai-rmux-0.8.0
bin/rmux rmux 0.8.0
bin/rmux-daemon
rmux 与 rmux-daemon 同目录,源码 hash、补丁和 cargoLock 在构建中被 Nix
校验。产品闭包中没有 /mnt/...、target/debug 或 target/rmux 引用。
8.2 Nix 测试
RMUX 的 checkPhase 实际命令与结果:
cargoCheckHook flags: -j 32 --profile release --target x86_64-unknown-linux-gnu \
--offline --locked -p rmux-ipc --lib --
test listener::linux_tests::imported_listener_rejects_and_closes_a_connected_socket ... ok
test listener::linux_tests::imported_listener_accepts_a_client_and_sets_close_on_exec ... ok
test result: ok. 16 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
这确认 cargoTestFlags 的字符串值被 buildRustPackage 接受并作为 cargo 参数执行,
不是只写在 Nix 表达式里。Kuruwa 产品包的 checkPhase:
test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
8.3 nix run
nix run path:/tmp/opencode/kuruwa-nix-src -- --help
# exit 0
Project-level execution environments and terminal access
Usage: kuruwa [OPTIONS] <COMMAND>
Commands:
serve Start the foreground management daemon
shutdown Stop the management daemon and clean up managed instances
instance Manage sandbox instances
8.4 包外烟测
PKG=/nix/store/4rik66n7syyjhrsd3f4j9bsqcv0hk67x-kuruwa-0.1.0
python3 scripts/accept-package.py "$PKG" # exit 0
关键输出:
ASSERT_OK installed configuration points at the packaged host init
ASSERT_OK installed configuration points at the packaged bwrap init
ASSERT_OK wrapper supplies the pinned RMUX 0.8.0 backend (/nix/store/...-rmux-0.8.0/bin/rmux)
ASSERT_OK host instance is created from the packaged definition
ASSERT_OK host run waits for the real exit
ASSERT_OK host terminal is exited
ASSERT_OK host run reports literal exit code 23
ASSERT_OK host transcript contains the literal output line
ASSERT_OK bwrap instance is created from the packaged definition
ASSERT_OK bwrap run reports literal exit code 29
ASSERT_OK bwrap terminal reports KURUWA_BWRAP=1
ASSERT_OK bwrap terminal starts in the private work directory
ASSERT_OK bwrap namespace hides the runtime control socket
ASSERT_OK invalid explicit KURUWA_RMUX_BIN fails startup
ASSERT_OK invalid override is reported by path instead of falling back
ASSERT_OK invalid override creates no management socket
ASSERT_OK shutdown returns stopped=true
ASSERT_OK serve exits 0 after normal shutdown
ASSERT_OK shutdown removes the admin socket
ASSERT_OK shutdown removes tenant entry sockets
ASSERT_OK shutdown removes managed instance directories
SMOKE_PASS assertions=27
烟测的 serve、客户端和 bwrap 实例都在 env -i 等价环境中启动,只有 HOME、
TMPDIR、PATH=/usr/bin:/bin、LANG;没有 KURUWA_RMUX_BIN。bwrap 终端 argv
使用 store bash 绝对路径,输出使用 store coreutils printf。显式 override 使用
不存在的路径,serve 以非 0 退出并在日志中报告该路径,没有创建 management
socket。烟测临时目录和实例进程已清理,未触碰其他服务。
8.5 未解决项与边界
- 直接对包含活动 harness socket 的仓库根执行
nix flake show path:$PWD会失败:error: file '/.../.harness/opencode/runtime/rmux.sock' has an unsupported type。 本次验收使用排除运行时目录的路径副本完成;包本身没有构建失败。 - 未把上游完整 RMUX workspace 测试纳入 Nix 构建检查;本次只声明并验证 IPC 库 范围。没有据此声称全套测试与 sandbox 不兼容。
- 未新增 LICENSE,未修改产品源码、原 examples、环境 pin、RMUX 补丁、Cargo 或 现有构建脚本;未提交 Git。