如何构建 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 来确保它们无法忽略 kill 消息。

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 文件的这一部分是另一个 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
----

当您第一次看到它时,您的第一个想法应该是 *youch*,我怎么才能让我的更新按照我想要的方式工作。
每个脚本运行时,它们都会传递软件包的已安装实例数。这有什么帮助?
它允许您执行 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" 命令,在链接之后,并将输出重定向到一个临时文件。该输出显示所有三个链接都已正确创建。但是当软件包安装完成后,系统上不存在其中两个链接。总是相同的两个。希望我知道是什么原因造成的。

尝试在 RPM 上下文之外从 CLI 运行创建链接的代码,看看会发生什么。

回复 ,作者:Dave44 (未验证)

当然,我已经这样做了——为了修复不完整的安装。效果很好。

回复 ,作者:dboth

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

回复 ,作者:dboth

知识共享许可协议本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.