如何构建 rpm 包

节省在多台主机上安装文件和脚本的时间和精力。
275 位读者喜欢这篇文章。
Gift box opens with colors coming out

Opensource.com

自从 20 多年前开始使用 Linux 以来,我就一直使用基于 rpm 的软件包管理器在 Red Hat 和 Fedora Linux 上安装软件。我使用过 rpm 程序本身、yumDNF(yum 的近亲)在我的 Linux 主机上安装和更新软件包。yum 和 DNF 工具是 rpm 实用程序的封装器,提供了额外的功能,例如查找和安装软件包依赖项的能力。

多年来,我创建了许多 Bash 脚本,其中一些脚本有单独的配置文件,我喜欢将它们安装在我的大多数新计算机和虚拟机上。安装所有这些软件包非常耗时,所以我决定通过创建一个 rpm 包来自动化这个过程,我可以将该 rpm 包复制到目标主机并在其正确的位置安装所有这些文件。虽然 rpm 工具以前用于构建 rpm 包,但该功能已被移除,并创建了一个新工具 rpmbuild 来构建新的 rpm。

当我开始这个项目时,我发现关于创建 rpm 包的信息很少,但我设法找到了一本书《Maximum RPM》,它帮助我弄明白了。这本书现在已经有点过时了,就像我发现的大部分信息一样。它也已绝版,二手书的价格高达数百美元。《Maximum RPM》的在线版本是免费提供的,并且会定期更新。RPM 网站还链接到其他网站,这些网站有很多关于 rpm 的文档。其他信息往往很简短,并且显然假设你已经对这个过程有相当多的了解。

此外,我找到的每一份文档都假设代码需要像在开发环境中那样从源代码编译。我不是开发人员。我是一名系统管理员,我们系统管理员有不同的需求,因为我们不应该——或者我们不应该——编译代码来用于管理任务;我们应该使用 shell 脚本。因此,我们没有源代码,因为它不是需要编译成二进制可执行文件的东西。我们拥有的是源代码,它也是可执行文件。

在大多数情况下,这个项目应该以非 root 用户 student 的身份执行。Rpms 绝不应该由 root 构建,而只能由非特权用户构建。我将指出哪些部分应该以 root 身份执行,哪些部分应该由非 root、非特权用户执行。

准备工作

首先,打开一个终端会话并 su 到 root。务必使用 - 选项以确保启用完整的 root 环境。我不相信系统管理员应该使用 sudo 来执行任何管理任务。在我的个人博客文章中了解原因:真正的系统管理员不用 sudo

[student@testvm1 ~]$ su -
Password: 
[root@testvm1 ~]#

创建一个可以用于此项目的 student 用户,并为该用户设置密码。

[root@testvm1 ~]# useradd -c "Student User" student 
[root@testvm1 ~]# passwd student
Changing password for user student.
New password: <Enter the password>
Retype new password: <Enter the password>
passwd: all authentication tokens updated successfully.
[root@testvm1 ~]#

构建 rpm 包需要 rpm-build 软件包,该软件包可能尚未安装。现在以 root 身份安装它。请注意,此命令还将安装几个依赖项。数量可能会有所不同,具体取决于主机上已安装的软件包;它在我的测试 VM 上总共安装了 17 个软件包,这已经非常少了。

dnf install -y rpm-build

除非另有明确指示,否则此项目的其余部分应以用户 student 的身份执行。打开另一个终端会话并使用 su 切换到该用户以执行其余步骤。使用以下命令从 GitHub 下载我准备的开发目录结构 tarball,utils.tar

wget https://github.com/opensourceway/how-to-rpm/raw/master/utils.tar

这个 tarball 包含最终 rpm 将安装的所有文件和 Bash 脚本。还有一个完整的 spec 文件,您可以使用它来构建 rpm。我们将详细介绍 spec 文件的每个部分。

以用户 student 身份,使用您的主目录作为当前工作目录 (pwd),解压 tarball。

[student@testvm1 ~]$ cd ; tar -xvf utils.tar 

