跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

设计归档

按日期归档 SOW 在所有权、仓库布局、发布、恢复与兼容性边界上的架构决策。

设计归档解释 SOW 为什么采纳某项契约、否决了哪些替代方案,以及结论实际达到了哪一层证据。 文章按 date 排列,记录设计理由首次形成可维护正文的日期;lastmod 记录后续编辑或与 实现对齐的日期。功能实际在哪个版本交付,仍以发布注记与源码 Tag 为准。

权威与证据

本专栏是持续维护的设计理由与决策历史权威来源。当前命令、配置与运维行为仍以 SOW 文档为准;历史版本的精确行为则绑定对应源码 Tag 与发布注记

每项运维结论都应与它真正达到的证据层一致:

设计 -> 实现 -> 聚焦测试 -> 真实客户端/Provider 实跑 -> 发布物

前一层通过,不代表后一层自动通过。平台与集成 记录自动化客户端、Provider 与文件系统覆盖;Release 制品属于独立交付门禁。

1 - 协同发布决策记录

SOW v0.4.0 保留原生发布 Provider 的最终决策,以及未交付的早期 rclone 提案边界。
SOW 0.4.0 已完成最终决策

本记录的早期版本曾提议采用 rclone Executor、publish --dry-run 与独立 sow audit 命令。这些表面均未进入 0.4.0:SOW 不依赖 rclone,publish 没有 --dry-runsow audit 也不是有效命令。

本页保留决策边界,避免历史提案被误解为当前产品。规范行为以 sow publish发布与恢复为准。

最终决策

SOW 0.4.0 保留原生 filesystemr2 发布 Provider。SOW 同时拥有 Repository 语义,以及 保持这些语义所需的传输操作:

  • 冻结 Generation 与精确 Change Plan;
  • 只创建的 Immutable Object 与 Conditional Mutable Write;
  • 持久 Commit Intent 与确定性 Pointer 顺序;
  • Attempt、Checkpoint、Inventory、Grace 与 Recovery Evidence;
  • Provider 校验与 Canonical Public Visibility Check。

公开承诺保持克制:发布顺序确定、可重启,并最终收敛到一个冻结 Generation;但它不是覆盖所有 Object Key 的原子事务,也不创建 DNS、Bucket Policy、CDN 配置或客户端信任策略。

为什么没有采用 Executor 提案

通用 Bulk-copy Executor 会增加另一个需要版本化的依赖,同时削弱 Plan 中单个 Object 与其 Conditional-write Receipt 的一一对应关系。对于无法保留 SOW 逐对象 SHA-256 Metadata 的批量工具, 还需要引入第二套 Checkpoint Identity 模型。

0.4 因而选择强化既有、更小的传输边界:有界条件式 Multipart Upload、分阶段 Header 与 Idle Progress Deadline、可重放安全操作的重试、精确 Changed-closure Verification,以及共用的 Public HTTP Verifier。这样保留了现有 Checkpoint 模型,也避免把传输迁移伪装成普通升级。

0.4.0 实际交付

原生增量发布

publish 计算精确 Generation Delta,在 Pointer 之前写入 Payload 与校验和寻址 Metadata,持久化 Commit Intent,按确定顺序推进 View,验证 Provider/Public Evidence,并记录 Applied Checkpoint。 Target 已经是当前代时返回幂等 No-op。

操作者确认的 Rebind

publish TARGET --rebind 可修改 Target Name、public_endpointmax_cache_ttl,同时保留稳定 Storage/Target Identity。Storage Endpoint、Provider、Region、Bucket、Repository 与 Prefix 变化 都必须配置新 Target。每次接受的 Rebind 都追加不可变 Audit Revision,并保留 Active Commit-intent Attempt 的前滚恢复。

强化公共可见性

Filesystem 与 R2 HTTP(S) 目标共用 Canonical-GET Verification、陈旧内容的 Cache-TTL 处理、短有界 Transient Retry、独立 Header/Body-idle Deadline 与 Oversize Guard。Filesystem 条件删除还会等待 Canonical Public 404/410 Evidence。R2 删除仍保持禁用、只报告候选。

操作者流程

sow build -r pgsql
sow check -r pgsql
sow changes -r pgsql
sow publish prod

