托管 RPM 仓库:分架构视图、noarch 中性投影、debuginfo 过滤、版本数量上限,以及可用的 dnf 客户端配置。
这是本节的多页打印视图。 .
SOW 文档
- 1: 上手
-
2: 教程
- 2.1: 搭建 YUM 仓库
- 2.2: 搭建 APT 仓库
- 2.3: 仓库签名
- 2.4: 服务与发布仓库
- 2.5: 演练构建 pigsty-infra 仓库
-
3: 功能
- 3.1: Plain 平面仓库
- 3.2: Managed 工作区
- 3.3: 包池与元数据视图
- 3.4: 成员策略
- 3.5: 签名模型
- 3.6: 事务与恢复
- 3.7: 可观测与审计
- 4: 参考
-
5: 命令
- 5.1: sow create
- 5.2: sow init
- 5.3: sow config
- 5.4: sow repo
- 5.5: sow dist
- 5.6: sow add
- 5.7: sow rm
- 5.8: sow ls
- 5.9: sow show
- 5.10: sow where
- 5.11: sow status
- 5.12: sow build
- 5.13: sow check
- 5.14: sow changes
- 5.15: sow publish
- 5.16: sow retain
- 5.17: sow gc
- 5.18: sow export
- 5.19: sow log
SOW 是 Pigsty 出品的自包含软件仓库管理器。sow create 能把目录中的 RPM 与 DEB
文件直接变成可用平面仓库;Managed 工作区则进一步提供成员关系、筛选策略、签名、不可变
Generation、审计历史与发布目标。
按 Ctrl 加 K(macOS 上也可用 ⌘ 加 K)搜索本站; 焦点不在输入框时按 /,可直接打开命令模式。
- 上手 — 安装 SOW、创建平面仓库,并构建第一个 Managed 工作区。
- 教程 — 完整的 YUM、APT、签名、对外服务与发布实战。
- 功能 — Plain/Managed 运行路径、包池投影、策略、签名、事务与审计。
- 设计归档 — 按日期记录所有权、布局、发布、恢复与兼容性决策。
- 命令 — 每条命令的语法、选择规则、输出、状态变化与退出行为。
- 参考 — 配置、包引用、目录布局、JSON、退出码、平台与集成覆盖。
选择路径
1 - 上手
SOW 生成 RPM/YUM 与 DEB/APT 静态仓库,本身不是 HTTP 守护进程。先选择一条相互隔离的 运行路径:
-
Plain:
sow create在普通目录中为现有软件包重建索引。 -
Managed: 工作区持续记录成员关系、Dist、架构视图、策略、签名、Generation、 审计历史与发布目标。
-
安装 — 选择 Release 归档、RPM/DEB 安装包或源码构建,并校验二进制。
-
快速上手 — 从一个软件包目录创建平面仓库,并通过 HTTP 提供服务。
-
第一个工作区 — 初始化 Managed 模式,创建 RPM/DEB Dist,添加软件包,然后构建并校验。
-
核心概念 — Workspace、Repository、Dist、Package Object、Desired Membership 与 Built Generation。
Managed 工作区需要具备建议锁、fsync 与原子 rename 语义的本地 POSIX 文件系统。元数据
在进程内生成;可选的 RPM 包签名需要 rpm,agent:// 元数据密钥需要 gpg 与
gpg-agent。
1.1 - 安装
SOW 只有一个可执行文件,不需要启用服务,也不依赖语言运行时。Release 构建目标是 Linux 与
macOS 的 amd64、arm64;Linux 另外提供 RPM 与 DEB 安装包。不支持 Windows。
在下载页选择匹配操作系统与架构的归档或 Linux 安装包。页面同时提供每个
已发布制品、对应源码 Tag 与 SHA256SUMS 的链接。
安装归档
下载一个归档与 SHA256SUMS,解压前只校验对应条目:
macOS 选择 darwin_amd64 或 darwin_arm64,并把 sha256sum -c - 换成
shasum -a 256 -c -。没有 root 时,把二进制装到已经加入 PATH 的目录,例如
~/.local/bin。
安装 Linux 软件包
Linux 软件包使用 1PGSTY Release 后缀:
只执行符合本机发行版与架构的那条命令。RPM 把 License 安装到
/usr/share/licenses/sow/LICENSE;DEB 把版权/协议文件安装到 /usr/share/doc/sow/。
从源码构建
Go Module 声明使用 Go 1.27.0,元数据生成不需要 C 工具链。请把 vX.Y.Z 替换为下载页
链接的源码 Tag:
这组命令使用 Release 构建参数,并把所选 Tag 的产品版本写入二进制。
校验
sow version 输出产品版本、目标 OS/架构与构建 Go 工具链;sow help 列出命令树。
归档中还包含 README.md、CHANGELOG.md 与 Apache-2.0 LICENSE。
升级 0.3 Managed Workspace
SOW 0.4 引入内部数据库 Schema v11 与 v12。公共布局和 schema: sow/v3 配置标识均不改变,
但每个既有 v0.3 Repository 都必须在普通读写前显式迁移。备份前先停止 Workspace 全部写入:
对 sow.yml 中的每个 Repository 重复最后两条命令。数据库 Transition 是单向的;迁移完成后
不要再用 SOW 0.3 打开 Workspace。状态、Signer 与 Publication Evidence 的修复范围见
sow repo migrate。
权限与可选工具
执行用户需要读取输入软件包,并能写入 Plain 目标目录或 Managed 工作区。Managed 工作区 应放在本地 POSIX 文件系统上;锁、fsync、安全路径与原子 rename 都属于正确性契约。
软件包解析与元数据渲染都在进程内完成。只有两条可选路径需要主机工具:
- RPM 包签名 需要
rpm与可用的 GPG 环境; agent://元数据密钥需要gpg与gpg-agent。
1.2 - 快速上手
Plain 模式在一个目录内生成平面仓库。它不读取 sow.yml,不创建工作区,也不维护数据库。
准备目录
把 RPM 和/或 DEB 文件放在目录顶层。sow create 不递归扫描,也不移动或改名包文件。
如果某种格式不存在,请只复制你实际拥有的软件包。
生成元数据
混合格式输出形态如下:
目录会变成:
Plain 模式不生成 DEB Release、InRelease 或 Release.gpg。RPM 与 DEB 元数据在一次
操作中生成;任何解析或渲染错误都会阻止新索引提交。
对外服务
本地检查可以使用任意静态文件服务器:
在另一个终端检查两个协议入口:
Python 服务器只适合预览;长期服务请使用正常维护的 HTTP 服务器。
配置客户端
把 REPO_HOST 换成客户端能访问的地址。
刷新索引并安装软件包:
APT source 末尾的 ./ 表示平面仓库。[trusted=yes] 与关闭 DNF 签名检查只适用于这个
未签名的快速示例;需要真实性保证时应使用已签名 Managed 仓库。
更新仓库
增删包文件后重新执行同一条命令:
目录内容就是 Plain 模式的全部状态。包字节不变时,生成的元数据具有确定性,重复运行会报告
noop=true。
自动化场景可使用带版本的 JSON 信封:
何时使用 Managed 模式
如果目录已经恰好包含要发布的全部内容,使用 Plain。需要具名 Dist、架构视图、成员策略、 已签名元数据、Generation、审计或发布目标时,使用 Managed 工作区。
另见 sow create 与
Plain 平面仓库。
1.3 - 第一个工作区
Managed 模式会持久保存配置、成员关系、Generation 与审计状态。下面从空目录开始。
初始化工作区
init 创建:
不要编辑或对外服务 .sow/。init 是幂等操作:重复执行会校验并收敛已声明的 Repository
与 Dist,不会重置有效工作区。
默认架构族是 x86_64 与 aarch64。配置接受 amd64、arm64 别名,并规范化为上述族名。
创建 Repository 与两个 Dist
一个 Repository 拥有一棵公共 pool/ + dists/ 树和一份私有状态数据库。每个 Dist 只有
一种格式。dist new 会立即生成合法空视图,客户端读取空 Dist 时得到空索引而不是 404。
此时公共布局为:
添加软件包
显式选择目标 Dist:
SOW 从包本身读取身份与架构,把接受的字节存入 local/pool/,更新 Desired Membership,
并在返回前构建受影响的 Dist。输入路径只用于导入;后续构建使用 Managed 包池。
需要合并多次成员变更时,用 --skip 暂不构建,最后统一收敛:
Desired Membership 领先于 Built Generation 时,Repository 状态为 dirty,且
ready_to_copy=false。
查看与校验
status 是低成本状态读取。check 是交付门禁:它校验配置、状态、公共文件权限、保留根、
包字节、Desired Membership、索引、签名与 Generation manifest,且不写入任何内容。
只有 clean 且所有层都通过的 Repository 才返回成功。
查看规范化配置与默认值:
对外服务 Repository
公共交付单元是 /srv/sow/local,不是工作区根。把这个目录挂到稳定 URL 前缀;不要暴露
sow.yml 或 .sow/。
- DNF base URL:
https://repo.example.com/local/dists/el9/x86_64/ - APT source:
deb https://repo.example.com/local bookworm main
安全的 Nginx 与 filesystem 发布流程见对外服务。
选择规则
- Workspace:从当前目录向上查找,或从
-C DIR开始查找。 - Repository:
-r NAME、当前路径所属 Repository,或唯一已配置 Repository。 - Dist:
-d NAME,可重复;只有命令能够唯一确定范围时才可省略。
存在歧义时直接报错;SOW 不会随便挑选 Repository 或 Dist。
下一步
1.4 - 核心概念
Plain 还是 Managed
两条运行路径相互独立。
| Plain | Managed | |
|---|---|---|
| 入口 | sow create DIR |
init、repo、dist、add、rm、build |
| 状态 | 软件包目录 | sow.yml 加私有 SQLite/操作日志 |
| 公共布局 | 平面 RPM/DEB 索引 | Repository pool/ + dists/ |
| 格式 | RPM 与 DEB 可共存于一个目录 | 每个 Dist 一种格式 |
| 架构视图 | 无 | 有 |
| 策略与审计 | 无 | 有 |
| 元数据签名与发布目标 | 无 | 有 |
目录内容已经等于目标仓库时使用 Plain。需要由 SOW 管理成员、策略、Generation、签名、 审计或发布时使用 Managed。
Managed 层级
- Workspace 是配置与发现边界。
- Repository 是隔离、Generation、发布和公共树边界。不同 Repository 之间不去重包体。
- Dist 是单一包格式的具名成员集合。
- 架构视图 是派生输出,不是第二套成员关系。
noarchRPM 与allDEB 会进入所有适用 视图,但包池字节不重复。
一条规范包体路径
Package Object 以确切字节的 SHA-256 标识;逻辑坐标来自 RPM header 或 DEB control, 不来自文件名。
每个已接受包体在 Repository pool/ 下只有一条规范路径。RPM 架构视图只含 repodata/,
包位置通过父级相对路径回到包池;APT Packages 直接指向同一包池。
普通包管理器与镜像工具是不同契约。默认 dnf reposync 会拒绝规范 RPM 视图中的父级跳转。
需要自包含 RPM 镜像 leaf 时,使用 sow export rpm-leaf 生成独立产物。
Desired 与 Built
Managed 模式分别追踪意图与公共字节:
add 与 rm 默认构建受影响的 Dist。--skip 只记录成员变更,Repository 会保持 dirty;
随后用 sow build 把 Desired 收敛为新的 Built Generation。
sow status低成本读取状态,并报告ready_to_copy。sow check执行完整只读交付证明。dirty 或 recovering 状态不可交付。sow changes [BASE_GENERATION]描述某个已记录 Generation 到当前 Built Generation 的 物理差异。它是证据与计划输出,不能替代发布恢复或远端验证。sow publish TARGET通过配置的 Provider 发布已校验 Generation,并记录 target 级恢复与 checkpoint 状态。
事务与失败状态
写操作由 Workspace 或 Repository 锁串行化,并在公共变更前记录意图。包体与不可变元数据 先准备,可变协议指针最后更新。下一条 writer 会先恢复被中断操作,再开始新工作。
| 状态 | 含义 |
|---|---|
clean |
Desired 与 Built 一致 |
dirty |
Desired 已变化;Built 仍是上一个已提交 Generation |
recovering |
存在必须解决的非终态操作 |
error |
持久证据冲突;SOW 拒绝猜测 |
用 status 诊断,用 check 做发布门禁。ready_to_copy=false 的 Repository 不应发布。
继续阅读
2 - 教程
这里的教程都从全新工作区开始。请按顺序执行命令,并按你的环境替换大写占位符与包路径。
如果还没装 SOW,先看安装与快速上手。 下面的教程介绍 Managed 仓库路径。
托管 DEB 仓库:Debian 风格包池、by-hash 索引与 deb822 客户端配置。
生成专用 GPG 签名钥,为仓库元数据与 RPM 包签名,并配置客户端拒绝一切未签名内容。
用 Nginx 服务 Repository,并把已校验 Generation 发布到配置好的 filesystem target, 同时避免暴露工作区私有状态。
把 infra-pkg 已产出的双架构 RPM 与 DEB 组织成真实仓库,并演练本地安装、滚动更新、 Stable 晋升与月度快照。
先看哪篇
| 你的处境 | 从这里开始 |
|---|---|
| 你要向 dnf 客户端分发 RPM | 搭建 YUM 仓库 |
| 你要为 Debian 或 Ubuntu 分发 DEB | 搭建 APT 仓库 |
| 需要已签名元数据或已签名 RPM 包体 | 仓库签名 |
| 树已经建好,但外部无法访问 | 对外服务 |
| 想把现有双架构包池变成可维护的 Infra 仓库 | 演练构建 pigsty-infra 仓库 |
YUM 与 APT 两篇是彼此独立的全新 Workspace 路径。实际使用中,如果它们适合共用同一所有权 边界,一个 Workspace 的同一 Repository 可以同时容纳 RPM 与 DEB Dist。
本板块约定
Shell 代码块里的命令不带 $ 提示符,方便整块复制。输出单独成块放在命令下方,只有一行时用注释
标注。需要你自行替换的值一律写成 大写。
每篇教程结尾都有验证步骤。sow check 返回 0,才证明所选 Repository 完整且与记录的
Generation 一致;非零结果不应作为交付物。
2.1 - 搭建 YUM 仓库
本教程从零创建一个 Managed RPM 仓库。你需要一个可写目录,以及一个或多个 RPM 文件。
1. 创建工作区
Dist 名称由你定义。SOW 不会根据 el9 推断操作系统版本。
2. 配置成员策略
编辑生成的 sow.yml。下面的配置按包名与架构各保留一个版本,并排除调试包:
手工修改配置后,先校验再写仓库状态:
策略先执行 exclude,再执行 limit。noarch 包会投影到每个启用的架构视图,不能写进
architectures。
3. 添加 RPM
add 解析包头,将每个接纳的包只保存一次,更新 Desired 成员关系并落成新 Generation。
被策略排除的输入会逐项报告,但不算命令失败。
公共树如下:
rpm-md 的 location href 通过相对路径访问根目录下的 pool/。不要只复制某个架构目录;
它不是独立仓库。
4. HTTP 预览
本地预览可以直接使用:
在另一个终端检查入口:
长期服务应使用持续维护的 HTTP Server。它必须完整暴露 pigsty/ 树,确保客户端解析后落到
pigsty/pool/ 的软件包 URL 可访问。
5. 配置 dnf
将 REPO_HOST 替换为客户端可访问的地址:
刷新并查询仓库:
这里有意使用未签名配置。只有完成仓库签名后,才应打开客户端验签。
6. 发布或导出
交付前必须通过深度校验:
向已配置的 filesystem 或 R2 目标交付时,使用 sow publish。
只有先复制到离线 staging、再原子切换上线时,才适合整根复制;不要对在线仓库做无序原地同步。
默认 dnf reposync 等工具会拒绝指向根包池的父级相对路径。遇到这种消费者时,导出一份
自包含 RPM Leaf:
目标目录必须不存在或为空。默认会复制包体;--hardlink 只适用于同一文件系统、可信且只读的
显式优化场景。
更新仓库
add 与 rm 修改 Desired 成员关系;策略或签名配置变化后用 build 收敛;发布门禁是
check,不能只看 status。
自动化客户端与平台覆盖见平台与集成。
2.2 - 搭建 APT 仓库
本教程从零创建一个 Managed DEB 仓库。你需要一个可写目录,以及一个或多个 DEB 文件。
1. 创建工作区
Dist 名称会成为 APT Suite。它由你定义;SOW 不会根据 trixie 推断发行版语义。
2. 配置成员策略
如果需要过滤或限制版本,编辑生成的 sow.yml:
然后校验:
配置中保存规范架构名,渲染时使用 Debian 生态名称:x86_64 对应 amd64,aarch64
对应 arm64;中立架构 all 包会进入两个视图。
3. 添加 DEB
接纳的包体只保存一次。公共树如下:
pool/ 下的路径按规范化源码包名分组;Packages 中的 Filename 相对 Archive Root;
SOW 会写入 SHA-256 by-hash 副本,并在 Release 中声明。
4. HTTP 预览
本地预览可以直接使用:
检查协议入口:
长期服务应使用持续维护的 HTTP Server,并完整暴露 pigsty/ 树。
5. 配置 APT
将 REPO_HOST 替换为客户端可访问的地址。未签名测试仓库可使用显式信任的 deb822 配置:
刷新并查询:
Trusted: yes 会关闭真实性校验,只适合受控测试。签名仓库应删除该行并配置 Keyring:
打开 Signed-By 前,请先完成仓库签名。
6. 安全发布
交付前必须通过深度校验:
向已配置的 filesystem 或 R2 目标交付时,使用 sow publish。
如果使用其他传输方式,应把完整仓库复制到离线 staging,再原子切换上线。不要逐文件更新在线
dists/ 树,否则客户端可能同时看到不同 Generation 的元数据与包体。
更新仓库
策略或签名配置变化后用 build 收敛;发布门禁是 check,不能只看 status。
自动化客户端与平台覆盖见平台与集成。
2.3 - 仓库签名
SOW 提供两条互相独立的签名路径:
| 路径 | 产物 | 客户端开关 |
|---|---|---|
| RPM 元数据 | repodata/repomd.xml.asc |
repo_gpgcheck=1 |
| APT 元数据 | InRelease 与 Release.gpg |
Signed-By |
| RPM 包体 | RPM 内嵌签名 | gpgcheck=1 |
APT 通过签名的 Release 信任包哈希;SOW 不重签 DEB 包体。先配置元数据签名;只有当你负责
这些 RPM 字节的签名策略时,再启用包体签名。
1. 创建专用密钥
下面生成一把无口令的示例密钥。生产环境应使用受保护密钥并配置 passphrase 引用,详见
配置参考。
私钥必须放在 Workspace 公共 Repository 树与所有 Web Root 之外。若 SOW 由专用服务账户运行,
目录 owner 应是该账户,而不是交互用户。只向客户端分发 repo-signing.pub。
2. 配置元数据签名
在 /srv/sow/sow.yml 的 Repository 下添加所需配置;未使用的包生态可以省略:
解析密钥引用、重建并执行发布门禁:
受口令保护的密钥可在 key 旁增加 passphrase: env://SOW_METADATA_PASSPHRASE,或使用
有界文件引用。SOW 不会把密钥或口令内容写入配置、SQLite、JSON 或日志。
3. 手工验证元数据
按实际 Dist 与架构调整路径:
sow check 会在深度一致性校验中检查配置的签名身份。建立客户端信任根时,仍应手工验证一次。
4. 可选:签署 RPM 包体
只有客户端要求内嵌包签名时,才添加 rpm.packages:
将占位符替换为 $FPR 中的 40 位十六进制指纹。该操作要求:
- 安装
rpm与gpg; - 匹配的私钥存在于
rpm使用的环境 GPG Keyring 中; fill保留已经由配置 key 或trusted_keys签好的包;always重签所有未由配置身份签好的包;never保持输入字节不变。
SOW 只会对私有 staged 副本调用 rpm --addsign 或 rpm --resign,不会修改输入文件。
修改策略后重新校验并构建:
用 rpmkeys --checksig /path/to/package.rpm 检查结果。
5. 启用 dnf 验签
通过可信通道把公钥传到客户端:
再打开与你实际签名范围对应的检查:
没有配置包体签名时,将 gpgcheck 设为 0;既然已经配置元数据签名,就不要关闭
repo_gpgcheck。
6. 启用 APT 验签
将公钥安装为独立 Keyring:
在 deb822 配置中引用它,且不要设置 Trusted: yes:
执行 apt update。任何签名错误都应视为部署失败,不能靠削弱客户端配置绕过。
Plain 模式 RPM 签名
Plain 模式可以签署 RPM 包体,但不会签署仓库元数据,也不会生成 APT Release:
key 必须是恰好 16、40 或 64 位十六进制字符,不能带 0x 前缀;匹配私钥必须能被环境中的
rpm/GPG 使用。不带 --overwrite 时,已有签名的 RPM 保持字节不变;带上该参数则显式重签
所有 RPM。SOW 先签署私有 staged 副本,再替换包体与元数据。
更换密钥
改变 key 引用或解析出的指纹会让相关 Dist 变为 dirty。元数据 key 可以先分发新公钥,再重建、
校验并切换客户端。RPM 包 key 必须分阶段轮换:Package Object 不可变,build 遇到不满足新策略的
既有 RPM 会拒绝,而不是原地重签。旧软件包坐标尚未下架或由新 Release 替代前,应使用 fill
并把旧公钥保留在 trusted_keys。最后在目标环境做真实客户端验收。
最后应使用生产中的确切 dnf/APT 版本与信任策略验收签名仓库。自动化覆盖见 平台与集成。
2.4 - 服务与发布仓库
SOW 只生成静态文件,不是 HTTP 服务器。本指南把可写 Workspace 与 Nginx 服务路径分开。
公共与私有路径
| 模式 | 公共单元 | 绝不能服务 |
|---|---|---|
| Plain | 传给 sow create 的目录 |
临时 .sow-plain-stage-* 输出;没有持久 journal |
| Managed | 一个 Repository 的完整 pool/ + dists/ 树 |
Workspace sow.yml、.sow/、SQLite、锁、日志与 staging |
第一个工作区里的源 Repository 是 /srv/sow/local。不要把
/srv/sow 设为 document root。
1. 校验源 Generation
只有 check 返回 0 才继续。status 适合诊断;check 才是完整只读交付证明。
2. 配置 filesystem target
先创建 endpoint 目录。它必须是真实、规范目录,不能是 symlink;SOW 不会替你创建缺失 endpoint。
第二条命令把写权限交给当前操作者;若 sow publish 由专用服务账户执行,应改为该账户。
在 /srv/sow/sow.yml 中增加 target:
三个布尔字段都是必填安全确认。endpoint 与 prefix 合并为 /srv/repo-public/local;
SOW 会在预先存在的 endpoint 下创建并拥有该 prefix。
校验并发布:
发布先复制不可变包体和元数据,再更新可变协议指针,随后校验结果并记录 target checkpoint。 同一 Generation 重复发布是幂等空操作。
不要让其他工具写入同一 target prefix。target 契约是单 writer、独占写入。
3. 用 Nginx 服务 target
校验配置后 reload Nginx。客户端 URL 为:
如果元数据或包体已签名,请单独发布对应公钥并配置 gpgkey/Signed-By;私钥绝不能放在
document root 下。
4. 验证服务入口
随后从客户端主机运行真实包管理器。HTTP 可达不等于客户端已验证,两层都要检查。
完整 Repository prefix 必须使用同一访问策略。RPM 元数据可能通过 ../../../pool/...
解析包路径,APT Filename 也直接指向 pool/...。只保护 dists/ 而误放开或拦截 pool/
都会破坏仓库。
手工与隔离交付
如果 sow publish 无法到达目标:
- 在源端运行
sow check; - 把完整 Repository 复制到新的、非 live staging/release 目录;
- 用
sow changes 0或 archive manifest 校验传输哈希; - 原子切换操作者拥有的父级引用到新目录;
- 保留上一版,直到客户端与缓存越过它。
不要直接对 live Repository root 执行无序 rsync --delete。它不保留 SOW 的指针顺序、
target checkpoint、缓存 grace 或恢复状态。sow changes 描述 Generation 差异,不代表可以
绕过这些控制直接修改 live target。
R2 target
provider: r2 使用 S3 兼容存储传输与只报告的 target GC。传输集成会针对固定 MinIO fixture
验证 list、HEAD、GET 与条件 PUT。启用生产目标前,请先在非生产 prefix 验证凭据、bucket
policy、公共 endpoint、缓存行为、重放与恢复。详见平台与集成。
下一步
2.5 - 演练构建 pigsty-infra 仓库
pgsty/infra-pkg 是 Pigsty Infra 软件包的上游构建源码。
本教程假设双架构 RPM 与 DEB 已经构建完成,只处理后半程:从一堆包开始,用 SOW 建成真正可消费、可维护的 infra 仓库。
1. 把包集中到 ~/repo
本教程固定使用 ~/repo,不再为每条路径定义环境变量。先把已有包复制进两个输入目录:
先确认四个“格式 × 架构”象限都有真实包体:
四个结果都必须大于零。此时目录只有输入包池:
2. 创建 infra Repository 与两个 Dist
初始化 Workspace,并创建名为 infra 的 Repository:
现在模型已经确定:
打开 ~/repo/sow.yml,把配置整理为:
limit: 1 按“包名 + 原生架构”只保留最新一个版本。因此 rpm 与 deb 就是两个滚动更新的
latest channel;它们仍会同时保留 x86-64 与 ARM64 两个架构。
3. 一次性导入并构建
先更新 Desired Membership,最后只构建一次:
sow check 返回 0,才算初始化完成。再核对 SOW 从包头读出的真实格式与架构:
预期至少出现:
SOW 使用规范化架构名,因此 DEB 的 amd64/arm64 在这里显示为 x86_64/aarch64。
4. 看懂生成的目录
打印实际目录:
关键结构应当是:
这里有一个容易混淆、但必须记住的路径规则:SOW 的 Dist 固定放在 dists/ 下。因此逻辑上的
infra/rpm 与 infra/deb,真实视图路径分别是 /infra/dists/rpm/ 和 /infra/dists/deb/;
包体则统一放在 /infra/pool/。发布或挂载时必须使用完整的 ~/repo/infra,不能只拿走某个 Dist。
5. 用 Nginx 只读服务仓库
使用官方 nginx:alpine 镜像,把 Repository Root 只读挂载到 /infra:
直接检查两种索引入口:
Nginx 只看得到 ~/repo/infra,既看不到 sow.yml 与 .sow/,也无法修改仓库。
6. 在 EL9 中只用 infra 安装 RPM
下面的 Rocky Linux 9 容器位于 --internal 网络中。脚本先删除所有预置仓库,再只启用刚创建的
infra,因此成功安装不能依赖公网软件源。
RPM 的 baseurl 必须落到具体架构视图。$basearch 会由 dnf 展开为 x86_64 或 aarch64。
7. 在 Ubuntu 24.04 中只用 infra 安装 DEB
APT 的 URI 指向 Repository Root,Suites 才是 Dist 名 deb:
本教程使用隔离 HTTP 仓库,所以临时关闭了验签。正式服务应配置 RPM/APT 元数据签名,并移除
gpgcheck=0 与 Trusted: yes。
Docker 默认验证宿主机架构。若要完成四格运行矩阵,分别给两条 docker run 增加
--platform linux/amd64 与 --platform linux/arm64 后各跑一次;跨架构运行需要 Docker 的
binfmt/QEMU 支持。仓库清单检查与客户端安装检查是两个独立门禁。
8. 日常维护:添加一个新版本
更新仓库的正常动作是 add,不是先删除旧包。假设已经拿到新版 pg-exporter 的四个包体:
因为 latest Dist 配了 limit: 1,新版本胜出后,旧版本会自动退出该 Dist 的 Desired Membership。
旧字节不会被立即删除,也不会因为以后放宽策略而自动回来。
可以重新运行第 6、7 节的客户端,先 makecache/update,再安装或升级,完成更新验收。
正常发布不要先 sow rm。如果某个错误包必须紧急撤回,先用 sow ls -r infra -d rpm --json
或对应的 -d deb 找到精确 SHA-256,
再依次执行 sow rm sha256:... -r infra -d rpm --check 与不带 --check 的同一命令。
rm 只删除 Dist Membership,pool 字节仍由保守的 sow gc 独立回收;不要用裸包名误删所有版本与架构。
9. 两层保留策略:latest 与 stable
rpm、deb 的 limit: 1 适合持续滚动,但不能表达“保留所有正式发布历史”。为此再创建两个 Dist:
新 Dist 的默认 limit 是 0,表示保留所有版本。最终策略是:
| Dist | 格式 | limit |
角色 |
|---|---|---|---|
rpm |
RPM | 1 | RPM latest |
deb |
DEB | 1 | DEB latest |
rpm-stable |
RPM | 0 | 累积所有已晋升 RPM |
deb-stable |
DEB | 0 | 累积所有已晋升 DEB |
注意:stable 不是把“包池里的所有历史”自动放回来,而是从现在开始,累积每次明确晋升的版本。
10. 把 latest 晋升到 stable
SOW 没有独立的 promote 命令。当前可靠做法是先冻结写入并导出源 Dist 的精确 Membership,
再把这些对象加入目标 Dist。输入直接取自 infra/pool;SOW 会校验并复用已有 Package Object,
不会重新打包,也不会在 pool 中复制第二份包体。
先确保源状态干净,并保存本次晋升清单:
在晋升结束前暂停对 rpm 与 deb 的写入,然后复用这些 pool 对象:
每次 add 应报告 reused。若中途失败,源 Dist 不受影响;修复问题后对同一清单重跑即可。
随着以后重复晋升,rpm/deb 仍只保留最新版本,而 rpm-stable/deb-stable 会逐次累积历史版本。
11. 从 stable 创建 2026-08 快照
客户端可见的月度快照也是两个新的 Dist:
在快照窗口内暂停 stable 写入,先把其精确 Membership 固化为清单:
再把清单加入对应快照 Dist:
把这次已验证的完整 Repository Generation 也加入保留集合,防止后续 GC 把它当作不可达历史处理:
retain 保留的是整个 Repository Generation,用于恢复与 GC 安全根;客户端可见的固定 URL 仍由
rpm-202608 与 deb-202608 两个 Dist 提供。SOW 尚不强制快照 Dist 只读,因而创建后不再对它们
执行 add 或 rm 是维护规约的一部分。
12. 客户端地址总表
同一个 Nginx 与同一份 infra/pool 支撑所有 channel:
| Channel | dnf baseurl |
APT URIs / Suites |
|---|---|---|
| latest | http://infra-nginx/infra/dists/rpm/$basearch/ |
http://infra-nginx/infra / deb |
| stable | http://infra-nginx/infra/dists/rpm-stable/$basearch/ |
http://infra-nginx/infra / deb-stable |
| 2026-08 | http://infra-nginx/infra/dists/rpm-202608/$basearch/ |
http://infra-nginx/infra / deb-202608 |
最终验收:
到这里,我们得到的不是一次性演示目录,而是一个可以继续收包、晋升与做月度快照的真实 Infra
Repository:rpm/deb 负责快速更新,rpm-stable/deb-stable 负责积累正式历史,月度 Dist 提供固定入口,
所有视图复用同一份不可变包体。
实验结束后可停止临时服务:
3 - 功能
SOW 提供两条相互隔离的运行路径。Plain 模式无状态地重建一个目录;Managed 模式在工作区中 持续记录软件包成员关系与不可变仓库 Generation。两者都不会暗中接管对方的状态。
能力矩阵
| 能力 | Plain | Managed |
|---|---|---|
| RPM 与 DEB 元数据 | 是 | 是 |
| RPM + DEB 混合操作 | 同一目录 | 同一 Repository、不同 Dist |
| 持久成员关系与 Generation | 否 | 是 |
| 分架构视图与中性包投影 | 否 | 是 |
exclude 与版本 limit 策略 |
否 | 是 |
| 元数据签名 | 否 | RPM 与 DEB |
| RPM 包签名 | --sign-with |
never、fill、always |
| 事务日志与恢复 | 重新运行 create |
Workspace、Repository、发布 |
| 可查询 Operation Log 与 JSONL 导出 | 否 | 是 |
| 发布目标 | 否 | filesystem 与 R2 |
SOW 在进程内解析软件包并渲染元数据,不调用 createrepo_c、dpkg-scanpackages、
reprepro 或 modifyrepo_c。RPM 包签名是例外:它会改写包体,因此需要主机上的 rpm
命令与 GPG 环境。
仓库格式
| 表面 | RPM/YUM | DEB/APT |
|---|---|---|
| 包事实来源 | RPM header | DEB control archive |
| 身份 | NEVRA + 确切字节 SHA-256 | name=version:arch + 确切字节 SHA-256 |
| 索引 | primary、filelists、other、repomd.xml |
Packages、Packages.gz、Release |
| 中性架构 | noarch |
all |
| 不可变索引路径 | 校验和命名 rpm-md | by-hash/SHA256 |
| Managed 元数据签名 | repomd.xml.asc |
InRelease、Release.gpg |
SOW 有意不生成 SQLite rpm-md、zchunk、modulemd、源码包索引与 MD5/SHA1 DEB manifest。 它负责构建仓库文件,不提供 HTTP 服务或 CDN。
按问题阅读
| 问题 | 页面 |
|---|---|
sow create 写什么、替代什么? |
Plain 平面仓库 |
| Workspace、Repository、Dist 与私有状态如何关联? | Managed 工作区 |
| 一个包池如何供给多个纯元数据视图? | 包池与元数据视图 |
| 为什么软件包被排除或限量? | 成员策略 |
| 哪把密钥签哪个对象? | 签名模型 |
| 中断后会发生什么? | 事务与恢复 |
| 如何查看、校验并审计仓库? | 可观测与审计 |
Release 目标、文件系统要求、客户端与 Provider 见平台与集成。
3.1 - Plain 平面仓库
sow create 接手一个已经放着 .rpm / .deb 的目录,在包旁边生成平面仓库索引。Plain 模式没有工作区、配置文件、数据库、期望状态,也没有操作 journal。包目录就是权威事实来源,所有索引都是当前目录内容的可丢弃投影。
这个边界是刻意的:Managed 仓库保存状态并恢复事务;Plain 仓库失败了就便宜地重建。一次运行失败或被中断后,重新执行同一条命令,覆盖派生元数据即可。
契约
Plain 模式由四条规则定义:
- 包是权威事实。 默认
create不修改包字节,只替换repodata/、Packages与Packages.gz。--pigsty和显式 RPM 签名是文档明确列出的例外。 - 包内容只扫一遍。 默认未签名路径中,每个选中包只打开一次、完整计算一次 SHA-256,并在同一遍里解析。完整 RPM/DEB 元数据保留给渲染阶段;渲染与输出校验不会再次打开包体。
- 收尾只做一次便宜校验。 发布前重新列出顶层包集合,把
stat事实与扫描快照比较,不再计算第二遍包 SHA-256。 - 失败就重建。 Plain 没有事务 journal、pre-image、前滚或回滚。失败可能留下部分已替换的派生元数据;下一次
sow create丢弃自有临时残留,按当前包目录完整重建。
输入字节不变时,输出仍然确定,重复运行报告 noop=true。
单遍流水线
--jobs 默认等于逻辑 CPU 数,控制唯一一次包内容扫描:
worker 完成先后不会影响结果:解析事实始终按规范 basename / 索引顺序消费。RPM XML 直接使用 worker 保留下来的完整解析对象;DEB Packages 直接使用保留的 control 段落与该 worker 已经算出的 SHA-256。
输出自校验仍会读取生成的 XML、repomd.xml、Packages 与 Packages.gz。这些是很小的派生元数据,不会再次读取包体。
最终 stat 校验保证什么
收尾校验要求:
- 顶层普通
.rpm/.debbasename 的排序集合完全相同; - 文件 identity / inode 相同;
- 文件类型与 mode 不变;
- size 与 mtime 不变。
任一事实不同,都在发布前以完整性退出码 5 拒绝。这能以一次列目录的成本发现正常的新增、删除、替换、截断与重写竞争。
它刻意不是密码学复验。若外部写者原地改字节,同时伪造保持 inode、size 与 mtime 不变,stat 无法发现。Plain 接受这个权衡,因为目标场景是本机单进程、协作写入、结果可重建。需要对抗并发篡改证据或持久恢复时,应使用 Managed 仓库。
扫描与输出规则
- 只考虑目录顶层普通文件;永不递归,也不跟随符号链接。
- 只选择
.rpm与.deb后缀。 - 包身份与架构来自 RPM header 或 DEB control,绝不从文件名猜;RPM
src/nosrc被拒绝。 - 所有合法版本都进索引;同一逻辑坐标对应不同字节流时拒绝。
- 默认模式拒绝空目录;
--pigsty可以把一次中断清理后已经为空的权威包集合收敛完成。
有 RPM 就生成 repodata/,有 DEB 就生成 Packages 与 Packages.gz:
平面位置全部是相对路径:RPM 使用裸 basename,DEB 使用 ./<basename>。公开目录固定 0755,生成文件与 repo_complete 固定 0644,不受 umask 影响。
某种包格式消失时,SOW 删除该格式已知的派生输出。重跑也会覆盖中断留下的半套输出,例如只有一个 Packages;新一代不再引用的 SOW checksum 形状 RPM 元数据会被移除,未知文件保持不动。
确定性与 no-op
repomd.xml 的 revision 与 timestamp 固定为 0,gzip header 固定,排序规范化。因此同一包集合生成逐字节一致的元数据。发布前 SOW 会比较 stage 与 live 元数据;如果无需清理/签名且所有输出已经相同,就只删私有 stage,不替换公开 inode,并返回 noop=true。
Plain create 不做 journal 恢复,因此 JSON 字段 recovered 始终为 false。
发布与中断语义
开始发布前,全部元数据都已在目标目录内的私有 stage 生成并验证。单文件替换使用同文件系统 rename;RPM 先安装 checksum 命名元数据,最后替换 repomd.xml。
这不是多文件事务。进程在发布中被杀,可能留下新 RPM 元数据配旧 DEB 元数据、只剩一个 DEB 索引文件,或多余的旧 checksum RPM 元数据。这些状态不是需要调和的事务证据,只是可丢弃输出。下一次运行按当前包集合重新渲染完整投影,覆盖或删除残留。
实现不创建持久 journal 或 recovery trash。启动时会先丢弃 Plain 保留 staging 命名空间中 属于 SOW 的残留,再开始全新扫描。
--pigsty marker 门禁
--pigsty 还会删除命中兼容规则的解析包事实(DEB i386 与 Patroni 3.0.4),并把剩余包按 basename 排序写入 repo_complete,格式为 <sha256><两个空格><basename>。RPM 不会仅因为架构是 i386/i486/i586/i686 而被删除。
发布顺序为:
marker 缺失就表示“尚未完成”,消费方不得使用该目录。若运行在撤 marker 后停止,重新执行 sow create --pigsty:它扫描现在仍存在的包,覆盖元数据、完成清理,最后写新 marker,无需动作日志。
默认模式看到已有 repo_complete 会拒绝运行,避免未受门禁控制的命令留下过期就绪声明。
显式 RPM 签名
--sign-with 是修改包体的显式授权,也是独立慢路径。SOW 在私有 stage 副本上签名,验证嵌入签名与 signature-neutral digest,重新解析结果,再先于元数据安装签后字节。这些必要的复制、签名与签后验证读取不属于默认未签名的一遍保证。签名中断后同样按当前包目录重跑,不从 journal 重放签名事务。
锁与适用范围
sow create 在一次运行期间锁定目标目录及其稳定父目录;--timeout / --no-wait 控制协作锁等待。锁能阻止另一个协作 SOW 进程同时写入,但不会把任意外部包修改变成受支持负载。
本地单进程创建一个可随时重建的平面仓库,用 Plain。需要期望状态、审计历史、原子 generation 切换或证据驱动崩溃恢复,用 Managed 工作区。
继续阅读
sow create参考 —— 参数、输出与失败契约- 事务与恢复 —— Managed 的耐久边界
- 快速上手 —— 五分钟建一个仓库
3.2 - 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 只需完整认证并解析每个新软件包一次;生产构建只按本次
选中的 Digest,以有界、确定性批次读取 Facts Row,遇到缺失或损坏记录时再从已认证包体惰性
重建。无关或超大的 Facts Row 不会被读取。
对于未改变的 Pool 文件,暖构建使用设备号、inode、size、mtime 与 ctime 指纹避免重新读取
包体。指纹漂移与 Facts 缺失共享一次权威 SHA-256 校验并自动修复;
sow check 仍是显式的完整密码学审计,并且无论指纹是否匹配,都会
对每个唯一物理包体哈希一次。
缓存与指纹只属于私有实现状态,不改变公共 pool/ + dists/ 布局。不要手工编辑数据库或
PRAGMA user_version。v0.3 Repository 必须先备份,再通过
sow repo migrate 显式升级,之后才能执行普通 0.4
读写。全新 0.4 Repository 已使用当前 Schema;其他维护只在 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 就停,不会越过它继续往上找"更好的那个"。
--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
3.3 - 包池与元数据视图
不变式
在一个 Repository 内,每个 live Package Object 在 pool/ 下只有一条正典 payload 路径。
Dist 与架构 view 拥有元数据,不拥有包体 alias:
相同 digest 出现在另一个 Repository 或发布 prefix 时,仍是另一个 owner 下的独立对象。 SOW 不会为了本地去重而制造共享的分布式所有权。
构建完成的 Repository
一个包含一份 x86_64 包与一份 noarch 包的 RPM Dist 形如:
不存在 dists/.../pool/ 子树。仍被保留的 live Generation 可以让多组内容寻址元数据并存;
repomd.xml 指针决定当前生效的是哪一组。
RPM view 使用计算出的父级相对 href
rpm-md 相对架构 view 解析 <location href>。SOW 从实际 view 计算回到正典 Pool 的路径:
深度从实际 view root 推导,不来自域名,也不写死部署路径。sow check 会解析、规范化每条
href,拒绝逃出 Repository 的路径,并证明它抵达预期 Pool 对象。
因此完整 Repository 根才是客户端与交付边界。DNF 指向 dists/el9/x86_64/,但对外服务或
复制时必须包含同级根 pool/ 的整个 Repository。
为什么 view 只含元数据
如果把软件包复制到每个架构 view,在没有 inode 身份的存储系统上就会产生额外 object key 与重复上传。SOW 因此把包体所有权集中在 Repository 包池,让索引负责投影成员关系。 完整复制、归档或发布都能保持这份契约,无需依赖 hardlink。
“只存一份”的边界是一个 Repository 或一个发布前缀,不是 Workspace、bucket、账号或整套 系统。相同软件包位于不同 Repository 或 target 时仍有各自独立的 owner。
中性包只被选入,不会复制
x86_64 view 选择 x86_64 + noarch;aarch64 view 选择 aarch64 + noarch。
中性包仍只有一份 Pool 对象,每个 view 只增加一条指回它的元数据记录。
DEB 在 archive root 层面同理:all 包进入每个适用的 Packages 索引,Filename: pool/...
始终指向唯一正典包体。
APT view
APT 原生把 Filename 定义成相对 archive root:
SOW 在 dists/<dist>/main/binary-<arch>/ 下渲染 Packages、Packages.gz 与 by-hash。
Release、InRelease、Release.gpg 是协议指针与签名。没有每 view 包体 alias,也没有
每架构 Release 存根。
普通客户端与 reposync 是两份契约
规范布局面向能消费完整 Repository 并正确处理协议相对路径的软件包客户端。默认 EL
dnf reposync 是另一份契约:它的 safe-write 检查会
拒绝规范化后落到 per-repository 下载目录上方的软件包路径。这是明确不支持的组合;该工作流
应使用导出的 Leaf。
需要自包含 RPM leaf 时,在 Repository 与所有已配置 filesystem 发布根之外创建导出:
导出拥有自己的包体树、repodata、manifest 与 .sow-export.json 完成标记。默认复制;
--hardlink 是显式的同文件系统、可信只读优化。导出不会成为 Membership、Generation、
publish input 或 GC root。
复制与发布
正典正确性不依赖 inode 身份。优先使用已配置的 Publication Target。必须使用其他传输方式时,
用 rsync、cp 或 tar 把完整、稳定的 pool/ + dists/ 复制到离线 staging,复验后再原子
切换上线;不要逐文件更新在线树。只复制某个 RPM 架构 Leaf 不受支持,因为其中元数据有意
引用同级根 Pool。
sow changes 只在 pool/ 下列一次包体,随后是元数据与指针:
dists/ 下不会出现 package payload 变更项。
继续阅读
- Managed 工作区——所有权与 Generation 状态
- 平台与集成——已验证与明确不支持的组合
- 对外服务——HTTP、复制与发布目标
- 仓库布局——准确的公有/私有路径
3.4 - 成员策略
策略回答的是这个问题:“我把整个构建目录倒进了这个 Dist,但我不想要 debuginfo 包,而且每个包只留最新版本。“两条规则完成这件事,它们按固定顺序执行,并且作用于 完整候选集,而不是你这次恰好 add 的那几个包。
两条规则与它们的顺序
exclude 丢掉命中规则的包,limit 再按包名与架构限制存活的版本数。顺序固定且不可配置 —— 反过来的话,一个即将被排除的包会在离场路上白白占掉一个版本名额。
两条规则在每次 add、每次 rm 和每次 build 时都强制执行。最后这条很关键:在 sow.yml 里改 limit 或 exclude 会让受影响的 Dist 变 dirty,下一次 build 就把新策略重新施加到现有成员集上。想让收紧后的策略生效,你不需要重新 add 任何东西。
exclude
exclude 是一个规则列表。同一条规则内,各字段之间是 AND;同一字段内,多个 pattern 之间是 OR;规则与规则之间是 OR —— 任一规则命中即排除。字段顺序和规则顺序都不影响结果。
这段读作:不分架构地丢掉所有 debug 类包,并且 丢掉名字以 test- 开头或以 -experimental 结尾的 aarch64 包。
允许五个字段:
| 字段 | 匹配对象 |
|---|---|
name |
二进制包名 |
source |
规范化后的 source 名 |
arch |
x86_64、aarch64 或 neutral |
kind |
下表固定枚举 |
format |
rpm 或 deb |
pattern 是区分大小写的精确字符串或 shell glob(*、?、[])。没有正则,没有版本比较,没有取反,也没有表达式语言。未知字段、空规则和非法 glob 会在 config check 时失败,而不是静默地什么都匹配不到。
kind 由二进制包名推导,优先取最具体的后缀:
| 格式 | 名称后缀 | kind |
|---|---|---|
| RPM | -debuginfo |
debuginfo |
| RPM | -debugsource |
debugsource |
| RPM | -llvmjit |
llvmjit |
| DEB | -dbgsym |
dbgsym |
| DEB | -dbg |
dbg |
| 任意 | 以上均不匹配 | main |
分类结果只来自包本身,不依赖文件所在目录,也不依赖当前主机,所以同一份输入永远分到同一类。sow show --json 会输出算出来的 kind。
被排除的包会被如实报告,不算解析失败,也不会被存下来:
命令退出码是 0。被排除的包本身没有任何问题,它只是不属于这个 Dist。如果一个包没有被任何 Dist 接受,就不会为它写下无主的 pool 对象。
limit
limit 按 (二进制包名, 原生架构) 分组,保留最新的 N 个:
0—— 保留全部版本,这是默认值。- 正整数
N—— 按原生版本序保留最新的 N 个。 - 负数 —— 配置错误。
有两个细节能回答现实中的绝大多数疑问。
分组键包含架构。 limit: 1 不是"这个 Dist 里这个包只留一个版本”,而是"每个包名 + 每个原生架构留一个版本”。所以 pg_sample-1.13(x86_64)与 pg_sample-1.17(noarch)可以同时存在于 limit: 1 的 Dist 里,因为它们属于不同分组。中性包(noarch/all)作为自己的原生架构只计一次,尽管它会渲染进多个视图。
排序用格式的原生规则。 RPM 用 EVR 比较 —— epoch、version、release,遵循标准 rpm 分段规则。DEB 用 Debian version 比较,版本串本身已经包含 epoch 与 revision。SOW 不发明版本方案,也不做字典序比较。
下面是 limit: 1 在同名同架构的两个 Debian 版本之间做决定:
注意这里有两级报告:条目的整体 status 是 excluded(它最终没在任何地方成为成员),而逐 Dist 的结果是 limited —— 告诉你它是输在版本上,不是被某条 exclude 规则命中。当你同时选中多个 Dist 时,每个 Dist 各报各的结果,所以一条命令里同一个包完全可能在一个 Dist 是 accepted、在另一个是 limited。
limit 移除旧成员、加入新成员发生在同一个 Operation 内,所以账本上看到的是一次原子决策,而不是一次删除加一次不相干的插入。
策略作用于完整候选集
一个常见误读是:add 只对命令行上的包施加策略。并非如此。把你的输入合并进目标成员集之后,SOW 会对每个选中 Dist 的 完整 成员集执行 exclude,再执行 limit。
现实后果是:往一个已经装着版本 1 和版本 2 的 limit: 2 Dist 里加版本 3,会在同一个操作里移除版本 1。你没法靠"分开单独 add"绕过版本上限,也不会因为"只拿增量与上限比"而落得 N+1 个成员。
放宽策略永远不会复活任何东西
这条语义最常被误以为是反过来的,所以值得直接演示。接着上面 limit: 1 的例子,把胜出的那个版本删掉:
Dist 空了。bookworm 那个构建没有回来 —— 尽管它的字节还躺在 pool 里,尽管 limit: 1 此刻明明空出了一个名额。
原因在于 exclude 与 limit 移除的是 真实的期望成员。SOW 不维护一份"被策略压下、将来也许还能回来的候选"影子清单。Pool 字节是存储,不是候选集。因此提高 limit 或放宽 exclude 只是给未来的添加腾出空间;它不会回头翻历史,猜哪些你曾经拥有过的包该重新出现。
想让它回来,就再显式 add 一次:
收敛是单向的,而且这是写进不变式的:收紧策略可以移除成员,放宽策略永远不会恢复成员。 正是这种不对称让 build 在任何时刻都能安全执行。假如它是对称的,那么编辑 sow.yml 就可能静默地重新发布一个你刻意下架的包 —— 而这恰恰是安全更新场景里最不能出的事故。
sow rm 移除的是成员关系,不是 pool 字节。包会从所有索引中消失,客户端不再能通过仓库
解析它。只有当包体不再被当前、保留、恢复、发布以及活动维护操作等任何安全根引用时,
才运行 sow gc。
已发布目标使用 sow gc TARGET;filesystem 删除是条件式的,R2 只生成报告。
不要绕过 SOW 状态手工删除规范包池文件。
预览一次决策
sow rm -c 计算将要移除的成员、策略后果,以及此刻 build 会产生的文件变化,但什么都不写:
-c/--check 不取写锁,并且与 --skip 互斥。同时给出 --timeout 或 --no-wait 属于用法错误 —— 免得有人误以为一次预览会去等待写事务。
继续阅读
sow.yml配置参考 —— 完整策略 schemasow add参考 —— 逐条目状态与部分成功退出码- 包池与架构视图 —— 存活下来的成员被渲染到哪里
sow retain与sow gc—— 包体生命周期控制
3.5 - 签名模型
客户端会对一个仓库提两个不同的问题,SOW 用两套彼此独立的机制分别回答。把它们混为一谈,是"我明明签了名,dnf 还是报错"这类问题最常见的来源 —— 所以本页先把两者拆开。
两条独立的信任链
| 元数据签名 | RPM 包体签名 | |
|---|---|---|
| 回答的问题 | “这份索引真是你出的、没被改过吗?” | “这个 .rpm 文件真是你出的吗?” |
| 配置项 | signing.rpm.metadata、signing.deb.metadata |
signing.rpm.packages |
| 产出 | repodata/repomd.xml.asc、InRelease、Release.gpg |
嵌入包内的 OpenPGP 签名 |
| 是否改变包字节 | 否 | 是 |
| 客户端配置 | dnf repo_gpgcheck=1、apt Signed-By |
dnf gpgcheck=1 |
| Plain 模式可用 | 否 | 是,通过 create -S KEY |
二者分别配置、可分别使用。通常正确的起点是只做元数据签名:它在一个地方为整份索引背书,而且完全不需要改动你从上游拿到的那些包。
Managed 的元数据签名完全由 sow.yml 控制。没有 CLI 覆盖开关,build 上没有 --sign 参数,也没有办法让这次构建和下次构建签得不一样。这是刻意的 —— 仓库的签名身份是仓库的属性,不是"碰巧更新了它的那条命令"的属性。
配置
RPM 与 DEB 的元数据密钥分开声明,所以你可以像上面这样两边共用同一把钥匙,也可以拆开用。每个 metadata 块除 key 外还接受可选的 passphrase 引用。
配了元数据密钥之后,每次构建都会产出签名文件 —— 空 Dist 也不例外:
- RPM,每个架构视图:
repodata/repomd.xml加一份 ASCII-armored 的repodata/repomd.xml.asc - DEB,每个 Dist:
Release加一份 clearsigned 的InRelease与一份分离式 armored 的Release.gpg
InRelease 的 clearsign 正文与 Release 完全一致。没有配元数据密钥时,这两个签名文件根本不会生成 —— 你只会得到 repomd.xml 和 Release。
四种密钥引用形态
密钥引用是一个 URI,scheme 决定由谁来签:
| 引用 | 含义 | 签名者 |
|---|---|---|
keys/repo-signing.asc |
相对 Workspace Root 的 ASCII-armored 密钥路径 | 进程内 Go signer |
file:///绝对路径.asc |
磁盘上的 ASCII-armored 私钥 | 进程内 Go signer |
env://VAR_NAME |
环境变量里的 armored 密钥材料 | 进程内 Go signer |
agent://<fingerprint> |
由环境中 GPG agent 持有的密钥 | 外部 gpg |
file:// 与 env:// 不需要装任何东西 —— SOW 自己签元数据,这也是为什么用 file:// 元数据密钥的仓库在 macOS 和最小化容器里能构建出一致的结果。agent:// 把签名委托给你的 GPG agent,适合私钥在智能卡上、或绝不能落盘的场景。agent:// 不能与 passphrase 引用同时使用,因为那次交互归 agent 管。
passphrase 引用接受相对 Workspace Root 的路径、file:// 或 env://,不接受 agent://。
任何秘密都不会被持久化。 配置、SQLite、日志、JSON 输出和错误文本里,只有引用字符串、fingerprint 和公钥验证证书。config show --all 打印引用与 fingerprint,绝不打印密钥材料。如果某个密钥引用无法解析或不可用于签名,config check 会在你执行 build 之前就告诉你。
RPM 包体签名
三种模式:
| 模式 | 行为 |
|---|---|
never |
原样保留输入字节 |
fill |
包未签名、或签名不受信任时用配置的 key 签;已有签名能被 trusted_keys 验证通过则保持字节不变 |
always |
确保最终包由配置的 key 有效签名;已经是了就保持字节,否则重签 |
trusted_keys 自动包含配置 key 的公钥部分。没有 key 时只能用 never;有 key 时默认 fill。
信任环彼此独立验证
对于带签名 RPM,SOW 会分别评估每个 Retained 单 Key Ring、当前 Policy Ring 与组合
trusted_keys Ring。所有可识别 OpenPGP Signature Packet 必须在同一个候选 Ring 内验证通过,
且至少一条通过的路径必须认证 Payload。SOW 绝不会把一个 Key 接受的 Packet 与另一个单 Key
Ring 接受的 Packet 拼在一起,虚构出 Retained Signer。
这条区别在有意双签过渡时尤其重要:组合 Trusted Ring 可以接受该包,但任一单独 Retained Key 都不能宣称自己独立证明了它。历史 CentOS OpenPGP v3/v4 签名继续受支持。所有候选 Ring 共享 同一遍 Signed-byte Stream,因此增加 Trusted Key 只改变授权结论,不会放大包体读取。
包体签名总是对私有 staged 副本调用环境中的 rpm --addsign 或 rpm --resign,不会就地
修改输入文件。签完后 SOW 会重新解析结果,要求嵌入签名存在、signature-neutral digest 与
NEVRA 不变,并且签名身份与配置完全一致。fill 与 always 必须有 rpm、gpg,且匹配私钥
必须存在于 rpm 使用的 GPG 环境中。key 引用用于标识并验证签名者,不会把私钥自动导入该环境。
由于签名里嵌入了时间戳,签名过程不可复现 —— 同一个未签名 RPM 签两次会得到不同字节。于是重复 add 一个已经加过的包看起来就像内容冲突。SOW 用 signature-neutral payload digest 解决这个问题:对不可变的 header 与 payload(排除 RPM signature header)计算 SHA-256。如果逻辑坐标已存在、neutral digest 相同,且既有对象满足当前策略,SOW 就复用既有的最终字节而不再签名。重复 add 同一个包是稳定的空操作。
这种复用的口子刻意开得很窄。never 模式要求完整字节一致,因为该模式承诺保留输入字节。如果 payload digest 不同,或既有对象不满足当前签名策略,那就是硬冲突 —— add 不会悄悄地在既有坐标上就地重签一个包。这里没有 --replace;如果重签导致字节变化,请提高 release,或专门规划一次密钥轮换流程。
更换密钥会让 Dist 变 dirty
一个 Dist 的 Built 配置摘要覆盖它的 format、canonical 架构、limit、exclude,以及 已冻结的签名身份。改动密钥引用或 fingerprint 会改变这个摘要,于是所有受影响的 Dist 变 dirty:
更换 元数据 key 后,sow build 会用新身份签署索引并产生新 Generation。
RPM 包体是不可变 Package Object;build 不会在同一坐标下静默重签既有对象。如果当前 Desired
RPM 不满足新的包签名策略,build 会拒绝。分阶段轮换通常使用 fill:将新 key 设为当前 key,
同时把旧公钥保留在 trusted_keys;旧 key 软件包保持字节不变,新加入的软件包使用新 key。
只有当旧坐标已下架或被新 Release 替代后,才移除旧信任。直接切到新 key 的 always,要求
每个 Desired RPM 已经由新 key 签名。
当前 Built 元数据的精确公钥证书身份按 Dist 记录,同一 primary fingerprint 的多个证书版本可以共存 —— 所以延长有效期或增加子钥,不会让已经发布出去的东西失效。
Plain 模式
Plain 模式只签 RPM 包体,没有元数据签名。KEY 必须是恰好 16、40 或 64 位十六进制 GPG key ID/fingerprint,不接受 0x 前缀;规范化为大写后作为 _gpg_name macro 传给 rpm。不带 --overwrite 时只签没有可解析嵌入签名的 RPM;带上则对全部保留的 RPM 重签。
--sign-with 要求 --pigsty 清理后至少保留一个顶层 RPM。纯 DEB 目录、缺少 rpm 可执行文件、密钥不可用,都在任何公开变更之前失败。签名是显式慢路径,必然包含复制、签名验证与最终 RPM 解析读取;中断后按当前包目录重跑,而不是重放 Plain journal。见 Plain 平面仓库。
客户端验证什么
repo_gpgcheck=1 让 dnf 验证 repomd.xml.asc;gpgcheck=1 让它验证每个包的嵌入签名。
APT 侧的 Signed-By 让 apt 验证 InRelease。自动化检查会直接校验生成的签名;完整的签名
Managed dnf/APT 验收必须在目标环境中使用真实客户端执行。确切证据见
平台与集成。
sow check 在常规运行中就会校验全部已声明的签名与文件哈希,所以签名配置出错会在发货之前暴露,而不是在客户机器上暴露。
继续阅读
- 仓库签名 —— 生成专用密钥并把两条链都接起来
sow.yml配置参考 —— 完整签名 schema 与密钥引用文法- 可观测与审计 ——
check如何证明这些签名
3.6 - 事务与恢复
本页说明 Managed 模式如何防止 live 指针指向缺失内容,以及如何协调 SQLite 状态与文件系统变更。
不变式
在受支持的本地 POSIX 文件系统上,沿 Managed 协议指针读取的客户端只会得到完整旧视图或 完整新视图,包括进程中断之后。
下面所有内容都是为了守住这条线:元数据在任何公开变更之前完整 stage 并校验,指针切换就是提交决策,每个操作都留下足够的持久证据,让下一条命令能把它做完或撤销,而不需要猜。
Plain sow create 刻意不属于这套事务模型。它以包目录为权威事实、把元数据视为可丢弃投影:一遍内容
扫描、一次最终 stat 校验,然后覆盖发布。中断后重新运行 sow create,而不是重放 journal。详见
Plain 平面仓库。
也要注意它 没有 声称什么。dirty 不是指索引写了一半;它表示 Desired 状态领先于
Built Generation,而旧的 Built View 仍然完整。SOW 也不承诺两个不同 Dist 在同一瞬间翻代;
它承诺每个协议视图始终自洽,且写命令返回时,本次 Operation 包含的每个 Dist 都处于记录的
Built Generation。
两类持久日志
Managed 仓库生命周期与变更使用两类持久化载体,各自作用域很窄:
| 日志 | 位置 | 覆盖范围 | 由谁恢复 |
|---|---|---|---|
| Workspace 文件 journal | .sow/workspace-ops/active.json |
init、repo new、repo rm |
下一条工作区生命周期命令 |
| Repository 操作日志 | 该仓库的 SQLite | dist new/rm、add、rm、build、log prune |
该仓库的下一条写命令 |
这个划分不是随意的。工作区生命周期操作发生在目标仓库数据库尚不存在、或即将被删除的时候,因此不能用它;仓库变更有可用数据库,就用数据库。Plain 两者都没有,因为它的恢复单元是按包重新构建。
Workspace journal 保存操作类型、随机 64 位十六进制 id、仓库名,以及新旧 sow.yml 的原始字节与各自 SHA-256。工作区锁保证同时只有一条 active operation。sow.yml 的原子 rename 就是提交决策:如果当前 config 仍然哈希为旧值,就清理 planned journal 并回滚;如果哈希为新值,就幂等地补齐仓库外壳,或把自有对象移入 recovery。两边都不匹配则拒绝猜测。
Repository 操作日志 在任何公开文件副作用 之前 先向 SQLite 提交一条 planned Operation,随后记录每次状态迁移。它的 payload 绑定仓库、config SHA-256、精确的选中 Dist 集合、精确的 build_dists、--skip 决策,以及一个 manifest 哈希 —— 后者覆盖新对象事实、完整期望集、逐 Dist 策略结果、RPM 公钥证书快照与目标 Generation。
这不是 SQLite 的 WAL。WAL 负责 SQLite 自己的页面事务,它无法原子地协调 pool、staging 区与 dists/。跨数据库记录与 POSIX 文件动作的,是这套应用级操作日志。
操作生命周期
| 状态 | 已耐久的东西 |
|---|---|
planned |
命令、参数、目标与预期动作 |
staged |
新包与元数据已写入私有 staging 区并校验 |
applied |
期望状态与所需私有 pending payload 已提交;公开树可能仍是旧一代 |
built |
完整静态 Generation 已切换 |
done / done_dirty |
终态;作为审计记录保留 |
sow log <OPERATION> 展示带时间戳的状态迁移:
只有你显式给出 --skip 才可能走到 done_dirty。默认 add 如果在 applied 之后渲染失败,命令返回错误、旧 Built 视图继续服务、Operation 保持可恢复 —— 它不会悄悄地以 dirty 收尾。
在 applied 之前失败的 Operation 会成为 failed。这里有一处契约上的微妙之处:add 必须在解析包之前先记录 planned Operation,所以一个架构不被许可的包确实会留下审计记录。但除了那条终态 failed 记录之外,什么都不会被写入 —— 没有包对象、没有成员关系、没有 pending 字节、没有公开树变化、没有 Generation。既留住了审计线索,又保证无效架构不会进入任何产品投影。
锁模型
锁是本机的 POSIX advisory flock。产品契约是单机、单写、本地 POSIX、协作式锁 —— 网络文件系统既不检测也不支持。
| 锁 | 文件 | 谁持有 |
|---|---|---|
| 工作区锁 | .sow/workspace.lock |
init、repo new/rm、dist new/rm |
| 仓库锁 | .sow/repo-locks/<repo>.lock |
add、rm、build、dist new/rm、log prune |
| Plain 目录锁 | 目标目录及其稳定父目录 | sow create |
两把都需要时,顺序固定:先工作区,后仓库,释放顺序相反。仓库锁的 inode 位于稳定路径,绝不随私有状态目录移动 —— 这样删除仓库时可以在别的进程还持有旧描述符的情况下撤下锁路径,而不会有第二个写者在新 inode 上形成。
sow create 同时锁住目标目录 和 它稳定的父目录。父目录锁的作用是:阻止另一个协作写者用 rename 把目录整个换掉,再对替身取得一把独立的锁。
只读命令从不取写锁,也不接受锁参数。其中需要组合读取配置、SQLite 与 live 元数据的那几个(config check、repo ls/show、dist ls/show)会在整个快照期间持有共享锁。status 刻意更轻:它只探测仓库锁,以便在写入进行中报告 recovering 或 locked,而不会被它阻塞。
两个参数控制等待行为,适用于所有取写锁的命令:
| 参数 | 行为 |
|---|---|
-T, --timeout DUR |
最多等待 DUR;0(默认)一直等 |
-N, --no-wait |
只尝试一次,锁被占用立即失败 |
两条失败路径都以 4 退出。--no-wait 与非零 --timeout 同时出现是用法错误,退出码 2。
在"宁可跳过这轮、也不要堆积"的 cron 作业里用 -N;在"排一小会儿队可以、但绝不能挂死"的 CI 里用 -T 30s。
提交顺序
每一代都按同样的四个阶段写入,而顺序正是不变式成立的原因:
- payload —— 规范包字节写入
pool/。此时还没有任何东西引用它们。 - metadata —— checksum 命名的 RPM 元数据、
Packages、Packages.gz,以及 by-hash 索引副本。此时仍没有指针指向它们。 - pointer —— 客户端入口:RPM 的
repomd.xml(配置了签名则连同.asc);Managed APT 则在每个架构的 direct 与 by-hash 索引都就位之后,才发布Release(连同InRelease与Release.gpg)。这一步就是提交。 - delete —— 清理已过保留窗口的旧代元数据。
Pending 包体在单写者下分批提升,每次 group commit 最多 512 个对象或 1 GiB。SOW 先持久化 Pool 目录项,再删除 pending 名称;恢复因此能把 pending-only、指向同一 inode 的双链接或 Pool-only 状态重新绑定到 Operation,而不会冒同时丢失两个名称的风险。
正着读:包一定先于引用它的索引存在,索引一定先于指向它的指针存在。反着读:在一个不再引用某文件的指针耐久落地之前,那个文件不会被删。不存在任何一个窗口,让客户端沿活的指针走到一个不存在的文件。
这一切都通过与目标同文件系统的 staging 区完成,初始化时通过比较 st_dev 校验。挂载点或设备不同是明确失败,绝不降级为复制。文件先写入、fsync、由 SOW 自己的解析器与闭包校验器验证,之后才用原子 rename 换入。公开文件不继承你的 umask:repodata/ 是 0755,索引文件与指针是 0644。
sow changes 用于审计与交付规划,描述 Generation Delta;它不能替代发布协议。请使用
sow publish,或把完整树复制到离线 staging 后再原子切换上线。见
可观测与审计。
崩溃恢复
每条 Managed 写命令都先恢复,再做自己的事。 没有单独的修复命令,也没有守护进程盯着陈旧状态;恢复是变更的前置条件。只要存在非终态 Operation,下一条 add、rm、build、dist new/rm 或 log prune 就先把它做完或回滚,然后才继续。
全局恢复顺序是固定的:先在工作区锁下恢复工作区生命周期;如果那不是一次仓库删除,再按仓库名顺序、在各自稳定的仓库锁下恢复仓库 Operation。已经越过"删除仓库"提交决策的工作区操作具有支配权,并禁止任何嵌套的仓库恢复 —— 在一个正被删除的仓库内部恢复状态毫无意义。
恢复由证据驱动,不做乐观假设。每个阶段都有明确规则:
| 已到达的阶段 | 恢复规则 |
|---|---|
planned |
config 仍为旧值 → 回滚 stage;否则证据冲突,退出 5 |
staged |
config 仍为旧值 → 可回滚;config 已为新值 → 只允许前滚 |
applied |
新 config 已原子换入,这就是提交决策,因此一律前滚 |
built |
指针与目录已耐久,前滚提交数据库行 |
done |
数据库、config 与树同代;清理 stage,重复恢复是空操作 |
这套规则的验收方式是在多个不同时机向 sow add 发送 SIGKILL。每一次,status 都报告 recovering,下一条写命令都先恢复该 Operation 再执行自身,最终 check 全部层通过,公开树从未撕裂。
sow build 是唯一的显式前滚恢复入口:它在收敛之前,会先尝试完成或回滚任何可判定的非终态 Operation。看到 recovering 时,执行 sow build 就是标准反应。
error 专留给 journal、数据库与文件证据互相矛盾、任何自动选择都不安全的情况。此时 build 拒绝覆盖,最后完成的视图继续服务,你应当从备份恢复,再跑 check 与 build。这里刻意没有 repair --force —— 一个可能猜错的修复,比一个拒绝执行的修复更糟。
fail-closed 的路径安全
Managed 路径从不由用户提供的字符串拼装。每次创建、rename 和删除都走同一套流程:
- 把工作区根解析为绝对真实路径;
- 用固定相对片段重新构造目标,并验证相对路径不含任何逃逸分量;
- 对路径上每个已存在的受控组件执行
Lstat,拒绝符号链接和非预期文件类型; - 只删除已经先被原子移入
.sow/.../recovery的对象; - 删除前再次证明该 recovery 目标确实位于对应的私有状态目录内。
名称必须匹配 [a-z0-9][a-z0-9._-]*,.、..、.sow、pool、dists 及工作区保留名一律拒绝。
对文件句柄也是同样的姿态。SQLite 以 O_NOFOLLOW 打开并绑定普通文件 inode,连接建立后再按路径复核一次;数据库、WAL、shm 或 rollback journal 中任何一个是符号链接、非普通文件、有多个硬链接,或在打开期间被换绑,都会被拒绝。log export 拒绝覆盖已存在的文件,也拒绝父目录是符号链接的目标 —— 这就是为什么在 macOS 上往 /tmp 导出会失败:那里的 /tmp 本身是个符号链接。
各类 journal 都有大小上限:工作区 32 MiB、仓库 Operation payload 16 MiB,外置的 mutation manifest 与 base manifest 各 64 MiB。超限既不截断也不降级,而是在提交窗口之外直接失败 —— 这样写者永远不会产出一条"自己写得进去、恢复读者却永远读不回来"的 Operation 记录。
以上没有一条声称能抵御以同一用户身份运行、拥有无限权限的恶意进程。它抵御的是现实中的失败模式:崩溃、协作进程之间的竞态,以及在检查与使用之间形态发生变化的路径。
继续阅读
3.7 - 可观测与审计
每个读取表面回答不同问题。
| 命令 | 问题 | 写入? |
|---|---|---|
status |
Repository 当前是什么状态? | 否 |
check |
所选 Repository 是否满足完整交付契约? | 否 |
changes |
两个 Built Generation 之间哪些物理文件不同? | 否 |
log |
记录了哪些操作与处置结果? | 否 |
retain ls |
哪些 Generation 是显式本地 GC root? | 否 |
status:低成本状态
它报告 Desired revision、Built Generation、dirty Dist、pending 包体计数、锁状态与
ready_to_copy。它不哈希公共树,不恢复操作,也不构建。
| Repository 状态 | 含义 |
|---|---|
clean |
Desired 与 Built 一致 |
dirty |
Desired 已变化;公共树仍是上一份 Built Generation |
recovering |
存在持久非终态操作 |
error |
持久证据冲突,自动恢复无法安全决策 |
用 status 诊断,不要把它当成 check 的替代品。
check:交付证明
稳态下,checker 按顺序报告九层:
| 层 | 校验内容 |
|---|---|
config |
严格配置与有效 Dist 输入 |
state |
SQLite schema 与关系状态 |
public-modes |
公共文件/目录权限 |
retained |
显式保留 Generation 记录与冻结元数据 |
package-bytes |
pool/pending object 与已记录 SHA-256 |
desired-membership |
包身份、成员关系与架构一致性 |
index |
渲染元数据与引用 closure |
signature |
声明的元数据与 RPM 包信任要求 |
generation-manifest |
已记录 Built manifest 与公共树 |
未完成布局迁移使用更短的诊断表面:依次报告 config、state、public-modes 与
layout-transition,随后停止,并在诊断指定的维护操作完成或在 commit 前安全中止前返回不可交付。
check 不写入也不修复。dirty 或 recovering Repository 不可交付,即使上一份已提交树仍可读取。
在发布流水线中执行 check,任何非零退出都应停止。
每次 Check 都会对每个唯一物理包体执行一次权威 SHA-256,不会因缓存指纹匹配而省略;随后在
Retained Record、Index、Signature、最终 Generation Manifest 与 changes 之间共享基于描述符
的证据。带签名 RPM 只额外使用一遍所有 Signature Packet 与 Trust Ring 共用的签名流,不会随
Dist、Key 或 Retained Generation 数量放大。
changes:Generation 差异
- 不给 base:比较当前 Built Generation 与前一代;
- base
0:描述完整当前公共树; - base
N:给出已记录 GenerationN到当前 Built 的净差异。
每行包含操作、phase、Repository 相对路径、大小与 SHA-256。phase 使用与本地构建相同的 payload、metadata、pointer、delete 词汇。
changes 是 manifest/差异表面。它不连接目标、不持久化远端 checkpoint、不执行 cache grace,
也不恢复中断传输。配置好的 live target 应使用 sow publish TARGET。离线复制应先 stage
完整树、复验,再原子切换上线。
Generation 保留与 GC
retain add 校验并冻结 Generation 的元数据与引用集合,不复制另一棵包体树。保留记录是显式
GC root。retain rm 移除该 root,本身不删除包字节。
本地 sow gc 只删除已证明不被当前状态、显式 retention、active recovery/publication 状态及
其他记录根引用的包体。Target GC 是另一项操作:sow gc TARGET 使用该 Provider 的安全模型。
普通构建会携带紧邻前一代的 RPM 不可变元数据与 APT by-hash object,让已读取旧指针的客户端
完成下载。这个有界协议窗口与显式 retain 是两回事。
操作日志
日志按操作记录 kind/state、时间、配置/manifest identity、包处置、成员变化,以及适用时的 物理 changeset。
log export 输出稳定 JSONL,拒绝覆盖既有文件,并校验输出路径。log prune 接受日期或
RFC 3339 时间,只移除符合条件的终态审计记录;不会删除当前状态、恢复证据或仍被需要的
Generation manifest。
操作模式
status 用于监控,check 用于门禁,publish 用于目标变更,log 用于事后证据。
延伸阅读
4 - 参考
这一部分记录配置字段、包引用、路径、退出码、JSON、平台与集成等稳定契约。CLI 语法和状态变化 见命令,使用模型见上手。
输出示例只说明形态;标识符、路径、哈希、时间戳与计数会随工作区变化。二进制自带的
sow help 始终是精确语法权威。
完整配置 schema:工作区、仓库、Dist、成员策略、签名与发布目标。
命令行上指代一个软件包的五种写法、歧义如何裁决,以及 rm / show / where 各自接受哪些形态。
Plain 与 Managed 两种模式下 SOW 创建的每一条路径、包池分组规则、名称约束, 以及绝对不能通过 HTTP 暴露的目录。
七个退出码分别代表什么。
sow.cli/v1 Envelope、各顶层字段含义与主要命令族的 Result 形态。
Release 目标、文件系统要求、仓库客户端检查、发布 Provider,以及各项自动化集成的确切范围。
约定
命令示例不带 $ 提示符,方便整块复制。输出块只代表结构,可变值与长结构会在标注处省略。
二进制自带的 sow help 始终是精确语法权威。
语法块中占位符用大写(NAME、DIR、PACKAGE),字面量用小写。方括号表示可选参数,
... 表示可重复,竖线分隔互斥项 —— 与 sow help 的写法一致。
4.1 - sow.yml 配置参考
sow.yml 是 Managed 托管工作区(Workspace)唯一的配置文件。它位于工作区根目录,
声明存在哪些仓库(Repository)与 Dist,并保存每次构建都要执行的成员策略与签名设置。
Plain 平面模式(sow create)完全不读它。
本页列出解析器接受的每一个字段。没有列在这里的字段一律拒绝 —— 不存在未公开的 键,也没有为将来预留的键。
文件是怎么读的
SOW 用严格 YAML 解析器读取 sow.yml。具体表现是:
- 未知字段是错误,不是警告。 把
repos:写成repositories:会直接失败(退出码2), 并指出出问题的行号。 - 只允许一个 YAML 文档。 用
---引入第二个文档会报错。 - 只接受普通文件。
sow.yml是符号链接,或者大于 16 MiB,在解析前就被拒绝。 - 默认值在解析时补齐,不写回磁盘。 想看完全展开后的形态,用
sow config show --all。
文件的一部分是机器维护的:sow init、sow repo new、sow repo rm、sow dist new、
sow dist rm 会作为各自事务的一部分原子改写 sow.yml。成员策略与签名则由你手工编辑 ——
没有对应的命令行参数。
任何手工修改之后,跑一次 sow config check。它解析文件、与每个已初始化仓库的 SQLite
状态交叉核对、并解析每一个签名 key 引用,全程只读:
根级字段
| 字段 | 类型 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
schema |
string | 是 | — | 必须恰好是 sow/v3,其他值一律是配置错误。 |
architectures |
字符串列表 | 否 | [x86_64, aarch64] |
本工作区允许管理的 CPU 架构族。 |
repos |
map | 否 | 空 | 仓库名到仓库配置的映射。 |
targets |
map | 否 | 空 | 发布目标名到目标配置的映射。 |
schema 配置值必须恰好是 sow/v3。
architectures
这是 上限,不是目标。它声明 SOW 最多可以接纳哪些架构;各 Dist 默认继承整张表, 除非自己再收窄。
目前只支持两个规范族(canonical family):x86_64 与 aarch64。DEB 生态名作为输入别名
被接受,并在解析边界规范化:
| 你可以写 | 存储与展示为 |
|---|---|
x86_64、amd64 |
x86_64 |
aarch64、arm64 |
aarch64 |
所以 architectures: [amd64, arm64] 与 architectures: [x86_64, aarch64] 是同一份配置。
把同一族的两个别名都写上 —— [amd64, x86_64] —— 属于重复,会失败:
noarch(RPM)与 all(DEB)不是 这里的架构。它们是中性(neutral)包,构建时投影进
每个适用视图,解析器拒绝把它们写进这个列表。不支持的值(如 riscv64)立即失败:
这个列表可以整体省略,但不能写成空列表。
Repository 仓库
| 字段 | 类型 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
protected |
bool | 否 | false |
为真时 sow repo rm 拒绝删除该仓库,-f 也不行。 |
signing |
map | 否 | 无 | 包体与元数据签名设置,见签名。 |
dists |
map | 否 | 空 | Dist 名到 Dist 配置的映射。 |
protected
protected: true 是防止误删整个仓库的闸门,它只拦一件事 —— 仓库删除:
这是退出码 6。其余一切照常:add、rm、build、建/删 Dist 都不受影响。
要真的删掉一个 protected 仓库,先把 sow.yml 改成 protected: false,
用 sow config check 确认,再执行 sow repo rm。
名称约束
仓库名与 Dist 名共用一套文法:必须匹配 [a-z0-9][a-z0-9._-]* —— 小写字母、数字、
点、下划线、连字符,且以字母或数字开头。大写被拒绝,因为名称会变成目录名,
必须在大小写敏感的 Linux 与默认大小写不敏感的 macOS 文件系统上表现一致:
下列名称是保留名,一律拒绝:.、..、.sow、pool、dists、sow.yml、
workspace.lock、workspace-ops、repo-locks。两个会在状态目录里撞车的仓库名
(比如 db 与 db.db)也会被拒绝:
原因见仓库布局。
Dist
| 字段 | 类型 | 必填 | 默认值 | 含义 |
|---|---|---|---|---|
format |
string | 是 | — | rpm 或 deb。一个 Dist 只承载一种格式。 |
architectures |
字符串列表 | 否 | 继承工作区列表 | 把该 Dist 收窄到工作区架构的一个子集。 |
limit |
integer | 否 | 0 |
同一包名 + 架构最多保留几个版本;0 表示全留。 |
exclude |
规则列表 | 否 | 空 | 把命中的包挡在该 Dist 之外的规则。 |
format
format 是 sow dist new 唯一从命令行接受的业务参数,并且创建之后不可更改 ——
RPM Dist 永远不会变成 DEB Dist。格式不匹配的包根本不会成为该 Dist 的候选:
architectures
省略这个字段,Dist 继承工作区列表 —— 绝大多数情况下这就是你要的。
只有需要 收窄 时才声明:比如双架构工作区里,某个 el9 Dist 只做 x86。
列表必须是工作区列表的子集,且不能为空:
在这里新增一个架构族会让该 Dist 变为待构建(dirty),下一次 sow build 渲染新视图。
移除一个仍被现有成员关系或已构建代引用的族,config check 与所有写命令都会拒绝。
limit
limit 限定该 Dist 中同一个包保留几个版本。分组键是 (二进制包名, 原生架构),
所以同一个包的 x86_64 与 aarch64 构建各自计数,noarch/all 包自成一组。
0(默认)保留全部版本。N > 0保留最新的N个,RPM 按 EVR 比较,DEB 按 Debian version 规则比较。- 负数是配置错误:
limit: 1 时,把旧版本和新版本一起加入,旧版本会被报告为 limited 且不建立成员关系:
事后调大 limit 不会 复活曾被策略移出的版本。包体字节可能还留在包池里,
但成员关系已经没了;要拿回来就重新 add 一次。理由见成员策略。
exclude
exclude 是规则列表。每条规则是若干字段的集合:规则内字段之间是 AND,
同一字段的多个 pattern 之间是 OR,规则与规则之间是 OR。任一规则命中即排除。
读作:丢掉所有 debug 类包;另外,丢掉名字以 test- 开头或以 -experimental 结尾的
aarch64 包。
允许五个字段:
| 字段 | 匹配对象 |
|---|---|
name |
二进制包名 |
source |
规范化后的 source 名(RPM 取 SOURCERPM,DEB 取 Source) |
arch |
x86_64、aarch64 或 neutral |
kind |
见下表分类 |
format |
rpm 或 deb |
kind 由二进制包名的后缀决定,取最具体的一个:
| 格式 | 名称后缀 | kind |
|---|---|---|
| RPM | -debuginfo |
debuginfo |
| RPM | -debugsource |
debugsource |
| RPM | -llvmjit |
llvmjit |
| DEB | -dbgsym |
dbgsym |
| DEB | -dbg |
dbg |
| 任意 | 以上都不匹配 | main |
pattern 区分大小写,只有两种形态:精确字符串,或使用 *、?、[...] 的 shell glob。
不支持正则、版本比较、否定,也没有表达式语法。空规则、空或带首尾空白的 pattern、
同一字段内重复的 pattern、非法 glob,都是配置错误:
策略顺序固定:先 exclude,后 limit。被排除的包逐条报告,不算失败:
签名
签名配置挂在仓库级(不是 Dist 级),覆盖两条互相独立的信任链:软件包本身, 以及客户端在信任其他一切之前先验证的仓库元数据。
树形是固定的:signing.rpm 下有 packages 与 metadata;signing.deb 下 只有
metadata —— DEB 包体永远不会被重签,因为 APT 通过 Release 验证整个仓库,
而不是逐包签名。
rpm.packages
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
mode |
string | 无 key 时 never,有 key 时 fill |
never、fill 或 always。 |
key |
key 引用 | 无 | 当前签名身份;公钥用于验证,匹配私钥必须存在于 rpm 使用的 GPG 环境。mode 不是 never 时必填。 |
trusted_keys |
key 引用列表 | 空 | fill 额外认可的公钥。 |
三种模式:
never—— 原样保存输入字节。包进来时带什么签名(或没有签名),客户端拿到的就是什么。fill—— 对没有签名、或签名无法被key及trusted_keys验证的包补签; 已经能验证通过的包保持字节不变。always—— 最终每个包都必须由key有效签名。已经由该 key 签好的保持字节, 其余一律重签。
有 key 时默认是 fill,因为它是唯一保留上游签名的模式。设成 fill 或 always 却不给
key 是错误:
trusted_keys 列出哪些公钥的签名被 fill 视为"已经合格"。key 的公钥部分自动受信,
不需要重复列出。同一个引用写两次是错误:
RPM 包签名是唯一会调用外部程序的操作:SOW 对 私有 stage 副本 调用环境里的
rpm --addsign / rpm --resign,永远不碰你的输入文件。私钥必须已经存在于 rpm 使用的
GPG 环境中。
rpm.metadata 与 deb.metadata
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
key |
key 引用 | 无 | 给仓库元数据签名的私钥。 |
passphrase |
passphrase 引用 | 无 | 私钥有口令时使用。 |
配置 rpm.metadata.key,每个 RPM 架构视图会额外发布分离签名
repodata/repomd.xml.asc;配置 deb.metadata.key,每个 DEB Dist 会额外发布
clearsign 的 InRelease 与分离的 Release.gpg。没配 key 就不生成这些文件 ——
repomd.xml 与 Release 则始终会写。
file:// 与 env:// 引用由 SOW 在 进程内 完成签名,不需要 gpg 可执行文件。
只有 agent:// 需要环境里有 gpg。
改变 key 引用或它背后的 fingerprint 会让相关 Dist 变 dirty ——
签名身份是每个 Dist 已构建配置摘要的一部分。下一次 sow build 重新签名并推进新的代。
key 引用文法
key 引用是下列四种写法之一:
| 形态 | 例子 | 说明 |
|---|---|---|
| 路径 | keys/repo-signing.asc |
ASCII-armored key 文件。相对路径相对 工作区根目录 解析,不是当前目录。 |
file://<path> |
file:///secure/repo-signing.asc |
与上一行等价,只是显式写出。绝对路径因此是三个斜杠。 |
env://<VAR> |
env://SOW_METADATA_KEY |
环境变量里存的是 armored key 内容本身,不是路径。变量名须匹配 [A-Za-z_][A-Za-z0-9_]*。 |
agent://<fingerprint> |
agent://7F721C4AD40F...CF3B |
委托给环境里的 gpg-agent。fingerprint 为 16、40 或 64 位十六进制,不区分大小写。 |
其他 scheme 一律拒绝:
引用分两阶段校验。文法 在解析时检查,失败退出码 2;引用 能否解析出真实密钥
由 sow config check 和每条写命令检查,失败退出码 6:
秘密内容永远不会离开引用本身。sow config show --all 只显示引用与解析出的 fingerprint:
私钥与口令永远不会写进 sow.yml、SQLite、操作日志、JSON 输出或错误文本。
passphrase 引用
passphrase 接受与 key 引用相同的路径、file://、env:// 三种写法,
但 不接受 agent:// —— 口令是一个值,不是密钥句柄。
两条规则:
-
有 passphrase 没有 key 是错误,因为它没有可解锁的对象:
-
passphrase 与
agent://key 同时出现是错误。私钥由 agent 持有并自行处理口令交互, 第二条口令通道只会被忽略:
发布目标
每个目标把一个已配置 Repository 绑定到一个存储命名空间。目标名使用与仓库相同的小写文法。
| 字段 | 必填 | 含义 |
|---|---|---|
repository |
是 | 本目标拥有的现有 Repository。 |
provider |
是 | filesystem 或 r2。 |
endpoint |
是 | 无尾斜杠的规范 file:///absolute/path,或 R2 的规范 https://host。 |
region |
R2 | 必须是 auto;filesystem 禁用。 |
bucket |
R2 | 小写规范 bucket 名;filesystem 禁用。 |
prefix |
是 | 相对公共树前缀;空串表示存储命名空间根。 |
credential |
R2 | env://NAME 或 file:///absolute/path;禁止内联秘密。 |
public_endpoint |
是 | 以 / 结尾、用于公共内容/缺失校验的规范 URL。Filesystem 接受 https://、http:// 或 file://;R2 必须为 HTTP(S)。 |
max_cache_ttl |
是 | 有界、规范的非负 Go Duration,包括显式 0s;溢出会被拒绝。 |
authoritative_workspace |
是 | 必须为 true。 |
single_writer |
是 | 必须为 true。 |
exclusive_write_authority |
是 | 必须为 true。 |
三个 authority 布尔值是显式安全确认,不是默认值。同一存储上的目标前缀不能重叠; filesystem 目标也不能解析到重叠的有效路径。这些规则防止并发写者破坏条件式发布与 GC。
对 provider: filesystem,配置校验只检查 URL 形态与重叠关系。真正发布时,endpoint 目录
必须已经存在、不能是 symlink,并且必须解析为唯一规范真实目录。SOW 会在 endpoint 下创建
配置的 prefix,但不会创建 endpoint 本身。
首次 publish 会持久绑定 Repository、Provider Storage Identity 与 Prefix。之后修改 Target Name、
public_endpoint 或 max_cache_ttl,必须通过
sow publish TARGET --rebind 得到操作者显式确认。
Provider、Storage Endpoint、Region、Bucket、Prefix 与 Repository Identity 均不能 rebind;应配置
新目标。每次接受的 rebind 都会追加不可变 Binding Revision。Pending Maintenance 会阻止 TTL
变化;Filesystem Conditional-delete Maintenance 还会阻止公共端点变化。
R2 凭据是私有引用。环境变量值或被引用文件必须包含一份严格 JSON 文档,不能写路径或 Shell 赋值:
临时凭据可以增加可选的 "session_token":"..."。未知字段、尾随内容、缺少 Access/Secret,
以及超过 64 KiB 的文档都会被拒绝。config show、JSON 输出与公共树不会包含凭据材料。
完整示例
一个工作区,两个仓库:一个受保护的生产仓库(两条元数据签名链 + RPM 补签), 一个不签名、不过滤的临时仓库。
用之前先验证:
sow.yml 里没有什么
有些你可能以为能配的东西,是 有意 不做成配置项的:
- 仓库路径。 仓库永远位于
<workspace>/<name>,没有path:字段。 见仓库布局。 - APT component。 固定为
main;YUM 没有 component 概念。 - 架构视图。 由
architectures与包头共同推导,不能逐包声明。 - 内联秘密。 目标只接受 credential 引用;key 与 passphrase 材料同样留在引用背后。
- 自动保留数量。 保留是显式
sow retain add/rm操作,不是配置中的滚动计数。
延伸阅读
4.2 - 包引用
sow rm、sow show、sow where 都接受一个 PACKAGE 参数。本页定义你能在那里写什么。
三条命令共用同一套文法,只有对 歧义名称 的处理不同。
这里的内容与 sow add 无关 —— add 接受的是文件系统路径,不是包引用。
五种形态
| 形态 | 例子 | 匹配 |
|---|---|---|
| 内容摘要 | sha256:d06d7f23b9cf...b98b1229 |
恰好一个包对象 |
| RPM 坐标 | rpm:pev2-0:1.23.0-1.noarch |
恰好一个 RPM |
| DEB 坐标 | deb:libpq5=18.3-1.pgdg12+1:amd64 |
恰好一个 DEB |
| 完整文件名 | pev2-1.23.0-1.noarch.rpm |
以该文件名存储的包 |
| 裸包名 | pev2 |
该名称的全部版本与架构 |
前三种是 精确 引用:指名道姓,要么命中要么失败。后两种是便利写法,可能匹配多个对象。
你不需要手工拼这些字符串。sow ls 会直接打印每个包的摘要与坐标,可以原样粘回命令行:
内容摘要
已存储包体完整字节的 SHA-256。这是 SOW 里最强的引用形式:它就是对象身份,不可能有歧义。
摘要必须完整且小写。不支持 前缀匹配,也不做大小写折叠 —— 位数不足或大写都属于用法拒绝, 不是"没找到":
注意这个摘要覆盖的是 已存储 的字节。如果仓库对 RPM 包体做了重签,
对象摘要与你交给 sow add 的那个文件的摘要就不一样了。
RPM 坐标
完整 NEVRA,加 rpm: 前缀。每一段都必填,包括 epoch —— 包本身没有 epoch 时写 0。
在 shell 里请加引号:NEVRA 含冒号,否则可能被历史展开或路径补全改写。
前缀与 epoch 都是必需的。少任何一个,这串字符就会被当成裸名解析,从而什么都找不到:
架构那一段取自 RPM 包头:x86_64、aarch64 或 noarch。它 不是 规范族名 ——
noarch 包这里就写 noarch,尽管 SOW 内部把它归类为 neutral(中性)。
DEB 坐标
Debian 身份三元组,加 deb: 前缀。版本是含 epoch 与 revision 的完整 Debian 版本号;
架构是生态名(amd64、arm64、all),不是规范族名。
三段都必填。deb:libpq5=18.3-1.pgdg12+1 不带架构,匹配不到任何东西。
完整文件名
包存储时的完整文件名,含扩展名:
看着目录列表操作时,这是最好敲的写法。但它 不是身份 —— SOW 不用文件名区分包, 理论上两个不同对象可以叫同一个名字。脚本里请优先用坐标或摘要。
裸包名
只写二进制包名:
它的含义取决于命令:
-
sow rm把它理解为所选 Dist 中该名称的 全部 版本与原生架构。这是有意设计的 —— 下架一个包通常意味着全部下架。先用-c预览: -
sow show与sow where要求它唯一命中。这两条命令描述的是单个包, 名称匹配多个时会连同候选列表一起拒绝:每个候选都同时给出坐标与摘要,所以修正方式就是把其中一条粘回命令行。
哪些写法不成立
不带 rpm: 前缀的 NEVRA 看起来像坐标,实际会被当作裸名解析,
而裸名里不含 epoch 和架构:
另外,这里没有 glob、没有正则、没有版本区间,也没有 --all 参数。
如果你想按模式 筛选 一批包,那是 sow.yml 里的成员策略,
不是命令行选择器。命令行永远只用来指代 已经存在 的包。
作用域
引用总是在某个作用域内解析,而作用域由常规的选择参数决定,与引用写法无关:
| 命令 | 默认作用域 | 收窄方式 |
|---|---|---|
sow rm |
所选仓库的所选 Dist | -r、-d(存在多个时必填) |
sow show |
所选仓库 | -r、-d |
sow where |
工作区内全部仓库 | -r、-d |
sow where 是那条"广搜"命令 —— 当你知道某个包在某处、但不知道在哪个仓库时用它。
sow show 则是在一个仓库内把一个对象的细节全部展开。
两条命令没找到时的措辞也不同,可以据此判断自己跑的是哪一条:
坐标与身份
上面的坐标形态是包的 逻辑身份。SOW 强制约束:一个仓库内,一个坐标最多对应一个内容对象。
用已存在的坐标加入一个 不同 的文件是硬冲突 —— SOW 不会悄悄挑一个赢家,也没有 --replace。
因此,两个只有签名不同的包仍然会冲突,因为它们坐标相同。如果你真的要重签发布,
请提高 release 号;如果只是把同一个输入再加一次,SOW 会识别出来并报告 reused。
延伸阅读
4.3 - 仓库布局
SOW 的 Managed 布局只有一种:软件包体在 pool/ 下只存一份,dists/
只保存客户端视图元数据。对外服务、复制或发布时,单位始终是完整仓库目录。
Plain 模式
sow create 在现有软件包旁写入索引,不修改无关文件:
平面 RPM 元数据引用裸文件名,平面 DEB 元数据使用 ./<filename>。构建期间
.sow-plain-stage-* 保存私有生成输出。Plain 没有持久 journal 或 recovery 状态;下次
create 会丢弃保留命名空间里的陈旧临时路径并重建。不得服务或复制这些临时路径。
Managed 工作区
去重不跨仓库边界。.sow/ 与 pending 目录权限为 0700。Pending 包体文件直接使用最终
公开权限 0644,因此提升只需修改命名空间。私有状态
可能包含尚未发布的包体、从凭据派生的状态与恢复数据。
<repo>.db 与可重建的软件包事实缓存都属于私有状态,不改变公共仓库布局或
sow/v3 配置标识。
SOW 0.4 使用内部数据库 Schema v12。Append-only Publication-target Binding Revision、Package
Facts、Signer Projection 与 Recovery Evidence 均只存在于 <repo>.db。v0.3 数据库必须先备份,
再通过 sow repo migrate 升级;绝不要手工修改 PRAGMA user_version,也不要脱离匹配的公共/私有
Repository 状态单独复制数据库。
规范包池
每个包体只有一条规范路径:
源码名取自 RPM SOURCERPM 或 DEB Source;缺失时回落到二进制包名。
前缀是首个小写字符,以 lib 开头时取前四个字符:
| Source | 示例 |
|---|---|
postgresql-18 |
pool/p/postgresql-18/libpq5_18.3-1.pgdg12+1_amd64.deb |
blackbox_exporter |
pool/b/blackbox_exporter/blackbox_exporter-0.28.0-1.x86_64.rpm |
libfoo |
pool/libf/libfoo/libfoo1_1.0-1_amd64.deb |
Pool 对象不可变。从 Dist 删除成员关系不会立即删除字节;sow gc 只有在检查当前、保留、
恢复、发布以及活动维护操作等全部安全根后,才会处理不可达包体。
RPM 纯元数据视图
这里没有 dists/<dist>/<arch>/pool/。原生包只出现在匹配架构的元数据中,noarch
出现在每个架构视图中。rpm-md 回指规范包池:
该布局要求客户端在完整 Repository Root 内正确处理 rpm-md 相对路径。默认 dnf reposync
会拒绝父级跳转 href,因为下载目标逃出
View Root。下游工具需要自包含 Leaf 时,请显式导出:
导出目录有自己的 pool/ 和改写后的 href;它是兼容性产物,不是规范 Managed 仓库。
DEB 视图
Packages 从 archive 根引用同一规范包池:
Release 使用 SHA256 清单并声明 Acquire-By-Hash: yes。校验和命名的 rpm-md 文件与
APT by-hash 条目让上一组元数据在可变指针最后替换时仍然可达。
发布目标
filesystem 与 r2 目标都会在配置前缀下得到同一棵逻辑公共树:
发布单位始终是完整仓库命名空间。不要只发布某一个 RPM 架构目录,它的 href 会有意回指 根包池。
名称与服务边界
仓库名与 Dist 名必须匹配 [a-z0-9][a-z0-9._-]*。.、..、.sow、pool、
dists、sow.yml、workspace.lock、workspace-ops 与 repo-locks 在相应位置为
保留名。SOW 会拒绝大小写不敏感的池路径冲突,使产物能在 Linux 与默认 macOS 文件系统间迁移。
Web 服务器应指向 <workspace>/<repo>/,而不是工作区根目录。公共仓库需要同时包含
pool/ 与 dists/;私有 .sow/ 必须隐藏。
延伸阅读
4.4 - 退出码
每条 sow 命令都以七个退出码之一结束。它们是稳定的、对所有命令一致,
并且就是设计来给脚本做分支判断的 —— 区分"这件事失败了"和"这件事被正确地拒绝了",
正是设置多个非零码的全部意义。
| 码 | 含义 |
|---|---|
0 |
完整成功,或幂等 no-op |
1 |
运行时 I/O、解析器、渲染器或未知内部错误 |
2 |
用法、工作区发现或配置错误 |
3 |
部分成功:至少一项已提交,至少一项失败 |
4 |
写锁不可用 —— 被占用且指定了 --no-wait,或等待超时 |
5 |
完整性/恢复错误,或 check 判定当前结果不可交付 |
6 |
预期拒绝:冲突、protected、无匹配、架构不兼容 |
人类可读的结果写 stdout,警告与诊断写 stderr。每个码在 stderr 上有稳定的消息前缀,
在 JSON 输出中有对应的 class:
| 码 | stderr 前缀 | JSON class |
|---|---|---|
1 |
随子系统而异 | runtime |
2 |
usage error: / workspace discovery error: / configuration error: |
usage |
3 |
... batch partially succeeded |
partial |
4 |
lock unavailable: |
lock |
5 |
integrity or recovery error: |
integrity |
6 |
operation rejected: |
rejected |
sow create 是前缀那一列的例外:它不在 Managed 层内,stderr 上打印的是原始领域错误
(plain: scan …、plain: marker gate …),没有 operation rejected: 前缀。该前缀仍然出现在
它的 JSON errors[].message 里。
0 —— 成功或无操作
命令完成了你要求的事,或者发现无事可做。两者都算成功:对未变化的目录重跑
sow create,或对 clean 仓库执行 sow build,都返回 0 并如实说明。
"noop":true 才是区分"没干活"与"干了活"的依据,退出码不区分这两者。
sow status 是有意为之的特例:只要状态数据库可读,它在 clean、dirty、
recovering、error 四种状态下都返回 0 —— 让脚本去读结构化状态,
而不是从退出码反推。需要"闸门"时请用 sow check。
1 —— 运行时错误
I/O、解析或渲染层面出了问题:目录不可写、磁盘满、包读不出来。 这类是 环境问题,不是用法问题。
stage 目录之所以一开始就创建,正是为了让这类失败发生在 任何东西被发布之前。 本来就有合法索引的仓库,索引依然完好。
2 —— 用法、发现或配置错误
你要求的事情 CLI 无法执行:未知参数、目标不明确、找不到工作区,或 sow.yml 解析不通过。
什么都没有尝试执行。
未知参数:
互斥参数:
目标不明确 —— 仓库有两个 Dist,而命令需要确定其一:
当前目录向上找不到任何工作区 —— 错误会说明搜索位置与修复方式:
起始目录本身不是真实目录(比如符号链接,macOS 上的 /tmp 就是)时,搜索根本不会开始:
配置文件格式有问题 —— 注意错误会指出具体行号:
sow.yml 的所有文法与 schema 错误都归到这一码。
3 —— 部分成功
一个批次里有的项已提交、有的项失败。这个码存在的意义是:你永远不必猜测一次失败的
sow add 是否让仓库毫发无损 —— 返回 3 就意味着合法的包 已经进去了,
失败的那些会被逐条点名。
失败的输入文件原地不动。加上 --json 时,已提交的项仍然完整列出 ——
非零退出 从不 截断 result:
sow init 在已提交了部分声明的仓库或 Dist、随后在后面某项上失败时,也用这个码。
4 —— 锁不可用
另一个进程持有写锁。SOW 在设计上就是单写者(single-writer), 所以这是 正常且预期 的结果 —— 重试,或者多等一会儿。
带 --no-wait 时立即失败:
带超时时,恰好等待这么久后失败:
-T 0(默认)一直等待。只读命令不取写锁,永远不会返回 4;
sow status 甚至把这种争用作为一个字段报告出来:
5 —— 完整性、恢复,或不可交付
两种不同的情况共用这个码,它们的含义都是"先别把这棵树发出去"。
常见的那种:仓库的期望状态领先于已构建的内容 —— sow add --skip 之后,
或者改了策略/签名之后,对它执行 sow check。每一层校验都通过,
仓库只是 尚未收敛:
解决办法是 sow build。这正是部署脚本应该拿来做闸门的码 ——
它区分的是"磁盘上的树完整且最新"与"磁盘上的树完整但过期"。
少见的那种是真正的完整性失败:状态数据库、journal 与文件树互相矛盾,
且 SOW 无法安全地自行裁决。此时它拒绝覆盖任何东西,你应该从备份恢复,而不是强行修复。
这里 有意 没有 --force。
6 —— 预期拒绝
命令写法正确、环境也没问题,是 SOW 主动判定"不行"。 这些是策略与安全决策,不是故障。
受保护的仓库:
匹配不到任何东西的引用:
有歧义的裸名 —— 候选会一并列出,方便你挑一个:
工作区不允许的架构。注意逐项错误会点名检测到的值,并告诉你去哪里改:
目录里没有任何可索引的包:
--pigsty 完成标记挡住了对既有构建的覆盖:
签名 key 引用文法正确但解析不出密钥 —— 文法错误是 2,解析失败是 6:
在脚本里使用
这些码的设计目标就是让部署流水线 不必解析文本 即可分支:
这里的 mirror 是 pigsty 已配置的 Publication Target。
两个值得养成的习惯:把 4 当作 可重试 而不是致命错误;
永远不要把 6 当作崩溃 —— 它通常意味着需要改的是你的输入,而不是 SOW。
延伸阅读
4.5 - JSON 输出
所有产出数据的命令都接受 --json。输出是 stdout 上的 一行 版本化信封 ——
不管是哪条命令产生的,都能直接管道给 jq。
(此处为便于阅读做了折行,实际输出是一行。)
信封结构
| 字段 | 类型 | 含义 |
|---|---|---|
schema |
string | 恒为 sow.cli/v1。解析其他字段前先检查它。 |
command |
string | 实际调用的命令,含子命令:add、repo ls、config show。 |
ok |
bool | errors 为空时为 true,等价于退出码 0。 |
repository |
string 或 null | 选定的仓库;工作区级与 Plain 模式命令为 null。 |
operation |
string 或 null | 写命令的 Operation ID,只读命令为 null。 |
result |
object 或 null | 命令专属载荷,详见下文。 |
errors |
array | 零个或多个 {code, class, message} 对象。 |
七个字段 永远存在。命令在有意义工作之前失败时(例如未知参数、发现失败,或尚未选中
Repository 就遇到无效配置),result 为 null。已提交的部分结果,以及 check、
rm --check 的诊断结果,即使命令非零退出也会保留。
errors
| 字段 | 含义 |
|---|---|
code |
进程退出码 —— 1 到 6。 |
class |
runtime、usage、discovery、config、partial、lock、integrity、rejected 之一。discovery 与 config 是退出码 2 的稳定细分类。 |
message |
与写到 stderr 的文本相同。 |
请对 class 做分支判断,不要匹配 message 文本。message 里含路径和包名,会变;class 不会。
批次部分成功时,ok 是 false,同时 result 会完整列出已提交的内容。
不要因为退出码非零就丢掉载荷 —— 对 add 来说,那正是你了解"哪些包落地了"的唯一途径。
Operation ID 是字符串
Operation ID 是 64 位值,序列化为十进制 字符串,因为它经常超出 IEEE 754 双精度
能精确表示的范围。在 JavaScript 里,JSON.parse 处理裸数字会静默损坏它们。
请保持字符串形态;jq 原样处理即可。
Generation ID 是固定宽度字符串
Generation ID 覆盖完整的无符号 64 位范围,并固定序列化为 20 位、左侧补零的十进制字符串。
generation、built_generation、base_generation 以及表示 Generation 的 base 字段都应
按字符串处理;固定宽度也能保持普通字节序比较与数值顺序一致。
stdout 与 stderr
结果和 JSON 信封写 stdout;警告与错误诊断写 stderr,同时 也出现在 errors 数组里。
所以这样写是可行的:
各命令的 result 形态
create
| 字段 | 含义 |
|---|---|
dir |
被索引的绝对目录 |
rpm、deb |
各格式的包数量 |
kept |
进入索引的文件名,已排序 |
removed |
被 --pigsty 清理删除的包;否则为空 |
marker |
是否写入了 repo_complete |
marker_sha256 |
marker 文件的摘要;仅 --pigsty 时出现 |
noop |
索引本来就正确、什么都没改时为 true |
recovered |
为兼容稳定 schema 保留;Plain create 没有 journal 恢复,始终为 false |
signed |
被重签的文件名;仅 --sign-with 时出现 |
init
对已存在的工作区重跑时,计数为 0,existing 说明找到了什么:
config check 与 config show
config show 返回有效配置本身,形态与规范化之后的
sow.yml 一致:
带 --all 时,签名条目会额外携带 key_fingerprint。私钥内容与口令永不出现。
repo ls / repo new / repo show
repo ls 返回数组;repo new 与 repo show 返回同一形态的单个对象。
packages 统计包池中不同的包对象数;memberships 统计 Dist 成员关系数 ——
同一个包出现在两个 Dist 里,前者计一次,后者计两次。
repo rm 只返回结果:
dist ls / dist new / dist show
每个架构条目同时给出两个名字:family 是配置里用的规范名,
ecosystem_arch 是发布树里出现的名字 —— RPM 两者相同,DEB 分别是 amd64/arm64。
desired_members 大于 built_members,或 dirty: true,都表示还欠一次 sow build。
effective_config_sha256 是所有喂给渲染器的输入的摘要;它一变,该 Dist 就变 dirty。
dist rm 与 repo rm 一致:{"name":"el9","noop":false,"removed":true}。
add
每个输入路径对应一条 items,顺序稳定。status 是该项的总体结果,
dists 给出 逐 Dist 的裁决:
status |
含义 |
|---|---|
accepted |
新包对象,已建立成员关系 |
reused |
完全相同的对象已存在;可能仍为其他 Dist 新增成员关系 |
excluded |
被策略挡下 —— 看 dists 区分是 excluded 还是 limited |
failed |
未被接纳;error 给出原因 |
逐 Dist 的取值是 accepted、excluded、limited。同一条命令里,
一个包可以被某个 Dist 接受、被另一个 Dist 限流:
失败项携带 error,不带包字段:
memberships_added 与 memberships_removed 双向计数,因为 limit 可能在接纳新版本的
同一个操作里淘汰旧版本。
rm
带 -c/--check 运行时 check 为 true,此时 什么都没写,changes 是一份预测。
注意 removed 只列出成员关系的移除 —— rm 永远不删除包池字节。
build
noop: true 表示期望状态已经与已构建的树一致,没有产生新的代。
dists 列的是被纳入考量的 Dist,不一定是真正重建了的那些。
status
status 取值为 clean、dirty、recovering、error。部署脚本该读的字段是
ready_to_copy —— 但记住 status 在任何状态下都返回 0,所以要判断 字段,不是退出码:
pending 统计 add --skip 之后私有保存、尚未发布的包体。
repository_locked 报告当前是否有其他进程持有写锁。
check
稳态 Check 按固定顺序返回九层,每层给出检查项数与问题。未完成布局迁移则只返回 config、
state、public-modes 与 layout-transition,随后停止并返回不可交付。dirty 仓库可以让全部稳态层
ok: true,但仍以退出码 5 失败 —— 因为层校验的是 自洽性,
而 ready_to_copy 报告的是 时效性:
changes
| 字段 | 取值 |
|---|---|
op |
add、update、delete |
phase |
payload、metadata、pointer、delete |
path |
永远相对仓库根,永远用 / 分隔 |
size、sha256 |
add 与 update 有;delete 没有 |
按这个 phase 顺序施加变更,客户端永远不会取到悬空引用:先包体,
再校验和命名的元数据,然后是协议指针(repomd.xml、Release),最后才删除被取代的文件。
sow changes 0 把当前整棵树作为一个 add 集合给出 —— 也就是一份完整交付清单。
ls / show / where
ls 返回包对象数组;show 在 package 下返回恰好一个。
值得关注的字段:
| 字段 | 含义 |
|---|---|
architecture |
包头里的原始写法:x86_64、noarch、amd64、all |
canonical_arch |
SOW 用于分组的族:x86_64、aarch64 或 neutral |
payload_sha256 |
仅 RPM —— 签名无关摘要,用于识别同一包的重签副本 |
signature_key |
包内嵌签名的 key ID(包带签名时) |
storage |
已发布为 pool,--skip 加入的为 pending |
dists / built_dists |
期望成员集 与 上一次构建实际发布的集合 |
dists 比 built_dists 长,是判断"还欠一次 build"的另一种方式。
where 搜索整个工作区,返回位置而不是完整对象:
publish、retain、gc、export
Managed 生命周期命令使用同一 envelope,数字 Generation 仍序列化为 JSON 字符串:
| 命令 | 重要 result 字段 |
|---|---|
publish |
repository、target、provider、generation、attempt、checkpoint、phase、objects、noop |
publish --abort |
repository、target、provider、attempt、phase、objects |
publish --rebind |
与 publish 相同;Binding Revision 属于持久私有审计状态,不新增 Wire Field |
retain add / retain rm |
repository、record、record_identity、path |
retain ls |
repository、generations[],元素使用同一 retained record 形态 |
本地 gc |
operation、repository、base_generation、generation、objects、bytes、noop |
目标 gc |
repository、target、provider、phase、reports、candidates、deleted_objects、deleted_bytes、retained_objects、pending_grace、completed_attempts、noop |
export rpm-leaf |
repository、repository_id、generation、dist、arch、directory、method、signed、signer_identity、packages、files、manifest_sha256 |
attempt、checkpoint 或本地 GC operation 等可选 identity 没有值时直接省略。
R2 目标 GC 会把候选计入 retained,SOW 从不报告自己执行了远端删除。
log
sow log 返回操作账本,由新到旧:
payload_json 与 result_json 是 内含 JSON 的字符串,不是对象。
它们原样保存以保证审计记录字节稳定;需要二次解析:
传入 Operation ID 会返回完整细节 —— 状态迁移、结构化 build_progress 事件、包、成员关系与每一个文件动作:
sow log prune 返回它清理了什么:
注意 before 会回显裸日期在本地时区解析出的绝对时间戳。
log export 不是信封
sow log export 输出 JSON Lines —— 每行一条完整的 Operation 记录,
没有信封,也没有 --json 参数。它是给归档用的,不是给单条命令脚本用的:
它拒绝覆盖已存在的文件,也拒绝父目录是符号链接的目标。
一个完整例子
仓库既自洽又最新时才允许部署,然后列出该复制哪些文件:
延伸阅读
4.6 - 平台与集成
本页说明 SOW 提供哪些构建目标、工作区依赖什么存储语义,以及自动化集成具体覆盖哪些行为。 仓库生成在 SOW 二进制内部完成;部署后的最终门禁仍是实际软件包管理器。
Release 目标
| 操作系统 | amd64 |
arm64 |
制品 |
|---|---|---|---|
| Linux | 是 | 是 | 归档、RPM、DEB |
| macOS | 是 | 是 | 归档 |
| Windows | 否 | 否 | 不支持 |
Release 二进制使用 CGO_ENABLED=0,不需要语言运行时。0.4.0 制品使用 Go 1.27.0 构建;
源码 Module 要求 Go 1.27.0 或更新版本。归档包含 README.md、CHANGELOG.md 与 Apache-2.0
LICENSE,Linux 软件包会随二进制安装同一份
协议文件。使用 sow version 查看产品版本、目标 OS/架构与构建工具链。
工作区文件系统
Managed 工作区应放在本地 POSIX 文件系统上。正确性依赖建议锁、fsync、基于描述符的路径
校验与同文件系统原子 rename;NFS 等网络文件系统不属于受支持的工作区位置。
公共 <workspace>/<repo>/ 树是另一条边界:它是闭合的 pool/ + dists/ 命名空间,可整根
复制或发布,不依赖 SQLite、私有 journal 或 view-local hardlink identity。必须保持完整
Repository,不得暴露 .sow/。
SOW 会拒绝符号链接控制路径、不安全普通文件、重叠 filesystem target,以及大小写折叠后冲突 的 Pool 路径,使同一个 Repository 可以在大小写敏感的 Linux 与默认大小写不敏感的 macOS 文件系统间移动。
自动化集成矩阵
| 表面 | 环境 | 已验证行为 |
|---|---|---|
| 生产 CLI 干净环境 | Linux CI | 构建交付二进制;生成混合 Plain RPM/DEB 元数据;初始化 sow/v3;创建 RPM/DEB Dist;加入 Fixture;执行查询、build、check、changes、config 与 log 命令 |
| Plain APT 客户端 | Ubuntu 22.04 容器 | 通过 HTTP 服务 sow create 输出;在显式信任未签名源时执行 apt-get update、包发现、精确版本选择、下载与安装 |
| RPM 分离签名切换 | AlmaLinux 8、9、10 容器 | 使用真实 DNF 客户端遍历 repomd.xml / repomd.xml.asc 串行切换状态,并固定各组合的成功/失败行为 |
| S3 兼容传输 | 固定 MinIO 容器 | 验证 Bucket 列表、HEAD、GET、仅创建/CAS 写入、重放、条件式 Multipart Upload、对象元数据、重试与 Prefix 约束 |
| Release 打包 | Linux CI | 构建四个归档、两个 RPM、两个 DEB 与 SHA256SUMS;检查包内路径、Apache-2.0 元数据与协议文件字节 |
DNF 签名切换是协议测试,不是完整 Managed RPM 安装;APT 作业覆盖未签名 Plain 仓库,不覆盖 Managed 元数据签名。正式上线前,应使用部署中的确切 dnf/APT 版本、仓库 URL、访问策略与 签名策略完成验收。
仓库客户端契约
Plain RPM 仓库在包文件旁提供 repodata/;Plain DEB 仓库在包文件旁提供 Packages 与
Packages.gz。配置好客户端信任策略后,可通过 file:// 或 HTTP 使用。
Managed 客户端必须消费完整 Repository Root:
- APT 索引位于
dists/<dist>/main/binary-<arch>/,并引用根级pool/。Release声明 SHA-256 by-hash 索引;配置签名后增加InRelease与Release.gpg。 - RPM 元数据位于
dists/<dist>/<arch>/repodata/,通过相对位置回指根级pool/。必须服务 整个 Repository,不能只发布一个架构目录。
默认 dnf reposync 会拒绝规范 Managed RPM 的父级相对包路径。该工作流应使用
sow export rpm-leaf 生成自包含副本。导出使用本地包路径并带有
完成清单,但不是第二个规范 Repository。
发布 Provider
| Provider | 契约 |
|---|---|
filesystem |
发布到预先存在且安全的 file:// endpoint 下。Target GC 只有在缓存 grace 与存储/公共缺失证据成立后,才执行精确条件删除。 |
r2 |
通过 S3 兼容存储传输发布。Target GC 只写入精确候选报告,绝不删除远端对象。 |
两种 Provider 都在配置 Prefix 下发布同一棵完整 pool/ + dists/ 树。public_endpoint 属于
Target 校验的一部分;SOW 不创建 HTTP 服务、DNS 记录、Bucket Policy、CDN 或凭据。启用生产
发布前,应先在非生产 Prefix 验证这些由部署方负责的表面。
Filesystem 与 R2 的 HTTP(S) 端点共用 canonical-GET 内容 verifier;Filesystem 还可使用基于
描述符的 file:// 校验,R2 公共端点则必须为 HTTP(S)。Target Name、public_endpoint 与
max_cache_ttl 只能通过显式 publish --rebind
修改;Storage Identity 与 Prefix 不可变。
部署门禁
交付前必须通过深度校验,并检查物理变更计划:
发布后,再访问实际 repomd.xml 或 Release URL,并运行目标软件包管理器。本地构建、Provider
写入、HTTP 可达与客户端安装是四个独立检查。
5 - 命令
每条顶层命令单独成页;config、repo、dist、retain、
export、log 等命令组在同一页说明其子命令。
二进制内置的 sow help 是语法权威。本手册在此基础上补充选择规则、状态变化、输出契约、
失败行为与可直接使用的示例。
命令索引
sow create 是 Plain 模式的仓库命令,直接作用于目录。sow init 用于启动 Managed 模式,
必要时会创建 sow.yml;其余有状态命令发现既有工作区。help 与 version 是工具命令,
不需要进入任何模式。
| 命令 | 模式 | 用途 |
|---|---|---|
sow create [DIR] |
Plain | 就地生成平面 RPM/DEB 仓库 |
sow init [DIR] |
Managed | 初始化工作区并收敛已声明的 Repository/Dist |
sow config check|show |
Managed | 校验配置或打印有效配置 |
sow repo ls|new|show|migrate|rm |
Managed | 管理 Repository;migrate 是专用维护命令 |
sow dist ls|new|show|rm |
Managed | 管理 Dist |
sow add PATH... |
Managed | 将软件包加入期望成员集 |
sow rm PACKAGE... |
Managed | 从期望成员集中移除软件包 |
sow ls |
Managed | 列出期望成员与已构建成员 |
sow show PACKAGE |
Managed | 查看一个 Package Object |
sow where PACKAGE |
Managed | 在整个工作区定位 Package Object |
sow status |
Managed | 快速读取 Repository 状态 |
sow build |
Managed | 将 Desired 状态收敛为 Built Generation |
sow check |
Managed | 校验配置、状态、包体、视图、签名与清单 |
sow changes [BASE_GENERATION] |
Managed | 将 Generation 差异输出为文件交付计划 |
sow publish TARGET |
Managed | 将已验证 Generation 发布到配置目标 |
sow retain add|ls|rm |
Managed | 管理显式保留的 Generation 根 |
sow gc [TARGET] |
Managed | 回收本地不可达包体,或维护发布目标 |
sow export rpm-leaf |
Managed | 生成独立的 RPM 兼容 leaf |
sow log [OPERATION] |
Managed | 查询、导出与裁剪 Operation 审计账本 |
全局语法
不带参数运行 sow 会打印命令列表并退出 0。用 sow help COMMAND 或
sow help COMMAND SUBCOMMAND 查看内置帮助。sow version 与 sow --version 打印二进制身份。
SOW 没有全局 --format、--yes、--dry-run、-q、-v 或 --config。未知参数直接按
用法错误处理。
工作区发现
Managed 命令按以下规则寻找最近的 sow.yml:
- 有
-C/--workdir DIR时从DIR开始,否则从当前目录开始。 - 逐级向上查找,在第一个
sow.yml停止。 - 首次查找失败且设置了
SOW_DIR时,再从该目录查找。显式-C会取代当前目录候选, 但不会禁用SOW_DIR回退。 - 仍未发现工作区则退出
2。
--workdir 只改变发现起点,不会切换进程工作目录;相对位置参数仍相对于真实当前目录解析。
sow create 完全不参与工作区发现。
Repository 选择
需要唯一 Repository 的命令按以下顺序选择:
- 显式
-r/--repo NAME; - 发现起点所在的 Repository;
- 工作区中唯一的 Repository;
- 否则退出
2并列出候选项。
repo new 与 repo rm 用位置参数接收 NAME,不接受 -r。sow where 默认搜索所有
Repository,-r 只用于收窄范围。发布目标自身绑定 Repository,因此 publish TARGET 与
gc TARGET 不再接受额外的 Repository 选择。
Dist 选择
add、rm、ls 要求明确的 Dist 集合,并按以下顺序选择:
- 一个或多个
-d/--dist NAME; - 发现起点所在的 Dist;
- 所选 Repository 中唯一的 Dist;
- 否则退出
2并列出候选项。
其他命令有意采用不同规则:
- 未指定
-d时,build、check、status默认作用于全部 Dist; show默认搜索所选 Repository,-d只用于收窄;where默认跨工作区搜索全部匹配 Dist,-r/-d用于收窄;changes作用于整个 Repository,明确拒绝-d。
锁
除 init 外,写命令接受 -T/--timeout DUR 与 -N/--no-wait。init 获取 Workspace 锁,且不提供
命令行超时覆盖;其他锁均为 Repository 级,但 repo new 与 repo rm 同样使用 Workspace 锁。
--timeout 0 表示无限等待;正数使用 Go duration,例如 500ms、30s、5m。--no-wait
立即失败;它与正数 timeout 互斥。获取锁失败退出 4。
只读命令不获取写锁;status 仍会报告 Repository 是否正被写者持锁。
并发
只有需要解析软件包、哈希包体、渲染索引或执行校验的命令才接受 -j/--jobs N:create、
add、rm、build、check 与 repo migrate。默认值是逻辑 CPU 数,且不得小于 1。
JSON 输出
支持 --json 的命令在 stdout 输出一个带版本的 Envelope,诊断信息仍写入 stderr:
任何非零退出都会令 ok 为 false;部分成功的批处理仍会返回已提交项与失败项。完整结果结构见
JSON 输出。
不带 --json 时,每条 Managed 命令都有稳定的人类可读 renderer,适合交互使用,但不属于机器
协议。需要结构化字段的脚本应始终使用 --json;它也是唯一受支持的机器接口。
退出码
| 代码 | 含义 |
|---|---|
0 |
成功或幂等空操作 |
1 |
运行时 I/O、解析、渲染、签名或传输错误 |
2 |
用法、工作区发现或配置错误 |
3 |
批处理部分成功 |
4 |
写锁不可用 |
5 |
完整性/恢复失败,或 check 判定目录不可交付 |
6 |
可预期拒绝:冲突、受保护对象、无匹配或架构不兼容 |
各命令的精确触发条件见退出码。
5.1 - sow create
sow create 把一个已经放着 .rpm / .deb 的目录变成平面仓库(flat repository):在包旁边写出索引
文件。它就是 Plain 平面模式的全部——没有 sow.yml、没有 SQLite、不做工作区发现。本页讲清单遍扫描
契约、--pigsty 完成门禁,以及 --sign-with 的 RPM 包签名。
语法
DIR 默认为当前目录。
说明
create 读取 DIR 顶层的普通文件,按发现的内容渲染对应索引:有 RPM 就生成 repodata/,有 DEB 就
生成 Packages 与 Packages.gz,混合目录两套一起生成。架构全部来自包头——Plain 模式没有架构参数,
也没有架构许可表。
平面元数据只引用同目录的包:RPM 的 location 是裸 basename,DEB 的 Filename 是
./<basename>。无论目录作为 file:// 源还是 HTTP 根暴露,两者都保持相对引用。
默认情况下 create 不删除、不移动、不重命名、不重签、不改写任何一个包字节。它只替换自己拥有的索引
路径,未知文件原样保留。
参数
| 参数 | 说明 | 默认 |
|---|---|---|
-j, --jobs N |
唯一一次包哈希/解析扫描的并发 worker 数 | 逻辑 CPU 数 |
--pigsty |
启用 Pigsty 兼容清理与完成 marker | 关闭 |
-S, --sign-with KEY |
用 16/40/64 位十六进制 GPG key ID 给未签名 RPM 补签 | 关闭 |
--overwrite |
重签全部 RPM;必须与 --sign-with 同用 |
关闭 |
-T, --timeout DUR |
等待锁的最长时间;0 表示无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
-h, --help |
显示帮助 | — |
扫描规则
- 只考虑顶层、以
.rpm或.deb结尾的普通文件。 - 不递归、不跟随符号链接、不读工作区配置。
- 所有有效版本都进入索引。两个文件对应同一逻辑坐标但内容不同时,硬失败。
- 默认模式下没有受支持包会被拒绝;
--pigsty接受空权威集合,以便中断的“删除全部包”清理能够收敛并写 marker。
包 I/O 与最终校验
默认未签名路径中,每个选中包恰好只有一次完整内容扫描。worker 打开包、计算一次 SHA-256、解析
header/control,并保留完整解析结果。RPM XML 与 DEB Packages 都从该结果渲染;渲染和生成元数据
校验都不会重新打开包体。--jobs 并行化这一次扫描,规范结果顺序保证 worker 调度不改变输出字节。
发布前,create 重新列出顶层包集合,把文件 identity、类型/mode、size 与 mtime 同扫描后快照比较。
这是便宜的 stat 校验,不是第二次哈希。集合或 stat 变化会在任何 stage 输出发布前以完整性错误 5
退出。原地改字节同时刻意保持 inode、size、mtime 不变,不属于本机协作写者契约。
显式 RPM 签名是例外:复制、签名、签名验证以及解析最终签后 RPM,会对实际修改的包增加必要读取。
确定性输出与幂等
对给定输入集,渲染出的元数据是字节稳定的:gzip 输出确定,repomd.xml 写 <revision>0</revision>
与 timestamp 0。对未变化的目录重跑 create 不写任何字节,报告 noop=true:
repo_complete 门禁
默认模式永不生成 repo_complete。如果 marker 已经存在,create 宁可拒绝写索引,也不留下一个内容
已过期却仍宣称"完成"的旧 marker:
要么加 --pigsty 重跑(由它按文档顺序撤下并重新发布 marker),要么自己先把 marker 移走。
–pigsty
--pigsty 在一次调用中同时启用三项相互关联的兼容动作。发布顺序受 marker 门禁保护,但中断后是
重新扫描重建,不会从 journal 恢复:
- 删除解析架构为
i386的 DEB;RPM 不会仅因为架构是i386/i486/i586/i686而被删除。 - 删除二进制包名恰为
patroni且 upstream 版本恰为3.0.4的 RPM/DEB。RPM 比较VERSION,忽略 epoch 与 release;DEB 先剥掉 epoch 与 Debian revision 再比。3.0.4+foo不算命中。 - 全部索引渲染成功后写出
repo_complete:剩余顶层 RPM/DEB 的 SHA-256,按 basename 字节序排序, 格式为<sha256><两个空格><basename>。
清理只触碰解析成功且命中规则的顶层普通包文件,绝不按宽泛 glob 删目录或未知文件。
发布顺序对以 marker 为门禁的调用方很关键:先撤下已有的 repo_complete,再切换索引,只在替换
元数据安装后删除命中包,最后才写入新 marker。调用方必须把 marker 缺失视为尚未完成。
把 repo_complete 缺失当作"构建进行中"。这正是 --pigsty 设计围绕的契约。
RPM 包签名
-S/--sign-with KEY 是修改 RPM 字节的显式授权。KEY 必须是恰好 16、40 或 64 位十六进制 GPG key ID/fingerprint,不接受 0x 前缀。SOW 将其规范化为大写,通过 _gpg_name macro 传给环境中的
rpm --addsign。私钥、passphrase、GPG home、pinentry 以及额外 RPM macro 都由你的运行环境提供——
SOW 不接收、不持久化、不回显任何秘密。
- 默认只给没有可解析嵌入 OpenPGP 签名的 RPM 补签;已有签名的包保持原字节。
--overwrite必须与--sign-with同用,改为对全部保留 RPM 执行rpm --resign。- 签名发生在同文件系统的私有 stage 副本上。每个结果都会重新解析以确认嵌入签名存在、 signature-neutral digest 与 NEVRA 未变,并以最终完整字节生成 rpm-md。
--pigsty清理后至少要保留一个顶层 RPM,且PATH中要有rpm。
锁、staging 与覆盖重建
create 对目标目录取写锁,服从 --timeout/--no-wait。全部元数据先写入私有 stage 并验证,之后
才开始发布。锁协调本机 SOW 写者;任意外部进程同时修改包不属于受支持负载。
Plain create 不创建持久操作 journal、回滚 pre-image 或 recovery trash。发布由多个单文件 rename 组成,
因此崩溃可能留下部分替换的派生文件。使用你当前想要的参数重新执行 sow create:它丢弃保留命名空间
中的陈旧 Plain 临时状态,再按现在仍存在的包重建全部索引。recovered 始终为 false;
重跑是一次全新覆盖构建,不是事务重放。
平面目录没有整个仓库的 generation 指针,RPM 与 DEB 入口也无法用一次 POSIX rename 同时切换。因此
Plain 不承诺跨文件瞬时原子性。--pigsty 用 repo_complete 做门禁;需要事务恢复时使用 Managed。
示例
给混合目录建索引:
机器可读结果:
用八个 worker 替换 Pigsty 现有的平面构建:
失败时的 envelope:
退出码
| 码 | 触发条件 |
|---|---|
0 |
索引写出成功,或输入未变化产生 no-op |
1 |
目录不可读或不存在、包解析失败、渲染失败、签名工具失败 |
2 |
用法错误——--overwrite 未配 --sign-with、key 格式非法、--no-wait 与非零 --timeout 同用 |
4 |
目录写锁被占用,且给了 --no-wait 或 --timeout 到期 |
5 |
发布前输入集合/stat 变化,或受控输出路径未通过完整性检查 |
6 |
未找到受支持的包、撞上 repo_complete 门禁、对 DEB-only 目录用 --sign-with、坐标冲突 |
参见
- Plain 平面仓库 ——
create背后的设计 - 快速上手 —— 五分钟平面仓库演练
- 仓库布局 —— 平面目录树长什么样
- 仓库签名 —— 生成与使用签名钥
5.2 - sow init
sow init 创建根级 sow.yml 与私有状态目录 .sow/,这两样东西让一个目录成为工作区(Workspace)。
它同时也是手写配置的收敛命令:如果 sow.yml 里已经声明了 Repository 与 Dist,init 会把还不存在
的那些实体化出来,已完成的原样跳过。
语法
DIR 默认为当前目录。init 不接受 -C/--workdir——位置参数已经明确指定了目标。
说明
首次 init 写出最小配置与私有状态目录:
.sow/ 里放着 workspace.lock、工作区生命周期命令使用的持久文件 journal workspace-ops/、
repo-locks/,以及后续每个 Repository 一个的 SQLite 数据库。它的权限是 0700,绝不能对外提供
HTTP 访问。
参数
| 参数 | 说明 | 默认 |
|---|---|---|
--json |
输出版本化 JSON envelope | false |
-h, --help |
显示帮助 | — |
幂等规则
init 被设计成可以反复运行——无论是在 provisioning 脚本里还是手工执行:
-
创建新配置时写入
schema: sow/v3与默认architectures: [x86_64, aarch64]。 -
它从不自动创建 Repository。请用
sow repo new,或先在sow.yml中声明。 -
它从不覆盖已存在的
sow.yml。重复运行只报告现状,并列出发现了什么: -
非空目录可以初始化,但若已有文件与 SOW 保留路径冲突则失败。
收敛已声明的配置
如果 sow.yml 里已经描述了 Repository 与 Dist,init 会为它们补齐缺失的目录树、SQLite 数据库与
空索引。已初始化的对象直接跳过,因此计数器准确反映本次运行做了什么。
这样创建出来的 Dist 立刻具备协议完整的空发布面:RPM Dist 每个架构视图有一份空
repodata/,DEB Dist 有空的 Packages/Packages.gz、by-hash 与 Release。
再跑一次什么都不会变:
锁与恢复
工作区生命周期命令——init、repo new、repo rm——运行在目标 Repository 数据库存在之前或被删除
之后,因此它们使用 .sow/workspace.lock 加 .sow/workspace-ops/ 里的持久文件 journal,而不是
SQLite Operation Journal。被中断的 init 会由下一条工作区生命周期命令前滚完成或回滚。
示例
建好工作区后手工添加 Repository:
初始化当前目录之外的目录:
从版本控制中的配置文件 provision:
退出码
| 码 | 触发条件 |
|---|---|
0 |
工作区创建成功,或已收敛(no-op) |
1 |
写配置或状态目录时的运行时 I/O 错误 |
2 |
用法错误,或已存在的 sow.yml 解析/校验不通过 |
3 |
部分成功——部分声明的 Repository/Dist 已提交,至少一个失败 |
5 |
工作区 journal 无法恢复到终态 |
6 |
已有文件与 SOW 保留路径冲突 |
参见
- 第一个工作区 —— 十分钟带练版本
- Managed 工作区 —— 三层模型
- sow.yml 配置参考 —— 全部配置键
- sow repo 与 sow dist
- 仓库布局 ——
.sow/里有什么
5.3 - sow config
sow config 有两个只读子命令。config check 是对 sow.yml 的全量预检——每次手工改完配置以及在
CI 里都该跑一遍。config show 打印 SOW 实际算出来的配置,用它确认默认值、继承的架构与规范化别名
是不是按你预期解析的。
两个子命令都不创建目录、不碰数据库、不自动修正你的文件。
语法
sow help config 会列出两者。
sow config check
解析并校验完整的 sow.yml:schema 版本、名称、路径冲突、架构许可表、Dist 格式、成员策略与签名 key
引用。它会回报解析到的工作区以及校验了多少对象。
参数
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
--json |
输出版本化 JSON envelope | false |
-h, --help |
显示帮助 | — |
严格拒绝未知字段
未知键是错误,不是警告。一个拼写错误不会静默地让某条策略失效:
schema 版本被钉死:
唯一有效值是 schema: sow/v3。不要靠修改 Schema 字符串绕过校验错误。
check 还会验证声明的每个签名 key 引用可解析且适用于签名——过程中绝不打印密钥材料。如果你从许可表
里删掉一个架构,而仍有 Dist 配置、Membership 或已构建代在用它,config check 会拒绝该配置。
sow config show
以 YAML 打印当前选定作用域的有效配置。
对比磁盘上的文件——里面只有你写的内容:
show 补上了 protected: false、每个 Dist 继承来的 architectures、limit: 0 与空的 exclude
列表。架构一律以规范化 family(x86_64、aarch64)打印,绝不用生态别名——amd64 与 arm64 只是
同两个 family 的 DEB 写法。
参数
| 参数 | 说明 | 默认 |
|---|---|---|
--all |
展开整个工作区的默认值与规范化架构 | 关闭 |
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-d, --dist NAME |
选择一个 Dist;可重复 | 按选择规则 |
--json |
输出版本化 JSON envelope | false |
-h, --help |
显示帮助 | — |
用 -r/-d 做作用域投影
-r 与 -d 把输出收窄到选中的对象。要回答"这一个 Dist 上实际生效的策略是什么",这是最快的方式:
--all 方向相反:无论你站在哪里,它都展开整个工作区。
秘密永不输出
密钥材料与 passphrase 不会出现在 config show、JSON、操作日志或错误文本中。只显示引用形态
(file://…、env://…、agent://…)与 fingerprint。
示例
在 CI 里先校验再构建:
比较两个 Dist 的有效策略:
退出码
| 码 | 触发条件 |
|---|---|
0 |
配置合法,或输出成功打印 |
1 |
读取配置文件时的运行时 I/O 错误 |
2 |
用法错误、工作区未找到、未知字段、schema 不符,或任何校验失败 |
6 |
指定的仓库或 Dist 不存在 |
config check 把校验失败报为退出码 2 而不是 6:非法的 sow.yml 属于配置错误,不是被拒绝的
操作。
参见
- sow.yml 配置参考 —— 全部配置键与完整示例文件
- 成员策略 ——
exclude与limit如何求值 - 签名模型 —— key 引用文法与两条信任链
- sow init —— 收敛手写配置
- sow check —— 校验磁盘字节的运行时对照命令
5.4 - sow repo
一个仓库(Repository)独占一份 pool/、一份 dists/、一个 SQLite 数据库与一个私有状态目录。它是
锁、事务恢复、Generation 编号与 Changeset 的边界——跨仓库不去重,也不承诺跨仓库原子提交。
sow repo 管理的就是这条边界。
语法
命名
仓库名必须匹配 [a-z0-9][a-z0-9._-]*,且不能是 .、..、.sow、pool、dists,也不能与工作区
保留文件冲突。
路径不可指定。仓库永远位于 <workspace>/<NAME>/。
sow repo ls
只读列出工作区里的全部仓库。
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
--json |
输出版本化 JSON envelope | false |
STATUS 取值为 clean、dirty、recovering 或 error。各状态对客户端意味着什么,见
事务与恢复。
sow repo new
原子更新 sow.yml,然后创建 <workspace>/<NAME>/{pool,dists}、SQLite 数据库与私有状态目录。新仓库
处于 Generation 0、clean 状态。
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
repo new 取的是工作区锁而不是仓库锁——此时仓库数据库还不存在。它不接受 -r,位置参数已经指明了
目标。
对已存在的仓库再跑一次是收敛型 no-op,只报告当前状态,因此在 provisioning 脚本里是安全的。
sow repo show
只读显示一个仓库的细节。省略 NAME 时按 CLI 全局约定 中的仓库选择规则
解析。
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
省略 NAME 时用它选择仓库 |
按选择规则 |
--json |
输出版本化 JSON envelope | false |
同时给出 NAME 与 -r 时两者必须一致;不一致会在读取任何状态前失败:
sow repo migrate
这是专用维护命令,不属于全新的 0.4 Managed 工作流。由 SOW 0.4 创建的 Repository 已经使用 当前单包体布局与 Schema。
但从既有 v0.3 Workspace 升级时,迁移是强制步骤:先停止全部 Workspace 写入并完成备份,再在 执行普通读写之前逐个迁移所有已配置 Repository。
0.4 Transition 会安装 Schema v11 与 v12:按全部 Dist 重新派生 Repository 状态;在不猜测缺失 历史签名者的前提下修复 Publication 与 Generation Signer Projection;移除陈旧 abandoned-object evidence;并回填 append-only publication-target binding ledger 的 Revision 1。v0.3 未记录的历史 Signer 保持显式未验证,不能进入 Current Head,也不能成为 retained trust assertion。
Schema Transition 完成后不可逆,不要再用 SOW 0.3 打开数据库,也不要手工修改
PRAGMA user_version。--abort 只适用于诊断出的 pre-commit layout-maintenance attempt,不能
撤销已经完成的 Schema Migration。除升级或 SOW 明确诊断外,不要试探性执行 migrate。
| 参数 | 含义 | 默认值 |
|---|---|---|
-j, --jobs N |
并行校验/渲染 worker | 逻辑 CPU 数 |
--abort |
在提交决策前放弃维护尝试 | false |
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
省略 NAME 时选择仓库 |
选择规则 |
-T, --timeout DUR |
最长锁等待;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
sow repo rm
删除一个仓库:它在 sow.yml 中的条目、数据库、pool/、dists/ 与私有状态。绝不跟随符号链接,
也绝不越出固定的仓库路径。
不加 -f 时,只能删除空仓库——没有 Dist、没有 Membership、没有 Package Object:
| 参数 | 说明 | 默认 |
|---|---|---|
-f, --force |
删除非空的、未 protected 的仓库 | false |
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
-f 到底降级了什么
-f 只放宽为空这一个前置条件。它不绕过路径安全检查、不绕过符号链接拒绝、也不绕过 protected
门禁。
protected
sow.yml 中的 protected: true 直接封死仓库删除,加 -f 也不行:
要删除受保护的仓库,必须先改 sow.yml,通过
sow config check,再重试。没有 --yes,也没有临时覆盖开关。
protected 只作用于仓库删除。受保护仓库上的包级操作不受影响——add、rm、build,乃至
dist rm 都照常工作:
示例
为两层结构创建仓库:
在 cron 任务中快速失败,而不是排队等另一个写者:
一行一个仓库地做审计:
退出码
| 码 | 触发条件 |
|---|---|
0 |
列出、创建、显示、迁移、放弃 pre-commit transition 或删除成功;或 repo new 收敛了已存在的仓库 |
1 |
创建或删除目录树时的运行时 I/O 错误 |
2 |
用法错误、工作区未找到,或仓库选择有歧义 |
4 |
工作区锁被占用,且给了 --no-wait 或 --timeout 到期 |
5 |
工作区 journal 的完整性或恢复错误 |
6 |
名称非法、仓库不存在、非空但未给 -f、protected,或 NAME 与 -r 冲突 |
参见
- sow dist —— 下一层
- Managed 工作区 —— 三层模型与发现规则
- 事务与恢复 —— 锁作用域与
recovering状态 - sow.yml 配置参考 ——
protected与仓库级签名配置 - 仓库布局 —— 固定目录结构
5.5 - sow dist
Dist 是一个仓库内、单一格式(rpm 或 deb)的具名包集合。客户端指向的就是它。一个仓库可以同时拥有
RPM Dist 与 DEB Dist,两者共用一份 pool/,但渲染进完全独立的 dists/ 子树。
语法
命名
Dist 名与仓库名规则相同:[a-z0-9][a-z0-9._-]*,排除 .、..、.sow、pool、dists。
对 SOW 而言这个名字是不透明字符串。el9、trixie、el9-beta、customer-acme、2026-07-31 都只是
名字——beta 频道、按客户切分的视图、快照,都是你自己施加的命名约定,不是 SOW 建模的功能。
sow dist ls
只读平铺列出选定仓库的全部 Dist。
DESIRED 与 BUILT 是成员计数。两者不一致时,DIRTY_REASONS 会说明原因:
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
--json |
输出版本化 JSON envelope | false |
架构按规范化 family 打印。JSON 输出同时给出两种写法,用它可以确认 DEB Dist 渲染的是
binary-amd64 与 binary-arm64:
sow dist new
创建一个普通的、后续可继续修改的 Dist。唯一的业务参数是 --format。
| 参数 | 说明 | 默认 |
|---|---|---|
--format FORMAT |
必填;rpm 或 deb |
— |
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
--format 必填且取值封闭:
没有 --arch。架构从工作区许可表继承;高级用户在 sow.yml 里为某个 Dist 声明子集来收窄。策略
(limit、exclude)同样只在 sow.yml 中配置,绝不在命令行上重复建模。
用相同名称与相同格式重跑 dist new 是收敛操作,只报告当前状态。同名但格式不同会被拒绝:
三方事务
dist new 要在三个地方同时提交:sow.yml 条目、仓库数据库、磁盘目录树。它走 SQLite Operation
Journal(此时仓库数据库已存在,与 repo new 不同),并产生一个带空索引的新 Built Generation。
因此新建的 Dist 立刻具备协议完整的空发布面。RPM Dist 在每个架构视图下有一份空
repodata/;DEB Dist 有空的 Packages、Packages.gz、by-hash/SHA256/ 条目,
以及 Release;配置签名时再生成 InRelease 与 Release.gpg。
sow dist show
只读显示一个 Dist 的细节。
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
--json |
输出版本化 JSON envelope | false |
JSON 形态额外给出 effective_config_sha256,即解析后 Dist 配置的摘要。当你改动 limit、exclude
或签名 key 时,正是这个摘要让 Dist 变 dirty——配置身份变了,已构建代就不再等于期望状态。
sow dist rm
删除一个 Dist 的 Membership 与衍生索引。
| 参数 | 说明 | 默认 |
|---|---|---|
-f, --force |
删除成员与索引,但保留 pool 中的包 | false |
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
删除 Dist 不会删除 pool 字节
删除 Dist 绝不会从 pool/ 删包。整个 Dist 目录被移入恢复区后原子移除,包池完全不受影响:
失去引用的 Pool 对象会继续保留,直到 sow gc 证明它不再被当前、保留、恢复、发布以及
活动维护操作等任何安全根引用。
仓库的 protected: true 只封死仓库删除;受保护仓库上的常规 Dist 维护照常进行。
示例
给一个仓库同时配上 RPM 与 DEB 两副面孔:
加一个带独立保留策略的 beta 频道——先建 Dist,再在 sow.yml 里写策略并收敛:
哪些 Dist 落后于期望状态:
退出码
| 码 | 触发条件 |
|---|---|
0 |
列出、创建、显示或删除成功;或 dist new 收敛了已存在的 Dist |
1 |
创建空索引时的运行时 I/O 或渲染错误 |
2 |
用法错误——--format 缺失或非法、工作区未找到、仓库选择有歧义 |
4 |
仓库锁被占用,且给了 --no-wait 或 --timeout 到期 |
5 |
Operation Journal 的完整性或恢复错误 |
6 |
名称非法、Dist 不存在、同名不同格式冲突、非空但未给 -f |
参见
5.6 - sow add
sow add 是主要的写入路径。它解析你指定的包,从包头推导格式与架构,执行 Dist 的成员策略,并且——
除非你加 --skip——在返回前重建全部受影响的索引。命令退出码为 0 时,客户端已经能看到新包了。
语法
参数
| 参数 | 说明 | 默认 |
|---|---|---|
-R, --recursive |
递归进入 PATH 目录的子目录 |
关闭(只扫顶层) |
--skip |
只更新期望状态,不构建 | 关闭 |
-j, --jobs N |
解析、哈希与渲染的并发 worker 数 | 逻辑 CPU 数 |
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-d, --dist NAME |
选择一个 Dist;可重复 | 按选择规则 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
输入与目标
PATH 可以是文件或目录。目录默认只扫描顶层,除非你加 -R。
最终必须确定恰好一个仓库与至少一个目标 Dist——见选择规则。RPM 与 DEB 混合批次是允许的:每个包只会被考虑放进格式相同的目标 Dist;一个包如果没有任何兼容目标,则该包失败。
SOW 绝不从 manifest、目录名或宿主机 OS 推断目标。
汇总行给出 Operation ID、逐项计数、成员增减、新的 Desired Revision、Built Generation,以及仓库是否
留在 dirty 状态。随后是每个输入一行 item,顺序稳定。
逐项状态
每行 item 带一个总体 status,以及 dists= 中的逐 Dist 判定。
| 状态 | 含义 |
|---|---|
accepted |
新建 Package Object,且至少增加一条 Membership |
reused |
内容已存在于本仓库;可能只是新增了 Membership 引用 |
excluded |
策略把它从所有目标 Dist 中移除——看 dists= 区分是 excluded 还是 limited |
failed |
该包被拒绝,error= 字段说明原因 |
reused 表示内容幂等:同一个文件加两次绝不会产生第二个对象或重复 Membership。默认重复执行
add 时还会收敛所选 Dist;Dist 已经最新时 Generation 不变,先前 --skip 或配置变更留下
dirty 状态时,则会补做构建并可能推进 Generation:
把同一个对象加进第二个 Dist 同样是 reused——包池只保留一份,只是多了一条 Membership。
如果仍想保留 dirty 批次,应再次显式使用 --skip。
架构是读出来的,不是猜的
add 从包头读取格式与原生架构,再对照工作区许可表。不在许可表中的架构会让该包失败,并明确告诉你
要改什么:
它不会创建目录,也不会修改 sow.yml。
RPM 的 noarch 与 DEB 的 all 是架构中性(neutral)的。它们只产生一个 Package Object 与一条
Membership,但会渲染进目标 Dist 的每个有效架构视图。它们不会自动扩散到你没有用 -d 选中的 Dist。
策略:exclude 与 limit
合并进目标 Membership 之后,SOW 会在完整的 Dist 候选集上重新求值 exclude,再求值 limit。被策略
移除的包会被明确报告,不算解析失败。
这里 trixielim 配了 exclude: [{kind: [dbgsym]}] 与 limit: 1。dbgsym 包被规则排除;
libpq5 18.2-1 在版本上限下输给了 18.3-1,报告为 limited。两者的顶层状态都是 excluded,
靠 dists= 字段区分。
limit 按 (二进制包名, 原生架构) 分组,因此 18.3-1:amd64 与 18.3-1:arm64 在 limit: 1 下都能
留下。同一次运行中,一个包可以被某个 Dist 接受、被另一个 Dist 跳过。
exclude 与 limit 移除的是真实的期望成员。之后放宽策略不会把它们变回来——pool/ 里残留的字节
不构成候选集。请重新执行 sow add。
部分成功的批次
即使同批有失败项,合法且无冲突的包依然会提交。失败的输入原地不动,各自带自己的错误信息,命令退出
3:
如果一个都没被接受,整个操作以退出码 6 被拒绝,仓库保持原样:
没有 rejected/隔离目录。
–skip
--skip 在期望状态提交后就停下。公开的 pool/ 与 dists/ 字节不变,Built Generation 保持原位,
仓库变为 dirty。新包字节被持久保存在私有 pending 存储中,直到下一次 build 才发布。
pending=4/2326 表示私有存储里有 4 个对象、共 2326 字节在等待。它们不会出现在
sow changes 中——只有成功的 build 才会把它们提升进可交付树。
批量导入时用 --skip,最后一次性收敛:
处理顺序
一次 add 的执行顺序如下:
- 取得仓库写锁,并恢复任何未完成的 Operation。
- 在 SQLite 中提交一条
plannedOperation。 - 只读解析输入,计算逻辑坐标与输入字节 SHA-256(RPM 还会计算 signature-neutral payload digest)。
- 校验架构许可表,并查询已有坐标。
- 只对确实全新的坐标,在 stage 副本上执行可选的 RPM 签名并计算最终 SHA-256,再校验内容与路径唯一 性。
- 合并目标 Membership,然后在完整 Dist 集合上执行
exclude与limit。 - 提交期望状态;新字节写入私有 pending 内容存储。
- 除非给了
--skip,把仍被需要的 pending 对象发布进pool/并渲染索引——一次命令中每个 Dist 最多 构建一次。
任何模式下,输入文件都不会被修改、移动或删除。
RPM 签名模式
Managed 模式的 RPM 包签名在 sow.yml 的 signing.rpm.packages.mode 中配置,命令行没有覆盖开关。
| 模式 | 行为 |
|---|---|
never |
完整保留输入字节 |
fill |
无签名或签名不受信任时用配置 key 签名;已有能被 trusted_keys 验证的签名则保持字节。配置了 key 时的默认值 |
always |
确保最终包由配置 key 有效签名;否则对 stage 副本重签 |
没有配置 key 时只能用 never。
由于签名包含非确定字段,SOW 无法先重签再比较最终哈希。重试幂等因此建立在坐标上:输入字节完全相同
则直接复用;RPM signature-neutral digest 相同、且既有对象满足当前策略时也复用。payload digest 不同,
或既有对象已不满足策略,则是硬冲突——add 不会静默地对同一坐标原地重签。
退出码
| 码 | 触发条件 |
|---|---|
0 |
全部输入被接受或复用;索引已重建(或因 --skip 跳过) |
1 |
运行时 I/O、解析器、渲染器或签名失败 |
2 |
用法错误、工作区未找到,或仓库/Dist 选择有歧义 |
3 |
部分批次——至少一项已提交,至少一项失败 |
4 |
仓库锁被占用,且给了 --no-wait 或 --timeout 到期 |
5 |
完整性或恢复错误,包括在 applied 之后构建失败 |
6 |
一个都没接受——架构不受支持、没有兼容的目标 Dist,或坐标冲突 |
参见
5.7 - sow rm
sow rm 把包从你选定的 Dist 的期望成员集中拿掉,并默认立即重建受影响的索引。它不会从 pool/
删除字节——成员关系与内容是两个概念,回收由独立的保守操作 sow gc 完成。
语法
参数
| 参数 | 说明 | 默认 |
|---|---|---|
-c, --check |
只预览:计算并打印方案,不写任何东西 | 关闭 |
--skip |
只更新期望状态,不构建 | 关闭 |
-j, --jobs N |
并发 worker 数 | 逻辑 CPU 数 |
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-d, --dist NAME |
选择一个 Dist;可重复 | 按选择规则 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
--check 与 --skip 互斥:
包引用
PACKAGE 接受五种形态。完整文法与歧义规则见包引用,简版如下:
| 形态 | 例子 |
|---|---|
| 内容哈希 | sha256:d6f332ed157de1d42058ec785b392a1cc4b5836c27830af8fbf083cce29ef0ab |
| RPM 坐标 | rpm:epel-release-0:7-5.noarch |
| DEB 坐标 | deb:libpq5=18.3-1:amd64 |
| 完整文件名 | epel-release-7-5.noarch.rpm |
| 裸二进制包名 | epel-release |
裸名表示选定 Dist 中该名称的全部版本与原生架构——正因如此,sow rm patroni 才是一条好用的下架
命令。非裸名的模糊短引用会失败并列出候选,而不是替你猜。
sow ls 会直接打印精确的 sha256: 引用与规范化坐标,你不需要手工
拼接。
引用匹配不到任何东西属于拒绝,不是静默成功:
没有 --allow-empty、没有 --all、没有 --yes、没有 --source-list。
用 –check 预览
-c/--check 精确算出会移除什么、策略随后会怎么判定、以及立即构建会触碰哪些文件——并且什么都不写。
注意两个 centos-release 版本都被裸名命中了。change 行是一份真实交付计划,按
payload → metadata → pointer → delete 排列。其他程序需要对应的 removed[] 与 changes[]
数组时应使用 --json。
预览与写操作使用同一套候选配置和完整性预检;预览未通过门禁时,不能据此认为实际写入会成功。
--check 有意不取写锁。把它与锁参数一起用是用法错误,免得有人以为预览会排队等待写事务:
默认行为:移除并重建
不带 --check 或 --skip 时,rm 提交期望状态变更,并在返回前重建每个受影响的 Dist。pool 对象
留在磁盘上。
汇总之后会为每个受影响文件输出一行 change(本次运行共八行)。加上 --json 后,同一结果
以稳定的标准 Envelope 返回。
移除一个 Dist 的最后一个成员是允许的。SOW 仍会渲染合法的空索引(配了 key 就带签名)——空的
Packages 配可验签的 InRelease,或每架构的空 repodata/。
–skip
--skip 提交期望状态变更并把仓库标为 dirty,不触碰公开树。旧的 Built Generation 对客户端依然完全
自洽。
changes 为空是因为什么都没构建。执行 sow build 收敛。
与策略的交互
移除属于期望状态编辑,因此策略会在新的候选集上重新求值——移除操作绝不会让先前被 limit 挤掉的包
复活。如果你从一个 limit: 1 的 Dist 里删掉 libpq5 18.3-1,18.2-1 不会回来;需要重新显式 add。
示例
安全下架——先预览,再执行:
一次从两个 Dist 中移除同一个精确对象:
批量移除后只重建一次:
把预览计划喂给其他工具:
退出码
| 码 | 触发条件 |
|---|---|
0 |
成员已移除并重建,或 --check 预览已打印 |
1 |
运行时 I/O 或渲染失败 |
2 |
用法错误——--check 与 --skip 同用、--check 与锁参数同用、选择有歧义、工作区未找到 |
3 |
部分批次——至少一个引用被移除,至少一个失败 |
4 |
仓库锁被占用,且给了 --no-wait 或 --timeout 到期 |
5 |
完整性或恢复错误 |
6 |
引用无匹配,或非裸名的引用有歧义 |
参见
5.8 - sow ls
sow ls 是针对 Package Object 与 Dist Membership 的只读查询。它显示所选 Dist 应包含哪些包,
以及这些成员是否已经进入当前 Built Generation。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
-d, --dist NAME |
选择 Dist;可重复 | 选择规则 |
--json |
输出 sow.cli/v1 Envelope |
false |
该命令没有 --pool、--match 或输出格式参数。
输出
| 列 | 含义 |
|---|---|
SHA256 |
不可变内容身份,可直接传给 show 或 rm |
COORDINATE |
规范的 rpm: 或 deb: 包引用 |
DISTS |
所选范围内的 Desired Membership |
BUILT_DISTS |
当前 Built Generation 中的成员关系 |
POOL_PATH |
Repository 内的不可变包体路径 |
Desired 与 Built 不一致时,首行显示 dirty=true。BUILT_DISTS 为空表示该包已进入期望状态,
但客户端尚不可见;运行 sow build 完成收敛。
多个所选 Dist 共享同一对象时,只输出一行,成员列表用逗号分隔。空 Dist 只有表头、没有包行, 仍然是成功结果。
选择范围
ls 要求 Dist 集合无歧义。Repository 包含多个 Dist 时,应传入一个或多个 -d,或从
<repo>/dists/<dist>/ 内运行。
该命令不获取写锁,也不重新哈希包文件。--json 在 result.packages 中返回同一批记录。
示例
列出尚未构建对象的精确引用:
按路径列出包体:
退出码
| 代码 | 触发条件 |
|---|---|
0 |
已输出成员列表,包括空列表 |
1 |
运行时 I/O 错误 |
2 |
用法错误、未发现工作区或隐式 Repository/Dist 选择有歧义 |
5 |
Repository 状态库不可读或不一致 |
6 |
显式指定的 Repository 或 Dist 未配置 |
参见
5.9 - sow show
sow show 在所选 Repository 中解析一个包引用,并打印完整 Package Object。该命令只读,
不获取写锁。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
-d, --dist NAME |
将候选项收窄到指定 Dist;可重复 | 整个 Repository |
--json |
将结果包装进 sow.cli/v1 Envelope |
false |
包引用
PACKAGE 可使用 sha256:<hex> 内容身份、规范的 rpm:<NEVRA> 或
deb:<name>=<version>:<arch> 坐标、完整包文件名或裸二进制包名。精确文法见
包引用。
裸包名必须在所选范围内唯一。sow rm foo 会移除所有匹配版本,而 sow show foo 只允许返回
一个对象;有歧义时会列出候选项:
从错误信息或 sow ls 复制精确坐标/SHA-256 后重试。
输出
不带 --json 时,show 以紧凑的人类可读格式输出身份、存储路径与 Desired/Built 位置:
加上 --json 后,标准 Envelope 的 result 会返回完整 Package Object,包括下列规范化字段。
| 字段 | 含义 |
|---|---|
canonical_arch |
x86_64、aarch64,或 RPM noarch / DEB all 对应的 neutral |
kind |
策略分类:main、debuginfo、debugsource、llvmjit、dbgsym、dbg |
source |
标准化源码包名 |
payload_sha256 |
RPM 去签名摘要,用于保证重签名幂等 |
signature_key |
包内签名的 Key ID(如有) |
storage |
构建前为 pending,进入仓库树后为 pool |
dists / built_dists |
Desired 与当前 Built Membership |
-d 只收窄候选解析范围,不改变包身份。
退出码
| 代码 | 触发条件 |
|---|---|
0 |
已输出一个 Package Object |
1 |
运行时 I/O 错误 |
2 |
用法错误、未发现工作区或隐式 Repository 选择有歧义 |
5 |
Repository 状态库不可读或不一致 |
6 |
显式范围未配置,或引用没有匹配/匹配多个对象 |
参见
5.10 - sow where
sow where 用于回答工作区中哪些 Dist 仍包含某个 Package Object。它默认搜索全部 Repository,
只读且不获取写锁。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
将搜索限制到一个 Repository | 全部 Repository |
-d, --dist NAME |
将搜索限制到指定 Dist;可重复 | 全部 Dist |
--json |
输出 sow.cli/v1 Envelope |
false |
引用解析
PACKAGE 与 sow show 使用相同文法:SHA-256、规范 RPM/DEB 坐标、
完整文件名或裸包名。
解析范围是完整的所选工作区范围。裸包名必须标识唯一 Package Object;即使同名对象位于不同
Repository,也会产生歧义。使用 -r/-d 收窄范围,或提供精确坐标/SHA-256。
输出
不带 --json 时,where 先输出摘要,再为每个位置输出一行:
每个位置同时给出 Desired dists 与当前 built_dists,可用于确认已移除或已替换版本是否仍对
客户端可见。
加上 --json 后,同一对象位于 result 下。引用不存在属于明确拒绝,而不是空成功:
示例
列出仍在提供某个精确版本的全部位置:
退出码
| 代码 | 触发条件 |
|---|---|
0 |
已输出一个解析后的 Package Object 及其位置 |
1 |
运行时 I/O 错误 |
2 |
用法错误或未发现工作区 |
5 |
某个 Repository 状态库不可读或不一致 |
6 |
显式 Repository/Dist 未配置,或引用没有匹配/在所选范围内有歧义 |
参见
5.11 - sow status
sow status 是低成本的 Repository 状态查询。它读取状态,但不哈希文件、不验签、不恢复
Operation、不构建元数据,也不获取写锁。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
-d, --dist NAME |
只查看指定 Dist;可重复 | 全部 Dist |
--json |
输出 sow.cli/v1 Envelope |
false |
Repository 状态
每个 Repository 同时跟踪 SQLite 中的 Desired Revision,以及公开 dists/ 树对应的
Built Generation。
| 状态 | 含义 | 公开视图 |
|---|---|---|
clean |
Desired 与 Built 一致 | 当前且完整的 Generation |
dirty |
Desired 已领先,常见于 --skip 或配置变化之后 |
上一个完整 Generation |
recovering |
存在非终态 Operation,下一条写命令必须先恢复 | 上一个已完成的协议指针 |
error |
自动恢复无法安全裁决 | 保留上一个完整视图,不尝试覆盖 |
dirty 不代表仓库只写了一半。协议指针最后切换,因此读者看到的始终是完整旧视图或完整新视图。
输出
人类可读输出包含 Repository 状态、ready_to_copy、Desired Revision、Built Generation、
受影响 Dist、待处理对象数量/字节数与写锁状态。
JSON 结果还包含 dirty_reasons 与最近一次 Operation:
ready_to_copy=false 是明确警告;true 只是廉价状态判断,并非字节级完整性证明。交付前应运行
sow check。
只读契约
status 不迁移也不修复状态。Repository 数据库无法安全读取时,命令退出 5;请先执行
诊断信息明确指出的维护命令,再重新查询。尤其是 v0.3 Repository,使用 0.4 读取表面前必须先
备份,并逐个执行 sow repo migrate。
退出行为
只要状态可读,status 在 clean、dirty、recovering、error 四种状态下都返回 0。
脚本应读取结构化状态,而不是把后三者当作命令执行失败。
| 代码 | 触发条件 |
|---|---|
0 |
Repository 状态可读 |
1 |
运行时 I/O 错误 |
2 |
用法错误、未发现工作区或隐式 Repository 选择有歧义 |
5 |
状态库不可读或不一致 |
6 |
显式指定的 Repository 或 Dist 未配置 |
参见
5.12 - sow build
sow build 是显式的 Desired-to-Built 收敛命令。它获取 Repository 写锁,恢复任何可裁决的
未完成 Operation,渲染并验证完整 Generation,最后切换协议指针。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-j, --jobs N |
并行 Worker 数,不得小于 1 |
逻辑 CPU 数 |
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
-d, --dist NAME |
构建指定 Dist;可重复 | 全部受影响 Dist |
-T, --timeout DUR |
最长等锁时间;0 表示无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
将结果包装进 sow.cli/v1 Envelope |
false |
不带 -d 时,SOW 收敛所选 Repository 中全部受影响 Dist;带 -d 时只收敛指定 Dist,
未选择的变化继续保持 dirty。
结果
不带 --json 时,build 输出一行人类可读摘要:
需要标准 Envelope 中的命令专属对象时使用 --json。
空操作构建
成员关系、相关策略、渲染设置与签名配置均未变化时,build 是幂等空操作,不增加 Generation:
策略收敛
build 会重新执行当前 exclude 与 limit 策略。收紧策略可能移除 Desired Membership;
放宽策略不会从残留包池字节恢复历史成员,需要重新运行 sow add。
提交与恢复
SOW 在同一文件系统暂存新元数据,验证完成后再切换可变协议指针。RPM 校验和命名元数据与 APT by-hash 确保新旧读者看到的视图始终自洽。
Pending 包体提升采用有界单写者 group commit。每批最多 512 个对象或 1 GiB:先创建 Pool 链接并持久化所有不同的目标父目录,再删除 pending 名称并持久化共享 pending 目录。中断只会 留下 pending-only、指向同一 inode 的双链接或 Pool-only 状态,都能按 journal 恢复;不会 持久地同时丢失两个名称。
一个 Operation 可以覆盖多个 Dist。每个 Dist 始终暴露完整视图;build 返回时,本次包含的所有
Dist 属于同一个 Built Generation。
开始新工作前,build 会尝试前向恢复或安全回滚非终态 Operation。如果日志、数据库与文件系统
证据互相矛盾,Repository 进入 error,build 拒绝猜测;不存在强制修复参数。
进度事件
耗时较长的构建会向 Operation Log 追加结构化 build_progress 记录。每条事件包含 phase、
completed、total 与 jobs。当前阶段为:
rendering;promoting_payload;publishing_dists;normalizing_public_tree;finalizing。
这些事件不会推进 Operation 状态,也不会在每次更新后 checkpoint SQLite;它们只用于审计
与可观测性,不参与恢复决策。使用 sow log OPERATION
查看明细。
元数据签名
Managed 元数据签名只从 sow.yml 读取,没有命令行 Key 覆盖。配置的 Key 引用或指纹改变时,
相关 Dist 变为 dirty,下一次 build 重新签名。
- RPM:总是生成
repodata/repomd.xml;配置签名后额外生成repomd.xml.asc。 - DEB:总是生成
Release;配置签名后额外生成InRelease与Release.gpg。
退出码
| 代码 | 触发条件 |
|---|---|
0 |
收敛成功或无需操作 |
1 |
渲染、签名或文件系统错误 |
2 |
用法错误、未发现工作区或隐式 Repository 选择有歧义 |
4 |
Repository 写锁不可用 |
5 |
无法安全完成恢复,或 Repository 处于 error |
6 |
显式范围未配置,或当前配置拒绝既有状态 |
参见
sow status—— 判断是否需要收敛sow check—— 验证构建结果sow changes—— 查看生成的文件差异- 事务与恢复 —— 完整提交协议
5.13 - sow check
sow check 是 Managed Repository 的深度只读门禁。它哈希包体、校验状态、重建期望视图并验证
已声明签名;不会修复、构建、恢复 Operation,也不会获取写锁。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-j, --jobs N |
并行校验 Worker 数,不得小于 1 |
逻辑 CPU 数 |
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
-d, --dist NAME |
校验指定 Dist;可重复 | 全部 Dist |
--json |
输出 sow.cli/v1 Envelope |
false |
校验层
稳态下,checker 按顺序报告九层校验:
| 层 | 校验内容 | checked 计数 |
|---|---|---|
config |
sow.yml 能否针对该 Repository 解析并通过校验 |
配置对象 |
state |
SQLite quick_check、外键、日志与恢复证据 |
一个状态库 |
public-modes |
服务目录中所有文件与目录权限 | 已检查路径 |
retained |
显式保留记录与冻结 Generation Manifest | 保留记录 |
package-bytes |
包池与私有 pending 包体的 SHA-256 | Package Object |
desired-membership |
Membership 能否在当前策略下解析到真实对象 | 成员关系 |
index |
渲染索引是否与其声明的成员关系一致 | Dist |
signature |
所有已声明元数据/软件包签名是否有效 | 签名 |
generation-manifest |
Built Generation Manifest 是否与磁盘文件一致 | 一个 Manifest |
Repository 处于未完成布局迁移时,check 改为依次报告 config、state、public-modes 与条件
层 layout-transition,随后停止,并在诊断指定的
repo migrate 完成或在 commit 前中止前返回不可交付。
物理证据与 I/O 契约
package-bytes 绝不会把缓存指纹当作真实性证明。每次运行都会对每个唯一物理包体执行恰好一次
哈希;证据绑定设备号、inode、size、mtime、ctime 与真正读取的文件描述符。同 inode 的硬链接
共享证明;Retained Generation、最终 Manifest 遍历与 changes 复用它,不再扫描包体。
checked 列统计逻辑对象,不代表全文流数量。
DEB 或无签名 RPM 只需一遍完整包体流。带签名 RPM 最多再用一遍从主 Header 到 EOF 的流, 对全部签名 Packet 与候选 Trust Ring 验证;成本不会随 Dist、Retained Generation 或 Trust Ring 数量增加。伪造或并发替换文件会让描述符证据失效并失败关闭。
dirty 不可交付
dirty Repository 的九层校验可以分别成立:旧 Built Generation 完整,新 Desired 状态也有效; 但二者不一致,因此整体仍未通过交付门禁:
此时退出 5。运行 sow build 后重新校验,不应让发布流水线放行该状态。
退出码
| 代码 | 触发条件 |
|---|---|
0 |
全部校验层通过,Repository 可复制交付 |
1 |
校验期间发生 I/O 错误 |
2 |
用法错误、未发现工作区或隐式 Repository 选择有歧义 |
5 |
某个校验层失败,或 Repository 不可交付 |
6 |
显式指定的 Repository 或 Dist 未配置 |
参见
sow status—— 低成本状态查询sow build—— 收敛 Desired 与 Built- 退出码 —— dirty 为什么映射到
5 - 可观测与审计 —— 组合使用校验与审计
5.14 - sow changes
sow changes 比较 Built Generation,输出物理的 Repository 相对文件差异。它不显示尚未构建的
Desired 变化,也不是远端事务协议。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
--json |
输出 sow.cli/v1 Envelope |
false |
该命令作用于整个 Repository,明确拒绝 -d/--dist。
输出
各列依次为操作、阶段、Repository 相对路径、大小、SHA-256。
| 字段 | 取值 |
|---|---|
| 操作 | add、update、delete |
| 阶段 | payload、metadata、pointer、delete |
阶段描述 SOW 如何构建本地 Generation。不要把单行直接重放到线上目录;应使用
sow publish,或先暂存完整副本再原子切换。
Base Generation
不带参数时,SOW 比较当前 Built Generation 与它的前一代。
BASE_GENERATION 是 0..当前代 范围内的十进制整数。Base 0 输出当前 Generation 的完整
交付清单,但不包含私有 sow.yml 与 .sow/。Base 等于当前代时输出空计划;从未构建的
Repository 同样输出空的 0 -> 0 计划。
dirty 与恢复状态
Desired 为 dirty 时,首行显示 dirty=true,但计划仍以当前 Built Generation 结束。私有 pending
包体尚不可交付,不会出现在结果中。
Repository 为 recovering 或 error 时,changes 拒绝输出计划,避免把待定文件动作误认为已完成
Generation。
示例
输出当前完整清单:
生成 Repository 级计划后按路径筛选一个 Dist:
退出码
| 代码 | 触发条件 |
|---|---|
0 |
已输出计划,包括空计划 |
1 |
运行时 I/O 错误 |
2 |
用法错误、传入 -d、未发现工作区或隐式 Repository 选择有歧义 |
5 |
Repository 处于 recovering/error,或状态证据不一致 |
6 |
显式 Repository 未配置,或 Base Generation 超出有效范围 |
参见
sow build—— 创建下一代 Generationsow publish—— 使用受支持的发布协议sow log—— 语义 Operation 及其文件动作- 仓库布局 —— 公开/私有路径边界
5.15 - sow publish
sow publish 将某个 Repository 的当前 Built Generation 交付到 sow.yml 的 targets: 中指定的
目标。目标已经绑定 Repository 与 Provider,因此命令不接受 --repo 或 --dist。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
--abort |
放弃已对账、但尚未写入持久 commit intent 的尝试 | false |
--rebind |
确认并记录允许修改的目标名称、公共端点或缓存 TTL | false |
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-T, --timeout DUR |
最长 Repository 等锁时间;0 表示无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出 sow.cli/v1 Envelope |
false |
TARGET 必须是已配置的 filesystem 或 r2 发布目标。--abort 与 --rebind 互斥。
发布协议
交付前,SOW 要求存在已完成的 Built Generation,并验证公开树与冻结 Generation Manifest 精确 一致;然后按以下顺序规划并写入对象:
- 不可变包体;
- 校验和寻址元数据;
- 可变协议指针;
- 验证并持久化 Checkpoint。
精确对象集合、Receipt、阶段与 commit intent 都会落盘,确保中断后可以对账恢复。目标已经位于 当前 Generation 时,重复发布是幂等空操作。
重绑定可变目标配置
首次成功 publish 会持久绑定 Repository、存储命名空间与 Target Identity。之后配置发生漂移时,
SOW 会拒绝静默接受。只有诊断信息明确提示 --rebind,并且你已经复核变更后,才执行:
如果改的是 targets: Map Key,请使用新目标名。Rebind 保留 active attempt 与 checkpoint identity,
并追加不可变、由操作者确认的 binding revision。
| 可以修改 | 不可变;必须配置新目标 |
|---|---|
| 目标名称 | Repository Identity |
public_endpoint |
Provider、存储 endpoint 或 region |
max_cache_ttl |
bucket 或 prefix |
Rebind 与发布使用同一组 Workspace/Repository 锁,并在数据库事务内重新核对不可变字段;它可以
继续前滚 active commit-intent attempt。Target Maintenance 未完成时禁止改变 TTL;filesystem
正在进行 conditional-delete 维护时禁止改变 public_endpoint。首次绑定必须运行普通 publish,
不能使用 --rebind。
Abort 与恢复
--abort 只允许在持久 commit intent 之前使用。SOW 会对账已经创建的对象,保留后续安全判断所需
证据,并在不继续复制或删除远端对象的前提下放弃本次尝试。
写入 commit intent 后只能前向恢复:重新运行 sow publish TARGET,不能使用 --abort。
公共可见性校验
Provider 存储写入成功还不够:写入 Checkpoint 之前,发布流程还要校验 canonical
public_endpoint。HTTP(S) 目标以普通 GET 为最终权威;no-cache Probe 只能促进 Revalidation,
必须由之后的普通 GET 才能通过。陈旧内容与缺失对象按 max_cache_ttl 重试;408、425、429 与
5xx 使用较短的有界重试窗口。等待 Header 与 Body 空闲进度分别计时;响应超长时失败关闭。
Filesystem 可使用 file:// 或 HTTP(S) 公共端点。执行条件删除时,file:// 需要精确文件身份
缺失,HTTP(S) 则需要 canonical 404/410。R2 必须使用 HTTP(S) 公共端点;R2 Target GC 仍然
只报告候选,不执行远端删除。
安全边界
- SOW 只发布到配置目标,不接受任意目标路径。
- 尚未构建的 Desired 变化不会进入发布。dirty Repository 因而可以发布上一个完整 Built
Generation;如果目标必须反映当前 Desired 状态,应先运行
build。 - 布局迁移与相互矛盾的恢复证据会阻止发布;可裁决的未完成 Dist 操作会在选择源 Generation 之前恢复。
- 对象顺序保证包管理器指针不会引用尚不存在的内容。
- 发布命令不负责外部 Web Server、Bucket Policy、DNS 路由或缓存配置。
退出行为
| 代码 | 触发条件 |
|---|---|
0 |
发布完成,或目标已经是当前 Generation |
1 |
文件系统、Provider、网络、验证或绑定冲突(包括必须 rebind) |
2 |
用法、工作区发现或 sow.yml 无效 |
4 |
Repository 写锁不可用 |
5 |
本地/发布恢复证据不一致,或源不可交付 |
6 |
目标不存在/不安全,或其他安全前置条件拒绝 Publish/Abort/Rebind |
参见
sow status与sow check—— 判断当前 Built Generation 是否正是准备交付的版本sow gc—— 保守的目标维护sow.yml发布目标 —— Provider 配置- 发布模型 —— 阶段、Receipt、恢复与 Cache Grace
5.16 - sow retain
sow retain 管理显式的本地 Generation 根。retain add 只能冻结当前 Built Generation;后续
构建使它成为历史版本后,该代所需的软件包体仍受保护。
语法
GENERATION 必须是大于零的十进制整数。
retain add
要求 GENERATION 等于当前 Built Generation,校验后将其 Manifest 冻结到工作区私有状态中,
并添加显式 GC 根。不能在事后用 retain add 重建一个更老的 Generation。
保留记录只保护包体,不切换当前视图,也不执行发布。重复添加同一 Generation 时,只有已验证记录 与当前证据一致才可视为幂等。
retain ls
列出显式保留记录。它是只读命令,因此不接受锁参数或 --dist。
空列表也是成功结果。
retain rm
只移除显式保留根:
该命令不删除软件包体。移除一个未被保留的 Generation 是幂等空操作。只有在其他安全根也无法
到达这些包体时,后续本地 sow gc 才可能回收。
参数
| 参数 | 适用命令 | 含义 |
|---|---|---|
-C, --workdir DIR |
全部 | 工作区发现起点 |
-r, --repo NAME |
全部 | 选择 Repository |
-T, --timeout DUR |
add、rm |
最长写锁等待时间 |
-N, --no-wait |
add、rm |
锁被占用时立即失败 |
--json |
全部 | 输出 sow.cli/v1 Envelope |
退出行为
| 代码 | 触发条件 |
|---|---|
0 |
操作完成,包括空列表 |
1 |
文件系统或运行时 I/O 错误 |
2 |
Generation 语法无效、发现错误或隐式 Repository 选择有歧义 |
4 |
add/rm 无法获取写锁 |
5 |
Generation Manifest 或 Repository 状态不一致 |
6 |
显式 Repository 未配置、retain add 不是当前 Built Generation,或其他安全规则拒绝请求 |
参见
sow gc—— 使用保留根执行回收sow changes—— 查看 Built Generation 差异- 仓库布局 —— 私有保留记录位置
5.17 - sow gc
sow gc 有两种严格分离的模式:不带位置目标时,回收本地不可达包体;带 TARGET 时,维护一个
已配置发布目标。
语法
| 参数 | 含义 | 默认值 |
|---|---|---|
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
仅用于选择本地 GC 的 Repository | 选择规则 |
-T, --timeout DUR |
最长 Repository 等锁时间;0 表示无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出 sow.cli/v1 Envelope |
false |
目标自身已绑定 Repository,因此 gc TARGET -r NAME 属于用法错误。两种模式都不接受 --dist。
本地 GC
本地 GC 只删除所有安全根都无法到达的包池对象。安全根包括:
- 当前 Built Generation;
- 显式
retain记录; - 恢复状态与非终态 Operation;
- 发布尝试及其证据;
- 活跃维护操作。
操作会记入日志。实际删除包体时,Repository 前进到新 Generation;没有合格对象时为幂等空操作。
目标 GC
目标维护使用发布 Checkpoint、不存在性证据与配置的 Cache Grace,具体行为取决于 Provider:
| Provider | 行为 |
|---|---|
filesystem |
仅在 Grace 到期且已有存储/公开不存在性记录后,条件删除合格对象 |
r2 |
持久化精确的只报告候选集合;绝不发送对象删除请求 |
空操作表示当前没有到期维护任务,并不代表目标已经做过穷尽式重新验证。
退出行为
| 代码 | 触发条件 |
|---|---|
0 |
GC 完成或没有合格对象 |
1 |
文件系统、Provider、网络或其他运行时错误 |
2 |
用法、工作区发现、sow.yml 无效或隐式 Repository 选择有歧义 |
4 |
Repository 写锁不可用 |
5 |
恢复、状态、Receipt 或 Manifest 证据不一致 |
6 |
显式 Repository/目标未配置或不安全,或删除被安全前置条件拒绝 |
参见
sow retain—— 创建与移除显式本地根sow publish—— 创建目标 Checkpoint 与 Receipt- 发布模型 —— Provider 保证与 Cache Grace
5.18 - sow export
SOW 提供一个导出子命令:sow export rpm-leaf。它创建外部、独立的 RPM 仓库,
repodata 使用本地 pool/... href。
语法
| 参数 | 要求 |
|---|---|
DIST |
已配置的规范 RPM Dist 名称 |
ARCH |
x86_64 或 aarch64 |
DIR |
不存在或为空,且不与 Repository、私有状态、filesystem 目标根重叠的目录 |
| 选项 | 含义 | 默认值 |
|---|---|---|
--hardlink |
对可信、同文件系统、只读目标使用硬链接 | 复制文件 |
-C, --workdir DIR |
工作区发现起点 | 当前目录 |
-r, --repo NAME |
选择 Repository | 选择规则 |
--json |
输出 sow.cli/v1 Envelope |
false |
该命令不接受 --dist、jobs、timeout 或锁参数。
输出
目标目录包含:
- 使用本地包体 href 重写的 RPM repodata;
- 所需软件包目录树;
- 导出 Manifest;
.sow-export.json来源记录。
源必须是已完成的 Built Generation。导出物是独立制品,不属于 Desired Membership、 Built Generation、发布输入或 GC 根。
复制与硬链接
复制是安全默认值。--hardlink 只适用于同一文件系统、且消费者无法修改的可信只读目标。硬链接
包体与 SOW 包池共享 inode,不能用于可写或不可信目标。
SOW 会拒绝与已配置 filesystem 发布根重叠的输出,避免导出物被误认为或修改 Managed 发布目标。
退出行为
| 代码 | 触发条件 |
|---|---|
0 |
独立 RPM leaf 导出完成 |
1 |
文件系统、复制、硬链接或元数据写入错误 |
2 |
命令语法、Dist/架构 Token 无效,或发现/隐式 Repository 选择有歧义 |
5 |
源 Generation 或 Repository 状态不一致 |
6 |
显式 Repository 未配置、Dist 不是 RPM、视图/签名者不可用,或目标不安全/非空/重叠 |
参见
- 平台与集成 —— 已验证与明确不支持的工作流
- 仓库布局 —— 源目录与导出边界
sow publish—— 向配置目标执行 Managed 交付
5.19 - sow log
仓库内的每条写命令,都会先在该仓库的 SQLite 中提交一条应用级 Operation,然后才产生任何外部文件
副作用。这条记录让崩溃恢复成为可能——而当 Operation 进入终态之后,同一条记录就是你的审计轨迹。
sow log 读的就是它。
语法
Operation 生命周期
读懂 state 字段,日志就读懂了一大半:
| 状态 | 含义 |
|---|---|
planned |
命令、参数、目标与预期动作已持久化 |
staged |
新包/元数据已写入临时位置并校验通过 |
applied |
期望状态与所需的私有 pending 载荷已提交 |
built |
完整的静态 Generation 已切换 |
done |
终态——一次正常成功的命令 |
done_dirty |
终态——给了 --skip,公开树被有意保留在旧代 |
failed |
终态——在 applied 之前失败,什么都没提交 |
rolled_back |
终态——applied 之后失败,但进程安全地回滚了 |
recovering |
非终态;下一条写命令必须先完成或回滚它 |
工作区生命周期命令(init、repo new、repo rm)走的是工作区文件 journal,不会出现在仓库的
SQLite 日志中。dist new/dist rm 会出现——那时仓库数据库已经存在。
sow log
不带参数时,按由新到旧打印最近 50 条 Operation。
输出节选,operations 数组中的一个 Operation 对象:
payload_json 记录意图——包括当时生效配置的摘要 config_sha256,以及结果 Generation 的
manifest_sha256。result_json 记录结果。失败的 Operation 还会带 error_class 与
error_message:
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-d, --dist NAME |
只显示触及该 Dist 的 Operation | 全部 |
--json |
输出版本化 JSON envelope | false |
查看单条 Operation
给出 Operation ID,就能得到它的完整状态迁移、耗时、包、成员与文件动作。
输出节选:
files 数组使用与 sow changes 相同的 phase 词表:payload、
metadata、pointer、delete。
Build Operation 还会包含进度事件。它们保持当前 state,并把版本化对象放入 detail_json:
阶段包括 rendering、promoting_payload、publishing_dists、
normalizing_public_tree 与 finalizing。进度行是持久审计数据,但不会推进恢复状态机,也不会
单独触发 SQLite checkpoint。
按 Dist 过滤
-d 把列表限制为触及该 Dist 的 Operation——一个仓库服务多个发行版时很有用:
sow log export
把终态 Operation 以 JSONL 写出——每行一条完整的 Operation 明细记录——用于归档或送入日志管道。
省略 FILE 或传 - 则写到 stdout:
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-d, --dist NAME |
只导出触及该 Dist 的 Operation | 全部 |
export 没有 --json——JSONL 就是它的输出格式。
它拒绝覆盖
目标已存在属于拒绝,绝不覆盖——审计导出不能静默毁掉上一份:
export 同样拒绝父目录不是真实目录的目标——符号链接,或根本不存在的目录:
macOS 上 /tmp 是指向 /private/tmp 的符号链接,所以在那里会触发这条拒绝。请写到明确的真实路径。
sow log prune
删除早于 BEFORE 且符合条件的终态审计记录,并安全压缩数据库。
绝对时间戳会被回显,这样本地时区的解释永远不含糊。
| 参数 | 说明 | 默认 |
|---|---|---|
-C, --workdir DIR |
工作区发现的起始目录 | 当前目录 |
-r, --repo NAME |
选择一个仓库 | 按选择规则 |
-T, --timeout DUR |
等待锁的最长时间;0 无限等待 |
0 |
-N, --no-wait |
锁被占用时立即失败 | false |
--json |
输出版本化 JSON envelope | false |
prune 在仓库级工作,不接受 -d——清掉半条 Operation 只会留下毫无意义的记录。
BEFORE 语法
BEFORE 是 ISO-8601 日期 YYYY-MM-DD(按本地时区零点解释),或带时区的 RFC 3339 时间戳。
prune 永不删除什么
prune 在设计上是保守的。它绝不会删除:
- 非终态的 Operation;
- 当前恢复仍需要的记录;
- 当前的 Package 或 Membership 状态;
- Built Generation 或其 Changeset。
pruned 计数准确告诉你有多少条记录符合条件——通常少于截止时间之前的 Operation 总数。日志与
Changeset 位于同一个 SQLite 数据库,但保留规则不同。
示例
排查最近一次写入:
列出所有失败:
每月归档并收缩:
哪条 Operation 最后触碰了某个 Dist:
退出码
| 命令 | 码 | 触发条件 |
|---|---|---|
log |
0 |
记录已打印,包括空账本 |
log |
2 |
用法错误(包括非数字的 Operation ID)、工作区未找到,或选择有歧义 |
log |
5 |
状态数据库不可读 |
log |
6 |
给定的 Operation ID 不存在 |
log export |
0 |
导出成功 |
log export |
1 |
写目标时 I/O 失败,或父目录不是真实目录 |
log export |
2 |
用法错误或选择有歧义 |
log export |
6 |
目标已存在 |
log prune |
0 |
清理完成,包括一条都没清 |
log prune |
2 |
BEFORE 格式非法、给了 -d,或选择有歧义 |
log prune |
4 |
仓库锁被占用,且给了 --no-wait 或 --timeout 到期 |
log prune |
5 |
完整性或恢复错误 |
参见
- 可观测与审计 ——
log与status、check、changes的配合 - 事务与恢复 —— 日志所记录的那个 journal
- sow changes —— 语义 Operation 的物理对照物
- JSON 输出 —— 完整的 log result 结构