使用 tree 命令验证 ~/development 的目录结构和包含的文件是否类似于以下输出

[student@testvm1 ~]$ tree development/
development/
├── license
│   ├── Copyright.and.GPL.Notice.txt
│   └── GPL_LICENSE.txt
├── scripts
│   ├── create_motd
│   ├── die
│   ├── mymotd
│   └── sysdata
└── spec
    └── utils.spec

3 directories, 7 files
[student@testvm1 ~]$

mymotd 脚本创建一个“每日消息”数据流,该数据流被发送到 stdout。create_motd 脚本运行 mymotd 脚本并将输出重定向到 /etc/motd 文件。此文件用于向使用 SSH 远程登录的用户显示每日消息。

die 脚本是我自己的脚本,它将 kill 命令包装在一段代码中,该代码可以查找与指定字符串匹配的正在运行的程序并终止它们。它使用 kill -9 以确保它们无法忽略终止消息。

sysdata 脚本可以输出数万行关于您的计算机硬件、已安装的 Linux 版本、所有已安装的软件包以及硬盘驱动器元数据的数据。我使用它来记录主机在某个时间点的状态。我以后可以使用它作为参考。我过去常常这样做来维护我为客户安装的主机的记录。

您可能需要将这些文件和目录的所有权更改为 student.student。如有必要,使用以下命令执行此操作

chown -R student.student development

此树中的大多数文件和目录将由您在此项目期间创建的 rpm 安装在 Fedora 系统上。

创建构建目录结构

rpmbuild 命令需要非常特定的目录结构。您必须自己创建此目录结构,因为没有提供自动方法。在您的主目录中创建以下目录结构

~ ─ rpmbuild
    ├── RPMS
    │   └── noarch
    ├── SOURCES
    ├── SPECS
    └── SRPMS

我们将不创建 rpmbuild/RPMS/X86_64 目录,因为这将是特定于 64 位编译二进制文件的架构。我们有不特定于架构的 shell 脚本。实际上,我们也不会使用 SRPMS 目录,该目录将包含编译器的源文件。

检查 spec 文件

每个 spec 文件都有许多部分,其中一些部分可能会被忽略或省略,具体取决于 rpm 构建的具体情况。这个特定的 spec 文件不是一个可工作的最小文件的示例,但它是一个中等复杂程度的 spec 文件的良好示例,该文件打包了不需要编译的文件。如果需要编译,它将在 %build 部分执行,该部分在此 spec 文件中被省略,因为它不是必需的。

前导码

这是 spec 文件中唯一没有标签的部分。它由您在运行命令 rpm -qi [软件包名称] 时看到的大部分信息组成。每个数据都是单行,由一个标签(用于标识它)和文本数据(作为标签的值)组成。

###############################################################################
# Spec file for utils
################################################################################
# Configured to be built by user student or other non-root user
################################################################################
#
Summary: Utility scripts for testing RPM creation
Name: utils
Version: 1.0.0
Release: 1
License: GPL
URL: http://www.both.org
Group: System
Packager: David Both
Requires: bash
Requires: screen
Requires: mc
Requires: dmidecode
BuildRoot: ~/rpmbuild/

# Build with the following syntax:
# rpmbuild --target noarch -bb utils.spec

注释行会被 rpmbuild 程序忽略。我总是喜欢在这个部分添加一个注释,其中包含创建软件包所需的 rpmbuild 命令的确切语法。“Summary”标签是软件包的简短描述。“Name”、“Version”和“Release”标签用于创建 rpm 文件的名称,例如 utils-1.00-1.rpm。递增发布版本号和版本号使您可以创建可用于更新旧版本的 rpm。

“License”标签定义了软件包发布的许可证。我总是使用 GPL 的变体。指定许可证对于澄清软件包中包含的软件是开源的这一事实非常重要。这也是我将许可证和 GPL 声明包含在将要安装的文件中的原因。

“URL”通常是项目或项目所有者的网页。在本例中,它是我的个人网页。