changes 是只读的本地 Generation Delta,不是远端 Dry Run。publish 只接受已配置 Target,并由 自身完成远端 Preflight。机器可读结果使用 --json

报告配置漂移时,应先核对字段。只有允许的可变修正才使用 --rebind;Storage 或 Prefix 变化 必须创建新 Target。

恢复决策

是否已经记录持久 Commit Intent?
├── 否 -> 重跑 publish,或对账后 publish --abort
└── 是 -> 重跑 publish;前滚是唯一合法方向

重复原命令就是 Resume 操作,不存在 --resume。Attempt、Checkpoint、Provider 或 Public Evidence 互相矛盾时失败关闭,不会选择一段看起来方便的历史。

明确边界

  • 没有全前缀远端 sow audit 命令。sow check 证明本地 Repository;普通发布证明精确 Changed Closure 与受影响的 Public Pointer。
  • R2 Target GC 记录精确、只报告的候选,绝不删除远端对象。
  • 多独立写者、分布式锁、自动 Cache Purge、DNS 管理与任意 Executor Plugin 不属于当前契约。
  • Provider 写入成功不代表包管理器验收;部署中的 dnf/APT Client 与签名策略是独立 Release Gate。

参考资料

2 - SOW 设计演进:从 Route Graph 收敛到 Repository Core

v0.1 如何退役、v0.2 如何交付单包体 Repository Core,以及 v0.3-v0.4 如何继续强化。

本文对应 2026-08-10 围绕 Repository Core 完成的 SOW 收敛决策。它从早期规划、ADR、评审、 迁移与验证目录中提炼值得维护的结论,但不会把每份过程制品都变成另一套产品文档。

部分封存目录使用“v0.2”“v0.3”表示连续的发布前设计线;下文版本号只指正式 Git Tag。 C2 硬链接开发线在 v0.2.0 打 Tag 之前已经被替换。

简要历史

版本线 设计中心 历史状态
v0.1 Git/CAS 所有权、Route-aware Materialization、Edge 鉴权、Provider 工作流与 Pigsty 迁移 已发布 v0.1.0;后续被取代
v0.2 本地 Plain/Managed 引擎、Repository Scope 单包体树、纯元数据视图、Retained Generation 与 Target Scope 发布 已发布 v0.2.0;公共布局延续至今
v0.3 移除 V1、Package Facts 缓存、Plain 单遍构建与包体有界组提交 已发布 v0.3.0;公共布局不变
v0.4 单遍校验、独立 RPM Trust Ring、显式 Target Rebind,以及迁移/恢复强化 本文更新时的当前发布基线

每条已发布版本线的精确代码与陈述,仍保存在 v0.1.0v0.2.0v0.3.0v0.4.0 源码 Tag 中。

v0.1:问题很有价值,产品概念太多

第一版计划探索了一套宽广的软件分发控制面:Git-backed Canonical State、CAS、Route 与 View、 Materialized Snapshot、远端 Provider、Edge 鉴权、旧系统迁移与证据门禁删除。四十五份 ADR 和庞大的验证树,把许多单点失败模式都写得很清楚。

这些工作留下了可复用的安全思想,但组合后的产品表面过大。太多概念都可能拥有或重新解释同一份 包体与公开路径;在小型本地仓库模型稳定之前,Edge、Provider、Migration 与 Repository 已经 彼此耦合。

因此 v0.2 重置时淘汰了 Git 正典产品状态、独立 CAS 产品层、Route/View/Snapshot 抽象、 把 Edge Entitlement 作为仓库前置条件的设计,以及“用一个模型同时拥有本地生成、远端分发、 旧系统迁移与 CDN 策略”的目标。

v0.2:先建立小而明确的所有权模型

v0.2 有意把产品拆成两条运行路径:

  • Plain 扫描调用者拥有的目录,就地重建 RPM 或 DEB 元数据。
  • Managed 拥有一个包含独立 Repository 的 Workspace;每个 Repository 都有自己的 Pool、 Dist、SQLite 状态、锁、Operation 与恢复证据。

这次重置建立了沿用至今的持久对象词汇:Package Object、Membership、Desired/Built State、 Generation、Changeset 与显式 Operation Journal。它还确定了稳定锁文件、Pointer-last 文件替换、 Fail-closed 恢复,以及“测试或规格不能自动升级成真实客户端证据”的原则。

