概述

makefile关系到了整个工程的编译规则。一个工程的源文件不计其数,并且按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像是一个shell脚本一样,其中也可以执行操作系统的命令。

makefile带来的好处就是——自动化编译。一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释Makefile中指令的命令工具。

关于程序的编译和链接

无论是C还是C++,首先要把源文件编译成中间代码文件,在Windows下面是 .obj文件,UNIX下面是 .o文件,也就是object file。这个动作叫做编译(compile),然后再把大量的object file合成执行文件,这个动作叫链接(link)。

编译时,编译器需要的是语法的正确,函数与变量的生命正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件。

链接时,主要是链接函数和全局变量。所以我们可以使用这些中间目标文件来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件。在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显的指出中间目标文件名,这对于编译很不方便。所以,我们要给中间目标文件打个包,在Windows下这种包叫做 库文件(library file),也就是 .lib文件,在Unix下是 Archive file,也就是 .a文件。

总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成可执行文件。在编译时,编译器只检测程序语法和函数、变量是否被声明。如果函数未被声明,编译器发出一个警告,但可以生成Object File。而链接的时候,链接器会在所有的Object File中寻找函数的实现,如果找不到,就会报出链接错误。

makefile介紹

makefile规则

1
2
<target> : <prerequisites>  
[tab]  <commands>  
  • target 可以是一个object file,也可以是一个执行文件,还可以是一个label。
  • prerequisites 生成该target所依赖的文件和/或 target
  • command 该target要执行的命令(任意的shell命令)

这是一个文件的依赖关系,也就是说,target这一个或多个目标文件依赖于prerequisites中的文件,其生成规则定义在command中。prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就要执行。

一个简单的例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
edit : main.o kbd.o command.o display.o \  
       insert.o search.o files.o utils.o    cc -o edit main.o kbd.o command.o display.o \        insert.o search.o files.o utils.o  
main.o : main.c defs.h  
    cc -c main.ckbd.o : kbd.c defs.h command.h  
    cc -c kbd.ccommand.o : command.c defs.h command.h  
    cc -c command.cdisplay.o : display.c defs.h buffer.h  
    cc -c display.cinsert.o : insert.c defs.h buffer.h  
    cc -c insert.csearch.o : search.c defs.h buffer.h  
    cc -c search.cfiles.o : files.c defs.h buffer.h command.h  
    cc -c files.cutils.o : utils.c defs.h  
    cc -c utils.cclean :  
    rm edit main.o kbd.o command.o display.o \       insert.o search.o files.o utils.o  

在这个makefile文件中,目标文件(target)包括:可执行文件edit和中间目标文件文件,依赖(prerequisites)就是冒号后面的.c文件和.h文件。每一个.o文件都有一组依赖文件,而这些.o文件又是执行文件edit的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的。

make中使用变量

在上面的例子中,我们看到.o文件的字符串被重复了两次,其实我们可以在makefile中使用变量,比如叫做objects,OBJECTS,objs,OBJS等都可以。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
objects = main.o kbd.o command.o display.o \  
          insert.o search.o files.o utils.o  
edit : $(objects)  
    cc -o edit $(objects)main.o : main.c defs.h  
    cc -c main.ckbd.o : kbd.c defs.h command.h  
    cc -c kbd.ccommand.o : command.c defs.h command.h  
    cc -c command.cdisplay.o : display.c defs.h buffer.h  
    cc -c display.cinsert.o : insert.c defs.h buffer.h  
    cc -c insert.csearch.o : search.c defs.h buffer.h  
    cc -c search.cfiles.o : files.c defs.h buffer.h command.h  
    cc -c files.cutils.o : utils.c defs.h  
    cc -c utils.cclean :  
    rm edit $(objects)  

make自动推导

GNU的make可以自动推导文件及文件依赖关系后面的命令,于是我们就没有必要去在每个.o文件后面都写上类似的命令。只要make看到一个.o文件,它就会自动的把.c文件加在依赖关系中。如果make找到一个whatever.o,那么whatever.c就会是whatever.o的依赖文件。并且cc -c whatever.c也会被推导出来(-c option says not to run the linker)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
objects = main.o kbd.o command.o display.o \  
        insert.o search.o files.o utils.o  
edit : $(objects)  
    cc -o edit $(objects)  
main.o : defs.h  
kbd.o : defs.h command.h  
command.o : defs.h command.h  
display.o : defs.h buffer.h  
insert.o : defs.h buffer.h  
search.o : defs.h buffer.h  
files.o : defs.h buffer.h command.h  
utils.o : defs.h  
  
.PHONY : clean  
clean :  
    rm edit $(objects)  

这种方法就是make的 隐晦规则。在上面的文件内容中,.PHONY表示clean是一个伪目标文件。

清空目标文件的规则

一般的风格是

1
2
clean:  
    rm edit $(objects)  

更加稳健的风格是

1
2
3
.PHONY : clean  
clean :  
    -rm edit $(objects)  

其中在rm命令前面加了一个减号表示也许某些文件出现了问题,但是不要管,继续做后面的事情。另外,clean一般都是放在文件的最后。

引用其他的Makefile

在Makefile中使用include关键字可以把其他的Makefile包含进来,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是

1
include <filename>  

filename可以包含路径和通配符。

在 include 前面可以有一些空字符,但是绝不能是 Tab 键开始。include 和 <filename> 可以用一个或多个空格隔开。举个例子,你有这样几个 Makefile:a.mk 、b.mk 、c.mk ,还有一个文件叫foo.make,以及一个变量 $(bar),其包含了 e.mk 和 f.mk ,那么,下面的语句:

1
include foo.make *.mk $(bar)  

等价于:

1
include foo.make a.mk b.mk c.mk e.mk f.mk  

make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。就好像 C/C++ 的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make 会在当前目录下首先寻找,如果当前目录下没有找到,那么,make 还会在下面的几个目录下找:

  • 如果 make 执行时,有 -I 或 –include-dir 参数,那么 make 就会在这个参数所指定的目录下去寻找
  • 如果目录<prefix>/include(一般是/usr/local/bin 或 /usr/include)存在的话,make也会去找

如果有文件没有找到的话,make 会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成 makefile 的读取,make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。如果你想让 make 不理那些无法读取的文件,而继续执行,你可以在 include 前加一个减号“-”。

1
-include <filename>  

make的工作方式

  1. 读入所有的 Makefile。
  2. 读入被 include 的其它 Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

1-5 步为第一个阶段,6-7 为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但 make 并不会完全马上展开,make 使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

书写规则

规则包含两个部分,一个是依赖关系,一个是生成目标的方法。

