构建RPM软件包

二维码
| Apr 22, 2019 | 译文 - 原文

1 简介

RPM Package Manager(简称:RPM)正如其名,是一款被广泛应用在Red HatCentOSFedora等操作系统的包管理系统。利用RPM,使得在软件管理、分发、更新等更加便捷。使用RPM你可以:

  1. 安装、重装、卸载、更新和验证软件
    用户可以使用标准包管理工具如 Yum 或者 PackageKit 来安装、卸载、更新 RPM 包。

  2. 可以查询或验证已安装的软件包
    RPM 使用数据库来维护已经安装的软件包和相关文件,用户可以很容易查询验证安装的软件包。

  3. 每个RPM包使用 metadata 来描述安装包版本,尺寸大小,安装说明,项目URL等。

  4. RPM 可以基于软件源代码打包生成可供用户使用的源码包和二进制包。针对于源码包(source packages), 还可以使用以:源代码 + 补丁的方式来更新发布新包。

  5. RPM 包可以添加数字签名,用户可以用签名验证该软件包是否真实有效。

2 什么是源代码? (Source Code)

在正式学习 RPM 打包之前,我们需要了解一些源代码相关的背景知识。那么什么是源代码(source code)呢?

源代码指的是人类可以读懂的计算机指令,常常表示为计算机编程语言。(简单粗暴的说,源代码就是用编程语言写的代码),编程语言千差万别,不过可以大致归为如下三类:

  1. 原生编译型代码(Natively Compiled Code
  2. 原生解释性代码(Raw Interpreted Code
  3. 字节码类型代码(byte compiling

2.1 原生编译型代码

该类型代码需要编译成机器码(或者直接编写机器码),最终生成的是可执行二进制文件,能够独立运行。机器码是电脑CPU直接读取运行的机器指令,运行速度最快,但是非常晦涩难懂,一般直接编写机器码的很少,一般都是使用上层语言编写,再由编译工具编译生成,比如 C语言 编写逻辑,然后使用 gcc 编译生成。

使用这种方式构建的 RPM 包是有架构体系区分的,而且如果你使用的是 64位操作系统构建的包,那么在32位系统上是无法运行的。并且,具体的架构类型会标记在包的名称上,比如 nginx 的某个包:nginx-1.10.0-1.el7.ngx.x86_64.rpm

2.2 解析型代码

有些语言不会直接编译成机器码,例如:bashpython, 这些代码会被专门的语言解析器或者语言虚拟机解释运行。

2.2.1 原生解析型代码

这种代码不需要编译,直接可以被解释器解读运行(interpreter)。

2.2.2 字节码类型代码

字节码类型的代码需要把代码编译成字节码,然后由专门的语言解析器执行。不过有些语言既提供了原生解释型执行和字节编译型。

3 构建RPM包

从这节开始我们来开始学习如果使用源码构建 RPM 包,根据第二节提到的三种不同代码类型,构建RPM包的方式也不一样:

  1. 原生编译型代码: 此种类型的代码需要编译生成机器码,所以构建包的过程中,需要有编译的流程,这种 RPM 构建完成最终生成的包可以被计算机直接运行。
  2. 原生解析型代码(raw interpreted): 这种类型的代码,在打包的时候不需要构建build, 可以直接执行。
  3. 字节码编译代码(byte-compiled):这种代码需要编译生成字节码,供语言虚拟机执行。

3.1 原生编译型

这个例子中,我们使用c语言编写一段代码,最终编程生成一个可执行机器码程序: cello.c

#include <stdio.h>

int main(void) {
   printf("Hello World\n");
   return 0;
}

我们可以尝试使用gcc工具手动执行编译这个文件:

# 编译链接一步到位,生成可执行文件:cello
gcc -g -o cello cello.c

# 运行
$ ./cello
Hello World

但是在大型软件构建中,往往使用 自动构建, 创建一个 Makefile 文件,然后使用: make 工具编译构建:

# Makefile
cello:
        gcc -g -o cello cello.c

clean:
        rm cello

然后运行make命令:

$ make
make: 'cello' is up to date.

因为之前改程序已经构建过,所以make并没有重新在构建,可以使用make clean删除之前的已经构建好的程序在构建一次:

$ make clean
rm cello

$ make
gcc -g -o cello cello.c

# 运行
./cello
Hello World

3.2 解释型代码打包

接下来,我们使用 bash 编写一段原生解释型代码脚本,用来演示原生解释型代码(不需要编译)打包方式;使用 python 来编写另一段程序来演示字节码类型(需要编译成字节码)打包方式。

3.2.1 bash原生解释型代码(Raw Interpreted Code)

#!/bin/bash

printf "Hello World\n"

如之前所说,原生解释型代码不需要编译,直接可以由对应语言解释器直接执行:

# 需要赋予该文件可执行权限
$ chmod +x bello

$ ./bello
Hello World

3.2.2 字节编译型代码(Byte-Compiled Code)

python 这门语言其实同时支持原生解释型执行和字节码编译执行,但是编译成字节码文件后,执行更快,所以我们将演示先将 python 代码编译成字节码文件后,再打 RPM 包。

#!/usr/bin/env python

print("Hello World")

编译成字节码文件:

$ python -m compileall pello.py

$ file pello.pyc
pello.pyc: python 2.7 byte-compiled

不过标准的 python 包分发打包过程和这里描述的可能不太一样,具体请参考:https://docs.python.org/2/library/distribution.html ;不过思路是一致的。

执行 python 字节码文件:

$ python pello.pyc
Hello World

3.3 补丁软件包 (Patching Software)

补丁(patch)其实就是使用些源代码更新其他源代码的方式。往往是通过 diff 的方式对比出两个版本有差别的代码部分做成一个补丁(patch),然后使用 path 工具把这个补丁应用到原有的代码上。

那么,RPM 包如何补丁包呢?

我们使用之前的原生编译型代码文件:cello.c 来演示:

# 基于cello.c 拷贝一份新文件
cp cello.c cello.new.c

编辑 cello.new.c 文件:

#include <stdio.h>

int main(void) {
    printf("Hello World from my very first patch!\n");
    return 0;
}

然后我们使用 diffpatch 命令来制作补丁文件,先对比俩文件的差异部分,然后生成一个补丁文件:

$ diff -Naur cello.c cello.new.c > cello-output-first-patch.patch

如果要应用这个补丁到 cello.c 文件,使用 patch 命令即可:

$ patch < cello-output-first-patch.patch
patching file cello.c

# 可以查看cello.c的变化
cat cello.c

然后可以重新编译代码:

$ make clean
rm cello

$ make
gcc -g -o cello cello.c

$ ./cello
Hello World from my very first patch!

3.4 关于软件安装

得益于 Filesystem Hierarchy Standard (Linux文件系统层次标准), RPM 包的安装遵循 FHS 标准,因此`RPM 打包的任意类型的软件都会被安装到指定的系统目录之上,比如:源代码、二进制文件、预编译文件等等。

我们将演示常用的两种安装软件方式:installmake install 命令。

3.4.1 使用 install 命令

有时候,如果只是安装一个很简单的软件包,比如仅仅拷贝软件包到某个特定的目录赋予一些操作权限而已,那么 install 命令比 make 更适合些(install 工具由 coreutils 包提供)。

比如说,我们使用 install 命令安装之前的 bello 文件到 /usr/bin 目录下,并且赋予这个文件 0755 的权限。

$ sudo install -m 0755 bello /usr/bin/bello

执行如上操作之后,bello 会被添加到系统 $PATH 变量列表里,接下来我们就可以在系统的任意目录位置使用该程序脚本了:

# 进入家目录
$ cd ~

# 运行该程序
$ bello
Hello World

3.4.2 使用 make install 命令

另一个比较常用的安装软件方式是使用 make install 命令,唯一要做的就是定义一个 Makefile 配置文件。

注意:Makefile 使用开发人员写的,而不是打包工具自己生成的。

# Makefile
cello:
        gcc -g -o cello cello.c

clean:
        rm cello

install:
        mkdir -p $(DESTDIR)/usr/bin
        install -m 0755 cello $(DESTDIR)/usr/bin/cello

$(DESTDIR) 变量是GNU make内置变量,用户可以在安装的时候指定非root的自定义目录,比如:

make DESTDIR=/tmp/stage install

使用上面的 Makefile 文件不仅可以编译代码文件,同时也可以安装文件到指定系统上,例如:

# 使用Makefile 编译文件
$ make
gcc -g -o cello cello.c

# 安装文件
$ sudo make install
install -m 0755 cello /usr/bin/cello

# 可以运行测试结果
$ cd ~
cello
Hello World

3.5 RPM打包准备工作

制作软件包之前,我们需要做两件事情:

  1. 准备一个合适的 license
  2. 把需要打包的源码压缩打包

3.5.1 选择license文件

软件分发应该准备一份 license 说明,不同的 license 对软件使用约束不同,选择什么样的 license 请自行决定,我们这里使用 GPLv3 license 作为演示:

$ cat /tmp/LICENSE
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

3.5.2 准备好源码文件

这里我们把之前编写三种不同类型的代码文件打包成 gzip 压缩格式 tar 文件供RPM打包使用。

3.5.2.1 bello 文件

bello 文件由bash编写,见上文。假设软件版本为:0.1 我们把 license 文件和 bello 文件放到一个临时目录:

$ mkdir /tmp/bello-0.1
$ mv ~/bello /tmp/bello-0.1/
$ cp /tmp/LICENSE /tmp/bello-0.1/

然后打包该目录并且放到 ~/rpmbuild/SOURCES/ 目录下:

$ cd /tmp/
$ tar -cvzf bello-0.1.tar.gz bello-0.1
$ mv /tmp/bello-0.1.tar.gz ~/rpmbuild/SOURCES/

3.5.2.2 pello 文件

同样的,我们把 python 编写的代码也打包捆绑好。不过定义另一个版本,以便区分开:0.1.1

$ mkdir /tmp/pello-0.1.1
$ mv ~/pello.py /tmp/pello-0.1.1/
$ cp /tmp/LICENSE /tmp/pello-0.1.1/

# 同样移动到打包源码目录
$ cd /tmp/

$ tar -cvzf pello-0.1.1.tar.gz pello-0.1.1
pello-0.1.1/
pello-0.1.1/LICENSE
pello-0.1.1/pello.py

$ mv /tmp/pello-0.1.1.tar.gz ~/rpmbuild/SOURCES/

3.5.2.3 cello 文件

同样使用 C 语言编写的 Hello World 文件也再来操作一遍,版本定义为:1.0

$ mkdir /tmp/cello-1.0
$ mv ~/cello.c /tmp/cello-1.0/
$ mv ~/Makefile /tmp/cello-1.0/
$ cp /tmp/LICENSE /tmp/cello-1.0/

$ cd /tmp/

$ tar -cvzf cello-1.0.tar.gz cello-1.0
cello-1.0/
cello-1.0/Makefile
cello-1.0/cello.c
cello-1.0/LICENSE

$ mv /tmp/cello-1.0.tar.gz ~/rpmbuild/SOURCES/

并且我们再把补丁代码文件也拷贝到打包源文件目录:

$ mv ~/cello-output-first-patch.patch ~/rpmbuild/SOURCES/

4 开始制作RPM包

RPM 包其实就是包含一些列需要拷贝到目标操作系统的归档文件,且这些归档文件需要定义一些头信息metadata文件来告诉 RPM 包管理工具如何操作这些文件,例如装在操作系统哪个目录下、是否需要编译、以及版本、描述等等。

RPM 包根据分为两种不同类型的包:

  1. source RPM (SRPM) - 源码RPM包
  2. binary RPM - 二进制RPM包

SRPM 包和 RPM 包都使用同一份归档文件和相应制作工具,但是包的作用却不同。SRPM 包含源码、或者补丁文件、以及一个 SPEC 文件说明如何使用这份源码文件打包成一个二进制 RPM 包,SRPM 包就像是一个半成品,只能用来生成最终的二进制 RPM 包;而二进制包则只是包含二进制文件,用于最终安装到操作系统下供用户使用。

4.1 RPM打包工具

制作RPM包需要使用 rpmdevtools 工具包,如果系统没有则需要安装,可以查看系统是否有这些相关工具包:

rpm -ql rpmdevtools | grep bin

# 如果没有则需要安装
# yum install rpmdevtools

4.2 RPM目录说明

我们使用 rpmdev-setuptree 脚手架来初始化一个打包模板目录基本结构:

$ rpmdev-setuptree

$ tree ~/rpmbuild/
/home/user/rpmbuild/
|-- BUILD
|-- RPMS
|-- SOURCES
|-- SPECS
`-- SRPMS

5 directories, 0 files

接下来,我们简要介绍下这些目录的作用:

目录含义
BUILD在打包的时候,%buildroot 所在的目录。对于打包失败而错误日志无法定位问题的情况时,这个目录将很有用。
RPMS二进制RPMS包存放目录,不同架构的rpm包,子目录名称可能不能一样,例如64位操作系统的包位于 x86_64 子目录下,而制作的无架构差别的包位于 noarch 子目录下。
SOURCES打包时提供源代码的目录,rpmbuild执行时,将在此目录查找相应的源代码文件或者补丁文件。
SPECSSPEC描述文件目录。
SPEMS构建SRPMS包的生成目录。

4.2.1 关于SPEC文件

可以把spec文件比作是:制作 RPM 包的配方文件,SPEC 文件具体定义了如果制作一个 RPM 包,文件内容主要包括两部分:头信息(Preamble ) 和 正文部分(Body);头信息定义了一系列元数据 metadata 供正文使用,而正文内容描述了构建 RPM 包具体内容具体流程和步骤。

4.2.1.1 头信息字段

接下来我们先来看看spec文件中的头信息(Preamble)字段内容。

指令作用
Name包名,建议和对应spec文件名保持一致(但其实文件名可以不一致)
Version上游软件版本号
Release对应版本的软件所构建包的次数,一般来初始化值是:1%{?dist},每构建一次数量增加1。但如果是基于新的 Version 构建,那么把该值再新设为:1%{?dist}
Summary构建包的总结摘要。
License打包软件的 License
URL源代码程序相关说明地址,大部分时候会使用上游的软件源代码项目地址URL
Source0上游源代码压缩文件所对应的路径或者url地址。这个路径地址必须是可靠的。如果有需要,可以定义多个源路径地址,如:Source1、Source2、Source3等等。
Patch0应用到源代码的第一个补丁文件。如有需要,也可以定义多个补丁文件,如:Patch1、Patch2等等。
BuildArch构建架构,如果一个包不需要考虑计算机架构依赖,比如整个包都是使用解释型语言编写的,那么此时可以把改参数设置为成 BuildArch: noarch 。如果不设置,那么构建打包的时候,会根据打包机器所在架构环境作为参考值来构建包,比如64位环境下则是: x86_64
BuildRequires包构建时所依赖的其他软件包资源,多个依赖包使用逗号或空白符间隔。当然也可以定义多个 BuildRequires 字段,每个 BuildRequires 独占一行。
Requires软件包运行时所依赖的软件包资源,同 BuildRequires 类似,多个包可以使用逗号或空白符分隔,也可以定义多个 Requires 字段,每个 Requires 独占一行。
ExcludeArch定义软件不支持的系统架构,如果软件对于特定的架构系统不支持,那么可以再此处定义该架构名称,表示该软件排除该架构系统。

最终生成的 RPM 包的包名是由:NameVersionRelease 三个指令对应值组合而成的。RPM 包维护人员或者系统管理人员经常简称这个命名结构为:N-V-R 或 NVR,因为 RPM 包名组成格式即:NAME-VERSION-REELASE

你不妨使用 rpm 查询某个包验证一下是否是这样的:

$ rpm -q python
# Name-Version-Release
python-2.7.5-34.el7.x86_64

python 对应 Name 字段,2.7.5 对应:Version 字段,34.el7 对应 Release 字段,x86_64 为系统架构类型,也是唯一一个由当前打包环境添加的字段。

4.2.1.2 正文打包内容部分

如下部分为spec文件正文部分内容字段:

指令描述
%description软件描述,描述内容可以占多行、多段落。
%prep准备打包阶段,该阶段可以定义一些打包前的准备工作,比如解压:Source0 源码包,该指令可以使用 shell 脚本命令。
%build打包阶段,包含一些列打包命令,用于编译构建生成机器码或者字节码。
%install安装阶段,该阶段会把 %build 所生成的文件拷贝到 %buildroot 所对应的目录,在该目录下会生成上文提到的 NVR 结构软件包子目录,一般来说就是从 ~/rpmbuild/BUILD 目录拷贝对应编译过的文件到 ~/rpmbuild/BUILDROOT ,并生成相应文件目录结构。这只是会在打包的时候运行,用户最终安装软件包的时候不会发生。
%check测试阶段,可以定义一些列命令来测试构建的软件包。比如可以运用一些单元测试。
%files文件什么阶段,定义声明需要最终安装到用户系统的文件目录清单。
%changelog记录打包构建不同的版本或Release操作记录信息,相当于本次打包说明。

除了上述定义的指令外令,SPEC 文件还包含一些高级指令,这些高级指令将在后面的章节中详细介绍。

4.2.2 构建根 BuildRoots

RPM 打包的上下文中,buildroot 代表的是一个 chroot 环境。那么什么是 chroot 环境?简单来说,chroot,即 change root directory (更改 root 目录);在 linux 系统中,系统默认的目录结构都是以 /,即是以根 (root) 开始的。而在使用 chroot 之后,系统的目录结构将以指定的位置作为 / 位置。

这也就意味着打包构建所在的目录结构和最终软件安装在用户系统所对应的目录结构是一致的,在 buildroot 下的文件将使用 cpio 制作成一个压缩包,这个压缩包就是RPM包的主要内容,当这个包被用户安装时,它就会被解压到用户所在系统根目录(root)相对于的目录下。

4.2.3 RPM 宏指令

rpm 宏指令 其作用类似于 C 语言中的宏的作用, SPEC 文件相同的宏指令,都会被替换为相对应的值,可以使用如下命令查看一个宏的具体结果值:

# rpm --eval %{_MACRO}
$ rpm --eval %{_bindir}
/usr/bin

一个常见的宏:%{?dist} 代表的是”系统发行版本标签“,表示这个软件包是由何种发行版本的系统构建出来的,比如下面的使用RedHat企业版的结果:

# On a RHEL 7.x machine
$ rpm --eval %{?dist}
.el7

关于更多的宏指令,请参考章节:

4.2.4 开始制作SPEC文件

制作RPM包的大部分工作即是编写 SPEC 文件,接下来我们将介绍如何编写一个 spec 文件。

如果要想构建一个新的 RPM 软件包,那么就需要创建一个对应 SPEC 文件,建议使用 rpmdev-newspec 命令来生成基本的文件内容结构。

我们还是使用之前编写的三个hello world!程序代码作为demo程序:

把这几个准备好的压缩包放到 ~/rpmbuild/SOURCES 目录下,然后我们将为这三种不同源码类型程序制作对应的 SPEC 文件。命令如下:

$ cd ~/rpmbuild/SPECS

$ rpmdev-newspec bello
bello.spec created; type minimal, rpm version >= 4.11.

$ rpmdev-newspec cello
cello.spec created; type minimal, rpm version >= 4.11.

$ rpmdev-newspec pello
pello.spec created; type minimal, rpm version >= 4.11.

现在 ~/rpmbuild/SPECS 目录下将会有三个不同的 SPEC 文件:bello.speccello.specpello.spec

上述的三种类型程序代表这三种不同类型的RPM构建类型,在编写自己的RPM包时可以选择和自己相类似的构建方式:

软件名称说明
bello由解释型语言编写(bash)的程序。这种代码不需要编译,只需要安装即可。或者如果你是想打包一个已经编译好的二进制文件到RPM包,也可以采用这种方式,因为二进制文件已经是一个编译过的文件。
pello一个字节码解释型程序,需要编译为字节码,然后安装编译后的优化文件。
cello一个编译型的语言编写的程序,需要编译,然后安装编译后的可执行文件。
4.2.7.1 bello

先来制作 bello.spec 文件内容,为了节约篇幅,先把最后的配置结果展示如下,然后再做详细解释。

Name:           bello
Version:        0.1
Release:        1%{?dist}
Summary:        Hello World example implemented in bash script

License:        GPLv3+
URL:            https://www.example.com/%{name}
Source0:        https://www.example.com/%{name}/releases/%{name}-%{version}.tar.gz

Requires:       bash

BuildArch:      noarch

%description
The long-tail description for our Hello World Example implemented in
bash script.

%prep
%setup -q

%build

%install

mkdir -p %{buildroot}/%{_bindir}

install -m 0755 %{name} %{buildroot}/%{_bindir}/%{name}

%files
%license LICENSE
%{_bindir}/%{name}

%changelog
* Tue May 31 2016 Adam Miller <maxamillion@fedoraproject.org> - 0.1-1
- First bello package
- Example second item in the changelog for version-release 0.1-1

下面来对上述的字段一一解释:

对于以上的 URLSource0 字段,我们还是用了宏指令%{name}%{version}, 能够方便后期修改,更改一处,所有宏位置都会被替换。

如果之前所说,BuildRequires 代表的是打包构建时所依赖的软件包名称,但 bello 程序不需要依赖其他软件(因为不需要编译),所以直接删除该指令即可。

%changelog 描述的是打包日志记录信息,并非是上游源码更新的日志。并且有具体的格式内容,如下:

* Day-of-Week Month Day Year Name Surname <email> - Version-Release

首行内容必须以 * 开始,时间格式也是固定的,不能调整改变格式。紧接着的是具体更改内容,可以为多行,每行以 - 开头。

bello.spec 文件制作完毕,所有内容参见开头部分。

4.2.7.2 pello

pello 使用python编写,也是属于解释型语言,和系统架构无关,只需要用户系统拥有 python 环境即可,但需要编译为字节码pyc文件,因此需要调整 %build 部分,pello.spec 文件内容如下:

Name:           pello
Version:        0.1.1
Release:        1%{?dist}
Summary:        Hello World example implemented in python

License:        GPLv3+
URL:            https://www.example.com/%{name}
Source0:        https://www.example.com/%{name}/releases/%{name}-%{version}.tar.gz

BuildRequires:  python
Requires:       python
Requires:       bash

BuildArch:      noarch

%description
The long-tail description for our Hello World Example implemented in
Python.

%prep
%setup -q

%build
# 编译python源码文件为字节码文件
python -m compileall %{name}.py

%install
# 创建相应目录
mkdir -p %{buildroot}/%{_bindir}
mkdir -p %{buildroot}/usr/lib/%{name}

# 安装
cat > %{buildroot}/%{_bindir}/%{name} <<-EOF
#!/bin/bash
/usr/bin/python /usr/lib/%{name}/%{name}.pyc
EOF

chmod 0755 %{buildroot}/%{_bindir}/%{name}

install -m 0644 %{name}.py* %{buildroot}/usr/lib/%{name}/

%files
%license LICENSE
%dir /usr/lib/%{name}/
%{_bindir}/%{name}
/usr/lib/%{name}/%{name}.py*

%changelog
* Tue May 31 2016 Adam Miller <maxamillion@fedoraproject.org> - 0.1.1-1
  - First pello package

需要注意的是,%files 指令下定义了%dir /usr/lib/%{name}/ 目录来声明,该目录以及目录下所有文件所有权归属于该软件包。对于 %files 清单里一个目录是否使用 %dir 指令来标示,其作用是不一样的,详细内容请参考:http://ftp.rpm.org/max-rpm/s1-rpm-inside-files-list-directives.html,简单来说即是:未使用 %dir 指令的目录表示该目录以及目录下所有子目录以及相关文件都会被标示为打包文件;使用 %dir 指令的目录则只会标示该目录本身,不会标示目录下任何文件。

4.2.7.3 cello

cello 文件是由 C 语言编写,我们需要在打包环节把他编译成二级制机器码文件,然后安装到对应目录,并且它是和系统架构有关的,并且我们还需要打一个补丁文件 Patch0:

Name:           cello
Version:        1.0
Release:        1%{?dist}
Summary:        Hello World example implemented in C

License:        GPLv3+
URL:            https://www.example.com/%{name}
Source0:        https://www.example.com/%{name}/releases/%{name}-%{version}.tar.gz

Patch0:         cello-output-first-patch.patch

BuildRequires:  gcc
BuildRequires:  make

%description
The long-tail description for our Hello World Example implemented in
C.

%prep
%setup -q

%patch0

%build
make %{?_smp_mflags}

%install
%make_install

%files
%license LICENSE
%{_bindir}/%{name}

%changelog
* Tue May 31 2016 Adam Miller <maxamillion@fedoraproject.org> - 1.0-1
- First cello package

首先,该脚本编译时需要 gccmake 工具,并且该脚本比较简单,未定义任何编译配置文件,因此直接移除 %configure 指令,直接使用 make 编译即可。

%patch0 指令则是在打包准备阶段,自动把对应的补丁文件应用到源代码文件中,实现自动打补丁功能。并且其实 %setup -q%patch0 还可以精简为另一个宏指令: %autosetup,其作用是一致的,只是让我们的 spec 文件看起来更为简洁,详细请参考:%autosetup

cello.spec 文件剩于部分内容和之前的 spec 文件类似,这里不再赘述。

4.3 构建RPM包

RPM 包的构建使用的是 rpmbuild 命令来完成。我们经常构建如下两种类型的包:

  1. RPM 源码包,以.srpm 后缀结尾。
  2. 二进制RPM包,以.rpm 后缀结尾。

4.3.1 RPM 源码包(SRPM)

构建RPM源码包(SRPM)往往基于如下两个目的:

  1. 想保留部署到用户系统的RPM包的具体的源数据,包括SPEC文件,里面的源代码,或相应的补丁文件。这对于包的历史回溯和debug非常有用。
  2. 可以基于这份源码包,调整相应的硬件平台或者架构环境构建二进制包。

构建命令如下:

~]$ rpmbuild -bs SPECFILE

-bs 代表的是:build source 的意思。应用到当前示例项目,则如下:

$ cd ~/rpmbuild/SPECS/

$ rpmbuild -bs bello.spec
Wrote: /home/admiller/rpmbuild/SRPMS/bello-0.1-1.el7.src.rpm

$ rpmbuild -bs pello.spec
Wrote: /home/admiller/rpmbuild/SRPMS/pello-0.1.1-1.el7.src.rpm

$ rpmbuild -bs cello.spec
Wrote: /home/admiller/rpmbuild/SRPMS/cello-1.0-1.el7.src.rpm

4.3.2 构建二进制RPM包

对于用户来说,想使用的软件包功能,需要安装的是二进制RPM包,SRPM 包也只是用来构建二进制RPM的一种途径,但SRPM包无法为用户直接提供软件功能。

制作二进制RPM包,可以通过如下两种途径:

  1. 重新打包 SRPM 包,生成对应的二进制包,使用:rpmbuild --rebuild 命令
  2. 直接基于SPEC文件直接生成二进制RPM包:rpmbuild -bb 命令,-bb 是: build binary 缩写,意思是构建二进制。
  3. rpm -i *.src.rpm 临时安装源码包到当前 ~/rpmbuild/下 ,然后再根据代码,打二进制包。
4.3.2.1 基于SRPM包构建二进制包

直接使用如下命令构建之前三个SPRM包的二进制版本:

$ rpmbuild --rebuild ~/rpmbuild/SRPMS/bello-0.1-1.el7.src.rpm
...

$ rpmbuild --rebuild ~/rpmbuild/SRPMS/pello-0.1.1-1.el7.src.rpm
...

$ rpmbuild --rebuild ~/rpmbuild/SRPMS/cello-1.0-1.el7.src.rpm
...

执行 rpmbuild --rebuild 命令实质做了如下几件事情:

  1. 安装SRPM 包到 ~/rpmbuild 相应目录下。
  2. 使用安装的spec文件和源码文件构建二进制包。
  3. 删除spec文件和源码文件。

如果你想保留SPEC文件和源码,可以通过下面两种方式:

  1. 在构建的时候使用 --recompile 参数替换 --rebuild 参数。
  2. 或者在安装时候使用 rpm -Uvh *.src.rpm 源码包。
4.3.2.2 基于SPEC文件直接构建二进制包

如果有SPEC文件,那么可以直接构建二进制包:

$ rpmbuild -bb ~/rpmbuild/SPECS/bello.spec

$ rpmbuild -bb ~/rpmbuild/SPECS/pello.spec

$ rpmbuild -bb ~/rpmbuild/SPECS/cello.spec

4.4 测试RPM

构建完一个新的RPM包之后,我们需要检测所构建的包是否满足需要。并且检测构建的RPM包是否有错误,我们将使用 rpmlint 工具来分析构建的RPM包。

不过 rpmlint 工具过于严格,在某些情况下,可能需要忽略某些警告信息,这个请根据自身情况判定检测结果是否需要优化处理。

4.4.1 测试SPEC文件

$ rpmlint bello.spec
bello.spec: W: invalid-url Source0: https://www.example.com/bello/releases/bello-0.1.tar.gz HTTP Error 404: Not Found
0 packages and 1 specfiles checked; 0 errors, 1 warnings.

在这里检测之后,Source0 字段提供的url地址不可访问。可以根据自己源代码网址自行修改,此处为demo程序,缺少对应的URL地址。

4.4.2 测试二进制RPM包

同样检测二进制包也使用 rpmlint 工具即可:

$ rpmlint ~/rpmbuild/RPMS/noarch/bello-0.1-1.el7.noarch.rpm

对于检测中发生的警告和错误,请根据自己实际情况自行调整优化。

5 高级功能

5.1 签名

为了防止用户下载的软件包被恶意篡改伪造,在给制作的包做一个数字签名是非常有必要的,用户可以签名来验证包的合法性。以下有三种做签名的场景:

  1. 给打好的包添加一个数字签名。
  2. 给打好的包更新数字签名。
  3. 在构建二进制包的时候同时添加数字签名。
5.1.1 签名前的准备工作

在制作签名之前,首先需要使用 gpg2 --gen-key 在本机上生成 RSA 非对称公钥,详细步骤请参考: https://access.redhat.com/articles/3359321

不过需要注意的是,该命令在CentOS7下 使用 su 切换用户场景下无法设置证书 passphrase 密码,如果你也遇到类似的问题,可以使用如下方式解决:

# https://bugzilla.redhat.com/show_bug.cgi?id=659512#c9
$ script /dev/null
$ gpg2 --gen-key

此外,gpg 生成命令在为获取到足够多的随机数时,当前操作步骤会发生假死现象(屏幕被卡主),这时候我们可以使用另外一个生成随机数辅助工具 1rng-tools 加快随机数生成速度,另外打开一个shell窗口,然后执行如下命令:

yum -y install  rng-tools
rngd -r /dev/urandom

在执行完上述辅助操作过后,你会发现gpg2 --gen-key已经顺利完成,我们可以使用:gpg2 --list-key 来看刚才所生成的key:

$ gpg2 --list-keys
/home/work/.gnupg/pubring.gpg
-----------------------------
pub   1024R/029A3FD9 2019-02-12
uid                  liangqi <liangqi000@gmail.com>
sub   1024R/1B3F1650 2019-02-12

密钥对已经生成结束,那么接下来我们还需要配置rpmbuild的环境变量:

$ echo %_signature gpg2 >> ~/.rpmmacros # centos6 去掉此行,%_signature为centos7新增宏
$ echo "%_gpg_name liangqi" >> ~/.rpmmacros
5.1.2 为已有包添加签名

为已有的包签名请使用 --addsign 参数:

[work@localhost ~]$ rpm --addsign ~/rpmbuild/RPMS/x86_64/php-7.3.2-3.el7.x86_64.rpm 
Enter pass phrase: 
Pass phrase is good.

提示:如果运行该命令提示 rpmsign command not found 错误, 那么你需要安装一下rpm-sign包:yum install rpm-sign

--addsign 可以添加多个签名,这对于软件包的分发流转非常有用,因为很多场景下,可能会遇到一个包从制作到最终用户使用,中间可能经历了很多分销渠道,这些分销平台获得软件包以后,自己会追加一个签名,这样下游如果基于此分销渠道获得软件包既可以验证该包的合法性,这样可以保证一个软件包在整个流转环节的每一步都能够保证其安全性。

5.1.3 更新签名
rpm --resign ~/rpmbuild/RPMS/x86_64/php-7.3.2-3.el7.x86_64.rpm
Enter pass phrase: 
Pass phrase is good.

更新签名使用参数 --resign 即可。并且,更新签名也可以批量改签:

$ rpm --resign b*.rpm
Enter pass phrase:
Pass phrase is good.
5.1.4 构建时生成签名

在构建包的时候也可以立即生成签名,只要附带 --sign 参数即可,如果同时构建SRPMRPM包,都会有签名。

rpmbuild -ba --sign ~/rpmbuild/SPECS/php.spec
5.1.3 签名验证

软件包添加了签名,那么如何验证签名呢,验证签名的场景也分为两种:

  1. rpm 方式验证,需要把公钥导入rpm数据库中。
  2. Yum 方式安装软件,需要导出证书,配置相应Yum repo文件字段。

不管是哪种方式我们都需要导出公钥文件,因此我们提前做好准备工作,执行如下命令:

gpg2 --export -a 'liangqi' > METO-CC-RPM-GPG-KEY
5.1.3.1 rpm 方式验证

查看当前机器rpm数据保存的公钥:

$ rpm -q  gpg-pubkey-*
gpg-pubkey-f4a80eb5-53a7ff4b

导入公钥到rpm数据库,注意需要root权限:

$ sudo rpm --import METO-CC-RPM-GPG-KEY  

# 验证是否导入成功
$ rpm -q  gpg-pubkey-*
gpg-pubkey-f4a80eb5-53a7ff4b
gpg-pubkey-029a3fd9-5c629583

接下来验证签名,验证签名使用 -K 或者 --checksig 均可

$ rpm -K ~/rpmbuild/RPMS/x86_64/php-7.3.2-3.el7.x86_64.rpm
blather-7.9-1.i386.rpm: size pgp pgp md5 OK
5.1.3.1 Yum方式验证

使用 Yum 安装 rpm 包时,需要在对应的配置文件里配置相应的公钥key文件路径,已经开启验证功能:

...
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/METO-CC-RPM-GPG-KEY

这样如果软件包是通过该源安装的,那么就会自动实现签名验证。请根据自身情况,合理配置公钥key路径。

5.2 更多宏指令用法(Macros)

RPM内置宏指令非常多,在 rpm官方宏指令文档 有详细介绍,接下来的内容部分我们将介绍其中一些常用指令。

并且,RedHat 版本还包含了一些特有的一些宏指令,在本节中我们也会提到。

5.2.1 自定义宏指令

除了使用内置的宏指令,用户还可以自定义宏指令,如果想详细了解宏指令内容,请参考 RPM 官方文档 ,自定义格式如下:

%global <name>[(opts)] <body>

所有\周围的空白字符都会被移除。指令名称name的命名要求为:至少三位字符,英文字母、数字、下划线组合。自定义指令还可以附加一个(opts)可选项参数,对于没有(opts)参数的简单宏指令只会执行递归宏扩展操作。当一个带有(opts)参数的宏指令被调用解析时,opts 值(括号中的字符串)会原样传递给argc/argv处理程序中getopt(3)方法来处理。

注意

旧的RPM SPEC 文件里可能常用:%define <name> <body> 来定义,%define%global 指令的区别如下:

  • %define 拥有的是局部作用域,这意味着它只适用于spec文件的指定部分。此外,当使用%define宏时,它的主体将被展开——它将被延迟计算。
  • %global 拥有的是全局作用域,这意味着它作用于整个SPEC文件。此外,%global 定义的主体值载定义的时候就会展开。

例如:

%global githash 0ec4e58
%global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")

注意

宏指令都是属于计算类型的,哪怕是在注释中。计算型特性有时候也没有坏处。不过在第二条示例语句中,我们执行 python 命令来获取宏的内容。即使注释掉宏,也将执行此命令。或者当您将宏放入%changelog 里事也是如此。如果要注释掉宏,请使用 %%。例如,%%global

5.2.2 %setup

宏指令 %setup 可以在基于源代码压缩包构建RPM包时使用。在 rpmbuild 构建打印输出内容中我们可以看到 %setup 指令到底执行了什么操作。在构建的每个阶段开头部分,都会显示出类似如下格式内容:

Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.DhddsG

因为 Shell 脚本输出里设置 set -x 参数,因此会打印出脚本里每一个步骤所执行的命令及参数。但是,在 rpmbuild 打包构建成功后,/var/tmp/rpm-tmp.DhddsG 临时文件会被立即删除,如果想要查看该文件的内容,那么需要加上 --debug 参数。下面的例子展示了打包构建时,初始化环境配置(%setup代表的操作)内部所执行的结果:

cd '/builddir/build/BUILD'
rm -rf 'cello-1.0'
/usr/bin/gzip -dc '/builddir/build/SOURCES/cello-1.0.tar.gz' | /usr/bin/tar -xof -
STATUS=$?
if [ $STATUS -ne 0 ]; then
  exit $STATUS
fi
cd 'cello-1.0'
/usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .

%setup 这一条指令就把源码解压缩,设置正确的目录权限,删除之前存在的同名目录操作等一并解决了,并且 %setup 指令可添加额外参数来定制自身的行为。

5.2.2.1 %setup -q

选项 -q 限制了%setup结果的详细程度。原有的 tar -xvvof 命令会被替换为 tar -xof 命令。需要注意的是,该参数需要放在首位位置。

5.2.2.2 %setup -n

很多时候,源代码压缩包名可能和%{name}-%{version}不一致。这时候如果使用 %setup 会出现错误。那么这时候可以使用 -n 参数来指定压缩包的真实名称。

比如,我们打包的名称为 cello, 但是上游提供的源代码却是:hello-1.0.tgz 且解压后的目录名是 hello, 那么这种情况下,我们需要按如下来定义:

Name: cello
Source0: https://example.com/%{name}/release/hello-%{version}.tar.gz
…
%prep
%setup -n hello
5.2.2.3 %setup -c

选项 -c 可以用于源码压缩包里没有包含任何子目录的场景,集当压缩包解压的时候文件都被解压缩到当前目录下,这种情况下,-c 选项可以帮助我们创建一个目录,然后把解压内容到该目录下,相当于执行了如下类似命令:

/usr/bin/mkdir -p cello-1.0
cd 'cello-1.0'
5.2.2.4 %setup -D and -T

-D 选项代表:禁止删除源代码目录。在多次使用 %setup 指令时可能会需要,-D 参数意味着下面的命令不在会使用:

rm -rf 'cello-1.0'

-T 参数意味着下面这行解压命令会被移除:

/usr/bin/gzip -dc '/builddir/build/SOURCES/cello-1.0.tar.gz' | /usr/bin/tar -xvvof -
5.2.2.5 %setup -a and -b

-b 选项代表:在切换目录前解压第n个Source*源,它需要和source 字段配合使用。-b 选项必须指定一个数字参数来匹配 source 标签,如果未指定数字,则会报错。

spec文件中第一个source字段隐含的索引数字是:0,如果我们尝试运行:%setup -b 0

cd /usr/src/redhat/BUILD
rm -rf cdplayer-1.0
gzip -dc /usr/src/redhat/SOURCES/cdplayer-1.0.tgz | tar -xvvf -
if [ $? -ne 0 ]; then
  exit $?
fi
gzip -dc /usr/src/redhat/SOURCES/cdplayer-1.0.tgz | tar -xvvf -
if [ $? -ne 0 ]; then
  exit $?
fi
cd cdplayer-1.0
cd /usr/src/redhat/BUILD/cdplayer-1.0
chown -R root.root .
chmod -R a+rX,g-w,o-w .

从执行结果中发现,解压命令执行了两次,这也正说明了 -T 参数的意义所在,-T 参数会禁用掉默认的解压缩操作,因此这俩命令往往会结合使用:

%setup -T -b 0

# 结果如下
cd /usr/src/redhat/BUILD
rm -rf cdplayer-1.0
gzip -dc /usr/src/redhat/SOURCES/cdplayer-1.0.tgz | tar -xvvf -
if [ $? -ne 0 ]; then
  exit $?
fi
cd cdplayer-1.0
cd /usr/src/redhat/BUILD/cdplayer-1.0
chown -R root.root .
chmod -R a+rX,g-w,o-w .

-a 参数代表: 在切换目录之后解压缩指定的第n个资源Source,同-b参数,-a 参数也需要和-T参数一起使用来避免压缩包被解压两次。输出结果类似如下:

cd /usr/src/redhat/BUILD
rm -rf cdplayer-1.0
cd cdplayer-1.0
gzip -dc /usr/src/redhat/SOURCES/cdplayer-1.0.tgz | tar -xvvf -
if [ $? -ne 0 ]; then
  exit $?
fi
cd /usr/src/redhat/BUILD/cdplayer-1.0
chown -R root.root .
chmod -R a+rX,g-w,o-w .

关于跟多的 %setup 参数介绍,请参考rpm 官方文档中关于 %setup 指令文档内容。

5.2.3 %files

%files 指令用来声明哪些文件需要被打包到RPM最终的包里,需要注意的是,需要注意的是,如果%{buildroot} 下构建的包中有一个文件没有在 %files说明,那么打包就会失败。并且该 %files 清单有如下一些宏指令来辅助声明:

描述
%license声明一个整数许可文件
%doc使用该标记标记的文件会被RPM包识别为文档类型的文件,例如:%doc README。该类型不光包括文档内容,还包括如示例代码以及文档相关附件等。不过需要注意的是,文档中的示例代码应该移除可执行的权限。
%dir用来声明一个目录本身隶属于RPM包,例如:%dir %{_libdir}/%{name}。精确的什么哪些目录和文件属于软件包本身,在卸载软件的时候显得尤为重要。但请注意 %dir 只标记目录本身,并不会标记该目录下任何文件或子目录,如果项目标记一个目录以及目录下所有的子文件都需要被打包,那么直接声明该目录即可,不要使用 %dir 来标记。
之所以存在%dir目录原因也在于此,直接声明一个目录,该目录所有文件都会被打包,但是有些时候我们并不需要这样,可能想排除掉某些文件。
%config(norepalce)声明文件为配置文件,标记为配置文件的好处在于,如果更新一个安装包,此配置文件不会被覆盖如果此配置文件在用户系统配置里被修改过。当出现这种情况时,rpm会创建一个基于原有配置文件名并附加一个.rpmnew后缀来保存新的配置,同时保留原有配置文件原封不动。

5.2.4 内置宏指令

操作系统了包含了很多宏指令,可以使用 rpm --showrc 来查看所有的宏指令。

您还可以通过查看rpm-ql rpm的输出,并记下目录结构中名为macros的文件,找到有关直接随系统版本的rpm提供的宏的信息。

5.2.5 重载发布宏指令

我们可以在 ~/.rpmmacros 中重写分发的宏指令,不过记住,这里的修改是全局的,会影响到后续所有的构建包。比如如下可以覆盖的宏:

%_topdir 工作根目录,默认值是 ~/rpmbuild

%_smp_mflags -l3 该宏常常用来传递给Makefile文件,例如:make %{?_smp_mflags} 并在构建阶段设置一些并发进程。默认是:-jXX代表的是机器内核数量,不过修改内核数量可能会加速也可能减缓构建速度。

当然你也可以自定义其他一些宏指令到 ~/.rpmmacros 文件下,

5.3 Epoch, Scriptlets, and Triggers

Epoch 标签提供一种基于软件版本加权的操作。如果未定义该值,则默认值为0,并且官方不建议使用该指令,因为它会打乱原有基于版本号来对比软件方式。比如,软件名为:hello 的第一个版本为 Version: 1.0Epoch: 1,但是,当我们升级 hello 软件版本 Version: 2.0 之后,忘记配置 Epoch 指令,导致 Epoch 默认值变为了 0,那么这个新软件包用户是无法升级使用的。

因此,如果我们非要使用 Epoch 标记,那么一定要不能让该值出现递减的趋势,并且其值需要是一个正整数值。

所以,Epoch 标记往往只在万不得已的时候使用,比如上游软件重新设计了软件的版本号或者软件的版本号包含一些字母导致无法准确的对比软件的版本,这时候可以考虑使用 Epoch 指令。

5.3.1 Triggers and Scriptlets

RPM 包里,当软件被用户安装到操作系统中期间可以出发一些宏指令,这些宏指令可以适当的修改操作系统的某些配置,它们被称作是:脚本(Scriptlets)

一个常见的例子,一个软件被安装到系统中时,你希望把它同时配置到系统的 systemd 服务中,由 systemd 来管理该软件的运行;且当该软件卸载的时候,我们还需要把他从系统的 system 服务里移除,避免软件被删除,systemd 因找不到该软件而异常报错。

脚本指令和章节头指令像 %build%install 类似,在这些指令之后可以定义多行标准的POSIX shell脚本命令代码,或者一些其他编程语言,只要这种编程语言在目标系统里允许使用即可。下面是一些常用的脚本头宏指令:

指令定义
%pre软件安装前
%post软件安装后
%preun软件卸载前
%postun软件卸载后

另一个对整个RPM安装事务提供更细粒度控制的方式是所谓的触发器(Triggers)。这些实际上与 scriptlet 是效果是相同,但是触发器可以在RPM安装或升级事务期间以非常特定的操作顺序执行,从而允许对整个过程进行更细粒度的控制。

指令执行顺序和具体详情如下:

all-%pretrans
...
any-%triggerprein (%triggerprein from other packages set off by new install)
new-%triggerprein
new-%pre      for new version of package being installed
...           (all new files are installed)
new-%post     for new version of package being installed

any-%triggerin (%triggerin from other packages set off by new install)
new-%triggerin
old-%triggerun
any-%triggerun (%triggerun from other packages set off by old uninstall)

old-%preun    for old version of package being removed
...           (all old files are removed)
old-%postun   for old version of package being removed

old-%triggerpostun
any-%triggerpostun (%triggerpostun from other packages set off by old un
            install)
...
all-%posttrans

以上的指令来自于 /usr/share/doc/rpm-4.*/triggers 文档。

5.2.1.1 使用非shell脚本语言

脚本参数选项 -p 可以配置使用的其他解析器来运行脚本指令下定义的代码,默认使用的是 -p /bin/sh 解析器。比如我们在安装一个 pello.py 文件后想用 python 打印一条消息:

%post -p /usr/bin/python3
print("This is {} code".format("python"))

当该软件包被安装的时候,则会打印如下消息:

Installing       : pello-0.1.1-1.fc27.noarch                              1/1
Running scriptlet: pello-0.1.1-1.fc27.noarch                              1/1
This is python code

想使用其他脚本语言,也是类似操作。

5.4 RPM条件表达式

RPM 条件语句可以应用到spec配置的各个节段,常用到如下部分中:

  1. 架构定义场景
  2. 操作系统场景
  3. 特定操作系统或者版本适配问题
  4. 宏的定义或者检测是否存在场景
5.4.1 %if 语法格式
%if expression
...
%endif

#
%if expression
...
%else
...
%endif
5.4.2 条件语法示例

如果操作系统是rhel6,则额外添加如下两条指令:

%if 0%{?rhel} == 6
sed -i '/AS_FUNCTION_DESCRIBE/ s/^/#/' configure.in
sed -i '/AS_FUNCTION_DESCRIBE/ s/^/#/' acinclude.m4
%endif

变量按条件重写:

%define ruby_archive %{name}-%{ruby_version}
%if 0%{?milestone:1}%{?revision:1} != 0
%define ruby_archive %{ruby_archive}-%{?milestone}%{?!milestone:%{?revision:r%{revision}}}
%endif
5.4.2.1 %if特有变量

%ifarch 用来检测是否是匹配某个架构,多个架构类型可以使用逗号或空白字符(空格)分隔:

%ifarch i386 sparc
...
%endif

上述代码表示只有32位Amd的系统架构才执行 %ifarch%endif 之间的代码。

%ifnarch 表示对 %ifarch 取反操作,即非某种架构执行该语句下代码。

%ifos 用于匹配是否是某种操作系统,可以匹配一个或多个操作系统:

%ifos linux
...
%endif