Linux二进制包(RPM包)制作教程

学习rpm打包,一篇文章就够了

本文详细讲解打包的各个步骤并重点讲述spec文件的编写。

rpm打包过程使用的源码包制作可以参考教程:Linux源码包的制作教程

RPM(Redhat Package Manager)是用于Redhat、CentOS、Fedora等Linux 分发版(distribution)的常见的软件包管理器。因为它允许分发已编译的软件,所以用户只用一个命令就可以安装软件。

本文从配置文件到打包,从打包到安装,从安装到原理深度解析。并用案例展示详细步骤,也可供作为模板修改,也可作为打包时的资料查询。

如果只是想知道怎么打包,不需要了解原理,请从“案例”直接看,并修改相应内容即可。

一、准备工作

本文基于CentOS 7.9系统,rpm版本4.11.3,理论上只要是使用rpm的发行版都适用

1
2
3
4
5
6
[root@node1 ~]# cat /etc/redhat-release
CentOS Linux release 7.9.2009 (Core)
[root@node1 ~]# uname -a
Linux node1 3.10.0-1160.49.1.el7.x86_64 #1 SMP Tue Nov 30 15:51:32 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
[root@node1 ~]# rpm --version
RPM 版本 4.11.3

1.安装基础软件

gcc,make 用于编译c语言源代码

tree 方便查看文件夹目录

dos2unix 保证文件可执行性

1
yum -y install gcc make tree dos2unix rpmlint

2.Rpm打包软件安装

RPM打包使用的是rpmbuild命令,这个命令来自rpm-build包,这个是必装的

1
yum -y install rpm-build

当然也可以直接安装rpmdevtools,这个工具还包含一些其他的工具,同时它依赖rpm-build,所以直接安装的话会同时把rpm-build装上。

1
yum -y install rpmdevtools

二、RPM打包原理

1.工作空间

RPM打包的时候需要编译源码,还需要把编译好的配置文件啊二进制命令文件啊之类的东西按照安装好的样子放到合适的位置,还要根据需要对RPM的包进行测试,这些都需要先有一个标准化的“工作空间”。

1.1.rpmdev-setuptree工具生成目录结构

rpmdev-setuptree这个命令就是安装rpmdevtools带来的 。可以看到运行了这个命令之后,在$HOME家目录下多了一个叫做rpmbuild的文件夹 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[root@node2 ~]# rpmdev-setuptree
[root@node2 ~]# tree rpmbuild
rpmbuild
├── BUILD
├── RPMS
├── SOURCES
├── SPECS
└── SRPMS

5 directories, 0 files

1.2.mkdir命令生成目录结构

如果没有安装rpmdevtools的话,其实用mkdir命令创建这些文件夹也是可以的。

1
mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

1.3.目录结构说明

默认位置 宏代码 名称 用途
~/rpmbuild/SPECS %_specdir Spec 文件目录 保存 RPM 包配置(.spec)文件
~/rpmbuild/SOURCES %_sourcedir 源代码目录 保存源码包(如 .tar 包)和所有 patch 补丁
~/rpmbuild/BUILD %_builddir 构建目录 源码包被解压至此,并在该目录的子目录完成编译
~/rpmbuild/RPMS %_rpmdir 标准 RPM 包目录 生成/保存二进制 RPM 包
~/rpmbuild/SRPMS %_srcrpmdir 源代码 RPM 包目录 生成/保存源码 RPM 包(SRPM)
~/rpmbuild/BUILDROOT %_buildrootdir 最终安装目录 保存 %install 阶段安装的文件
  • SPECS下是RPM包的配置文件,是RPM打包的“图纸”,这个文件会告诉rpmbuild命令如何去打包。“宏代码”这一列就可以在SPEC文件中用来代指所对应的目录,类似于编程语言中的宏或全局变量。当然~/rpmbuild这个文件夹也是有宏代码的,叫做%_topdir。

  • rpmbuild 默认工作路径的确定,通常由在 /usr/lib/rpm/macros 这个文件里的一个叫做 %_topdir 的宏变量来定义。如果用户想更改这个目录名,rpm 官方并不推荐直接更改这个目录,而是在用户家目录下建立一个名为 .rpmmacros 的隐藏文件(Linux下隐藏文件,前面的点不能少),然后在里面重新定义 %_topdir,指向一个新的目录名。这样就可以满足某些用户的差异化需求了。.rpmmacros 文件里内容,比如:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    michael@localhost  ~  cat .rpmmacros
    
    %_topdir %(echo $HOME)/rpmbuild
    
    %_smp_mflags %( \
        [ -z "$RPM_BUILD_NCPUS" ] \\\
            && RPM_BUILD_NCPUS="`/usr/bin/nproc 2>/dev/null || \\\
                                 /usr/bin/getconf _NPROCESSORS_ONLN`"; \\\
        if [ "$RPM_BUILD_NCPUS" -gt 16 ]; then \\\
            echo "-j16"; \\\
        elif [ "$RPM_BUILD_NCPUS" -gt 3 ]; then \\\
            echo "-j$RPM_BUILD_NCPUS"; \\\
        else \\\
            echo "-j3"; \\\
        fi )
    
    %__arch_install_post \
        [ "%{buildarch}" = "noarch" ] || QA_CHECK_RPATHS=1 ; \
        case "${QA_CHECK_RPATHS:-}" in [1yY]*) /usr/lib/rpm/check-rpaths ;; esac \
        /usr/lib/rpm/check-buildroot
    

2.打包过程