开发过程中,C2 布局仍会在每个 RPM View 中创建包体硬链接。封存档案准确记录了这条发布前 设计线,但不能把它重新标注成正式 v0.2.0 Tag 已交付的行为。

v0.2.0:把 Repository Root 变成交付单元

2026-08-05 的单包体决策从 RPM View 中移除了正典包体别名,并在三天后的 v0.2.0 正式交付。 一个 Repository 只有一个正典 pool/ 与纯元数据 dists/;完整根目录才是复制、托管、 鉴权与发布单元。

v0.2.0 同时明确了状态归属:

  • Package Object、Desired/Built、Generation、Changeset 与本地 Retention 属于 Repository。
  • Publication Attempt、Applied Checkpoint、远端 Inventory、Grace 与删除证据属于一个 Repository 加一个 Target Prefix。

设计明确否决跨 Repository 去重与分布式 Prefix 仲裁。这些不是“还没做的优化”,而是会改变 所有权与失败模型的另一类产品。兼容性导出始终位于正典状态之外。

v0.3:在同一模型上做减法与优化

v0.3 保持 v0.2.0 公共布局不变。它删除退役的 V1 CLI/Runtime 与迁移 Harness,引入 Package Facts 缓存,把 Plain 生成简化为可重建的单遍投影,批量展开 Membership,并用有界 组提交替代逐对象 Promotion。这些变化减少了工作量与代码表面,但没有改变 Repository pool/ + dists/ 的所有权。

v0.4:强化证据,不再增加一套模型

v0.4 保持 v0.2.0 引入、v0.3 优化的公共布局不变,把重点放在完整性与恢复。Managed 深度校验 收敛成有界单遍契约; RPM Trust 必须来自一条独立验证的 Trust Ring,不能拼接无关密钥的证据;可变 Target 设置只能 通过显式、可审计的 Rebind 修正;Migration、Publication Recovery 与公共交付检查也得到强化。

早期协同发布提案曾考虑把批量传输交给 rclone。最终决策继续使用原生 Provider,因为发布正确性 取决于 Provider Receipt、精确对象身份、提交顺序、Checkpoint State、公共可见性与恢复, 而不只是字节传输。

每次重构都保留下来的原则

v0.1 的产品模型虽然被淘汰,其中真正有用的教训仍然保留:

  • 每个持久事实都需要一个明确 Owner;
  • 包体与可重建投影不能共享 Authority;
  • 不可变包体与元数据先准备,可变 Pointer 最后提交;
  • Commit 之后只能前滚恢复,证据矛盾必须 Fail Closed;
  • 删除需要同时满足可达性、Grace、所有权与 Provider Capability;
  • Path、Client、Mirror、Provider、Release 与 Deployment 属于独立证据门禁;
  • 历史 PASS 始终绑定当时的版本、源码与环境。

这些原则如今落在一套更小的对象模型中,详见设计原则系统模型单包体仓库发布与恢复

文档权威边界

现在的维护边界很简单:

  • SOW 文档定义当前用户可见的命令、配置、格式与运维行为。
  • 本设计专栏定义维护中的设计理由与按日期记录的决策历史。
  • 发布注记定义版本级交付与升级声明。
  • 源码 Tag 保存精确的历史实现、测试与已被取代的原始文字。

原始规划 Prompt、中间评审、生成的证据摘要与废弃规格仍是历史材料。它们可以解释一个结论如何 形成,但不再是与本站并行的文档权威。

3 - 发布与恢复

发布、恢复、保留与安全删除仓库对象的 target-scoped 状态机。

构建与发布是两个独立状态迁移。构建产生与 target 无关的 Generation;发布把该 Generation 应用到一个供应商前缀,并记录足以在不猜测的前提下恢复的证据。

所有权拆分

Repository 作用域 Target prefix 作用域
Package Object Publication Attempt
Desired 与 Built 状态 Applied Checkpoint
Generation 与 Changeset 远端 inventory
retained 包体/元数据引用 grace 与删除证据

这项拆分避免把 filesystem 发布成功当成 R2 的证据,也避免一个 target 的半完成尝试污染另一个。

持久 Target Identity 与 Rebind