“Group”标签很有趣,通常用于 GUI 应用程序。“Group”标签的值决定了应用程序菜单中哪个图标组将包含此软件包中可执行文件的图标。与“Icon”标签(我们在此处未使用)结合使用,“Group”标签允许将图标和启动程序所需的信息添加到应用程序菜单结构中。

“Packager”标签用于指定负责维护和创建软件包的个人或组织。

“Requires”语句定义了此 rpm 的依赖项。每个语句都是一个软件包名称。如果指定的软件包之一不存在,则 DNF 安装实用程序将尝试在 /etc/yum.repos.d 中定义的存储库之一中找到它,并在存在的情况下安装它。如果 DNF 找不到一个或多个必需的软件包,它将抛出一个错误,指示缺少哪些软件包并终止。

“BuildRoot”行指定了 rpmbuild 工具将在其中查找 spec 文件并在其中创建临时目录以构建软件包的顶级目录。完成的软件包将存储在我们之前指定的 noarch 子目录中。显示用于构建此软件包的命令语法的注释包括选项 –target noarch,该选项定义了目标架构。因为这些是 Bash 脚本,所以它们与特定的 CPU 架构无关。如果省略此选项,构建将以执行构建的 CPU 的架构为目标。

rpmbuild 程序可以针对许多不同的架构,使用 --target 选项使我们能够在具有与执行构建的架构不同的架构的主机上构建特定于架构的软件包。因此,我可以在 x86_64 主机上构建一个用于 i686 架构的软件包,反之亦然。

将打包者名称更改为您自己的名称,如果您有自己的网站,请将 URL 更改为您自己的网站。

%description

spec 文件的 %description 部分包含 rpm 软件包的描述。它可以很短,也可以包含多行信息。我们的 %description 部分相当简洁。

%description
A collection of utility scripts for testing RPM creation.

%prep

%prep 部分是在构建过程中执行的第一个脚本。此脚本不会在软件包安装期间执行。

此脚本只是一个 Bash shell 脚本。它准备构建目录,根据需要创建用于构建的目录,并将相应的文件复制到各自的目录中。这将包括作为构建的一部分完成编译所需的源代码。

$RPM_BUILD_ROOT 目录表示已安装系统的根目录。在 $RPM_BUILD_ROOT 目录中创建的目录是完全限定的路径,例如实时文件系统中的 /user/local/share/utils、/usr/local/bin 等。

对于我们的软件包,我们没有预编译源代码,因为我们所有的程序都是 Bash 脚本。因此,我们只需将这些脚本和其他文件复制到它们在已安装系统中所属的目录中。

%prep
################################################################################
# Create the build tree and copy the files from the development directories    #
# into the build tree.                                                         #
################################################################################
echo "BUILDROOT = $RPM_BUILD_ROOT"
mkdir -p $RPM_BUILD_ROOT/usr/local/bin/
mkdir -p $RPM_BUILD_ROOT/usr/local/share/utils

