微型语言之RPM

开源软件的优势是什么呢?自然是允许用户了解它的内部工作原理了。有了源代码,您就不怕它留什么后门儿了。而且如果您足够牛,还可以自行改进和扩展它。甚至借鉴这些代码将其用于其它用途(一定得是许可证允许的情况下)。可是这种自由度真的是您需要的吗?答案可能是未必!即便您足够牛气,也不乐意所有软件都自己来编译。毕竟时间跟生命是有关的,浪费时间等同于浪费生命啊。所以还是直接使用已经编译好的软件是最划算的,只需要运行安装程序,回答几个提示就行,轻松又愉快。

在Linux下,这种已经编译好的开源软件大多是采用“包”的形式发布的。包捆绑了一个软件所需的所有二进制文件、数据、配置文件和技术文档。包还规范了将软件安装到系统上所需的步骤,通常就是执行一个命令,回答几个提示。但是由于不同的Linux发行版会有自己独特的特性,这就使得包仅适用于特定系统。因此,不同的Linux发行版会提供自己的包管理器,一个用于在系统中添加和删除包的特殊程序。

rpm就是一种包管理器。它由RedHat在1997年开发出来。由于Red Hat在Linux领域的领导地位,使得rpm成为当今最为流行的一种包管理器,且被大多数Linux发行版所采用。使用Linux的人几乎没有不知道rpm的。

在Linux下使用rpm来安装软件是非常简单的,这使得很多大规模使用Linux作为服务器操作系统的公司,在部署自己的软件系统时也会采用。比如阿里巴巴和新浪就是这样。所以,学习如何制作一个rpm软件包,应该是每个Linux系统管理员的必修课程。至少具备了这个技能,还是有助于找到一份相对体面的工作不是?

制作一个rpm软件包需要使用一种微型语言。得意其精妙的设计,使得它能够承担各种类型软件的安装工作。它是本章要介绍的所有微型语言中,最为年轻的一个,也是唯一一个为Linux而生的微型语言。这种微型语言易学易懂,即便是刚刚接触Linux的小白也能搞定。但是到目前为止,依然有很多Linux的资深用户不知道如何制作rpm软件包。这最主要的原因就是缺乏文档。Linux系统也有这个毛病,这或许就是Linux不能走进普通用户电脑的最大障碍。

1.1 构建第一个RPM

构建rpm包虽然不是什么技巧活,但体力还是需要有一定付出的,而且rpm的设计在笔者看来总是很坑爹的。要不是这东西十分流行和处于对读者负责任的态度出发,我是非常懒得讲它的。

不管它坑爹还是坑妈吧,先来看一个例子,制作一个lighttpd的rpm包。lighttpd是一个非常著名的轻量级http服务器,具有极高的性能。在稳定性方面是绝对不输于Apache的,至少在笔者的应用中,lighttpd从来没有出现过稳定性的问题,所以互联网行业的读者们一定不要错过它。

到本书截稿为止,lighttpd的最新版本是1.4.31,它的官方网址是http://www.lighttpd.net。通过源代码来安装lighttpd的方法跟Linux其它的软件安装方法差不多,同样是“make三步法”。由于lighttpd在处理配置文件的时候要使用到pcre这个正则表达式库,所以读者的机器中应该预先安装好(安装pcre-devel这个包)。对于大多数用户而言,不需要给“configure”脚本传递什么特别参数就可以满足需求,安装配置lighttpd非常简单。这也是笔者选择它作为第一个例子的原因。

事先声明一下,不建议使用root账号来构建rpm,因为这很有可能会破坏你的系统。笔者在设计这个例子的时候也考虑到了这一点。只要读者实验的系统不是特别古老,应该不会出现什么问题。好了,废话不多说,我们开始吧。

构建一个rpm包,下面的几个步骤是必须的:

l   依照rpmbuild规范创建一个目录结构;

l   将源代码和附带文件放在目录中合适的位置;

l   创建spec文件;

l   编译rpm。

rpmbuild规范的目录结构包括5个子目录:BUILD、RPMS、SOURCES、SPECS和SRPMS。BUILD目录是用来编译源代码的;RPMS目录用于存放最后生成的rpm包文件,它会使用不同的子目录来区分对应不同硬件平台的rpm包,比如x86_64;SOURCES目录顾名思义,就是用来存放源代码的;SPECS是用来保存spec文件的,也就是用于构建一个rpm包的脚本文件;SPRMS目录存放着生成源代码rpm包。

构建一个符合rpmbuild规范的目录,笔者采用下列命令:

 

$ cd ~

$ mkdir rpmbuild

$ cd rpmbuild

$ mkdir BUILD RPMS SOURCES SPECS SRPMS

 

这些命令执行完毕之后,就在读者的HOME目录/home/jagen下,创建了一个符合rpmbuild规范的目录了。

然后将lighttpd的源代码文件lighttpd-1.4.31.tar.gz复制到SOURCES子目录中。

接下来,在SPECS子目录中创建spec文件。我将这个文件命名为lighttpd.spec。具体内容见代码4-1所示:

 

# This is a simple spec file for lighttpd                                                                                                 

 

%define _topdir   /home/jagen/rpmbuild

 

Name:        lighttpd