Target 有两层嵌套身份:Storage Identity 覆盖 Provider、Endpoint、Region 与 Bucket;Target Identity 再加入公共树 Prefix。首次绑定后,Repository Identity、两层 Target Identity 与三个 Authority Acknowledgement 都不可变。这样配置文件变化时,Attempt、Checkpoint、Grace 与 Maintenance 才不会被错误解释成另一命名空间的证据。

Target Name、public_endpointmax_cache_ttl 只能通过显式 publish --rebind 修改。首次绑定、迁移回填与 每次接受的 Rebind 都追加不可变 Binding Revision。Rebind 与 Publish 使用同一组独占锁,并在 事务内重查不可变字段;Storage 或 Prefix 变化必须使用新 Target。

Target Maintenance 未完成时,TTL 无论增加还是减少都被冻结;Filesystem Conditional-delete Workflow 还会冻结 public_endpoint。改变当前 TTL 不会重写历史 Grace:删除资格时间取已存 Deadline 与 Applied Checkpoint 加当前 Grace Minimum 中更晚的一个。

发布阶段

plan
  -> 只增不删的包体
  -> 校验和命名的元数据
  -> 持久 commit intent
  -> 按视图逐一写协议指针
  -> Provider 与 Canonical Public Verification
  -> applied checkpoint
  -> grace
  -> 证据门禁删除

提交意图之前,只允许写 add-only object。publish --abort 可以 reconcile 并移除私有 filesystem stage,但不会删除远端对象。SOW 保留精确的 abandoned-object evidence,让后续尝试可以安全识别 并复用相同字节。

首个可变 APT stable alias 或协议指针写入前,commit intent 必须已经持久化。此后唯一合法恢复 方向是前向。对象存储没有多 key 原子提交,因此 SOW 允许一个有界 mixed-generation 窗口, 并按确定顺序逐个把 view 前滚。

指针顺序

每个 view 都先安装不可变内容;签名伴随文件先于对应的可变指针:

RPM: 校验和命名 repodata -> repomd.xml.asc -> repomd.xml
APT: by-hash/direct indexes -> Release.gpg -> InRelease/Release

因此客户端一旦看到新指针,就一定能取到它声明的全部对象与签名。

公共验证

日常发布验证精确 Changed Closure 与所有受影响的最终 Pointer,不会从公网重新下载每个未变化 Package。Provider Receipt 与 Public Visibility 是两种独立证明。Filesystem 与 R2 HTTP(S) 目标共用 Canonical-GET Verifier:响应 Header 与 Body Idle 分别计时;Size+1 防止超长响应; 408/425/429/5xx 使用短有界重试;陈旧内容或缺失对象按 Cache TTL 重试。

No-cache 请求只是 Revalidation Hint;只有之后的普通 Canonical GET 看到期望字节,发布才能写入 Checkpoint。Filesystem 条件删除以 HTTP 404/410 证明公共缺失;陈旧 200 会重试至 TTL。 file:// 则使用精确、基于描述符的缺失证明。R2 Public Absence 与远端删除仍保持禁用。

Filesystem 别名会在持久绑定前按 Prospective Canonical Path 检查,包括大小写不敏感卷上的大小写 别名。Backend/Path Preflight 失败不会创建 Binding Row,也不会创建 Target Prefix。

已发布指针栅栏

一个 configured target 已经拥有 Applied Checkpoint 后,本地配置不能静默撤销该 target 仍拥有的 公开 Dist、architecture 或签名指针。操作者必须先退役/解绑 target,或者用新名称和新 prefix 发布替代品。

这样,危险的“配置漏了一项”会变成显式生命周期决策。

不复制包体的保留机制

Retained Generation 保存元数据、manifest 与引用集合,不保存另一棵包体树。Repository-local 可达性包括:

  • 当前 Desired/Built Membership;
  • retained Generation 引用;
  • active operation 与 recovery journal;
  • publication grace 与 recovery root。

只有正典 Pool 对象位于完整 closure 之外,且当前文件身份仍与候选记录精确一致时,本地 GC 才允许删除它。

远端删除是一项能力

远端物理删除还要求权威 inventory、target ownership、grace 到期、必要的缓存 absence evidence, 以及原子条件删除 primitive。无法满足这些条件的供应商仍可用于发布,但 SOW 只能报告不可达 候选,不能发出不安全的无条件删除。

r2 Provider 采用这种处理:发布路径已实现,但明确禁用远端物理删除,target GC 只报告候选。