2.1.打包的过程有点像是流水线,分好几个工序:

  • 首先,需要把源代码放到%_sourcedir中;
  • 然后,进行编译,编译的过程是在%_builddir中完成的,所以需要先把源代码复制到这个目录下边,一般情况下,源代码是压缩包格式,那么就解压过来即可;
  • 第三步,进行“安装”,这里有点类似于预先组装软件包,把软件包应该包含的内容(比如二进制文件、配置文件、man文档等)复制到%_buildrootdir中,并按照实际安装后的目录结构组装,比如二进制命令可能会放在/usr/bin下,那么就在%_buildrootdir下也按照同样的目录结构放置;
  • 然后,需要配置一些必要的工作,比如在实际安装前的准备啦,安装后的清理啦,以及在卸载前后要做的工作啦等等,这样也都是通过配置在SPEC文件中来告诉rpmbuild命令;
  • 还有一步可选操作,那就是检查软件是否正常运行;
  • 最后,生成的RPM包放置到%_rpmdir,源码包放置到%_srpmdir下。

2.2.以上这些步骤都是配置在SPEC文件中的,具体来说各个阶段

阶段 读取的目录 写入的目录 具体动作
%prep %_sourcedir %_builddir 读取位于 %_sourcedir 目录的源代码和 patch 。之后,解压源代码至 %_builddir 的子目录并应用所有 patch。
%build %_builddir %_builddir 编译位于 %_builddir 构建目录下的文件。通过执行类似 ./configure && make 的命令实现。
%install %_builddir %_buildrootdir 读取位于 %_builddir 构建目录下的文件并将其安装至 %_buildrootdir 目录。这些文件就是用户安装 RPM 后,最终得到的文件。注意一个奇怪的地方: 最终安装目录 不是 构建目录。通过执行类似 make install 的命令实现。
%check %_builddir %_builddir 检查软件是否正常运行。通过执行类似 make test 的命令实现。很多软件包都不需要此步。
bin %_buildrootdir %_rpmdir 读取位于 %_buildrootdir 最终安装目录下的文件,以便最终在 %_rpmdir 目录下创建 RPM 包。在该目录下,不同架构的 RPM 包会分别保存至不同子目录, noarch 目录保存适用于所有架构的 RPM 包。这些 RPM 文件就是用户最终安装的 RPM 包。
src %_sourcedir %_srcrpmdir 创建源码 RPM 包(简称 SRPM,以.src.rpm 作为后缀名),并保存至 %_srcrpmdir 目录。SRPM 包通常用于审核和升级软件包。

3.SPEC文件

SPEC 文件,rpm构建包的核心文件,命名格式一般是“软件名-版本.spec”的形式,需将其拷贝到 SPECS 目录下

3.1.RPM宏

  • rpm宏是直接文本替换

  • 查看宏对应的实际内容

    1
    2
    
    [root@node2 ~/rpmbuild/SPECS]# rpm --eval %{?dist}
    .el7
    

3.2.生成 SPEC 文件

  • 使用vim命令生成模板文件

    1
    
    vim myapp.spec
    
  • 使用rpmdevtools 工具生成模板文件

    1
    
    rpmdev-newspec -o myapp.spec
    

3.3.SPEC 配置项说明

  • spec文件序言部分

    SPEC指令 定义
    Name 软件名 ,包基本名称,应于spec文件名匹配
    Version 版本号
    Release 发布编号 ,打包次数,递增。初始值为 1%{?dist}
    Summary 简介, 简要说明,英文的话第一个字母应大写,以避免 rpmlint 工具警告
    License 软件许可协议版本
    Group 目前该标记已丢弃,vim的模板还有这一条,删掉即可*
    URL 通常为软件官网
    Source0 源代码压缩归档的路径或URL,多个时使用 SourceX
    Patch0 补丁,多个时 PatchX
    BuildArch 系统架构,如x86_64 noarch
    BuildRequires 以逗号或空格分隔的依赖包列表,用于构建过程,可以有多个条目
    Requires 以逗号或空格分隔的运行时依赖列表,用于安装过程,可以有多个条目
    ExcludeArch 需要排除的系统架构
  • spec文件Body部分

    SPEC指令 定义
    %description 软件的完整描述,可跨多行,以空行结束
    %prep 准备阶段,常为解压源码归档,可包含shell脚本
    %build 构建阶段
    %install 安装阶段,将文件从%builddir复制到%buildroot BUILD -> BUILDROOT 并在其中创建必要目录
    %check 测试阶段,通常运行单元测试
    %files 指定最终需要安装的文件列表
    %changelog 更新记录
  • tips:

    • %changelog 标签应包含每个 Release 所做的更改日志,尤其应包含上游的安全/漏洞补丁的说明。Changelog 日志可使用 rpm –changelog -q 查询,通过查询可得知已安装的软件是否包含指定漏洞和安全补丁。%changelog 条目应包含版本字符串,以避免 rpmlint 工具警告

    • %changelog 格式

      1
      2
      
      * Day-of-Week Month Day Year Name Surname <email> - Version-Release
      - XXX
      
    • 多行的部分,如 %changelog 或 %description 由指令下一行开始,空行结束

    • spec中不能有空项目和无用项目 (如 BuildRequires 和 Requires) ,可以删除无内容的项目,也可使用 ‘#’ 注释

  • spec文件示例

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    Name:		
    Version:	
    Release:	1%{?dist}
    Summary:	
    
    Group:		
    License:	
    URL:		
    Source0:	
    
    
    %description
    
    
    %prep
    %setup -q
    
    
    %build
    %configure
    make %{?_smp_mflags}
    
    
    %install
    make install DESTDIR=%{buildroot}
    
    
    %files
    %doc
    
    
    %changelog
    

4.SPEC各阶段详解

4.1 %prep阶段

%prep 部分描述了解压源码包的方法。一般而言,其中包含 %autosetup 命令。另外,还可以使用 %setup 和 %patch 命令来指定操作 Source0、Patch0 等标签的文件。

4.1.1 %autosetup 命令

