一文看完Makefile
1 Makefile 是什么?
回顾一下我们前面几个章节,我们使用到的示例代码只有一个 hello.c 文件,非常简单,所以直接执行下面的指令进行编译也非常方便。
编译一个文件
gcc hello.c -o hello
但是随着我们课程逐渐深入,以后在我们的项目工程中,势必会有越来越多的 C 文件和 H 头文件。当一个工程中有很多 C 源文件和 H 头文件时,再直接使用编译器指令就非常麻烦了,光输入文件名都会影响心情,比如下面的例子:
编译多个文件
gcc hello.c aaa.c bbb.c -o hello
而且哪怕你只是修改一个文件,也需要重新编译所有的文件,白白浪费了很多开发时间。要解决这个问题,最好的方式就是把工程的编译规则写下来,让编译器自动加载该规则进行编译。解决方法就是使用 make 和 Makefile,这两个工具是搭配使用的,下面给大家介绍一下:
• make 工具:它可以帮助我们找出项目里面修改变更过的文件,并根据依赖关系,找出受修改影响的其他相关文件,然后对这些文件按照规则进行单独的编译,这样一来,就能避免重新编译项目的所有的文件。
• Makefile 文件:上面提到的规则、依赖关系主要是定义在这个 Makefile 文件中的,我们在其中合理地定义好文件的依赖关系之后, make 工具就能精准地进行编译工作。
它们的关系如下图所示:
从我们上面的介绍,大家可以知道,我们管理一个项目工程,实质上就是管理项目文件间的依赖关系。所以我们在学习和使用 Makefile 的时候,一定要牢牢抓住它这种面向依赖的思想,心里一定要谨记, Makefile 中所有的复杂、晦涩的语法都是更好地为解决依赖问题而存在的。理解了它的本质目的之后,我们以后在学习它的过程中就不用死记硬背各种语法了, 只要想想这个本质目的,你会觉得一切都是那么地顺理成章。
是否真正驾驭 Makefile 的标志,就在于脑海中是否清晰地知道目标和依赖的关系。当你的大脑能够像 make 工具一样,准确无误地解释执行 Makefile 的时候,就是一个 Makefile 高手了。我们就是要奔着这个目标去的。
这里再多介绍一下,当工程复杂度再上一个台阶的时候,会觉得手写 Makefile 也很麻烦,那个时候可以用 CMake、 autotools 等工具来帮忙生成 Makefile。实际上 Windows 系统下很多 IDE 工具内部也是使用类似 Makefile 的方式组织工程文件的,只不过被封装成图形界面,对用户不可见而已。
2 Makefile 概览
Makefile 对于一个嵌入式 Linux 开发人员来说,怎么强调它的重要性都不为过。虽然它跟程序员的编程能力并不直接挂钩,但是它所体现出来的是一种工程能力, 这种工程能力恰恰就是专业程序员和业余程序员的一个分水岭。因为在 Linux 开发环境下,我们并没有太多的集成开发环境IDE,也就是说程序的所有控制权全部在于开发者,你势必要对底层软件的编译、链接、装载有着相当深入的了解,才能说真正地驾驭了你的程序。虽然前面的章节已经简单介绍了部分底层相关内容,但是还不够深入,我们以后的课程还要更加深入。
而且我们以后的课程也会陆续讲解 Uboot 移植开发、 Linux 内核移植开发和其他的一些开源项目,倘若我们 Makefile 基础不够扎实,那么很可能怎么着手分析项目都不知道。程序的架构组织都梳理不清楚,就更别谈修改程序功能了。如下图:
接下来我们先整体了解 Makefile 的相关语法,这里要提前跟大家说清楚一点, Makefile 经过多年发展,虽然功能非常强大,但是也留下了沉重的历史包袱,我们没有办法详细介绍 make 的每一个详细知识点 (也没有必要),借鉴于以往的开发经验,我们总结了 80% 常用的 Makefile 知识点,定位在为以后研究 Uboot、 Linux kernel 和其他开源项目打下结实的基础。虽然这个目标难度还是比较大的,要学习的知识点也不少,不过我们会通过一系列难度逐步迭代的小实验,来帮助大家无痛学会 Makefile。
我一直提倡:学习知识之前,要现在脑海中初步建立知识点的整体框架,以此来指导进一步的学习。
接下来我们先整体看一下要学习 Makefile 的知识点,如下图所示:
图里面的知识点不少,我们在这里先不深入学习具体语法,而是告诉大家每一个语法的出现是为了解决什么问题,我们从左上角开始讲起:
1、基础语法–描述目标和依赖的特定格式, Makefile 的核心。
2、变量–记录特定的信息,避免重复输入原始信息。尤其是手动输入原始信息很长时,特别好用。
3、分支判断–灵活控制多个不同的编译过程,方便兼容不同属性。
4、头文件依赖–监控头文件的变化,头文件也是程序的关键内容。
5、隐含规则–利用 Makefile 的一些默认规则,可以减少编写 Makefile 的工作量。
6、自动化变量–利用 Makefile 的默认的自动化变量,可以减少编写 Makefile 的工作量。
7、模式规则–灵活使用正则表达式,可以减少编写 Makefile 的工作量。
8、函数–使用 Makefile 的各种函数,可以更方便地实现 Makefile 的功能。
了解完 Makefile 的知识点,从上面的分析可以知道, Makefile 的核心在于基础语法,用来描述目标和依赖的关系。其他语法的目的,是为了减少我们编写 Makefile 工作量,让我们能够以更加优雅、更加简洁、更好维护的方式来实现 Makefile 的功能。这跟我们程序开发是很相似的,不止要实现功能,还要兼顾程序的可读性、拓展性、可维护性等等.
使用 Makefile 控制编译:
关于 Makefile 的详细使用可参考《跟我一起写 Makefile》一书或 GNU 官方的 make 说明文档:https://www.gnu.org/software/make/manual,本章仅以示例对 Makefile 的基础语法进行讲解.
3.Makefile 小实验:
第一个见面实验
为了直观地演示 Makefile 的作用,我们使用一个示例进行讲解,首先使用编辑器创建一个名为“Makefile”的文件,输入如下代码并保存,其中使用“#”开头的行是注释,自己做实验时可以不输入,另外要注意在“ls -lh”、” touch test.txt”等命令前要使用 Tab 键,不能使用空格代替。
#Makefile 格式
#目标: 依赖的文件或其它目标
#Tab 命令 1
#Tab 命令 2
#第一个目标,是最终目标及 make 的默认目标
#目标 a,依赖于目标 targetc 和 targetb
#目标要执行的 shell 命令 ls -lh,列出目录下的内容
targeta: targetc targetb
ls -lh
#目标 b,无依赖
#目标要执行的 shell 命令,使用 touch 创建 test.txt 文件
targetb:
touch test.txt
#目标 c,无依赖
#目标要执行的 shell 命令, pwd 显示当前路径
targetc:
pwd
#目标 d,无依赖
#由于 abc 目标都不依赖于目标 d,所以直接 make 时目标 d 不会被执行
#可以使用 make targetd 命令执行
targetd:
rm -f test.txt
这个 Makefile 文件主要是定义了四个目标操作,先大致了解它们的关系:
• targeta:这是 Makefile 中的第一个目标代号,在符号“:”后面的内容表示它依赖于 targetc 和targetb 目标,它自身的命令为“ls -lh”,列出当前目录下的内容。
• targetb:这个目标没有依赖其它内容,它要执行的命令为“touch test.txt”,即创建一个 test.txt文件。
• targetc:这个目标同样也没有依赖其它内容,它要执行的命令为“pwd”,就是简单地显示当前的路径。
• targetd:这个目标无依赖其它内容,它要执行的命令为“rm -f test.txt”,删除目录下的 test.txt文件。与 targetb、 c 不同的是,没有任何其它目标依赖于 targetd,而且它不是默认目标。
下面使用这个 Makefile 执行各种 make 命令,对比不同 make 命令的输出,可以清楚地了解 Makefile的机制。在主机 Makefile 所在的目录执行如下命令:
在主机上 Makefile 所在的目录执行如下命令
#查看当前目录的内容
ls
#执行 make 命令, make 会在当前目录下搜索“Makefile”或“makefile”,并执行
make
#可看到 make 命令后的输出,它执行了 Makefile 中编写的命令
#查看执行 make 命令后的目录内容,多了 test.txt 文件
ls
#执行 Makefile 的 targetd 目标,并查看,少了 test.txt 文件
make targetd
ls
#执行 Makefile 的 targetb 目标,并查看,又生成了 test.txt 文件
make targetb
ls
#执行 Makefile 的 targetc 目标
make target
上图中包含的原理说明如下:
make 命令:
• 在终端上执行 make 命令时, make 会在当前目录下搜索名为“Makefile”或“makefile”的文件,然后根据该文件的规则解析执行。如果要指定其它文件作为输入规则,可以通过“-f”
参数指定输入文件,如“make -f 文件名”。
• 此处 make 命令读取我们的 Makefile 文件后,发现 targeta 是 Makefile 的第一个目标,它会被当成默认目标执行。
• 又由于 targeta 依赖于 targetc 和 targetb 目标,所以在执行 targeta 自身的命令之前,会先去完成 targetc 和 targetb。
• targetc 的命令为 pwd,显示了当前的路径。
• targetb 的命令为 touch test.txt ,创建了 test.txt 文件。
• 最后执行 targeta 自身的命令 ls -lh ,列出当前目录的内容,可看到多了一个 test.txt 文件。
make targetd 、 make targetb、 make targetc 命令:
• 由于 targetd 不是默认目标,且不被其它任何目标依赖,所以直接 make 的时候 targetd 并没有被执行,想要单独执行 Makefile 中的某个目标,可以使用” make 目标名“的语法,例如
上图中分别执行了” make targetd“、” make targetb“和” make targetc“指令,在执行” make
targetd”目标时,可看到它的命令 rm -f test.txt 被执行, test.txt 文件被删除。
从这个过程,可了解到 make 程序会根据 Makefile 中描述的目标与依赖关系,执行达成目标需要
的 shell 命令。简单来说, Makefile 就是用来指导 make 程序如何干某些事情的清单
第二个实验:使用 Makefile 编译程序:
使用 GCC 编译多个文件:
接着我们使用 Makefile 来控制程序的编译,为方便说明,先把前面章节的 hello.c 程序分开成三个文件来写,分别为 hello_main.c 主文件, hello_func.c 函数文件, hello_func.h 头文件,其内容如下代码所示,
相对于基础的hello.c编译命令,此处主要是增加了输入的文件数量, 如“hello_main.c” 、 “hello_func.c” ,另外新增的“-I .”是告诉编译器头文件路径,让它在编译时可以在“.”(当前目录)寻找头文件,其实不加” -I .”选项也是能正常编译通过的,此处只是为了后面演示 Makefile 的相关变量。
使用 Makefile 编译:
可以想象到,只要把 gcc 的编译命令按格式写入到 Makefile,就能直接使用 make 编译,而不需要每次手动直接敲 gcc 编译命令。
操作如下使用编辑器在 hello_main.c 所在的目录新建一个名为“Makefile”的文件,并输入如下内容并保存。
#Makefile 格式
#目标: 依赖
#Tab 命令 1
#Tab 命令 2
#默认目标
#hello_main 依赖于 hello_main.c 和 hello_func.c 文件
hello_main: hello_main.c hello_func.c
gcc -o hello_main hello_main.c hello_func.c -I .
#clean 目标,用来删除编译生成的文件
clean:
rm -f *.o hello_main
该文件定义了默认目标 hello_main 用于编译程序, clean 目标用于删除编译生成的文件。特别地,其中 hello_main 目标名与 gcc 编译生成的文件名” gcc -o hello_main”设置成一致了,也就是说,此处的目标 hello_main 在 Makefile 看来,已经是一个目标文件 hello_main。
这样的好处是 make 每次执行的时候,会检查 hello_main 文件和依赖文件 hello_main.c、 hello_func.c的修改日期,如果依赖文件的修改日期比 hello_main 文件的日期新,那么 make 会执行目标其下的 Shell 命令更新 hello_main 文件,否则不会执行。
请运行如下命令进行实验:
目标与依赖:
下面我们再总结一下 Makefile 中跟目标相关的语法:
[目标 1]: [依赖]
[命令 1]
[命令 2]
[目标 2]: [依赖]
[命令 1]
[命令 2]
• 目标:指 make 要做的事情,可以是一个简单的代号,也可以是目标文件,需要顶格书写,
前面不能有空格或 Tab。一个 Makefile 可以有多个目标,写在最前面的第一个目标,会被
Make 程序确立为“默认目标”,例如前面的 targeta、 hello_main。
• 依赖:要达成目标需要依赖的某些文件或其它目标。例如前面的 targeta 依赖于 targetb 和targetc,又如在编译的例子中, hello_main 依赖于 hello_main.c、 hello_func.c 源文件,若这些文件更新了会重新进行编译。
• 命令 1,命令 2…命令 n: make 达成目标所需要的命令。只有当目标不存在或依赖文件的修改时间比目标文件还要新时,才会执行命令。要特别注意命令的开头要用“Tab”键,不能使用空格代替,有的编辑器会把 Tab 键自动转换成空格导致出错,若出现这种情况请检查自己的编辑器配置。
第三个实验:伪目标
前面我们在 Makefile 中编写的目标,在 make 看来其实都是目标文件,例如 make 在执行的时候由于在目录找不到 targeta 文件,所以每次 make targeta 的时候,它都会去执行 targeta 的命令,期待执行后能得到名为 targeta 的同名文件。如果目录下真的有 targeta、 targetb、 targetc 的文件,即假如目标文件和依赖文件都存在且是最新的,那么 make targeta 就不会被正常执行了,这会引起误会。为了避免这种情况, Makefile 使用“.PHONY”前缀来区分目标代号和目标文件,并且这种目标代号被称为“伪目标”, phony 单词翻译过来本身就是假的意思。也就是说,只要我们不期待生成目标文件,就应该把它定义成伪目标,前面的演示代码修改如下。
#使用.PHONY 表示 targeta 是个伪目标
.PHONY:targeta
目标 a,依赖于目标 targetc 和 targetb
目标要执行的 shell 命令 ls -lh,列出目录下的内容
targeta: targetc targetb
ls -lh
#使用.PHONY 表示 targetb 是个伪目标
.PHONY:targetb
目标 b,无依赖
目标要执行的 shell 命令,使用 touch 创建 test.txt 文件
targetb:
touch test.txt
使用.PHONY 表示 targetc 是个伪目标
.PHONY:targetc
目标 c,无依赖
目标要执行的 shell 命令, pwd 显示当前路径
targetc:
pwd
#使用.PHONY 表示 targetd 是个伪目标
.PHONY:targetd
#目标 d,无依赖
#由于 abc 目标都不依赖于目标 d,所以直接 make 时目标 d 不会被执行
#可以使用 make targetd 命令执行
targetd:
rm -f test.txt
GNU 组织发布的软件工程代码的 Makefile,常常会有类似以上代码中定义的 clean 伪目标,用于清除编译的输出文件。常见的还有“all”、“install”、“print”、“tar”等分别用于编译所有内容、安装已编译好的程序、列出被修改的文件及打包成 tar 文件。虽然并没有固定的要求伪目标必须用这些名字,但可以参考这些习惯来编写自己的 Makefile。
如果以上代码中不写“.PHONY:clean”语句,并且在目录下创建一个名为 clean 的文件,那么当执行“make clean”时, clean 的命令并不会被执行,感兴趣的可以亲自尝试一下。
默认规则
在前面《GCC 编译过程》章节中提到整个编译过程包含如下图中的步骤, make 在执行时也是使用同样的流程,不过在 Makefile 的实际应用中,通常会把编译和最终的链接过程分开。
也就是说,我们的 hello_main 目标文件本质上并不是依赖 hello_main.c 和 hello_func.c 文件,而是依赖于 hello_main.o 和 hello_func.o,把这两个文件链接起来就能得到我们最终想要的 hello_main目标文件。另外,由于 make 有一条默认规则,当找不到 xxx. o 文件时,会查找目录下的同名xxx.c 文件进行编译。根据这样的规则,我们可把 Makefile 修改如下。
第四个修正实验:
#Makefile 格式
目标文件: 依赖的文件
#Tab 命令 1
#Tab 命令 2
hello_main: hello_main.o hello_func.o
gcc -o hello_main hello_main.o hello_func.o
以下是 make 的默认规则,下面两行可以不写
#hello_main.o: hello_main.c
gcc -c hello_main.c
以下是 make 的默认规则,下面两行可以不写
#hello_func.o: hello_func.c
gcc -c hello_func.c
以上代码的第 5~6 行把依赖文件由 C 文件改成了.o 文件, gcc 编译命令也做了相应的修改。第8~13 行分别是 hello_main.o 文件和 hello_func.o 文件的依赖和编译命令,不过由于 C 编译成同名的.o 文件是 make 的默认规则,所以这部分内容通常不会写上去。
使用修改后的 Makefile 编译结果如下图所示。
从 make 的输出可看到,它先执行了两条额外的“cc”编译命令,这是由 make 默认规则执行的,它们把 C 代码编译生成了同名的.o 文件,然后 make 根据 Makefile 的命令链接这两个文件得到最终目标文件 hello_main。
第五个实验:使用变量
使用 C 自动编译成 *.o 的默认规则有个缺陷,由于没有显式地表示 *.o 依赖于.h 头文件,假如我们修改了头文件的内容,那么 *.o 并不会更新,这是不可接受的。并且默认规则使用固定的“cc”进行编译,假如我们想使用 ARM-GCC 进行交叉编译,那么系统默认的“cc”会导致编译错误。要解决这些问题并且让 Makefile 变得更加通用,需要引入变量和分支进行处理。
基本语法
在 Makefile 中的变量,有点像 C 语言的宏定义,在引用变量的地方使用变量值进行替换。变量的命名可以包含字符、数字、下划线,区分大小写,定义变量的方式有以下四种:
•“=”:延时赋值,该变量只有在调用的时候,才会被赋值
•“:=”:直接赋值,与延时赋值相反,使用直接赋值的话,变量的值定义时就已经确定了。
•“?=”:若变量的值为空,则进行赋值,通常用于设置默认值。
•“+=”:追加赋值,可以往变量后面增加新的内容。
当我们想使用变量时,其语法如下:
1 $(变量名)
下面通过一个实验来讲解这四种定义方式,对于后两种赋值方式比较简单,主要思考延时赋值和
直接赋值的差异,实验代码如下所示。
列表 9: base_linux/makefile/test5/Makefile
VAR_A = FILEA
VAR_B = $(VAR_A)
VAR_C :=
(
V
A
R
A
)
V
A
R
A
+
=
F
I
L
E
B
V
A
R
D
?
=
F
I
L
E
D
.
P
H
O
N
Y
:
c
h
e
c
k
c
h
e
c
k
:
@
e
c
h
o
"
V
A
R
A
:
"
(VAR_A) VAR_A += FILEB VAR_D ?= FILED .PHONY:check check: @echo "VAR_A:"
(VARA)VARA+=FILEBVARD?=FILED.PHONY:checkcheck:@echo"VARA:"(VAR_A)
@echo “VAR_B:”
(
V
A
R
B
)
@
e
c
h
o
"
V
A
R
C
:
"
(VAR_B) @echo "VAR_C:"
(VARB)@echo"VARC:"(VAR_C)
@echo “VAR_D:”$(VAR_D)
这里主要关心 VAR_B 和 VAR_C 的赋值方式,实验结果如下图所示。执行完 make 命令后,只有 VAR_C 是 FILEA。这是因为 VAR_B 采用的延时赋值,只有当调用时,才会进行赋值。当调用 VAR_B 时, VAR_A 的值已经被修改为 FILEA FILEB,因此 VAR_B 的变量值也就等于 FILEA FILEB。
改造默认规则
接下来使用变量对前面 hello_main 的 Makefile 进行大改造,如下所示。
第六个实验,变量修正:
在这里插入图片描述
#定义变量
CC=gcc
CFLAGS=-I.
DEPS = hello_func.h
#目标文件
hello_main: hello_main.o hello_func.o
$(CC) -o hello_main hello_main.o hello_func.o
#*.o 文件的生成规则
%.o: %.c $(DEPS)
$(CC) -c -o $@ $< $(CFLAGS)
伪目标
.PHONY: clean
clean:
rm -f *.o hello_main
代码的 1~4 行:分别定义了 CC、 CFLAGS、 DEPS 变量,变量的值就是等号右侧的内容,定义好的变量可通过” $(变量名)”的形式引用,如后面的” $(CC)”、” $( CFLAGS)”、” $(DEPS)”
等价于定义时赋予的变量值” gcc”、” -I.”和” hello_func.h”。
• 代码的第 8 行:使用 $(CC) 替代了 gcc,这样编写的 Makefile 非常容易更换不同的编译器,如要进行交叉编译,只要把开头的编译器名字修改掉即可。
• 代码的第 11 行:” %”是一个通配符,功能类似” *”,如” %.o”表示所有以” .o”结尾的文件。
所以” %.o:%.c”在本例子中等价于” hello_main.o: hello_main.c”、” hello_func.o: hello_func.c”,
即等价于 o 文件依赖于 c 文件的默认规则。不过这行代码后面的” $(DEPS)”表示它除了依
赖 c 文件,还依赖于变量” $(DEPS)”表示的头文件,所以当头文件修改的话, o 文件也会
被重新编译。
• 代码的第 12 行:这行代码出现了特殊的变量” $@”,” $<”,可理解为 Makefile 文件保留
的关键字,是系统保留的自动化变量,” $@”代表了目标文件,” $<”代表了第一个依赖文
件。即” $@”表示” %.o”,” $<”表示” %.c”,所以,当第 11 行的” %”匹配的字符为”
hello_func”的话,第 1 2 行代码等价于:
#当"%" 匹配的字符为"hello_func" 的话:
$(CC) -c -o $@ $< $(CFLAGS)
#等价于:
gcc -c -o hello_func.o func_func.c -I .
也就是说 makefile 可以利用变量及自动化变量,来重写.o 文件的默认生成规则,以及增加头文件的依赖。
第七个实验:改造链接规则
与 *.o 文件的默认规则类似,我们也可以使用变量来修改生成最终目标文件的链接规则,具体参考如下代码.
这部分说明如下:
• 代码的第 2 行:定义了 TARGET 变量,它的值为目标文件名 hello_main。
• 代码的第 6 行:定义了 OBJS 变量,它的值为依赖的各个 o 文件,如 hello_main.o、 hello_func.o
文件。
• 代码的第 9 行:使用 TARGET 和 OBJS 变量替换原来固定的内容。
• 代码的第 10 行:使用自动化变量“
@
”表示目标文件“
@”表示目标文件“
@”表示目标文件“(TARGET)”,使用自动化变量“
”
表示所有的依赖文件即“
^” 表示所有的依赖文件即“
”表示所有的依赖文件即“(OBJS)”。
也就是说以上代码中的 Makefile 把编译及链接的过程都通过变量表示出来了,非常通用。使用这样的 Makefile 可以针对不同的工程直接修改变量的内容就可以使用。
其它自动化变量
Makefile 中还有其它自动化变量,此处仅列出方便以后使用到的时候进行查阅,见下表。
表自动化变量
使用函数
待续。。