哈少译的GNU Make指南

一、认识Makefile:

(1)规则的格式

一个简单的Makefile文件包含一系列的“规则”,其样式如下:

    目标(target)…: 依赖(prerequiries)…

    <tab>命令(command)

      …

      …

说明:

    <1>目标(target)通常是要产生的文件的名称。

        目标可以是可执行文件或OBJ文件,也可是一个执行的动作名称,比如:'clean'(详细内容请参阅假想目标一节)。

    <2>依赖(prerequiries)是用来输入从而产生目标的文件

        一个目标经常有几个依赖。

    <3>命令是Make执行的动作,一个规则可以含有几个命令,每个命令占一行。

        注意:每个命令行前面必须是一个Tab字符,即命令行第一个字符是Tab。这是不小心容易出错的地方。

(2)变量

    一般在文件开始定义:

        变量名 = 值1 值2 值3 ……

    使用变量:

        $(变量名)

(3)默认变量,也就是make不定义就可以使用的变量:

    AR:档案管理程序;缺省为:‘ar'.

    AS:汇编编译程序;缺省为:‘as'.

    CC:C语言编译程序;缺省为:‘cc'.

    CXX:C++编译程序;缺省为:‘g++'.

    CPP:带有标准输出的C语言预处理程序;缺省为:‘$(CC) -E'.

    RM:删除文件的命令;缺省为:‘rm -f'.

(4)自动变量,在写规则中的命令时使用,

    比如

    目标(target)…: 依赖(prerequiries)…

    <tab>命令(command)

    那么,以下符号出现在命令中时有特殊含义:

    $@:规则的目标文件名。对于有多个目标的格式规则,它指导致规则命令运行的目标文件名。

    $<:第一个依赖的文件名。如果目标更新命令来源于隐含规则,该变量的值是隐含规则添加的第一个依赖。

    $^:所有依赖的名字,名字之间用空格隔开。如果在依赖列表中,同一个文件名出现多次,则仅包含该文件名一次。

    $*:和隐含规则匹配的stem(径),stem的解释待会再说

(5)有以上的基本知识,可以看一个简单的Makefile:

    === makefile 开始 ===

    OBJS = foo.o bar.o

    CC = gcc

    CFLAGS = -Wall -O -g

    

    myprog : $(OBJS)

     $(CC) $(OBJS) -o myprog

    

    foo.o : foo.c foo.h bar.h

     $(CC) $(CFLAGS) -c foo.c -o foo.o

    

    bar.o : bar.c bar.h

     $(CC) $(CFLAGS) -c $< -o $@

    === makefile 结束 ===

   

    说明:

    <1>开始三行定义变量:OBJS、CC、CFLAGS

    <2>这里有三条规则,而第一条一般作为默认规则,也就是说如果在shell命令行中只输入make的话就从这里开始

    <3>第一条规则是说:要去编译foo.o和bar.o,编译完后再执行gcc foo.o bar.o -o myprog从而生成myprog文件

    <4>第二条规则是说如何编译foo.o,它执行命令:gcc -Wall -O -g -c foo.c -o foo.o

    <5>第三条规则比第二条复杂一点点:$<是指bar.c,$@是指bar.o,所以命令是:gcc -Wall -O -g -c bar.c bar.o

二、较高级的知识

(1)什么时候更新目标文件?

    Make程序根据Makefile文件中的数据和每个文件更改的时间戳决定哪些文件需要更新。对于这些需要更新的文件,Make基于Makefile文件发布命令进行更新,进行更新的方式由提供的命令行参数控制。

    比如上面那个简单的Makefile,make 从最上面开始,把上面第一个目的,‘myprog’,做为它的主要目标(一个它需要保证其总是最新的最终目标)。给出的规则说明只要文件‘myprog’ 比文件‘foo.o’或‘bar.o’中的任何一个旧,下一行的命令将会被执行。

    但是,在检查文件 foo.o 和 bar.o 的时间戳之前,它会往下查找那些把 foo.o 或 bar.o 做为目标文件的规则。它找到的关于 foo.o 的规则,该文件的依靠文件是 foo.c, foo.h 和 bar.h 。 它从下面再找不到生成这些依靠文件的规则,它就开始检查磁盘上这些依靠文件的时间戳。如果这些文件中任何一个的时间戳比 foo.o 的新,命令 'gcc -o foo.o foo.c' 将会执行,从而更新 文件 foo.o 。

