3.0 书写makefile
# 3.1 Makefile 里都有什么
Makefile 中有五种东西:
- explicit rule
- implicit rule
- variable definition
- directives (指令)
- commments
显式规则。显式规则说明了如何生成一个或多个目标文件。这是由 Makefile 的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
隐式规则。由于我们的 make 有自动推导的功能,所以隐式规则可以让我们比较简略地书写 Makefile,这是由 make 所支持的。
(具体看 chapter4)变量的定义。在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像你 C 语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
(具体看 chapter10)指令。其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 include 一样 (见 3.3 节);另一个是指根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样 (见 Chapter 7);还有就是定义一个多行的命令 (见 6.8 节)。有关这一部分的内容,我会在后续的部分中讲述。
注释。Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用 # 字符,这个就像 C/C++ 中的 // 一样。如果你要在你的 Makefile 中使用 # 字符,可以用反斜杠进行转义,如: # 。
- 注释可以是多行的,只要在行的结尾用 blackslash 即可
# This is a comment \ that continues onto the next line \ and continues onto yet another line1
2
3- 不可以在变量引用时使用注释,变量 FOO 将被设置为 "This is a # comment"
FOO = This is a # comment1- 不可以在函数调用时使用注释,函数 some_function 将被调用,参数为 "This is a # comment",而不是 "This is a"
$(call some_function, This is a # comment)1- 在 recipe 中的注释将会跟随 recipe 的 command 部分一起传给 shell,然后是否将
#解释为注释取决于 shell - 在 define 指令中,注释不会被忽略,而是保留在变量的值中,举例如下:
define my_variable This is a # comment This is another line endef $(info $(my_variable))1
2
3
4
5
6在上面的例子中,我们使用 define 指令定义一个名为 my_variable 的变量。该变量包含两行,第一行包含一个注释,第二行是普通文本。当我们使用 $(info) 函数扩展 my_variable 变量时,输出将如下所示:
This is a # comment This is another line1
2
# 3.1.1 将长行划分为多行
Makefile 使用了”line-based“ syntax,每行都是一个语句, GNU make 并没有限制一行的长度。所以如果一行语句很长的话,为了增加可读性,我们可以使用 backslash ( \ ) 来将一行语句分割为多行。
之后的文章中都会使用两种名字来辨别不同的行类型:
physical lines:
无论在行尾前有没有 blackslash,只要以\n结尾的行,我么都叫它是一个 physical line。logical lines: 一个完整的 statement (包括所有的 blaskslash 分割的多行) 构成了一个 logical line。
值得注意的是,blackslash+newline 的结合如何处理取决于这条语句是否是 recipe。如果这条 statement 是 recipe,将在 5.1.1 节进行讨论。
如果这条 statement 不是 recipe,那么 blackslash+newline 和他们前后的所有 space 一起被转化为一个 space。
# 分割行但是不添加 Whitespace
如果需要分割一行但是 make 在解释的时候不添加任何空格在他们之间,可以使用一个 trick (很巧妙)😁:
将 blackslash+newline 替换为 dollar sign+blackslash_newline.
var := one$\
word
2
通过上面说的规则,make 会把 blackslash+newline 和他们前后的所有 space 一起被转化为一个 space 得到:
var := one$ word
然后 $ 根据变量扩展的规则进行解释,因为没有定义过变量 ,所以会被解释为空字符串 "" ,然后就会等价于:
var := oneword
# 3.2 如何给 Makefile 命名
默认的情况下,make 命令会在当前目录下按顺序寻找文件名为 GNUmakefile 、 makefile 和 Makefile 的文件。在这三个文件名中,最好使用 Makefile 这个文件名,因为这个文件名在排序上靠近其它比较重要的文件,比如 README。
当然,你可以使用别的文件名来书写 Makefile,比如:“Make.Solaris”,“Make.Linux” 等,如果要指定特定的 Makefile,你可以使用 make 的 -f 或 --file 参数,如: make -f Make.Solaris 或 make --file Make.Linux 。如果你使用多条 -f 或 --file 参数,你可以指定多个 makefile。
# 3.3 如何包含其他的 makefile
在 Makefile 使用 include 指令可以把别的 Makefile 包含进来,这很像 C 语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:
(注意阅读不是执行,先阅读完所有的 makefile 进行汇总后,才会进行执行,具体看 3.5 小节)
include filenames...
<filenames>可以是当前操作系统 Shell 的文件模式 (可以包含路径和通配符)。如果 filenames 为空,不会包含任何东西,也不会报告错误。- 不能以 tab 开始这一行,因为这样会把这一行解释为是一个
recipe。 - 可以在 include 结束的地方加注释
#... - 如果 filenames 中包含变量或者函数引用,它们都会进行扩展。
假设现在有 a.mk, b.mk 和 c.mk 三个文件,以及 $(bar) 会被扩展为 bish 和 bash,那么下面的表达式:
include foo *.mk $(bar)
会扩展为:
include foo a.mk b.mk c.mk bish bash
make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。就好像 C/C++ 的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make 会在当前目录下首先寻找,如果当前目录下没有找到,那么,make 还会在下面的几个目录下找
1. 如果 make 执行时,有 -I 或 --include-dir 参数,那么 make 就会在这个参数所指定的目录下去寻找。
2. 接下来按顺序寻找目录 <prefix>/include (一般是 /usr/local/bin 、 /usr/gnu/include 、 /usr/local/include 、 /usr/include )。
如果有文件没有找到的话,make 会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成 makefile 的读取,make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。如果你想让 make 不理那些无法读取的文件,而继续执行,你可以在 include 前加一个减号 “-”。如:
-include <filenames>...
其表示,无论 include 过程中出现什么错误,都不要报错继续执行。
include 命令的使用场景:
- 当多个 Makefile 共享同一组变量定义和规则时,可以使用 include 命令,即减少了可能产生的错误,也提高了可维护性,举例如下:
假设 A 和 B 两个程序都要使用相同的一组变量,比如说 CC(编译器),CFLAGS(编译选项)和 LDFLAGS(链接选项)等。在这种情况下,你可以创建一个名为 common.mk 的文件,将这些变量定义在其中,如下所示:
CC = gcc
CFLAGS = -Wall -O2
LDFLAGS = -lm
2
3
然后,在程序 A 和 B 的各自的 makefile 中,你可以使用包含指令来引用 common.mk 文件中定义的这些变量,如下所示:
# makefile for program A
include ../common.mk
program_A: program_A.o
$(CC) $(CFLAGS) $(LDFLAGS) -o program_A program_A.o
program_A.o: program_A.c
$(CC) $(CFLAGS) -c program_A.c
clean:
rm -f program_A program_A.o
2
3
4
5
6
7
8
9
10
11
12
# makefile for program B
include ../common.mk
program_B: program_B.o
$(CC) $(CFLAGS) $(LDFLAGS) -o program_B program_B.o
program_B.o: program_B.c
$(CC) $(CFLAGS) -c program_B.c
clean:
rm -f program_B program_B.o
2
3
4
5
6
7
8
9
10
11
12
有时我们需要通过 GCC 对源文件自动生成依赖关系,而这些依赖关系可以被放在一个被主 Makefile 包含的文件中。这种做法通常比其他版本的 Makefile 在末尾追加依赖关系的做法更为简洁并且有更好的可维护性。
先来解释一下什么是自动生成依赖关系:
假设有一个 main.c 文件,里面引用了一个头文件 (defs),那么我通过运行:
cc -M main.c
就可以自动得到依赖关系,通过这种方式,如果哪天 main.c 多引用了一个新的头文件,也能通过 gcc -M 自动生成新的依赖关系。
-M : 生成 user header files 和 system header files 的依赖到终端.
-MD : 生成 user header files 和 system header files 的依赖到文件.
-MM : 生成 user header files 的依赖到终端.
-MMD : 生成 user header files 的依赖到终端到文件中.
示例,假设我们有一个源文件 foo.c 和一个 Makefile,我们希望自动生成 foo.c 的依赖关系并将其保存在一个名为 deps.mk 的文件中:
# Makefile
# Include the dependency file
-include deps.mk
# Generate the dependency file
# $< is foo.c, $@ is deps.mk
deps.mk: foo.c
gcc -M $< > $@
2
3
4
5
6
7
8
9
在上面的示例中,我们使用 include 指令将 deps.mk 文件包含在 Makefile 中。如果这个文件存在,它会被读取并包含其中的依赖关系。我们还使用了一个规则来生成 deps.mk 文件。这个规则使用 gcc 的 - M 选项来自动生成 foo.c 的依赖关系,并将其保存到 deps.mk 文件中。注意,这个规则使用了一个特殊的目标 deps.mk,即 foo.c 的依赖关系文件。
# 3.4 环境变量 MAKEFILES
如果你的当前环境中定义了环境变量 MAKEFILES ,那么 make 会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的 Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的 Makefile 的 “目标” 不会起作用,如果环境变量中定义的文件发现错误,make 也会不理。
# 3.5 make 的工作方式:
读入所有的 Makefile。
读入被 include 的其它 Makefile。
初始化文件中的变量。
推导隐式规则,并分析所有规则。
为所有的目标文件创建依赖关系链。
根据依赖关系,决定哪些目标要重新生成。
执行生成命令。
1-5 步为第一个阶段,6-7 为第二个阶段。第一个阶段中,需要使用到的变量会被展开。第二个阶段中,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。
总结来说: Makefile 的变量展开使用的是按需展开。