Version:     1.4.31

Release:     1%{?dist}

Summary:     A light http server

License:     BSD

URL:         http://www.lighttpd.net/

Source0:     %{name}-%{version}.tar.gz

Group:       Development/Tools

Prefix:      /usr/local

BuildRoot:   %{_topdir}/BUILDROOT/%{name}-%{version}-%{release}%{?dist}.%{_arch}

 

%description

Lighttpd is a secure,fast,compliant,and very flexible

web-server that has been optimized for high-performance environments.

 

%prep

%setup -q

 

%build

./configure --prefix=%{prefix} --libdir=%{prefix}/lib64

make %{?_smp_mflags}

 

%install

rm -rf $RPM_BUILD_ROOT

make install DESTDIR=$RPM_BUILD_ROOT

 

%files

%defattr(-,root,root,-)

%{prefix}/sbin/lighttpd

%{prefix}/sbin/lighttpd-angel

%{prefix}/lib64/*.so

%{prefix}/lib64/*.la

%{prefix}/share/man/man8/lighttpd.8

 

代码4-1lighttpd.spec文件

 

这段代码在笔者的Red HatEnterprise Linux Server release 5.2 64位版本下测试通过,这个版本的Linux系统在淘宝的应用是很广泛的。它所附带的rpm版本是4.4.2,比较老了。比较新的被广泛应用的rpm版本应该是4.7.0以后的版本。使用较新的rpm版本,这段代码能够更简单。

当spec文件准备好之后,我们就开始构建rpm包了。使用下面的命令:

 

$ rpmbuild -ba SPECS/lighttpd.spec

 

如果读者没有写错什么,那么应该会在RPMS子目录中得到名称类似于lighttpd-1.4.31-1.x86_64.rpm和lighttpd-debuginfo-1.4.31-1.x86_64.rpm这两个文件。在SPRMS子目录中得到名称类似于lighttpd-1.4.31-1.src.rpm的文件。具体是什么文件名,取决于读者的系统和硬件平台。而且在RPMS子目录中还应该有类似x86_64或i386这样的子目录。只要没有错误,你的.rpm文件应该在就在那里。

1.2 工作原理

虽然第一个例子是怎么工作的,或许通过rpmbuild命令的输出可以一窥其端倪。但大多数初学者可能还会是一头雾水,spec文件里的内容都代表什么,rpm怎么就能将软件安装安装到我们想要的位置呢?这就需要从rpm的工作原理说起。

spec文件中的内容就是本节所讲述的重点,它是rpm所使用的一种微型语言。我们从头到尾分析一下这个spec文件,看看rpmbuild是怎样处理它的。

一个spec文件大体上可以分为几个“段”:定义段、描述段、预处理段、构建段、安装段和打包段等。每个段会描述一些动作。

1.2.1   定义段

第一行的内容我想各位读者都能想到,就是注释。它处在定义段。在spec文件中,所有以“#”开头后面接一个空格的内容都视为注释。其实这个空格是很坑爹的,很多人在这个地方栽跟头。当然,注释不是定义段的专利,任何地方都可以有注释。

接着定义了一个变量“_topdir”,后面出现的“%{_topdir}”就是对变量的引用。同样是在定义段。这个变量决定了rpm工作目录的最顶层目录。换句话说,_topdir变量是由rpm内部提供的,即便你不定义它,它也会有默认值(其实很少有spec文件定义这个变量)。不同版本的rpm,这个默认值不同。这又是一个很坑爹的设计。你说没事儿你经常改这个干什么呢?

下面所有的类似“标签: 值”的内容,就是在设定一个rpm包的基本信息。也属于定义段。具体什么含义,应该是不言自明的。不过这同样是一个很坑爹的设计。为什么呢?因为这些标签的值可以使用变量来引用。比如“{%name}”来引用“Name”这个标签的值,“{%version}”来引用“Version”标签的值。标签名不区分大小写,引用它的变量有些必须全都是小写字母,而有些必须全都是大写字母,比如对“Source”标签的引用是“%{SOURCE}”,是不是很乱?

标签名不能乱写的,必须是rpm定义的才行。且不同的标签有不同的含义,也不能乱用。这里比较重要的是“Source”和“BuildRoot”标签。Source标签规定了源代码的名称或来源;BuildRoot标签规定了rpm在什么地方找到编译好的二进制文件或其它需要安装的文件,并打包到.rpm文件中去。

这里比较特殊的是“%{?dist}”这个变量的引用。特殊的地方是附带了这个问号“?”。什么意思呢?这得从rpm处理变量的行为说起。如果一个变量的定义过,那么所有引用变量的地方就是变量的值;如果一个变量没有定义过,rpm并不会像我们所期望的那样,使用空值来代替,而是保持引用变量的字符串不变。就拿“dist”这个变量来说,如果之前定义这个变量的值为“.fc16”[1],那么当我们使用“%{dist}”来引用的时候就会得到“.fc16”这个值,反之会得到“%{dist}”这样的结果。所以有时候为了处理不知道是否定义过得变量时,尤其是那些rpm内置的变量,很让人头痛。使用问号“?”可以有效解决这个问题。这个时候的引用变量的行为跟我们想象中的是一样的,如果没有定义这个变量,就得到空值。dist变量正好就是一个rpm的内置变量,不同Linux发行版上的rpm会有不同定义,有些甚至没有定义。它实际上是代表Linux发行版的缩写。

1.2.2   描述段

“%description”下面的内容是对这个rpm包的描述信息,这就是描述段了。没有什么特别的地方,随便写什么都行。使用“rpm -qi”命令查询到的信息就是定义段和描述段中的一些内容。如果你想做一个负责任的打包者,那么就在这里给软件包写一份详细的说明吧。

1.2.3   预处理段

“%prep”代表预处理段的开始。这个段的作用就是整理代码,比如解压缩源代码包,给源代码打补丁等。rpm不要求源代码一定要放在SOURCES子目录中,完全可以通过网络下载。也就是说“Source”标签的值完全可以是源代码的URL地址。如果是这样,rpm在执行到预处理段时就是会去下载它,并保存在SOURCES子目录中。

需要注意的是,例子中实际使用的标签是“Source0”。这是因为rpm允许将多份源代码制作成一个包。如果有多份源代码,可以在Source后面添加数字,比如“Source1”。“Source0”和“Source”相同。

“%setup -q”是预处理段的一个宏。它会根据“Source”标签的值决定自己是什么。在本例中它会产生这样的执行结果:

 

+ umask 022

+ cd /home/jagen/rpmbuild/BUILD

+ LANG=C

+ export LANG

+ unset DISPLAY

+ cd /home/jagen/rpmbuild/BUILD

+ rm -rf lighttpd-1.4.31

+ /usr/bin/gzip -dc /home/jagen/rpmbuild/SOURCES/lighttpd-1.4.31.tar.gz

+ /bin/tar -xf -

+ STATUS=0

+ '[' 0 -ne 0 ']'

+ cd lighttpd-1.4.31

+ /bin/chmod -Rf a+rX,u+w,g-w,o-w .

+ exit 0

 

这是rpmbuild在处理到预处理段时产生的输出结果,从这些命令的基本含以上看,就是解压缩lighttpd-1.4.31.tar.gz这个文件,并进入到解压后所产生的目录中。至于“-q”这个参数,它就是让%setup所产生的命令能够以静默的方式解压,不输出任何内容。

估计很多读者会问,如果遇到多份源代码的情况怎么办呢?最后要进入到那个目录?可以使用“-b”或“-a”选项,选项的参数就是Source标签后面的数字,比如“%setup -b 1”。“%setup”无论处理多少份源代码,最后总是会进入“Source”标签所代表的源代码解压后的目录。换句话说,“Source”标签所描述的源代码是主,其它标签如“Source1”等描述的源代码是辅助的。这种行为更像是给主源代码打补丁。因此%setup宏提供“-b”和“-a”这两个选项,分别代表进入源代码目录之前处理和进入源代码目录之后处理。

但是需要特别注意的地方是,即便只书写了如 “%setup -b 1”或“%setup -a 1”这样的代码(显然你忽略了主源代码),它们也会去解压主源代码。一个比较有爱的功能啊,可以减少一些打包者的工作。可是这个功能对只有一个主源代码和一个辅助源代码的项目很有爱。如果又多个辅助源代码可就很烦人了。这主要有两个原因:第一,setup不会很智能的说我前面解压了主源代码,后面就不在管了,这就导致每次处理辅助源代码都会导致主源代码的解压缩;第二,每次解压缩主源代码之前,都要删除原有解压过的目录,这就导致早前解压好的代码被破坏掉了。这种情况就会显得这种有爱很有碍了。为了处理这种问题,需要使用%setup宏的另外一个参数“-T”。这个参数阻止%setup解压主源代码。因此很常见的形式是类似“%setup -T -a 1”这样的写法。

如果读者没有弄明白,可以亲自在自己的系统中尝试。使用“rpmbuild -bp”这样的命令可以只处理预处理段的内容。

1.2.4   构建段

“%build”代表了构建段,也就是编译源代码。它下面的内容是我们很熟悉的“configure”和“make”过程。

在构建段中应该传递给configure足够的参数。我们想如何安装,就给定什么样的参数。具体都应该设置哪些参数,在入门部分的章节中就已经介绍过了,这里就不再复述了。

在例子中,构建段的make命令后面出现了“%{?_smp_mflags}”这样的变量引用。它代表了make的并行编译选项,在笔者的机器中,变量值是“-j2”。有关make并行编译的内容,我们下一章将详细介绍。

1.2.5   安装段

“%install”是安装段,它与我们正常安装软件的方式不同,要求将软件安装到rpm指定的目录中。这就是为什么在本例中使用“DESTDIR=$RPM_BUILD_ROOT”这个参数的原因。

“DESTDIR”实际上是Makefile中的一个变量,代表将编译好的程序安装到什么位置。这与configure脚本的“--prefix”选项有很大不同,“DESTDIR”是“--prefix”更为高级的一个目标目录,如果DESTDIR的值是“abcd”,“--prefix”的值是“efgh”,那么软件的最终安装路径应该为“abcd/efgh”。当然,这并不是所有的软件都可以用这种方式来处理,只有用Autotools套件生成的Makefile开源软件可以这样做。而lighttpd恰好就是这样的软件。具体Autotools套件如何使用,下一章将会详细介绍。

比较坑爹的是“$RPM_BUILD_ROOT”这个变量。它实际上是“BuildRoot”标签的值。很奇怪从来没见有人使用“%{buildroot}”,而且笔者实际测试的结果是使用%{buildroot}也能打包成功。既然人们都是使用$RPM_BUILD_ROOT,或许这里有什么秘密,只是笔者不知道罢了。那么读者们就这么凑合用吧。

前面已经说过了,BuildRoot标签规定了rpm在什么地方找到编译好的二进制文件或其它需要安装的文件,并打包到.rpm文件中去。那么例子中的写法就非常明确了,将lighttpd安装到BuildRoot标签所描述的位置,这样就可以让rpm找到并生成.rpm文件了。

那么遇到了没有采用Autotools的开源软件怎么办呢?那就只能您手工将要打包的文件复制到BuildRoot标签所描述的地方了。而且具体的相对路径也要处理好。

1.2.6   打包段

“%files”是打包段,它实际上就是一个要打包的文件列表。 “%defattr”宏描述了各文件的默认权限。一共有四个参数,分别对应了文件权限、所属用户、所属组和目录权限。文件权限和目录权限通常是常用的8进制权限描述法,如“755”。如果使用“-”,则代表采用当前文件的权限。当然,所属用户和所属组也可以用“-”来描述。但是我们采用的是非root用户来构建.rpm文件,而在安装的时候确希望是root,所以采用了例子中的写法。这也是常用写法。

需要注意的是,编写文件列表的时候,使用的路径是相对于BuildRoot的。当然,如果去掉BuildRoot,就是实际要安装的路径了。rpm也是这样做的。它从相对于BuildRoot读取文件来打包,而安装的时候会却掉BuildRoot。

1.2.7   总结

好了,到此我们就已经完成了一个简单的rpm包的制作了。每一个rpm包都会有一个spec文件与它对应。也就是说,spec是rpm打包的源代码文件,这是一种微型语言。

一个spec文件一般会分成几个段:定义段、描述段、预处理段、构建段、安装段和打包段等。预处理段、构建段和安装段的内容可以包含一些宏和shell命令;而宏最终是去执行一些shell命令。其它段中不能含有shell命令。

rpm的打包过程中最重要的是BuildRoot这个标签所描述的路径。它是打包过程中,rpm所有要打包的文件的根路径。而相对于它的那些路径,实际上是按照安装时的路径组织的。rpm在安装软件的时候,只需要去掉BuildRoot就可以了。

这就是rpm打包的基本原理。软件的编译过程与使用源代码来安装软件时的编译过程时完全一样的。只是在安装的时候有一些差别,注意BuildRoot在这里起到的作用就可以了。接下来,我们继续看一下,rpm的一些其它特性。

1.3 给源代码打补丁

前面说过,在预处理段是可以给源代码打补丁的。不过让笔者很郁闷的是,很难找到针对lighttpd的补丁,或许是它无敌强大?或许是用的人不多?具体不得而知,反正好用我知道。既然找不到lighttpd的补丁,有舍不到找别人做例子,那么我就自己给它做补丁。做一个从1.4.30升级到1.4.31的补丁包,你说我是不是有点蛋疼呢:)

各位读者应该不会在没等看完本书就忘记如何制作软件补丁了吧?这可是入门章节中的内容啊。好吧,如果你忘记了,我在这里在给你提个醒吧。制作补丁需要用到diff这个命令,而应用补丁,则需要patch这个命令。

那么下载好两个版本的lighttpd,然后执行下面的命令:

 

$ diff -urN lighttpd-1.4.30 lighttpd-1.4.31 > lighttpd-1.4.30-to-31.patch

 

这个时候就获得了lighttpd-1.4.30-to-31.patch这样的一个升级补丁文件。接下来可以使用下面的命令,将1.4.30的代码升级成1.4.31的:

 

$ cd lighttpd-1.4.30

$ patch -p1 < ../lighttpd-1.4.30-to-31.patch

 

不过这是在命令行中这样操作,那么如果在spec文件中操作呢?我们修改一下第一个例子,见代码4.3(不变的代码基本被省略掉了):

 

# This is a simple spec file for lighttpd                                                                                                 

 

%define _topdir   /home/jagen/rpmbuild

 

Name:        lighttpd

Version:     1.4.31

Release:     1%{?dist}

Summary:     A light http server

License:     BSD

URL:         http://www.lighttpd.net/

Source0:     %{name}-1.4.30.tar.gz

Patch0:      %{name}-1.4.30-to-31.patch

Group:       Development/Tools

Prefix:      /usr/local

BuildRoot:   %{_topdir}/BUILDROOT/%{name}-%{version}-%{release}%{?dist}.%{_arch}

 

......

 

%prep

%setup -q -n lighttpd-1.4.30

%patch0 -p1

 

%build

./configure --prefix=%{prefix} --libdir=%{prefix}/lib64

make %{?_smp_mflags}

 

......

 

代码4-3 具有补丁的lighttpd.spec文件

 

这段代码的变动还是很有特点的。首先“Source0”标签的值,源代码包的文件名中版本部分不再使用%{version}来引用了。这个很好理解。毕竟我们的源代码版本比较低一些,没法引用。但是在预处理段中,“%setup”后面居然出现了一个“-n lighttpd-1.4.30”这样的参数。这是十分奇怪的地方。原因就在于rpm本身做了很多假定。对于源代码解压后的目标目录名就有这种假定,假定它为“%{name}-%{version}”这样的形式。可是我们真正对源代码解压后得到的确不是这样的目标目录(本例中实际的目录是lighttp-1.4.30)。在%setup宏解压完主源代码要进入这个目录时就会失败。为了应对这种情况,%setup宏提供了“-n”这个参数,强制进入其指定的目录。对于rpm本身做的各种假定,主要都来自于描述段的各种标签中。所以不要随便的判定一个标签仅是用来说明,没有实际作用。

另外一处显著的变动就是增加了“Patch0”这样的标签。它与“Source”具有相同的特点,为了解决需要多个补丁的情况而采取在标签后面添加数字后缀。但是在处理补丁的时候要比处理源代码的时候人性化多了。在预处理阶段可以使用类似"%patch0”这样的宏来对源代码打补丁。后面的数字后缀与Patch标签的数字后缀保持相同即可。而且具有后缀“0”和没有后缀等价。%patch宏的参数与patch命令的参数基本相同,只是不需要提供具体的补丁文件罢了。

其它的部分就没有变化了。最后执行“rpmbuild -ba lighttpd.spec”就会得到相应的.rpm包。

1.4 依赖关系

在基础部分的章节中介绍使用rpm安装软件的时候就说过,由于Linux太过自由,导致软件依赖问题很严重。rpm的一个重要功能就是解决软件的依赖关系问题。

在本节的开头就说过,lighttpd在处理配置文件的时候要使用pcre这个正则表达式库。如果你打好的lighttpd包被安装在了没有安装pcre库的系统中,是不能工作的。这就是一种软件的依赖关系,lighttpd依赖pcre。如果期望在用户安装你的包时提示用户缺少pcre,或者在已经安装好lighttpd后用户不经意要卸载pcre时发出警告,就要在spec文件中指明这种依赖关系。另外,spec文件也可以作为源代码来发布的。用来告诉用户你的软件是如何打包的,应用了那些补丁。实际上好多采用rpm作为软件管理器的发行版,都是用这种方式来公布源代码的。那么为了让获得了spec文件的用户能够顺利的完成打包工作,也应该做好打包时的软件依赖关系,当不满足条件时,提醒用户。

接下来我们就看一下如何在spec文件中来处理这种依赖关系。具体参见代码4-4所示。它修改自代码4-3,不变的地方会被忽略。代码改动不多,就是在描述段中添加了“BuildRequires”和“Requires”两个标签。BuildRequires标签用于描述在打包阶段依赖的软件包,在我们的例子中是pcre-devel;Requires标签用于描述在安装rpm包(不要与安装段弄混)的时候依赖的软件包,在我们的例子中是pcre。这也非常好理解这种依赖方式,因为在打包阶段,编译lighttpd需要pcre的头文件;而在安装阶段,只需要pcre这个库,至于头文件是不需要的。也可以从pcre和pcre-devel这两个软件包的名字看出,后者是用于开发的。

 

......

 

BuildRoot:   %{_topdir}/BUILDROOT/%{name}-%{version}-%{release}%{?dist}.%{_arch}

BuildRequires: pcre-devel >= 8

Requires:      pcre >= 8

 

......

 

%prep

%setup -q -n lighttpd-1.4.30

%patch0 -p1

 

%build

./configure --prefix=%{prefix} --libdir=%{prefix}/lib64

make %{?_smp_mflags}

 

.....

 

代码4-4 具有依赖关系的lighttpd.spec文件

 

另外一个比较重要的是“>=”这个关系运算符。这表明不单单是对pcre和pcre-devel的依赖,还明确了依赖的版本,就是大于或等于8。依赖关系能够使用的关系运算符还包括:=、>、<、<=。比较严格的是“=”,这个明确只依赖特定的版本,一般很少出现。“<”和“<=”更是不常见的,至于原因你肯定能想明白的。如果不书写关系运算符的话,就表明不对版本有依赖。如果需要依赖多个软件包,也可以在这两个标签后面直接书写,用空格分割即可。很多时候为了代码的可读,可以书写几个标签,不怕重复的。

1.5 一个软件多个包

正如我们刚刚看到的,pcre和pcre-devel实际上是同一个软件的不同部分。前者提供基本功能,而后者是用于开发的。那这样的一个软件做成不同的rpm软件包,是否需要编写不同的spec文件呢?答案是否定的。一个spec文件可以产生多个rpm软件包,可以说是一石多鸟。

由于lighttpd是可以进行二次开发制作插件的,所以具备这种将一个软件打包成不同部分潜力。那么接下来我们就做一个lighttpd和lighttpd-devel这两个rpm包。的具体该怎么弄,还是用代码来说话吧!见代码4-5所示。这一次对整个代码的调整还是非常大的,定义段、描述段、安装段和打包段都涉及到了。这是最小的改动。读者们推理一下就可以得出为什么必须要修改这四个段的原因了。

 

......

 

%description

Lighttpd is a secure,fast,compliant,and very flexible

web-server that has been optimized for high-performance environments.

 

%package -n devel

Summary:        Header files for lighttpd development

Group:          Development/Libraries

Requires:       %{name} = %{version}-%{release}

 

%description -n devel

The lighttpd-devel package contains the header files needed

to develop plugin that used by lighttpd server.

 

......

 

%build

./configure --prefix=%{prefix} --libdir=%{prefix}/lib64

make %{?_smp_mflags}

 

%install

rm -rf $RPM_BUILD_ROOT

make install DESTDIR=$RPM_BUILD_ROOT

install -m 755 -d $RPM_BUILD_ROOT%{prefix}/include/lighttpd

install -m 755 src/*.h $RPM_BUILD_ROOT%{prefix}/include/lighttpd

 

......

 

%files -n devel

%defattr(-,root,root,-)

%{prefix}/include/lighttpd/*.h

 

代码4-5 一石多鸟的lighttpd.spec

 

首先,我们要制作的是两个包,两个包肯定有不同的定义信息和描述信息,这必然涉及到要对定义段和描述段的调整。那么如何区分这些定义或描述信息是不同包的呢?rpm的处理方式是划分主包和次包。主包代表着一个软件的主要功能,是完整的独立的运行个体;次包一般代表着软件的扩展功能,如插件或二次开发接口等。对于主包的定义段和描述段,在这个例子中并没有变化。但是次包则使用“%package”宏来声明,它属于定义段的内容。每一个%package都带有一个参数“-n”来为次包命名,在我们的例子中是“devel”。主包和次包在定义段的很大区别是主包决定了软件的名称、版本、版权授权方式、源代码等重要信息,次包是不能另外再从新定义的。但是次包可以有自己的简介、分组和依赖关系的信息。次包的依赖关系大多是主包,而且是用“=”关系描述符规定的严格依赖关系。当然,如果一个软件可以产生多个次包,次包之间也可以形成这种严格的依赖关系。另外,对于描述段,则主包和次包是完全分开写的。重要的区别就在于“%description”这个宏是否带有“-n”参数。带有参数的就是次包的描述段。具体属于哪个次包,就取决于-n参数的值。因此这个参数值就不能乱写,它必须与%package宏的“-n”参数值保持一致。另外,从这个例子中读者们也可以发现,定义段和描述段是可以交叉编写的。其实完全可以将它们认为是一个段,一定要命名的话,就叫它定义描述段吧。

其次,虽然要制作两个包,但是预处理段、构建段和安装段并不会做任何区分。因此这这些段中,就不会发现类似描述段那样,可以定义几个,并使用参数做区分的情况发生。这也很容易理解,毕竟主包和次包都是源于同一份源代码,在这几个阶段的完全没有必要单独处理的。至于在本例中之所以要修改安装段的代码,主要是因为lighttpd本身不具备安装它那些作为二次开发所必须的头文件的能力,只能选择手工安装的方式。其实很多软件都是这样,这样的写法并不是lighttpd的专利。

最后,由于打包阶段涉及到了要将那些文件放入那个软件包中,那么在打包段就必须要进行区分了。与描述段相同,在%files宏后面添加一个“-n”参数来区分。而具体的打包过程,主包和次包就没有什么本质上的区别了。至于安装路径的问题,是在安装段处理的。

这里rpm有一个较坑爹的是地方,就是“-n”这个参数可以被忽略,直接提供参数值。初学者经会困惑,在现有的那些发行版的spec文件中,有时候能看到“-n”这个参数,有些看不到,总是拿捏不准应该怎么办。我在这里就告诉你,至少现在写不写都一样,以后发现有错了你就改一下,就不要在这种鸡毛蒜皮的小事上纠结了。

对于lighttpd的例子,实际上是可以再单独划分出一个包的,那就是plugin包。因为lighttpd的很多功能都是被制作成插件来提供的(就是那些.so文件)。在本例中这些插件都放在了主包中。至于怎样制作这个plugin包,就当做是作业,留给读者们自己去完成吧。

1.6 操控软件的安装、升级和反安装

用笔者目前所列举的这些例子而生成的那些软件包,rpm在安装它们的时候只不过是将包中的文件复制到了指定的位置罢了;执行反安装,也就是将安装时复制的文件间的的删掉。虽然就lighttpd这个软件而言,或许已经足够了。但是如果你认为这就是软件安装的全部,那可就大错特错了。

1.6.1   控制点

就lighttpd来说,它可以是一个守护进程。一般出于安全考虑,建议守护进程使用一个独立的没有实际权限的用户来运行,那么在实际安装lighttpd之前,最好是能创建一个新的用户;如果能够在执行安装之后就自动运行起来,也应该是一个对用户很有爱的功能;如果lighttpd正在运行而要执行反安装的话,让反安装过程自动终止lighttpd的执行也是理所当然的事儿。毕竟科技是要以人为本的嘛!另外,rpm是支持软件升级安装的,这也是最烦人的地方,一旦处理不好,旧版本的程序清理不干净,新版本的程序还工作不了,那可就大事不妙了。

那么rpm本身会帮我们做这些事情吗?只能说rpm很想做,但是它没有那么聪明。所以还是要把这种要求高智商的行为交给人来做。一个spec文件中,除了必须要包含前面介绍的那几个打包过程所必须的六个段添外,还可以添加六个能够控制软件安装过程的段。这相当于是在软件的安装、升级和反安装过程设下了六个控制点。

其中有四个是控制安装过程的,分别是:“%pretrans”、“%pre”、“%post”和“%posttrans”,这个顺序也是它们在安装过程的执行顺序。比较重要的差别是“%pretrans”和“%pre”在软件被真正安装之前被执行,而“%post”和“posttrans”则是在这之后执行。至于控制反安装过程的就只有两个了,分别是:“%preun”和“%postun”。前者在软件被真正卸载之前执行,而后者在这之后执行。

这六个段中的内容,对于大多数软件来讲,只是一两个命令;对于复杂一点软件,还可以是一段脚本程序,默认的是shell脚本。无论命令还是脚本程序,都会在软件被安装或反安装时的某个特定时刻被执行。所以很多文章将这六个段称之为“钩子(hooks)”或“小脚本程序(scriptlet)”。都对,放在其它段中这么称呼也很形象。其实怎么称呼是无关紧要的,重要的是了解它们被调用的时机。

1.6.2   复杂的软件升级过程

软件的安装和反安装过程是比较简单的流程。安装时无外乎复制文件,调整系统配置。反安装时也差不多。但是对一个已经安装的软件进行升级,并不是按照人们推理的那样先反安装旧版本软件再安装新版本软件。这是一个复杂的过程,rpm设计了8个步骤:

1.执行新版本软件包的%pretrans段的操作;

2.执行新版本软件包的%pre段的操作;

3.安装心版本软件的所有文件;

4.执行新版本软件包的%post段的操作;

5.执行旧版本软件包的%preun段的操作;

6.卸载旧版本软件;

7.运行旧版本包的%postun段的操作;

8.运行新版本包的%posttrans段的操作。

很多人说第5步和第7步的操作可以帮助一个安装失败的软件部分或全部回退1~4步的操作。真的不知道谁这么能扯。而真实的一幕是,如果不在这两个地方附加点条件就执行的话,是非常有可能破坏新版本软件滴。为了防止这种乌龙事件的发生,rpm会给每一个段传递一个参数或标记,用来告诉它们,现在执行的是个啥操作。shell脚本可以使用“$1”来引用这个值。表4-6列举了这个参数的取值情况。

从表4-6中可以看出,比较特殊的是“%pretrans”和“%posttrans”这两个段的标记值。在能够调用它们的操作中,这个标记都是0。这也变相说明,使用这两个段的情景是不用区分到底是安装软件还是升级软件的。还要注意的是,这两个段只有4.4版本以后的rpm才支持。%pretrans永远被最先执行,而%posttrans永远被最后执行。为了能更清晰的说明如何应用这个标记,我列举两个例子,分别使用shell和perl来完成。见代码4-6a和代码4-6b:

 

 

安装

升级

反安装

%pretrans

$1==0

$1==0

N/A

%pre

$1==1

$1==2

N/A

%post

$1==1

$1==2

N/A

%preun

N/A

$1==1

$1==0

%postun

N/A

$1==1

$1==0

%posttrans

$1==0

$1==0

N/A

 

表4-6 各安装控制段在不同情况下的标记值表

 

 

......

 

%pre

if [ "$1" = "1" ]; then

    echo "install"

elif [ "$1" = "2"]; then

    echo "upgrade"

fi

 

......

 

代码4-6a shell脚本来定义%pre段的操作

 

......

 

%pre -p /usr/bin/perl

if ( $ARGV[0] == 1 ) {

    print "install"

}

elsif ( $ARGV[0] == 2 ) {

    print "upgrade"

}

 

......

 

代码4-6b perl脚本来定义%pre段的操作

 

在这里给出两个在功能甚至形式上都完全一样的例子,或许有些读者会埋怨笔者为了稿费凑数。我要说写这本书是按版税结算的,多几个字少几个字是不影响收入滴。既然不为稿费,那是不是在这里炫技术呢?我要说炫是白富美、高富帅们的专利。像我这种土肥圆的苦逼程序猿是不没有这个爱好滴。列举使用perl的例子是为了顺便引入“-p”这个参数。这个参数的作用是限制一个段仅调用一个命令。比如最常见的在安装完带有共享库的软件后更新动态连接器缓存文件的操作:

 

%post

/sbin/ldconfig

 

可以这样写:

 

%post -p /sbin/ldconfig

 

可以这样使用perl,是因为除了perl命令本身,其它的内容都可以看作是perl的参数。而perl又支持这种执行方式,因此可以使用perl来完成每个段的操作。具体到其它解释型语言也可以使用类似的方法。需要注意的是,使用“-p”参数时,后面的命令应该给出全路径。

1.6.3   上下文依赖

既然每个控制点都可以执行几个命令或一段脚本,那么这些命令或脚本执行失败了会怎么样呢?

安装的时候,如果错误发生在“%pretrans”和“%pre”这两个段内,则会导致rpm直接退出。

那如果错误发生在“%post”和“posttrans”这两个段内会发生什么情况呢?rpm也直接退出?因为有错误,证明软件没有被成功安装,直接退出是很不负责任的。那就回滚操作,卸载软件!这个功能只能留给各位读者去实现了,目前版本的rpm还不具备这个能力。这该怎么办呢?唯一的办法就是不要在这两个段内发生错误。

可能读者们已经意识到了,由于“%pretrans”和“%pre”这两个段会在软件被真正安装前调用,而且还可以阻止rpm的继续执行,那么就可以利用它们对当前的系统进行一系列检测,确认是否满足软件的安装条件来防止软件安装失败。这是一个绝妙的好主意。很多现有的rpm软件包就是这样做的。

当然,即便在软件安装之前可以尽量避免软件在安装后调用“%post”和“%posttrans”两个段的代码而发生错误,但是这并不能保证万无一失。所以有人就想:干脆不用它们算了。但是这又是很难避免的。比如某些软件你不先安装好,是没法进行配置的。或者犹如lighttpd这样的作为守护进程的软件,必须安装好才能启动。那么,比较可行的办法是尽量让“%post”和“%posttrans”的代码足够简单。绝大多数现有的rpm软件包就是这样做的。基本上就是一两行代码,或者干脆就是一个命令。

其实无论是在安装、反安装、升级的哪个阶段,都有这种问题,不希望有错误发生。尤其悲催的是那些用于检测系统环境的命令或工具软件没有被安装。遇到这种情况,有的读者会想到依赖关系,添加一个就行了。这是个主意,不过这是一个馊主意。为什呢?因为一旦添加了依赖关系,就表明当你的软件包被正确安装后,被依赖的那个软件就不能随便卸载了。而且你的软件并不是离开它就不能运行,只是在安装的时候利用人家一下。显然这种方法有些过于野蛮。

但是利用依赖关系还真的是一个办法,rpm的开发者们也想到了。那就把馊主意变成好主意吧!于是引入了上下文依赖标签。写法是这样的:

 

Requires(X): foo

 

这里的X可以是pretrans、pre、post、preun、postun、posttrans中的任何一个。这告诉rpm在需要执行那些对应的段中代码时要检测对foo的依赖。比如最常见的写法:

 

Requires(post): /sbin/ldconfig

 

表明在需要执行%post段的代码时,要验证是否有“/sbin/ldconfig”这个命令存在。

上下文依赖很好的解决软件安装、反安装和升级时对工具软件包的依赖问题。只有在rpm需要调用对应段的代码时,才去检测依赖关系。被依赖的软件是随时都可以被卸载的。

1.6.4   常用案例

只是写一些无关痛痒的文字,实在是空洞之极。摆出一个个实实在在的案例呈现给大家,才能具有启发性和说服力。那么在本小节的结尾处,笔者就列举两个控制软件安装过程的常用案例,期望能够给读者们一些启发。

作为lighttpd这样的可以是守护进程的软件,在一开始就说过,出于安全考虑,应该使用一个没有任何权限的用户来运行它。添加这个用户的操作应该在lighttpd被安装前。可以选择在%pretrans或%pre段中执行这个操作。出于兼容性考虑,一般会选择%pre段。代码如下:

 

%pre

/usr/sbin/useraddr -c "lighttpd" -u 99 \

    -s /sbin/nologin -r -d/var/www/ lighttpd 2> /dev/null || :

 

至于这段代码是什么含义,读者们可以参考“useraddr”命令的联机文档,而且很幸运的是,还有中文:)

有一些软件会提供用于二次开发的程序库。如果它提供的是共享库,那么若想在安装之后能够正常使用,就需要调用ldconfig命令来更新动态连接器的缓存文件。在它被卸载后,也应该调用ldconfig命令再次更新动态连接器的缓存文件,以消除它对系统产生的影响。这个操作一定要在软件被安装之后或被卸载之后执行才有作用。那么最好的选择就是%post和%postun这两个段。因此会经常看到下面这样的代码:

 

%post -p /sbin/ldconfig

%postun -p /sbin/ldconfig

 

这两个例子在现有的大多数rpm包中,是十分常见。当然,也并不仅限于这样,丰富的例子还有很多,只是不能穷举罢了。至于实际当中应该如何应用,还是留给读者们自己去研究吧。

1.7 RPM小结

好了,有关RPM知识就到这里了。虽然还不够详尽,但是应该足够应付绝大多数的rpm打包工作了。如果想获得更为详尽的帮助,可以访问其官方网站。网址是:http://www.rpm.org。

目前最新的rpm版本是5,可是由于一些兼容性的原因,rpm5一直没有被广泛应用起来,即便Red Hat自家的Feodra也是如此。感兴趣的读者可以去它的官方网站查看更为详尽的信息。网址是:http://www.rpm5.org。

此外,很多读者可能比较希望了解一些deb软件包的打包方法。本书不准备做过多的介绍。其中一个最主要的原因是目前在企业应用中deb还没有流行起来;另外一个原因就是deb要比rpm容易很多,有兴趣的读者随便在网上找几篇文章就能搞定。



[1]Fedora 16中,dist变量的值是“.fc16”。


转自:http://www.atatech.org/articles/1538#

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值