How to write SPEC file

如果说《SPEC 基础知识》解决“能打包”,这一篇解决的是“如何把 SPEC 写得可维护、可扩展、可发布”。

1. 先建立一个正确心智模型

打包过程可以拆成三层:

  1. 上游构建系统(configure / Makefile)决定“会生成哪些文件”。
  2. SPEC 的 %install 决定“这些文件安装到 %{buildroot} 的哪个路径”。
  3. SPEC 的 %files 决定“最终哪些文件进入哪个 RPM 包”。

核心结论:SPEC 不负责创造文件,只负责组织和选择文件

2. 从 Makefile 安装路径到 %files 的映射

假设上游项目里有:

1
2
scriptdir = $(libexecdir)/mydaemon/scripts
script_SCRIPTS = conf/a.sh conf/b.sh conf/c.sh

执行 make install DESTDIR=%{buildroot} 后,文件会进入:

1
%{buildroot}%{_libexecdir}/mydaemon/scripts/

在 SPEC 中就应该声明:

1
2
%files
%{_libexecdir}/mydaemon/scripts/*

这就是“构建产物 -> 安装路径 -> 打包清单”的完整闭环。

3. 单包与多子包(%package

默认一个 SPEC 生成一个主包。
当你需要拆分能力(例如主程序、sudo 规则、systemd 集成)时,使用子包:

1
2
3
4
5
6
7
8
9
%package sudoers
Summary: Sudo policy for %{name}
Requires: sudo

%description sudoers
Sudoers policy for %{name}.

%files sudoers
%attr(0440,root,root) %{_sysconfdir}/sudoers.d/%{name}

实践建议:

  1. 把“可选能力”拆成子包,避免主包依赖过重。
  2. 把“运行时配置”与“核心二进制”分开,升级更稳。

4. BuildRequiresRequires 的边界

  1. BuildRequires:构建机需要(编译器、pkgconfig、devel 包)。
  2. Requires:用户安装后运行需要。

常见写法:

1
2
BuildRequires: gcc, make, pkgconfig(systemd)
Requires: %{name}-sudoers%{?_isa} = %{version}-%{release}

注意 %{?_isa}:让依赖和架构绑定,更适合多架构场景。

5. systemd 脚本建议用官方宏

不要手写复杂脚本,优先用宏(可读性高、行为一致)。

1
2
3
4
5
6
7
8
%post
%systemd_post mydaemon.service

%preun
%systemd_preun mydaemon.service

%postun
%systemd_postun_with_restart mydaemon.service

验证方法:

1
rpm --scripts -qp mydaemon.rpm

你会看到宏展开后的实际 shell 脚本。

6. %files 精细控制:配置文件与权限

6.1. 配置文件策略

  1. %config:升级时可能覆盖并保留旧配置为 .rpmsave
  2. %config(noreplace):尽量保留用户改动,新配置写成 .rpmnew

一般建议:业务配置用 %config(noreplace),减少升级时覆盖风险。

6.2. 权限与归属

1
2
3
%files
%attr(0755,root,root) %{_bindir}/mydaemon
%attr(0440,root,root) %{_sysconfdir}/sudoers.d/%{name}

7. 循环依赖怎么拆

原文提到的典型问题:A 依赖 B,B 又依赖 A。
这种场景不要硬写依赖,应该拆层:

  1. core:核心能力,不依赖对方。
  2. integration/plugin:集成层,依赖 core + 对端。

示例:

  1. mydaemon-core:只放 daemon 核心。
  2. api-gateway-core:只放 gateway 核心。
  3. mydaemon-api-integration:依赖前两者。

这样可以打破循环依赖并保持安装顺序可解。

8. 利用 .pc 暴露能力给其他包

如果你要给其他模块提供“编译期发现能力”,建议提供 pkg-config 文件:

1
2
3
4
5
6
7
prefix=/usr
libdir=${prefix}/lib64
plugindir=${libdir}/mydaemon/plugins

Name: mydaemonplugin
Description: Plugin SDK for mydaemon
Version: 1.0.0

其他包就可以写:

1
BuildRequires: pkgconfig(mydaemonplugin)

优势是:路径变化时,依赖方不需要硬编码路径。

9. 架构与调试包控制

1
2
ExcludeArch: aarch64
%global debug_package %{nil}

说明:

  1. ExcludeArch 用于明确不支持的平台。
  2. 关闭 debug 包要谨慎,建议只在确实不需要或上游策略明确时使用。

10. 交付前检查清单

每次发版前建议跑这几步:

1
2
3
4
5
6
7
8
9
10
11
# 查看包头
rpm -qpi xxx.rpm

# 查看打包文件列表
rpm -qpl xxx.rpm

# 查看脚本
rpm --scripts -qp xxx.rpm

# 查某个文件归属哪个包
rpm -qf /path/to/file

另外建议本地安装一次验证:

  1. 新装(rpm -ivh
  2. 升级(rpm -Uvh
  3. 卸载(rpm -e

重点观察配置文件、systemd 服务、脚本副作用是否符合预期。

11. 我的实战建议

  1. 先做“最小可用主包”,再拆子包,不要一开始过度设计。
  2. %install%files 对不上,是 SPEC 问题里最常见的根源。
  3. 遇到依赖泥团,优先做“分层拆包”,不要用硬冲突规则互相压制。
  4. 把检查命令写进 CI(qpi/qpl/scripts),让问题在合并前暴露。

当你能稳定回答“这个文件为何在这个包里、这个依赖为何是 BuildRequires 而不是 Requires”,SPEC 基本就进入工程化阶段了。