Makefile 学习精简(一)[待续]

来自陈浩的一片老文,但绝对营养。

示例工程:3 个头文件*.h,和 8 个 C 文件*.c

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

  • Makefile规则:
//target: 目标文件
//prerequisites: 依赖文件
//command: 执行命令
target  : prerequisites    
command

示例,

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.c    
kbd.o : kbd.c defs.h command.h    
cc -c kbd.c    
command.o : command.c defs.h command.h    
cc -c command.c    
display.o : display.c defs.h buffer.h    
cc -c display.c    
insert.o : insert.c defs.h buffer.h    
cc -c insert.c    
search.o : search.c defs.h buffer.h    
cc -c search.c    
files.o : files.c defs.h buffer.h command.h    
cc -c files.c 
utils.o : utils.c defs.h    
cc -c utils.c    
clean :    
rm edit main.o kbd.o command.o display.o \    
insert.o search.o files.o utils.o
  • 使用变量
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.c    
kbd.o : kbd.c defs.h command.h    
cc -c kbd.c    
command.o : command.c defs.h command.h    
cc -c command.c    
display.o : display.c defs.h buffer.h    
cc -c display.c    
insert.o : insert.c defs.h buffer.h    
cc -c insert.c    
search.o : search.c defs.h buffer.h    
cc -c search.c    
files.o : files.c defs.h buffer.h command.h    
cc -c files.c    
utils.o : utils.c defs.h    
cc -c utils.c    
clean :    
rm edit $(objects)
  • 自动推导(隐晦规则)
    只要 make 看到一个*.o 文件,它就会自动的把*.c文件加在依赖关系中,.PHONY伪目标文件。
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)
  • 清空目标文件
    一般做法,
clean:    
rm edit $(objects)

推荐做法,

.PHONY : clean    
clean :    
-rm edit $(objects)

.PHONY表示 clean 是一个“伪目标”,-rm 是某些文件出现问题,但不要管,继续做后面的事,clean的规则放在文件的最后”。

细节

Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。在 Makefile 中的命令,必须要以[Tab]键开始。

  1. 显式规则
    显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
  2. 隐晦规则
    由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 所支持的。
  3. 变量的定义
    Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点你 C 语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示
  • 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的include 一样;
  • 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译#if 一样;
  • 定义一个多行的命令。
  1. 注释
    Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用#字符。如果你要在你的 Makefile 中使用#字符,可以用反斜框进行转义,如:\#
  • Makefile 文件名
    默认的情况下,make 命令会在当前目录下按顺序为GNUmakefilemakefileMakefile的文件,推荐Makefilemake -f[--file]参数,指定特定的 Makefile

  • 引用Makefile
    include <filename>
    <filename>可以是当前操作系统 Shell 的文件模式(可以保含路径和通配符),在 include前面可以有一些空字符,不能是[Tab]键。include<filename>用一个或多个空格隔开。例,

bar = e.mk f.mk
include foo.make *.mk $(bar)

等价于,

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

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

  1. 如果 make 执行时,有-I--include-dir参数,那么 make 就会在这个参数所指定的目录下去寻找。
  2. 如果目录<prefix>/include(一般是:/usr/local/bin/usr/include)存在的话,make 也会去找。如果有文件没有找到的话,make 会生成一条警告信息,继续载入其它的文件,一旦完成 Makefile 的读取,make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。在 include 前加一个减号-,如: -include <filename>其表示,无论 include 过程中出现什么错误,都不要报错继续执行。其它版本 make兼容的相关命令是 sinclude
  • 环境变量 MAKEFILES
    如果你的当前环境中定义了环境变量 MAKEFILES,那么,make 会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的 Makefile,用空格分隔。
    ** 建议不要使用这个环境变量。 **

  • make 的工作方式
    GNUmake 工作时的执行步骤入下:

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

1-5 步为第一个阶段,6-7 为第二个阶段。第一个阶段中,如果定义的变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

书写规则

规则包含两个部分,一个是依赖关系,一个是生成目标的方法。
Makefile 中,第一条规则中的目标将被确立为最终的目标。

一、规则举例

foo.o : foo.c defs.h # foo 模块    
cc -c -g foo.c  