恢复结果

持久边界 合法结果
没有 commit intent reconcile 后放弃或重试
已有 commit intent 只能前滚
已有 Applied Checkpoint 收敛并进入 grace
证据互相矛盾 失败关闭,不虚构状态

4 - 系统模型

连接软件包、Dist、Generation 与发布目标的对象模型和状态迁移。

SOW 刻意把模型分层:配置表达意图,数据库记录有主状态,公共树是确定性投影。 任何一层都不能悄悄替代另一层。

对象层级

Workspace
├── Repository
│   ├── Package Object
│   ├── Dist
│   │   └── Membership -> Package Object
│   ├── Desired 状态
│   ├── Built Generation
│   └── Retained Generation 引用
└── Publication Target
    └── Repository + provider + prefix

Workspace

Workspace 提供发现、配置与协调,拥有 sow.yml、私有 .sow/ 目录和稳定锁路径; 它不是包体去重域。

Repository

Repository 是最小的自包含公共归档,也是软件包身份的作用域。它拥有一份 pool/、一组 dists/、一个状态数据库和一条 Generation 序列。即使输入相同,两个 Repository 也不共享 包体与计数器。

Package Object

Package Object 由完成全部包签名后的最终 SHA-256 标识。名称、版本、release 与架构等逻辑坐标 只是元数据,digest 才是字节身份。重复加入同一 digest 是幂等操作;同一正典 Pool 路径出现 不同字节则是硬冲突。

Dist 与 Membership

Dist 是一套 APT 或 RPM 发布策略及其成员集合。Membership 是多对多关系:一个 Package Object 可以属于多个 Dist,但不会因此多出一份正典包体。中性包(all / noarch)会投影到每个 适用架构视图中,逻辑上仍只有一条成员关系。

Desired、Built 与 Generation

Desired 是配置和包操作想要的状态;Built 是最后一次完整渲染并校验通过的公共树。 Generation 是该 Built 状态的不可变清单,Changeset 则是两份清单之间的精确差异。

Desired 与 Built 分离后,中断状态可以被诚实描述:意图可能已经改变,但上一次提交的公共树 仍然有效。

Publication Target

target 把一个 Repository 绑定到供应商 endpoint 与 prefix。发布尝试、已应用 checkpoint、 远端 inventory、grace 与删除证据都属于 target。构建 Repository 与目标无关,发布则不是。

Storage Identity(Provider/Endpoint/Region/Bucket)、Target Identity(Storage 加 Prefix)与 Repository Identity 都是持久身份。只有 Target Name、Public Endpoint 与 Cache TTL 可通过操作者 确认的 Rebind 修改;每次接受的变化都会追加不可变 Binding Revision。

状态流

软件包输入
    -> 检查/签名/哈希
    -> Package Object + Membership
    -> Desired 状态
    -> 渲染并校验
    -> Built Generation + Changeset
    -> target 发布尝试
    -> applied checkpoint

每一条箭头都受 journal 或事务保护;后一阶段消费前一阶段的不可变身份,不重新解释可变路径。

公共状态与私有状态

公共:必须整体复制 私有:绝不能对外服务
<repo>/pool/ sow.yml
<repo>/dists/ .sow/ 数据库与 journal
协议签名与索引 锁、stage、恢复前镜像
Generation 描述的常规文件 凭据与供应商回执

只复制公共树可以得到一个有效的静态仓库,但它不是权威写入者:缺少匹配的私有状态时, 它无法安全续接发布历史、保留策略或垃圾回收。

锁边界

Workspace 生命周期操作取得 Workspace 锁;Repository 变更取得稳定 Repository 锁。 两者都需要时,固定先 Workspace、后 Repository,释放顺序相反。稳定锁路径避免 rename 或恢复 操作在新 inode 上意外形成第二个写入者。

模型假设每个配置 target prefix 只有一个权威 Workspace 且写权限独占;多个独立 Workspace 之间的分布式仲裁不在目标范围内。

5 - 设计原则

SOW 用来约束所有权、派生状态、发布与证据的一组不变式。

SOW 首先不是一个“元数据生成器”,而是一个所有权与状态迁移系统,只是它的输出恰好是 APT 与 RPM 仓库。下面这些原则让整套系统保持可推理。

