本文向Linux新手介绍一种无价的资源,Larry Wall的patch程序。
patch是用来查找文件之间差异的GNU diff命令的一个接口;diff有很多选项,但是该命令最常用的用途是用来生成一个文件,该文件中列出了内容发生改变的行,显示两个原始文件、修改过的行以及由于内容没有变化而忽略掉的行。patch典型地用于把一个目录下的源代码文件更新到新的版本,从而就避免了下载整个新的源代码档案的必要。下载一个有效的patch仅仅需要下载发生变化的那些代码行就可以了。
patch最初源自十年前,那时网络带宽的限制促进了patch的发展,然而和当时的很多Unix工具一样,直到现在,patch还在广泛应用。在Dr. Dobb之旅的2月份的程序员杂志中,Larry Wall对早期的patch做了一些很有趣的说明:
patch和diff哪个出现的早?
从很长一段时间来说,diff出现地比较早。我想diff大约比patch早10年出现,一回想起来,我就纳闷为什么没有人早些想到使用patch呢?
但是我想我知道这中间的原因。这很大程度上是心理因素使然。当开发出diff时,程序员增加了一个e选项,我想就是这个选项的原因,该选项后来滋生为一个ed脚本,因此大家都会对自己说,"嗯,如果我想自动使用diff,那么我就使用这个选项。"因此从来都没有人编写一个计算机程序来获取其它格式的输出并使用这些结果。或者是那些设计diff的人员,或者是那些使用diff格式而受益的人员太沉迷其中了,因为你可以对那些已经修改过的内容使用diff操作并让这些内容正常工作都是很容易的。
现在回想起来,这个问题是显而易见的。但是平心而论,与其说这是一个天才的灵感的闪现,还不如说这是自信心的体现。我开发出rn的第一个版本,然后继续为它编写补丁,这整个事情就是一团乱麻。你不可能强制用户使用补丁,因为他们可以手工完成这些工作。因此,他们就会省略一些自己认为不必要的工作,他们把新的修改加诸于原来的程序之上,因此而使得程序混乱。我编写补丁,这样就没有人找借口说这很难了。
我不清楚是否事实就是如此,但是多年以来,我一直对别人讲patch对于计算机文化的影响比rn和Perl的影响都要大。现在Internet的速度比原来有大幅度的提高,把整个发行版本分散到世界各地也变得更加简单,似乎只有在开发者之间才需要传送补丁。我已经很多年没有传送Perl的patch工具包了。我认为虽然patch整体上的重要性在逐渐降低,但是仍然是开发者交流思想的一种方法。但是就那一段时间而言,patch真正在相当大的程度上都影响了软件的开发方式。
Larry Wall针对patch对于计算机业界总体上重要性正在降低的评价可能是正确的,但是在自由软件世界中,patch仍然是一种必不可少的工具。无处不在的patch使得新手和非程序员能够简单地参与软件的alpha测试和beta测试,这对于整个计算机业界是十分有益的。
在我留意到在Linux内核邮件列表中会周期性的出现这样一件事情时就产生了写这篇文章的念头。大约每三个月就会有人张贴要求把Linux内核源代码的发行版本独立出来的文章,据说这是因为有些人只对i386的代码和IDE的磁盘驱动感兴趣,他们并不想为每个内核发行版本都下载Alpha、Sparc等等的文件和众多的SCSI驱动程序。这篇文章后面紧跟的是一些耐心的回复文章(有些文章则并没有耐心),大部分文章都是在讨论原来的使用有关patch来更新内核源代码。接着Linus Torvalds就会再次声明自己没有兴趣投身于这种把内核源程序切割成小块的繁杂的劳动,但是如果有人愿意,他们可以自由地开展这项独立的工程。到现在为止都没有志愿者出现。我并不想谴责那些内核黑客不能耐心等待,却把生活变得复杂;我猜想直接使用内核工作可能比检查整个内核的发行版本方案要更加有趣、更富有挑战性。下载11M的内核源程序包可是件非常耗费时间的事情(对于那些按照时间上网的人来说,这是很昂贵的),但是内核patch才只有几十K大小,很少会超过1M。我的硬盘上的2.1.99开发内核源程序经过patch的升级,已经升级到了2.1.119版本,我怀疑如果我紧随内核的发展而不断升级,那么也许我就要完整地下载每一个发行版本了。
使用patch
patch附带有一个很好的帮助,其中罗列了很多选项,但是99%的时间只要两个选项就能满足我们的需要:
patch -R < [patchfile] (used to undo a patch)
-p1选项代表patchfile中文件名左边目录的层数,顶层目录在不同的机器上有所不同。要使用这个选项,就要把你的patch放在要被打补丁的目录下,然后在这个目录中运行path -p1 < [patchfile]。来自Linux内核patch的一个简短的引用可以这样实现:
int swap_header_version;
int lock_map_size = PAGE_SIZE;
int nr_good_pages = 0; - char tmp_lock_map = 0; + unsigned long tmp_lock_map = 0;
应用来自本段中使用-p1开关拷贝的patch可以有效地减短patch定位的路径;patch会查找当前目录下一个名为/mm的子目录,接着应该会在这儿发现swapfile.c文件,然后等待打补丁。在这个过程中,以破折号(“-”号,译者注)开始的行会被一个以加号(“+”号,译者注)开始的行代替。一个典型的patch会包含对多个文件的更新,每个部分中都由对两个版本的文件运行diff -u命令的输出结果组成。
patch在操作时把自己的输出结果显示在屏幕上,但是这种输出通常都滚屏太快,来不及观看。原来准备patch的文件名为*.orig,新的patch文件会覆盖这个初始文件名。
打补丁的问题
使用不同版本的patch问题来源可能不同,所有的版本在网络上都是可用的。Larry Wall近年来已经不再做很多工作来更新patch了,这可能是由于他最后发行的一个版本在大部分情况下都能正常运行。最近几年以来,一直是GNU项目的FSF程序员发行新版本的patch。他们首先修订有问题的patch,但是我最近一直使用没有问题的2.5版本(这是Debian2.0的发行版本号)。过去,我的2.1版本也一直运行的很好。当前的GNU patch的版本可以从GNU FTP站点上获取,然而大部分人都只使用他们Linux发行版中所提供的版本。
让我们假定你已经对一个目录下的源程序文件进行了patch修补工作,但是patch并没有清晰地发挥作用。这可能会偶然发生,在打补丁的过程中会显示错误信息,其中带有行号,说明哪一个文件出现了问题。有时错误是很明显的,例如缺少了分号,这种错误可以不费多大力气就能改正。另外一种可能是从patch部分删除了产生问题的部分,但是这样根据所涉及到的文件的不同可能会正常工作,也可能不能正常工作了。
另外一种常见的错位为:假设你有一个未使用tar打包的内核源程序文件,在/linux/arch/下浏览各个子目录时你会发现各种机器体系结构子目录,例如alpah、sparc等等。如果你和大多数Linux用户一样,使用的是Intel的处理器(或者是Intel系列),你可以决定删除这些目录,这些目录对于编译你特殊的内核并不需要,只是白白占用了磁盘空间。一段时间之后发行了一个新的内核patch,此时试图进行patch操作,当它发现不能找到自己打补丁需要的Alpha或者PPC文件,就会停顿下来。幸运的是patch在这些地方允许用户参与,它会询问"Skip this patch?"回答"y",patch就可以按照正确的路径继续执行。也许你需要回答这个问题很多次,因此允许自己不需要的目录保留在磁盘上是一种很好的方法。
给内核打补丁的技巧
很多Linux用户使用patch都主要是给内核源程序打补丁,因此有一些技巧可以使用。可能最简单的方法是使用shell脚本给内核打补丁,这可以在内核源程序树中的/scripts子目录中找到。这种方便的、编写良好的脚本是由Nick Holloway在1995年编写的;两年以后,Adam Sulmicki增加了多种压缩格式的支持,包括*.bz、*.bz2、compress、gzip和无格式文本(也就是已经解压的patch)。这个脚本假定在你使用新版本的patch时,你的内核源程序是在/usr/src/linux目录中。这些缺省值可以通过这种格式的命令行开关覆盖:patch-kernel [sourcedir [patchdir] ]。如果任何一部分的patch失败,对内核打补丁的过程都会失败,但是如果patch清晰地起作用,它就会调用find,这会删除所有的patch留下的*.orig文件。
如果你准备查看命令的输出,或者可能你希望保留*.orig文件直到你确定打过补丁的源程序编译已经通过,按照我的经验,直接运行patch(正如前面介绍的一样,patch位于内核源程序的最高目录)是很可靠的。为了避免对patch进行解压,在使用之前,可以使用这样一个技巧:
或者
bzip2 -dc patchXX.bz2 | patch -p1
在使用patch之后,可以使用find程序来检测被拒绝的文件:
find . -name *.rej
第一次使用这个命令,语法可能有些不清楚。点号(“.”)说明find应该查找当前目录并递规查找当前目录之下的所有子目录。记住,点号前后都应该有一个空格。通配符"*"号前面的反斜线把星号转义出来,以免shell会搞混,星号是有其它意义的。如果find找到了任何的*.rej文件,它就会把文件名打印到屏幕上。如果没有任何输出find就退出了,那么就差不多能确定patch正确发挥作用了。
find的另外一个工作是删除*.orig文件:
这个命令敲起来相当麻烦,可以使用一个新的shell别名来代替这个命令。在你的~/.bashrc文件中类似这样的一行:
可以允许你只输入findorig就可以调用前面的命令。如果别名命令的定义中包含空格,那么就必须使用单引号。为了不用先退出再重新登陆就可以使用一个新的别名,可以在命令行中敲如~/.bashrc。
附加内容和结束语
在撰写本文时,我刚好把自己的机器从2.1版本使用patch升级到了2.5版本。这两个版本都是来自现在的FSF/GNU维护人员。马上我就注意到2.5版本默认的输出已经改变了,屏幕上显示的信息变少了。原来在patch检测进行修补的行号时显示的Larry Wall的"...hmm"不见了。2.5版本的输出只剩下诸如"patching file [filename]"之类的信息了,而没有早期版本显示的那么多信息了。无可否认,信息滚屏太快,根本无法阅读,但是输出可以重定向到一个文件中供以后使用。这种变化不会影响程序的功能,但是减少了人为的成分。在我看来,使用诸如原来的"...hmm"信息和源代码中的注释一样,对于提醒用户程序是显示执行的工作的结果是很有价值的,这就像人在呼吸一样,而不应该使用一些毫无结果的位集合。通过对patch命令行增加--verbose开关可以恢复原来的显示内容,但是我相信很多用户既不会注意到这个选项,也不会不辞辛劳地输入这个选项。2.1和2.5版本的另外一个不同是除非patch给出了-b选项,否则不能创建*.orig备份文件。
对于那些对软件和内核"前沿"bug报告测试和提供测试报告不感兴趣的人来说,patch并不是必须的,但是通常大部分Linux世界里有趣的开发都是属于这个范畴的。获得patch的使用方式并不困难,这种努力可以得到充分的回报。
介绍:补丁Patch是天才程序员、Perl的发明者Larry Wall发明的,它应高效地交流程序源代码之需求而生,patch这个概念已经成为开放源代码发起者、贡献者和参与者的集体无意识的一部分。
patch只包含了对源代码修改的部分,这对于开放源代码社区的协同开发模式具有重要意义,意味的软件新版本的发布和对软件的缺陷或改进可以以更小的文件发布,可以减少网络的传输量,方便软件维护者的管理工作。
patch文件有多种格式,不同平台上所支持的格式不尽相同,但最常见的是context格式和unified格式。context格式被广泛使用,是patch文件格式事实上的标准。该格式包含了差异部分及其邻近的若干行,邻近就是所谓的上下文,这些行虽然没有变化,但它们出现在patch文件使得还原patch的程序具备更强的容错性。unified格式常见于GNU的patch实现,以patch形式发布的linux内核就使用了该格式。此外,还有其它比较少用的格式,如Normal格式,并排对比模式(side-by-side),ed script和RCS script模式等。除了并排对比模式方便用户观察文件差异,其它格式大多数是为了兼容旧的patch格式。
patch的工具
patch有十分丰富的工具,在Unix/Linux系统下diff/patch是最主要的patch命令行工具。另外,一些版本控制软件TortoiseCVS、 Subversion, WinMerge及集成开发环境如Eclipse包含了patch生成、应用的功能。使用命令行工具生成在Unix/Linux系统下只需要使用diff命令,在Windows下可以安装Cygwin来模拟Unix环境,当然有时候Cygwin显得过于庞大,此时可以使用unxutils,它是常用的GNU命令在Windows系统上的移植版,其中包含了diff和patch命令。从http://unxutils.sourceforge.net下载该软件包后,将其usr\local\wbin目加到Path环境变量即可使用这些命令。
产生补丁
生成patch命令用diff命令,该命令的基本用法是:
diff [选项] 旧文件或目录 新文件或目录
该命令根据选项产生新旧文件或目录之间的patch,并把该patch输出到标准输出上。
选项用来指定输出的patch文件格式,如果比较的是目录是否需要递归,对只在一边目录中存在的文件的处理方法,如何处理空格、制表符、小写等;常用选项有
-c 表示产生context格式的patch
-u 表示产生unified格式的patch
-r 表示对目录进行递归操作,用于产生整个代码树的patch
-N 表示如果文件不存在则将其等价为空文件,这个用于产生有文件增加或删除的patch
接下来,需要指定比较的对象,它们可以是文件,也可以时目录。旧版本列在前面,新版本列在后面。当然,文件或目录也可以从标准输入获得。此时,文件或目录名称用减号"-"表示。实例子:
cat build.xml |diff -y -W 100 - build-1.10.xml
以每列100个字符的宽度并排对比输出build.xml和build-1.10.xml之间的差异到屏幕(标准输出)
diff -c web.xml web2.xml > web.xml.diff
产生web2.xml相对于web.xml的修改的上下文格式补丁文件到web.xml.diff中去
diff -crN src src_XFIRE > xfire-patch.diff
产生代码树src_XIRE相对于代码树src的上下文补丁文件到xfire-patch.diff中去,在src_XFIRE中新增的文件的内容也会被包含在补丁中。
打补丁
打补丁可以使用命令行工具patch。它的基本用法是:patch -pnum < 补丁文件。
打补丁时将工作目录改到需要打补丁源代码顶层目录,然后确定p后面的数字,该数字表示需要去掉的补丁文件中目录的层数,该数字和补丁创建时候工作目录和代码目录的相对位置有关,一般补丁的作者为在补丁文档中指明。如果没有指定,可以通过观察补丁文件中列出的文件完整路径和代码树中该文件所在相对路径得出。
例子:
patch -p1 < xfire-patch.diff
命令:patch
功能说明:修补文件。
语 法:patch [-bceEflnNRstTuvZ][-B <备份字首字符串>][-d <工作目录>][-D <标示符号>][-F <监别列数>][-g <控制数值>][-i <修补文件>][-o <输出文件>][-p <剥离层级>][-r <拒绝文件>][-V <备份方式>][-Y <备份字首字符串>][-z <备份字尾字符串>][--backup-if -mismatch][--binary][--help][--nobackup-if-mismatch][--verbose][原始文件 <修补文件>] 或 path [-p <剥离层级>] < [修补文件]
补充说明:patch指令让用户利用设置修补文件的方式,修改,更新原始文件。倘若一次仅修改一个文件,可直接在指令列中下达指令依序执行。如果配合修补文件的方式则能一次修补大批文件,这也是Linux系统核心的升级方法之一。
参 数:
-b或--backup 备份每一个原始文件。
-B<备份字首字符串>或--prefix=<备份字首字符串> 设置文件备份时,附加在文件名称前面的字首字符串,该字符串可以是路径名称。
-c或--context 把修补数据解译成关联性的差异。
-d<工作目录>或--directory=<工作目录> 设置工作目录。
-D<标示符号>或--ifdef=<标示符号> 用指定的符号把改变的地方标示出来。
-e或--ed 把修补数据解译成ed指令可用的叙述文件。
-E或--remove-empty-files 若修补过后输出的文件其内容是一片空白,则移除该文件。
-f或--force 此参数的效果和指定"-t"参数类似,但会假设修补数据的版本为新 版本。
-F<监别列数>或--fuzz<监别列数> 设置监别列数的最大值。
-g<控制数值>或--get=<控制数值> 设置以RSC或SCCS控制修补作业。
-i<修补文件>或--input=<修补文件> 读取指定的修补问家你。
-l或--ignore-whitespace 忽略修补数据与输入数据的跳格,空格字符。
-n或--normal 把修补数据解译成一般性的差异。
-N或--forward 忽略修补的数据较原始文件的版本更旧,或该版本的修补数据已使 用过。
-o<输出文件>或--output=<输出文件> 设置输出文件的名称,修补过的文件会以该名称存放。
-p<剥离层级>或--strip=<剥离层级> 设置欲剥离几层路径名称。
-f<拒绝文件>或--reject-file=<拒绝文件> 设置保存拒绝修补相关信息的文件名称,预设的文件名称为.rej。
-R或--reverse 假设修补数据是由新旧文件交换位置而产生。
-s或--quiet或--silent 不显示指令执行过程,除非发生错误。
-t或--batch 自动略过错误,不询问任何问题。
-T或--set-time 此参数的效果和指定"-Z"参数类似,但以本地时间为主。
-u或--unified 把修补数据解译成一致化的差异。
-v或--version 显示版本信息。
-V<备份方式>或--version-control=<备份方式> 用"-b"参数备份目标文件后,备份文件的字尾会被加上一个备份字符串,这个字符串不仅可用"-z"参数变更,当使用"-V"参数指定不同备份方式时,也会产生不同字尾的备份字符串。
-Y<备份字首字符串>或--basename-prefix=--<备份字首字符串> 设置文件备份时,附加在文件基本名称开头的字首字符串。
-z<备份字尾字符串>或--suffix=<备份字尾字符串> 此参数的效果和指定"-B"参数类似,差别在于修补作业使用的路径与文件名若为src/linux/fs/super.c,加上"backup/"字符串后,文件super.c会备份于/src/linux/fs/backup目录里。
-Z或--set-utc 把修补过的文件更改,存取时间设为UTC。
--backup-if-mismatch 在修补数据不完全吻合,且没有刻意指定要备份文件时,才备份文件。
--binary 以二进制模式读写数据,而不通过标准输出设备。
--help 在线帮助。
--nobackup-if-mismatch 在修补数据不完全吻合,且没有刻意指定要备份文件时,不要备份文件。
--verbose 详细显示指令的执行过程。