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 命令后,发生了如下的动作
- make 会在当前目录下找名字叫 “Makefile” 或 “makefile” 的文件。如果找到,它会找文件中的第一个目标文件(target)作为最终的目标文件,在上面的例子中即为 edit 这个文件
- 如果 edit 文件不存在,或是 edit 所依赖的后面的
.o
文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。 - 如果 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
23objects = 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
19objects = 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
3include 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 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
- 显式规则:说明了如何生成一个或多个目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
- 隐晦规则: make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由 make 所支持的。
- 变量的定义。在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点 C 语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的
#include
一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译#if
一样; 还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。 - 注释。Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用
#
字符,这个就像 C/C++ 中的//
一样。