Makefile 简介

C/C++ 在 linux 下可通过 gcc 进行编译,当文件数量少,文件依赖关系简单时可通过命令进行编译,但是当文件数量庞大且关系复杂时,就要依赖于 make 和 Makefile 管理这些复杂关系了。MakeFile 类似于 shell 脚本,定义了文件的依赖关系,以及编译的先后顺序。本文主要介绍 Makefile 的基本语法,本系列文章主要参考了 跟我一起写 Makefile

基本格式

Makefile 书写格式一般如下

1
2
3
4
5
6
# 方式一
target : prerequisites
command

# 方式二
target : prerequisites; command
  • target 可以是一个 object file (目标文件),也可以是一个执行文件,还可以是一个标签(label)
  • prerequisites 是要生成那个 target 所需要的文件或是目标。
  • command 也就是 make 需要执行的命令,如果其不与 targets : prerequisites 在一行,那么,必须以 Tab键开头,如果和 prerequisites 在一行,那么可以用分号做为分隔。

这是一个文件的依赖关系,也就是说,target 这一个或多个的目标文件依赖于 prerequisites 中的文件,其生成规则定义在 command 中。说白一点就是说,prerequisites 中如果有一个以上的文件比 target 文件要新的话,command 所定义的命令就会被执行。这就是 Makefile 的规则。也就是 Makefile 中最核心的内容。

示例

如果一个工程有 3 个头文件,和 8 个 c 文件,我们为了完成前面所述的那三个规则,我们的 Makefile 应该是下面这个样子的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 如果后面这些.o文件比edit可执行文件新,那么才会去执行命令
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

我们可以把这个内容保存在名字为 “makefile” 或 “Makefile” 的文件中,然后在该目录下直接输入命令 “make” 就可以生成执行文件 edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下 make clean 就可以了。

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 tab 键作为开头。记住,make 并不管命令是怎么工作的,他只管执行所定义的命令。make 会比较 targets 文件和 prerequisites 文件的修改日期,如果 prerequisites 文件的日期要比 targets 文件的日期要新,或者 target 不存在的话,那么,make 就会执行后续定义的命令

输入 make 命令后,发生了如下的动作

  1. make 会在当前目录下找名字叫 “Makefile” 或 “makefile” 的文件。如果找到,它会找文件中的第一个目标文件(target)作为最终的目标文件,在上面的例子中即为 edit 这个文件
  2. 如果 edit 文件不存在,或是 edit 所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
  3. 如果 edit 所依赖的 .o 文件也不存在,那么 make 会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(这有点像一个堆栈的过程)

这就是整个 make 的依赖性,make 会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件 , 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么 make 就会直接退出并报错。

而像 clean 这种没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,但可以通过 make clean 进行显式执行,以此来清除所有的目标文件,以便重编译。

如果这个工程已被编译过,当我们修改了其中一个源文件时,比如 file.c,那么根据我们的依赖性,我们的目标 file.o 会被重编译,因此 file.o 文件修改时间要比 edit 要新,所以 edit 也会被重新链接了

使用变量与自动推导

上面同一个 .o 文件在多个地方都重复了,因此可通过变量的方式简化 Makefile,Makefile 的变量也就是一个字符串,类似于 C 语言的宏,通过 $(变量名) 来访问变量内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit $(objects)

make 能够自动推导文件以及文件依赖关系后面的命令,即只要 make 看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果 make 找到一个 whatever.o,那么 whatever.c,就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的 Makefile 再也不用写得这么复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc = gcc

edit : $(objects)
cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
-rm edit $(objects)

.PHONY 意思表示 clean 是一个伪目标。而在 rm 命令前面加了一个小减号的意思是当某些文件出现问题时不要管,继续做后面的事。当然,clean 的规则不要放在文件的开头,这就会变成 make 的默认目标。不成文的规矩是 clean 从来都是放在文件的最后

引用其他 MakeFile

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

1
include <filename>;

filename 可以是当前操作系统 Shell 的文件模式(可以包含路径和通配符)

比如有这样几个 Makefile:a.mk、b.mk、c.mk,还有一个文件叫 foo.make,以及一个变量 $(bar),其包含了 e.mk 和 f.mk,那么,下面两个语句等价:

1
2
3
include foo.make *.mk $(bar)

include foo.make a.mk b.mk c.mk e.mk f.mk

make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。类似 C/C++ 的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make 会在当前目录下首先寻找,如果当前目录下没有找到,那么,make 还会在下面的几个目录下找

  • 如果 make 执行时,有 -I--include-dir 参数,那么 make 就会在这个参数所指定的目录下去寻找。
  • 如果目录 <prefix>/include(一般是:/usr/local/bin 或 /usr/include)存在的话,make 也会去找。 如果有文件没有找到的话,make 会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成 makefile 的读取, make 会再重试这些没有找到,或是不能读取的文件,如果还是不行,make 才会出现一条致命信息。

如果想让 make 忽略那些无法读取的文件,可以在 include 前加一个减号 -, 即

-include <filename>;

表示无论 include 过程中出现什么错误,都不要报错继续执行。和其它版本 make 兼容的相关命令是 sinclude,其作用和这一个是一样的。

小结

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++ 中的 // 一样。