本文是以 RPM Packaging Guide 为主线,进行简化、翻译,中间根据个人的学习需要增减了一些内容,掺入其他很多来源的资料改编而成。应该适合对 linux 打包没什么概念的同学(比如我)阅读
一些基础概念
软件补丁 ( Patch )
软件补丁,是用来修改其他代码的代码。我们使用 diff
工具来创建补丁,并借助 patch
工具来为源代码打补丁。
注意,在打包时对原始的软件代码的修改要以 patch 的形式来做,而不能直接修改代码。RPM 包中的程序源代码需要与代码作者提供的源代码一致
整挺好,那么我们要怎么来制作补丁呢?
-
首先,备份要修改的代码
$ cp ./cello.c ./cello.c.orig 复制代码
原始代码长这样:
#include
int main(void) { printf("Cello! Wtf!\n"); return 0; } 复制代码 -
对
cello.c
的代码进行修改#include
int main(void) { printf("I am the patched Cello!\n"); // 只改了这里 return 0; } 复制代码 -
使用 diff 输出 patch
$ diff -Naur cello.c.orig cello.c > cello-output-first-patch.patch 复制代码
于是,我们得到了这样的一个 patch 文件
-
然后再用之前备份的原始代码,替换掉我们修改过的代码
$ rm cello.c $ mv cello.c.orig cello.c 复制代码
patch 的使用
在原始代码和 patch 所在的文件夹中,执行命令:
$ patch < cello-output-first-patch.patch # 把 .patch 文件作为 patch 软件的输入
复制代码
再打开目录下的 cello.c
文件,就会发现该代码已经被修改好了
安装( install )
安装,是把 RPM 包中的东西【放到其应该在的地方】并【调整好用户对这些东西的权限】的过程。比如可执行文件要放在 PATH 的目录中并且要让所有者可执行,用于开发的头文件要放在包含目录中,库文件要放在库文件该在的地方等。
安装有两种方式,一种是我们可以使用系统自带的 install
工具来把包里的文件放到文件系统中的指定位置,并根据需要设置用户对被安装文件所拥有的权限;另一种是在用 make 进行构建时使用 make install
install
示例操作如下:
$ gcc -o cello cello.c # 编译 cello
$ sudo install -m 0755 cello /usr/bin/cello # 安装 cello 到 /usr/bin ,权限 0755
复制代码
如此一来,在任何目录都可以直接使用 cello
来调用我们的程序了
make install
make install
的具体设置需要在 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) 是用来在我们不想把东西装到根目录的时候,指定目录前缀用的。比如,如果想装到当前目录下,可以使用如下命令:
$ make # 编译
$ sudo make install DESTDIR="./" # 编译出的文件会装到 ./usr/bin/
复制代码
这样,二进制文件就会被装到带前缀的目录下。如果想装在根目录下,省略对 DESTDIR 的指定即可:
$ make
$ sudo make install # 编译出的文件会装到 /usr/bin/
复制代码
准备代码
可以使用原文作者提供的代码:rpm-packaging-guide/example-code
注意:每一份代码都要和一个 LICENSE
文件放在一起,装到一个文件夹中(文件夹命名需要是%{name}-%{version},如 cello-1.0 ),再将这个文件夹压缩打包为 cello-1.0.tar.gz
,放在 ~/rpmbuild/SOURCES/
中。
我们要将 .tar.gz
文件和 .patch
一起全部放入 ~/rpmbuild/SOURCES/
打包
创建 RPM 包的流程:
- 在
~/rpmbuild/SPECS/
创建.spec
文件,命名为 “软件包名.spec” ,并在其中配置打包信息。我们可以使用 rpmdev-newspec 工具来从模板自动生成一个 spec``` $ rpmdev-newspec myfirstpackage.spec 复制代码 ``` 样例以及各个标签的解释说明,见文末附录
- 将源代码包放入
~/rpmbuild/SOURCES/
- 切换到
~/rpmbuild/SPECS/
,对.spec
文件执行rpmbuild
命令:``` $ rpmbuild -ba NAME.spec # -ba: 根据 NAME.spec 构建源代码和二进制软件包 复制代码 ```
关于 SRPM 包和 RPM 包:SRPM 包( source RPM )中包含源代码、补丁和 SPEC 文件(描述如何用 SRPM 包中的文件构建 RPM 包);RPM 包(binary RPM)则只包含编译好的二进制文件。
建立目录树和 SPEC 文件
注意,不要使用 root 用户进行打包操作。在家目录下执行:
$ rpmdev-setuptree
复制代码
该命令会建立 npmbuild
目录。
到 ~/rpmbuild/SPECS/
中,执行:
$ rpmdev-newspec cello.spec
复制代码
该命令会生成一个空的 spec 文件: cello.spec
然后将源代码压缩包 cello-1.0.tar.gz
和补丁cello-output-first-patch.patch
一起放入 ~/rpmbuild/SOURCES/
准备工作基本完成,接下来我们开始写 SPEC 文件
/etc/rpmdevtools/
中含有多种内置的 spec 模板
rpmbuild 目录树简介
- BUILD:构建包时将会在此创建多个不同的
%buildroot
目录。当构建失败时,可以到这里来查找构建失败的原因 - RPMS:保存生成的 binary RPM
- SOURCES:存放代码压缩包和补丁,供
rpmbuild
命令使用 - SPECS:存放 SPEC 文件
- SRPMS:保存生成的 source RPM
编写 SPEC
注:这里会涉及到一些 RPM 内置宏,部分见文末附录
注 2 :附录中还有一个 Fedora 16 eject 的 spec 作为示例参考,写了不少注释
.spec 是干嘛的呢?
.spec 文件定义了该如何构建、安装该软件包中的软件。
它说明了:
- 该软件包是哪来的、什么版本、代码放在哪里、有哪些补丁、有什么依赖
- 系统下载了该软件包后,该怎么处理源代码(比如把代码从压缩包中解压出来、打补丁等)、该如何编译构建软件、在构建完成后该怎么安装(比如把二进制文件复制到
/usr/local
)、
打开刚刚自动生成的 cello.spec
-
会发现最上面的 Name 已经被用 ”cello“ 填好了——该名称需要与 spec 文件名相同,所以已经被 spec 创建的命令自动填好了
Name: cello 复制代码
-
Version 值需要与上游代码的版本号一致,此处为 1.0
Version: 1.0 复制代码
-
Release 是发行版号,根据我们自己发布该包的版本来写,每次进行新发布要增加 1。当需要跟着上游更新代码时,要重置为 1
Release: 1 # 也可以使用宏来实现打包时 Release 自增,写法如下 # Release: 1%{?dist} 复制代码
-
Summary 是简介,要用简洁的语言对包进行描述,结尾不要有标点。如果是来自 github 的项目,Summary 可以直接用 github 项目中的 description
Summary: Hello World example implemented in C 复制代码
License 要根据实际情况来填写。需要注意填写简称
URL 填写项目网址。 Source0 最好填写可以获取到包中源码的 url
如果包中有多个源码,则编号递增。Patch 处理方式相同
-
BuildRequires 填写编译时依赖
BuildRequires: gcc BuildRequires: make 复制代码
这里省略了 Requires 。该处应填写运行时依赖。本包中的程序非常简单,不需要 C 运行时之外的东西,所以省略
接下来填写带百分号的这些 directive 值。这些 directive 后面的内容可以换行,可以包含 bash 脚本等
%description 可以多写点,但最好不要写太多,一段即可
-
%prep 的意思是 prepare:该部分可用于准备构建和编译的环境。一般来说这里进行的操作,都是解压代码压缩包、给解压好的代码打补丁、为接下来的操作做准备等。这里只用一个宏
%prep %setup -q 复制代码
-
%build 部分描述了具体应该如何从源码开始构建程序,所以用 rpmdev 为 GNU make 提供的宏即可
%build make %{?_smp_mflags} # 上面这个其实就是 "make" 复制代码
%configure 用于填写配置脚本( configure script )。此处不涉及,所以这里省略
-
%install 用于告诉 rpmbuild 应如何把刚刚构建好的软件安装到 BUILDROOT 中。BUILDROOT 是一个 chroot 根目录,我们可以在里面根据需要建立任何我们想要的路径或目录结构,实现为我们的软件指定其在最终用户的客户机上安装的位置。比如,我们一般会指定把二进制可执行文件放到
/usr/bin/
中。BUILDROOT 见附录,有另一段介绍
然而,我们这个 cello 小程序是使用 make 进行安装的,因此我们可以使用 rpmdev 宏来指定使用 make 进行安装操作,而不在这个 spec 文件中再次指明,示例如下:
%install %make_install 复制代码
-
%files 部分是我们希望本 RPM 包中的、最终会被装到用户电脑上的文件的列表,以及这些文件应该被放在哪里。注意,这里与 %{buildroot} 无关,这里要写的应该是我们最终希望的、我们的 RPM 包中文件被放置在用户电脑上的完整路径。因此,将 cello 可执行文件在这里写为:
%files %license LICENSE %{_bindir}/%{name} 复制代码
-
%changelog 应重点体现软件包 release 的变动,如对软件打了新的补丁、构建过程变动等。每个版本变动可以包含多个变动条目,每个条目以一个
-
开头,如:%changelog * Tue May 31 2016 Adam Miller
- 0.1-1 - First cello package 复制代码
完成的 spec 文件如下:
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 - 1.0-1
- First cello package
复制代码
附录
标准 RPM 内置宏
来自:Built-in macros
%_prefix /usr
%_exec_prefix %{_prefix}
%_bindir %{_exec_prefix}/bin
%_sbindir %{_exec_prefix}/sbin
%_libexecdir %{_exec_prefix}/libexec
%_datadir %{_prefix}/share
%_sysconfdir %{_prefix}/etc
%_sharedstatedir %{_prefix}/com
%_localstatedir %{_prefix}/var
%_libdir %{_exec_prefix}/lib
%_includedir %{_prefix}/include
%_oldincludedir /usr/include
%_infodir %{_prefix}/info
%_mandir %{_prefix}/man
复制代码
除了标准宏之外,不同的发行版(很)可能会有自己的宏
Fedora 对于 spec 各个 section 以及部分宏的介绍
来自:How to create an RPM package
当执行此命令时,rpmbuild
会自动读取 .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 包通常用于审核和升级软件包。 |
在 rpmbuild
中,对上表中的每个宏代码都有对应的目录:
宏代码 | 名称 | 默认位置 | 用途 |
---|---|---|---|
%_specdir |
Spec 文件目录 | ~/rpmbuild/SPECS |
保存 RPM 包配置(.spec )文件 |
%_sourcedir |
源代码目录 | ~/rpmbuild/SOURCES |
保存源码包(如 .tar 包)和所有 patch 补丁 |
%_builddir |
构建目录 | ~/rpmbuild/BUILD |
源码包被解压至此,并在该目录的子目录完成编译 |
%_buildrootdir |
最终安装目录 | ~/rpmbuild/BUILDROOT |
保存 %install 阶段安装的文件 |
%_rpmdir |
标准 RPM 包目录 | ~/rpmbuild/RPMS |
生成/保存二进制 RPM 包 |
%_srcrpmdir |
源代码 RPM 包目录 | ~/rpmbuild/SRPMS |
生成/保存源码 RPM 包(SRPM) |
如果某一阶段失败,请查看输出信息以了解失败原因,并根据需要修改 .spec
文件。
Fedora 16 eject 程序的 spec 文件
来自:How to create an RPM package
# 如果是来自 github 的项目,Summary 可以直接用 github 项目中的 description ,结尾不要有标点
Summary: A program that ejects removable media using software control
# Name 要与 spec 文件名相同
Name: eject
# Version 要填写上游的版本号,也就是此代码拥有者定义的版本号
Version: 2.1.5
# Release 是发行版号,根据我们自己发布该包的版本来写,每次发布递增 1。跟着上游更新时,要重置为 1
Release: 21%{?dist}
# 简写,参考:https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing
License: GPLv2+
# 指定软件包组
Group: System Environment/Base
# 最好使用 URL 地址。若只有一个源码包,则 Source 和 Source0 等价。若有多个,则从 0 往后排
# 例:Source0: https://downloads.sourceforge.net/%{name}/%{name}-%{version}.tar.gz
Source: %{name}-%{version}.tar.gz
# 源码补丁名称。补丁也放在 ~/rpmbuild/SOURCES 目录中
# 若要在源码包解压后修改代码,应修改代码并使用 diff 生成 patch 文件
Patch1: eject-2.1.1-verbose.patch
# 一个补丁只做一种修改,做多种修改需要拆分补丁
Patch2: eject-timeout.patch
Patch3: eject-2.1.5-opendevice.patch
Patch4: eject-2.1.5-spaces.patch
Patch5: eject-2.1.5-lock.patch
Patch6: eject-2.1.5-umount.patch
# 软件包项目主页。若代码来自 github 且没有官方主页,则放该项目的 github 主页地址
URL: http://www.pobox.com/~tranter
# ExcludeArch:排除某些架构;ExclusiveArch:独占某些架构。
# 若代码与架构无关,则用 BuildArch: noarch 替代掉下面的
ExcludeArch: s390 s390x
# 编译依赖不会自动列出,所以在下面给出需要的依赖包列表。可以指定版本,写法如 ocaml >= 3.08
BuildRequires: gettext
BuildRequires: libtool
# 本例中没有的标签:
# Requires:安装和运行时所需的依赖包列表,以逗号分隔。一般 rpmbuild 会自动探测安装运行的依赖,
# 可以明示写出,也可以在无法自动检测的时候写出来
# BuildRoot:在 %install 阶段(%build 阶段后)文件需要安装至此位置
# 默认根目录为 "%{_topdir}/BUILDROOT/"
# 简短描述。如果是 Github 项目,可以直接复制项目 README 最开头的简介
%description
The eject program allows the user to eject removable media (typically
CD-ROMs, floppy disks or Iomega Jaz or Zip disks) using software
control. Eject can also control some multi-disk CD changers and even
some devices' auto-eject features.
Install eject if you'd like to eject removable media using software
control.
# 打包准备阶段执行一些命令(如,解压源码包,打补丁等),以便开始编译。一般仅包含 "%autosetup"
%prep
# 仅解压源码包至 %{name} 目录
%autosetup -n %{name}
# 若解压后包含多个目录,使用 : %autosetup -c %{name},都解压到 %{name} 目录中
# 如果要解压多个文件,还有更多选项可用。详见:
# https://fedoraproject.org/wiki/How_to_create_an_RPM_package/zh-cn
%build
# 此处可以写构建所需的脚本命令,比如调用某个 .sh 、执行一句 make && make install
%configure
make %{?_smp_mflags}
# 包含安装阶段的脚本命令,比如把编译好的文件从 `%builddir` 拷贝到 `%buildroot`
# %builddir(BUILD)是解压、编译、生成二进制文件的目录
# %buildroot(BUILDROOT)是存放编译好的文件以及目录结构的目录
%install
%make_install
install -m 755 -d %{buildroot}/%{_sbindir}
ln -s ../bin/eject %{buildroot}/%{_sbindir}
%find_lang %{name}
# 需要被安装到用户操作系统中的文件列表
%files -f %{name}.lang
%doc README TODO COPYING ChangeLog
%{_bindir}/*
%{_sbindir}/*
%{_mandir}/man1/*
# RPM 包变更日志。注意不是软件本身的变更日志
%changelog
* Tue Feb 08 2011 Fedora Release Engineering - 2.1.5-21
- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild
* Fri Jul 02 2010 Kamil Dudka 2.1.5-20
- handle multi-partition devices with spaces in mount points properly (#608502)
复制代码
BUILDROOT 介绍
The parameter names are largely self-evident, but
BuildRoot
merits some explanation to differentiate it from the BUILD directory you already created.BuildRoot
is a proxy for the final installation directory. In other words, ifwget
is ultimately installed in /usr/local/bin/wget and other subdirectories in /usr/local, such as /usr/local/man for documentation,BuildRoot
stands in for /usr/local during the RPM build process. Once you setBuildRoot
, you can access its value using theRPM_BUILD_ROOT
environment variable. You should always setBuildRoot
in your spec file and check the contents of that directory to verify what is going to be installed by the package.developer.ibm.com/tutorials/l…