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
4
target ... : 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
11
main : 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
13
objs = 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
2
3
4
5
6
7
8
9
10
11
objs = main.o fun.o

main : $(objs)
cc -o main $(objs)

main.o : fun.h
fun.o : fun.h

.PHONY : clean
clean :
rm main $(objs)

上面文件内容中, .PHONY 表示 clean 是个伪目标文件。

清空目标文件

每个makefile都最好提供一个清空目标文件的指令。约定熟成:将这个指令放在makefile最后面。

makefile里面有什么

Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

  1. 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。

  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。

  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。

  4. 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。

  5. 注释。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.Linuxmake --file Make.AIX

引用其它makefile

在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是():

1
include filename ...

filename可以是shell的格式(可以包含路径和通配符),可以有多个filename(用空格隔开)。

一个例子:

1
include foo.make *.mk $(bar)
make命令开始时,会找寻 include 所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找: 1. 如果make执行时,有 -I--include-dir 参数,那么make就会在这个参数所指定的目录下去寻找。

  1. 如果目录 <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也是类似)

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

参考

书写规则

文件搜寻

Makefile文件中的特殊变量 VPATH。当指明了该变量,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。

1
VPATH = src:../headers
上面的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“:”分隔。(当然,当前目录永远是最高优先搜索的地方) 另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:

  • vpath <pattern> <directories> 为符合模式<pattern>的文件指定搜索目录<directories>

  • vpath <pattern> 清除符合模式<pattern>的文件的搜索目录。

  • vpath 清除所有已被设置好了的文件搜索目录。

    例如

    1
    vpath = %.h ../headers

    该语句表示,要求make在''../headers'目录下搜索所有以 .h 结尾的文件。(如果某文件在当前目录没有找到的话)

我们可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的 ,或是被重复了的,那么,make会按照vpath语句的先后顺序来执行搜索。

伪目标

“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。

1
.PHONY : clean
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性:
1
2
3
4
5
6
7
8
9
10
11
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

目标也可以成为依赖。所以,伪目标同样也可成为依赖。看下面的例子:

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
“make cleanall”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。