%autosetup 命令用于解压源码包。可用选项包括:

  • -n name : 如果源码包解压后的目录名称与 RPM 名称不同,此选项用于指定正确的目录名称。例如,如果 tarball 解压目录为 FOO,则使用 “%autosetup -n FOO”。
  • -c name : 如果源码包解压后包含多个目录,而不是单个目录时,此选项可以创建名为 name 的目录,并在其中解压。
4.1.2 %setup 命令

如果使用 %setup 命令,通常使用 -q 抑止不必要的输出。 如果需要解压多个文件,有更多 %spec 选项可用,这对于创建子包很有用。常用选项如下:

  • -a number:在切换目录后,只解压指定序号的 Source 文件(例如 “-a 0” 表示 Source0)。
  • -b number :在切换目录前, 只解压指定序号的 Source 文件(例如 “-b 0” 表示 Source0)。
  • -D:解压前,不删除目录。
  • -T:禁止自动解压归档。
4.1.3 %patch 命令

如果使用 %autosetup 命令,则不需要手动进行补丁管理。如果你的需求很复杂,或需要与 EPEL 兼容,需要用到此部分的内容。%patch0 命令用于应用 Patch0(%patch1 应用 Patch1,以此类推)。Patches 是修改源码的最佳方式。常用的 -pNUMBER 选项,向 patch 程序传递参数,表示跳过 NUM 个路径前缀。

补丁文件名通常像这样 telnet-0.17-env.patch,命名格式为 %{name} - %{version} - REASON.patch(有时省略 version 版本)。补丁文件通常是 diff -u 命令的输出;如果你在 ~/rpmbuild/BUILD 子目录执行此命令,则之后便不需要指定 -p 选项。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
为一个文件制作补丁的步骤:

cp foo/bar foo/bar.orig
vim foo/bar
diff -u foo/bar.orig foo/bar > ~/rpmbuild/SOURCES/PKGNAME.REASON.patch

如果需要修改多个文件,简单方法是复制 BUILD 下的整个子目录,然后在子目录执行 diff。切换至 ~rpmbuild/BUILD/NAME 目录后,执行以下命令:

cp -pr ./ ../PACKAGENAME.orig/
... 执行修改 ...
diff -ur ../PACKAGENAME.orig . > ~/rpmbuild/SOURCES/NAME.REASON.patch

如果你想在一个补丁中编辑多个文件,你可以在编辑之前,使用 .orig 扩展名复制原始文件。然后,使用 gendiff(在 rpm-build 包中)创建补丁文件。

4.2 %build阶段

%build阶段顾名思义就是对解压到%_builddir下的源码进行编译的阶段,整个过程在该目录下完成。 许多程序使用 GNU configure 进行配置。默认情况下,文件会安装到前缀为 “/usr/local” 的路径下,对于手动安装很合理。然而,打包时需要修改前缀为 “/usr”。共享库路径视架构而定,安装至 /usr/lib 或 /usr/lib64 目录。 由于 GNU configure 很常见,可使用 %configure 宏来自动设置正确选项(例如,设置前缀为 /usr)。一般用法如下:

1
2
 %configure
 make %{?_smp_mflags}

若需要覆盖 makefile 变量,请将变量作为参数传递给 make:

1
make %{?_smp_mflags} CFLAGS="%{optflags}" BINDIR=%{_bindir}

你会发现SPEC中会用到很多预定义好的宏,用来通过一个简单的宏来完成一个或一系列常见的操作,比如:%prep阶段用于解压的%setup%autosetup%build阶段的%configure等。

4.3 %install阶段

此阶段包含安装阶段需要执行的命令,即从 %{_builddir} 复制相关文件到 %{buildroot} 目录(通常表示从 ~/rpmbuild/BUILD 复制到 ~/rpmbuild/BUILDROOT/XXX) 目录,并根据需要在 %{buildroot} 中创建必要目录。

容易混淆的术语:

  • “build 目录”,也称为 %{_builddir},实际上与 “build root”,又称为 %{buildroot},是不同的目录。在前者中进行编译,并将需要打包的文件从前者复制到后者, %{buildroot}通常为 ~/rpmbuild/BUILD/%{name}-%{version}-%{release}.%{arch}。
  • 在 %build 阶段,当前目录为 %{buildsubdir},是 %prep 阶段中在 %{_builddir} 下创建的子目录。这些目录通常名为 ~/rpmbuild/BUILD/%{name}-%{version}。
  • %install 阶段的命令不会在用户安装 RPM 包时执行,此阶段仅在打包时执行。

一般,这里执行 “make install” 之类的命令:

1
2
3
%install
rm -rf %{buildroot} # 仅用于 RHEL 5
%makeinstall
  • 理想情况下,对于支持的程序,你应该使用 %makeinstall(这又是一个宏),它等同于 DESTDIR=%{buildroot},它会将文件安装到 %{buildroot} 目录中。

    使用 “%makeinstall” 宏。此方法可能有效,但也可能失败。该宏会展开为 make prefix=%{buildroot}%{_prefix} bindir=%{buildroot}%{_bindir} … install,可能导致某些程序无法正常工作。请在 %{buildroot} 根据需要创建必要目录。

  • 使用 auto-destdir 软件包的话,需要 BuildRequires: auto-destdir,并将 make install 修改为 make-redir DESTDIR=%{buildroot} install。这仅适用于使用常用命令安装文件的情况,例如 cp 和 install。

  • 手动执行安装。这需要在 %{buildroot} 下创建必要目录,并从 %{_builddir} 复制文件至 %{buildroot} 目录。要特别注意更新,通常会包含新文件。示例如下:

    1
    2
    3
    4
    
    %install
    rm -rf %{buildroot}
    mkdir -p %{buildroot}%{_bindir}/
    cp -p mycommand %{buildroot}%{_bindir}/
    

