part 6 修复:worktree 根移出 /app——RETEST5 挂载点回归

RETEST5:part 5 的 --bind=<workspace>/.chv-worktrees:/app/work
(adc7bf6,排在 --bind-ro=/app 之后)让所有 cell 启动失败:
后挂的 bind 只有在挂载点已存在时才能盖住,挂载点不存在时
nspawn 在只读 /app 下连建都建不起来(Failed to create mount
point .../app/work: Read-only file system,全量 73 次,
CELL-RC=0 共 0 次),缺陷 1 的 15/15 回退为 0/15。

三选一取 (a) worktree 根移出 /app:
- chv-cell 不再发任何指向 /app 之下的 bind,删
  --bind=...:/app/work 与 worktrees_dir 参数;WORKTREE_DIR
  (/home/chv/workspace/.chv-worktrees)改经
  --setenv=CHV_WORKTREE_ROOT 传进单元——该路径在已可写绑进
  单元的 workspace 之下,挂载点问题按构造即不存在;backing
  仍在客机 workspace 下随磁盘持久化,隔离与落盘语义不变,
  只是单元内路径从 /app/work/host-worktrees 改为
  /home/chv/workspace/.chv-worktrees/host-worktrees。
- host.py 新增 worktree_base():CHV_WORKTREE_ROOT 优先;
  CHV_CELL=1 但变量未设时回落到同一单元内路径(旧版 chv-cell
  / 单元内手动跑 host.py 仍不出 /app);单元外沿用
  HERE/work/host-worktrees,行为不变。make_worktree 与
  dispatch_from_branch(第二个 HERE/work 调用方)同走该函数,
  goal/fork/依赖链一并修到。
- 不选 (b) 默认 --no-worktree:隔离丢了,且 goal/fork 同样
  调 make_worktree,照样撞 Errno 30;不选 (c) 镜像预建空
  目录 /app/work:修法耦合镜像构建,某次重建漏建即静默回归,
  且单测无法钉住镜像内容。
- 根因 1(39084ce)与根因 3(0203f32)代码未动。

单测:删 part 5 钉 argv 顺序的 4 项(该顺序本身已被真机证伪),
新增 CellPart6WorktreeTest 4 项 + HostWorktreeBaseTest 5 项:
argv 里无任何指向 /app 之下的 bind、CHV_WORKTREE_ROOT 经
setenv 进单元、worktree 根只经可写 workspace 绑定可达、
两处同值常量一致、worktree_base 三路选择、make_worktree
真 git worktree 落点、dispatch_from_branch 同根。

全量:cloud/ 下 discover -s tests,2067 项 OK(skipped=9)。
(注:仓库根跑 discover 会把 queue 解析成标准库、致
test_queue_sandbox 导入失败,须在 cloud/ 下跑。)
This commit is contained in:
host committed 2026-11-07 14:23:48 +00:00
1 parent 0203f32627
commit 50cae2a0af
3 files changed
+249 -55

No files matched your search