cp /home/student/development/utils/scripts/* $RPM_BUILD_ROOT/usr/local/bin
cp /home/student/development/utils/license/* $RPM_BUILD_ROOT/usr/local/share/utils
cp /home/student/development/utils/spec/* $RPM_BUILD_ROOT/usr/local/share/utils

exit

请注意,此部分末尾的 exit 语句是必需的。

%files

spec 文件的这一部分定义了要安装的文件及其在目录树中的位置。它还指定了文件属性以及要安装的每个文件的所有者和组所有者。文件权限和所有权是可选的,但我建议显式设置它们,以消除安装时这些属性不正确或不明确的任何可能性。如果在安装期间目录尚不存在,则会根据需要创建目录。

%files
%attr(0744, root, root) /usr/local/bin/*
%attr(0644, root, root) /usr/local/share/utils/*

%pre

在我们实验室项目的 spec 文件中,此部分为空。这将是放置在 rpm 安装期间但在安装文件之前需要运行的任何脚本的地方。

%post

spec 文件的 %post 部分是另一个 Bash 脚本。此脚本在文件安装后运行。此部分几乎可以是您需要的或想要的任何内容,包括创建文件、运行系统命令以及重新启动服务以在进行配置更改后重新初始化它们。我们的 rpm 软件包的 %post 脚本执行其中一些任务。

%post
################################################################################
# Set up MOTD scripts                                                          #
################################################################################
cd /etc
# Save the old MOTD if it exists
if [ -e motd ]
then
   cp motd motd.orig
fi
# If not there already, Add link to create_motd to cron.daily
cd /etc/cron.daily
if [ ! -e create_motd ]
then
   ln -s /usr/local/bin/create_motd
fi
# create the MOTD for the first time
/usr/local/bin/mymotd > /etc/motd

此脚本中包含的注释应使其用途明确。

%postun

此部分包含一个脚本,该脚本将在 rpm 软件包卸载后运行。使用 rpm 或 DNF 删除软件包会删除 %files 部分中列出的所有文件,但它不会删除 %post 部分创建的文件或链接,因此我们需要在此部分中处理它。

此脚本通常由清理任务组成,这些清理任务仅仅擦除 rpm 以前安装的文件无法完成。对于我们的软件包,它包括删除 %post 脚本创建的链接并恢复 motd 文件的已保存原始文件。

%postun
# remove installed files and links
rm /etc/cron.daily/create_motd

# Restore the original MOTD if it was backed up
if [ -e /etc/motd.orig ]
then
   mv -f /etc/motd.orig /etc/motd
fi

%clean

此 Bash 脚本在 rpm 构建过程之后执行清理。“%clean”部分中的两行代码删除了 rpm-build 命令创建的构建目录。在许多情况下,可能还需要额外的清理。

%clean
rm -rf $RPM_BUILD_ROOT/usr/local/bin
rm -rf $RPM_BUILD_ROOT/usr/local/share/utils

%changelog

这个可选的文本部分包含 rpm 及其包含文件的更改列表。最新的更改记录在此部分的顶部。

%changelog
* Wed Aug 29 2018 Your Name <Youremail@yourdomain.com>
  - The original package includes several useful scripts. it is
    primarily intended to be used to illustrate the process of
    building an RPM.

将标题行中的数据替换为您自己的姓名和电子邮件地址。

构建 rpm

spec 文件必须位于 rpmbuild 树的 SPECS 目录中。我发现最简单的方法是在该目录中创建一个指向实际 spec 文件的链接,这样就可以在开发目录中对其进行编辑,而无需将其复制到 SPECS 目录。将 SPECS 目录设为您的 pwd,然后创建链接。

cd ~/rpmbuild/SPECS/
ln -s ~/development/spec/utils.spec

运行以下命令来构建 rpm。如果未发生错误,则只需片刻即可创建 rpm。

rpmbuild --target noarch -bb utils.spec

检查 ~/rpmbuild/RPMS/noarch 目录以验证新的 rpm 是否存在于那里。

[student@testvm1 ~]$ cd rpmbuild/RPMS/noarch/
[student@testvm1 noarch]$ ll
total 24
-rw-rw-r--. 1 student student 24364 Aug 30 10:00 utils-1.0.0-1.noarch.rpm
[student@testvm1 noarch]$

测试 rpm

以 root 身份,安装 rpm 以验证它是否正确安装以及文件是否安装在正确的目录中。rpm 的确切名称将取决于您在 Preamble 部分中使用的标签值,但如果您使用了示例中的值,则 rpm 名称将如以下示例命令所示

[root@testvm1 ~]# cd /home/student/rpmbuild/RPMS/noarch/
[root@testvm1 noarch]# ll
total 24
-rw-rw-r--. 1 student student 24364 Aug 30 10:00 utils-1.0.0-1.noarch.rpm
[root@testvm1 noarch]# rpm -ivh utils-1.0.0-1.noarch.rpm
Preparing...                          ################################# [100%]
Updating / installing...
   1:utils-1.0.0-1                    ################################# [100%]

检查 /usr/local/bin 以确保新文件在那里。您还应该验证是否已在 /etc/cron.daily 中创建了 create_motd 链接。

使用 rpm -q --changelog utils 命令查看变更日志。使用 rpm -ql utils 命令(ql 中的 L 为小写)查看软件包安装的文件。

[root@testvm1 noarch]# rpm -q --changelog utils
* Wed Aug 29 2018 Your Name <Youremail@yourdomain.com>
- The original package includes several useful scripts. it is
    primarily intended to be used to illustrate the process of
    building an RPM.

[root@testvm1 noarch]# rpm -ql utils
/usr/local/bin/create_motd
/usr/local/bin/die
/usr/local/bin/mymotd
/usr/local/bin/sysdata
/usr/local/share/utils/Copyright.and.GPL.Notice.txt
/usr/local/share/utils/GPL_LICENSE.txt
/usr/local/share/utils/utils.spec
[root@testvm1 noarch]#

删除软件包。

rpm -e utils

实验

现在您将更改 spec 文件以要求一个不存在的软件包。这将模拟无法满足的依赖项。在现有 Requires 行下方立即添加以下行

Requires: badrequire

构建软件包并尝试安装它。显示什么消息?

我们使用 rpm 命令来安装和删除 utils 软件包。尝试使用 yum 或 DNF 安装软件包。您必须与软件包位于同一目录中,或者指定软件包的完整路径才能使其工作。

结论

在对创建 rpm 软件包的基础知识的了解中,我们没有涵盖许多标签和几个部分。下面列出的资源可以提供更多信息。构建 rpm 软件包并不困难;您只需要正确的信息。我希望这对您有所帮助——我花了几个月的时间才自己弄清楚。

我们没有介绍从源代码构建,但如果您是开发人员,那么从这一点开始应该是一个简单的步骤。

创建 rpm 软件包是成为一个懒惰的系统管理员并节省时间和精力的另一种好方法。它为分发和安装我们作为系统管理员需要在许多主机上安装的脚本和其他文件提供了一种简单的方法。

资源

  • Edward C. Baily,《Maximum RPM》,Sams Publishing,2000 年,ISBN 0-672-31105-4

  • Edward C. Baily,《Maximum RPM》,更新的在线版本

  • RPM 文档:此网页列出了 rpm 的大多数可用在线文档。它包括许多指向其他网站的链接以及关于 rpm 的信息。

标签
David Both
David Both 是一位开源软件和 GNU/Linux 倡导者、培训师、作家和演讲者。自 1996 年以来一直从事 Linux 和开源软件工作,自 1969 年以来一直从事计算机工作。他是“系统管理员 Linux 哲学”的坚定支持者和传播者。

11 条评论

这是一个有趣的指南,但作为在 rpm 包方面有一些经验的人,我仍然有一些提示给你。

1. 安装文件时使用相对路径(应该在 %install 而不是 %prep 中),并通过使用 'SourceX' 语句(将文件放在 SOURCES 目录中)将它们添加到构建中。这允许在不同的机器上以不同的用户进行重现。

例如
Source0: create_motd
Source1: mymotd
...
%install
cp %{SOURCE0} /use/bin

2. 尽量减少 %pre/%post 的使用。人们不喜欢软件包搞乱他们的系统。例如,为什么不直接将 create_motd 安装到 /etc/cron.daily?

3. 有一个名为 rpmlint 的工具,可用于检查您的 spec 文件是否存在问题。

如果您有更多问题,请告诉我。我很乐意帮忙。

感谢这些提示。我很感激。为 Opensource.com 写作的一件事是,像你这样的许多人都愿意分享自己的专业知识。我一定会尝试提示 #1。

至于第 2 项,我特别希望我的 create_motd 脚本——以及任何其他脚本——可以从命令行获得,并将它们放在 /usr/local/etc 中可以做到这一点,因为它始终在 PATH 中,而 /etc/cron.daily 不在。因此,在我看来,在 cron.daily 中添加链接是使脚本每天运行的最佳方式。

我已经忘记了 rpmlint,我感谢您让所有阅读本文的人都知道它。

回复 作者 ganto (未验证)

我还建议您看看“fpm”,它可能是一种更轻量级的方式来创建 RPM 软件包,而无需设置构建树、摆弄 .spec 文件等。

例如,如果您只想将目录树转换为 RPM 软件包,那么 fpm 可以通过一个命令来完成,例如“fpm -s dir -t rpm [..]”

fpm 在 Github 上:https://github.com/jordansissel/fpm

看起来非常复杂。
我更喜欢使用 openbuildservice,它使打包更容易...

虽然我同意你的观点,OpenBuildService 确实使这更容易,但了解幕后发生的事情也是有好处的。

回复 作者 DocB (未验证)

非常好的文章,谢谢你。
关于 %pre %post 语句,请非常非常小心地使用它们。在执行更新时,%pre、%post 和其他脚本的顺序不会按照许多人认为的顺序发生。以下内容可以在触发器文档的末尾找到 ( cat /usr/share/doc/rpm*/triggers )
----
作为参考,以下是单个脚本的执行顺序
软件包升级