(2)假想目标 (Phony Targets):假想目标并不是一个真正的文件名,它仅仅是您制定的一个具体规则所执行的一些命令的名称。

    应用一:

    all : exec1 exec2

    其中 exec1 和 exec2 是我们做为目的的两个可执行文件。 make 把这个 'all' 做为它的主要目的,每次执行时都会尝试把 'all' 更新。但既然这行规则里没有哪个命令来作用在一个叫 'all' 的 实际文件(事实上 all 并不会在磁盘上实际产生),所以这个规则并不真的改变 'all' 的状态。可既然这个文件并不存在,所以 make 总会尝试更新 all 规则,因此就检查它的依靠 exec1, exec2 是否需要更新,如果需要,就把它们更新,从而达到我们的目的。

   应用二:

   clean:

        rm *.o temp

   那么,在shell下输入'make clean'就会去执行rm *.o temp了,但如果你的磁盘上存在一个叫 clean 文件,会发生什么事?这时因为在这个规则里没有任何依赖文件,所以这个目的文件一定是最新的了(所有的依赖文件都已经是最新的了),所以既使用户明确命令 make 重新产生它,也不会有任何事情发生。解决方法是用 .PHONY标明假想目标,这就告诉 make 不用检查它们是否存在于磁盘上,也不用查找任何隐含规则,直接假设指定的目的需要被更新。在 makefile 里加入下面这行包含上面规则的规则:

    .PHONY : clean

(3)隐含规则

   允许make对一个目标文件寻找传统的更新方法,您所有做的是避免指定任何命令。可以编写没有命令行的规则或根本不编写任何规则。这样,make将根据存在的源文件的类型或要生成的文件类型决定使用何种隐含规则。

例如,假设makefile文件是下面的格式:

foo : foo.o bar.o

        cc -o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)

因为您提及了文件‘foo.o’,但是您没有给出它的规则,make将自动寻找一条隐含规则,该规则能够告诉make怎样更新该文件。无论文件‘foo.o’存在与否,make都会这样执行。然后make就会找到如下规则生成'foo.o':

Compiling C programs(编译C程序)

‘n.o' 自动由‘n.c' 使用命令 ‘$(CC) -c $(CPPFLAGS) $(CFLAGS)'生成 。

    所以,当使用隐含规则时,还要关注一下编译器额外标志变量,比较常用的有:

    ASFLAGS:用于汇编编译器的额外标志 (当具体调用‘.s'或‘.S'文件时)。

    CFLAGS:用于C编译器的额外标志。

    CXXFLAGS:用于C++编译器的额外标志。

    CPPFLAGS:用于C预处理以及使用它的程序的额外标志 (C和 Fortran 编译器)。

    LDFLAGS:用于调用linker(‘ld’)的编译器的额外标志。

(4)条件语句

   一个条件语句可以导致根据变量的值执行或忽略makefile文件中一部分脚本。条件语句可以将一个变量与其它变量的值相比较,或将一个变量与一字符串常量相比较。条件语句用于控制make实际看见的makefile文件部分,不能用于在执行时控制shell命令。

   下述的条件语句的例子告诉make如果变量CC的值是‘gcc’时使用一个数据库,如不是则使用其它数据库。它通过控制选择两命令行之一作为该规则的命令来工作。‘CC=gcc’作为make改变的参数的结果不仅用于决定使用哪一个编译器,而且决定连接哪一个数据库。

   libs_for_gcc = -lgnu

   normal_libs =

 

   foo: $(objects)

   ifeq ($(CC),gcc)

        $(CC) -o foo $(objects) $(libs_for_gcc)

   else

        $(CC) -o foo $(objects) $(normal_libs)

   endif

   该条件语句使用三个指令:ifeq、else和endif。

   Ifeq指令是条件语句的开始,并指明条件。它包含两个参数,它们被逗号分开,并被扩在圆括号内。运行时首先对两个参数变量替换,然后进行比较。在makefile中跟在ifeq后面的行是符合条件时执行的命令;否则,它们将被忽略。

   else指令指出:如果前面的条件失败将导致跟在其后面的命令执行。在上述例子中,意味着当第一个选项不执行时,和第二个选项连在一起的命令将执行。在条件语句中,else指令是可选择使用的。

   endif指令结束条件语句。任何条件语句必须以endif指令结束,后跟makefile文件中的正常内容。

   当变量CC的值是gcc,上例的效果为:

   foo: $(objects)

        $(CC) -o foo $(objects) $(libs_for_gcc)

   当变量CC的值不是gcc而是其它值的时候,上例的效果为:

   foo: $(objects)

        $(CC) -o foo $(objects) $(normal_libs)

   上例表明条件语句工作在原文水平:条件语句的行根据条件要么被处理成makefile文件的一部分或要么被忽略。这是makefile文件重大的语法单位(例如规则)可以跨越条件语句的开始或结束的原因。