在 Makefile 中,规则的顺序是很重要的,因为,Makefile 中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让 make 知道你的最终目标是什么。一般来说,定义在Makefile 中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make 所完成的也就是这个目标。

在规则中使用通配符

1
2
clean:  
    rm -f *.o  

或者说

1
objects = *.o  

在这个例子中,表示通配符同样可以用在变量当中。并不是说 *.o会展开,不!objects的值就是 *.o。Makefile中的变量实质就是C/C++中的宏。如果想要通配符在变量中展开,也就是让objects的值是所有.o文件的集合,可以这样

1
objects := $(wildcard *.o)  

文件搜寻

在一些大的工程中,有大量的源文件,我们通常的做法是讲这许多的源文件分类,并存放在不同的目录中。当make需要去寻找文件的依赖关系的时候,你可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make自动去找。

Makefile文件中的特殊变量VPATH就是完成这个功能的。如果没有指明这个变量,make只会在当前目录中去寻找依赖文件和目标文件。如果定义了这个变量,那么make就会在当前目录找不到的情况下到所指定的目录下寻找文件。

1
VPATH = src:../headers  

上面的定义指定两个目录,src../headers,make会按照这个顺序进行搜索。目录由冒号 :分隔(当然,当前目录永远是最高优先搜索的地方)

伪目标

伪目标并不是一个文件,只是一个标签,由于 伪目标不是文件,所以make无法生成它的依赖关系和决定它是否执行。我们只有通过显示的指明这个目标才能让其生效。当然,伪目标的取名不能和文件名重名,不然就失去了伪目标的意义了。

为了避免和文件重名的情况,我们可以使用特殊的标记 .PHONY来显式的指明一个目标是伪目标,向make说明,不管是否有这个文件,这个目标就是伪目标。

伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。

1
2
3
4
5
6
7
8
9
all : prog1 prog2 prog3  
.PHONY : all  
  
prog1 : prog1.o utils.o  
    cc -o prog1 prog1.o utils.o  
prog2 : prog2.o  
    cc -o prog2 prog2.o  
prog3 : prog3.o sort.o utils.o  
    cc -o prog3 prog3.o sort.o utils.o  

上面的例子也说明,目标可以成为依赖,所以伪目标也可以成为依赖

1
2
3
4
5
6
7
8
.PHONY: cleanall cleanobj cleandiff  
  
cleanall : cleanobj cleandiff  
    rm program  
cleanobj :  
    rm *.o  
cleandiff :  
    rm *.diff  

多目标

静态模式

自动生成依赖性

书写命令

每条规则中的命令和操作系统Shell的命令行是一致的。make会按顺序一条一条的执行命令,每条命令的开头必须以Tab键开头,除非,命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略。但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令。

显示命令

通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用 @字符在命令行前,那么这个命令将不会被make显示出来,最具有代表性的一个例子是,我们用这个功能向屏幕显示一些信息。如:

1
@echo 正在编译XXX模块......  

当make执行时,会输出“正在编译XXX模块……”字串,但是不会输出命令。如果没有 @,那么make将输出:

echo 正在编译XXX模块......  
正在编译XXX模块......  

如果 make 执行时,带入 make 参数 -n 或 –just-print ,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。

而 make 参数 -s 或 –silent 或 –quiet 则是全面禁止命令的显示。

命令执行

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,需要你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令卸载良好,而应该写在一行上,用分号分隔。

1
2
exec:  
    cd /home/cosmos    pwd  

这种情况下执行 make exec时,cd没有作用,pwd会打印出当前的Makefile目录。

1
2
exec:  
    cd /home/cosmos; pwd  

这种情况下cd就起作用了,pwd会打印出 /home/cosmos

命令出错

每当命令运行完后,make会检测出每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将可能终止所有规则的执行。

有些时候,命令的出错并不表示就是错误的。例如 mkdir 命令,我们一定需要建立一个目录,如果目录不存在,那么 mkdir 就成功执行,万事大吉,如果目录存在,那么就出错了。我们之所以使用mkdir 的意思就是一定要有这样的一个目录,于是我们就不希望 mkdir 出错而终止规则的运行。为了做到这一点,忽略命令的出错,我们可以在 Makefile 的命令行前加一个减号 - (在 Tab 键之后),标记为不管命令出不出错都认为是成功的。如:

1
2
clean:  
    -rm -f *.o  

嵌套执行make

在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile。这样有利于让我们的Makefile更加简洁,更加易于维护。这个技术对于我们模块编译和分段编译都有很大的好处。

例如,我们有一个子目录叫 subdir,这个目录下有个 Makefile 文件,来指明了这个目录下文件的编译规则。那么我们总控的 Makefile 可以这样书写:

1
2
subsystem:  
    cd subdir && $(MAKE)  

其等价于:

1
2
subsystem:  
    $(MAKE) -C subdir  

定义 $(MAKE) 宏变量的意思是,也许我们的 make 需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入“subdir”目录,然后执行 make 命令。我们把这个 Makefile 叫做“总控 Makefile”,总控 Makefile 的变量可以传递到下级的 Makefile中(如果你显示的声明),但是不会覆盖下层的 Makefile 中所定义的变量,除非指定了 -e 参数。

如果你要传递变量到下级 Makefile 中,那么你可以使用这样的声明:

1
export <variable ...>;  

如果你不想让某些变量传递到下级 Makefile 中,那么你可以这样声明:

1
unexport <variable ...>;  

如:
示例一:

1
export variable = value  

需要注意的是,有两个变量,一个是 SHELL ,一个是 MAKEFLAGS ,这两个变量不管你是否 export, 其总是要传递到下层 Makefile 中,特别是 MAKEFLAGS 变量,其中包含了 make 的参数信息,如果我们执行“总控 Makefile”时有 make 参数或是在上层 Makefile 中定义了这个变量,那么 MAKEFLAGS变量将会是这些参数,并会传递到下层 Makefile 中,这是一个系统级的环境变量。但是 make 命令中的有几个参数并不往下传递,它们是 -C , -f , -h, -o 和 -W (有关 Makefile参数的细节将在后面说明),如果你不想往下层传递参数,那么,你可以这样来:

subsystem:  
    cd subdir && $(MAKE) MAKEFLAGS=```  
  
如果你定义了环境变量 MAKEFLAGS ,那么你得确信其中的选项是大家都会用到的,如果其中有 -t, -n 和 -q 参数,那么将会有让你意想不到的结果,或许会让你异常地恐慌。还有一个在“嵌套执行”中比较有用的参数,-w 或是 --print-directory 会在 make 的过程中输  
出一些信息,让你看到目前的工作目录。比如,如果我们的下级 make 目录是“/home/hchen/gnu/make”,如果我们使用 make -w 来执行,那么当进入该目录时,我们会看到:  
  

make: Entering directory ‘/home/hchen/gnu/make’.

  
而在完成下层 make 后离开目录时,我们会看到:  
  

make: Leaving directory ‘/home/hchen/gnu/make’

  
当你使用 -C 参数来指定 make 下层 Makefile 时,-w 会被自动打开的。如果参数中有 -s(--slient )或是 --no-print-directory ,那么,-w 总是失效的。  
  
### 定义命令包  
  
如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以 `define`开始,以 `endef`结束  
  
```Makefile  
define run-yacc  
yacc $(firstword $^)  
mv y.tab.c $@  
endef  

这里,run-yacc是这个命令包的名字,其不要和 Makefile 中的变量重名。在 define 和 endef中的两行就是命令序列。这个命令包中的第一个命令是运行 Yacc 程序,因为 Yacc 程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看吧。

1
2
foo.c: foo.y  
    $(run-yacc)  

我们可以看见,要使用这个命令包,我们就好像使用变量一样。在这个命令包的使用中,命令包 run-yacc中的 $^ 就是 foo.y ,$@ 就是 foo.c (有关这种以 $ 开头的特殊变量,我们会在后面介绍), make 在执行命令包时,命令包中的每个命令会被依次独立执行。

使用变量

在 Makefile 中的定义的变量,就像是 C/C++ 语言中的宏一样,他代表了一个文本字串,在 Makefile中执行的时候其会自动原模原样地展开在所使用的地方。其与 C/C++ 所不同的是,你可以在 Makefile中改变其值。在 Makefile 中,变量可以使用在“目标”, “依赖目标”,“命令”或是 Makefile 的其它部分中。

变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有 : 、# 、= 或是空字符(空格、回车等)。变量是大小写敏感的,“foo”、“Foo”和“FOO”是三个不同的变量名。传统的Makefile 的变量名是全大写的命名方式,但我推荐使用大小写搭配的变量名, 如MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。

有一些变量是很奇怪字串,如 $<$@ 等,这些是自动化变量,我会在后面介绍。

变量的基础

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。如果你要使用真实的 $字符,那么你需要用$$ 来表示。变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。先看一个例子:

1
2
3
4
objects = program.o foo.o utils.o  
program : $(objects)  
    cc -o program $(objects)  
$(objects) : defs.h  

变量会在使用它的地方严格的展开,就像C/C++中的宏一样,例如:

1
2
3
foo = c  
prog.o : prog.$(foo)  
    $(foo)$(foo) -$(foo) prog.$(foo)  

展开后得到

1
2
prog.o: prog.c  
    cc -c prog.c  

变量中的变量

在定义变量的值的时候,我们可以使用其他变量来构造变量的值,在Makefile中有两种方式来用变量定义变量的值。先看第一种方式,也就是简单的使用 = 号,在 = 左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。如:

1
2
3
4
5
6
foo = $(bar)  
bar = $(ugh)  
ugh = Huh?  
  
all:  
    echo $(foo)  

我们执行 make all将会打出变量$(foo)的值是Hug? 可见,变量是可以使用后面的变量来定义的。这种方式好在可以把变量的真实值推到后面来定义,但也有可能递归定义。

为了避免上面的用法,我们可以使用make中的另一种用变量来定义的方法。这种方法使用的是 :=操作符。

1
2
3
x := foo  
y := $(x) bar  
x := later  

其等价于

1
2
y := foo bar  
x := later  

这种方法前面的变量不能使用后面的变量,只能使用前面已经定义好了的变量。如果是这样的:

1
2
y := $(x) bar  
x := foo  

那么y的值是 bar,而不是 foo bar

1
2
nullstring :=  
space := $(nullstring) # end of line  

nullstring是一个Empty变量,其中什么也没有,而我们的space的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里的技术很管用,先用一个Empty变量来标明变量的值开始了,而后面用 #表示变量定义终止,这样我们可以定义出其值是一个空格的变量。如果我们这样定义一个变量:

1
dir := /foo/bar/    # directory to put frobs in  

dir这个变量的值是 /foo/bar ,注意这后面是有四个空格的。如果我们使用这样变量来指定别的目录——$(dir)/file,那就完蛋了。

还有一个比较有用的一个操作符是 ?=,先看示例

1
FOO ?= bar  

其含义是,如果FOO没有被定义过,那么变量FOO的值就是 bar,否则这条语句什么也不做,其等价于

ifeq ($(origin FOO), undefined)  
    FOO = barendif  

变量高级用法

变量值的替换

我们可以替换变量中的共有的部分,其格式是 $(var:a=b)或是 ${var:a=b},其意思是把变量 var中所有以a字符串结尾的a天换成b字串。这里的结尾意思是 空格结束符

1
2
foo := a.o b.o c.o  
bar := $(foo:.o=.c)  

$(bar)的值是 a.c b.c c.c

另一种变量替换的技术是以 静态模式定义的

1
2
foo := a.o b.o c.o  
bar := $(foo:%.o=%.c)  

这依赖于被替换字串中的相同模式,模式中必须包含一个 %字符,这个例子同样让 $(bar)的值是 a.c b.c c.c

把变量的值再当成变量

1
2
3
x = y  
y = z  
a := $($(x))  

这个例子中,$(a)的值就是 z

追加变量值

我们可以使用 +=操作符给变量追加值。

1
2
objects = main.o foo.o bar.o utils.o  
objects += another.o  

于是$(objects)值变成 main.o foo.o bar.o utils.o another.o。如果变量之前没有定义过,那么,+= 会自动变成 = ,如果前面有变量定义,那么 += 会继承于前次操作的赋值符。如果前一次的是 := ,那么 += 会以 := 作为其赋值符,如:

1
2
variable := value  
variable += more  

等价于

1
2
variable := value  
variable := $(variable) more  

但是如果是下面这种情况:

1
2
variable = value  
variable += more  

由于前次的赋值是 =,所以 += 也会以 = 来作为赋值,那么岂不会发生变量的递归定义?不用担心,make会自动帮我们解决这个问题。

override指示符

如果有变量是通常 make 的命令行参数设置的,那么 Makefile 中对这个变量的赋值会被忽略。如果你想在 Makefile 中设置这类参数的值,那么,你可以使用“override”指示符。其语法是:

1
2
override <variable>; = <value>;  
override <variable>; := <value>;  

当然,你还可以追加

1
override <variable>; += <more text>;  