foo.o 是我们的目标,foo.cdefs.h 是目标所依赖的源文件,而只有一个命令cc -c -g foo.c(以Tab 键开头)。

  • 文件的依赖关系,foo.o 依赖于 foo.cdefs.h 的文件,如果 foo.cdefs.h 的文件日期要比 foo.o 文件日期要新,或是 foo.o 不存在,那么依赖关系发生。
  • 如果生成(或更新)foo.o 文件。也就是那个 cc 命令,其说明了,如何生成 foo.o这个文件。

二、规则的语法

targets : prerequisites    
command

或是这样:

targets : prerequisites ; command    
command    

targets 是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。command 是命令行,如果其不与target:prerequisites在一行,那么,必须以[Tab]开头,如果和 prerequisites 在一行,那么可以用;做为分隔。 prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么, 目标就被认为是“过时的”,被认为是需要重生成的。

如果命令太长,你可以使用反斜框(\)作为换行符。make 对一行上有多少个字符没有限制。规则告诉 make 两件事,文件的依赖关系和如何成成目标文件。

三、在规则中使用通配符
如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make 支持三各通配符:*?[...]。这是和 Unix 的 B-Shell 是相同的。 波浪号(~)字符在文件名中也有比较特殊的用途。如果是“~/test,这就表示当前用户的$HOME 目录下的 test 目录。而~hchen/test则表示用户 hchen 的宿主目录下的 test 目录。而在 Windows 或是 MS-DOS 下,用户没有宿主目录,那么波浪号所指的目录则根据环境变量HOME而定。

通配符代替了你一系列的文件,如*.c表示所以后缀为 c 的文件用转义字符\,如\*来表示真实的*字符。
例:

clean:    
rm -f *.o
print: *.c    
lpr -p $?    
touch print

这个例子说明了通配符也可以在我们的规则中,目标 print 依赖于所有的.c文件。其中的$?是一个自动化变量。

objects = *.o

上面这个例子,表示了,通符同样可以用在变量中。并不是说*.o会展开,不!objects的值就是*.o。如果要让通配符在变量中展开,也就是让 objects 的值是所有.o的文件名的集合,那么,你可以这样:

objects := $(wildcard *.o)    

这种用法由关键字wildcard指出,关于 Makefile 的关键字。

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

VPATH = src:../headers

上面的的定义指定两个目录,src../headersmake 会按照这个顺序进行搜索。目录由:分隔。
另一个设置文件搜索路径的方法是使用 makevpath关键字,不是变量,这是一个 make 的关键字,可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

  • vpath <pattern> <directories>
    为符合模式<pattern>的文件指定搜索目录<directories>
  • vpath <pattern>
    清除符合模式<pattern>的文件的搜索目录。
  • vpath
    清除所有已被设置好了的文件搜索目录。

vapth 使用方法中的<pattern>需要包含%字符匹配零或若干字符,例如 , %.h表示所有以 .h结 尾 的 文 件 。<pattern>指 定 了 要 搜 索 的 文 件 集 , 而 <directories>则指定了<pattern>的文件集的搜索的目录。例如:vpath %.h ../headers,要求 make../headers目录下搜索所有以.h结尾的文件。
我们可以连续地使用 vpath 语句,以指定不同搜索策略。如果连续的 vpath 语句中出现了相同的<pattern>,或是被重复了的<pattern>,那么,make 会按照 vpath 语句的先后顺序来执行搜索。如:

vpath %.c foo    
vpath % blish    
vpath %.c bar

其表示.c结尾的文件,先在foo目录,然后是blish,最后是bar目录。

vpath %.c foo:bar    
vpath % blish

而上面的语句则表示.c结尾的文件,先在foo目录,然后是bar目录,最后才是blish目录。

伪目标

最早先的一个例子中,我们提到过一个“clean”的目标,这是一个“伪目标”,
clean:
rm *.o temp
正像我们前面例子中的“clean”一样,即然我们生成了许多文件编译文件,我们也应
该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make clean”来使用该目
标)
因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,
由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只
有通过显示地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,
不然其就失去了“伪目标”的意义了。

当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显
示地指明一个目标是“伪目标”,向 make 说明,不管是否有这个文件,这个目标就是“伪
目标”。
.PHONY : clean
只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make
clean”这样。于是整个过程可以这样写:
第 20 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
.PHONY: clean
clean:
rm *.o temp

伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同
样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的 Makefile 需要
一口气生成若干个可执行文件,但你只想简单地敲一个 make 完事,并且,所有的目标文件
都写在一个 Makefile 中,那么你可以使用“伪目标”这个特性:

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
我们知道,Makefile 中的第一个目标会被作为其默认目标。我们声明了一个“all”的
伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三
个目标就总是不如“all”这个目标新。所以,其它三个目标的规则总是会被决议。也就达
到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目
标”。

随便提一句,从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也
可成为依赖。看下面的例子:
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
“make clean”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪
目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和
“make cleandiff”命令来达到清除不同种类文件的目的。
第 21 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
六、多目标

Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖
于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。当然,多个目标的
生成规则的执行命令是同一个,这可能会可我们带来麻烦,不过好在我们的可以使用一个自
动化变量“$@”(关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目
标的集合,这样说可能很抽象,还是看一个例子吧。

bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于:
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput

其中,-$(subst output,,$@)中的“$”表示执行一个 Makefile 的函数,函数名为 subst,
后面的为参数。关于函数,将在后面讲述。这里的这个函数是截取字符串的意思,“$@”表
示目标的集合,就像一个数组,“$@”依次取出目标,并执于命令。
七、静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵

。我们还是先来看一下语法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
....

targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern 是指明了 targets 的模式,也就是的目标集模式。
prereq-parrterns 是目标的依赖模式,它对 target-parrtern 形成的模式再进行一次依赖
目标的定义。

这样描述这三个东西,可能还是没有说清楚,还是举个例子来说明一下吧。如果我们的
第 22 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
<target-parrtern>定义成“%.o”,意思是我们的<target>集合中都是以“.o”结尾的,而
如果我们的<prereq-parrterns>定义成“%.c”,意思是对<target-parrtern>所形成的目标
集进行二次定义,其计算方法是,取<target-parrtern>模式中的“%”(也就是去掉了[.o]
这个结尾),并为其加上[.c]这个结尾,形成的新集合。

所以,我们的“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果你的文
件名中有“%”那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。
看一个例子:
objects = foo.o bar.o

all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) {1}lt; -o $@
上面的例子中,指明了我们的目标从$object 中获取,“%.o”表明要所有以“.o”结
尾的目标,也就是“foo.o bar.o”,也就是变量$object 集合的模式,而依赖模式“%.c”
则取模式“%.o”的“%”,也就是“foo bar”,并为其加下“.c”的后缀,于是,我们的
依赖目标就是“foo.c bar.c”。而命令中的“{1}lt;”和“$@”则是自动化变量,“{1}lt;”表示
所有的依赖目标集(也就是“foo.c bar.c”),“$@”表示目标集(也就是“foo.o bar.o”)。
于是,上面的规则展开后等价于下面的规则:

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

试想,如果我们的“%.o”有几百个,那种我们只要用这种很简单的“静态模式规则”
就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,
那会一个很强大的功能。再看一个例子:

files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) {1}lt; -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile {1}lt;
$(filter %.o,$(files))表示调用 Makefile 的 filter 函数,过滤“$filter”集,只要其
中模式为“%.o”的内容。其的它内容,我就不用多说了吧。这个例字展示了 Makefile 中更
大的弹性。

第 23 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

八、自动生成依赖性
在 Makefile 中,我们的依赖关系可能会需要包含一系列的头文件,比如,如果我们的 main.c
中有一句“#include "defs.h"”,那么我们的依赖关系应该是:

main.o : main.c defs.h
但是,如果是一个比较大型的工程,你必需清楚哪些 C 文件包含了哪些头文件,并且,
你在加入或删除头文件时,也需要小心地修改 Makefile,这是一个很没有维护性的工作。
为了避免这种繁重而又容易出错的事情,我们可以使用 C/C++编译的一个功能。大多数的
C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依
赖关系。例如,如果我们执行下面的命令:
cc -M main.c

