在linux上,GNU make原生支持makefile语法,很多c/c++的工程都是基于makefile来管理,来自动化编译。
如果需要跨平台,可以考虑使用cmake或gyp之类的配置来管理工程,在linux上会调用make,在mac上会基于xcode,在windows上会基于vs。
因此如果应用面向的只是linux系统,那么使用makefile来管理工程是方便直接的。
基本语法
target: prerequisites
command
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。
如果命令太长,可以使用反斜框(\
)作为换行符,同时反斜框还可以用于转义,如:\#
,\*
注释使用#号
关于自动推导
make支持一些隐晦的规则,可以自动推到
比如对象 main.o 会自动寻找 main.c|.cc|.cpp
清空目标文件的规则
一般的风格都是:
clean:
rm edit $(objects)
包含文件
在 Makefile 使用 include 关键字可以把别的 Makefile 包含进来:
include filename
一般被依赖的makefile文件后缀使用 .mk
,并且支持通配符和变量
include *.mk $(bar)
make 的工作方式
GNU 的 make 工作时的执行步骤入下:(想来其它的 make 也是类似)
- 读入Makefile文件
- 读入被 include 的其它mk文件
- 初始化文件中的变量
- 推导隐晦规则,并分析所有规则
- 为所有的目标文件创建依赖关系链
- 根据依赖关系,决定哪些目标要重新生成
- 执行生成命令
1-5 步为第一个阶段,6-7 为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么, make 会把其展开在使用的位置。但 make 并不会完全马上展开,make 使用的是拖延战术,如 果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展 开。
目标规则
在Makefile中,规则的顺序是很重要的,因为,Makefile中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让make知道你的最终目标是什么。一般来说,定义在Makefile中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make所完成的也就是这个目标。
目标命名规范
在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。
- all
- 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
- clean
- 这个伪目标功能是删除所有被make创建的文件。
- install
- 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
- tar
- 这个伪目标功能是把源程序打包备份。也就是一个tar文件。
- check和test
- 这两个伪目标一般用来测试makefile的流程。
使用通配符
make支持三各通配符:“*”,“?”和“[…]”。这是和Unix的B-Shell是相同的。
字符 | 含义 | 实例 |
---|---|---|
* | 匹配 0 或多个字符 | a*b a与b之间可以有任意长度的任意字符, 也可以一个也没有, 如aabcb, axyzb, a012b, ab。 |
? | 匹配任意一个字符 | a?b a与b之间必须也只能有一个字符, 可以是任意字符, 如aab, abb, acb, a0b。 |
[list] | 匹配 list 中的任意单一字符 | a[xyz]b a与b之间必须也只能有一个字符, 但只能是 x 或 y 或 z, 如: axb, ayb, azb。 |
[!list] | 匹配 除list 中的任意单一字符 | a[!0-9]b a与b之间必须也只能有一个字符, 但不能是阿拉伯数字, 如axb, aab, a-b。 |
[c1-c2] | 匹配 c1-c2 中的任意单一字符 | [0-9] [a-z] a[0-9]b 0与9之间必须也只能有一个字符 如a0b, a1b… a9b。 |
注意:
波浪号(“~”)字符
波浪号在文件名中也有比较特殊的用途。如果是“~/test”,这就表示当前用户的$HOME目录下的test目录。
关于变量中使用通配符
objects = *.o
上面这个例子,表示了,通符同样可以用在变量中。并不是说[.o]会展开,不!objects的值就是“.o”。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有[.o]的文件名的集合,那么,你可以这样:
objects := $(wildcard *.o)
这种用法由关键字“wildcard”指出,关于Makefile的关键字
伪目标
“伪目标”并不是一个文件,只是一个标签,只有通过显示地指明这个“目标”才能让其生效。
为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”
比如清理操作:
.PHONY : clean
clean :
-rm edit $(objects)
.PHONY 意思表示 clean 是一个“伪目标”,。而在 rm 命令前面加了一个小 减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean 的规 则不要放在文件的开头,不然,这就会变成 make 的默认目标,相信谁也不愿意这样。不成 文的规矩是——“clean 从来都是放在文件的最后”
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。
一个示例就是,如果你的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
静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。
我们还是先来看一下语法:
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
- targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
- target-parrtern是指明了targets的模式,也就是的目标集模式。
- prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。
看一个例子:
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
上面的例子中:
- 目标从$object中获取,
%.o
表明要所有以.o
结尾的目标,也就是foo.o bar.o
,也就是变量$object集合的模式, - 而依赖模式
%.c
则取模式%.o
的%
,也就是foo bar
,并为其加下.c
的后缀,于是,我们的依赖目标就是foo.c bar.c
。 - 而命令中的
$<
和$@
则是自动化变量,$<
表示所有的依赖目标集(也就是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
静态模式的过滤规则
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))表示调用Makefile的filter函数,过滤“$filter”集,只要其中模式为“%.o”的内容。
自动生成依赖性
如果是一个比较大型的工程,你必需清楚哪些C/C++文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改Makefile,这是一个很没有维护性的工作。
为了避免这种繁重而又容易出错的事情,我们可以使用GNU的C/C++编译器的一个功能,用-MM
参数,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
命令执行
当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。
需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:
exec:
cd /home/suninf; pwd
pwd会打印出“/home/suninf”
命令出错
如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。
忽略命令的出错,我们可以在Makefile的命令行前加一个减号“-”(在Tab键之后),标记为不管命令出不出错都认为是成功的。如:
clean:
-rm -f *.o
变量
一些重复使用的内容,可以用变量来代替使用
objects = main.o kbd.o command.o display.o
edit : $(objects)
cc -o edit $(objects)
变量的命名字可以包含字符、数字,下划线(可以是数字开头),变量是大小写敏感的,传统的Makefile的变量名是全大写的命名方式。
变量在声明时需要给予初值,而在使用时,需要给在变量名前加上“$”符号,但最好用小括号“()”或是大括号“{}”把变量给包括起来。如果你要使用真实的“$”字符,那么你需要用“$$”来表示。
变量会在使用它的地方精确地展开,就像C/C++中的宏一样,例如:
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
展开后得到:
prog.o : prog.c
cc -c prog.c
关于变量递归定义
简单的使用“=”号,在“=”左侧是变量,右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。
但是这会让make陷入无限的变量展开过程中去:
A = $(B)
B = $(A)
使用:=
操作符
这种方法,前面的变量不能使用后面的变量
# x := foo
y := $(x) bar
x := later
由于x未先定义,则$(x)为空,y为bar
+=
操作符给变量追加值
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
系统变量说明
- CC
- C语言编译程序。默认命令是“cc”。
- CXX
- C++语言编译程序。默认命令是“g++”。
- CFLAGS
- C语言编译器参数。
- CXXFLAGS
- C++语言编译器参数。
$@
- 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,”$@“就是匹配于目标中模式定义的集合。
$<
- 依赖目标中的第一个目标名字。如果依赖目标是以模式(即”%“)定义的,那么”$<“将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
$^
- 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
- 依赖目标中的第一个目标名字。如果依赖目标是以模式(即”%“)定义的,那么”$<“将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
函数与条件语句
使用条件判断
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif
例如:
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)
ifeq,ifneq,ifdef,ifndef
使用函数
函数调用语法
函数调用,很像变量的使用,也是以“$”来标识的,其语法如下:
$(<function> <arguments>)
字符串操作函数
- subst:字符串替换
- patsubst:模式字符串替换函数
- strip:去掉字串中开头和结尾的空字符
- findstring:查找字符串函数
- filter:模式过滤
- sort:排序
- word:获取字符串中的第N个字符
- wordlist:子字符串
- words:统计字符个数
- firstword:第一个字符
- join:连接函数
文件名操作函数
- dir:取目录
- notdir:取文件名非目录部分
- suffix:取后缀函数
- basename:取前缀
- addsuffix:加后缀
- addprefix:加前缀
foreach函数
$(foreach <var>, <list>, <text>)
把参数中的单词逐一取出放到参数所指定的变量中,然后再执行
names := a b c d
files := $(foreach n, $(names), $(n).o)
返回:”a.o b.o c.o d.o”
if 函数
$(if <condition>, <then-part> [, <else-part>])
call函数
call函数是唯一一个可以用来创建新的参数化的函数。这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。
其语法是:
$(call <expression>, <parm1>, <parm2>, <parm3>...)
例如:
reverse = $(1) $(2)
foo = $(call reverse, a, b)
foo的值就是“a b”
shell函数
它的参数应该就是操作系统Shell的命令。它和反引号“`”是相同的功能。这就是说,shell函数把执行操作系统命令后的输出作为函数返回。于是,我们可以用操作系统命令以及字符串处理命令awk,sed等等命令来生成一个变量,如:
contents := $(shell cat foo)