4.4 %check 阶段

如果需要执行测试,使用 %check 是个好主意。测试代码应写入 %check 部分(紧接在 %install 之后,因为需要测试 %{buildroot} 中的文件),而不是写入 %{build} 部分,这样才能在必要时忽略测试。通常,此部分包含:

1
2
3
4
5
6
7
#通常使用
make test

#有时候也使用
make check

#请熟悉 Makefile 的用法,并选择适当的方式。

4.5 %files 部分

此部分列出了需要被打包的文件和目录。

4.5.1 %files 基础

%defattr 用于设置默认文件权限,通常可以在 %files 的开头看到它。注意,如果不需要修改权限,则不需要使用它。其格式为:

1
%defattr(<文件权限>, <用户>, <用户组>, <目录权限>)

第 4 个参数通常会省略。常规用法为 %defattr(-,root,root,-),其中 “-” 表示默认权限。 您应该列出该软件包拥有的所有文件和目录。尽量使用宏代替目录名,具体的宏列表如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%{_sysconfdir}        /etc
%{_prefix}            /usr
%{_exec_prefix}       %{_prefix}
%{_bindir}            %{_exec_prefix}/bin
%{_libdir}            %{_exec_prefix}/%{_lib}
%{_libexecdir}        %{_exec_prefix}/libexec
%{_sbindir}           %{_exec_prefix}/sbin
%{_sharedstatedir}    /var/lib
%{_datarootdir}       %{_prefix}/share
%{_datadir}           %{_datarootdir}
%{_includedir}        %{_prefix}/include
%{_infodir}           /usr/share/info
%{_mandir}            /usr/share/man
%{_localstatedir}     /var
%{_initddir}          %{_sysconfdir}/rc.d/init.d
%{_var}               /var
%{_tmppath}           %{_var}/tmp
%{_usr}               /usr
%{_usrsrc}            %{_usr}/src
%{_lib}               lib (lib64 on 64bit multilib systems)
%{_docdir}            %{_datadir}/doc
%{buildroot}          %{_buildrootdir}/%{name}-%{version}-%{release}.%{_arch}
$RPM_BUILD_ROOT       %{buildroot}

如果路径以 “/” 开头(或从宏扩展),则从 %{buildroot} 目录取用。否则,假设文件在当前目录中(例如:在 %{_builddir} 中,包含需要的文档)。如果您的包仅安装一个文件,如 /usr/sbin/mycommand,则 %files 部分如下所示:

1
2
%files
%{_sbindir}/mycommand

若要使软件包不受上游改动的影响,可使用通配符匹配所有文件:

1
%{_bindir}/*

包含一个目录:

1
%{_datadir}/%{name}/

注意,%{_bindir}/* 不会声明此软件包拥有 /usr/bin 目录,而只包含其中的文件。如果您列出一个目录,则该软件包拥有这个目录,及该目录内的所有文件和子目录。因此,不要列出 %{_bindir},并且要小心的处理那些可能和其他软件包共享的目录。

如果存在以下情况,可能引发错误:

通配符未匹配到任何文件或目录 文件或目录被多次列出 未列出 %{buildroot} 下的某个文件或目录 您也可以使用 %exclude 来排除文件。这对于使用通配符来列出全部文件时会很有用,注意如果未匹配到任何文件也会造成失败。

4.5.2 %files 前缀

上边的“hello”的示例中,%files部分还有用到%doc等宏,可能您看得一知半解,这里详细介绍一下。 如果需要在 %files 部分添加一个或多个前缀,用空格分隔。

%doc 用于列出 %{_builddir} 内,但不复制到 %{buildroot} 中的文档。通常包括 README 和 INSTALL等。它们会保存至 /usr/share/doc 下适当的目录中,不需要声明 /usr/share/doc 的所有权。

注意: 如果指定 %doc 条目,rpmbuild < 4.9.1 在安装前会将 %doc 目录删除。这表明已保存至其中的文档,例如,在 %install 中安装的文档会被删除,因此最终不会出现在软件包中。如果您想要在 %install 中安装一些文档,请将它们临时安装到 build 目录(不是 build root 目录)中,例如 _docs_staging,接着在 %files 中列出,如 %doc _docs_staging/* 这样。

配置文件保存在 /etc 中,一般会这样指定(确保用户的修改不会在更新时被覆盖):

1
%config(noreplace) %{_sysconfdir}/foo.conf

如果更新的配置文件无法与之前的配置兼容,则应这样指定:

1
%config %{_sysconfdir}/foo.conf

“%attr(mode, user, group)” 用于对文件进行更精细的权限控制,”-” 表示使用默认值:

1
%attr(0644, root, root) FOO.BAR

“%caps(capabilities)” 用于为文件分配 POSIX capabilities。例如:

1
%caps(cap_net_admin=pe) FOO.BAR

如果包含特定语言编写的文件,请使用 %lang 来标注:

1
%lang(de) %{_datadir}/locale/de/LC_MESSAGES/tcsh*

使用区域语言(Locale)文件的程序应遵循 处理 i18n 文件的建议方法:

  • 在 %install 步骤中找出文件名: %find_lang ${name}
  • 添加必要的编译依赖: BuildRequires: gettext
  • 使用找到的文件名: %files -f ${name}.lang

4.6 Scriptlets

当用户安装或卸载 RPM 时,您可能想要执行一些命令。这可以通过 scriptlets 完成。

脚本片段可以:

  • 在软体包安装之前 (%pre) 或之后 (%post) 执行
  • 在软体包卸载之前 (%preun) 或之后 (%postun) 执行
  • 在事务开始 (%pretrans) 或结束 (%posttrans) 时执行

例如,每个二进制 RPM 包都会在动态链接器的默认路径中存储共享库文件,并在 %post 和 %postun 中调用 ldconfig 来更新库缓存。如果软件包有多个包含共享库的子包,则每个软体包也需要执行相同动作。

%post -p /sbin/ldconfig %postun -p /sbin/ldconfig

如果仅执行一个命令,则 “-p” 选项会直接执行,而不启用 shell。然而,若有许多命令时,不要使用此选项,按正常编写 shell 脚本即可。

如果你在脚本片段中执行任何程序,就必须以 Requires(CONTEXT)(例: Requires(post))的形式列出所有依赖。

%pre、%post、%preun 和 %postun 提供 $1 参数,表示动作完成后,系统中保留的此名称的软件包数量。因此可用于检查软件安装情况,不过不要比较此参数值是否等于 2,而是比较是否大于等于 2。对于%pretrans 和 %posttrans,$1 的值恒为 0。

例如,如果软件包安装了一份 info 手册,那么可以用 info 包提供的 install-info 来更新 info 手册索引。首先,我们不保证系统已安装 info 软件包,除非明确声明需要它;其次,我们不想在 install-info 执行失败时,使软件包安装失败:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Requires(post): info
Requires(preun): info

...

%post
/sbin/install-info %{_infodir}/%{name}.info %{_infodir}/dir || :

%preun
if [ $1 = 0 ] ; then
/sbin/install-info --delete %{_infodir}/%{name}.info %{_infodir}/dir || :
fi

上边的示例中还有一个安装 info 手册时的小问题需要解释一下。install-info 命令会更新 info 目录,所以我们应该在 %install 阶段删除 %{buildroot} 中无用的空目录:

1
rm -f %{buildroot}%{_infodir}/dir

三、构建RPM包

RPM包文件名格式:Name-Version-Release[dist].BuildArch

1.rpmbuild打包

1.1.rpmbuild 命令选项

rpmbuild 命令的选项 rpmbuild 命令有不少选项,用得比较多的有:

1
2
3
4
5
6
7
-bp 只解压源码及应用补丁
-bc 只进行编译
-bi 只进行安装到%{buildroot}
-bb 只生成二进制 rpm 包
-bs 只生成源码 rpm 包
-ba 生成二进制 rpm 包和源码 rpm 包
--target 指定生成 rpm 包的平台,默认会生成 i686 和 x86_64 的 rpm 包,但一般我只需要 x86_64 的 rpm 包

1.2.只生成二进制格式的 rpm 包

1
rpmbuild -bb {路径}软件名-版本.spec 

1.3.完全打包

1
rpmbuild -ba {路径}软件名-版本.spec 

2.rpmlint检查

为避免常见错误,请先使用 rpmlint 查找 SPEC 文件的错误:

1
rpmlint program.spec

如果返回错误/警告,使用 “-i” 选项查看更详细的信息。

也可以使用 rpmlint 测试已构建的 RPM 包,检查 SPEC/RPM/SRPM 是否存在错误。 如果你位于 SPEC 目录中,请执行:

1
rpmlint NAME.spec ../RPMS/*/NAME*.rpm ../SRPMS/NAME*.rpm