+43 -30
View File
@@ -33,10 +33,14 @@ volatile 空根(part 4):--volatile=yes 下单元根是基本空的
tmpfs,/app、/etc/resolv.conf、/etc/hosts、/etc/ssl/certs 都须
显式绑进去(/app 与 CA 只读绑;resolv/hosts 是 run_cell 按客机
有效 DNS 生成的单元专属文件,只读绑;不绑 nsswitch.conf,
glibc 默认 files+dns 正是单元需要的)。part 5 补一条:
/app/work 以可写 bind 盖住(backing 在客机 workspace 下),
否则 host.py 的 make_worktree 在只读 /app 下建不了 worktree
(RETEST4 新发现 2,OSError Errno 30)。
glibc 默认 files+dns 正是单元需要的)。part 6(RETEST5)纠正
part 5 的设想:/app/work 以可写 bind 盖住只读 /app 的做法在真机
上不成立——后挂的 bind 只有在挂载点已存在时才能盖住,挂载点
不存在时 nspawn 在只读 /app 下连建都建不起来(全量 73 次同一
错误,所有 cell 启动失败)。worktree 根改移出 /app:单元内
make_worktree 改用 workspace 绑定下已可写的
/home/chv/workspace/.chv-worktrees(经 CHV_WORKTREE_ROOT 环境
变量传进单元),不再发任何指向 /app 之下的 bind。
环境透传:nspawn 不透传调用方环境,本脚本只转发白名单变量(模型
网关与代理变量)。GITEA_TOKEN 等凭证变量不在白名单里,绝不进单元。
@@ -73,16 +77,21 @@ CELL_GID = 1000
WORKSPACE = "/home/chv/workspace"
CODEX_HOME = "/home/chv/.codex"
BROKER_DIR = "/run/chv"
# part 5:单元内 worktree 的可写落点(RETEST4 新发现 2)。
# host.py 的 make_worktree 把 worktree 建在 HERE/work(单元内
# HERE=/app,而 /app 是 --bind-ro),带 repo 的默认 dispatch
# 撞 OSError Errno 30。把客机 workspace 下的 .chv-worktrees
# 以可写 bind 挂到单元内的 /app/work 上:worktree 的语义、隔离
# 与落盘位置都不变(仍是 /app/work/host-worktrees/<任务>),
# 只是 backing store 落在 bind 出去的 workspace 下、随客机磁盘
# 持久化(跨 cell 的 run/merge 续跑仍找得到 worktree);不碰
# /usr 的只读约束。必须排在 --bind-ro=/app 之后:nspawn 按顺序
# 挂载,后挂的才盖住只读子树下的该子路径。
# part 6(RETEST5 新发现):单元内 worktree 根(RETEST4 新发现 2
# 的旧址是 HERE/work,即单元内 /app/work——/app 是 --bind-ro,
# 带 repo 的默认 dispatch 撞 OSError Errno 30;part 5 把该目录以
# 可写 bind 挂到 /app/work 上,真机证明不可行:挂载点不存在时
# nspawn 在只读 /app 下连建都建不起来,所有 cell 启动失败)。
# 新址是 workspace 绑定下的 .chv-worktrees:--bind=/home/chv/
# workspace 已把整个 workspace 以可写方式挂进单元(目的路径与
# 源相同、挂载点即 workspace 自身,挂载时已存在),worktree 根
# 落在这里无需任何额外 bind;backing 仍在客机 workspace 下、
# 随客机磁盘持久化(跨 cell 的 run/merge 续跑仍找得到 worktree),
# 隔离与落盘语义不变,只是单元内路径从 /app/work/host-worktrees
# 改为 /home/chv/workspace/.chv-worktrees/host-worktrees。
# 该值经 --setenv=CHV_WORKTREE_ROOT 传进单元,host.py 的
# worktree_base() 读它(与 host.py 的 CELL_WORKTREE_ROOT 同值,
# 两处须保持一致)。
WORKTREE_DIR = "/home/chv/workspace/.chv-worktrees"
CELL_PID_FILE = "/run/chv/cell.pid"
@@ -224,17 +233,19 @@ def build_nspawn_argv(command, network="veth",
workspace=WORKSPACE, codex_home=CODEX_HOME,
broker_dir=BROKER_DIR,
playwright_package_dir=None,
cell_net=None, extra_ro_binds=None,
worktrees_dir=WORKTREE_DIR):
cell_net=None, extra_ro_binds=None):
"""产 nspawn 参数表(纯函数,可单测)。command 是单元内要跑的命令
(list,如 ["python3", "/app/host.py", ...])。
cell_net 为 None 时用旧常量身份(argv 子命令与旧测试);
run_cell 一律传 allocate_cell_net() 的唯一身份。
extra_ro_binds 是 [(src, dest)],只读绑进单元(如单元专属
resolv.conf/hosts;/app 与 /etc/ssl/certs 是固定只读绑定,
直接列在下面)。worktrees_dir 是客机侧 worktree backing
目录(part 5:以可写 bind 挂到单元内 /app/work 上,见
WORKTREE_DIR;排在 --bind-ro=/app 之后才能盖住只读子树)。"""
直接列在下面)。worktree 根不经 bind:WORKTREE_DIR 在已可写
绑进单元的 workspace 之下(part 6,RETEST5),经
--setenv=CHV_WORKTREE_ROOT 传进单元供 host.py 的
worktree_base() 用;argv 里绝不出现指向 /app 之下(只读父
目录)的 bind——挂载点不存在时 nspawn 在只读父目录下建不
了挂载点,单元直接起不来。"""
if not command:
raise ValueError("chv-cell: 单元内命令不能为空")
if network not in ("veth", "shared"):
@@ -286,14 +297,11 @@ def build_nspawn_argv(command, network="veth",
# 单元内无 resolved/dbus,绑了反而可能挂起;glibc 缺
# nsswitch.conf 时默认 files+dns,正是单元需要的。
"--bind-ro=/app",
# part 5(RETEST4 新发现 2):/app/work 以可写 bind 盖住
# 只读 /app 下的该子路径——host.py 的 make_worktree 在
# /app/work/host-worktrees 下建目录,绑了才写得进;必须在
# --bind-ro=/app 之后(nspawn 按顺序挂载),且必须是
# --bind(可写),不是 --bind-ro。backing 目录在客机
# workspace 下(run_cell 起单元前 _ensure_bind_source
# 建好),worktree 随客机磁盘持久化。
"--bind=%s:/app/work" % worktrees_dir,
# part 6(RETEST5):此处绝不再加 --bind=...:/app/work。
# part 5 的该绑定已证不可行:/app/work 挂载点在 --bind-ro
# 的 /app 镜像里不存在,nspawn 在只读父目录下建不了挂
# 载点(EROFS),单元启动即失败。worktree 根已移出 /app,
# 见 WORKTREE_DIR 与 --setenv=CHV_WORKTREE_ROOT。
"--bind-ro=/etc/ssl/certs",
"--tmpfs=/tmp",
]
@@ -303,6 +311,10 @@ def build_nspawn_argv(command, network="veth",
argv.append("--network-veth")
argv += [
"--setenv=CHV_CELL=1",
# part 6:单元内 worktree 根(host.py worktree_base() 读
# 它;值与 WORKTREE_DIR 一致,客机侧与单元内是同一路径,
# 因 workspace 以同名可写 bind 进单元)。
"--setenv=CHV_WORKTREE_ROOT=%s" % WORKTREE_DIR,
"--setenv=HOME=/home/chv",
]
for name in FORWARD_ENV_ALLOWLIST:
@@ -683,9 +695,10 @@ def run_cell(command, network="veth"):
_ensure_bind_source(WORKSPACE)
_ensure_bind_source(CODEX_HOME)
_ensure_bind_source(BROKER_DIR, uid=0, gid=0)
# part 5:worktree backing 目录须在 Popen 前存在(nspawn
# --bind 的源不存在即起不来),且属 chv(单元内 root 即客机
# chv,属主不对就写不进 worktree)。
# part 6:worktree 根目录须在 Popen 前存在,且属 chv(单元内
# root 即客机 chv,属主不对就写不进 worktree)。它在 workspace
# 绑定之下,进单元后 host.py 的 makedirs 也能建,这里预建只
# 是把属主一次定对;不再是任何 bind 的源(无 /app/work 绑定)。
_ensure_bind_source(WORKTREE_DIR)
net = None
files = None