\verbatim
all-%pretrans
...
any-%triggerprein (%triggerprein 来自新安装触发的其他软件包)
new-%triggerprein
new-%pre 用于要安装的软件包的新版本
... (所有新文件都已安装)
new-%post 用于要安装的软件包的新版本

any-%triggerin (%triggerin 来自新安装触发的其他软件包)
new-%triggerin
old-%triggerun
any-%triggerun (%triggerun 来自旧卸载触发的其他软件包)

old-%preun 用于要删除的软件包的旧版本
... (所有旧文件都已删除)
old-%postun 用于要删除的软件包的旧版本

old-%triggerpostun
any-%triggerpostun (%triggerpostun 来自旧的取消
安装)
...
all-%posttrans
\endverbatim
----

当您看到这一点时,您的第一个想法应该是 *糟糕*,我怎样才能让我的更新按照我想要的方式工作。
当每个脚本运行时,它们会传递软件包的安装实例数。这有什么帮助?
它允许您执行 if 语句来确定是否应该运行脚本。

%postun
echo "%name-%version-%release : POSTUN : \$1=$1"
if [ "$1" -eq 0 ] ; then
echo " 我们是最后一个 rpm,我们应该真正运行我们的脚本"
fi
if [ "$1" -ge 1 ] ; then
echo " 我们不是最后一个 rpm,小心运行我们的脚本"
fi