(5)在目录中搜寻依赖

    <1>变量VPATH(注意是大写的)的值指定了make搜寻的目录。经常用到的是那些包含依赖的目录,并不是当前的目录;但VPATH指定了make对所有文件都适用的目录搜寻序列,包括了规则的目标所需要的文件。

    在VPATH变量定义中,目录的名字由冒号或空格分开。目录列举的次序也是make 搜寻的次序。在MS-DOS、MS-WINDOWS系统中,VPATH变量定义中的目录的名字由分号分开,因为在这些系统中,冒号用为路径名的一部分(通常在驱动器字母后面)。例如:

    VPATH = src:../headers

指定了两个目录,‘src’和‘…/headers’,make也按照这个次序进行搜寻。

    <2>vpath指令(注意字母是小写)和VPATH变量类似,但却更具灵活性。vpath指令允许对符合一定格式类型的文件名指定一个搜寻路径。这样您就可以对一种格式类型的文件名指定一个搜寻路径,对另外格式类型的文件名指定另外一个搜寻路径。总共由三种形式的vpath指令:

    vpath pattern directories 对一定格式类型的文件名指定一个搜寻路径,搜寻的路径VPATH变量定义要搜寻的路径格式一样。

    vpath pattern 清除和一定类型格式相联系的搜寻路径。

    vpath 清除所有前面由vapth指令指定的搜寻路径。

(6)静态格式规则与径stem

   静态格式规则的语法格式如下:

   targets ...: target-pattern: dep-patterns ...

          commands

          ...

   目标列表指明该规则应用的目标。目标可以含有通配符,具体使用和平常的目标规则基本一样

   目标的格式和依赖的格式是说明如何计算每个目标依赖的方法。从匹配目标格式的目标名中依据格式抽取部分字符串,这部分字符串称为径。将径分配到每一个依赖格式中产生依赖名。

   每一个格式通常包含字符‘%’。目标格式匹配目标时,‘%’可以匹配目标名中的任何字符串;这部分匹配的字符串称为径;剩下的部分必须完全相同。如目标‘foo.o’匹配格式‘%.o’,字符串‘foo’称为径。而目标‘foo.c’和‘foo.out’不匹配格式。

   每个目标的依赖名是使用径代替各个依赖中的‘%’产生。如果一个依赖格式为‘%.c’,把径‘foo’代替依赖格式中的‘%’生成依赖的文件名‘foo.c’。在依赖格式中不包含‘%’也是合法的,此时对所有目标来说,依赖是相同的。

   如果一个目标为‘dir/a.foo.b',目标格式规则为:‘a.%.b' ,则stem为‘dir/foo'。在构建相关文件名时stem 十分有用。   在静态格式规则中,stem是匹配目标格式中字符‘%’的文件名中那一部分。在一个没有stem具体规则中;变量‘$*' 不能以该方法设置。如果目标名以一种推荐的后缀结尾(参阅过时的后缀规则),变量‘$*'设置为目标去掉该后缀后的部分。例如,如果目标名是‘foo.c',则变量‘$*' 设置为‘foo', 因为‘.c' 是一个后缀。GNU make 处理这样奇怪的事情是为了和其它版本的make兼容。在隐含规则和静态格式规则以外,您应该尽量避免使用变量‘$*'。在具体规则中如果目标名不以推荐的后缀结尾,则变量‘$*’在该规则中设置为空值。

   下面是两个例子:

例一:

files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c

        $(CC) -c $(CFLAGS) $< -o $@

$(filter %.elc,$(files)): %.elc: %.el

        emacs -f batch-byte-compile $<

在这个例子中,‘$(filter %.o,$(files))'的结果是‘bar.o lose.o',第一个静态格式规则是将相应的C语言源文件编译更新为OBJ文件,‘$(filter %.elc,$(files))' 的结果是‘foo.elc',它由‘foo.el’构造。

例二:在静态格式规则中使用‘$*’

bigoutput littleoutput : %output : text.g

        generate text.g -$* > $@

当命令generate执行时,$*扩展为径,即‘big’或‘little’二者之一。