其输出是:
main.o : main.c defs.h
于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关
系,而由编译器自动生成了。需要提醒一句的是,如果你使用 GNU 的 C/C++编译器,你得用
“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
gcc -M main.c 的输出是:
main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.h
gcc -MM main.c 的输出则是:
main.o: main.c defs.h
那么,编译器的这个功能如何与我们的 Makefile 联系在一起呢。因为这样一来,我们
的 Makefile 也要根据这些源文件重新生成,让 Makefile 自已依赖于源文件?这个功能并不
现实,不过我们可以有其它手段来迂回地实现这一功能。GNU 组织建议把编译器为每一个源
第 24 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
文 件 的 自 动 生 成 的 依 赖 关 系 放 到 一 个 文 件 中 , 为 每 一 个 “name.c” 的 文 件 都 生 成 一 个
“name.d”的 Makefile 文件,[.d]文件中就存放对应[.c]文件的依赖关系。于是,我们可
以写出[.c]文件和[.d]文件的依赖关系,并让 make 自动更新或自成[.d]文件,并把其包含
在我们的主 Makefile 中,这样,我们就可以自动化地生成每个文件的依赖关系了。
这里,我们给出了一个模式规则来产生[.d]文件:

%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) {1}lt; > $@.$$; \
sed 's,$∗.o[ :]*,\1.o $@ : ,g' < $@.$$ > $@; \
rm -f $@.$$
这个规则的意思是,所有的[.d]文件依赖于[.c]文件,“rm -f $@”的意思是删除所有
的目标,也就是[.d]文件,第二行的意思是,为每个依赖文件“{1}lt;”,也就是[.c]文件生成
依赖文件,“$@”表示模式“%.d”文件,如果有一个 C 文件是 name.c,那么“%”就是
“name”,“$$”意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第
三行使用 sed 命令做了一个替换,关于 sed 命令的用法请参看相关的使用文档。第四行就是
删除临时文件。

总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即
把依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
于是,我们的[.d]文件也会自动更新了,并会自动生成了,当然,你还可以在这个[.d]
文件中加入的不只是依赖关系,包括生成的命令也可一并加入,让每个[.d]文件都包含一个
完赖的规则。一旦我们完成这个工作,接下来,我们就要把这些自动生成的规则放进我们的
主 Makefile 中。我们可以使用 Makefile 的“include”命令,来引入别的 Makefile 文件(前
面讲过),例如:
sources = foo.c bar.c
include $(sources:.c=.d)
上 述 语 句 中 的 “$(sources:.c=.d)” 中 的 “.c=.d” 的 意 思 是 做 一 个 替 换 , 把 变 量
$(sources)所有[.c]的字串都替换成[.d],关于这个“替换”的内容,在后面我会有更为详
细的讲述。当然,你得注意次序,因为 include 是按次来载入文件,最先载入的[.d]文件中
的目标会成为默认目标。

第六部分 书写命令

第 25 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
每条规则中的命令和操作系统 Shell 的命令行是一致的。make 会一按顺序一条一条的
执行命令,每条命令的开头必须以[Tab]键开头,除非,命令是紧跟在依赖规则后面的分号
后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以 Tab 键开头的,
那么 make 会认为其是一个空命令。

我 们 在 UNIX 下 可 能 会 使 用 不 同 的 Shell , 但 是 make 的 命 令 默 认 是 被
“/bin/sh”——UNIX 的标准 Shell 解释执行的。除非你特别指定一个其它的 Shell。
Makefile 中,“#”是注释符,很像 C/C++中的“//”,其后的本行字符都被注释。
一、显示命令
通常,make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在
命令行前,那么,这个命令将不被 make 显示出来,最具代表性的例子是,我们用这个功能
来像屏幕显示一些信息。如:
@echo 正在编译 XXX 模块......
当 make 执行时,会输出“正在编译 XXX 模块......”字串,但不会输出命令,如果没
有“@”,那么,make 将输出:
echo 正在编译 XXX 模块......
正在编译 XXX 模块......
如果 make 执行时,带入 make 参数“-n”或“--just-print”,那么其只是显示命令,
但不会执行命令,这个功能很有利于我们调试我们的 Makefile,看看我们书写的命令是执
行起来是什么样子的或是什么顺序的。

而 make 参数“-s”或“--slient”则是全面禁止命令的显示。

二、命令执行

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

示例一:
exec:
cd /home/hchen
pwd

第 26 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
示例二:
exec:
cd /home/hchen; pwd