多行变量

还有一种设置变量值的方法是使用 define 关键字。使用 define 关键字设置变量的值可以有换行,这有利于定义一系列的命令(前面我们讲过“命令包”的技术就是利用这个关键字)。define 指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以 endef 关键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其它变量。因为命令需要以 Tab键开头,所以如果你用 define 定义的命令变量中没有以 Tab 键开头,那么 make 就不会把其认为是命令。下面的这个示例展示了 define 的用法:

1
2
3
4
5
define two-lines  
   echo "hello world"   echo "hello houmin"endef  
  
all:  
   echo "hello cosmos"   ${two-lines}  

执行后

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
houmin@cosmos:~$ make all  
echo "hello cosmos"hello cosmosecho "hello world"hello worldecho "hello houmin"hello houmin```  
  
### 环境变量  
  
make 运行时的系统环境变量可以在 make 开始运行时被载入到 Makefile 文件中,但是如果Makefile 中已定义了这个变量,或是这个变量由 make 命令行带入,那么系统的环境变量的值将被覆盖。(如果 make 指定了“-e”参数,那么,系统环境变量将覆盖 Makefile 中定义的变量)  
因此,如果我们在环境变量中设置了 `CFLAGS` 环境变量,那么我们就可以在所有的 Makefile 中使用这个变量了。这对于我们使用统一的编译参数有比较大的好处。如果 Makefile 中定义了 CFLAGS,那么则会使用 Makefile 中的这个变量,如果没有定义则使用系统环境变量的值,一个共性和个性的统一,很像“全局变量”和“局部变量”的特性。当 make 嵌套调用时(参见前面的“嵌套调用”章节),上层 Makefile 中定义的变量会以系统环境变量的方式传递到下层的 Makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层 Makefile 传递,则需要使用 exprot 关键字来声明。(参见前面章节)  
  
当然,我并不推荐把许多的变量都定义在系统环境中,这样在我们执行不用的 Makefile 时,拥有的是同一套系统变量,这可能会带来更多的麻烦。  
  
### 目标变量  
  
前面我们所讲的在 Makefile 中定义的变量都是“全局变量”,在整个文件,我们都可以访问这些变量。当然,“自动化变量”除外,如 $< 等这种类量的自动化变量就属于“规则型变量”,这种变量的值依赖于规则的目标和依赖目标的定义。  
  
当然,我也同样可以为某个目标设置局部变量,这种变量被称为“Target-specific Variable”,它可以和“全局变量”同名,因为它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。  
  
其语法是:  
  
```Makefile  
<target ...> : <variable-assignment>;  
<target ...> : override <variable-assignment>  

<variable-assignment>可以是前面讲过的各种赋值表达式,如 =:+或是 ?=。第二个语法是针对make命令行带入的变量,或者是系统环境变量。这个特性非常有用,当我们设置了这样一个变量,这个变量会作用到由这个目标所引发的所有规则中。如:

1
2
3
4
5
6
7
8
9
prog : CFLAGS = -g  
prog : prog.o foo.o bar.o  
    $(CC) $(CFLAGS) prog.o foo.o bar.o  
prog.o : prog.c  
    $(CC) $(CFLAGS) prog.c  
foo.o : foo.c  
    $(CC) $(CFLAGS) foo.c  
bar.o : bar.c  
    $(CC) $(CFLG) bar.c  

在这个示例中,不管全局的 $(CFLAGS) 的值是什么,在 prog 目标,以及其所引发的所有规则中(prog.o foo.o bar.o 的规), $(CFLAGS) 的值都是 -g.

模式变量

在 GNU 的 make 中,还支持模式变量(Pattern-specific Variable),通过上面的目标变量中,我们知道,变量可以定义在某个目标上。模式变量的好处就是,我们可以给定一种“模式”,可以把变量定义在符合这种模式的所有目标上。

我们知道,make 的“模式”一般是至少含有一个 % 的,所以,我们可以以如下方式给所有以 .o 结尾的目标定义目标变量:

1
%.o : CFLAGS = -O  

同样,模式变量的语法和“目标变量”一样:

1
2
<pattern ...>; : <variable-assignment>;  
<pattern ...>; : override <variable-assignment>;  

override 同样是针对于系统环境传入的变量,或是 make 命令行指定的变量。

使用条件判断

示例

下面的例子,判断$(CC)是否为gcc,如果是的话,则使用GNU函数编译目标

1
2
3
4
5
6
7
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  

可见,在上面示例的这个规则中,目标 foo 可以根据变量 $(CC) 值来选取不同的函数库来编译程序。我们可以从上面的示例中看到三个关键字: ifeqelseendif

  • ifeq 的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。
  • else 表示条件表达式为假的情况。
  • endif 表示一个条件语句的结束,任何一个条件表达式都应该以 endif 结束。

当我们的变量 $(CC) 值是 gcc 时,目标 foo 的规则是:

1
2
foo: $(objects)  
$(CC) -o foo $(objects) $(libs_for_gcc)  

而当我们的变量 $(CC) 值不是 gcc 时(比如 cc ),目标 foo 的规则是:

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

当然,我们还可以把上面的那个例子写得更简洁一些:

1
2
3
4
5
6
7
8
9
libs_for_gcc = -lgnu  
normal_libs =  
  
ifeq ($(CC),gcc)  
    libs=$(libs_for_gcc)else  
    libs=$(normal_libs)endif  
  
foo: $(objects)  
    $(CC) -o foo $(objects) $(libs)  

语法

条件表达式的语法为:

1
2
3
<conditional-directive>  
<text-if-true>  
endif  

以及:

1
2
3
4
5
<conditional-directive>  
<text-if-true>  
else  
<text-if-false>  
endif  

其中 <conditional-directive> 表示条件关键字,如 ifeq 。这个关键字有四个。

第一个是我们前面所见过的 ifeq

1
2
3
4
5
ifeq (<arg1>, <arg2>)  
ifeq '<arg1>' '<arg2>'  
ifeq "<arg1>" "<arg2>"  
ifeq "<arg1>" '<arg2>'  
ifeq '<arg1>' "<arg2>"  

比较参数 arg1 和 arg2 的值是否相同。当然,参数中我们还可以使用 make 的函数。如:

1
2
3
ifeq ($(strip $(foo)),)  
<text-if-empty>  
endif  

这个示例中使用了 strip 函数,如果这个函数的返回值是空(Empty), 那么 <text-if-empty> 就生效。

第二个条件关键字是 ifneq 。语法是:

1
2
3
4
5
ifneq (<arg1>, <arg2>)  
ifneq '<arg1>' '<arg2>'  
ifneq "<arg1>" "<arg2>"  
ifneq "<arg1>" '<arg2>'  
ifneq '<arg1>' "<arg2>"  

其比较参数 arg1 和 arg2 的值是否相同,如果不同,则为真。和 ifeq 类似。

第三个条件关键字是 ifdef 。语法是:

1
ifdef <variable-name>  

如果变量 <variable-name> 的值非空,那到表达式为真。否则,表达式为假。当然, <variable-name>同样可以是一个函数的返回值。注意, ifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前
位置。还是来看两个例子:
示例一:

1
2
3
4
5
bar =  
foo = $(bar)  
ifdef foo  
    frobozz = yeselse  
    frobozz = noendif  

示例二:

1
2
3
4
foo =  
ifdef foo  
    frobozz = yeselse  
    frobozz = noendif  

第一个例子中, $(frobozz) 值是 yes ,第二个则是 no。

第四个条件关键字是 ifndef 。其语法是:
ifndef <variable-name>
这个我就不多说了,和 ifdef 是相反的意思。

<conditional-directive> 这一行上,多余的空格是被允许的,但是不能以 Tab 键做为开始(不然就被认为是命令)
。而注释符 # 同样也是安全的。else 和 endif 也一样,只要不是以 Tab 键开始就行了。

特别注意的是, make 是在读取 Makefile 时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把自动化变量(如 $@ 等)放入条件表达式中,因为自动化变量是在运行时才有的。而且为了避免混乱,make 不允许把整个条件语句分成两部分放在不同的文件中。

使用函数

make的运行

make的退出码

make命令执行后有三个退出码

  • 0 表示成功执行
  • 1 表示make运行时出现错误
  • 2 如果你使用了make的 -q选项,并且make使得这些目标不需要更新,那么返回2

指定Makefile

GNU make找寻默认的Makefile的规则是在当前目录下依次找三个文件 ———— GNUmakefilemakefileMakefile。其按顺序找这三个文件,一旦找到,就开始读取这个文件并且执行。

当前,我们也可以给make指令指定一个特殊名字的Makefile。要达到这个功能,我们要使用make的 -f或是 --file参数。例如,我们有个makefile的名字是 env.mk,那么我们可以让make来执行这个文件

1
make -f env.mk  

如果在 make 的命令行是,你不只一次地使用了 -f 参数,那么,所有指定的 makefile 将会被连在一起传递给 make 执行。

指定目标

一般来说,make 的最终目标是 makefile 中的第一个目标,而其它目标一般是由这个目标连带出来的。这是 make 的默认行为。当然,一般来说,你的 makefile 中的第一个目标是由许多个目标组成, 你可以指示 make,让其完成你所指定的目标。要达到这一目的很简单,需在 make 命令后直接跟目标的名字就可以完成(如前面提到的“make clean”形式)

任何在 makefile 中的目标都可以被指定成终极目标,但是除了以 - 打头,或是包含了 = 的目标, 因为有这些字符的目标,会被解析成命令行参数或是变量。甚至没有被我们明确写出来的目标也可以成为 make 的终极目标,也就是说,只要 make 可以找到其隐含规则推导规则,那么这个隐含目标同样可以被指定成终极目标。

有一个 make 的环境变量叫 MAKECMDGOALS,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。这个变量可以让你使用在一些比较特殊的情形下。比如下面的例子:

1
2
3
sources = foo.c bar.c  
ifneq ($(MAKECMDGOALS), clean)  
    include $(sources:.c=.d)endif  

基于上面的这个例子,只要我们输入的命令不是“make clean”, 那么 makefile 会自动包含“foo.d”和“bar.d”这两个 makefile。

使用指定终极目标的方法可以很方便地让我们编译我们的程序,例如下面这个例子:

.PHONY: all  
all: prog1 prog2 prog3 prog4  

从这个例子中,我们可以看到,这个 makefile 中有四个需要编译的程序——“prog1”,“prog2”, “prog3”和“prog4”,我们可以使用“make all”命令来编译所有的目标(如果把 all 置成第一个目标,那么只需执行“make”),我们也可以使用“make prog2”来单独编译目标“prog2”。

既然 make 可以指定所有 makefile 中的目标,那么也包括“伪目标”,于是我们可以根据这种性质来让我们的 makefile 根据指定的不同的目标来完成不同的事。在 Unix 世界中,软件发布时,特别是 GNU 这种开源软件的发布时,其 makefile 都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的 makefile 中的目标。

  • all: 这个伪目标是所有目标的目标,其功能一般是编译所有的目标
  • clean: 这个伪目标功能是删除所有被make创建的文件
  • install:这个伪目标功能视安装已经编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去
  • print: 这个伪目标功能是输出改变过的源文件
  • tar: 这个伪目标功能是把源程序打包备份
  • dist: 这个伪目标功能是创建一个压缩文件,一般是把tar文件压成gz文件或者bz2文件
  • TAGS: 这个伪目标功能是更新所有目标,以备完整地重编译使用
  • check和test: 一般用来测试makefile的流程

指定规则

有时候,我们不想让我们的 makefile 中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。于是我们可以使用 make 命令的下述参数:

  • -n, –just-print, –dry-run, –recon 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试 makefile 很有用处。
  • -t, –touch 这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make 假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
  • -q, –question 这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
  • -W <file>, –what-if=<file>, –assume-new=<file>, –new-file=<file> 这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make 会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

另外一个很有意思的用法是结合 -p 和 -v 来输出 makefile 被执行时的信息(这个将在后面讲述)。

make的参数