(7)格式规则与径stem

   格式规则是在目标中包含字符‘%’(只有一个)的规则,其它方面看起来和普通规则相同。目标是可以匹配文件名的格式,字符‘%’可以匹配任何非空的字符串,而其它字符仅仅和它们自己相匹配。

   例如‘%.c’匹配任何以‘.c’结尾的文件名;‘s.%.c’匹配以‘s.’开始并且以‘.c’结尾的文件名,该文件名至少包含5个字符(因为‘%’至少匹配一个字符)。匹配‘%’的子字符串称为stem(径)。依赖中使用‘%’表示它们的名字中含有和目标名相同的stem。要使用格式规则,文件名必须匹配目标的格式,而且符合依赖格式的文件必须存在或可以创建。下面规则:

%.o : %.c ; command...

表明要创建文件‘n.o’,使用‘n.c’作为它的依赖,而且文件‘n.c’ 必须存在或可以创建。

在格式规则中,依赖有时不含有‘%’。这表明采用该格式规则创建的所有文件都是采用相同的依赖。这种固定依赖的格式规则在有些场合十分有用。格式规则的依赖不必都包含字符‘%’,这样的规则是一个有力的常规通配符,它为任何匹配该目标格式规则的文件提供创建方法。

(8)函数

   函数调用和变量引用类似,它的格式如下:

   $(function arguments)

或这样:

   ${function arguments}

   'function'是函数名,是make内建函数列表中的一个。当然您也可以使用创建函数call创建的您自己的函数。

   'arguments'是该函数的参数。参数和函数名之间是用空格或Tab隔开,如果有多个参数,它们之间用逗号隔开。

    <1>$(subst from,to,text)

    在文本‘text’中使用‘to’替换每一处‘from’。例如:$(subst ee,EE,feet on the street)结果为‘fEEt on the street’。

   

    <2>$(patsubst pattern,replacement,text)

    寻找‘text’中符合格式‘pattern’的字,用‘replacement’替换它们。

    这里‘pattern’中包含通配符‘%’,它和一个字中任意个数的字符相匹配。如果‘replacement’中也含有通配符‘%’,则这个‘%’被和‘pattern’中通配符‘%’匹配的文本代替。例如:$(patsubst %.c,%.o,x.c.c bar.c)的结果为:‘x.c.o bar.o'。

    替换引用是实现函数patsubst功能一个简单方法:

    $(var:pattern=replacement)等同于:$(patsubst pattern,replacement,$(var))

    来看一个例子:怎样使用函数subst和patsubst告诉C编译器在相同路径列表中搜寻头文件?(假设变量VPATH=src:../headers)

    首先,函数subst将冒号变为空格:$(subst :, ,$(VPATH))这产生值‘src ../headers'。

    然后,函数patsubst为每一个路径名加入‘-I’标志,$(patsubst %,-I%,$(subst :, ,$(VPATH))),产生文本为‘-Isrc -I../headers’

    最后,将这些路径加到变量CFLAGS中:override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))结果是在以前给定的变量CFLAGS的值后追加文本‘-Isrc -I../headers’。Override指令的作用是即使以前使用命令参数指定变量CFLAGS的值,新值也能起作用。

    <3>$(filter pattern...,text)

    返回在‘text’中由空格隔开且匹配格式‘pattern...’的字,对于不符合格式‘pattern...’的字移出。格式用‘%’写出,和前面论述过的函数patsubst的格式相同。

    函数filter可以用来变量分离类型不同的字符串。例如:

    sources := foo.c bar.c baz.s ugh.h

    foo: $(sources)

         cc $(filter %.c %.s,$(sources)) -o foo

    表明‘foo' 依靠‘foo.c',‘bar.c',‘baz.s' 和‘ugh.h';但仅有‘foo.c',‘bar.c' 和 ‘baz.s' 指明用命令编译。

    <4>$(filter-out pattern...,text)

    返回在‘text’中由空格隔开且不匹配格式‘pattern...’的字,对于符合格式‘pattern...’的字移出,即函数filter的剩下的那些。

    例如:

    objects=main1.o foo.o main2.o bar.o

    mains=main1.o main2.o

    下面产生不包含在变量‘mains’中的OBJ文件的文件列表:

    $(filter-out $(mains),$(objects))

    <5>$(wildcard pattern)

    参数‘pattern’是一个文件名格式,典型的包含通配符(和shell中的文件名一样)。函数wildcard的结果是一列和格式匹配的且文件存在的文件名,文件名之间用一个空格隔开。如果没有和指定格式一致的文件,则函数wildcard的输出将会省略。注意这和在规则中通配符扩展的方式不同,在规则中使用逐字扩展方式,而不是省略方式。

    使用函数wildcard得到指定目录下所有的C语言源程序文件名:

    $(wildcard *.c)

    我们可以把所获得的C语言源程序文件名的字符串通过将‘.c’后缀变为‘.o’转换为OBJ文件名的字符串,其格式为:

    $(patsubst %.c,%.o,$(wildcard *.c))

    这样,一个编译特定目录下所有C语言源程序并把它们连接在一起的makefile文件可以写成如下格式:

    objects := $(patsubst %.c,%.o,$(wildcard *.c))

    foo : $(objects)

        cc -o foo $(objects)

    这里使用了编译C语言源程序的隐含规则,因此没有必要为每个文件写具体编译规则。‘:=’是‘=’的变异。

    <6>$(addprefix prefix,names...)

    参数‘names’作为一系列的文件名,文件名之间用空格隔开;prefix作为一个单位。将preffix(前缀)的值附加在每一个独立文件名的前面,完成后将文件名串联起来,它们之间用单个空格隔开。例如:$(addprefix src/,foo bar)结果为‘src/foo src/bar’。