当我们执行“make exec”时,第一个例子中的 cd 没有作用,pwd 会打印出当前的
Makefile 目录,而第二个例子中,cd 就起作用了,pwd 会打印出“/home/hchen”。make
一般是使用环境变量 SHELL 中所定义的系统 Shell 来执行命令,默认情况下使用 UNIX 的
标准 Shell——/bin/sh 来执行命令。但在 MS-DOS 下有点特殊,因为 MS-DOS 下没有 SHELL
环境变量,当然你也可以指定。如果你指定了 UNIX 风格的目录形式,首先,make 会在 SHELL
所指定的路径中找寻命令解释器,如果找不到,其会在当前盘符中的当前目录中寻找,如果
再找不到,其会在 PATH 环境变量中所定义的所有路径中寻找。MS-DOS 中,如果你定义的命
令解释器没有找到,其会给你的命令解释器加上诸如“.exe”、“.com”、“.bat”、“.sh”
等后缀。

三、命令出错

每当命令运行完后,make 会检测每个命令的返回码,如果命令返回成功,那么 make 会
执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个
规则中的某个命令出错了(命令退出码非零),那么 make 就会终止执行当前规则,这将有可
能终止所有规则的执行。
有些时候,命令的出错并不表示就是错误的。例如 mkdir 命令,我们一定需要建立一个
目录,如果目录不存在,那么 mkdir 就成功执行,万事大吉,如果目录存在,那么就出错了。
我们之所以使用 mkdir 的意思就是一定要有这样的一个目录,于是我们就不希望 mkdir 出错
而终止规则的运行。

为了做到这一点,忽略命令的出错,我们可以在 Makefile 的命令行前加一个减号“-”
(在 Tab 键之后),标记为不管命令出不出错都认为是成功的。如:
clean:
-rm -f *.o

还有一个全局的办法是,给 make 加上“-i”或是“--ignore-errors”参数,那么,
Makefile 中所有命令都会忽略错误。而如果一个规则是以“.IGNORE”作为目标的,那么这
个规则中的所有命令将会忽略错误。这些是不同级别的防止命令出错的方法,你可以根据你
的不同喜欢设置。

还有一个要提一下的 make 的参数的是“-k”或是“--keep-going”,这个参数的意思
是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。
第 27 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
四、嵌套执行 make

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

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

如果你要传递变量到下级 Makefile 中,那么你可以使用这样的声明:
export <variable ...>
如果你不想让某些变量传递到下级 Makefile 中,那么你可以这样声明:
unexport <variable ...>
如:
示例一:
export variable = value
其等价于:
variable = value
export variable
第 28 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

其等价于:
export variable := value
其等价于:
variable := value
export variable
示例二:
export variable += value
其等价于:
variable += value
export variable
如果你要传递所有的变量,那么,只要一个 export 就行了。后面什么也不用跟,表示
传递所有的变量。
需要注意的是,有两个变量,一个是 SHELL,一个是 MAKEFLAGS,这两个变量不管你是
否 export,其总是要传递到下层 Makefile 中,特别是 MAKEFILES 变量,其中包含了 make
的参数信息,如果我们执行“总控 Makefile”时有 make 参数或是在上层 Makefile 中定义
了这个变量,那么 MAKEFILES 变量将会是这些参数,并会传递到下层 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”来执行,那么当进入该目录时,
我们会看到:

第 29 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
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”结束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef

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

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

第七部分 使用变量

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

变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有“:”、
第 30 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
“#”、“=”或是空字符(空格、回车等)。变量是大小写敏感的,“foo”、“Foo”和“FOO”
是三个不同的变量名。传统的 Makefile 的变量名是全大写的命名方式,但我推荐使用大小
写搭配的变量名,如:MakeFlags。这样可以避免和系统的变量冲突,而发生意外的事情。

有一些变量是很奇怪字串,如“{1}lt;”、“$@”等,这些是自动化变量,我会在后面介绍。

一、变量的基础

变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好
用小括号“()”或是大括号“{}”把变量给包括起来。如果你要使用真实的“$”字符,
那么你需要用“$”来表示。
变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及新的变量中。先
看一个例子:
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)
$(objects) : defs.h
变量会在使用它的地方精确地展开,就像 C/C++中的宏一样,例如:
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
展开后得到:
prog.o : prog.c
cc -c prog.c
当然,千万不要在你的 Makefile 中这样干,这里只是举个例子来表明 Makefile 中的变
量在使用处展开的真实样子。可见其就是一个“替代”的原理。

另外,给变量加上括号完全是为了更加安全地使用这个变量,在上面的例子中,如果你
不想给变量加上括号,那也可以,但我还是强烈建议你给变量加上括号。
第 31 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
二、变量中的变量

