《跟我一起写Makefile》 是 陈皓 发表在其 CSDN 博客上的系列文章。该系列文章翻译整理自 GNU Make Manual ,一直受到读者的推荐,是很多人学习Makefile的首选文档。
Makefile 里有什么
- 显式规则
- 显式规则说明了如何生成一个或多个目标文件。这是由 Makefile 的书写者明显指出要 生成的文件、文件的依赖文件和生成的命令。
- 隐晦规则
- 由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Make- file,这是由 make 所支持的。
- 变量的定义
- 在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像你 C 语 言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示
- 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 include 一样
- 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样;
- 定义一个多行的命令
- 注释。
- Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用 # 字符,这个就像 C/C++ 中的 // 一样。如果你要在你的 Makefile 中使用 # 字符,可以用反斜杠进行转义,如:# 。
在 Makefile 中的命令,必须要以 Tab 键开始。
引用其它的 Makefile
make 的工作方式
- 读入所有的 Makefile。
- 读入被 include 的其它 Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
书写规则
1 2 3
| targets : prerequisites command ...
|
通配符
文件搜寻
1 2 3 4
| VPATH = src:../headers
vpath %.h ../headers
|
伪目标
“伪目标”并不是一个文件,只是一个标签,由于“伪目标” 不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。
1 2 3
| .PHONY : clean clean : -rm *.o temp
|
多目标
1 2
| bigoutput littleoutput : text.g generate text.g -$(subst output,,$@) > $@
|
静态模式
1 2 3
| <targets ...> : <target-pattern> : <prereq-patterns ...> <commands> ...
|
- targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
- target-pattern 是指明了 targets 的模式,也就是的目标集模式。
- prereq-patterns 是目标的依赖模式,它对 target-pattern 形成的模式再进行一次依赖目标的定义。
1 2 3 4 5 6
| objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c $(CC) -c $(CFLAGS) $< -o $@
|
自动生成依赖性
一个模式规则来产生 .d 文件:
1 2 3 4
| %.d: %.c @set -e; rm -f $@; \ $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
|
引入别的 Makefile 文件
1 2
| sources = foo.c bar.c include $(sources:.c=.d)
|
书写命令
显示命令
命令出错
在 Makefile 的命令行前加一个减号 - (在 Tab 键之 后),标记为不管命令出不出错都认为是成功的
嵌套执行 make
1 2 3 4
| export variable = value
variable = value export variable
|
1 2 3 4
| export variable := value
variable := value export variable
|
1 2 3 4
| export variable += value
variable += value export variable
|
定义命令包
1 2 3 4
| define run-yacc yacc $(firstword $^) mv y.tab.c $@ endef
|
使用
1 2
| foo.c : foo.y $(run-yacc)
|
使用变量
变量的基础
需要给在变量名前加上 $
符号,但最好用小括号 ()
或是大括号 {}
把变量给包括起来。
如果你要使用真实的 $
字符,那么你需要用 $$
来表示。
变量中的变量
$<
:第一个依赖文件;
$@
:目标;
$^
:所有不重复的依赖文件,以空格分开
= 使用后面定义
1 2 3 4 5
| objects = program.o foo.o utils.o program : $(objects) cc -o program $(objects)
$(objects) : defs.h
|
我们执行“make all”将会打出变量 $(foo) 的值是 Huh?
($(foo) 的值是 \((bar) ,\)(bar) 的值 是 \((ugh) ,\)(ugh) 的值是 Huh? )
把变量的真实值推到后面来定义
1 2
| FLAGS = $(include_dirs) -O include_dirs = -Ifoo -Ibar
|
不好的地方:递归定义
:= 前面的变量不能使用后面的变量
1 2 3 4 5 6
| x := foo y := $(x) bar x := later
y := foo bar x := later
|
前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。
定义空格
1 2
| nullstring := space := $(nullstring)
|
nullstring 是一个 Empty 变量,其中什么也没有,而我们的 space 的值是一个空格
如果 FOO 没有被定义过,那么变量 FOO 的值就是“bar”,如果 FOO 先前被定义过,那么这条语将什么也不做,其等价于:
1 2 3
| ifeq ($(origin FOO), undefined) FOO = bar endif
|
+= 追加变量值
1 2 3 4 5
| objects = main.o foo.o bar.o utils.o objects += another.o
objects = main.o foo.o bar.o utils.o objects := $(objects) another.o
|
所不同的是,用 +=
更为简洁。
变量高级用法
变量值的替换
1 2
| foo := a.o b.o c.o bar := $(foo:.o=.c)
|
另外一种变量替换的技术是以“静态模式”
1 2
| foo := a.o b.o c.o bar := $(foo:%.o=%.c)
|
把变量的值再当成变量
override 指示符
1 2
| override <variable>; = <value>; override <variable>; := <value>;
|
多行变量 define 关键字
1 2 3 4
| define two-lines echo foo echo $(bar) endef
|
环境变量
- CFLAGS 环境变量
- Makefile 中定义了 CFLAGS
目标变量
1 2 3
| <target ...> : <variable-assignment>;
<target ...> : overide <variable-assignment>
|
模式变量
使用条件判断
1 2 3 4 5 6 7 8 9
| 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
|
更简洁一些:
1 2 3 4 5 6 7 8 9 10
| 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
|
if
koijm,l;[p;l,]
使用函数
函数调用,很像变量的使用,也是以 $
来标识的,其语法如下:
1 2 3
| $(<function> <arguments>)
${<function> <arguments>}
|
Demo
1 2 3 4 5
| comma:= , empty:= space:= $(empty) $(empty) foo:= a b c bar:= $(subst $(space),$(comma),$(foo))
|
字符串处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| $(subst <from>,<to>,<text>) $(subst ee,EE,feet on the street)
$(patsubst <pattern>,<replacement>,<text>) $(patsubst %.c,%.o,x.c.c bar.c)
$(strip <string>) $(strip a b c )
$(findstring <find>,<in>) $(findstring a,a b c)
$(filter <pattern...>,<text>) sources := foo.c bar.c baz.s ugh.h foo: $(sources) cc $(filter %.c %.s,$(sources)) -o foo
$(filter-out <pattern...>,<text>)
$(sort <list>)
$(word <n>,<text>) $(word 2, foo bar baz)
$(wordlist <ss>,<e>,<text>)
$(words <text>) $(words foo bar baz)
$(firstword <text>)
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))
|
文件名操作函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| $(dir <names...>) $(dir src/foo.c hacks)
$(notdir <names...>) $(notdir src/foo.c hacks)
$(suffix <names...>) $(suffix src/foo.c src-1.0/bar.c hacks)
$(basename <names...>) $(basename src/foo.c src-1.0/bar.c hacks)
$(addsuffix <suffix>,<names...>) $(addsuffix .c, foo bar)
$(addprefix <prefix>,<names...>) $(addprefix src/, foo bar)
$(join <list1>,<list2>) $(join aaa bbb,111 222 333)
$(foreach <var>,<list>,<text>) names := a b c d files := $(foreach n,$(names),$(n).o)
$(if <condition>,<then-part>) $(if <condition>,<then-part>,<else-part>)
$(call <expression>,<parm1>,<parm2>,...,<parmn>) reverse = $(1) $(2)
foo = $(call reverse,a,b)
$(origin <variable>)
ifdef bletch ifeq "$(origin bletch)" "environment" bletch = barf, gag, etc. endif endif
contents := $(shell cat foo) files := $(shell echo *.c)
$(error <text ...>) $(warning <text ...>)
ifdef ERROR_001 $(error error is $(ERROR_001)) endif
ERR = $(error found an error!) .PHONY: err err: $(ERR)
|
make 的运行
make 的退出码
make 命令执行后有三个退出码:
- 0 表示成功执行。
- 1 如果 make 运行时出现任何错误,其返回 1。
- 2 如果你使用了 make 的“-q”选项,并且 make 使得一些目标不需要更新,那么返回 2。
指定 Makefile
指定目标
1 2
| .PHONY: all all: prog1 prog2 prog3 prog4
|
GNU 这种开源软件
all
: 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
clean
: 这个伪目标功能是删除所有被 make 创建的文件。
install
: 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
print
: 这个伪目标的功能是例出改变过的源文件。
tar
: 这个伪目标功能是把源程序打包备份。也就是一个 tar 文件。
dist
: 这个伪目标功能是创建一个压缩文件,一般是把 tar 文件压成 Z 文件。或是 gz 文件。
TAGS
: 这个伪目标功能是更新所有的目标,以备完整地重编译使用。
check
和 test
: 这两个伪目标一般用来测试 makefile 的流程。
检查规则
- -n, --just-print, --dry-run, --recon 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试 makefile 很有用处。
- -t, --touch 这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make 假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
- -q, --question 这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出, 当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
- -W , --what-if=, --assume-new=, --new-file= 这个参数需要指定一个 文件。一般是是源文件(或依赖文件),Make 会根据规则推导来运行依赖于这个文件的命令,一般 来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。
另外一个很有意思的用法是结合 -p 和 -v 来输出 makefile 被执行时的信息。
make 的参数
-debug[=] 输出 make 的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是 输出最简单的调试信息。下面是 的取值:
- a: 也就是 all,输出所有的调试信息。(会非常的多)
- b: 也就是 basic,只输出简单的调试信息。即输出不需要重编译的目标。
- v: 也就是 verbose,在 b 选项的级别之上。输出的信息包括哪个 makefile 被解析,不需要被 重编译的依赖文件(或是依赖目标)等。
- i: 也就是 implicit,输出所以的隐含规则。
- j: 也就是 jobs,输出执行规则中命令的详细信息,如命令的 PID、返回码等。
- m: 也就是 makefile,输出 make 读取 makefile,更新 makefile,执行 makefile 的信息。
隐含规则
1 2 3 4 5 6 7
| foo : foo.o bar.o cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)
foo.o : foo.c cc –c foo.c $(CFLAGS) bar.o : bar.c cc –c bar.c $(CFLAGS)
|
隐含规则一览
默认的后缀列表是: .out, .a, .ln, .o, .c, .cc, .C, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym, .def, .h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch .web, .sh, .elc, .el
。
- 编译 C 程序的隐含规则。
- .o 的目标的依赖目标会自动推导为 .c ,并且其生成命令是 $(CC) –c $(CPPFLAGS) $(CFLAGS)
- 编译 C++ 程序的隐含规则。
- .o 的目标的依赖目标会自动推导为 .cc 或是 .C ,并且其生成命令是 $(CXX) –c $(CPPFLAGS) $(CFLAGS) 。(建议使用 .cc 作为 C++ 源文件的后缀,而不是 .C ) 编译 Pascal 程序的隐含规则。
- 编译 Pascal 程序的隐含规则。
- .o 的目标的依赖目标会自动推导为 .p ,并且其生成命令是 $(PC) –c $(PFLAGS) 。
- 编译 Fortran/Ratfor 程序的隐含规则。
- .o 的目标的依赖目标会自动推导为 .r 或 .F 或 .f ,并且其生成命令是: * .f $(FC) –c $(FFLAGS)
- .F $(FC) –c $(FFLAGS) $(CPPFLAGS)
- .f $(FC) –c $(FFLAGS) $(RFLAGS)
- 预处理 Fortran/Ratfor 程序的隐含规则。
- .f 的目标的依赖目标会自动推导为 .r 或 .F 。这个规则只是转换 Ratfor 或有预处理的 Fortran 程序到一个标准的 Fortran 程序。其使用的命令是:
- .F $(FC) –F $(CPPFLAGS) $(FFLAGS)
- .r $(FC) –F $(FFLAGS) $(RFLAGS)
- 编译 Modula-2 程序的隐含规则。
- .sym 的目标的依赖目标会自动推导为 .def ,并且其生成命令是:$(M2C) $(M2FLAGS) \((DEFFLAGS) 。<n>.o 的目标的依赖目标会自动推导为 <n>.mod ,并且其生成命令是:\)(M2C) $(M2FLAGS) $(MODFLAGS) 。
- 汇编和汇编预处理的隐含规则。
- .o 的目标的依赖目标会自动推导为 .s ,默认使用编译器 as ,并且其生成命令是:$ (AS) \((ASFLAGS) 。<n>.s 的目标的依赖目标会自动推导为 <n>.S ,默认使用 C 预编译器 cpp ,并且 其生成命令是:\)(AS) $(ASFLAGS) 。
- 链接 Object 文件的隐含规则。
- 目标依赖于 .o ,通过运行 C 的编译器来运行链接程序生成(一般是 ld ),其生成命令 是:$(CC) $(LDFLAGS) .o $(LOADLIBES) $(LDLIBS) 。这个规则对于只有一个源文件的工程 有效,同时也对多个 Object 文件(由不同的源文件生成)的也有效。例如如下规则:
- 并且 x.c 、y.c 和 z.c 都存在时,隐含规则将执行如下命令:
1 2 3 4 5 6 7
| 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)相关联,那么,你最好写 出自己的生成规则,不然,隐含规则会报错的。
- Yacc C 程序时的隐含规则。
- .c 的依赖文件被自动推导为 n.y (Yacc 生成的文件),其生成命令是:$(YACC) $(YFALGS)
。(“Yacc”是一个语法分析器,关于其细节请查看相关资料)
- Lex C 程序时的隐含规则。
- .c 的依赖文件被自动推导为 n.l(Lex 生成的文件),其生成命令是:$(LEX) $(LFALGS) 。(关 于“Lex”的细节请查看相关资料)
- Lex Ratfor 程序时的隐含规则。
- .r 的依赖文件被自动推导为 n.l (Lex 生成的文件),其生成命令是:$(LEX) $(LFALGS) 。
- 从 C 程序、Yacc 文件或 Lex 文件创建 Lint 库的隐含规则。
- .ln(lint 生成的文件)的依赖文件被自动推导为 n.c ,其生成命令是:$(LINT) $(LINTFALGS) $(CPPFLAGS) -i 。对于 .y 和 .l 也是同样的规则。
隐含规则使用的变量
例如,第一条隐含规则——编译 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 的编译器生成。我们把这一系列的隐含规则叫做“隐含规则链”。
使用 make 更新函数库文件
函数库文件的成员
这个不是一个命令,而一个目标和依赖的定义。一般来说,这种用法基本上就是为了 ar 命令来服务 的。如:
1 2
| foolib(hack.o) : hack.o ar cr foolib hack.o
|
函数库文件的后缀规则
1 2 3 4
| .c.a: $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o $(AR) r $@ $*.o $(RM) $*.o
|
等效于
1 2 3 4
| (%.o) : %.c $(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o $(AR) r $@ $*.o $(RM) $*.o
|