每个持久事实只有一个主人

每类持久事实都只有一个作用域与权威:

事实 所有者
包体与软件包身份 Repository
Desired 成员关系与 Built 状态 Repository
Generation 与 Changeset Repository
发布目标绑定与 Revision Repository + Target Storage/Prefix
发布尝试与已应用 Checkpoint Repository + target prefix
远端 inventory、grace 与删除证据 Repository + target prefix

状态不会在 Repository 或发布前缀之间暗中共享。因此同一个包可以在不同 Repository 或 target 中各存一份;这是有意为之:本地去重不能制造分布式所有权。

正典数据与可重建投影

Managed 模式下 pool/ 中的包体是正典数据;Plain 模式下,顶层包文件本身就是正典。 协议索引、架构视图、报告与兼容导出都是投影,永远不会成为包体的第二个主人。

耐久规则服从权威来源。Managed 通过有记录的操作移除投影,且只有计算完所有 live / retained owner 的可达性才删除正典数据;Plain 则直接按当前包目录重新生成自有索引路径。

公共树是交付单元

Repository 根由 pool/ + dists/ 组成;整棵树才是静态托管、复制、鉴权与发布单元。 某个 RPM 架构 leaf 是客户端视图,但它不是一个独立拥有状态的 Repository。

sow.yml.sow/、锁、journal、凭据与恢复文件都是私有状态,绝不能随公共树对外服务。

包体做准备,指针做提交

发布顺序固定为:

包体 -> 不可变/校验和命名的元数据 -> 可变指针 -> 宽限期 -> 删除

包体与不可变元数据可以提前写入而暂不可见;repomd.xmlReleaseInRelease 这样的 协议指针才是提交边界。只有新指针已经持久化、旧读者与缓存窗口已经关闭后,旧对象才可删除。

恢复成本服从状态成本

Plain 没有需要保存的期望状态历史。它成本最低的正确恢复方式就是重新做一遍内容扫描并覆盖重建, 因此不保存事务 journal,也不花与包体大小成正比的 I/O 去证明上一次尝试。

Managed 状态不同。提交意图之前,如果精确 reconcile 能证明公共指针从未改变,操作可以放弃; 提交意图之后只能前向恢复。Managed 不猜测一次半完成发布“应该已经成功”,而是比较 journal、 manifest、checkpoint、供应商身份与公共树。

证据互相矛盾时操作必须停止。一次可见的拒绝,比仓库历史悄悄分叉更安全。

兼容性是一张矩阵

协议合规、普通客户端、镜像工具、对象存储布局、代理规范化与文件系统语义是不同问题。 SOW 分别记录它们,并用与结论对应的真实客户端或供应商验证。

因此规范 Repository 与导出的 RPM 镜像 leaf 是两种独立产物;其中一种产物的证据不能证明 另一种产物兼容。

证据不会自动升级

规格不是实现,单测不是真实客户端结果,本地 Hugo 构建也不是公网发布。日期化结果始终绑定 当时的源码 revision、环境与版本;布局变化后,必须重跑对应门禁,不能把旧 PASS 直接继承。

非目标让模型保持诚实

当前契约不承诺跨 Repository 去重、重叠多写者、bucket 全局协调、任意第三方镜像工具兼容, 也不在供应商缺少原子条件删除时承诺安全远端删除。这些排除项属于安全契约,不是“尚未补完”。

6 - 为什么 SOW 每个仓库只保留一棵包体树

用唯一正典 Pool 与纯元数据视图,替代 RPM 视图内包体硬链接的设计决策。

这项决策起草于 2026-08-05 的 v0.2 发布前设计收敛阶段,并在 2026-08-08 随 SOW v0.2.0 正式交付,随后延续为 v0.3 与 v0.4 的布局契约。它记录的是架构选择,不是笼统的兼容性 PASS:协议闭包、普通软件包客户端、镜像工具、静态托管与对象存储始终属于彼此独立的证据层。

最终决策

每个 SOW Repository 只拥有一棵公共树:

<repository>/
  pool/                         # 正典包体
  dists/
    <dist>/
      <architecture>/           # 只有协议元数据

一个 Package Object 在一个 Repository 内只有一个正典包体路径。把同一对象加入多个 Dist 或架构视图,只会增加 Membership 与元数据引用,不会再创建一份正典包文件。