我刚刚将我的 testrpm spec 文件放在 github 上,如果您想要更好的一组关于脚本何时运行的示例。每当我不清楚哪个脚本何时运行时,我都会使用这些脚本多年。
https://github.com/tdawson/testrpm

您好,

我有一个关于重建 rpm 的查询。

我正在使用 "rpm --prefix=/home/cloud-user -ivh xyz.rpm" 命令安装 rpm。

当我在 spec 文件中重建此 rpm 时,我想访问此前缀 (/home/cloud-user)。

哪个变量将具有此值?spec 文件如何从命令访问它?

注意:我正在研究可重定位目录,而我正在使用的 rpm 不支持可重定位目录。我只是想找到一种方法来做到这一点。

我注意到您的 %post 中有一个 "ln -s" 命令。我有三个,但有些不起作用。为了调试,我在 %post 中包含了一个 "ls -ld" 命令,在链接之后,并将输出重定向到一个临时文件。该输出显示所有三个链接都已正确创建。但是当软件包安装完成时,系统上不存在其中两个链接。总是相同的两个。希望我知道是什么原因造成的。

当然,我已经这样做了——为了修复不完整的安装。工作正常。

回复 作者 dboth

找到了我的问题。失败的两个链接是指向目录,而不是文件。RPM 文档说 %ghost 属性仅适用于文件,因此我没有将它用于这两个链接。添加它之后,链接在软件包安装后仍然存在。

回复 作者 dboth

Creative Commons License本作品根据 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.