进入 ~/rpmbuild/RPMS 下的特定架构目录中,您会发现有许多二进制 RPM 包。使用以下命令快速查看 RPM 包含的文件和权限

1
rpmls *.rpm

3.rpm安装卸载

1
2
3
4
5
6
#安装
rpm -ivp package1.rpm package2.rpm package3.rpm ...
#升级
rpm -Uvp package1.rpm package2.rpm package3.rpm ...
#卸载
rpm -e package1 package2 package3

四、一些名称解释

1.configure、make、make install 命令的区别

  • configure 它是个 shell 脚本,./configure 那么就是运行这个 shell 脚本啦!./configure 是用来检测你的安装平台的目标特征的。比如它会检测你是不是有 CCGCC, 并不是需要 CCGCC。这一步一般用来生成 Makefile,为下一步的编译做准备。 可以通过在 configure 后加上参数来对安装进行控制。例如,./configure --prefix=/usr。意思是将该软件安装在 /usr 下面,执行文件就会安装在 /usr/bin (如果不指定,默认的路径是 /usr/local/bin), 资源文件就会安装在 /usr/share(而不是默认的 /usr/local/share)。可以通过 ./configure --help 察看详细的说明帮助。之前就写过一篇文章 CentOS 源码编译安装 Python3
  • make 是用来编译的,它从 Makefile 中读取指令,然后编译。如果 在 make 过程中出现 error ,你就要记下错误代码(注意不仅仅是最后一行),然后你可以向开发者提交 bugreport(一般在 INSTALL 里有提交地址),或者你的系统少了一些依赖库等,这些需要自己仔细研究错误代码。
  • make install 就是把编译出来的二进制文件,库,配置文件等等放到相应目录下
  • make uninstal 是卸载,不加参数就是默认的进行源代码编译。
  • make clean 清除编译结果

make 是 Linux 开发套件里面自动化编译的一个控制程序,他通过借助 Makefile 里面编写的编译规范(语法很多,类似一个可以运行的脚本程序。反正我是看不懂,所以你也别问我怎么编写)。进行自动化的调用 gcc 、ld 以及运行某些需要的程序进行编译的程序。

一般情况下,他所使用的 Makefile 控制代码,由 configure 这个设置脚本根据给定的参数和系统环境生成。

2.Makefile是什么?

makefile 是用于自动编译和链接的,一个工程有很多文件组成,每一个文件的改变都会导致工程的重新链接–但是不是所有的文件都需要重新编译,makefile 能够纪录文件的信息,决定在链接的时候需要重新编译哪些文件。

3.cc 和 gcc 又是什么?

