跳转到主要内容

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

返回本页常规视图.

教程

构建、签名、托管并更新 RPM 与 DEB 软件仓库。

本节介绍仓库创建、签名、发布与更新。尚未安装 SOW 时,先看安装。

替换平面 RPM 仓库的元数据生成工具,保留发布时间、签名与客户端缓存兼容性。

创建 RPM 架构视图,过滤软件包,设置版本上限并配置 DNF。

签署元数据与 RPM 软件包,再配置客户端验签。

发布到 filesystem 目标,并用 Nginx 提供服务。

同时使用两种格式与架构,测试安装,维护滚动频道、稳定发布与月度快照。

使用示例

按顺序执行命令,把大写占位符与 /path/to/ 输入路径换成实际值。/srv 下的示例需要可写目录,也可以换成用户主目录下的路径。

YUM 与 APT 教程分别从新工作区开始。如果要合并使用,在已有 pigsty 仓库中创建第二个 Dist,并在 sow.yml 保留两个 Dist 的定义。

各教程都包含结果检查。命令报错或操作中断时,参见故障处理。

1 - 搭建 YUM 仓库

创建托管 RPM 仓库,配置成员策略,对外服务并接入 dnf。

本教程从零创建一个 Managed RPM 仓库。先安装 SOW、Python 3,并准备适合目标客户端的 RPM 文件。使用当前用户可创建、可写入的空目录 /srv/sow,或换成其他可写路径。

1. 创建工作区

mkdir -p /srv/sow
cd /srv/sow
sow init .
sow repo new pigsty
sow dist new el9 --format rpm -r pigsty

el9 是这个 RPM 包集合的名称,可以按需要更改。它不改变软件包的操作系统兼容性。

2. 配置成员策略

编辑生成的 sow.yml。下面的配置按包名与架构各保留一个版本,并排除调试包与 LLVM JIT 包:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  pigsty:
    dists:
      el9:
        format: rpm
        limit: 1
        exclude:
          - kind: [debuginfo, debugsource, llvmjit]
targets: {}

检查配置:

sow config check

策略先执行 exclude,再执行 limit。SOW 把 noarch 包加入每个启用的架构视图。architectures 中只填写 CPU 架构名。

3. 添加 RPM

