如果程序包含很多个源文件,用gcc命令逐个去编译时,就发现很容易混乱而且工作量大,所以就出现了make工具
make工具可以看成是一个智能的批处理工具,它本身并没有编译和链接的功能,而是用类似于批处理的方式——通过调用makefile文件中用户指定的命令来进行编译和链接的。
makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile命令中就包含了调用gcc(也可以是别的编译器)去编译某个源文件的命令。但是当工程非常大的时候,手写makefile也是非常麻烦的,而且如果换了个平台makefile又要重新修改,这时候就出现了Cmake这个工具。
cmake就可以更加简单的生成makefile文件给make用。当然cmake还可以跨平台生成对应平台能用的makefile,我们就不用再自己去修改了。cmake根据一个叫CMakeLists.txt文件(学名:组态档)去生成makefile。
总的来说 就是由cmake来跨平台生成makefile,再由make/ninja等编译工具根据makefile的规范进行编译
语法规则
Makefile的规则:
target ... : prerequisites ...
command
...
...
targets是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。
command是命令行,如果其不与target:prerequisites在一行,那么,必须以[Tab键]开头,如果和prerequisites在一行,那么可以用分号做为分隔。
prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是过时的,被认为是需要重生成的。
如果命令太长,可以使用反斜框(/)作为换行符。make对一行上有多少个字符没有限制。
规则告诉make两件事,文件的依赖关系和如何成成目标文件。
一般来说,make会以UNIX的标准Shell,也就是/bin/sh来执行命令。
文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。
说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心
的内容。
我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,清除中间文件等等。
可以定义一个目标文件其冒号后什么也没有,那么,make就不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable(也就是target)的名字。

其中clean为清除中间文件的命令 调用时使用make clean调用
编译makefile
在默认的方式下,也就是我们只输入make命令。那么,
make默认会在当前目录下找名字叫Makefile或makefile的文件。要指定特定的Makefile,你可以使用make的-f和--file参数,如:
make -f Make.Linux
make --file Make.AIX
- 如果找到
makefile文件,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到demo.out这个文件,并把这个文件作为最终的目标文件。 - 如果
demo.out文件不存在,或是demo.out所依赖的后面的.o文件的文件修改时间要比这个目标文件新,那么,他就会执行后面所定义的命令来生成demo.out这个文件。 - 如果
demo.out所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程) - 当然,找到最后你的
C源文件和H头文件是存在的啦,于是make会生成.o文件,然后再用 .o文件完成make的终极任务,也就是执行文件demo.out了。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make会忽略。
具体规则
Makefile里主要包含了五个东西:变量定义、显式规则、隐晦规则、文件指示和注释。
- 变量的定义。
Makefile中的变量其实就是C/C++中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
例如上面我们需要编辑的文件有两个 那么我们可以将其定义为objects = test.o demo.o以帮助我们在更多文件时减少重复使用。使用变量时,通过$(变量名)调用。Eg.$(objects)-
使用关键字
wildcard可以让通配符在变量中展开
在Makefile规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数wildcard,它的用法是:$(wildcard PATTERN...)。
在Makefile中,它被展开为已经存在的、使用空格分开的、匹配此模式的所有文件列表。如果不存在任何符合此模式的文件,函数会忽略模式字符并返回空。objects := $(wildcard *.o)这里,
objects的值是所有.o的文件名的集合
-
- 显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由
Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。 刚才写的形似shell脚本的Makefile全部都是显示规则。 - 隐晦规则。由于我们的
make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。 - 文件指示。其包括了三个部分
- 第一个是在一个
Makefile中引用另一个Makefile,就像C语言中的include一样 - 第二个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样
- 第三个就是定义一个多行的命令。
- 第一个是在一个
- 注释。Makefile中只有行注释,和UNIX的
Shell脚本一样,其注释是用#字符,这个就像C/C++中的//一样。如果你要在你的Makefile中使用#字符,可以用反斜框进行转义,如:`/#``。
make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,并且命令 gcc -c xx.c 也会被推导出来,于是,我们的makefile再也不用写得上面这么复杂。
同时 也可以将多个目标文件写在一起共享同一个依赖
为了避免和文件重名的情况,我们可以使用一个特殊的标记.PHONY来显示地指明一个目标是伪目标,向make说明,不管是否有这个文件,这个目标就是伪目标。
.PHONY表示,clean是个伪目标文件

rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。
.SUFFIXES: 用于配置"后缀规则"。后缀规则是.a .b形式的规则,例如在.f .o规则中到的规则。它们是一种告诉make的方法,只要您看到例如.f文件(源文件),就可以按照该规则从其中创建.o文件(目标文件).
.SUFFIXES:.c .o
.c.o:
gcc -c -o $@ $<
这等效于%形成模式规则:
%.o: %.c
gcc -c -o $@ $<
在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。
include的语法是:
include <filename>
filename可以是当前操作系统Shell的文件模式(可以保含路径和通配符)
执行步骤
GNU的make工作时的执行步骤如下:
- 读入所有的
Makefile。 - 读入被
include的其它Makefile。 - 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
预定义变量
常用的预定义变量:
$*不包含扩展名的目标文件名称。$+所有的依赖文件,以空格分开,并以出现的先后为序,可能包含重复的依赖文件。$<第一个依赖文件的名称。$?所有的依赖文件,以空格分开,这些依赖文件的修改日期比目标的创建日期晚。$@目前规则中所有的目标的集合。$^所有的依赖文件,以空格分开,不包含重复的依赖文件。$%如果目标是归档成员,则该变量表示目标的归档成员名称。
文件搜索
可以通过 定义特殊变量VPATH,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。
目录由:分隔。(当然,当前目录永远是最高优先搜索的地方)
VPATH = src : ../header
另一个设置文件搜索路径的方法是使用make的vpath关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:
-
为符合模式
<pattern>的文件指定搜索目录<directories>。vpath <pattern> <directories> -
清除符合模式
的文件的搜索目录。 vpath <pattern> -
清除所有已被设置好了的文件搜索目录。
vpath
vapth使用方法中的<pattern>需要包含%字符。%的意思是匹配零或若干字符,例如,%.h表示所有以.h结尾的文件。
<pattern>指定了要搜索的文件集,而<directories>则指定了<pattern>的文件集的搜索的目录。
vpath %.h ../headers
该语句表示,要求make在../headers目录下搜索所有以.h结尾的文件。(如果某文件在当前目录没有找到的话)
静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。语法:
<targets ...>: <target-pattern>: <prereq-patterns ...>
<commands>
...
targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern是指明了targets的模式,也就是目标集模式,是从targets中取出的目标集。
prereq-parrterns是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。即对targets中取出的目标集更改%后内容。
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)。
书写命令
通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用@字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:
@echo 正在编译XXX模块......
当make执行时,会输出正在编译XXX模块......字串,但不会输出命令,如果没有@,那么,make将输出:
echo 正在编译XXX模块......
正在编译XXX模块......
make执行时,带入make参数-n或--just-print,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。
如果要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。
每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。
但有些时候,命令的出错并不表示就是错误的。例如mkdir命令,我们一定需要建立一个目录,如果目录不存在,那么mkdir就成功执行,万事大吉,如果目录存在,那么就出错了。我们之所以使用mkdir的意思就是一定要有这样的一个目录,于是我们就不希望mkdir出错而终止规则的运行。
为了做到这一点,需要忽略命令的出错,我们可以在Makefile的命令行前加一个减号-(在Tab键之后),标记为不管命令出不出错都认为是成功的。
嵌套执行make
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁
例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:
subsystem:
cd subdir && $(MAKE)
我们把这个Makefile叫做总控Makefile,总控Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了-e参数。
如果你要传递变量到下级Makefile中,那么你可以使用这样的声明:
export <variable ...>
如果你要传递所有的变量,那么,只要一个export就行了,后面什么也不用跟。
如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:
unexport <variable ...>
定义命令包
如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以define开始,以endef结束,如:
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
这里,run-yacc是这个命令包的名字,其不要和Makefile中的变量重名。在define和endef中间的两行就是命令序列。
变量值的替换。
我们可以替换变量中的共有的部分,其格式是$(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。
我们可以使用+=操作符给变量追加值,如:
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被追加进去了)
局部变量
前面我们所讲的在Makefile中定义的变量都是全局变量,在整个文件,我们都可以访问这些变量。
同样 我们也可以为某个目标设置局部变量, 它可以和全局变量同名, 它的作用范围只在这条规则以及连带规则中,所以其值也只在作用范围内有效。而不会影响规则链以外的全局变量的值。
<target ...> : <variable-assignment>
<target ...> : overide <variable-assignment>
如果设置了这样一个局部变量,这个变量会作用到由这个目标所引发的所有的规则中去
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) $(CFLAGS) bar.c
在这个示例中,不管全局的$(CFLAGS)的值是什么,在prog目标,以及其所引发的所有规则中(prog.o foo.o bar.o的规则),$(CFLAGS)的值都是-g
模式变量
局部变量可以定义在某个目标上,模式变量的好处就是,我们可以给定一种模式,可以把变量定义在符合这种模式的所有目标上。
可以以如下方式给所有以.o结尾的目标定义目标变量:
%.o : CFLAGS = -O
条件判断
使用条件判断,可以让make根据运行时的不同情况选择不同的执行分支。条件表达式可以是比较变量的值,或是比较变量和常量的值。
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo: $(objects)
$(CC) -o foo $(objects) $(libs)
ifneq ifeq 测试两个变量是否相等
ifdef ifndef 测试一个变量是否有值,其并不会把变量扩展到当前位置
使用函数
函数调用,很像变量的使用,也是以$来标识的,其语法如下:
$(<function> <arguments>)
<function>就是函数名,make支持的函数不多。<arguments>是函数的参数,参数间以逗号,分隔,而函数名和参数之间以空格分隔。
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
$(comma)的值是一个逗号。
$(space)使用了$(empty)定义了一个空格
$(foo)的值是a b c
$(bar)的定义调用了函数subst,这是一个替换函数,这个函数有三个参数,第一个参数是被替换字串,第二个参数是替换字串,第三个参数是替换操作作用的字串。这个函数也就是把$(foo)中的所有空格替换成逗号,所以$(bar)的值是a,b,c。
字符串函数
$(subst <from>,<to>,<text>)
把字串<text>中的<from>字符串替换成<to>并返回被替换过后的字符串
$(patsubst <pattern>,<replacement>,<text>)
查找<text>中的单词(单词以空格、Tab或回车 换行分隔)是否符合模式<pattern>,如果匹配的话,则以<replacement>替换。返回被替换过后的字符串
$(strip <string>)
去掉<string>字串中开头和结尾的空字符。
$(findstring <find>,<in>)
在字串<in>中查找<find>字串。如果找到,那么返回<find>,否则返回空字符串。
$(word <n>,<text>)
取字符串<text>中第<n>个单词。(从一开始)
$(sort <list>)
给字符串<list>中的单词排序(升序)
$(foreach <var>,<list>,<text>)
把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。
每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。所以,<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会使用<var>这个参数来依次枚举<list>中的单词。
names := a b c d
files := $(foreach n,$(names),$(n).o)
$(name)中的单词会被挨个取出,并存到变量n中,$(n).o每次根据$(n)计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)的值是a.o b.o c.o d.o。
注意,foreach中的<var>参数是一个临时的局部变量,foreach函数执行完后,参数<var>的变量将不在作用,其作用域只在foreach函数当中。