diff --git a/chv-guest/chv-cell.py b/chv-guest/chv-cell.py index d863a61..6c7be00 100644 --- a/chv-guest/chv-cell.py +++ b/chv-guest/chv-cell.py @@ -33,7 +33,10 @@ 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 正是单元需要的)。 +glibc 默认 files+dns 正是单元需要的)。part 5 补一条: +/app/work 以可写 bind 盖住(backing 在客机 workspace 下), +否则 host.py 的 make_worktree 在只读 /app 下建不了 worktree +(RETEST4 新发现 2,OSError Errno 30)。 环境透传:nspawn 不透传调用方环境,本脚本只转发白名单变量(模型 网关与代理变量)。GITEA_TOKEN 等凭证变量不在白名单里,绝不进单元。 @@ -70,6 +73,17 @@ 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 按顺序 +# 挂载,后挂的才盖住只读子树下的该子路径。 +WORKTREE_DIR = "/home/chv/workspace/.chv-worktrees" CELL_PID_FILE = "/run/chv/cell.pid" # 单元网络(与客机 tap 网 192.168.10.0/24 不重叠的独立 /24)。 @@ -210,14 +224,17 @@ 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): + cell_net=None, extra_ro_binds=None, + worktrees_dir=WORKTREE_DIR): """产 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 之后才能盖住只读子树)。""" if not command: raise ValueError("chv-cell: 单元内命令不能为空") if network not in ("veth", "shared"): @@ -269,6 +286,14 @@ 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, "--bind-ro=/etc/ssl/certs", "--tmpfs=/tmp", ] @@ -658,6 +683,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)。 + _ensure_bind_source(WORKTREE_DIR) net = None files = None uplink = None diff --git a/cloud/tests/test_chv_muse_p2.py b/cloud/tests/test_chv_muse_p2.py index c8f7dac..c2daa17 100644 --- a/cloud/tests/test_chv_muse_p2.py +++ b/cloud/tests/test_chv_muse_p2.py @@ -854,6 +854,39 @@ class CellPart4ArgvTest(unittest.TestCase): self.assertEqual(net.subnet, "10.200.0.0/24") +class CellPart5WorktreeTest(unittest.TestCase): + """part 5 修复 2(RETEST4 新发现 2):/app/work 以可写 bind + 盖住只读 /app——默认 dispatch 的 make_worktree 才写得进。""" + + def test_worktree_bind_is_writable_under_workspace(self): + argv = CELL.build_nspawn_argv(["true"]) + self.assertIn( + "--bind=/home/chv/workspace/.chv-worktrees:/app/work", + argv) + # 必须是可写 bind,不是只读绑。 + self.assertNotIn( + "--bind-ro=/home/chv/workspace/.chv-worktrees:/app/work", + argv) + + def test_worktree_bind_stacks_after_readonly_app(self): + # nspawn 按顺序挂载:work bind 必须在 --bind-ro=/app 之后, + # 才能盖住只读子树下的 /app/work。 + argv = CELL.build_nspawn_argv(["true"]) + self.assertLess( + argv.index("--bind-ro=/app"), + argv.index( + "--bind=/home/chv/workspace/.chv-worktrees:/app/work")) + + def test_worktree_bind_custom_dir(self): + argv = CELL.build_nspawn_argv(["true"], + worktrees_dir="/tmp/wt") + self.assertIn("--bind=/tmp/wt:/app/work", argv) + + def test_worktree_dir_constant_under_workspace(self): + self.assertTrue( + CELL.WORKTREE_DIR.startswith(CELL.WORKSPACE + "/")) + + class CellFilesTest(unittest.TestCase): """part 4:单元专属 resolv.conf/hosts 生成。"""