在定义变量的值时,我们可以使用其它变量来构造变量的值,在 Makefile 中有两种方
式来在用变量定义变量的值。
先看第一种方式,也就是简单的使用“=”号,在“=”左侧是变量,右侧是变量的值,右侧
变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,
其也可以使用后面定义的值。如:
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
我们执行“make all”将会打出变量$(foo)的值是“Huh?”( $(foo)的值是$(bar),
$(bar)的值是$(ugh),$(ugh)的值是“Huh?”)可见,变量是可以使用后面的变量来定义的。

这个功能有好的地方,也有不好的地方,好的地方是,我们可以把变量的真实值推到后
面来定义,如:
CFLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar
当“CFLAGS”在命令中被展开时,会是“-Ifoo -Ibar -O”。但这种形式也有不好的地
方,那就是递归定义,如:
CFLAGS = $(CFLAGS) -O
或:
A = $(B)
B = $(A)
这会让 make 陷入无限的变量展开过程中去,当然,我们的 make 是有能力检测这样的定
义,并会报错。还有就是如果在变量中使用函数,那么,这种方式会让我们的 make 运行时
非常慢,更糟糕的是,他会使用得两个 make 的函数“wildcard”和“shell”发生不可预知
的错误。因为你不会知道这两个函数会被调用多少次。

为了避免上面的这种方法,我们可以使用 make 中的另一种用变量来定义变量的方法。
这种方法使用的是“:=”操作符,如:
第 32 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

x := foo
y := $(x) bar
x := later
其等价于:
y := foo bar
x := later
值得一提的是,这种方法,前面的变量不能使用后面的变量,只能使用前面已定义好了的变
量。如果是这样:
y := $(x) bar
x := foo
那么,y 的值是“bar”,而不是“foo bar”。
上面都是一些比较简单的变量使用了,让我们来看一个复杂的例子,其中包括了 make
的函数、条件表达式和一个系统变量“MAKELEVEL”的使用:
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami)
host-type := $(shell arch)
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif

关于条件表达式和函数,我们在后面再说,对于系统变量“MAKELEVEL”,其意思是,
如果我们的 make 有一个嵌套执行的动作(参见前面的“嵌套使用 make”),那么,这个变
量会记录了我们的当前 Makefile 的调用层数。

下面再介绍两个定义变量时我们需要知道的,请先看一个例子,如果我们要定义一个变
量,其值是一个空格,那么我们可以这样来:
nullstring :=
space := $(nullstring) # end of the line
nullstring 是一个 Empty 变量,其中什么也没有,而我们的 space 的值是一个空格。因为

操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个 Empty 变量来
标明变量的值开始了,而后面采用“#”注释符来表示变量定义的终止,这样,我们可以定
义出其值是一个空格的变量。请注意这里关于“#”的使用,注释符“#”的这种特性值得我
第 33 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
们注意,如果我们这样定义一个变量:

dir := /foo/bar # directory to put the frobs in
dir 这个变量的值是“/foo/bar”,后面还跟了 4 个空格,如果我们这样使用这样变量来指
定别的目录——“$(dir)/file”那么就完蛋了。
还有一个比较有用的操作符是“?=”,先看示例:
FOO ?= bar
其含义是,如果 FOO 没有被定义过,那么变量 FOO 的值就是“bar”,如果 FOO 先前被定义
过,那么这条语将什么也不做,其等价于:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
三、变量高级用法

这里介绍两种变量的高级使用方法,第一种是变量值的替换。我们可以替换变量中的共有的
部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以
“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束
符”。

还是看一个示例吧:
foo := a.o b.o c.o
bar := $(foo:.o=.c)
这个示例中,我们先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所
有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c
c.c”。
另外一种变量替换的技术是以“静态模式”(参见前面章节)定义的,如:
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
这依赖于被替换字串中的有相同的模式,模式中必须包含一个“%”字符,这个例子同
样让$(bar)变量的值为“a.c b.c c.c”。
第二种高级用法是——“把变量的值再当成变量”。先看一个例子:
第 34 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓

x = y
y = z
a := $($(x))
在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,
是“x=y”,而不是“x=$(y)”)

我们还可以使用更多的层次:
x = y
y = z
z = u
a := $($($(x)))

这里的$(a)的值是“u”,相关的推导留给读者自己去做吧。
让我们再复杂一点,使用上“在变量定义中使用变量”的第一个方式,来看一个例子:
x = $(y)
y = z
z = Hello
a := $($(x))

