善用GNU Make做开发

发表于:2007-07-04来源:作者:点击数: 标签:
在编写小型的 Linux 应用程序时,一般情况下只会有少数几个源文件。这样 程序员 能够很容易地理清它们之间的包含和引用关系。但随着软件项目逐渐变大,对源文件的处理也将变得越来越复杂起来。此时单纯依赖手工方式进行管理的做法就显得有些力不从心了。为此
在编写小型的Linux应用程序时,一般情况下只会有少数几个源文件。这样程序员能够很容易地理清它们之间的包含和引用关系。但随着软件项目逐渐变大,对源文件的处理也将变得越来越复杂起来。此时单纯依赖手工方式进行管理的做法就显得有些力不从心了。为此,Linux专门为软件开发提供了一个自动化管理工具GNUmake。通过它,程序员可以很方便地管理软件编译的内容、方式和时机,从而使程序员能够把主要精力集中在代码的编写上。

    make将整个软件项目的代码分开放在几个小的源文件里,在改动其中一个文件的时候,可以只对该文件重新进行编译,然后重新连接所有的目标文件。对于那些由许多源文件组成的大型软件项目来说,全部重新进行编译需要花费很长的时间,而采用这种项目管理方法则可以极大地提高工作效率,让原本复杂繁琐的开发工作变简单。

 
    Makefile文件

    GNUmake是一个用来控制软件构建过程的自动工具,程序员通过定义构建规则来控制代码的创建过程。这些规则通常定义在一个名为Makefile的文件中。Makefile被用来告诉make编译哪些文件、怎样编译和何时编译。Makefile中的每条规则事实上都包含如下一些内容:
    ◆ 目标(target)是make最终需要创建的对象;
    ◆ 依赖(dependency)通常是一个列表,指明编译目标时需要用到的其它文件;
    ◆ 命令(command)也是一个列表,指明从依赖文件创建出目标对象所需要执行的命令。

    虽然Makefile中的目标通常都是可执行程序,但事实上可以是诸如文本文件和HTML页面等任何内容,甚至能够用来测试或设置环境变量。Makefile中的命令则不仅可以是编译命令,还可以是任何Shell命令。

    先来看一个例子。假设整个软件项目是由control.c、io.c和main.c三个源文件所构成的,编写的Makefile文件内容如下:

all : program
program : control.o ui.o main.o
gcc -o program control.o ui.o main.o
control.o : control.c
gcc -Wall -c -o control.o control.c
ui.o : ui.c
gcc -Wall -c -o ui.o ui.c
main.o : main.c
gcc -Wall -c -o main.o main.c
clean :
rm -f program *.o


    在将上述Makefile文件与源文件保存到同一目录之后,就可以在命令行中输入“make”命令来编译整个项目了。make在执行过程中,首先会查找到Makefile文件第一条规则中的目标,即上述文件中的all。根据设定好的规则,该目标需要依赖于program。由于all并不是一个已经存在的文件,所以每次在make被调用的时候,显然都需要先检查program。继续往下不难发现,program目标是依赖于control.o、ui.o和main.o的。这就意味着如果其中任何一个比生成的可执行文件要新,那么就需要重新构建可执行文件program,否则就没有必要执行这一步了。

    在Makefile文件的其余部分,为每一个中间生成的目标文件都专门定义了一条规则,用来指明创建过程中它们与C源文件的依赖性。也就是说,如果一个特定的C源文件被更新了,那么与之对应的目标文件也必须重新生成。下面是make在构建项目过程中的输出结果:

#make
gcc -Wall -c -o control.o control.c
gcc -Wall -c -o ui.o ui.c
gcc -Wall -c -o main.o main.c
gcc -o program control.o ui.o main.o


    不难看出,首先是C源文件被编译成目标文件,然后才是目标文件被连接成最终的可执行文件。由于相互间依赖关系的制约,这些步骤会被有条不紊地依次执行。最终可执行文件要求目标文件都被更新过,而每个目标文件则要求C源文件被更新过。如果此时重新执行“make”命令,会出现下面的结果。原因是程序已经被编译过了,并且没有做过任何改动,所以就没有再编译的必要了:

# make
make: Nothing to be done for 'all'.


    如果只是改变了其中的部分文件,那么make会自动检测出需要对哪些源文件重新进行编译,并连接成最后的可执行文件。用户可以参考下面的过程:

#touch main.c
# make
gcc -Wall -c -o main.o main.c
gcc -o program control.o ui.o main.o


    当make检测到main.o目标时,发现main.c文件已经被更新,于是main.o文件必须被重新编译,相应地program需要被重新连接。make的魅力就在于能够自动进行条件检测,并采取适当的行动。它永远也不会去编译那些没有改动过的源文件,因此大大节省了在开发大型软件项目时所浪费在编译上的时间。

    变量

    为了简化Makefile的编写,make引入了变量。变量实际上是为文本串在Makefile中定义一个便于记忆的名称。变量的定义和应用与Linux的环境变量一样,变量名大写,变量一旦定义之后,就可以通过将变量名用圆括号包起来,并在前面加上“$”符号来进行引用。

    变量一般都在Makefile的头部定义。如果变量的值发生了改变,很显然只需在一个地方进行修改就可以了,从而大大简化了Makefile的维护。下面是将前面用到的Makefile利用变量进行改写后的结果:

OBJS = control.o ui.o main.o
CC = GCC
CFLAGS = -Wall
all : program
program : $(OBJS)
$(CC) $(OBJS) -o program
control.o : control.c
$(CC) $(CFLAGS) -c -o control.o control.c
ui.o : ui.c
$(CC) $(CFLAGS) -c -o ui.o ui.c
main.o : main.c
$(CC) $(CFLAGS) -c -o main.o main.c
clean :
rm -f program $(OBJS)


    make将其使用的变量细分为两类:递归展开变量和简单展开变量。递归展开变量在被引用时会逐层展开,即如果在展开式中包含了对其它变量的引用,则这些变量也会被展开,直到没有需要被展开的变量为止。假设变量TOPDIR和SUBDIR的定义如下:

TOPDIR = /home/xiaowp
SUBDIR = $(TOPDIR)/project


    此时变量SUBDIR的值在解析时会被正确地展开为/home/xiaowp/project,但对于下面的定义:

TOPDIR = /home/xiaowp
SUBDIR = $(TOPDIR)/project
SUBDIR = $(SUBDIR)/src


    很清楚,希望得到的结果是/home/xiaowp/project/src,但实际并非如此。SUBDIR在引用时会被递归展开,从而陷入一个无限循环当中,make能够检测到这个问题并报告如下错误:
    *** Recursive variable 'SUBDIR' references itself (eventually). Stop

    为了避免这个问题,可以使用简单展开变量。与递归展开变量在引用时展开不同,简单展开变量是在定义处展开的,并且只展开一次,从而消除了变量的嵌套引用。在定义时,其语法与递归展开变量有细微的不同:

TOPDIR = /home/xiaowp
SUBDIR := $(TOPDIR)/project
SUBDIR += /src


    SUBDIR在第一次定义时使用“:=”将其值设置为“/home/xiaowp/project”,而在第二次定义时则使用“+=”在已有的基础上添加“/src”,这样就使得SUBDIR的最终值变为“/home/xiaowp/project/src”。许多程序员在Makefile中只使用简单展开变量,以避免可能出现的错误。

    除了用户自定义变量之外,在Makefile中还可以使用环境变量、自动变量和预定义变量。使用环境变量的方法相对来讲比较简单,make在启动时会自动读取系统当前已经定义了的环境变量,并且会创建与之具有相同名称和数值的变量。需要注意的是,如果用户在Makefile中定义了相同名称的变量,那么用户自定义变量将会覆盖同名的环境变量。

表1 常用自动变量
javascript:window.open(this.src);" style="CURSOR: pointer" onload="return imgzoom(this,550)">


    此外,make还提供了一些预定义变量和自动变量,但它们看起来都不如自定义变量那么直观。之所以称为自动变量是因为make会自动用特定的、熟知的值来替换它们,表1给出了常用的部分自动变量。

    利用make的自动变量和预定义变量,可以简化前面给出的那个Makefile文件:

OBJS = control.o ui.o main.o
CC = GCC
CFLAGS = -Wall
all : program
program : $(OBJS)
$(CC) $(OBJS) -o $@
control.o : control.c
$(CC) $(CFLAGS) -c -o $@ $<
ui.o : ui.c
$(CC) $(CFLAGS) -c -o $@ $<
main.o : main.c
$(CC) $(CFLAGS) -c -o $@ $<
clean :
$(RM) program $(OBJS)


    伪目标

    在Makefile中,并不是所有的目标都对应于磁盘上的文件。有的目标存在只是为了形成一条规则,从而完成特定的工作,并不生成新的目标文件,这样的目标称为伪目标。它并不是真正意义上的目标文件,只是为了满足Makefile的语法规则而存在的。

    在已经给出的Makefile文件中,最后一个目标clean就是伪目标。它规定了make应该执行的命令。当make处理到目标clean时,会先查看其对应的依赖对象。由于clean没有任何依赖对象,所以make会认为该目标是最新的而不会执行任何操作。为了编译这个目标体,必须手工执行如下命令:# make clean

    作为惯例,clean目标一般用于删除最终生成的可执行文件和在编译过程中产生的所有目标文件。问题是,如果恰巧有一个名为clean的文件存在时该怎么办呢?此时因为在这个规则里没有任何依赖对象,所以目标文件肯定是最新的,规则中的命令无论如何也不会被执行,即使用命令“makeclean”也无济于事。解决这一问题的方法是标明该规则中的目标是伪目标,并不对应于任何文件。这可以通过.PHONY目标实现。它告诉make不检查规则的目标文件是否存在于磁盘上,也不查找任何隐含规则,而直接假设指定的目标需要被更新就行了。在使用了.PHONY之后,前面的给出的Makefile文件就将变为如下的内容:

OBJS = control.o ui.o main.o
CC = GCC
CFLAGS = -Wall
all : program
program : $(OBJS)
$(CC) $(OBJS) -o $@
control.o : control.c
$(CC) $(CFLAGS) -c -o $@ $<
ui.o : ui.c
$(CC) $(CFLAGS) -c -o $@ $<
main.o : main.c
$(CC) $(CFLAGS) -c -o $@ $<
.PHONY : clean
clean :
$(RM) program $(OBJS)


    其它规则

    除了可以在Makefile中明确指定规则(显示规则)之外,make还维护了一整套隐式规则。隐式规则可以在用户没有完整地给出某些命令的时候,自动执行恰当的操作。隐式规则最大的好处是可以简化Makefile的编写和维护,例如前面给出的Makefile运用隐式规则后可以简化为如下内容:

OBJS = control.o ui.o main.o
program : $(OBJS)
$(CC) $(OBJS) -o $@
.PHONY : clean
clean :
$(RM) program $(OBJS)


    默认目标program依赖于control.o、ui.o和main.o三个目标文件,但Makefile中并没有给出怎样编译生成这些目标的规则。此时make就会使用隐式规则,对每一个名为foo.o的目标文件,找到与之对应的源代码foo.c,然后使用“gcc -cfoo.c -o foo.o”命令来生成对应的目标文件。

    除了系统预定义的隐式规则外,在Makefile中还可以定义自己的隐式规则,这种规则也被称为模式规则。模式规则类似于普通规则,但它的目标必须含有“%”这一通配符,以便能与任何非空字符相匹配,与目标对应的依赖文件中也必须使用通配符,例如下面的规则:

%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@


    上面的规则将告诉make所有形为foo.o的目标文件,都应该根据指定的命令从源文件foo.c编译而来。

    小结

    在构建大型的软件项目时,make是一个优秀的持续集成工具。它对于软件开发过程来讲非常重要。本文介绍了基本的make命令,以及如何编写简单实用的Makefile文件,相信用户已经能够使用make来管理软件项目的创建和维护过程了。(T111)


原文转自:http://www.ltesting.net