多目标

Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。减少冗余代码。多个目标的生成规则的执行命令不是同一个,这可能会给我们带来麻烦,不过好在我们可以使用一个自动化变量 \(@ (关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合,这样说可能很抽象,还是看一个例子吧。

1
2
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
等价于
1
2
3
4
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中, `-\)(subst output,,\(@)` 中的 `\)表示执行一个Makefile的函数,函数名为subst,后面的为参数。关于函数,将在后面讲述。这里的这个函数是替换字符串的意思,\(@` 表示目标的集合,就像一个数组, `\)@` 依次取出目标,并执于命令。

静态模式

静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:

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 $@
上面的例子中,指明了我们的目标从$(objects)中获取, %.o 表明要所有以 .o 结尾的目标,也就是 foo.o bar.o ,也就是变量 $(objects) 集合的模式,而依赖模式 %.c 则取模式 %.o% ,也就是 foo bar ,并为其加下 .c 的后缀,于是,我们的依赖目标就是 foo.c bar.c 。而命令中的 $<$@ 则是自动化变量, $< 表示第一个依赖文件, $@ 表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:
1
2
3
4
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
试想,如果我们的 %.o 有几百个,那么我们只要用这种很简单的“静态模式规则”就可以写完一堆规则,实在是太有效率了。“静态模式规则”的用法很灵活,如果用得好,那会是一个很强大的功能。再看一个例子:

1
2
3
4
5
6
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函数,过滤“$(files)”集,只要其中模式为“%.o”的内容。其它的内容,我就不用多说了吧。这个例子展示了Makefile中更大的弹性。

自动生成依赖

源文件可能包含头文件,会产生一个依赖关系。例如如果main.c包含头文件defs.h。那么就有一个依赖关系:

1
main.o : main.c defs.h
由于头文件不编译生成文件,很容易会缺了这个依赖,这样一旦对这个头文件发生修改,make会察觉不到,导致修改不生效。 但是项目一大,这个依赖关系就会变得非常多而且复杂。每次添加或删除一个头文件引用,就要修改大量依赖关系。因此,可以使用在编译器使用-M参数,会打印出依赖关系,例如
1
cc -M main.c
会输出
1
main.o : main.c defs.h
注意,如果你使用GNU的C/C++编译器,你得用 -MM 参数,不然, -M 参数会把一些标准库的头文件也包含进来。例如会输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
main.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
如何在makefile使用这个生成的依赖?不太可能是根据生成的依赖再生成一份makefile(每次修改都要重新生成一次),这个功能并不现实,不过我们可以有其它手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个 name.c 的文件都生成一个 name.d 的Makefile文件, .d 文件中就存放对应 .c 文件的依赖关系。 于是,我们可以写出 .c 文件和 .d 文件的依赖关系,并让make自动更新或生成 .d 文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。 这里,我们给出了一个模式规则来产生 .d 文件:

1
2
3
4
5
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$

这个规则的意思是,所有的 .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
2
exec:
cd /home/ubuntu ; pwd

将打印/home/ubuntu, 而

1
2
3
exec:
cd /home/ubuntu
pwd
将打印当前工作目录,并未发送改变

命令出错

当命令运行完成后,make会检查每条命令是否出错(返回值非0),如果出错,会终止执行当前规则。这可能导致终止所有规则。

对于不打算检查出错命令,例如mkdir(文件夹存在时会放回非0值,但确保文件夹存在就是我们期望的)或rm(如果删除文件不存在,会放回非0值,确保文件不存在就是我们期望的),在命令前面添加-(在Tab后面)。

1
2
clean:
-rm -f *.o

make使用参数-i--ignore-errors可以忽略所有错误; 使用参数-k--keep-going可以终止出错的规则,但继续执行其它规则。 而如果一个规则是以 .IGNORE 作为目标的,那么这个规则中的所有命令将会忽略错误。例如

1
.IGNORE : clean

嵌套执行make

当项目很大时,不同模块文件夹下可能有各自的makefile,这时需要一个总控makefile来调用分散在其它模块中的makefile。 make提供了一些参数,有助于在makefile中嵌套使用make。例如进入某一目录执行该目录下的makefile:

1
2
subsystem:
cd subdir && $(MAKE)
等价于
1
2
subsystem:
$(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
3
export 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
2
subsystem:
cd subdir && $(MAKE) MAKEFLAGS=
使用参数-w--print-directory会在make过程中输出工作目录的变化。当使用 -C 参数指定make下层makefile时,-w会被自动打开。如果参数中含有-s(--slient--no-print-directory),那么-w会失效。

定义命令包

除了定义值的变量,还可以为命令序列定义一个变量,便于复用。命令序列的语法以 define 开始,以 endef 结束,如(注意可以不用Tap开头):

1
2
3
4
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
可以像使用变量一样使用这个命令包
1
2
foo.c : foo.y
$(run-yacc)

使用变量

变量基础

变量使用时要在变量名前面加上$符号,最好用(){}把变量包起来(给变量加上括号完全是为了更加安全地使用这个变量)。如果要使用真实的 $ 字符,那么你需要用 $$ 来表示。 变量会在使用它的地方精确地展开,就像C/C++中的宏一样。

变量中的变量

变量可以被一个变量定义,一种定义方法如下:

1
2
foo = $(hi)
hi = 123
右侧的变量不一定是要有定义的,这意味着可以把变量的真实值推后定义。 但是,这也可能导致递归定义,造成无限展开(make还是有能力检测这样的定义的,会报错)。例如:

1
CFLAGS = $(CFLAGS) -O

如果在变量中使用函数(例如$(shell pwd)),可能会导致未知错误,你不知道这个函数会被调用多少次。

为了避免这种情况,可以使用:=定义,这样一个变量只能使用前面定义好的变量,不能使用后面的变量。

1
2
3
x := foo
y := $(x) bar // 将等价于 y := foo bar
x := later

1
2
y := $(x) bar // 将等价于 y := bar
x := later

定义变量时有一些需要注意的事情。如果需要定义一个空格,可以借助注释符#实现

1
2
nullstr :=
space := $(nullstr) # end of the line
直接为变量定义一个空格是困难的,这里可以先使用一个空变量确定开头,然后添加一个空格,最后使用一个注释符#结尾标识结尾。这样就有了一个空格的变量。 注释符#这个特性要特别地注意,如果不小心误用,可以会出现意想不到的结果。例如

1
dir = /foo/bar    # dir

此时dir的值会是/foo/bar(后跟4个空格),如果后面用了$(dir)/file,就完蛋了。

还有一个有用的操作符?=。如果它左侧的变量没被定义过,那就将该变量赋为右侧的值;否则什么也不干。

变量高级用法

  1. 变量值的替换

格式:$(var:a=b)${var:a=b}。将变量var中以a串结尾替换为b串,’结尾‘代表被空格或结束符隔开。例子:

1
2
foo := a.o b.o c.o
bar := $(foo .o=.c)

bar的值为"a.c b.c c.c"。

  1. “把变量的值再当成变量”

例子:

1
2
3
x = y
y = z
a := $($(x))

a的值为"z"。

使用这种方式,还可以使用多个变量组成一个变量的名字:

1
2
3
4
first_second = Hello
a = first
b = second
all = $($a_$b)

all的值为"Hello"。

追加变量值

使用+=追加。

如果变量之前没有定义过,那么, += 会自动变成 = ,如果前面有变量定义,那么 += 会继承于前次操作的赋值符。如果前一次的是 := ,那么 += 会以 := 作为其赋值符,如:

1
2
variable := value
variable += more

等价于

1
2
variable := value
variable := $(variable) more

多行变量

define指示符可以定义含有多行内容的变量。前面讲过的“命令包”的技术就是利用这个关键字。

define指示符后面跟着变量名,从第二行开始是变量内容,以endef结尾。

1
2
3
4
define two-lines
echo foo
echo $(bar)
endef

override指示符

如果有变量是通过命令行指定的,那么在makefile中对该变量的赋值就会被忽略。如果不希望被忽略,可以使用在赋值语句前使用override指示符赋值。

格式如下:

1
2
3
override <variable> = <value>
override <variable> := <value>
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
2
3
<target ...> : <variable-assignment>

<target ...> : overide <variable-assignment>

例如:

1
2
3
4
5
6
7
8
9
10
11
12
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目标以及其引发的所有规则中,$(CFLAGS)都是-g

模式变量

还可以对模式变量设置目标变量,格式如下:

1
2
3
<pattern ...>; : <variable-assignment>;

<pattern ...>; : override <variable-assignment>;

例如:

1
%.o : CFLAGS = -O

这样所有.o结尾的目标的CFLAGS都是-O了。

条件判断

ifeqifneq

示例

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

ifeq的语法如下:

1
2
3
4
5
ifeq (<arg1>, <arg2>)
ifeq '<arg1>' '<arg2>'
ifeq "<arg1>" "<arg2>"
ifeq "<arg1>" '<arg2>'
ifeq '<arg1>' "<arg2>"

ifneq类似

ifdefifndef

ifdef可以查看变量是否被定义,语法如下:

1
ifdef <variable-name>

注意ifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置。例如

1
2
3
4
5
6
7
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
1
2
3
4
5
6
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif

第一个例子中, $(frobozz) 值是 yes ,第二个则是 no

还有ifndef,就是ifdef的反义词。

特别注意的是,make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把自动化变量(如 $@ 等)放入条件表达式中,因为自动化变量是在运行时才有的。

使用函数

函数的调用语法

和变量非常类似,语法如下:

1
2
3
$(<function> <arguments>)
# 或
${<function> <arguments>}

函数名和参数间用空格隔开,参数之间用逗号隔开。例子:

1
2
3
4
5
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

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
2
3
4
sources = foo.c bar.c
ifneq ( $(MAKECMDGOALS),clean)
include $(sources:.c=.d)
endif

上面指令功能是只要输入的命令不是"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代码,以便快速上手:

施工中

作者

limil

发布于

2024-09-25

更新于

2025-04-05

许可协议