makefile学习笔记
概述
C/C++ 编译流程: 编译、汇编、链接
在汇编完成后,会生成中间文件。windows下是.obj
,Unix下是.o
。
编译时,只要源代码的语法正确,编译器就可以编译出中间目标文件。
链接时,主要是链接函数和全局变量。所以,我们可以使用这些中间目标文件( .o
文件或 .obj
文件)来链接我们的应用程序。在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便。所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib
文件,在UNIX下,是Archive File,也就是 .a
文件。
参考
基本就是下面的教程的搬运+一点的自己理解
makefile介绍
make命令执行时,需要一个makefile文件,以告诉make命令需要怎么样的去编译和链接程序。 在第一个项目中,有一个fun.h
的头文件,main.c
fun.c
两个c文件。
makefile规则
粗略地看一下makefile规则: 1
2
3
4target ... : prerequisites ...
command
...
...
target
1 | 可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。 |
prerequisites
1 | 生成该target所依赖的文件和/或target |
command
1 | 该target要执行的命令(任意的shell命令) |
第一个示例
第一个项目有1个头文件和2个c文件,对应地makefile如下: 1
2
3
4
5
6
7
8
9
10
11main : main.o fun.o
cc -o main main.o fun.o
main.o : main.c fun.h
cc -c main.c
fun.o : fun.c fun.h
cc -c fun.c
clean :
rm main main.o fun.o
当make发现main未生成或main后面的依赖列表中存在比main更新的,就会执行下面的语句重新编译。
同理,当它后面的依赖没生成或太旧了,也会执行其对应的指令。整个编译过程环环相扣。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件(make会找makefile的第一个目标文件(target),作为最终的目标文件)。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错。而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在找了依赖关系之后,冒号后面的文件还是不在,make就会直接退出,并报错。
还有一点,这里 clean 不是一个文件,它只不过是一个动作名字,有点像c语言中的label一样,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个label的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。
makefile中使用变量
从上面的例子可以看到,依赖列表被重复了两次(一次在依赖中,一次在下面的编译命令中)。这时可以使用变量,代表这个依赖。
1 | objects = main.o fun.o |
使用变量后,makefile变成这样: 1
2
3
4
5
6
7
8
9
10
11
12
13objs = main.o fun.o
main : $(objs)
cc -o main $(objs)
main.o : main.c fun.h
cc -c main.c
fun.o : fun.c fun.h
cc -c fun.c
clean :
rm main $(objs)
这样一来,如果有新的 .o 文件加入,只需简单地修改一下 objects 变量就可以了。
让make自动推导
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个 .o 文件后都写上类似的命令,因为,make会自动识别,并自己推导命令。
只要make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个 whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,makefile再也不用写得这么复杂。新makefile如下:
1 | objs = main.o fun.o |
上面文件内容中, .PHONY 表示 clean 是个伪目标文件。
清空目标文件
每个makefile都最好提供一个清空目标文件的指令。约定熟成:将这个指令放在makefile最后面。
makefile里面有什么
Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用
#
字符,这个就像C/C++中的//
一样。如果你要在你的Makefile中使用#
字符,可以用反斜杠进行转义,如:\#
。
最后,还值得一提的是,在Makefile中的命令,必须要以 Tab
键开始。
makefile 文件名
默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。 你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的 -f
和 --file
参数,如: make -f Make.Linux
或 make --file Make.AIX
。
引用其它makefile
在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include
,被包含的文件会原模原样的放在当前文件的包含位置。 include
的语法是(): 1
include filename ...
filename可以是shell的格式(可以包含路径和通配符),可以有多个filename(用空格隔开)。
一个例子: 1
include foo.make *.mk $(bar)
include
所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 #include
指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找: 1. 如果make执行时,有 -I
或 --include-dir
参数,那么make就会在这个参数所指定的目录下去寻找。
- 如果目录
<prefix>/include
(一般是:/usr/local/bin
或/usr/include
)存在的话,make也会去找。
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如: 1
-include <filename>
其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和这一个是一样的。
环境变量MAKEFILES
如果你的当前环境中定义了环境变量 MAKEFILES
,那么,make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。
建议不要使用这个环境变量,因为只要这个变量一被定义,所有的Makefile都会受到它的影响,也许有时候Makefile出现了怪事,可以看看当前环境中有没有定义这个变量。
make工作方式
GNU的make工作时的执行步骤如下:(想来其它的make也是类似)
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。
参考
书写规则
文件搜寻
Makefile文件中的特殊变量 VPATH。当指明了该变量,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。 1
VPATH = src:../headers
vpath <pattern> <directories>
为符合模式<pattern>
的文件指定搜索目录<directories>
。vpath <pattern>
清除符合模式<pattern>
的文件的搜索目录。vpath
清除所有已被设置好了的文件搜索目录。例如
1
vpath = %.h ../headers
该语句表示,要求make在''../headers'目录下搜索所有以 .h 结尾的文件。(如果某文件在当前目录没有找到的话)
我们可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的
伪目标
“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。 1
.PHONY : clean
1
2
3
4
5
6
7
8
9
10
11all : 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
目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子: 1
2
3
4
5
6
7
8
9
10.PHONY : cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
多目标
Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。减少冗余代码。多个目标的生成规则的执行命令不是同一个,这可能会给我们带来麻烦,不过好在我们可以使用一个自动化变量 \(@ (关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合,这样说可能很抽象,还是看一个例子吧。 1
2bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@1
2
3
4bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是替换字符串的意思,
\(@` 表示目标的集合,就像一个数组, `\)@` 依次取出目标,并执于命令。
静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法: 1
2
3<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...
target-pattern是指明了targets的模式,也就是的目标集模式。
prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。
例子: 1
2
3
4
5
6objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@$(objects)
中获取, %.o
表明要所有以 .o
结尾的目标,也就是 foo.o bar.o
,也就是变量 $(objects)
集合的模式,而依赖模式 %.c
则取模式 %.o
的 %
,也就是 foo bar
,并为其加下 .c
的后缀,于是,我们的依赖目标就是 foo.c bar.c
。而命令中的 $<
和 $@
则是自动化变量, $<
表示第一个依赖文件, $@
表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则: 1
2
3
4foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
1 | files = foo.elc bar.o lose.o |
$(filter %.o,$(files))
表示调用Makefile的filter函数,过滤“$(files)
”集,只要其中模式为“%.o”的内容。其它的内容,我就不用多说了吧。这个例子展示了Makefile中更大的弹性。
自动生成依赖
源文件可能包含头文件,会产生一个依赖关系。例如如果main.c
包含头文件defs.h
。那么就有一个依赖关系: 1
main.o : main.c defs.h
-M
参数,会打印出依赖关系,例如 1
cc -M main.c
1
main.o : main.c defs.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21main.o: main.c /usr/include/stdc-predef.h defs.h /usr/include/stdio.h \
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \
/usr/include/x86_64-linux-gnu/bits/wordsize.h \
/usr/include/x86_64-linux-gnu/bits/long-double.h \
/usr/include/x86_64-linux-gnu/gnu/stubs.h \
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
/usr/lib/gcc/x86_64-linux-gnu/9/include/stddef.h \
/usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h \
/usr/include/x86_64-linux-gnu/bits/types.h \
/usr/include/x86_64-linux-gnu/bits/timesize.h \
/usr/include/x86_64-linux-gnu/bits/typesizes.h \
/usr/include/x86_64-linux-gnu/bits/time64.h \
/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h \
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \
/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h \
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \
/usr/include/x86_64-linux-gnu/bits/types/FILE.h \
/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h \
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
1 | %.d: %.c |
这个规则的意思是,所有的 .d
文件依赖于 .c
文件, rm -f $@
的意思是删除所有的目标,也就是 .d
文件,第二行的意思是,为每个依赖文件 $<
,也就是 .c
文件生成依赖文件, $@
表示模式 %.d
文件,如果有一个C文件是name.c,那么 %
就是 name , $$$$
意为一个随机编号,第二行生成的文件有可能是“name.d.12345”,第三行使用sed
命令做了一个替换,关于sed
命令的用法请参看相关的使用文档。第四行就是删除临时文件。 总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,即把依赖关系:
1 | main.o : main.c defs.h |
转成: 1
main.o main.d : main.c defs.h
这样每个.d
文件文件都可以自动更新了。最后,添加include
指令将这些.d
文件全部导入进来,这些.d
文件就是内含依赖关系,这样就可以导入依赖关系了。
书写命令
命令的书写
- 每条命令会按照shell命令按顺序运行,每条命令必须以
Tab
开头,除非命令是紧跟在依赖规则后面的分号后的。 - 在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令。
- make的命令默认是被
/bin/sh
——UNIX的标准Shell 解释执行的。除非特别指定一个其它的Shell。
显示命令
使用@不显示命令
例:
@echo 123
可以不打印命令到屏幕上,只是运行它。使用make的参数
-n
或--just-print
可以只显示命令而不运行,有助于调试。(此时加了@的命令同样会被打印)使用make的
-s
或--silent
或--quiet
则是全面禁止命令的显示。
命令执行
make执行命令应该是为每个命令fork一个进程来运行,因此如果使用了cd
,并不会改变后续命令的工作目录。 如果需要将将第一条命令的结果应用到第二条命令,用;
,例如:
1 | exec: |
将打印/home/ubuntu
, 而 1
2
3exec:
cd /home/ubuntu
pwd
命令出错
当命令运行完成后,make会检查每条命令是否出错(返回值非0),如果出错,会终止执行当前规则。这可能导致终止所有规则。
对于不打算检查出错命令,例如mkdir
(文件夹存在时会放回非0值,但确保文件夹存在就是我们期望的)或rm
(如果删除文件不存在,会放回非0值,确保文件不存在就是我们期望的),在命令前面添加-
(在Tab
后面)。 1
2clean:
-rm -f *.o
make使用参数-i
或--ignore-errors
可以忽略所有错误; 使用参数-k
或--keep-going
可以终止出错的规则,但继续执行其它规则。 而如果一个规则是以 .IGNORE
作为目标的,那么这个规则中的所有命令将会忽略错误。例如 1
.IGNORE : clean
嵌套执行make
当项目很大时,不同模块文件夹下可能有各自的makefile,这时需要一个总控makefile来调用分散在其它模块中的makefile。 make提供了一些参数,有助于在makefile中嵌套使用make。例如进入某一目录执行该目录下的makefile: 1
2subsystem:
cd subdir && $(MAKE)1
2subsystem:
$(MAKE) -C subdir
这里$(MAKE)是make命令的宏变量,因为嵌套运行make可能还需要带有一些参数。
Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了 -e 参数。
在make指令后面指定变量,例如make var=value -C subdir
,然后在子makefile中就可以使用传入的var
变量
或是在父makefile中export <variable ...>
显式指定要传入的子makefile的变量。单独的export
会把所有变量传入子makefile。例如: 1
2
3export variable1 = value
export variable2 += value
export variable2 := value
如果不想让某些变量传递到下一层,可以声明unexport <variable ...>
有两个变量,一个是 SHELL ,一个是 MAKEFLAGS ,这两个变量不管你是否export,其总是要传递到下层 Makefile中,特别是 MAKEFLAGS 变量,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层 Makefile中定义了这个变量,那么 MAKEFLAGS 变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。但是make命令中的有几个参数并不往下传递,它们是 -C , -f , -h, -o 和 -W 。如果不想往下层传递参数,可以 1
2subsystem:
cd subdir && $(MAKE) MAKEFLAGS=-w
或--print-directory
会在make过程中输出工作目录的变化。当使用 -C
参数指定make下层makefile时,-w
会被自动打开。如果参数中含有-s
(--slient
或--no-print-directory
),那么-w
会失效。
定义命令包
除了定义值的变量,还可以为命令序列定义一个变量,便于复用。命令序列的语法以 define
开始,以 endef
结束,如(注意可以不用Tap
开头): 1
2
3
4define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef1
2foo.c : foo.y
$(run-yacc)
使用变量
变量基础
变量使用时要在变量名前面加上$
符号,最好用()
或{}
把变量包起来(给变量加上括号完全是为了更加安全地使用这个变量)。如果要使用真实的 $
字符,那么你需要用 $$
来表示。 变量会在使用它的地方精确地展开,就像C/C++中的宏一样。
变量中的变量
变量可以被一个变量定义,一种定义方法如下: 1
2foo = $(hi)
hi = 123
1 | CFLAGS = $(CFLAGS) -O |
如果在变量中使用函数(例如$(shell pwd)
),可能会导致未知错误,你不知道这个函数会被调用多少次。
为了避免这种情况,可以使用:=
定义,这样一个变量只能使用前面定义好的变量,不能使用后面的变量。 1
2
3x := foo
y := $(x) bar // 将等价于 y := foo bar
x := later
1 | y := $(x) bar // 将等价于 y := bar |
定义变量时有一些需要注意的事情。如果需要定义一个空格,可以借助注释符#
实现 1
2nullstr :=
space := $(nullstr) # end of the line#
结尾标识结尾。这样就有了一个空格的变量。 注释符#
这个特性要特别地注意,如果不小心误用,可以会出现意想不到的结果。例如
1 | dir = /foo/bar # dir |
此时dir的值会是/foo/bar
(后跟4个空格),如果后面用了$(dir)/file
,就完蛋了。
还有一个有用的操作符?=
。如果它左侧的变量没被定义过,那就将该变量赋为右侧的值;否则什么也不干。
变量高级用法
- 变量值的替换
格式:$(var:a=b)
或${var:a=b}
。将变量var
中以a串结尾替换为b串,’结尾‘代表被空格或结束符隔开。例子:
1 | foo := a.o b.o c.o |
bar
的值为"a.c b.c c.c"。
- “把变量的值再当成变量”
例子:
1 | x = y |
a
的值为"z"。
使用这种方式,还可以使用多个变量组成一个变量的名字:
1 | first_second = Hello |
all
的值为"Hello"。
追加变量值
使用+=
追加。
如果变量之前没有定义过,那么, +=
会自动变成 =
,如果前面有变量定义,那么 +=
会继承于前次操作的赋值符。如果前一次的是 :=
,那么 +=
会以 :=
作为其赋值符,如:
1 | variable := value |
等价于
1 | variable := value |
多行变量
define
指示符可以定义含有多行内容的变量。前面讲过的“命令包”的技术就是利用这个关键字。
define
指示符后面跟着变量名,从第二行开始是变量内容,以endef结尾。
1 | define two-lines |
override指示符
如果有变量是通过命令行指定的,那么在makefile中对该变量的赋值就会被忽略。如果不希望被忽略,可以使用在赋值语句前使用override指示符赋值。
格式如下:
1 | override <variable> = <value> |
例如对于CFLAG
变量,无论如何都要加一个-g
参数来调试。为了防止被命令行赋值覆盖,可以
1 | override CFLAGS += –g |
对于多行变量定义同样可以,在define
前使用override
即可。
环境变量
make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)
因此,如果我们在环境变量中设置了 CFLAGS
环境变量,那么我们就可以在所有的Makefile中使用这个变量了。这对于我们使用统一的编译参数有比较大的好处。如果Makefile中定义了CFLAGS,那么则会使用Makefile中的这个变量,如果没有定义则使用系统环境变量的值,一个共性和个性的统一,很像“全局变量”和“局部变量”的特性。
当make嵌套调用时(参见前面的“嵌套调用”章节),上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层Makefile传递,则需要使用export关键字来声明。(参见前面章节)
目标变量
前面都是全局变量,同样可以对特定的目标设置变量。这样设置的变量可以传递到由这个目标引发的所有规则过去。格式如下:
1 | <target ...> : <variable-assignment> |
例如:
1 | prog : CFLAGS = -g |
这样不管全局的CFLAGS是什么,在prog目标以及其引发的所有规则中,$(CFLAGS)
都是-g
。
模式变量
还可以对模式变量设置目标变量,格式如下:
1 | <pattern ...>; : <variable-assignment>; |
例如:
1 | %.o : CFLAGS = -O |
这样所有.o
结尾的目标的CFLAGS
都是-O
了。
条件判断
ifeq
、ifneq
示例
1 | libs_for_gcc = -lgnu |
ifeq的语法如下:
1 | ifeq (<arg1>, <arg2>) |
ifneq类似
ifdef
、ifndef
ifdef可以查看变量是否被定义,语法如下:
1 | ifdef <variable-name> |
注意ifdef
只是测试一个变量是否有值,其并不会把变量扩展到当前位置。例如
1 | bar = |
1 | foo = |
第一个例子中, $(frobozz)
值是 yes
,第二个则是 no
。
还有ifndef
,就是ifdef
的反义词。
特别注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把自动化变量(如 $@
等)放入条件表达式中,因为自动化变量是在运行时才有的。
使用函数
函数的调用语法
和变量非常类似,语法如下:
1 | $(<function> <arguments>) |
函数名和参数间用空格隔开,参数之间用逗号隔开。例子:
1 | comma:= , |
subst
是一个替换函数,第一个参数是被替换字符串,第二个参数替换的字符串,第三个参数替换操作作用的字符串。执行完上面的指令后,$(foo)
的a b c
替换成a,b,c
赋给bar
。
make有很多函数可用,具体查看使用函数 — 跟我一起写Makefile。
make的运行
make退出码
0:表示成功执行
1:如果make运行时出现任何错误,返回1
2:如果使用"-q"参数,并且make使得一些目标不需要更新,那么返回2
指定makefile
假如你有一个makefile文件hchen.mk
,可用使用-f
(或--file
,--makefile
)参数告诉make使用哪个makefile。
1 | make -f hchen.mk |
指定目标
默认情况下,make的最终目标是makefile中第一个目标。也可以在make之后添加目标名指定最终目标,例如make clean
,说明最终目标是clean
。任何在makefile中的目标都可以被指定成终极目标,但是除了以 -
打头,或是包含了 =
的目标,因为有这些字符的目标,会被解析成命令行参数或是变量。甚至没有被我们明确写出来的目标也可以成为make的终极目标,也就是说,只要make可以在隐含规则推导中找到这个隐含目标,同样可以被指定成终极目标。
make的环境变量MAKECMDGOALS
存放你指定的终极目标列表。如果在命令行上没有指定最终目标,这个变量是空值。这个变量可用用在一些特殊情况下,例如:
1 | sources = foo.c bar.c |
上面指令功能是只要输入的命令不是"make clean",那么makefile会自动加载"foo.d"和"bar.d"这两个makefile。
在Unix世界中,软件发布时,特别是GNU这种开源软件的发布时,其makefile都包含了编译、安装、打包等功能。我们可以参照这种规则来书写我们的makefile中的目标。
- all:这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
- clean:这个伪目标功能是删除所有被make创建的文件。
- install:这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
- print:这个伪目标的功能是例出改变过的源文件。
- tar:这个伪目标功能是把源程序打包备份。也就是一个tar文件。
- dist:这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。
- TAGS:这个伪目标功能是更新所有的目标,以备完整地重编译使用。
- check和test:这两个伪目标一般用来测试makefile的流程。
为自己的makefile添加上面功能,更显专业。
检查规则
有时我们并不想要运行makefile,而是想要检查makefile执行情况,下面参数会有所帮助:
1 | -n`, `--just-print`, `--dry-run`, `--recon |
不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。
1 | -t`, `--touch |
这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
1 | -q`, `--question |
这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
1 | -W <file>`, `--what-if=<file>`, `--assume-new=<file>`, `--new-file=<file> |
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。
make参数
不同版本厂商make参数大同小异,GNU make 3.80版见make的参数。
makefile常用的代码片段
下面列出一些常用需求对应的makefile代码,以便快速上手:
施工中
makefile学习笔记