这里的$($(x))被替换成了$($(y)),因为$(y)值是“z”,所以,最终结果是:a:=$(z),也
就是“Hello”。
再复杂一点,我们再加上函数:
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))

这 个 例 子 中 , “$($($(z)))” 扩 展 为 “$($(y))” , 而 其 再 次 被 扩 展 为 “$($(subst
1,2,$(x)))”。$(x)的值是“variable1”,subst 函数把“variable1”中的所有“1”字
串替换成“2”字串,于是,“variable1”变成“variable2”,再取其值,所以,最终,
$(a)的值就是$(variable2)的值——“Hello”。(喔,好不容易)

在这种方式中,或要可以使用多个变量来组成一个变量的名字,然后再取其值:
first_second = Hello
a = first
第 35 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
b = second
all = $($a_$b)

这里的“$a_$b”组成了“first_second”,于是,$(all)的值就是“Hello”。
再来看看结合第一种技术的例子:
a_objects := a.o b.o c.o
1_objects := 1.o 2.o 3.o
sources := $($(a1)_objects:.o=.c)
这个例子中,如果$(a1)的值是“a”的话,那么,$(sources)的值就是“a.c b.c c.c”;
如果$(a1)的值是“1”,那么$(sources)的值是“1.c 2.c 3.c”。

再来看一个这种技术和“函数”与“条件语句”一同使用的例子:
ifdef do_sort
func := sort
else
func := strip
endif

bar := a d b g q c
foo := $($(func) $(bar))
这个示例中,如果定义了“do_sort”,那么:foo := $(sort a d b g q c),于是$(foo)
的值就是“a b c d g q”,而如果没有定义“do_sort”,那么:foo := $(sort a d bg q
c),调用的就是 strip 函数。
当然,“把变量的值再当成变量”这种技术,同样可以用在操作符的左边:
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef

这个例子中定义了三个变量:“dir”,“foo_sources”和“foo_print”。
第 36 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
四、追加变量值

我们可以使用“+=”操作符给变量追加值,如:
objects = main.o foo.o bar.o utils.o
objects += another.o
于是,我们的$(objects)值变成:“main.o foo.o bar.o utils.o another.o”(another.o
被追加进去了)

使用“+=”操作符,可以模拟为下面的这种例子:
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o
所不同的是,用“+=”更为简洁。
如果变量之前没有定义过,那么,“+=”会自动变成“=”,如果前面有变量定义,那么“+=”
会继承于前次操作的赋值符。如果前一次的是“:=”,那么“+=”会以“:=”作为其赋值符,
如:

variable := value
variable += more
等价于:
variable := value
variable := $(variable) more
但如果是这种情况:
variable = value
variable += more
由于前次的赋值符是“=”,所以“+=”也会以“=”来做为赋值,那么岂不会发生变量的递
补归定义,这是很不好的,所以 make 会自动为我们解决这个问题,我们不必担心这个问题。

五、override 指示符

如果有变量是通常make 的命令行参数设置的,那么Makefile 中对这个变量的赋值会被忽略。
第 37 页共 78 页 2005 年 10 月 14 日整理:祝冬华跟我一起写 Makefile 作者:陈皓
如果你想在 Makefile 中设置这类参数的值,那么,你可以使用“override”指示符。其语
法是:

override <variable> = <value>
override <variable> := <value>
当然,你还可以追加:
override <variable> += <more text>
对于多行的变量定义,我们用 define 指示符,在 define 指示符前,也同样可以使用 ovveride
指示符,如:

override define foo
bar
endef
六、多行变量
还有一种设置变量值的方法是使用 define 关键字。使用 define 关键字设置变量的值可
以有换行,这有利于定义一系列的命令(前面我们讲过“命令包”的技术就是利用这个关键
字)。

define 指示符后面跟的是变量的名字,而重起一行定义变量的值,定义是以 endef 关
键字结束。其工作方式和“=”操作符一样。变量的值可以包含函数、命令、文字,或是其
它变量。因为命令需要以[Tab]键开头,所以如果你用 define 定义的命令变量中没有以[Tab]
键开头,那么 make 就不会把其认为是命令。

下面的这个示例展示了 define 的用法:
define two-lines
echo foo
echo $(bar)
endef

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容