前言
本章,您将学习 RPM 软件包构建的基础过程。
在 RHEL 衍生版以及下游社区版中,通常安装软件可通过这么几种方式:
- 若存储库中没有相关软件,则可以上传预编译的二进制包,接着在操作系统中进行安装。对于软件包的依赖关系问题,可以手动解决,也可以自动解决
- 配置存储库,找到对应的软件包安装即可,自动解决软件包的依赖关系问题
- 下载源代码,手动编译安装到操作系统中
构建思路
构建一个 RPM 包,基本思路:
- 明确你要构建什么。一个 rpm 包可能只是一个应用程序,也可能是不需要编译的包含一堆网页文件的 rpm 包,也可能是一个包含库的单独的 rpm 包,也可能只是文档性质的 rpm 包,也可能是补丁性质的 rpm 包,也可能只是包含配置文件的 rpm 包。
- 准备好「原材料」,也就是项目源代码的压缩归档文件,常以
.tar.gz
或.tar.xz
或.tar.bz2
等后缀结尾 - 收集补丁,给 rpm 打补丁
- 升级,包含兼容性、功能改进和冲突解决等
- 依赖关系。每个 rpm 都有一个能力,这个能力大多数情况下都与其名字有关。众所周知,rpm 安装后生成一堆文件,释放出的对应文件也是一种能力体现,它们有可能被其他 rpm 包依赖。换言之,这里的能力包括包的名称、包安装后的文件。在制作 rpm 的时候,有两类依赖关系:一个叫编译依赖(BuildRequires);一个叫安装依赖(Requires)
- 编写一个 spec 文件。spec 是配置规范文件,可理解为打包成 rpm 格式的配方。
- 开始构建 rpm 包,生成 rpm 包
- 对 rpm 包进行测试
构建要求
- 目录结构要求 – rpm 包的构建需要对目录结构有一定的要求,可理解为构建时所必须的「工作目录」
- 构建时使用的用户 – 推荐使用 root (uid=0),当然普通用户(uid>=1000)也可以进行构建
- 将「原材料」放入到对应的目录中
- 编写 spec 文件,编译打包成 rpm 包
spec 文件
Q:什么是 spec 文件?
一个包含元数据、构建指令和安装规则的脚本,它驱动整个 RPM 包的生成流程,定义了软件从源码编译到最终打包的所有步骤。spec 的文件内容包含三部分:
- 序言项(Preamble items)
- 主体项(Body items)
- 高级项(Advanced items)
除了序言项,其他部分都需要定义一些指令为构建系统提供必要的信息。
序言项标签
- Name – 软件包的基本名称,应与 spec 文件名匹配
- Version – 软件包的上游版本号,注意!这里的版本号不要带 – ,- 在 spec 中有特殊的意义
- Release – 此软件包的发布次数。通常将初始值设置为 1%{?dist}
- Summary – 简短的、单行的说明软件包的摘要信息
- License – 被打包的软件包的开源许可证
- URL – 大多数情况下,这是所打软件包的上游项目网站。
- Source0 – 上游源代码压缩包存档的路径或URL
- Patch – 补丁,可以有两种方式应用该指令,在 Patch 后面加或不加数字。也可以使用 %Patch0 这种方式。
- BuildArch – 构建的体系架构,如果软件包不依赖特定的架构,可写为
BuildArch: noarch
。如果不写,则自动继承当前机器的体系架构 - BuildRequires – 编译时的依赖包,多个依赖包可用空格或逗号分隔。可以有多个 BuildRequires 条目
- Requires – 软件安装后运行所需的依赖包,多个依赖包可用空格或逗号分隔。可以有多个 Requires 条目
- ExcludeArch – 如果某个软件不能在特定的处理器架构上运行,你可以在此处排除该架构
- Conflicts – 冲突的包,与 Requires 相反,如果匹配到对应的包,则无法安装该包。
- Obsoletes – 废弃的包,也就是其他包提供的功能已经不推荐使用了
rpm 软件包的命名规范:
[Package_Name]-[Version]-[Release].[OS].[Arch].rpm
[Package_Name]-[Version]-[Release].[OS].[Arch].src.rpm
Note:通常一个 rpm 软件包的完整名称由 Name、Version、Release 组成,我们称其为 NVR,即 Name-Version-Release.rpm。在上面的命名规范中,有些资料也将 [Release].[OS].[Arch]
这三部分单独划分到 Release 中。
主体项指令
一个软件包能否打包成功,全看主体项的各种步骤。
- %description – rpm 软件包的完整描述,可跨越多行,可分为多列。
- %prep – 预处理(编译前的准备操作),为下一步编译安装做准备。
- %build – 编译位于 BUILD 目录里的文件,类似源代码安装
./configure && make
的操作。 - %install – 将 BUILD 目录的文件安装至 BUILDROOT 目录下,需要注意的是!这个 BUILDROOT 目录是最终用户安装 rpm 后得到的文件 。类似源代码安装中的
make install
- %check – 用于测试软件的一系列命令。类似源代码安装中的
make test
- %files – 制作 rpm 包时应该包含哪些文件。注意!这里的文件一定是和 BUILDROOT 目录下是一一对应的关系(除了 debug 目录)。BUILDROOT 是一个模拟操作系统根的临时目录。
- %changelog – 每个 release 所做的变更日志,例如漏洞修复、补丁、额外的功能增强等。
高级项
高级项包含 Scriptlet(程序段)与 Triggers(触发器)。
Scriptlet 是在安装或删除软件包之前或之后执行的一系列 RPM 指令;Triggers 提供了一种在安装和卸载软件包期间进行交互的方法。Triggers 难以调试,因此要尽量减少使用它。
- %pre – 在安装包之前执行的程序段
- %pretrans – 在安装或删除任何包之前执行的程序段
- %post – 在安装包之后执行的程序段
- %preun – 在卸载包之前执行的程序段,通常在升级的时候会执行
- %postun – 在卸载包之后执行的程序段
- %posttrans – 在事务结束时执行的程序段
关于这部分内容的更多内容,请参阅 这里。
rpm 软件包构建实验
实验要求:将 Nginx 最新版本构建成 rpm 包
环境:
- 操作系统:Rocky Linux 8.10
- rpm 版本:4.14.3
- 用户:root (uid=0)
准备好需要的命令行工具:
Shell > dnf -y install rpmdevtools rpm-build
其中 rpmdevtools 软件包包含这些基本的命令行工具:
命令 | 说明 |
---|---|
rpmdev-setuptree |
在当前用户家目录中生成 rpm 的构建树(build tree) |
rpmdev-diff |
比对两个归档的不同内容 |
rpmdev-newspec |
从模板中创建新的 .spec 文件 |
rpmdev-checksig |
使用备用的rpm密钥环检查软件包的签名 |
rpminfo |
打印有关可执行文件和库的信息 |
rpmdev-md5 |
显示 RPM 中所有文件的 md5 校验 |
步骤一:准备「工作目录」
Shell > cd ; rpmdev-setuptree
Shell > tree rpmbuild/
rpmbuild/
├── BUILD
├── RPMS
├── SOURCES
├── SPECS
└── SRPMS
目录 | 说明 |
---|---|
BUILD | 构建过程中的工作目录 |
RPMS | 存放生成的二进制 rpm 包 |
SOURCES | 存放构建所需的资源,包含源码压缩文件、补丁文件、配置文件等 |
SPECS | 核心目录,存放 spec 文件 |
SRPMS | 存放生成的源码 RPM 包(.src.rpm ),其包含源代码和 .spec 文件,便于分发和重新构建。 |
步骤二:使用 spec 文件
有两种方式创建 spec 文件:
- 使用者创建一个全新的 spec 文件,文件内容需要自行填写
- 使用
rpmdev-newspec
命令工具创建一个 spec 模块文件,使用者只需填写必要的指令与字段即可
Shell > cd /root/rpmbuild/SOURCES ; wget -c https://nginx.org/download/nginx-1.29.0.tar.gz
Shell > cd /root/rpmbuild/SPECS ; rpmdev-newspec nginx
nginx.spec created; type minimal, rpm version >= 4.14.
# 该文件的内容如下:
Shell > cat nginx.spec
Name: nginx
Version:
Release: 1%{?dist}
Summary:
License:
URL:
Source0:
BuildRequires:
Requires:
%description
%prep
%autosetup
%build
%configure
%make_build
%install
rm -rf $RPM_BUILD_ROOT
%make_install
%files
%license add-license-file-here
%doc add-docs-here
%changelog
* Fri Jul 4 2025 root
-
修改之后的内容如下:
Name: nginx
Version: 1.29.0
Release: 1%{?dist}
Summary: A high performance web server and reverse proxy server
License: BSD
URL: http://nginx.org/
Source0: %{name}-%{version}.tar.gz
BuildRequires: gcc gcc-c++ openssl openssl-devel make pcre pcre-devel gzip tar bzip2 bzip2-devel
# Requires:
%description
Nginx is a web server and a reverse proxy server for HTTP, SMTP, POP3 and \
IMAP protocols, with a strong focus on high concurrency, performance and low \
memory usage.
%prep
%setup -q
%build
./configure \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--http-log-path=/var/log/nginx/access.log \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_stub_status_module \
--with-http_gzip_static_module
make %{_smp_mflags}
%install
rm -rf $RPM_BUILD_ROOT
make install DESTDIR=%{buildroot}
%{__install} -p -d -m 0755 %{buildroot}/var/run/nginx
%{__install} -p -d -m 0755 %{buildroot}/var/log/nginx
%clean
rm -rf %{buildroot}
%pre
# $1表示软件包的执行方式,0表示卸载;1表示第一次安装;2表示升级
if [ $1 == 1 ];then
/usr/sbin/groupadd -r nginx 2> /dev/null
/usr/sbin/useradd -r -g nginx nginx 2> /dev/null
fi
%files
%defattr(-,root,root,-)
%doc LICENSE CHANGES README
%{_sbindir}/%{name}
%dir /var/run/nginx
%dir /var/log/nginx
%dir /etc/nginx
%config(noreplace) /etc/nginx/fastcgi.conf
%config(noreplace) /etc/nginx/fastcgi.conf.default
%config(noreplace) /etc/nginx/fastcgi_params
%config(noreplace) /etc/nginx/fastcgi_params.default
%config(noreplace) /etc/nginx/koi-utf
%config(noreplace) /etc/nginx/koi-win
%config(noreplace) /etc/nginx/mime.types
%config(noreplace) /etc/nginx/mime.types.default
%config(noreplace) /etc/nginx/nginx.conf
%config(noreplace) /etc/nginx/nginx.conf.default
%config(noreplace) /etc/nginx/scgi_params
%config(noreplace) /etc/nginx/scgi_params.default
%config(noreplace) /etc/nginx/uwsgi_params
%config(noreplace) /etc/nginx/uwsgi_params.default
%config(noreplace) /etc/nginx/win-utf
/usr/local/nginx/html/50x.html
/usr/local/nginx/html/index.html
%changelog
* Fri Jul 4 2025 root
- first version
- init
稍后我们将说明 spec 文件中的内容。
步骤三:使用 rpmbuild
进行构建
常见选项如下表所示:
选项 | 说明 |
---|---|
-bp |
只执行到 %prep 阶段 |
-bi |
只执行到 %install 阶段 |
-bc |
只执行到 %build 阶段 |
-bb |
制作二进制的 rpm 包 |
-bs |
制作源码格式的 rpm 包 |
-bl |
检测,检测哪些文件在 BUILDROOT 目录下安装生成了,但是在 spec 文件中的 %files 没有包含进来 |
-ba |
既制作二进制的 rpm 包,也制作源码格式的 rpm包 |
使用 -bp
选项时:
# 若输出文本的最后一行看到 exit 0 表示无任何问题
Shell > rpmbuild -bp /root/rpmbuild/SPECS/nginx.spec
Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.mgz8YD
+ umask 022
+ cd /root/rpmbuild/BUILD
+ cd /root/rpmbuild/BUILD
+ rm -rf nginx-1.29.0
+ /usr/bin/gzip -dc /root/rpmbuild/SOURCES/nginx-1.29.0.tar.gz
+ /usr/bin/tar -xof -
+ STATUS=0
+ '[' 0 -ne 0 ']'
+ cd nginx-1.29.0
+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .
+ exit 0
Shell > ls /root/rpmbuild/BUILD
nginx-1.29.0
Shell > ls -lh /root/rpmbuild/BUILD/nginx-1.29.0/
total 896K
drwxr-xr-x 6 root root 4.0K Jul 4 20:45 auto
-rw-r--r-- 1 root root 325K Jun 25 01:30 CHANGES
-rw-r--r-- 1 root root 497K Jun 25 01:30 CHANGES.ru
-rw-r--r-- 1 root root 5.1K Jun 25 01:22 CODE_OF_CONDUCT.md
drwxr-xr-x 2 root root 4.0K Jul 4 20:45 conf
-rwxr-xr-x 1 root root 2.6K Jun 25 01:22 configure
drwxr-xr-x 4 root root 4.0K Jul 4 20:45 contrib
-rw-r--r-- 1 root root 4.0K Jun 25 01:22 CONTRIBUTING.md
drwxr-xr-x 2 root root 4.0K Jul 4 20:45 html
-rw-r--r-- 1 root root 1.3K Jun 25 01:22 LICENSE
drwxr-xr-x 2 root root 4.0K Jul 4 20:45 man
-rw-r--r-- 1 root root 14K Jun 25 01:22 README.md
-rw-r--r-- 1 root root 4.8K Jun 25 01:22 SECURITY.md
drwxr-xr-x 9 root root 4.0K Jun 25 01:22 src
使用 -bc
选项时:
# 检查环境后会执行编译过程
## 同样的,若输出文本的最后一行看到 exit 0 表示无任何问题
Shell > rpmbuild -bc /root/rpmbuild/SPECS/nginx.spec
Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.3YQGsp
+ umask 022
+ cd /root/rpmbuild/BUILD
+ cd /root/rpmbuild/BUILD
+ rm -rf nginx-1.29.0
+ /usr/bin/gzip -dc /root/rpmbuild/SOURCES/nginx-1.29.0.tar.gz
+ /usr/bin/tar -xof -
+ STATUS=0
+ '[' 0 -ne 0 ']'
+ cd nginx-1.29.0
+ /usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .
+ exit 0
Executing(%build): /bin/sh -e /var/tmp/rpm-tmp.S4rJ6D
+ umask 022
+ cd /root/rpmbuild/BUILD
+ cd nginx-1.29.0
+ ./configure --sbin-path=/usr/sbin/nginx --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --pid-path=/var/run/nginx/nginx.pid --lock-path=/var/lock/nginx.lock --http-log-path=/var/log/nginx/access.log --user=nginx --group=nginx --with-http_ssl_module --with-http_v2_module --with-http_stub_status_module --with-http_gzip_static_module
checking for OS
+ Linux 4.18.0-553.54.1.el8_10.x86_64 x86_64
checking for C compiler ... found
+ using GNU C compiler
+ gcc version: 8.5.0 20210514 (Red Hat 8.5.0-26) (GCC)
checking for gcc -pipe switch ... found
checking for -Wl,-E switch ... found
checking for gcc builtin atomic operations ... found
checking for C99 variadic macros ... found
checking for gcc variadic macros ... found
checking for gcc builtin 64 bit byteswap ... found
checking for unistd.h ... found
checking for inttypes.h ... found
checking for limits.h ... found
checking for sys/filio.h ... not found
checking for sys/param.h ... found
checking for sys/mount.h ... found
checking for sys/statvfs.h ... found
checking for crypt.h ... found
checking for Linux specific features
checking for epoll ... found
checking for EPOLLRDHUP ... found
...
objs/src/http/modules/ngx_http_upstream_zone_module.o \
objs/src/http/modules/ngx_http_stub_status_module.o \
objs/ngx_modules.o \
-ldl -lpthread -lcrypt -lpcre2-8 -lssl -lcrypto -ldl -lpthread -lz \
-Wl,-E
make[1]: Leaving directory '/root/rpmbuild/BUILD/nginx-1.29.0'
+ exit 0
使用 -bi
选项时:
# 未出现 error 或 file not found 之类的错误则表示正常
Shell > rpmbuild -bi /root/rpmbuild/SPECS/nginx.spec
使用 -ba
选项时:
Shell > rpmbuild -ba /root/rpmbuild/SPECS/nginx.spec
...
Recommends: nginx-debugsource(x86-64) = 1.29.0-1.el8
Checking for unpackaged file(s): /usr/lib/rpm/check-files /root/rpmbuild/BUILDROOT/nginx-1.29.0-1.el8.x86_64
Wrote: /root/rpmbuild/SRPMS/nginx-1.29.0-1.el8.src.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/nginx-1.29.0-1.el8.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/nginx-debugsource-1.29.0-1.el8.x86_64.rpm
Wrote: /root/rpmbuild/RPMS/x86_64/nginx-debuginfo-1.29.0-1.el8.x86_64.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.DWMru0
+ umask 022
+ cd /root/rpmbuild/BUILD
+ cd nginx-1.29.0
+ rm -rf /root/rpmbuild/BUILDROOT/nginx-1.29.0-1.el8.x86_64
+ exit 0
Shell > tree /root/rpmbuild/RPMS/
/root/rpmbuild/RPMS/
└── x86_64
├── nginx-1.14.1-9.module+el8.4.0+542+81547229.x86_64.rpm
├── nginx-1.29.0-1.el8.x86_64.rpm
├── nginx-debuginfo-1.29.0-1.el8.x86_64.rpm
└── nginx-debugsource-1.29.0-1.el8.x86_64.rpm
1 directory, 4 files
步骤四:安装
我们的 nginx-1.29.0-1.el8.x86_64.rpm 已经制作完成,安装即可:
Shell > rpm -ivh /root/rpmbuild/RPMS/x86_64/nginx-1.29.0-1.el8.x86_64.rpm
Verifying... ################################# [100%]
Preparing... ################################# [100%]
Updating / installing...
1:nginx-1.29.0-1.el8 ################################# [100%]
# 释放出的文件
Shell > rpm -ql nginx
/etc/nginx
/etc/nginx/fastcgi.conf
/etc/nginx/fastcgi.conf.default
/etc/nginx/fastcgi_params
/etc/nginx/fastcgi_params.default
/etc/nginx/koi-utf
/etc/nginx/koi-win
/etc/nginx/mime.types
/etc/nginx/mime.types.default
/etc/nginx/nginx.conf
/etc/nginx/nginx.conf.default
/etc/nginx/scgi_params
/etc/nginx/scgi_params.default
/etc/nginx/uwsgi_params
/etc/nginx/uwsgi_params.default
/etc/nginx/win-utf
/usr/lib/.build-id
/usr/lib/.build-id/e0
/usr/lib/.build-id/e0/1c54002b83cb632eedb82df9cd1834b5c8597d
/usr/local/nginx/html/50x.html
/usr/local/nginx/html/index.html
/usr/sbin/nginx
/usr/share/doc/nginx
/usr/share/doc/nginx/CHANGES
/usr/share/doc/nginx/LICENSE
/usr/share/doc/nginx/README.md
/var/log/nginx
/var/run/nginx
请注意!我这里并没有将 nginx 的启动 unit 存放在 /usr/lib/systemd/system/ 目录中。
到这一步,构建 nginx 的简单 rpm 包就完成了。
spec 文件内容解释
-
指令后的括号表示设置相应的指令属性(如
%defattr(-,root,root,-)
) -
间隔一个空格则表示标识指令的作用对象(如
%dir /var/run/nginx
) -
有些指令还有相应的选项来控制行为(如
%setup -q
)。 -
#
开头表示注释行 -
指令可以嵌套指令以及宏变量,这样嵌套的指令被称为指令组,如:
%files %defattr(-,root,root,-) %doc LICENSE CHANGES README.md %{_sbindir}/%{name} ... /usr/local/nginx/html/50x.html /usr/local/nginx/html/index.html
-
空白行的作用是为了方便阅读并分割不同的指令组。
-
%( )
表示动态执行 shell 命令并获取输出结果,如%(echo $HOME)
宏变量
rpm 本身为了完成它的工作,定义了许多的 rpm 变量,被称为宏变量。
宏变量以 %
开头并加花括号进行引用,引用时:
- 单下划线前缀 – 对于运维人员,它是可调整的配置项。对于开发者,它表示宏变量为「内部使用」,但不强制限制访问,仅作为开发者间的约定
- 双下划线前缀 – 对于运维人员,它是禁止修改的系统命令。对于开发者,它表示宏变量为构建系统保留的强私有属性,通过名称修饰(Name Mangling)实现访问限制
遇到陌生的宏变量时,可使用 rpm --showrc
搭配 grep
完成,比如:
Shell > rpm --showrc | grep -i _topdir
...
-13: _topdir %(echo $HOME)/rpmbuild
Shell > rpm --showrc | grep -i buildrootdir
-13: _buildrootdir %{_topdir}/BUILDROOT
-13: buildroot %{_buildrootdir}/%{NAME}-%{VERSION}-%{RELEASE}.%{_arch}
最后
spec 文件的内容非常丰富,单靠这一篇文档是可能了解完整的,详情参阅官方的 手册页
对于一般的使用者或运维人员来说,构建考虑全面的 rpm 包是一件非常费时费力的事情,主要原因在于其涉及的多环节精细化管理与技术挑战:
- 依赖管理的复杂性。库依赖、依赖冲突等
- spec 文件编写的技术门槛。需要考虑多阶段构建流程、跨平台适配以及相关的脚本逻辑,比如一些关系型数据库的 RPM 包需在spec 中声明 400+ 文件路径及 20+ 依赖项
- 高昂的测试与验证成本。需要考虑卸载后无残留文件、升级时配置文件保留(.rpmnew 机制),还要考虑在不同的 RHEL 衍生版去验证一致性。