下面列举了所有 GNU make 3.80 版的参数定义。其它版本和产商的 make 大同小异,不过其它产商的 make 的具体参数还是请参考各自的产品文档。

  • -b, -m 这两个参数的作用是忽略和其它版本 make 的兼容性。
  • -B, --always-make 认为所有的目标都需要更新(重编译)
  • -C <dir>, --directory=<dir> 指定读取 makefile 的目录。如果有多个“-C”参数,make 的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make -C ~/test -C prog”等价于“make -C ~/test/prog”。
  • -debug[=<options>] 输出 make 的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是 <options> 的取值:
    • a: 也就是 all,输出所有的调试信息。(会非常的多)
    • b: 也就是 basic,只输出简单的调试信息。即输出不需要重编译的目标。
    • v: 也就是 verbose,在 b 选项的级别之上。输出的信息包括哪个 makefile 被解析,不需要被重编译的依赖文件(或是依赖目标)等。
    • i: 也就是 implicit,输出所以的隐含规则。
    • j: 也就是 jobs,输出执行规则中命令的详细信息,如命令的 PID、返回码等。
    • m: 也就是 makefile,输出 make 读取 makefile,更新 makefile,执行 makefile 的信息。
  • -d 相当于 --debug=a
  • -e, --environment-overrides 指明环境变量的值覆盖 makefile 中定义的变量的值。
  • -f=<file>, --file=<file>, --makefile=<file> 指定需要执行的 makefile。
  • -h, –help 显示帮助信息。
  • -i , --ignore-errors 在执行时忽略所有的错误。
  • -I <dir>, –include-dir=<dir> 指定一个被包含 makefile 的搜索目标。可以使用多个“-I”参数来指定多个目录。
  • -j [<jobsnum>], --jobs[=<jobsnum>] 指同时运行命令的个数。如果没有这个参数,make 运行命令时能运行多少就运行多少。如果有一个以上的“-j”参数,那么仅最后一个“-j”才是有效的。(注意这个参数在 MS-DOS 中是无用的)
  • -k, --keep-going 出错也不停止运行。如果生成一个目标失败了,那么依赖于其上的目标就不会被执行了。
  • -l <load>, --load-average[=<load>], -max-load[=<load>] 指定 make 运行命令的负载。
  • -n, --just-print, --dry-run, --recon 仅输出执行过程中的命令序列,但并不执行。
  • -o <file>, --old-file=<file>, --assume-old=<file> 不重新生成的指定的 <file>,即使这个目标的依赖文件新于它。
  • -p, --print-data-base 输出 makefile 中的所有数据,包括所有的规则和变量。这个参数会让一个简单的 makefile 都会输出一堆信息。如果你只是想输出信息而不想执行 makefile,你可以使用“make -qp”命令。如果你想查看执行 makefile 前的预设变量和规则,你可以使用“make –p –f /dev/null”。这个参数输出的信息会包含着你的 makefile 文件的文件名和行号,所以,用这个参数来调试你的 makefile 会是很有用的,特别是当你的环境变量很复杂的时候。
  • -q, --question 不运行命令,也不输出。仅仅是检查所指定的目标是否需要更新。如果是 0 则说明要更新,如果是 2 则说明有错误发生。
  • -r, --no-builtin-rules 禁止 make 使用任何隐含规则。
  • -R, --no-builtin-variabes 禁止 make 使用任何作用于变量上的隐含规则。
  • -s, --silent, --quiet 在命令运行时不输出命令的输出。
  • -S, --no-keep-going, --stop 取消“-k”选项的作用。因为有些时候,make 的选项是从环境变量“MAKEFLAGS”中继承下来的。所以你可以在命令行中使用这个参数来让环境变量中的“-k”选项失效。
  • -t, --touch 相当于 UNIX 的 touch 命令,只是把目标的修改日期变成最新的,也就是阻止生成目标
    的命令运行。- -v, --version 输出 make 程序的版本、版权等关于 make 的信息。
  • -w, --print-directory 输出运行 makefile 之前和之后的信息。这个参数对于跟踪嵌套式调用 make
    时很有用。
  • no-print-directory 禁止“-w”选项。
  • -W <file>, --what-if=<file>, --new-file=<file>, --assume-file=<file> 假定目标<file>;需要更新,如果和“-n”选项使用,那么这个参数会输出该目标更新时的运行动作。如果没有“-n”那么就像运行 UNIX 的“touch”命令一样,使得 <file>; 的修改时间为当前时间。
  • --warn-undefined-variables 只要 make 发现有未定义的变量,那么就输出警告信息。

隐含规则

使用隐含规则

如果要使用隐含规则生成你需要的目标,你所需要做的就是不要写出这个目标的规则。那么,make就会去自动推导这个目标的规则和命令,如果make可以推导生成这个目标的规则和命令,那么这个行为就是隐含规则的自动推导。

1
2
foo: foo.o bar.o  
    cc -o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)  

我们可以注意到,这个Makefile中并没有写下如何生成foo.o和bar.o这两个目标的规则和命令。因为make的隐含规则功能会自动为我们去推导这俩个目标的依赖目标和生成命令。

make会在自己的隐含规则库中寻找可以使用的规则,如果找到,那么就会使用。如果找不到,那么就会报错。在上面的这个例子中,make调用的隐含规则是:把.o的目标的依赖文件设置成.c,并使用C的编译命令 cc -c $(CFLAGS) foo.c来生成foo.o的目标。也就是说,我们完全没有必要写下下面两条规则:

1
2
3
foo.o: foo.c  
    cc -c foo.c $(CFLAGS)boo.o: bar.c  
    cc -c bar.c $(CFLAGS)  

因为这已经是约定好了的事情,make和我们约定好了用C编译命令cc生成.o文件的规则,就是隐含规则。当然,如果我们为.o文件书写了自己的规则,那么make就不会自动推导病调用隐含规则,他会按照我们写好的规则忠实的执行。另外,make的隐含规则库中,每一条隐含规则都是在其库中有其顺序,越是靠前的越被经常使用。所以,这会导致我们有些时候即使我们显示地指定了目标依赖,make 也不会管。如下面这条规则(没有命令)

1
foo.o : foo.p  

依赖文件 foo.p (Pascal 程序的源文件)有可能变得没有意义。如果目录下存在了 foo.c 文件, 那么我们的隐含规则一样会生效,并会通过 foo.c 调用 C 的编译器生成 foo.o 文件。因为,在隐含规则中,Pascal 的规则出现在 C 的规则之后,所以,make 找到可以生成 foo.o 的 C 的规则就不再寻找下一条规则了。如果你确实不希望任何隐含规则推导,那么,你就不要只写出“依赖规则”, 而不写命令。

隐含规则一览