从名字上看,老的 Unix 系统的 CC 程序叫做 C Compiler 。但 GCC 这个名字按 GNU 的说法叫做 Gnu Compiler Collection ,注意这是一个编译器集合,不仅仅是 c 或 c++。gcc 包含很多编译器(C, C++, Objective-C, Ada, Fortran,and Java) 。所以它们是不一样的,一个是一个古老的 C 编译器,一个是编译器的 Gnu 的编译器的集合(Gcc里的 C 编译器比 CC 强大太多了,所以你没必要用 CC)。

cc 是 gcc 的连接。cc 来自于昂贵的 Unix 系统,cc 是商业软件,gcc 是编译器。

GCC 可以用来编译 C/C++、FORTRAN、JAVA、OBJC、ADA等语言的程序,可根据需要选择安装支持的语言。

五、简单案例

1.创建工作空间

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[root@node2 ~]# rpmdev-setuptree
[root@node2 ~]# tree rpmbuild
rpmbuild
├── BUILD
├── RPMS
├── SOURCES
├── SPECS
└── SRPMS

5 directories, 0 files

#或者用mkdir命令也可以,二选一
mkdir -p rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

2.准备SPEC

1
2
cd ~/rpmbuild
vim SPECS/hello-world.spec
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Name: hello-world
Version: 1.0.0
Release: 1.el7
Summary: A test demo
License: FIXME
 
%description
This is a test demo
 
%prep
# nothing
 
%build
cat > hello-world.sh <<EOF
#!/bin/bash
echo Hello World
EOF
 
%install
mkdir -p %{buildroot}/usr/bin/
install -m 755 hello-world.sh %{buildroot}/usr/bin/hello-world.sh
 
%files
/usr/bin/hello-world.sh
 
%changelog
# nothing

3.RPM打包

1
rpmbuild -ba SPECS/hello-world.spec

4.安装RPM包

1
rpm -ivh /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.el7.x86_64.rpm

5.验证安装

1
2
3
4
5
[root@node2 ~/rpmbuild]# rpm -q hello-world
hello-world-1.0.0-1.el7.x86_64
[root@node2 ~/rpmbuild]# hello-world.sh 
Hello World
[root@node2 ~/rpmbuild]#

6.卸载rpm包

1
2
3
4
5
6
[root@node2 ~/rpmbuild]# rpm -e hello-world
[root@node2 ~/rpmbuild]# rpm -q hello-world
未安装软件包 hello-world 
[root@node2 ~/rpmbuild]# hello-world.sh 
-bash: /usr/bin/hello-world.sh: 没有那个文件或目录
[root@node2 ~/rpmbuild]# 

六、复杂案例

1.hello-world应用源码包

本步骤用于生成创建RPM包所需的源码包,本示例使用golang代码,其他语言类似

本用例源码包来自于 编译(Make和Makefile)教程 ,项目目录结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
├── build_sources                   二进制源码包制作文件夹
│   ├── bin                         构建后的二进制文件
│   ├── build                       存放构建脚本
│   ├── build                       存放构建脚本
│   │   ├── configure               用来打包二进制源码包
│   │   ├── Makefile                用来生成项目二进制文件
│   │   └── README.md               build文件夹说明
│   ├── go.mod                      使用Go Module包管理的依赖描述文件
│   ├── go.sum                      包管理依赖内容校验文件
│   ├── helle-world.gz              制作好的二进制源码安装包
│   ├── helle-world.go              应用主程序
│   ├── helle-world_test.go         应用程序测试文件
│   ├── Makefile                    二进制源码安装包中的Makefile,用户应用安装
│   └── README.md                   构建说明及制作流程

打包好的二进制安装包为hello-world-1.0.0.tar.gz,解压安装包可以看到安装包目录结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@dy-workspace /workspace/gocode-build/build_sources]# ll -h
总用量 4.7M
drwxr-xr-x. 2 samba samba 4.0K 4月  26 16:44 bin
drwxr-xr-x. 2 samba samba 4.0K 4月  27 09:15 build
drwxr-xr-x. 2 samba samba 4.0K 4月  26 16:17 conf
-rwxr--r--. 1 samba samba 1.1K 4月  26 15:39 go.mod
-rwxr--r--. 1 samba samba 6.1K 4月  26 15:33 go.sum
-rw-r--r--. 1 root  root  4.6M 4月  27 09:42 hello-world-1.0.0.tar.gz
-rwxr--r--. 1 samba samba 1.9K 4月  26 16:21 hello_world.go
-rwxr--r--. 1 samba samba  227 4月  26 15:35 hello_world_test.go
-rwxr--r--. 1 samba samba 1.5K 4月  27 09:42 Makefile
-rwxr--r--. 1 samba samba 4.3K 4月  26 18:02 README.md
[root@dy-workspace /workspace/gocode-build/build_sources]# tar -zxvf hello-world-1.0.0.tar.gz
hello-world-1.0.0/
hello-world-1.0.0/bin/
hello-world-1.0.0/bin/tips.md
hello-world-1.0.0/bin/hello-world
hello-world-1.0.0/Makefile
hello-world-1.0.0/conf/
hello-world-1.0.0/conf/hello-world.logrotate
hello-world-1.0.0/conf/hello-world.service
hello-world-1.0.0/conf/global.conf
[root@dy-workspace /workspace/gocode-build/build_sources]# cd hello-world-1.0.0

安装包文件夹结构如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[root@dy-workspace /workspace/gocode-build/build_sources/hello-world-1.0.0]# tree
.
├── bin
│   ├── hello-world
│   └── tips.md
├── conf
│   ├── global.conf
│   ├── hello-world.logrotate
│   └── hello-world.service
└── Makefile

2 directories, 6 files

