Managed 工作区
当同一个仓库要维护好几个月 —— 包成批到达、由策略决定谁留下、事后还得说清楚什么时候变了什么 —— 你需要的是 Managed 模式。本页讲三层模型、它产出的布局,以及命令怎么判断你说的是哪个仓库、哪个 Dist。
三个层级
每层只做一件事,边界很硬:
工作区(Workspace) 只拥有两样东西:根级 sow.yml 和 .sow/ 状态目录。工作区根下其他任何东西都不属于 SOW。它是发现的单位 —— 命令从某个起始目录向上找到工作区 —— 也是架构许可表所在的地方。
仓库(Repository) 固定在 <workspace>/<name>。你不能把它指到别处,没有 path 选项。一个 Repository 拥有自己的 pool/、dists/、SQLite、锁、恢复状态、Generation、保留代根、发布 checkpoint 与 GC 证据。两个 Repository 之间永不去重 —— 同一个包 add 进两个仓库就存两份,这是刻意的:这样删掉一个仓库永远不可能伤到另一个。
Dist 是一个单一格式(rpm 或 deb)的普通具名成员集合。名字对 SOW 是不透明字符串。el9、trixie、el9-beta、customer-acme —— 它们都不产生状态机、晋升流程或快照。想要一个 beta 频道,就建一个叫 el9-beta 的 Dist;含义存在于你的脑子和 .repo 文件里,不在 SOW 里。
架构视图(Architecture View) 是 build 渲染出来的东西,不产生第二份成员关系。一个 noarch RPM 只有一个包对象、一条成员记录,构建时投影进每个适用视图。参见包池与元数据视图。
一个 Repository 可以同时拥有 RPM Dist 与 DEB Dist,共用同一个 pool/。
磁盘布局
Managed 路径从不由用户输入拼装,而是由解析后的真实工作区根、已校验的名称和固定相对片段推导出来 —— 这就是为什么符号链接替换和路径逃逸没有可攻击的面。
执行过两次 dist new 与两次 add 之后的真实工作区:
<repo>/ 下是公共交付树:可以直接服务、由 SOW 发布,或整根复制到离线 staging 后原子切换。
.sow/ 下全部是私有状态,绝不能暴露;详见对外服务。
名称必须匹配 [a-z0-9][a-z0-9._-]*;.、..、.sow、pool、dists 以及工作区保留名一律拒绝。
状态数据库与软件包事实
私有 SQLite 数据库按 package_sha256 索引 Desired/Built Membership,因此查询与构建可以
一次批量展开完整成员投影,而不是为每个软件包单独查询。数据库还保存以不可变包体 SHA-256
为键、可重建的软件包事实缓存。Ingest 从已认证快照解析一次软件包事实;快照安装到 pending、
再提升到 Pool 时,为保证恢复正确性,各阶段仍独立验证内容,整个 add 并不是只读一遍包体。生产构建只按本次
选中的 Digest,以有界、确定性批次读取 Facts Row,遇到缺失或损坏记录时再从已认证包体惰性
重建。无关或超大的 Facts Row 不会被读取。
对于未改变的 Pool 文件,暖构建使用设备号、inode、size、mtime 与 ctime 指纹避免重新读取
包体。指纹漂移与 Facts 缺失共享一次权威 SHA-256 校验并自动修复;
sow check 仍是显式的完整密码学审计,并且无论指纹是否匹配,都会
对每个唯一物理包体哈希一次。
缓存与指纹只属于私有实现状态,不改变公共 pool/ + dists/ 布局。不要手工编辑数据库或
PRAGMA user_version。v0.3 或 v0.4 Repository 必须先备份,再通过
sow repo migrate 显式升级,之后才能执行普通 0.5
读写。Schema v13 为包池路径归属增加索引,使 add 只查询候选路径,不再扫描无关发布历史。
首次构建刷新受影响的 RPM 认证与 APT 元数据契约,之后未变化的包可复用匹配的 Built 认证证据。
其他维护只在 SOW 诊断明确指出时执行。
sow.yml 驱动一切
只有一个配置文件,用严格 decoder 解析。未知字段不会被忽略——它会失败。重复的规范化架构、非法名称或 format、Dist 架构不是工作区许可表的子集、非法 glob 或分类、不完整的 signing 块,同样失败。
config show --all 展开全部默认值与规范化别名,让你看到 SOW 实际的决定:
架构别名只在解析边界规范化一次:amd64 → x86_64,arm64 → aarch64。输出永远是 canonical family。生态名只在渲染出来的 DEB 视图目录名里出现(binary-amd64、binary-arm64)。
config check 不是 YAML lint。它会打开每个已初始化 Repository 的 SQLite,把候选配置与实际的 Dist、架构、成员集、Built 状态和签名可用性逐项比对。移除仍被成员或 Built 状态引用的架构族是预期拒绝(退出码 6);数据库或协议证据损坏是完整性错误(退出码 5)。每个写命令在写 journal 之前都跑同一套预检,所以 config check 能提前告诉你下一条 add 会不会被拒。
完整 schema(包括 filesystem 与 r2 发布目标)见
sow.yml 配置参考。
发现:哪个工作区?
Managed 命令按以下顺序寻找最近祖先中的 sow.yml:
- 给了
-C/--workdir DIR:从DIR向上找,并跳过当前目录候选。 - 否则从当前目录向上找。
- 仍未找到:从
$SOW_DIR向上找;显式-C查找失败后也保留这条回退。 - 还是没有:失败,并提示
sow init、--workdir与SOW_DIR。
找到第一个 sow.yml 就停,不会越过它继续往上找"更好的那个"。
查找沿传入路径的字面祖先进行,找到后再解析并绑定真实工作区根。符号链接起点可以使用,
但其字面祖先未必等于目标的物理祖先;别名引起选择歧义时,直接指定物理根。
sow config check 报告解析后的工作区,而不只是发现起点。
--workdir 不是 chdir。它只改变发现的起点。sow add 里的相对 PATH 仍然相对你真实的当前目录解析 —— 这正是 sow add ./build/*.rpm -C /srv/ws 应有的行为。
sow create 不参与上述任何一步。
选择:哪个仓库、哪个 Dist?
Repository 选择,按序:
- 显式
-r/--repo NAME。 - 命令起始目录位于
<workspace>/<repo>/内。 - 工作区只有一个 Repository。
- 否则失败并列出候选。
Dist 选择,按序:
- 一个或多个显式
-d/--dist NAME(可重复)。 - 起始目录位于
<workspace>/<repo>/dists/<dist>/内。 - 选定 Repository 只有一个 Dist。
- 否则失败并列出候选。
关键的不对称在这里:没给 -d 时,build、check、status 默认作用于选定 Repository 的 全部 Dist —— 对这几个命令而言,“没有过滤条件"解释成"全都要"是安全的。而 add、rm、ls 必须得到明确的 Dist 集合,因为猜一个包该落到哪里并不安全:
退出码 2。所有推断都在路径类型与符号链接校验之后才进行。
init 只做收敛,不做重置
sow init 的幂等性是设计出来的,它的规则是架构不变式而不是使用便利:
- 没有
sow.yml: 创建一个,写入schema: sow/v3与architectures: [x86_64, aarch64],同时创建.sow/。不自动创建任何 Repository。 - 已有合法配置: 按稳定名称顺序补齐尚未初始化的部分 —— 缺失的 Repository 外壳、缺失的 SQLite、整个缺失的 Dist。新建的 Dist 立刻生成其当前有效架构的全部空视图。
- 已有有效数据库状态或有效协议指针: 只校验。绝不覆盖、绝不清零 Generation、绝不重写字节。
- Dist 已初始化之后又往配置里加了架构:
init不渲染新视图、不推进 Generation。该 Dist 保持 dirty,等待显式build。移除仍被成员或 Built 状态使用的架构族则失败。
第三、第四条规则的意义在于:init 必须能安全地在装着真实内容的仓库上执行。它朝声明的配置收敛,但绝不会拿"还没初始化"当借口去重建一个本来就好好的东西。
对象按稳定顺序处理。如果靠前的配置、Repository 或 Dist 已经耐久提交,而靠后的对象失败了,已提交的计数会被保留:人类输出先报告已提交的结果,--json 保留结构化 result,命令以 3(部分成功)退出。如果此时还没有任何东西提交过,则按原始错误类别退出。
空 Dist 也有合法协议发布面
dist new 建出来的 Dist,在 add 第一个包之前就已具备完整协议入口。RPM Dist 每个架构族
有一份合法的空 repodata;DEB Dist 有 Packages、Packages.gz、by-hash 条目和
Release。如果 Repository 配了元数据密钥,空 Dist 也一样签名。
因此从 Dist 里移除最后一个包之后,留下的是一份合法的空索引(配置签名时仍签名), 而不是缺失或损坏的协议入口。真实包管理器验收仍是另一层兼容性门禁。
Protected 仓库
protected: true 会拒绝 repo rm,即使加了 -f,返回退出码 6。它不限制别的:add、rm、build 和常规 Dist 维护照常。真要删这个 Repository,你必须先改 sow.yml、通过 config check,然后才能删 —— 这个摩擦正是该 flag 存在的意义。
继续阅读
- 包池与元数据视图 ——
build到底写了什么 - 发布模型 —— Generation 如何抵达目标
- 成员策略 ——
exclude与limit如何决定谁留下 - 第一个工作区 —— 本页的十分钟动手版
sow.yml配置参考 —— 完整 schema