这里我们将讲述所有预先设置(也就是make内建)的隐含规则,如果我们不明确地写下规则,那么make就回在这些规则中寻找所需要的规则和命令。当然我们也可以使用make的参数 -r或是 --no-builtin-rules选项来取消所有预设值的隐含规则。

  • 编译C程序的隐含规则
    <n>.o的目标的依赖目标会自动推导为 <n>.c,并且其生成命令是 $(CC) -c $(CPPFLAGS) $(CFLAGS)

  • 编译C++程序的隐含规则
    <n>.o的目标的依赖目标会自动推导为 <n>.cc或是 <n>.C,并且其生成命令是 $(CXX) -c $(CPPFLAGS) $(CFLAGS)

  • 编译Pascal程序的隐含规则
    <n>.o的目标的依赖目标会自动推导为 <n>.p,并且其生成命令是 $(PC) -c $(PFLAGS)

  • 编译Fortran/Ratfor程序的隐含规则是
    <n>.o的目标的依赖目标会自动推导为 <n>.r<n>.F<n>.f,并且其生成命令为

    • .f $(FC) -c $(FFLAGS)
    • .F $(FC) -c $(FFLAGS) $(CPPFLAGS)
    • .r $(FC) -c $(FFLAGS) $(RFLAGS)
  • 预处理Fortran/Ratfor程序的隐含规则
    <n>.f 的目标的依赖目标会自动推导为 <n>.r<n>.F 。这个规则只是转换 Ratfor 或有预处理的 Fortran 程序到一个标准的 Fortran 程序。其使用的命令是:

    • .F $(FC) –F $(CPPFLAGS) $(FFLAGS)
    • .r $(FC) –F $(FFLAGS) $(RFLAGS)
  • 编译Modula-2程序的隐含规则
    <n>.sym的目标的依赖目标会自动推导为 <n>.def,并且其生成命令是 $(M2C) $(M2FLAGS) $(DEFFLAGS)<n>.o的目标的依赖目标会自动推导为 <n>.mod,并且其生成命令为 $(M2C) $(M2FLAGS) $(MODFLAGS)

  • 汇编和汇编预处理的隐含规则
    <n>.o的目标的依赖目标会自动推导为 <n>.s,默认使用编译器as,并且其生成命令是 $(AS) $(ASFLAGS)``<n>.s的目标的依赖目标会自动推导为 <n>.S,默认使用C预编译器cpp,并且其生成命令是 $(AS) $(ASFLAGS)

  • 链接Object文件的隐含规则
    <n>目标依赖于 <n>.o,通过运行C的编译器来运行链接程序生成(一般是ld),其生成命令是 $(CC) $(LDFLAGS) <n>.o $(LOADLIBES) $(LDLIBS)。这个规则对于只有一个源文件的工程有效,同时也对多个object文件(由不同的源文件生成)的也有效。

    1
    
    x : y.o z.o  
    

    并且 x.c 、y.c 和 z.c 都存在时,隐含规则将执行如下命令:

    1
    2
    
    cc -c x.c -o x.o  
    cc -c y.c -o y.o  cc -c z.c -o z.o  cc x.o y.o z.o -o x  rm -f x.o  rm -f y.o  rm -f z.o  
    

    如果没有一个源文件(如上例中的 x.c)和你的目标名字(如上例中的 x)相关联,那么,你最好
    写出自己的生成规则,不然,隐含规则会报错的。

隐含规则中使用的变量

在隐含规则中的命令中,基本上都是使用了一些预先设置的变量。你可以在你的 makefile 中改变这些变量的值,或是在 make 的命令行中传入这些值,或是在你的环境变量中设置这些值,无论怎么样,只要设置了这些特定的变量,那么其就会对隐含规则起作用。当然,你也可以利用 make 的 -R 或 --no–builtin-variables 参数来取消你所定义的变量对隐含规则的作用。

例如,第一条隐含规则——编译 C 程序的隐含规则的命令是 $(CC) –c $(CFLAGS) $(CPPFLAGS)。Make 默认的编译命令是 cc ,如果你把变量 $(CC) 重定义成 gcc ,把变量 $(CFLAGS) 重定义成 -g,那么,隐含规则中的命令全部会以 gcc –c -g $(CPPFLAGS)的样子来执行了。我们可以把隐含规则中使用的变量分成两种:一种是命令相关的,如 CC ;一种是参数相的关,如CFLAGS 。下面是所有隐含规则中会用到的变量:

关于命令的变量

  • AR : 函数库打包程序。默认命令是 ar
  • AS : 汇编语言编译程序。默认命令是 as
  • CC : C 语言编译程序。默认命令是 cc
  • CXX : C++ 语言编译程序。默认命令是 g++
  • CO : 从 RCS 文件中扩展文件程序。默认命令是 co
  • CPP : C 程序的预处理器(输出是标准输出设备)。默认命令是 $(CC) –E
  • FC : Fortran 和 Ratfor 的编译器和预处理程序。默认命令是 f77
  • GET : 从 SCCS 文件中扩展文件的程序。默认命令是 get
  • LEX : Lex 方法分析器程序(针对于 C 或 Ratfor)。默认命令是 lex
  • PC : Pascal 语言编译程序。默认命令是 pc
  • YACC : Yacc 文法分析器(针对于 C 程序)。默认命令是 yacc
  • YACCR : Yacc 文法分析器(针对于 Ratfor 程序)。默认命令是 yacc –r
  • MAKEINFO : 转换 Texinfo 源文件(.texi)到 Info 文件程序。默认命令是 makeinfo
  • TEX : 从 TeX 源文件创建 TeX DVI 文件的程序。默认命令是 tex
  • TEXI2DVI : 从 Texinfo 源文件创建军 TeX DVI 文件的程序。默认命令是 texi2dvi
  • WEAVE : 转换 Web 到 TeX 的程序。默认命令是 weave
  • CWEAVE : 转换 C Web 到 TeX 的程序。默认命令是 cweave
  • TANGLE : 转换 Web 到 Pascal 语言的程序。默认命令是 tangle
  • CTANGLE : 转换 C Web 到 C。默认命令是 ctangle
  • RM : 删除文件命令。默认命令是 rm –f

关于命令参数的变量

  • ARFLAGS : 函数库打包程序 AR 命令的参数。默认值是 rv
  • ASFLAGS : 汇编语言编译器参数。(当明显地调用 .s 或 .S 文件时)
  • CFLAGS : C 语言编译器参数。
  • CXXFLAGS : C++ 语言编译器参数。
  • COFLAGS : RCS 命令参数。
  • CPPFLAGS : C 预处理器参数。(C 和 Fortran 编译器也会用到)。
  • FFLAGS : Fortran 语言编译器参数。
  • GFLAGS : SCCS “get”程序参数。
  • LDFLAGS : 链接器参数。(如:ld )
  • LFLAGS : Lex 文法分析器参数。
  • PFLAGS : Pascal 语言编译器参数。
  • RFLAGS : Ratfor 程序的 Fortran 编译器参数。
  • YFLAGS : Yacc 文法分析器参数。

隐含规则链

有些时候,一个目标可能被一系列的隐含规则所作用。例如,一个 .o 的文件生成,可能会是先被Yacc 的 [.y] 文件先成 .c ,然后再被 C 的编译器生成。我们把这一系列的隐含规则叫做“隐含规则链”。

