make命令以及makefile
make命令以及makefile
使用RCS与CVS进行源代码控制
编写手册页
使用patch与tar发布软件
开发环境
多源代码的问题
当我们编写小程序时,许多人都是简单的在编辑后通过重新编译所有的文件重新构建我们的程序。然而,对于大程序,这种简单构建方法的问题就变得明显了。编辑-编译-测试的循环时间将会变长。即使是最为耐心的程序员也会希望避免当只修改一个文件时而编译所有的文件。
当创建多个头文件并且在不同的源文件中包含多个头文件时就会出现一个更为困难的问题。假设我们有三个头文件a.h,b.h以及c.h,和C源文件main.c,2.c以及3.c。而我们会具有如下的包含关系:
/*
main.c */
#include “a.h”
...
/* 2.c */
#include “a.h”
#include
“b.h”
...
/* 3.c */
#include “b.h”
#include
“c.h”
...
如果程序员修改了c.h,文件main.c以及2.c并不需要重新编译,因为他们并不依赖于这个头文件。文件3.c依赖于c.h,所以如果c.h被修改了,那么他就应被重新编译。然而,如果b.h被修改了,而程序员忘记重新编译2.c,那么所得到的程序也许就不会正常工作。
make程序通过确保修改所影响的所有文件在需要时进行重新编译来解决所有这些问题。
make命令与Makefile
然而,正如我们所看到的,make命令有许多内建的知识,他本身并不知道如何构建我们的程序。我们必须提供一个文件来告诉make我们的程序是如何组织的。这个文件就是makefile。
makefile文件通常与工程的源文件位于同一个目录中。我们可以同时在我们机子上有多个不同的makefile文件。确实,如果我们有一个非常大的工程,我们也许会选择对于工程的不同部分使用单独的makefile文件进行管理。
make命令以及makefile文件的组合提供了一个强大的工具来进行工程的管理。他通常不仅用于控制源代码的编译,而且也用于准备手册页以及将程序安装到目标目录。
makefile文件的语法
一个makefile文件由一组依赖与规则组成。一个依赖具有一个目标(将要创建的文件)以及他所依赖的源文件集合。规则描述了由依赖文件如何创建目标文件。通常,目标文件是一个可执行文件。
makefile
文件由make命令来执行,从而决定目标文件或是要创建的文件,然后比较源文件的日期与时间用于决定调用哪条规则来构建目标。通常,在构建最终的目标之前必须创建一些中间目标。make命令使用makefile文件来确定目标构建的正确顺序以及规则调用的正确顺序。
make选项与参数
make程序本身有多个选项。最常用的三个为:
-k,这个选项会通知make命令,当发现一个错误时会继续执行,而不是在检测到第一个问题时立即退出。例如,我们可以使用这个选项检测出哪个文件编译失败。
-n,这个选项会使得make命令打印出此命令将会做的事情而不会实际执行。
-f
<filename>,这个选项会通知make命令使用哪个文件作为makefile文件。如果我们没有使用这个选项,make首先在会当前目录查找名为makefile的文件。如果这个文件不存在,他就会查找Makefile。按约定,Linux程序使用Makefile。这会使用
makefile文件在一个满是小写字母名字的目录中第一个出现。
要告诉make来构建一个特定目录,通常是一个可执行文件,我们可以将目标名字作为参数传递给make命令。如果没有这样做,make就会试着构建makefile文件中的第一个目标。许多程序在其makefile文件中将
all指定为第一个目标,然后列出其他的all所依赖的目标。这个约定可以很清晰的指出当没有指定目标时makefile应尝试构建的默认目标。我们建议遵守这个约定。
依赖
依赖指出最终程序中的每一个文件如何与源文件相关。在我们本章开始时所显示的例子中,我们指出我们最终的程序依赖于main.o,2.o以及3.o;类似的
main.o依赖于main.c与a.h等。所以main.会受到main.c和a.h修改的影响,如果这两个文件中的任何一个发生变化,都需要通过重新编译main.c来重新创建main.o。
在makefile文件中,我们编写规则的格式如下:目标名,冒号,空格或是tab,然后是以空格或是tab分隔的用于创建目标文件的列表。我们例子中的依赖列表如下:
myapp:
main.o 2.o 3.o
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
这表明myapp依赖于main.o,2.o以及3.o,而main.o依赖于main.c以及a.h,等等。
依赖集合指出了一个层次显示了源文件之间如何彼此相关。我们可以很容易的看出,如果b.h发生变化,那么我们需要重新修正2.o以及3.o,而2.o与3.o发生了变化,我们也需要重新构建myapp。
如果我们希望编构建多个文件,那么我们可以使用伪目标all。假设我们的程序由二进制文件myapp与手册页myapp.1构成。我们可以用下面的语句来指定目标:
all:
myapp
myapp.1
再一次声明,如果我们没有指定all目标,make只是简单的创建他在makefile中查找到的第一个目标。
规则
makefile
文件中的第二部分指定了用于描述如何创建一个目标的规则。在我们前面部分的例子中,在make命令之后应该使用哪个命令来重新构建2.o呢?看起来也许只是简单的使用gcc
-c
2.c就足够了(而且,正如我们在后面将会看到的,make确实知道许多默认的规则),但是如果我们需要指定一个include目录,或是设置用于以后调试的符号信息选项时应怎么办呢?我们可以通过在makefile文件中显示的指定规则来做到。
注:在这里我们需要提示makefile文件一个非常奇怪的语法:一个空格与一个tab之间有区别。所有规则所在的行必须以一个tab开始;而不能以一个空格开始。因为多个空格与一个tab看起来很相似,而且因为在绝大多数的其他的Linux程序中在空格与tab之间并没有区别,所以如果不加区分就会出现问题。同时,makefile文件中一行结束处的空格也会使得make命令失败。然而,这是一个历史问题,而且已经有许多的makefile文件在尝试改进这个问题,所以我们要小心。幸运的是,当
make命令不能工作时,通常一个很明显的问题就是丢失了tab。
试验--一个简单的Makefile
大多数规则由本可以在命令行输入的简单命令组成。对于我们的例子而言,我们可以创建我们的第一个makefile文件,Makefile1:
myapp:
main.o 2.o 3.o
gcc -o myapp main.o 2.o 3.o
main.o: main.c a.h
gcc
-c main.c
2.o: 2.c a.h b.h
gcc -c 2.c
3.o: 3.c b.h c.h
gcc -c
3.c
我们需要使用-f选项来调用我们的make命令,因为我们的makefile文件并不是通常默认的makefile或是Makefile。如果我们在一个并不包含任何源码的目录中调用这个代码,我们就会得到下面的信息:
$
make -f Makefile1
make: *** No rule to make target ‘main.c’, needed by
‘main.o’.
$
make
命令认为makefile文件中的第一个目录myapp是我们希望创建的文件。然后他会查找其他的依赖,并且会确定需要一个名为main.c的文件。因为我们还没有创建这个文件,而makefile也并不知道如何创建这个文件,所以make命令就会报告错误。下面我们创建这些源文件并且再次尝试。因为我们对于结果并不感兴趣,所以这些文件可以非常简单。头文件实际上为空,所以我们使用touch命令来创建这些文件。
$
touch a.h
$ touch b.h
$ touch c.h
main.c
包含main函数,并且会调用function_two与function_three。其他的两个文件定义了function_two与
function_three。源文件包含#include行来指定头文件,所以他们会依赖所包含的头文件的内容。这并不是一个程序,但是我们在这里列出相应的部分:
/*
main.c */
#include <stdlib.h>
#include “a.h”
extern void
function_two();
extern void function_three();
int main()
{
function_two();
function_three();
exit (EXIT_SUCCESS);
}
/*
2.c */
#include “a.h”
#include “b.h”
void function_two() {
}
/*
3.c */
#include “b.h”
#include “c.h”
void function_three()
{
}
下面我们再试一次:
$ make
gcc -c
gcc -c
gcc -c
gcc
-o
$
这就是一次成功的make。
工作原理
make
命令处理makefile文件中的依赖部分,并且确定需要创建的文件以及创建的顺序。尽管我们首先列出了如何创建myapp,make可以确定创建文件的正确顺序。然后他会调用我们在这些规则中所指定的创建这些文件的命令。make命令会在执行相应的命令时显示这些命令。现在我们可以测试我们的
makefile文件来确定他是否正确的处理了b.h的修改。
$ touch b.h
$ make -f Makefile1
gcc
-c 2.c
gcc -c 3.c
gcc -o myapp main.o 2.o
3.o
$
make命令已经读入我们的makefile文件,确定重新构建myapp所需要的最小数量的命令,并且以正确的顺序执行这些命令。下面我们来看一下如果我们删除一个目标文件时会发生什么:
$
rm 2.o
$ make -f Makefile1
gcc -c 2.c
gcc -o myapp main.o 2.o
3.o
$
再一次,make正确的确定了所需要的动作。
Makefile中的注释
makefile文件中的注释以#开始,并且直到本行结束。正如在C源文件中一样,makefile文件中的注释有助于作者也其他人更好的理解文件编写时所期望的作用。
Makefile中的宏
即使有make与makefile是管理多个源码文件工程的强大工具。然而,对于由大量的文件所组成的工程来说,他们仍显得庞大和不灵活。所以Makefile允许我们使用宏,从而我们可以将其写为更为通用的形式。
我们在makefile文件中以MACRONAME=value的形式定义一个宏,然后以$(MACRONAME)或是${MACRONAME}的形式来访问MACRONAME的值。一些版本的make也可以接受$MACRONAME的形式。我们也可以通过将=之后的部分留空来设置一个空的宏。
在makefile文件中,宏通常用于编译的选项。通常,当正在开发一个程序时,我们通常并不会使用优化选项来进行编译,但是却需要包含调试信息。而对于发布一个程序通常是相反的情况:发布一个运行速度尽量快而不带任何调试信息的二进制程序。
Makefile1
的另一个问题是他假定编译器为gcc。在其他的Unix系统上,我们也许会使用cc或是c89。如果我们希望将我们的makefile运行在其他版本的
Unix系统上,或是如果我们在我们的系统上使用一个不同的编译器,我们就需要修改makefile的多行来使其工作。宏是用于收集系统相关部分的好方法,从而可以很容易的进行修改。
宏通常是在makefile文件本身内部定义的,但是也可以通过带有宏定义的make命令来指定,例如
make
CC=c89。类似这样的命令行定义可以覆盖makefile文件中的定义。当在makefile文件外部使用时,宏定义必须作为一个单独的参数进行传递,所以要避免空格或是使用引号的形式:make
"CC =
c89"。
试验--带有宏的Makefile
下面是我们的makefile的一个修正版本,Makefile2,其中使用一些宏:
all:
myapp
# Which compiler
CC = gcc
# Where are include files
kept
INCLUDE = .
# Options for development
CFLAGS = -g -Wall -ansi
#
Options for release
# CFLAGS = -O -Wall -ansi
myapp: main.o 2.o 3.o
$(CC) -o myapp main.o 2.o 3.o
main.o: main.c a.h
$(CC) -I$(INCLUDE)
$(CFLAGS) -c main.c
2.o: 2.c a.h b.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c
2.c
3.o: 3.c b.h c.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c
3.c
如果我们删除我们原有的安装,而使用这个新的makefile来创建一个新的安装,我们会得到下面的信息:
$ rm *.o
myapp
$ make -f Makefile2
gcc -I. -g -Wall -ansi -c main.c
gcc -I. -g
-Wall -ansi -c 2.c
gcc -I. -g -Wall -ansi -c 3.c
gcc -o myapp main.o 2.o
3.o
$
工作原理
make程序使用合适的定义来替换$(CC),$(CFLAGS)以及$(INCLUDE),与带有#define形式的C编译器类似。现在如是我们需要修改编译命令,我们只需要修改makefile文件中的一行。
事实上,make有多个内部宏,从而我们可以更为简洁的来使用。在下表中,我们列出其中最常用的一些;我们会在后的例子中看到他们的使用。这些宏中的每一个仅会在他们刚要被使用时进行扩展,所以宏的语义会随着makefile文件的处理而变化。事实上,如果这些不以这样的方进行工作,他们就不会太大的作用。
$?
距离当前目标最近一次修改的需求列表
$@ 当前目标的名字
$< 当前需求的名字
$*
不带前缀的当前需求的名字
还有另外两个有用的特殊字符,也许我们会在makefile文件中的命令前遇到:
-选择make忽略所有错误。例如,如果我们希望创建一个目录,但是希望忽略错误,也许是因为这个目录已经存在了,我们只需要在mkdir命令之前带有一个负号。我们将会在后面的章节中看到-的使用。
@告诉make在执行命令之前不要将命令输出到标准输出。如果我们希望使用echo来显示一些指令时,这个字符就特别有用。
多个目标
通常需要构建多个目标文件,而不是一个目标文件,或者是在一个地方收集多个命令组。我们可以扩展我们的makefile文件来完成这些任务。下面我们添加一个"clean"选项来移除不需要的目标文件,以及一个"install"选项来将完成的程序移动到另一个不同的目录中。
试验--多个目标
下面是下一个版本的makefile,Makefile3。
all:
myapp
# Which compiler
CC = gcc
# Where to install
INSTDIR =
/usr/local/bin
# Where are include files kept
INCLUDE = .
# Options for
development
CFLAGS = -g -Wall -ansi
# Options for release
# CFLAGS = -O
-Wall -ansi
myapp: main.o 2.o 3.o
$(CC) -o myapp main.o 2.o
3.o
main.o: main.c a.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c main.c
2.o:
2.c a.h b.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c 2.c
3.o: 3.c b.h c.h
$(CC) -I$(INCLUDE) $(CFLAGS) -c 3.c
clean:
-rm main.o 2.o
3.o
install: myapp
@if [ -d $(INSTDIR) ]; \
then \
cp
myapp $(INSTDIR);\
chmod a+x $(INSTDIR)/myapp;\
chmod og-w
$(INSTDIR)/myapp;\
echo “Installed in $(INSTDIR)”;\
else
\
echo “Sorry, $(INSTDIR) does not exist”;\
fi
在这个makefile中需要注意多个地方。首先,特殊目标all仍然只是指定myapp作为目标。所以,当我们执行make命令而没有指定一个目标时,默认的行为就是构建目标myapp。
接下来需要特别注意的另外两个目标,clean与install。clean目标使用rm命令来移除目标文件。这个命令以-开头,这会使得make命令忽略命令的结果,所以make
clean总会成功,即使并没有目标文件而rm命令返回一个错误。目标"clean"并没有为clean指定任何所依赖的条件;clean之后的行是空的。所以这个目标总是被认为是最新的,并且如果clean被指定为一个目标,那么其规则总是被执行。
install目标依赖于
myapp,所以make会知道在执行其他命令运行install之前必须先创建myapp。install的规则由一些shell脚本命令所组成。因为
make会为执行规则而调用shell,并且第一条规则使用一个新的shell,所以我们必须添加反斜线,这样所有的脚本命令就全在一个逻辑行,并且会全部传递给一个单独的shell调用。这个命令以@开头,这会通知make命令在执行规则之前不要在标准输出上打印出命令。
install目标顺序执行命令将程序安装在其最终位置上。他在执行下一条命令之前并不会检测前一条命令是否执行成功。如果只有前一条命令成功执行之后才可以执行后续的命令,那么我们必须使用&&符号将其联合,如下所示:
@if
[ -d $(INSTDIR) ]; \
then \
cp myapp $(INSTDIR)
&&\
chmod a+x $(INSTDIR)/myapp && \
chmod og-w
$(INSTDIR/myapp && \
echo “Installed in $(INSTDIR)” ;\
else \
echo “Sorry, $(INSTDIR) does not exist” ; false ; \
fi
也许我们会回想起第2章的内容,这就是一个shell
"and"命令,而且其效果是只有前一条命令执行成功之后才会执行后续的命令。在这里我们并不会关心是否保证前一条命令执行成功,所以我们只是使用这种较为简单的形式。
也许我们作为一个普通用户并不具有在/usr/local/bin目录下安装程序的权限。我们可以修改makefile文件从而使用另一个不同的安装目录,或是改变这个目录的权限,或者是在执行make
install之前切换到root用户。
$ rm *.o myapp
$ make -f Makefile3
gcc -I. -g
-Wall -ansi -c main.c
gcc -I. -g -Wall -ansi -c 2.c
gcc -I. -g -Wall -ansi
-c 3.c
gcc -o myapp main.o 2.o 3.o
$ make -f Makefile3
make: Nothing to
be done for ‘all’.
$ rm myapp
$ make -f Makefile3 install
gcc -o myapp
main.o 2.o 3.o
Installed in /usr/local/bin
$ make -f Makefile3 clean
rm
main.o 2.o
3.o
$
工作原理
首先,我们删除myapp与所有的目标文件。make命令会使用目标all,从而构建myapp。接下来我们再次运行make玲,因为myapp已经是最新的了,所以make不会做任何事情。然后我们删除myapp并且运行make
install。这会重新构建这个二进制文件,并且将其拷贝到安装目录。最后我们运行make
clean,这会删除目标文件。
内建规则
到目前为止,我们已经在makefile文件中确切的指定了如何执行过程的每一步。事实上,makefile有大量的内建规则从而可以很大程度的简化makefile文件,特别是当我们有大量源文件的时候。下面我们创建foo.c,这是一个传统的Hello
World程序。
#include <stdlib.h>
#include <stdio.h>
int
main()
{
printf(“Hello World\n”);
exit(EXIT_SUCCESS);
}
不指定makefile文件,我们尝试使用make来编译。
$ make
foo
cc foo.c -o
foo
$
正如我们所看到的,make知道如何调用编译器,尽管在这种情况下,他选择cc而不是gcc(在Linux下这可以正常工作,因为通常cc链接到gcc)。有时,这些内建规则是推断规则(inference
rules)。默认的规则使用宏,所以通过为这些宏指定一个新值,我们可以改变默认的行为。
$ rm foo
$ make CC=gcc
CFLAGS=”-Wall -g” foo
gcc -Wall -g foo.c -o
foo
$
我们可以使用-p选项使得make打印出其内建规则。内建规则太多而不能在这里全部列出,但是下面是GNU版本的make的make
-p的简短输出,演示了其中的部分规则:
OUTPUT_OPTION = -o $@
COMPILE.c = $(CC) $(CFLAGS)
$(CPPFLAGS) $(TARGET_ARCH) -c
%.o: %.c
# commands to execute
(built-in):
$(COMPILE.c) $(OUTPUT_OPTION)
$<
我们现在可以通过指定构建目标文件的规则使用这些内建规则来简化我们的makefile文件,所以makefile文件的相关部分简化为:
main.o:
main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
后缀与模式规则
我们所看到的内建规则使用后缀进行工作(与Windows和MS-DOS的文件名扩展相类似),所以当指定一个带有扩展名的文件时,make知道应使用哪条规则来创建带有不同扩展名的文件。在这里最通常的规则就是由以.c为结尾的文件创建以.o为结尾的文件。这个规则就是使用编译器编译文件,但是并不链接源文件。
有时我们需要能够创建新规则。程序开发作者过去在一些源文件上需要使用不同的编译器进行编译:两个在MS-DOS下,以及
Linux下的gcc。要满足MS-DOS编译器的要求,C++源文件而不是C源文件,需要以.cpp为后缀进行命名。不幸的是,现在Linux下使用的
make版本并没有编译.cpp文件的内建规则。(他确实具有一个在Unix下更为常见的.cc的规则)
所以或者是为每一个单独的文件指定一个规则,或者是我们需要教给make一个新的规则来由以.cpp为扩展名的文件创建目标文件。假如我们在这个工程中有大量的源文件,指定一个新规则节省了大量的输入工作,并且使得在工程中添加一个新源文件更为容易。
要添加一个新的后缀规则,我们首先在makefile文件中添加一行,告诉make新的后缀;然后我们就可以使用这个新的后缀来编写一条规则。make使用下面的语法样式来定义一条通用的规则来由具有旧后缀的文件创建具有新后缀的文件:
.<old_suffix>.<new_suffix>:
下面是我们的makefile文件中一条新的通用规则的代码片段,用于将.cpp文件转换为.o文件:
.SUFFIXES:
.cpp
.cpp.o:
$(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c
$<
特殊依赖.cpp.o:告诉make接下来的规则用于将以.cpp为后缀的文件转换为以.o为后缀的文件。当我们编写这个依赖时,我们使用特殊的宏名,因为我们并不知道我们将要转换的实际文件名。要理解这条规则,我们只需要简单的回忆起$<会扩展为起始文件名(带有旧后缀)即可。注意,我们只是告诉
make如何由.cpp文件得到.o文件;make已经知道如何由一个目标文件获得二进制可执行文件。
当我们调用make时,他使用我们的新规则由bar.cpp获得bar.o,然后使用其内建规则由.o获得一个可执行文件。-xc++标记用于告诉gcc这是一个C++源文件。
在近些时候,make知道如何处理带有.cpp扩展名的C++源文件,但是当将一种文件类型转换为另一种文件类型时,这个技术是十分有用的。
更为旧的make版本包含一个对应的语法用来达到同样的效果,而且更好。例如,匹配规则使用通配符语法来匹配文件,而不是仅依赖于文件扩展名。
对于上面例子中与.cpp规则等同的模式规则如下:
%.cpp:
%o
$(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c
$<
使用make管理库
当我们正处理一个大型工程时,使用库来管理多个编译产品通常是比较方便的。库是文件,通常以.a为扩展名,包含一个目标文件的集合。make命令有一个处理库的特殊语法,从而使得他们更易于管理。
这个语法就是lib
(file.o),这就意味着目标文件file.o存储在库lib.a中。make具有一个内建的规则用于管理库,通常如下面的样子:
.c.a:
$(CC) -c $(CFLAGS) $<
$(AR) $(ARFLAGS) $@
$*.o
宏$(AR)与$(ARFLAGS)通常分别默认为命令ar与选项rv。这个简短的语法告诉make由一个.c文件得到.a库,他必须执行两条规则:
第一条规则是他必须编译源文件并且生成一个目标文件
第二条规则是使用ar命令来修改库,添加新的目标文件
所以,如果我们有一个库fud,包含文件bas.o,在第一条规则中$<被替换为bas.c。在第二条规则中,$@被替换为库fud.a,而$*被替换为bas。
试验--管理库
实际上,管理库的规则的使用是相当简单的。下面我们修改我们的程序,从而文件2.o与3.o保存在一个名为mylib.a的库中。我们的makefile文件需要一些小的修改,所以Makefile5如下所示:
all:
myapp
# Which compiler
CC = gcc
# Where to install
INSTDIR =
/usr/local/bin
# Where are include files kept
INCLUDE = .
# Options for
development
CFLAGS = -g -Wall -ansi
# Options for release
# CFLAGS = -O
-Wall -ansi
# Local Libraries
MYLIB = mylib.a
myapp: main.o
$(MYLIB)
$(CC) -o myapp main.o $(MYLIB)
$(MYLIB): $(MYLIB)(2.o)
$(MYLIB)(3.o)
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
clean:
-rm main.o 2.o 3.o $(MYLIB)
install: myapp
@if [ -d
$(INSTDIR) ]; \
then \
cp myapp $(INSTDIR);\
chmod a+x
$(INSTDIR)/myapp;\
chmod og-w $(INSTDIR)/myapp;\
echo
“Installed in $(INSTDIR)”;\
else \
echo “Sorry, $(INSTDIR) does
not exist”;\
fi
在这里需要注意我们是如何使用默认规则来完成大多数工作的。现在让我们来测试我们的新版本makefile文件。
$ rm -f
myapp *.o mylib.a
$ make -f Makefile5
gcc -g -Wall -ansi -c -o main.o
main.c
gcc -g -Wall -ansi -c -o 2.o 2.c
ar rv mylib.a 2.o
a -
2.o
gcc -g -Wall -ansi -c -o 3.o 3.c
ar rv mylib.a 3.o
a - 3.o
gcc
-o myapp main.o mylib.a
$ touch c.h
$ make -f Makefile5
gcc -g -Wall
-ansi -c -o 3.o 3.c
ar rv mylib.a 3.o
r - 3.o
gcc -o myapp main.o
mylib.a
$
工作原理
我们首先删除所有的目标文件以及库,并且允许make构建myapp,他通过编译并且在使用库链接main.o之前创建库,从而创建myapp。然后我们测试3.o的测试规则,他会通知make,如果c.h发生变动,那么3.c必须进行重新编译。他会正确的完成这些工作,在重新链接之前会编译3.c并且更新库,从而创建一个新的可执行文件myapp。
高级主题:Makefile与子目标
如果我们编写一个大工程,有时将组成库的文件由主文件分离并且存储在一个子目录中是十分方便的。使用make可以两种方法来完成这个任务。
首先,我们在此子目录可以有第二个makefile文件来编译文件,将其存储在一个库中,然后将库拷贝到上一层主目录。在高层目录中的主makefile文件然后有一条规则用于构建这个库,其调用第二个makefile文件的语法如下:
mylib.a:
(cd
mylibdirectory;$(MAKE))
这就是说我们必须总是尝试构建mylib.a。当make调用这条规则用于构建库时,他会进入子目录mylibdirectory,然后调用一个新的
make命令来管理库。因为这会调用一个新的shell,使用makefile的程序并不会执行cd命令。然而,所调用的用于执行规则构建库的shell
是在一个不同的目录中。括号可以保证他们都会在一个单独的shell中进行处理。
第二个方法是在一个单独的makefile文件中使用一些额外的宏。这些额外的宏是通过在我们已经讨论过的这些宏的基础上添加D(对目录而言)或是F(就文件而言)来生成的。然后我们可以用下面的规则来覆盖内建的.c.o前缀规则:
.c.o:
$(CC) $(CFLAGS) -c $(@D)/$(<F) -o
$(@D)/$(@F)
来在子目录中编译文件并且将目标文件留下子目录中。然后我们可以用如下的依赖与规则来更新当前目录中的库:
mylib.a:
mydir/2.o mydir/3.o
ar -rv mylib.a
$?
我们需要决定在我们自己的工程中我们更喜欢哪种方法。许多工程只是简单的避免具有子目录,但是这样会导致在源码目录中有大量的文件。正如我们在前面的概览中所看到的,我们在子目录中使用make只是简单的增加了复杂性。
GNU
make与gcc
如果我们正使用GNU make与GNU
gcc编译器,还有两个有趣的选项:
第一个就是make的-jN("jobs")选项。这会使用make同时执行N条命令。此时make可以同时调用多条规则,独立的编译工程的不同部分。依据于我们的系统配置,这对于我们重新编译的时候是一个巨大的改进。如果我们有多个源文件,尝试这个选项是很有价值的。通常而言,小的数字,例如-j3,是一个好的起点。如果我们与其他用户共享我们的机器,那么要小心使用这个选项。其他用户也许不会喜欢每次编译时我们启动大量的进程数。
另一个有用的选项就是gcc的-MM选项。这会产生一个适合于make的依赖列表。在一个具有大量源码文件的工程中,每一个文件都会包含不同的头文件组合,要正确的获得依赖关系是非常困难的,但是却是十分重要的。如果我们使用每一个源文件依赖于每一个头文件,有时我们就会编译不必须的文件。另一方面,如果我们忽略一些依赖,问题就会更为严重,因为我们会没有编译那些需要重新编译的文件。
试验--gcc
-MM
下面我们使用gcc的-MM选项来为我们的例子工程生成一个依赖列表:
$ gcc -MM main.c
2.c 3.c
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h
c.h
$
工作原理
gcc编译只是简单的以适于插入一个makefile文件中的形式输出所需要依赖行。我们所需要做的就是将输出保存到一个临时文件中,然后将其插入makefile文件中,从而得到一个完美的依赖规则集。如果我们有一个gcc的输出拷贝,我们的依赖就没有出错的理由。
如果我们对于makefile文件十分自信,我们可以尝试使用makedepend工具,这些执行与-MM选项类似的功能,但是会将依赖实际添加到指定的makefile文件的尾部。
在我们离开makefile话题之前,也许很值得指出我们并不是只能限制自己使用makefile来编译代码或是创建库。我们可以使用他们来自动化任何任务,例如,有一个序列命令可以使得我们由一些输入文件得到一个输出文件。通常"非编译器"用户也许适用于调用awk或是sed来处理一些文件,或是生成手册页。我们可以自动化任何文件处理,只要是make由文件的日期与时间信息的修改可以处理的。
发布软件
程序发布的主要问题就是要保证包含所有的文件以及确切的版本。幸运的是,网络程序社区已经发展出一个健壮的方法集合可以很多的解决这个问题。这些方法包括:
使用在所有的Unix机器上均可用的标准工具将所有的组件文件打包进入一个包文件中
控制软件包的版本号
包文件采用包含版本号的命名约定从而用户可以很容易分辨出他们正在处理的版本
包中子目录的使用可以保证当文件由包文件中解压出来时,他们就会位于一个单独的目录中,这样就不会弄混哪些包含在包文件中而哪些没有
这些方法的发展就意味着程序可以很容易并且可靠的发布。程序安装的简便性是另一个问题,因为他会依赖于程序与所要安装的系统,但是至少我们可以保证我们拥有合适的组件文件。
patch程序
当程序发布以后,几乎是不可避免的出现用户发现bug或是程序的作者希望执行程序增加或是更新的情况。当作者将程序作为二进制文件发布时,他们通常只是简单的传递新的二进制文件。有时(更为经常),提供者只是简单的放出程序的一个新版本,通常是晦涩引用以及关于程序修改内容的少量信息。
另一个方面,将我们的软件作为一个源码包进行发布是一个好主意,因为这可以允许用户知道我们是如何实现的以及如何使用这些特性。也可以允许用户检测程序实际所做的工作并且可以重用部分源代码。
然而,例如Linux内核这样几十兆压缩源代码的重量级程序,传输内核源码的更新集合将会占用大量的资源,而事实上,在每个版本之间只有很少一部分的源代码进行了修改。
幸运的,有一个实用程序可以解决这个问题:patch。他是由Larry
Wall编写的,他同时也编写了Perl程序语言。patch命令可以使得我们只发布两个版本之间相区别的部分,这样任何具有版本1文件以及一个由版本1
到版本2的区别文件的人就可以使用patch命令自己生成版本2文件。
如果我们由版本1文件开始,
This is file
one
line 2
line 3
there is no line 4, this is line 5
line
6
然后我们创建版本2文件,
This is file two
line 2
line 3
line
4
line 5
line 6
a new line 8
我们可以使用diff命令来创建一个区别列表:
$
diff file1.c file2.c > diffs
diffs文件包含如下内容:
1c1
< This is
file one
—
> This is file two
4c4,5
< there is no line 4, this
is line 5
—
> line 4
> line 5
5a7
> a new line
8
这实际上是将一个文件修改为另一个文件的编辑器命令集合。假设我们有文件file1.c以及diffs文件,我们可以使用patch命令来更新我们的文件:
$
patch file1.c diffs
Hmm... Looks like a normal diff to me...
Patching file
file1.c using Plan A...
Hunk #1 succeeded at 1.
Hunk #2 succeeded at
4.
Hunk #3 succeeded at
7.
done
$
现在patch命令就已经将文件file1.c修改为与file2.c相同的ywyr
patch还有一个小技巧:去补丁的能力。假如我们不喜欢这些更改并且退回我们原始的file1.c文件。没有问题,只需要再次使用patch命令,使用-R选项即可。
$
patch -R file1.c diffs
Hmm... Looks like a normal diff to me...
Patching
file file1.c using Plan A...
Hunk #1 succeeded at 1.
Hunk #2 succeeded at
4.
Hunk #3 succeeded at
6.
done
$
file1.c已经回退到其原始状态了。
patch还有许多其他的选项,但是通常很善于由其输入来确定我们正在尝试做什么,然后简单的完成这些事情。如果patch命令失败,他就会创建一个以.rej为扩展名包含不能进行补丁操作的部分。
当我们正在处理软件补丁时,使用diff
-c选项是一个好主意,这会生成一个"context
diff"。这会在每一个修改之后与之后提供几行文本,从而patch在应用补丁之前可以验证内容匹配,而补丁文件也更容易阅读。
其他的发布程序
Linux
程序与源码通常以包含版本号的名字,并且以.tar.gz或是.tgz的扩展名进行发布。这些是使用gzip压缩的TAR文件,也就是所熟知的
tarball。如果我们使用通常的tar,我们必须通过两步来处理这些文件。下面我们来为我们的程序创建一个gzip文件。
$ tar cvf
myapp-1.0.tar main.c 2.c 3.c *.h myapp.1
Makefile5
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
现在我们有一个TAR文件。
$
ls -l *.tar
-rw-r--r-- 1 neil
$
我们可以使用gzip压缩程序使其变得更小:
$
gzip myapp-1.0.tar
$ ls -l *.gz
-rw-r--r-- 1
neil
$
正如我们所看到的,其结果在尺寸上非常小。.tar.gz然后可以简单的重命名为.tgz扩展名:
$ mv
myapp-1.0.tar.gz
myapp_v1.tgz
以逗点和三个字符进行重命名的习惯似乎是对Windows软件的让步,因为与Linux和Unix不同,Windows很依赖正在处理的正确的扩展名。要得到我们的文件,我们可以解压并且由tar文件中释放这些文件:
$
mv myapp_v1.tgz myapp-1.0.tar.gz
$ gzip -d myapp-1.0.tar.gz
$ tar xvf
myapp-1.0.tar
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
使用GNU版本的tar,事情会变得更为简单--我们可以一步创建压缩归档:
$
tar zcvf myapp_v1.tgz main.c 2.c 3.c *.h myapp.1
Makefile5
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
我们也可以进行简单的解压:
$
tar zxvf
myapp_v1.tgz
main.c
2.c
3.c
a.h
b.h
c.h
myapp.1
Makefile5
$
如果我们希望知道归档的内容而不实际的进行解压,我们可以使用另一个不同的tar
ztvf选项来调用tar程序。
我们在前面使用tar作为例子,而并没有描述除必须的选项以外的其他选项。下面我们简单的看一下tar命令以及一些常用的选项。正如由例子中所看到的,基本语法为:
tar
[options] [list of
files]
列表中的第一个项目为目标,尽管我们只是处理文件,他可以处理设备。列表中的其他项目要添加到一个新的或是已存在的归档中,这依赖于我们所使用的选项。这个列表也可以包含目录,在这种情况下,所有子目录默认情况下都会包含在文件中。如果我们释放文件,没有必要指定名字,因为tar会保存完全路径。
在这一部分,我们使用了六个不同选项的组合:
c:创建一个新的归档
f:指定目标是一个文件而不是一个设备
t:列出一个归档中的内容而不实际释放
v:tar显示出处理消息
x:由归档中释放文件
z:在GNU
tar中使用gzip过滤归档
tar命令还有更多的选项允许我们对于命令的操作以及他所创建的文档进行更好的控制。查看tar手册页可以得到更为详细的信息。