(9)简单扩展变量

    使用是 := 的作用是立即把定义中参考到的函数和变量都展开。如果使用 = 的话,函数和变量参考会留在那儿,就是说改变一个变量的值会导致其它变量的值也被改变。例如:

    

    A = foo

    B = $(A)

    # 现在 B 是 $(A) ,而 $(A) 是 'foo' 。

    A = bar

    # 现在 B 仍然是 $(A) ,但它的值已随着变成 'bar' 了。

    B := $(A)

    # 现在 B 的值是 'bar' 。

    A = foo

    # B 的值仍然是 'bar' 。

三、分析一个复杂的Makefile:它可以不经修改地用在大部分项目里。

    主要把它用在 djgpp 上,是一个 DOS 版的 gcc 编译器。因此你可以看到执行的命令名、 'alleg' 程序包、 和 RM -F 变量都反映了这一点。看懂了的话,恭喜你,你可以对绝大多数Makefile进行分析了。

    

    === makefile 开始 ===   

    # 用户设定

    #

    # 如果需要,调整下面的东西。 EXECUTABLE 是目标的可执行文件名, LIBS

    # 是一个需要连接的程序包列表(例如 alleg, stdcx, iostr 等等)。当然你

    # 可以在 make 的命令行覆盖它们,你愿意就没问题。

    #

    

    EXECUTABLE := mushroom.exe

    LIBS := alleg

    

    # 现在来改变任何你想改动的隐含规则中的变量,例如

    

    CFLAGS := -g -Wall -O3 -m486

    CXXFLAGS := $(CFLAGS)

    

    # 下面先检查你的 djgpp 命令目录下有没有 rm 命令,如果没有,我们使用

    # del 命令来代替,但有可能给我们 'File not found' 这个错误信息,这没

    # 什么大碍。如果你不是用 DOS ,把它设定成一个删文件而不废话的命令。

    # (其实这一步在 UNIX 类的系统上是多余的,只是方便 DOS 用户。 UNIX

    # 用户可以删除这5行命令。)

   

    ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)

        RM-F := rm -f

    else

        RM-F := del

    endif

    

    # 从这里开始,你应该不需要改动任何东西。

    

    #选出所有*.c、*.cc文件

    SOURCE := $(wildcard *.c) $(wildcard *.cc)

    #将SOURCE中的扩展名都变为*.o

    OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))

    #将SOURCE中的扩展名都变为*.d

    DEPS := $(patsubst %.o,%.d,$(OBJS))

    #指出还未生成的*.d文件

    MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))

    #指出还未处理头文件并成*.d的源文件假定以*.c或*.cc结尾

    MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \

    $(patsubst %.d,%.cc,$(MISSING_DEPS)))

    #从源码文件 .c 或 .cc 中生成的*.d 的文件

    CPPFLAGS += -MD

    .PHONY : everything deps objs clean veryclean rebuild

    

    everything : $(EXECUTABLE)

    

    deps : $(DEPS)

    

    objs : $(OBJS)

    

    clean :

     @$(RM-F) *.o

     @$(RM-F) *.d

    

    veryclean: clean

     @$(RM-F) $(EXECUTABLE)

    

    rebuild: veryclean everything

    

    ifneq ($(MISSING_DEPS),)

    $(MISSING_DEPS) :

       #把相应的.o文件从磁盘上删除,从而使得 make 重建它。因为 CPPFLAGS 指定了 -MD , 它的.d 文件也被重新产生。

       @$(RM-F) $(patsubst %.d,%.o,$@)

    endif

    

    -include $(DEPS)

    

    $(EXECUTABLE) : $(OBJS)

     gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))

    

    === makefile 结束 ===