完整的 pool/ + dists/ 根目录才是受支持的复制、静态托管、鉴权与发布单元。单个架构 Leaf 只是客户端视图,不是独立拥有状态的仓库。

从 C2 开发线到 v0.2.0

早期发布前 C2 开发线已经把每个包在根 Pool 中存一份,但会在每个 RPM View 内再创建包体 硬链接。这套布局可以让选定的 reposync 客户端得到自包含 Leaf,同时避免在本地复制字节。

问题在于,这种 inode 优化无法穿过完整交付边界:

  • 对象存储会把每条路径展开成独立 Key,别名重新变成重复包体;
  • 归档与跨文件系统复制可能展开或丢失硬链接身份;
  • View 内包路径看起来像正典数据,但所有权其实仍属于 Repository;
  • Retention 与 GC 容易误把 link count 当作产品状态;
  • 为迁就单个镜像工具,会迫使正典树服从它的本地输出假设。

在 v0.2.0 打 Tag 之前,最终单包体设计已经从正典 RPM View 中删除包体别名。归档中的 C2 文档仍准确描述那条开发线,但不能用来描述正式 v0.2.0 Release Tag。

元数据如何引用 Pool

APT 的 Filename 本来就是相对 Archive Root 的路径,因此软件包条目直接写正典路径:

Filename: pool/p/pkg/pkg_1.0_amd64.deb

RPM 则根据实际 View 目录与正典 Pool 路径计算每个 <location href>

view:    dists/el9/x86_64/
payload: pool/p/pkg/pkg-1.0-1.x86_64.rpm
href:    ../../../pool/p/pkg/pkg-1.0-1.x86_64.rpm

Renderer 与 Checker 复用同一个路径函数:先规范化 Repository-relative Path,再计算字面父级 导航,对数据字节恰好转义一次,基于目录 URI 解析结果,并要求 Round Trip 精确回到原 Pool 路径、且不能离开 Repository。

这样公共树仍然可搬迁:只要 pool/dists/ 的相对关系不变,同一份字节无需改写 元数据,就可以复制到另一目录、HTTP Origin、Bucket 或 Prefix。

被否决的方案

方案 为什么不进入正典布局
每个 RPM View 放包体硬链接或副本 制造路径别名与重复对象 Key,把文件系统优化伪装成持久所有权
包体 Symlink 无法移植到对象存储,在静态服务、归档与信任边界上也不安全
使用 /pool/... 绝对 href 不同客户端会把它解释成 Host Root 或 Repository-relative,也会破坏非根 Prefix
绝对 URL 或 xml:base 把元数据绑定到单一 Origin,无法原样搬迁
依赖 CDN Rewrite 或 Redirect Object 把外部 Router 变成仓库正确性的一部分
Workspace 或 Bucket 全局 CAS 引入 SOW 并不承诺的跨 Repository 所有权、锁、Retention 与删除协调

镜像工具兼容属于导出问题

普通软件包客户端只需解析元数据引用并取得包体;镜像工具还可能强制要求每个包都位于它收到的 View 目录之下。这是两种不同契约。

SOW 不会为了覆盖所有镜像工具而扭曲正典 Repository。需要独立 RPM Leaf 时, sow export rpm-leaf 会在仓库外生成带本地元数据的兼容制品。默认使用 Copy; --hardlink 只是受控只读边界内、同文件系统上的显式优化。导出目录不是 Generation、 发布源、Membership Owner 或 GC Root。

客户端与镜像工具结果统一记录在兼容性矩阵。 结构化 sow check 成功只能证明路径、摘要与闭包不变式, 不能把尚未运行的客户端或 Provider 单元格自动升级为 PASS。

结果

  • Package Identity、Membership、Generation、Retention 与本地 GC 都保持 Repository Scope。
  • Publication Attempt、Checkpoint、远端 Inventory、Grace 与删除证据保持 Target-prefix Scope。
  • 每个正典文件一对一映射到静态对象 Key。
  • 包体删除服从引用闭包与精确文件身份,不依赖 inode link count。
  • 不支持只复制某个 RPM 架构 Leaf;应复制 Repository Root 或显式创建 Leaf Export。

当前实现机制见包池与元数据视图;周边所有权与生命周期契约见 系统模型发布与恢复