在上面的例子中,如果文件 .c 存在,那么就直接调用 C 的编译器的隐含规则,如果没有 .c 文件,但有一个 .y 文件,那么 Yacc 的隐含规则会被调用,生成 .c 文件,然后,再调用 C 编译的隐含规则最终由 .c 生成 .o 文件,达到目标。我们把这种 .c 的文件(或是目标),叫做中间目标。不管怎么样,make 会努力自动推导生成目标的一切方法,不管中间目标有多少,其都会执着地把所有的隐含规则和你书写的规则全部合起来分析,努力达到目标,所以,有些时候,可能会让你觉得奇怪,怎么我的目标会这样生成?怎么我的 makefile 发疯了?

在默认情况下,对于中间目标,它和一般的目标有两个地方所不同:第一个不同是除非中间的目标不存在,才会引发中间规则。第二个不同的是,只要目标成功产生,那么,产生最终目标过程中,所产生的中间目标文件会被以 rm -f 删除。通常,一个被 makefile 指定成目标或是依赖目标的文件不能被当作中介。然而,你可以明显地说明一个文件或是目标是中介目标,你可以使用伪目标 .INTERMEDIATE 来强制声明。(如: .INTERMEDIATE: mid )

你也可以阻止 make 自动删除中间目标,要做到这一点,你可以使用伪目标 .SECONDARY 来强制声明(如:.SECONDARY : sec )。你还可以把你的目标,以模式的方式来指定(如:%.o )成伪目标.PRECIOUS 的依赖目标,以保存被隐含规则所生成的中间文件。

在“隐含规则链”中,禁止同一个目标出现两次或两次以上,这样一来,就可防止在 make 自动推导时出现无限递归的情况。Make 会优化一些特殊的隐含规则,而不生成中间文件。如,从文件 foo.c 生成目标程序 foo ,按道理,make 会编译生成中间文件 foo.o ,然后链接成 foo ,但在实际情况下,这一动作可以被一条cc 的命令完成(cc –o foo foo.c ),于是优化过的规则就不会生成中间文件。

定义规则模式

你可以使用模式规则来定义一个隐含规则。一个模式规则就好像一个一般的规则,只是在规则中,目标的定义需要有 % 字符。% 的意思是表示一个或多个任意字符。在依赖目标中同样可以使用 % ,只是依赖目标中的 % 的取值,取决于其目标。

有一点需要注意的是,% 的展开发生在变量和函数的展开之后,变量和函数的展开发生在 make 载入 Makefile 时,而模式规则中的 % 则发生在运行时。

模式规则介绍

如果 % 定义在目标中,那么,目标中的 % 的值决定了依赖目标中的 % 的值,也就是说,目标中的模式的 % 决定了依赖目标中 % 的样子。例如有一个模式规则如下:

1
%.o : %.c ; <command ......>;  

其含义是,指出了怎么从所有的 .c 文件生成相应的 .o 文件的规则。如果要生成的目标是 a.ob.o ,那么 %c 就是 a.c b.c 。

一旦依赖目标中的 % 模式被确定,那么,make 会被要求去匹配当前目录下所有的文件名,一旦找到,make 就会规则下的命令,所以,在模式规则中,目标可能会是多个的,如果有模式匹配出多个目标,make 就会产生所有的模式目标,此时,make 关心的是依赖的文件名和生成目标的命令这两件事。

模式规则示例

下面这个例子表示了, 把所有的 .c 文件都编译成 .o 文件.

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

其中, $@表示所有的目标的挨个值,$< 表示了所有依赖目标的挨个值。这些奇怪的变量我们叫 自动化变量

下面的这个例子中有两个目标是模式的:

1
2
%.tab.c %.tab.h: %.y  
    bison -d $<  

这条规则告诉 make 把所有的 .y 文件都以 bison -d <n>.y 执行,然后生成 <n>.tab.c<n>.tab.h 文件。(其中, <n> 表示一个任意字符串)。如果我们的执行程序 foo 依赖于文件 parse.tab.o和 scan.o ,并且文件 scan.o 依赖于文件 parse.tab.h, 如果 parse.y 文件被更新了,那么根据上述的规则,bison -d parse.y 就会被执行一次,于是,parse.tab.o 和 scan.o 的依赖文件就齐了。(假设, parse.tab.o 由 parse.tab.c 生成,和 scan.o 由 scan.c 生成,而 foo 由 parse.tab.o和 scan.o 链接生成,而且 foo 和其 .o 文件的依赖关系也写好,那么,所有的目标都会得到满足)

自动化变量

在上述的模式规则中,目标和依赖文件都是一系例的文件,那么我们如何书写一个命令来完成从不同的依赖文件生成相应的目标?因为在每一次的对模式规则的解析时,都会是不同的目标和依赖文件。

自动化变量就是完成这个功能的。在前面,我们已经对自动化变量有所提涉,相信你看到这里已对它有一个感性认识了。所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在规则的命令中。

下面是所有的自动化变量及其说明:

  • $@ : 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@ 就是匹配于目标中模式定义的集合。
  • $% : 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o),那么,$% 就是 bar.o ,$@ 就是 foo.a 。如果目标不是函数库文件(Unix 下是 .a ,Windows下是 .lib ),那么,其值为空。
  • $< : 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
  • $? : 所有比目标新的依赖目标的集合。以空格分隔。
  • $^ : 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
  • $+ : 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。
  • $* : 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且目标的模式是 a.%.b ,那么,$* 的值就是 dir/a.foo 。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么 $*也就不能被推导出,但是,如果目标文件的后缀是 make 所识别的,那么$* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是 make 所能识别的后缀名,所以,$*的值就是 foo 。这个特性是 GNU make 的,很有可能不兼容于其它版本的 make,所以,你应该尽量避免使用$* ,除非是在隐含规则或是静态模式中。如果目标中的后缀是 make 所不能识别的,那么 $* 就是空值。

当你希望只对更新过的依赖文件进行操作时,$? 在显式规则中很有用,例如,假设有一个函数库文件叫 lib ,其由其它几个 object 文件更新。那么把 object 文件打包的比较有效率的 Makefile 规则是:

1
2
lib : foo.o bar.o lose.o win.o  
ar r lib $?  

在上述所列出来的自动量变量中。四个变量($@$<$%$* )在扩展时只会有一个文件,而另三个的值是一个文件列表。这七个自动化变量还可以取得文件的目录名或是在当前目录下的符合模式的文件名,只需要搭配上 D 或 F 字样。这是 GNU make 中老版本的特性,在新版本中,我们使用函数dir 或 notdir 就可以做到了。D 的含义就是 Directory,就是目录,F 的含义就是 File,就是文件。

模式的匹配

重载内建隐含规则

使用make更新函数库文件