3.hello-world应用SPEC

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
[root@dy-workspace /workspace/gocode-build/build_rpm]# ll -h
总用量 16K
-rwxr--r--. 1 samba samba 1.5K 4月  27 09:51 hello-world-1.0.0.spec
-rwxr--r--. 1 samba samba 3.7K 4月  27 09:52 README.md
[root@dy-workspace /workspace/gocode-build/build_rpm]# cat hello-world-1.0.0.spec
Name:           hello-world
Version:        1.0.0
Release:        1.el7
Summary:        hello-world

Group:          System Environment/Daemons
License:        Commercial
URL:            https://blog.nevergiveup.tech/
Source0:        %{name}-%{version}.tar.gz

%description
hello-world

%prep
%setup -q

%build

%install
make install DESTDIR=%{buildroot}

%post
systemctl daemon-reload
if [ $1 -eq 1 ]
then
        systemctl enable hello-world.service >/dev/null 2>&1 || :
        systemctl start hello-world.service >/dev/null 2>&1 || :
else
        systemctl restart hello-world.service >/dev/null 2>&1 || :
fi
#basic server

%preun
if [ $1 -eq 0 ]
then
        #basic server
        systemctl disable hello-world.service >/dev/null 2>&1 || :
        systemctl stop hello-world.service >/dev/null 2>&1 || :
fi

%postun
if [ $1 -eq 0 ]
then
        [ -f /var/log/hello-world/go-gin-startup.log ] && rm -rf /var/log/hello-world
        [ -f /opt/hello-world/hello-world ] && rm -f /opt/hello-world/hello-world
fi
systemctl daemon-reload

%files
%attr(755,root,root) /opt/hello-world/hello-world
%attr(644,root,root) %config /usr/lib/systemd/system/hello-world.service
%attr(644,root,root) %config /etc/logrotate.d/hello-world.logrotate

%attr(755,root,root) %dir /var/log/hello-world
%attr(755,root,root) %dir /opt/hello-world
%attr(755,root,root) %dir /etc/hello-world
%attr(644,root,root) %config /etc/hello-world/global.conf

%define __debug_install_post \
%{_rpmconfigdir}/find-debuginfo.sh %{?_find_debuginfo_opts} "%{_builddir}/%{?buildsubdir}" \
%{nil}

3.创建工作空间

1
2
rm -rf ~/rpmbuild
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}

4.拷贝源码和spec文件

1
2
3
4
5
6
7
8
#拷贝源码包
#注意包位置: /workspace/gocode-build/build_sources/ 这里需要修改成自己的
cp -rf /workspace/gocode-build/build_sources/hello-world-1.0.0.tar.gz /root/rpmbuild/SOURCES/

#拷贝spec文件
#注意spec文件位置: /workspace/gocode-build/build_rpm/ 这里需要修改成自己的
dos2unix /workspace/gocode-build/build_rpm/hello-world-1.0.0.spec
cp -rf /workspace/gocode-build/build_rpm/hello-world-1.0.0.spec /root/rpmbuild/SPECS/

5.构建hello-world项目rpm包

1
2
3
cd /root/rpmbuild/SPECS/
rpmbuild -ba /root/rpmbuild/SPECS/hello-world-1.0.0.spec
#获取到源码包位置:/root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.el7.x86_64.rpm

6.hello-world应用rpm包安装&校验&卸载

1).安装rpm包

1
2
3
4
5
[root@dy-workspace ~/rpmbuild/SPECS]# rpm -ivh /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.el7.x86_64.rpm
准备中...                          ################################# [100%]
正在升级/安装...
   1:hello-world-1.0.0-1.el7          ################################# [100%]
[root@dy-workspace ~/rpmbuild/SPECS]#

2).检查是否安装成功

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[root@dy-workspace ~/rpmbuild/SPECS]# rpm -qa hello-world
hello-world-1.0.0-1.el7.x86_64
[root@dy-workspace ~/rpmbuild/SPECS]# rpm -ql hello-world
/etc/hello-world
/etc/hello-world/global.conf
/etc/logrotate.d/hello-world.logrotate
/opt/hello-world
/opt/hello-world/hello-world
/usr/lib/systemd/system/hello-world.service
/var/log/hello-world

[root@dy-workspace ~/rpmbuild/SPECS]# systemctl status hello-world
● hello-world.service - Hello-World service
   Loaded: loaded (/usr/lib/systemd/system/hello-world.service; enabled; vendor preset: disabled)
   Active: active (running) since 三 2022-04-27 09:49:30 CST; 18s ago
 Main PID: 18050 (hello-world)
   CGroup: /system.slice/hello-world.service
           └─18050 /opt/hello-world/hello-world

