开放源码的好处是人人都能修改源码,无论是修正错误还是增加新功能。基于开源项目的修改通常是以补丁(patch)的形式发布。其实补丁就是diff命令的输出。diff命令对两个文本文件进行比较,然后输出它们的差别。diff的输出有多种可选的格式,如context, ed, normal以及unified,这几种格式都可以用作补丁。下面我们来看一些实例。
比如我们有下面这段简单的程序hello.c:
#include<stdio.h>
int main(int argc, char* argv[]) {
printf("Hello, world!");
}
可是我们发现这个程序有个小小的问题:打印Hello, world!后没有换行。我们希望把它修改成下面的样子:
#include<stdio.h>
int main(int argc, char* argv[]) {
printf("Hello, world!\n");
}
如果我们为这个修改制作一个补丁,可以按下面的步骤进行:
1. 备份未修改的程序,将hello.c复制到hello.c.orig。为了保留文件的时间戳,用了-p参数。
$ cp -p hello.c hello.c.orig
2. 修改hello.c,给printf加上换行符“\n”。
3. 用diff命令生成补丁。由于diff命令输出到标准输出设备,我们将其重定向到hello.patch。参数-u用于指定unified输出格式。
$ diff -u hello.c.orig hello.c > hello.patch
请注意:在diff命令行上旧文件在前,新文件在后。
补丁文件hello.patch内容如下:
--- hello.c.orig 2010-11-23 10:12:09.725254213 +0800
+++ hello.c 2010-11-23 10:13:07.453254213 +0800
@@ -1,4 +1,4 @@
#include<stdio.h>
int main(int argc, char* argv[]) {
- printf("Hello, world!");
+ printf("Hello, world!\n");
}
这就是只包含单个文件的最简单的补丁。如果我们修改个多个文件,怎样才能把这些修改放在一个补丁文件里呢?其实我们可以用diff比较两个目录,请看下面的例子。
在old目录下有如下的hello.c程序:
old/hello.c
#include<stdio.h>
int main(int argc, char* argv[]) {
printf("Hello, world!\n");
}
我们希望把"Hello, world!\n"字符串定义为宏放到头文件里。在new目录下有修改后的hello.c以及新头文件hello.h。
new/hello.c
#include<stdio.h>
#include "hello.h"
int main(int argc, char* argv[]) {
printf("%s\n", GREETING);
}
new/hello.h
#ifndef _HELLO_H
#define GREETING "Hello, world!"
#endif
用diff比较两个目录时通常会使用-r参数,它告诉diff不仅要比较指定目录下的文件,还要比较其子目录,也就是要比较整个目录树。另外我们还会用-N参数,这个参数在增加或删除文件时特别有用,它使得diff把不存在的文件当作空文件处理。diff在比较目录时会把两个指定目录下相同路径及文件名的文件逐一进行比较。
$ diff -ruN old new > hello.patch2
hello.patch2:
diff -ruN old/hello.c new/hello.c
--- old/hello.c 2010-11-23 13:46:06.217254213 +0800
+++ new/hello.c 2010-11-23 13:48:05.557254212 +0800
@@ -1,4 +1,5 @@
#include<stdio.h>
+#include "hello.h"
int main(int argc, char* argv[]) {
- printf("Hello, world!\n");
+ printf("%s\n", GREETING);
}
diff -ruN old/hello.h new/hello.h
--- old/hello.h 1970-01-01 08:00:00.000000000 +0800
+++ new/hello.h 2010-11-23 13:48:05.557254212 +0800
@@ -0,0 +1,3 @@
+#ifndef _HELLO_H
+#define GREETING "Hello, world!"
+#endif
注意看这个补丁中old/hello.h的时间戳:
1970-01-01 08:00:00.000000000 +0800。这表明这个文件其实不存在。
下面讲讲如何打补丁。以hello.patch2为例,比如在old目录下打这个补丁:
方法一
$ cd old
$ patch -p1 < ../hello.patch2
patching file hello.c
patching file hello.h
$
或者
方法二
$ cd old
$ cat ../hello.patch2 | patch -p1
patching file hello.c
patching file hello.h
$
patch命令从标准输入设备上读入补丁,因此我们可以用输入重定向(方法一)或者管道(方法二)来为patch输入补丁。用管道的好处是我们可以方便的处理压缩的补丁,如下例所示:
$ bzcat /path/to/patch.bz2 | patch -p1
参数-p1的目的是使patch在匹配文件时去掉最前面的一层的目录。在上例中就是把路径old/hello.c变为hello.c。用这个参数通常是由于制作补丁时的顶层目录名与我们要打补丁的目标目录名不同。
在制作补丁的实际操作中我们往往会面临一个困难:通常在修改一个软件包时为了便于制作补丁,我们会把原始的软件包复制到一个新的目录下进行修改,然后用diff比较原始目录和新目录。但是我们通常会在新目录下进行编译/调试,这会产生不少额外的文件,而diff会把这些文件当作新文件加入补丁里。
为了避免这个问题,我用这样方法解决:直接在原始目录下进行修改、编译及调试,只是保证做到如下两点。
- 在修改任何文件之前先做一个备份:cp -p <filename> <filename>.orig
- 在增加任何新文件之前touch一个空文件:touch <filename>.orig
其中请用实际的文件名替换<filename>和<newfile>。比如要修改的文件是hello.c,则备份文件名是hello.c.orig。
在制作补丁是用下面的shell脚本。这个脚本会先找出所有以.orig结尾的文件,接着创建两个临时目录,一个放原始文件,一个放修改后的文件,然后用diff生成补丁。这个脚本应该在源码目录之外执行,临时目录和补丁文件会放在当前目录下。
mkpatch:
#!/bin/sh
if [ -z "$1" ] ; then
echo "USAGE: mkpatch <source dir>"
exit 1
fi
SRC_PATH="$1"
if [ ! -d "${SRC_PATH}" ] ; then
echo "FATAL: source directory '${SRC_PATH}' does not exist"
exit 1
fi
SRC_DIR=`basename "${SRC_PATH}"`
WORK_DIR=`pwd`
OLD_DIR="${WORK_DIR}/${SRC_DIR}.old"
NEW_DIR="${WORK_DIR}/${SRC_DIR}.new"
cd "${SRC_PATH}"
for f in `find . -name '*.orig' -a -type f` ;
do
dir=`dirname $f`;
file=`basename $f .orig`;
odir="${OLD_DIR}/$dir"
ndir="${NEW_DIR}/$dir"
mkdir -p "$odir";
mkdir -p "$ndir";
osize=`stat -c "%s" "$f"`;
if [ "x$osize" = "x0" ] ; then
echo "New file: $dir/$file"
else
echo "Modified file: $dir/$file"
cp -a "$dir/$file.orig" "$odir/$file";
fi
cp -a "$dir/$file" "$ndir";
done
cd "$WORK_DIR"
echo -n "Generating patch"
diff -ruN "${SRC_DIR}.old" "${SRC_DIR}.new" > "${SRC_DIR}.patch"
echo "done"