如果程序包含很多个源文件,用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.ou
t所依赖的.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
函数当中。