4月 27 09:49:30 dy-workspace hello-world[18050]: App Version:
4月 27 09:49:30 dy-workspace hello-world[18050]: [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware ...ttached.
4月 27 09:49:30 dy-workspace hello-world[18050]: [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
4月 27 09:49:30 dy-workspace hello-world[18050]: - using env:        export GIN_MODE=release
4月 27 09:49:30 dy-workspace hello-world[18050]: - using code:        gin.SetMode(gin.ReleaseMode)
4月 27 09:49:30 dy-workspace hello-world[18050]: [GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
4月 27 09:49:30 dy-workspace hello-world[18050]: [GIN-debug] GET    /favicon.ico              --> main.main.func2 (3 handlers)
4月 27 09:49:30 dy-workspace hello-world[18050]: [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
4月 27 09:49:30 dy-workspace hello-world[18050]: Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
4月 27 09:49:30 dy-workspace hello-world[18050]: [GIN-debug] Listening and serving HTTP on :9782
Hint: Some lines were ellipsized, use -l to show in full.
[root@dy-workspace ~/rpmbuild/SPECS]#

3).卸载rpm包

1
2
3
4
5
6
7
[root@dy-workspace ~/rpmbuild/SPECS]# rpm -e hello-world-1.0.0
[root@dy-workspace ~/rpmbuild/SPECS]# rpm -qa hello-world-1.0.0
[root@dy-workspace ~/rpmbuild/SPECS]# rpm -ql hello-world-1.0.0
未安装软件包 hello-world-1.0.0
[root@dy-workspace ~/rpmbuild/SPECS]# systemctl status hello-world
Unit hello-world.service could not be found.
[root@dy-workspace ~/rpmbuild/SPECS]#

7.可能出现的报错处理

1).*** WARNING: No build ID note found in ***

原因:debug信息,忽略即可

解决:在spec文件任意位置添加如下代码

1
2
3
%define __debug_install_post \
%{_rpmconfigdir}/find-debuginfo.sh %{?_find_debuginfo_opts} "%{_builddir}/%{?buildsubdir}" \
%{nil}

2).错误:line 59: unclosed macro or bad line continuation

原因:在windows下编辑造成的空格或tab不识别

解决:使用vim或者vi在服务器上对spec文件进行编辑

七、备查

1.rpmbuild目录

默认位置 宏代码 名称 用途
~/rpmbuild/SPECS %_specdir Spec 文件目录 保存 RPM 包配置(.spec)文件
~/rpmbuild/SOURCES %_sourcedir 源代码目录 保存源码包(如 .tar 包)和所有 patch 补丁
~/rpmbuild/BUILD %_builddir 构建目录 源码包被解压至此,并在该目录的子目录完成编译
~/rpmbuild/BUILDROOT %_buildrootdir 最终安装目录 保存 %install 阶段安装的文件
~/rpmbuild/RPMS %_rpmdir 标准 RPM 包目录 生成/保存二进制 RPM 包
~/rpmbuild/SRPMS %_srcrpmdir 源代码 RPM 包目录 生成/保存源码 RPM 包(SRPM)

2.spec文件阶段

阶段d 读取的目录d 写入的目录d 具体动作
%prepd %_sourcedird %_builddird 读取位于 %_sourcedir 目录的源代码和 patch 。之后,解压源代码至 %_builddir 的子目录并应用所有 patch。
%buildd %_builddird %_builddird 编译位于 %_builddir 构建目录下的文件。通过执行类似 ./configure && make 的命令实现。
%installd %_builddird %_buildrootdird 读取位于 %_builddir 构建目录下的文件并将其安装至 %_buildrootdir 目录。这些文件就是用户安装 RPM 后,最终得到的文件。注意一个奇怪的地方: 最终安装目录 不是 构建目录。通过执行类似 make install 的命令实现。
%checkd %_builddird %_builddird 检查软件是否正常运行。通过执行类似 make test 的命令实现。很多软件包都不需要此步。
bind %_buildrootdird %_rpmdird 读取位于 %_buildrootdir 最终安装目录下的文件,以便最终在 %_rpmdir 目录下创建 RPM 包。在该目录下,不同架构的 RPM 包会分别保存至不同子目录, noarch 目录保存适用于所有架构的 RPM 包。这些 RPM 文件就是用户最终安装的 RPM 包。
srcd %_sourcedird %_srcrpmdird 创建源码 RPM 包(简称 SRPM,以.src.rpm 作为后缀名),并保存至 %_srcrpmdir 目录。SRPM 包通常用于审核和升级软件包。

3.代表路径的宏列表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
%{_sysconfdir}        /etc
%{_prefix}            /usr
%{_exec_prefix}       %{_prefix}
%{_bindir}            %{_exec_prefix}/bin
%{_libdir}            %{_exec_prefix}/%{_lib}
%{_libexecdir}        %{_exec_prefix}/libexec
%{_sbindir}           %{_exec_prefix}/sbin
%{_sharedstatedir}    /var/lib
%{_datarootdir}       %{_prefix}/share
%{_datadir}           %{_datarootdir}
%{_includedir}        %{_prefix}/include
%{_infodir}           /usr/share/info
%{_mandir}            /usr/share/man
%{_localstatedir}     /var
%{_initddir}          %{_sysconfdir}/rc.d/init.d
%{_var}               /var
%{_tmppath}           %{_var}/tmp
%{_usr}               /usr
%{_usrsrc}            %{_usr}/src
%{_lib}               lib (lib64 on 64bit multilib systems)
%{_docdir}            %{_datadir}/doc
%{buildroot}          %{_buildrootdir}/%{name}-%{version}-%{release}.%{_arch}
$RPM_BUILD_ROOT       %{buildroot}	

4.rpm常用命令

  • 手动安装 rpm 包 rpm-ivh xxxxx.rpm
  • 查看 rpm 包信息 rpm-qpi xxxxx.rpm
  • 查看 rpm 包依赖 rpm -qpR xxxxx.rpm
  • 查看 rpm 包中包含那些文件 rpm -qlp xxxxx.rpm 可以加grep搜索 rpm -qlp xxxxx.rpm|grep spec
  • 使用工具rpm2cpio提取文件: rpm2cpio xxxxx.rpm |cpio -ivd xxx.jpg
  • 用rpm2cpio将rpm文件转换成cpio文件 rpm2cpio xxxxxx.rpm >xxxxx.cpio
  • 用cpio解压cpio文件 cpio -i --make-directories
  • 提取所有文件: rpm2cpio xxx.rpm | cpio -vi rpm2cpio xxx.rpm | cpio -idmv rpm2cpio xxx.rpm | cpio --extract --make-directories
  • cpio 参数说明: iextract 表示提取文件 v 表示指示执行进程 dmake-directory 表示根据包中文件原来的路径建立目录 m 表示保持文件的更新时间
  • 查看rpm包里的pre和post install脚本: rpm -qp --scripts xxxxx.rpm
  • 查看安装的过程中,代码的执行过程: rpm -ih -vv xxxxx.rpm

八、参考文献