sow add /path/to/packages/*.rpm -r pigsty -d el9
sow check -r pigsty

把输入路径换成实际 RPM 目录。add 将接受的软件包存入包池,更新成员并构建索引。检查逐包输出:被排除的文件会单独报告,不会使整条命令失败。

公共树如下:

/srv/sow/pigsty/
├── pool/...
└── dists/el9/
    ├── x86_64/repodata/...
    └── aarch64/repodata/...

RPM 索引通过相对路径指向 pool/。服务或复制完整的 pigsty/ 目录树,客户端才能下载软件包。

4. HTTP 预览

在另一个终端执行以下命令,并保持运行:

python3 -m http.server --bind 127.0.0.1 --directory /srv/sow/pigsty 8080

预览只提供仓库根目录中的文件,并且只能从本机访问。长期运行或供其他主机访问时,按对外服务配置。

5. 配置 dnf

在运行预览的 Linux 主机上,把以下内容保存为 /etc/yum.repos.d/pigsty.repo。远程客户端需先发布仓库,再换成其公共地址:

# /etc/yum.repos.d/pigsty.repo
[pigsty-el9]
name=Pigsty EL9
baseurl=http://127.0.0.1:8080/dists/el9/$basearch/
enabled=1
gpgcheck=0
repo_gpgcheck=0

刷新并查询仓库:

sudo dnf --disablerepo='*' --enablerepo=pigsty-el9 makecache --refresh
dnf --disablerepo='*' --enablerepo=pigsty-el9 list available

本例使用未签名软件包与元数据。按仓库签名启用验签。预览完成后,删除此源配置文件并停止 Python 服务器。

6. 发布或导出

使用 sow publish 发布到 filesystem 或 R2 目标。它按客户端所需的顺序上传软件包与元数据。隔离主机的手工交付步骤也见该教程。

默认 dnf reposync 等工具会拒绝指向根包池的父级相对路径。需要供这些工具使用时,导出一份 独立 RPM 仓库:

sow export rpm-leaf el9 x86_64 /srv/export/pigsty-el9-x86_64 -r pigsty

目标目录必须不存在或为空。导出默认复制软件包。--hardlink 只用于同一文件系统;导出文件与源包池共享存储,应保持只读。

更新仓库

sow add /path/to/new.rpm -r pigsty -d el9
sow check -r pigsty

add 会自动构建更新。配置 limit: 1 后,同包名、同架构的最新版本会替换此前成员。修改配置后执行 sow build -r pigsty。

撤下软件包时,先用 sow rm PACKAGE_NAME -r pigsty -d el9 --check 预览选择结果,再去掉 --check 执行。包名会选中其全部版本与架构;只删除一个包对象时,使用 SHA-256 引用。

已测试客户端见平台与集成。

2 - 迁移已有 YUM 仓库

将已有平面 RPM 仓库切换至 SOW,保留客户端缓存兼容性、软件包信任与发布时钟。

本指南适用于 RPM 与 repodata/ 放在同一层目录的平面仓库,例如此前用 createrepo_c 维护的仓库。它使用 Plain sow create,不创建 Managed 工作区。发布时间参数是 SOW 0.5.0 新增功能,请使用该版本或更新版本。

1. 准备工作副本

备份当前软件包与元数据,再准备独立工作副本用于迁移。计划重签的 RPM 使用独立复制,不使用硬链接。检查已安装的二进制:

SOW="$(command -v sow)"
"$SOW" version

确认版本为 0.5.0 或更新版本。

检查项 操作
软件包布局 Plain 只读取顶层普通 RPM;嵌套目录需要单独规划迁移
客户端功能 Plain 生成 primary/filelists/other XML,不生成 SQLite 数据库或模块流
包信任 保留合法上游签名,或明确换成自己的密钥
索引信任 客户端启用 repo_gpgcheck=1 时,单独签署并验证新 repomd.xml
已有客户端 保留旧缓存,使用原来的 URL 和 repo ID 验收

客户端使用模块过滤时,按需要保留仓库原有的 module_hotfixes=1 设置。Plain 元数据不包含模块流或 profile。

2. 保留发布时间历史

EL7 YUM 可能拒绝最大 data 时间戳早于缓存的新索引。读取当前 repomd.xml 与保留副本中的 <data><timestamp>,取客户端可能见过的最大值。如果在线索引被重置为零,从旧副本或发布记录恢复这个值。使用 data 时间戳,不使用 revision 或文件修改时间。

维护脚本需串行执行更新,并在服务目录之外保存最大发布时间。内容变化时,选择:

下一次时间 = max(当前 Unix 秒, 已知的最大发布时间 + 1)

切换公开索引前保存所选时间戳。发布失败可以使序列跳号。恢复旧内容或迁移本地目录时,保留这份记录。+1 用于区分发布;EL7 也接受相等时间戳。

内容不变时,检查索引、引用文件校验和与签名后复用时间戳。零时间戳或已倒退的时间戳需要修复。每次传入当前时间都会改变 repomd.xml,此时 create 就无法报告空操作。

3. 构建并签署候选

下面演示一次重建。先准备独立的 /srv/yum.candidate 副本。 将 HISTORICAL_MAX 设为从当前索引、已保存的最大发布时间和可信历史中核实的最大值。 将 SIGN_KEY 设为元数据签名密钥的完整指纹。 示例用 Python 3 选择严格递增的时间:

set -euo pipefail
: "${SOW:?Set the path to the tested SOW binary}"
: "${HISTORICAL_MAX:?Set the verified historical maximum Unix second}"
: "${SIGN_KEY:?Set the full metadata signing fingerprint}"
PUBLISH_TIME=$(python3 -c 'import sys,time; h=int(sys.argv[1]); assert 0 <= h < 253402300799; print(max(int(time.time()), h+1))' "$HISTORICAL_MAX")

"$SOW" create /srv/yum.candidate --metadata-timestamp "$PUBLISH_TIME"
gpg --batch --yes --armor --detach-sign --local-user "$SIGN_KEY" \
  --output /srv/yum.candidate/repodata/repomd.xml.asc \
  /srv/yum.candidate/repodata/repomd.xml
gpg --verify /srv/yum.candidate/repodata/repomd.xml.asc \
  /srv/yum.candidate/repodata/repomd.xml

发布候选目录前,在维护系统中记录 PUBLISH_TIME。核对 gpg --verify 显示的指纹与预期签名密钥是否一致。

--sign-with KEY 签署未签名 RPM,并保留已有签名,包括供应商签名。要把全部包签名换成自己的密钥,在候选副本中增加 --overwrite。这会改变包文件;create 根据签名后的结果生成索引。最后一次 create 完成后,再生成索引的分离签名。

4. 测试候选仓库

让测试客户端通过原来的仓库 URL 访问候选目录。保留该客户端的旧缓存、仓库 ID 与签名设置, 然后刷新索引、下载并安装一个包。这样可以同时检查缓存兼容性、下载路径与签名。

如果迁移的是自动更新脚本,再检查两种情况:

  1. 输入、选项与时间戳不变时,重复构建报告 noop=true。保留已有的有效索引签名。
  2. 包内容变化时,脚本增加时间戳,同一个客户端能获取新版软件包。

与 createrepo_c 比较时,检查依赖含义与客户端行为;XML 排版可以不同。 详情见平台与集成。

5. 发布仓库

Plain 会替换多个文件,不提供整个目录的原子切换。发布层需要协调包文件、索引、分离签名以及 缓存的可见顺序。仍可能被缓存客户端请求的旧元数据文件应继续保留;直接运行 Plain 重建,可能 删除目录中不再引用的 SOW 以校验和命名的元数据。

保留原始备份与已保存的最大发布时间。回滚软件包内容时,把它当成更晚的一次发布重新建库、签名。只恢复旧 repomd.xml、却搭配已经改变的 RPM,会重新引入校验和或时间戳问题。

Plain 仓库使用上述检查;sow check 用于 Managed 仓库。命令参数见 sow create,包签名与元数据签名见仓库签名。

3 - 搭建 APT 仓库

创建带 by-hash 索引的托管 DEB 仓库,并配置 APT 客户端。

本教程从零创建一个 Managed DEB 仓库。先安装 SOW、Python 3,并准备适合目标客户端的 DEB 文件。使用当前用户可创建、可写入的空目录 /srv/sow,或换成其他可写路径。

1. 创建工作区

mkdir -p /srv/sow
cd /srv/sow
sow init .
sow repo new pigsty
sow dist new trixie --format deb -r pigsty

Dist 名称 trixie 对应客户端配置中的 Suites,可以按需要更改。它不改变软件包的发行版兼容性。

2. 配置成员策略

如果需要过滤或限制版本,编辑生成的 sow.yml:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  pigsty:
    dists:
      trixie:
        format: deb
        limit: 1
        exclude:
          - kind: [dbgsym, dbg]
targets: {}

然后校验:

sow config check

配置中的 x86_64、aarch64 分别生成 Debian 的 amd64、arm64 索引。 架构无关的 all 包进入两个视图。

3. 添加 DEB

sow add /path/to/packages/*.deb -r pigsty -d trixie
sow check -r pigsty

把输入路径换成实际 DEB 目录。add 将接受的软件包保存一次,并构建索引。公共目录树如下:

/srv/sow/pigsty/
├── pool/...
└── dists/trixie/
    ├── Release
    └── main/
        ├── binary-amd64/
        │   ├── Packages
        │   ├── Packages.gz
        │   └── by-hash/SHA256/...
        └── binary-arm64/...

Packages 中的 Filename 相对于仓库根目录。保留完整目录树,客户端才能找到软件包。SOW 生成 by-hash 索引,供 APT 按内容哈希获取元数据。

4. HTTP 预览

在另一个终端执行以下命令,并保持运行:

python3 -m http.server --bind 127.0.0.1 --directory /srv/sow/pigsty 8080

预览只提供仓库根目录中的文件,并且只能从本机访问。长期运行或供其他主机访问时,按对外服务配置。

5. 配置 APT

在运行预览的 Linux 主机上,把以下内容保存为 /etc/apt/sources.list.d/pigsty.sources。ARM64 客户端将 Architectures 改为 arm64。远程客户端需先发布仓库,再换成其公共地址:

# /etc/apt/sources.list.d/pigsty.sources
Types: deb
URIs: http://127.0.0.1:8080
Suites: trixie
Components: main
Architectures: amd64
Trusted: yes

刷新并查询:

sudo apt update
apt-cache policy

输出中应出现 trixie/main 的软件包来源。Trusted: yes 用于本例的未签名仓库。 需要签名时,按仓库签名生成签名、安装公钥并配置 Signed-By。 预览完成后,删除测试源配置文件并停止 Python 服务器。

6. 发布仓库

使用 sow publish 发布到 filesystem 或 R2 目标。它先上传软件包与索引,再更新 Release 及其签名。隔离主机的手工交付步骤也见该教程。

更新仓库

sow add /path/to/new.deb -r pigsty -d trixie
sow check -r pigsty

add 会自动构建更新。配置 limit: 1 后,同包名、同架构的最新版本会替换此前成员。修改配置后执行 sow build -r pigsty。

撤下软件包时,先用 sow rm PACKAGE_NAME -r pigsty -d trixie --check 预览选择结果,再去掉 --check 执行。包名会选中其全部版本与架构;只删除一个包对象时,使用 SHA-256 引用。

已测试客户端见平台与集成。

4 - 仓库签名

签署 RPM 与 APT 元数据,可选签署 RPM 包体,并启用客户端验签。

仓库签名分为元数据签名与 RPM 软件包签名两层,需要分别配置:

路径 产物 客户端开关
RPM 元数据 repodata/repomd.xml.asc repo_gpgcheck=1
APT 元数据 InRelease 与 Release.gpg Signed-By
RPM 包体 RPM 内嵌签名 gpgcheck=1

APT 通过签名的 Release 文件验证包哈希,因此 DEB 不需要单独的包签名。先配置元数据签名;客户端要求 gpgcheck=1 时,再配置 RPM 软件包签名。

本教程使用 YUM 或 APT 教程中的 pigsty 仓库。只执行你已创建的格式与 Dist 所对应的命令。

1. 创建专用密钥

先安装 GPG。下面生成一把无口令的测试密钥,并导出到私有目录。已有带口令密钥时,按下文配置 passphrase 引用。

SIGNING_UID='SOW Repository <[email protected]>'
gpg --batch --pinentry-mode loopback --passphrase '' \
  --quick-generate-key "$SIGNING_UID" rsa3072 sign 2y

FPR="$(gpg --batch --with-colons --list-secret-keys "$SIGNING_UID" \
  | awk -F: '$1 == "fpr" {print $10; exit}')"
test -n "$FPR"

sudo install -d -m 0700 /srv/sow-secrets
sudo chown "$(id -u):$(id -g)" /srv/sow-secrets
gpg --batch --pinentry-mode loopback --passphrase '' --armor \
  --export-secret-keys "$FPR" > /srv/sow-secrets/repo-signing.asc
gpg --armor --export "$FPR" > /srv/sow-secrets/repo-signing.pub
chmod 600 /srv/sow-secrets/repo-signing.asc

SOW 从公共仓库目录之外的 /srv/sow-secrets 读取私钥。如果 SOW 服务账户不同于当前用户,给该账户目录访问权限。向客户端分发 repo-signing.pub。

2. 配置元数据签名

在 /srv/sow/sow.yml 中,将以下元数据配置合并到仓库已有的 signing 块。保留包签名策略与 Dist 定义,只加入使用的格式:

repos:
  pigsty:
    signing:
      rpm:
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc
      deb:
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc
    dists:
      # 保留已有 Dist 定义

校验配置、重新构建并检查:

cd /srv/sow
sow config check
sow build -r pigsty
sow check -r pigsty

带口令的密钥在 key 旁增加 passphrase: env://SOW_METADATA_PASSPHRASE,并向 SOW 提供该环境变量。也可以使用口令文件,或通过 agent://FINGERPRINT 调用 GPG 签名。详情见 passphrase 引用。配置只保存引用,不保存秘密值。

3. 可选:查看元数据签名者

前面的 sow check 已验证元数据签名。需要用 GPG 查看签名者时,执行对应 Dist 的命令:

gpg --verify \
  pigsty/dists/el9/x86_64/repodata/repomd.xml.asc \
  pigsty/dists/el9/x86_64/repodata/repomd.xml

gpg --verify pigsty/dists/trixie/InRelease
gpg --verify \
  pigsty/dists/trixie/Release.gpg \
  pigsty/dists/trixie/Release

将显示的指纹与分发给客户端的公钥指纹比较。

4. 可选:签署 RPM 包体

新仓库应在导入 RPM 前配置包签名。在已有 signing.rpm 下增加以下配置:

repos:
  pigsty:
    signing:
      rpm:
        packages:
          mode: fill
          key: agent://REPLACE_WITH_THE_FINGERPRINT
        metadata:
          key: file:///srv/sow-secrets/repo-signing.asc

将占位符替换为 $FPR 中的 40 位十六进制指纹。安装 rpm 与 gpg, 并将私钥放在 rpm 使用的 GPG 密钥环中。

fill 会签署新导入的包;如果已有签名能通过配置密钥或 trusted_keys 验证,则保留原签名。 其他模式见 RPM 签名策略。

SOW 在 add 时签署私有副本,输入文件保持不变。build 检查包池中的 RPM 是否满足策略,不会重签它们。

已有 RPM 时,先检查签名再启用 fill。把信任的上游公钥保留在 trusted_keys 中。对于包池中未签名或不受信任的包,准备使用新版本、新文件名且签名正确的包;也可以先配置签名,再把原始包导入新仓库。

配置策略后,导入新包并检查结果:

sow config check
sow add /path/to/new.rpm -r pigsty -d el9
sow check -r pigsty

需要独立查看包签名时,对 pigsty/pool/ 中的包文件执行 rpmkeys --checksig。

5. 启用 dnf 验签

先发布已签名仓库,再通过可信通道把公钥传到客户端:

sudo install -m 0644 /path/to/repo-signing.pub /etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty
sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty

再打开与你实际签名范围对应的检查:

[pigsty-el9]
name=Pigsty EL9
baseurl=https://repo.example.com/pigsty/dists/el9/$basearch/
enabled=1
repo_gpgcheck=1
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-pigsty

只签名元数据时,使用 repo_gpgcheck=1 与 gpgcheck=0。启用包验签时,除了自己的公钥,也要安装所信任的上游包签名公钥。随后执行 sudo dnf makecache --refresh 并安装一个包。

6. 启用 APT 验签

发布已签名仓库后,在客户端将公钥安装为独立密钥环:

sudo gpg --dearmor --yes \
  --output /usr/share/keyrings/pigsty-archive-keyring.gpg /path/to/repo-signing.pub
sudo chmod 0644 /usr/share/keyrings/pigsty-archive-keyring.gpg

在 deb822 配置中引用它,且不要设置 Trusted: yes:

Types: deb
URIs: https://repo.example.com/pigsty
Suites: trixie
Components: main
Architectures: amd64
Signed-By: /usr/share/keyrings/pigsty-archive-keyring.gpg

执行 sudo apt update 并安装一个包。验签失败时,核对客户端公钥环指纹与 gpg --verify 显示的签名者是否一致。ARM64 客户端将 Architectures 改为 arm64。

Plain 模式 RPM 签名

Plain 模式可以签署 RPM 包体,但不会签署仓库元数据,也不会生成 APT Release:

sow create /srv/flat --sign-with 0123456789ABCDEF

将 0123456789ABCDEF 换成你的密钥 ID 或指纹,匹配的私钥必须能被 rpm 与 GPG 使用。 命令保留已有签名,包括上游签名。需要全部换成自己的密钥时,在独立工作副本上使用 --overwrite。 密钥格式见 sow create。

已有 YUM 仓库按迁移指南保留发布时间, 并在最后一次 create 完成后签署 repomd.xml。

更换密钥

轮换元数据密钥时,先分发新公钥,再更新密钥引用,执行 build、check 与 publish。引用或解析出的指纹变化后,相关 Dist 会变为 dirty。

轮换 RPM 包密钥时,使用 fill,并在 trusted_keys 中保留旧公钥,直到旧密钥签署的软件包已撤下或替换。在此期间,也保留客户端对应的公钥。已有包对象保持不变。

用用户实际使用的 DNF 或 APT 版本测试最终仓库。自动化覆盖见平台与集成。

5 - 服务与发布仓库

把托管仓库发布到独立目录,并用 Nginx 对外提供服务。

SOW 生成静态仓库文件。本教程把 Managed 仓库发布到独立目录,再用 Nginx 提供服务。先完成第一个工作区。

源目录是 /srv/sow/local,公开副本将位于 /srv/repo-public/local。 两者都包含完整的 pool/ 与 dists/ 目录树。工作区配置与私有状态保留在服务目录之外。

1. 构建并检查

cd /srv/sow
sow build -r local
sow check -r local

check 成功后继续。

2. 添加 filesystem 目标

创建目标父目录,并给 SOW 操作者写权限:

sudo install -d -m 0755 /srv/repo-public
sudo chown "$(id -u):$(id -g)" /srv/repo-public

如果执行 sow publish 的账户不同,使用该账户。endpoint 必须已存在,并使用不含符号链接的物理路径。

在 /srv/sow/sow.yml 添加目标:

targets:
  public:
    repository: local
    provider: filesystem
    endpoint: file:///srv/repo-public
    prefix: local
    public_endpoint: file:///srv/repo-public/local/
    max_cache_ttl: 0s
    authoritative_workspace: true
    single_writer: true
    exclusive_write_authority: true

SOW 根据 endpoint 与 prefix 创建 /srv/repo-public/local。三个必填布尔值声明:本工作区是该目标的权威来源,也是唯一写入者。其他工具使用独立前缀。

本例通过 file:// 检查发布文件,并声明没有中间缓存。以后增加代理或 CDN 时,把 public_endpoint 改为公共仓库 URL, 把 max_cache_ttl 设为其最长缓存时间,并通过 publish --rebind 应用变更。 SOW 据此检查交付结果,并确定旧文件何时可以删除。

发布仓库:

sow publish public

SOW 先复制包文件与不可变元数据,再更新客户端入口,随后检查结果并记录发布快照。内容不变时重复发布会报告空操作。

3. 配置 Nginx

安装 Nginx,添加以下服务器配置,并把 repo.example.com 换成你的主机名:

server {
    listen 80;
    server_name repo.example.com;

    root /srv/repo-public;
    autoindex off;

    location / {
        try_files $uri $uri/ =404;
    }

    location ~ (^|/)\. {
        deny all;
    }
}

检查并重载配置:

sudo nginx -t
sudo nginx -s reload

上述命令假定 Nginx 已运行。首次安装时,在 nginx -t 成功后,用操作系统的服务管理器启动 Nginx。

客户端地址如下:

客户端 地址
DNF baseurl http://repo.example.com/local/dists/el9/$basearch/
APT URIs http://repo.example.com/local
APT Suites / Components bookworm / main

完整源配置见 YUM 或 APT 教程。需要验签时,分发公钥并按仓库签名配置。

4. 检查客户端访问

用上表地址配置客户端,刷新索引并安装一个软件包。 这会检查 dists/ 与 pool/ 是否都能访问。两个目录使用相同的访问规则。

发布更新

修改成员或配置后,构建尚未完成的变更,再发布:

sow build -r local
sow check -r local
sow publish public

发布中断时,重新执行 sow publish public 继续。恢复与目标变更见 sow publish;缓存宽限期后的清理见 sow gc。

交付到隔离主机

SOW 无法连接目标时,按以下步骤复制。创建并检查副本期间,暂停源工作区的写入。

  1. 在源端执行 sow check。
  2. 把完整仓库复制到目标的新发布目录。保留硬链接,例如使用 rsync -aH 或 tar。
  3. 根据 sow changes 0 或归档校验清单检查传输后的文件。
  4. 将服务器路径切换到新发布目录。
  5. 切换后,继续让旧索引引用的软件包 URL 和不可变元数据 URL 可访问,直到最长缓存期结束,再清理旧发布。

在此期间,HTTP 服务可将新目录中缺失的路径回退到上一发布目录。

直接覆盖在线目录,可能在包文件到达前暴露新索引。先准备新目录,再整体切换。

提供 Plain 仓库

Plain 模式直接提供传给 sow create 的目录。使用上例 Nginx 隐藏文件规则, 防止访问临时文件。

发布到 R2

Cloudflare R2 使用 provider: r2。配置 endpoint、bucket、凭据引用、公共 URL 与缓存时间,详见目标配置参考。先发布到测试前缀,再从实际包管理器检查。

R2 目标垃圾回收只报告待删除对象,SOW 不执行远端删除。已测试的存储操作见平台与集成。

6 - 演练构建 pigsty-infra 仓库

把既有的双架构 RPM 与 DEB 包池组织成 infra 仓库,完成本地安装验收、滚动更新、Stable 晋升与月度快照。

pgsty/infra-pkg 是 Pigsty Infra 软件包的上游构建源码。 本教程使用已构建的 x86-64 与 ARM64 RPM、DEB,创建包含滚动频道、稳定发布与月度快照的 infra 仓库。先安装 SOW、jq 与 Docker。安装示例使用 pg-exporter;请包含该包,以及客户端镜像中尚未安装的依赖。

1. 把包集中到 ~/repo

为本工作区使用新的 ~/repo 目录。从 infra-pkg 源码目录复制软件包到两个输入目录:

mkdir -p ~/repo/packages/rpm ~/repo/packages/deb
cp ~/pgsty/infra-pkg/dist/rpm/*.rpm ~/repo/packages/rpm/
cp ~/pgsty/infra-pkg/dist/deb/*.deb ~/repo/packages/deb/

输入需同时包含 x86-64 与 ARM64 包。第 3 步会按包内元数据检查格式与架构。

2. 创建 infra 仓库与两个 Dist

初始化工作区,并创建名为 infra 的仓库:

sow init ~/repo
cd ~/repo
sow repo new infra
sow dist new rpm --format rpm -r infra
sow dist new deb --format deb -r infra

在 ~/repo/sow.yml 中给两个 Dist 设置 limit: 1:

schema: sow/v3
architectures: [x86_64, aarch64]
repos:
  infra:
    dists:
      rpm:
        format: rpm
        limit: 1
      deb:
        format: deb
        limit: 1

limit: 1 按“包名 + 原生架构”只保留最新一个版本。因此 rpm 与 deb 就是两个滚动更新的 最新版本频道;它们仍会同时保留 x86-64 与 ARM64 两个架构。

sow config check

3. 一次性导入并构建

先导入两种格式的软件包,再统一构建索引:

cd ~/repo
sow add ~/repo/packages/rpm --recursive -r infra -d rpm --skip
sow add ~/repo/packages/deb --recursive -r infra -d deb --skip
sow build -r infra -d rpm -d deb
sow check -r infra

sow check 成功后,列出 SOW 从包内元数据读出的格式与架构:

sow ls -r infra -d rpm -d deb --json |
  jq -r '.result.packages | group_by(.format + "/" + .canonical_arch)[] |
    "\(.[0].format)\t\(.[0].canonical_arch)\t\(length) packages"'

预期至少出现:

deb     aarch64   ... packages
deb     x86_64    ... packages
rpm     aarch64   ... packages
rpm     x86_64    ... packages

SOW 使用规范化架构名,因此 DEB 的 amd64/arm64 在这里显示为 x86_64/aarch64。

4. 看懂生成的目录

关键结构应当是:

~/repo/
├── sow.yml                            # 配置,不对外服务
├── .sow/                              # 数据库、锁、恢复状态,不对外服务
├── packages/                          # 原始输入包池,可自行归档
│   ├── rpm/
│   └── deb/
└── infra/                             # 完整的公开仓库根目录
    ├── pool/                          # RPM 与 DEB 共享的单副本包池
    └── dists/
        ├── rpm/
        │   ├── x86_64/repodata/
        │   └── aarch64/repodata/
        └── deb/
            ├── Release
            └── main/
                ├── binary-amd64/
                └── binary-arm64/

SOW 把所有 Dist 放在 dists/ 下,把软件包放在 pool/ 下。发布或挂载完整的 ~/repo/infra 目录树,让每个 Dist 都能访问其软件包。

5. 用 Nginx 只读服务仓库

使用官方 nginx:alpine 镜像,把仓库根目录只读挂载到 /infra:

docker network create --internal infra-lab
docker run --detach \
  --name infra-nginx \
  --network infra-lab \
  --publish 127.0.0.1:8080:80 \
  --volume "$HOME/repo/infra:/usr/share/nginx/html/infra:ro" \
  nginx:alpine

Nginx 只看得到 ~/repo/infra,既看不到 sow.yml 与 .sow/,也无法修改仓库。

6. 在 EL9 中只用 infra 安装 RPM

下面的 Rocky Linux 9 容器位于 --internal 网络中。脚本先删除所有预置仓库,再只启用刚创建的 infra,因此成功安装不能依赖公网软件源。

docker run --rm --interactive --network infra-lab rockylinux:9 bash -s <<'ROCKY'
set -euxo pipefail

rm -f /etc/yum.repos.d/*.repo
cat >/etc/yum.repos.d/infra.repo <<'REPO'
[infra]
name=Pigsty Infra RPM
baseurl=http://infra-nginx/infra/dists/rpm/$basearch/
enabled=1
gpgcheck=0
repo_gpgcheck=0
REPO

dnf clean all
dnf --disablerepo='*' --enablerepo=infra makecache
dnf --disablerepo='*' --enablerepo=infra install -y pg-exporter
rpm -q --qf '%{NAME}\t%{VERSION}-%{RELEASE}\t%{ARCH}\n' pg-exporter
command -v pg_exporter
ROCKY

RPM 的 baseurl 必须落到具体架构视图。$basearch 会由 dnf 展开为 x86_64 或 aarch64。

7. 在 Ubuntu 24.04 中只用 infra 安装 DEB

APT 的 URI 指向仓库根目录,Suites 才是 Dist 名 deb:

docker run --rm --interactive --network infra-lab ubuntu:24.04 bash -s <<'UBUNTU'
set -euxo pipefail

rm -f /etc/apt/sources.list
rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources
cat >/etc/apt/sources.list.d/infra.sources <<'SOURCE'
Types: deb
URIs: http://infra-nginx/infra
Suites: deb
Components: main
Trusted: yes
SOURCE

apt-get clean
apt-get update
apt-get install -y --no-install-recommends pg-exporter
dpkg-query -W -f='${Package}\t${Version}\t${Architecture}\n' pg-exporter
command -v pg_exporter
UBUNTU

本例使用未签名仓库。按仓库签名配置后,为 RPM 元数据启用 repo_gpgcheck=1,为 APT 配置 Signed-By。RPM 包也有签名时,再启用 gpgcheck=1。从已签名 APT 源中删除 Trusted: yes。

默认情况下,Docker 容器使用宿主机架构。需要测试另一架构时,给对应的 docker run 增加 --platform linux/amd64 或 --platform linux/arm64;跨架构运行需要 binfmt/QEMU 支持。

8. 日常维护:添加一个新版本

更新仓库时直接执行 add。假设已经拿到新版 pg-exporter 的四个软件包:

cp ~/pgsty/infra-pkg/dist/rpm/pg-exporter-*.rpm ~/repo/packages/rpm/
cp ~/pgsty/infra-pkg/dist/deb/pg-exporter_*.deb ~/repo/packages/deb/

cd ~/repo
sow add ~/repo/packages/rpm/pg-exporter-*.rpm -r infra -d rpm --skip
sow add ~/repo/packages/deb/pg-exporter_*.deb -r infra -d deb --skip
sow build -r infra -d rpm -d deb
sow check -r infra

因为 latest Dist 配了 limit: 1,新版本胜出后,旧版本会自动退出该 Dist 的成员集合。 旧包文件不会被立即删除,也不会因为以后放宽策略而自动回来。

可以重新运行第 6、7 节的客户端,先 makecache/update,再安装或升级,完成更新验收。

撤下错误软件包时,用 sow ls -r infra -d rpm --json 找到其 SHA-256。先执行 sow rm sha256:DIGEST -r infra -d rpm --check 预览,再去掉 --check 执行。DEB 使用 -d deb。SHA-256 引用选中一个包对象;裸包名会选中其全部版本与架构。未使用的包池文件由 sow gc 单独回收。

9. 两层保留策略:latest 与 stable

rpm、deb 的 limit: 1 适合持续滚动,但不能表达“保留所有正式发布历史”。为此再创建两个 Dist:

cd ~/repo
sow dist new rpm-stable --format rpm -r infra
sow dist new deb-stable --format deb -r infra
sow config show -r infra -d rpm-stable -d deb-stable

新 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

把源 Dist 的包池文件加入目标 Dist,即可晋升软件包。SOW 校验并复用已有包对象,不增加第二份文件。导出清单前暂停其他仓库写入任务,晋升完成后再恢复。

先确保源状态干净,并保存本次晋升清单:

set -euo pipefail
cd ~/repo
sow check -r infra
mkdir -p ~/repo/manifests

sow ls -r infra -d rpm --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/rpm-latest-202608.list
sow ls -r infra -d deb --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/deb-latest-202608.list

把清单中的包池对象加入 stable Dist:

cd ~/repo
(
  set -euo pipefail
  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d rpm-stable --skip
  done < ~/repo/manifests/rpm-latest-202608.list

  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d deb-stable --skip
  done < ~/repo/manifests/deb-latest-202608.list

  sow build -r infra -d rpm-stable -d deb-stable
  sow check -r infra
)

每次 add 应报告 reused。若中途失败,源 Dist 不受影响;修复问题后对同一清单重跑即可。 随着以后重复晋升,rpm/deb 仍只保留最新版本,而 rpm-stable/deb-stable 会逐次累积历史版本。

11. 从 stable 创建 2026-08 快照

客户端可见的月度快照也是两个新的 Dist:

cd ~/repo
sow dist new rpm-202608 --format rpm -r infra
sow dist new deb-202608 --format deb -r infra
sow config show -r infra -d rpm-202608 -d deb-202608

创建快照期间,暂停其他仓库写入任务。先保存 stable 的包清单:

set -euo pipefail
sow check -r infra
sow ls -r infra -d rpm-stable --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/rpm-stable-202608.list
sow ls -r infra -d deb-stable --json |
  jq -r '.result.packages[].pool_path' > ~/repo/manifests/deb-stable-202608.list

再把清单加入对应快照 Dist:

cd ~/repo
(
  set -euo pipefail
  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d rpm-202608 --skip
  done < ~/repo/manifests/rpm-stable-202608.list

  while IFS= read -r pool_path; do
    sow add "$HOME/repo/infra/$pool_path" -r infra -d deb-202608 --skip
  done < ~/repo/manifests/deb-stable-202608.list

  sow build -r infra -d rpm-202608 -d deb-202608
  sow check -r infra
)

如需在将来删除快照 Dist 后仍保留这次构建,另外记录当前仓库构建快照:

sow retain add "$(sow status -r infra --json | jq -r '.result.built_generation')" -r infra
sow retain ls -r infra

retain 保存本次构建的元数据,并防止相关包文件被垃圾回收。客户端仍通过快照 Dist 访问。 SOW 不会将这些 Dist 设为只读;创建后保留其成员与生效配置,内容才会固定。

12. 客户端地址总表

同一个 Nginx 与同一份 infra/pool 支撑所有频道:

频道 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

用 sow dist ls -r infra 查看全部六个 Dist。

滚动 Dist 保留最新软件包,stable Dist 累积已晋升版本,月度 Dist 保留选定快照。所有视图共用同一个包池。

实验结束后可停止临时服务:

docker rm --force infra-nginx
docker network rm infra-lab